Files
Thing/ThingHK/HardwareManager.cs
T
2026-07-28 18:31:39 +08:00

440 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading.Channels;
using LibreHardwareMonitor.Hardware;
namespace ThingHK;
/// <summary>
/// 硬件访问层:封装 LibreHardwareMonitor Computer
/// 负责硬件发现、分层 Update(快/慢通道)、快照构建。
///
/// 设计要点:
/// - Computer 非 IDisposableLHB 0.9.5),用 Close() 显式释放
/// - 传感器按 SensorType 分快/慢通道:
/// * 快通道(temp/load/clock/power/voltage/throughput):高频,1s
/// * 慢通道(data/smalldata/factor/level/control/timing/energy 等):低频,5s
/// 原因:SMART 查询会阻塞,混在一起会拖慢整体;data 类(如内存使用量)变化缓慢
/// - Update 只作用于硬件设备,传感器读取由 visitor 遍历收集(LHB 设计)
/// </summary>
internal sealed class HardwareManager : IDisposable
{
private readonly Computer _computer;
private readonly SnapshotVisitor _visitor = new();
private readonly bool _isAdmin;
private readonly double _coldStartMs;
private readonly Stopwatch _startupSw;
private bool _ready;
private bool _closed;
private HardwareConfig _config;
// 缓存"哪个硬件属于快通道"——按 hardware type 判断更稳定(不同机型传感器命名不一致)
// 实际分频逻辑见 SamplingScheduler,此处只暴露硬件列表给调度器
public IReadOnlyList<IHardware> AllHardware => _visitor.AllHardware;
public bool IsAdmin => _isAdmin;
public double ColdStartMs => _coldStartMs;
public bool Ready => _ready;
public HardwareConfig Config => _config;
public HardwareManager(HardwareConfig config)
{
_config = config ?? HardwareConfig.Default;
_isAdmin = IsRunningAsAdmin();
_startupSw = Stopwatch.StartNew();
// LHB 行为:主板的传感器实际通过 SubHardwareSuperIO/EC)暴露,
// 而 SuperIO 作为顶层硬件时需 IsControllerEnabled 才能枚举。
// 联动策略:启用 motherboard 时自动启用 controller,避免主板勾选后无数据。
bool motherboardEnabled = _config.IsHardwareEnabled("motherboard");
bool controllerEnabled = _config.IsHardwareEnabled("controller") || motherboardEnabled;
_computer = new Computer
{
IsCpuEnabled = _config.IsHardwareEnabled("cpu"),
IsGpuEnabled = _config.IsHardwareEnabled("gpu"),
IsMemoryEnabled = _config.IsHardwareEnabled("memory"),
IsStorageEnabled = _config.IsHardwareEnabled("storage"),
IsMotherboardEnabled = motherboardEnabled,
IsControllerEnabled = controllerEnabled,
IsBatteryEnabled = _config.IsHardwareEnabled("battery"),
IsNetworkEnabled = _config.IsHardwareEnabled("network"),
IsPsuEnabled = _config.IsHardwareEnabled("psu"),
};
_computer.Open();
// 首轮扫描:发现所有硬件(Update 前的 Sensors 通常为空,但 Hardware 列表就绪)
_computer.Accept(_visitor);
// 首轮 Update:填充传感器值
// 注意:部分传感器第二轮才有值,调度器会持续 Update
UpdateAll();
_coldStartMs = _startupSw.Elapsed.TotalMilliseconds;
_startupSw.Stop();
_ready = true;
}
/// <summary>热更新传感器类型过滤(无需重启 Kernel,下次 BuildSnapshot 生效)</summary>
public void UpdateConfig(HardwareConfig newConfig)
{
_config = newConfig;
}
/// <summary>
/// 全量 Update 所有硬件。
/// 由 SamplingScheduler 按通道分频调用。
/// </summary>
public void UpdateAll()
{
foreach (var hw in _visitor.AllHardware)
{
try { hw.Update(); } catch { /* 单个硬件 Update 失败不影响整体 */ }
}
}
/// <summary>
/// 仅 Update 慢通道硬件(Storage/PSU/Battery 等)。
/// 快通道硬件(CPU/GPU/Memory/Network)由调度器更高频调用 UpdateAll。
/// </summary>
public void UpdateSlowOnly()
{
foreach (var hw in _visitor.AllHardware)
{
if (IsSlowHardware(hw.HardwareType))
{
try { hw.Update(); } catch { /* ignore */ }
}
}
}
/// <summary>
/// 构建 SensorSnapshot:遍历当前所有传感器,不触发 Update。
/// 调用者应先 Update 再 Snapshot,避免读到旧值。
/// </summary>
public SensorSnapshot BuildSnapshot()
{
var snap = new SensorSnapshot
{
SchemaVersion = 1,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
IsAdmin = _isAdmin,
Ready = _ready,
};
// 首个快照带上冷启动耗时,后续为 0
if (_coldStartMs > 0 && snap.Timestamp > 0)
{
snap.ColdStartMs = Math.Round(_coldStartMs, 1);
}
// 重新遍历以读取最新传感器值(visitor 缓存的是 hardware 引用,sensor 值实时)
var groups = new Dictionary<string, SensorGroup>();
// 为已启用但 LHB 未枚举到的硬件类型预创建空分组,
// 确保前端能显示"已启用但无数据"的硬件(而不是直接隐藏,让用户误以为配置未生效)
EnsureGroupsForEnabledHardware(groups, snap);
foreach (var hw in _visitor.AllHardware)
{
string groupId = hw.HardwareType.ToString().ToLowerInvariant();
string groupName = hw.HardwareType.ToString();
if (!groups.TryGetValue(groupId, out var g))
{
g = new SensorGroup { Id = groupId, Name = groupName };
groups[groupId] = g;
snap.Groups.Add(g);
}
foreach (var s in hw.Sensors)
{
// 按配置过滤传感器类型:关闭的类型不加入快照,减少传输数据量
string sensorType = s.SensorType.ToString().ToLowerInvariant();
if (!_config.IsSensorTypeEnabled(sensorType))
continue;
g.Sensors.Add(new SensorEntry
{
Id = $"{groupId}/{hw.Name}/{s.SensorType}/{s.Name}".Replace(' ', '_').ToLowerInvariant(),
Name = s.Name,
Type = sensorType,
Value = s.Value,
Unit = UnitFor(s.SensorType),
HardwareName = hw.Name,
});
}
}
return snap;
}
/// <summary>
/// 为已启用但 LHB 未枚举到的硬件类型预创建空分组。
/// 场景:用户在设置中勾选了主板/电池/电源等,但 LHB 在当前权限或机型下检测不到对应硬件,
/// 此时仍创建空分组让前端显示"已启用但无数据",避免用户误以为配置未生效。
/// GPU 特殊处理:LHB 会枚举到 GpuIntel/GpuAmd/GpuNvidia 之一,不预创建通用分组。
/// </summary>
private void EnsureGroupsForEnabledHardware(Dictionary<string, SensorGroup> groups, SensorSnapshot snap)
{
// (configKey, groupId, groupName)
// 注意:controller 对应 SuperIO 和 EmbeddedController 两个 HardwareType
var mapping = new (string, string, string)[]
{
("cpu", "cpu", "CPU"),
("memory", "memory", "Memory"),
("storage", "storage", "Storage"),
("motherboard", "motherboard", "Motherboard"),
("controller", "superio", "SuperIO"),
("controller", "embeddedcontroller", "EmbeddedController"),
("battery", "battery", "Battery"),
("network", "network", "Network"),
("psu", "psu", "Psu"),
};
foreach (var (key, groupId, groupName) in mapping)
{
if (_config.IsHardwareEnabled(key) && !groups.ContainsKey(groupId))
{
var g = new SensorGroup { Id = groupId, Name = groupName };
groups[groupId] = g;
snap.Groups.Add(g);
}
}
}
/// <summary>
/// 判断硬件是否属于慢通道(低频 Update 即可)。
/// 快通道:CPU/GPU/Memory/Network(变化快、查询轻量)
/// 慢通道:Storage/PSU/Motherboard/Battery/SuperIO/EmbeddedController(查询重或变化慢)
/// </summary>
public static bool IsSlowHardware(HardwareType t) => t switch
{
HardwareType.Storage => true,
HardwareType.Psu => true,
HardwareType.Motherboard => true,
HardwareType.Battery => true,
HardwareType.SuperIO => true,
HardwareType.EmbeddedController => true,
_ => false,
};
private static string UnitFor(SensorType t) => t switch
{
SensorType.Temperature => "°C",
SensorType.Load => "%",
SensorType.Power => "W",
SensorType.Voltage => "V",
SensorType.Fan => "RPM",
SensorType.Clock => "MHz",
SensorType.Data => "GB",
SensorType.SmallData => "MB",
SensorType.Frequency => "Hz",
SensorType.Throughput => "B/s",
SensorType.Level => "%",
SensorType.Factor => "",
SensorType.Control => "%",
SensorType.Flow => "L/h",
SensorType.TimeSpan => "s",
SensorType.Energy => "mWh",
SensorType.Noise => "dBA",
SensorType.Conductivity => "µS/cm",
SensorType.Humidity => "%",
_ => "",
};
private static bool IsRunningAsAdmin()
{
try
{
using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
var principal = new System.Security.Principal.WindowsPrincipal(identity);
return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
}
catch
{
return false;
}
}
public void Dispose()
{
if (_closed) return;
_closed = true;
try { _computer.Close(); } catch { /* Close 在 AOT 下偶有反射清理异常 */ }
}
}
/// <summary>
/// 遍历 Computer,收集所有 Hardware(含 SubHardware)。
/// LHB 的 Visit 会递归到 SubHardware。
/// 引用缓存:visitor 持有 hardware 引用,传感器值通过 hardware.Sensors 实时读取。
/// </summary>
internal sealed class SnapshotVisitor : IVisitor
{
public List<IHardware> AllHardware { get; } = new();
public void VisitComputer(IComputer computer)
{
foreach (var hw in computer.Hardware) hw.Accept(this);
}
public void VisitHardware(IHardware hardware)
{
AllHardware.Add(hardware);
foreach (var sub in hardware.SubHardware) sub.Accept(this);
}
public void VisitSensor(ISensor sensor) { }
public void VisitParameter(IParameter parameter) { }
}
/// <summary>
/// 采样调度器:按快/慢通道分频驱动 HardwareManager.UpdateAll。
/// 使用 Channel 向 SSE 推送层广播快照(解耦:调度器不关心有几个订阅者)。
///
/// 调度策略:
/// - 快通道 tickUpdateAll(含慢通道硬件,因 UpdateAll 成本主要在 SMART,已通过慢通道分频减少调用频率)
/// 实际优化:快通道 tick 只 Update 快通道硬件(UpdateFastOnly),慢通道单独按慢节奏 Update
/// - 慢通道 tickUpdateSlowOnly(仅 Storage/PSU/Motherboard 等)
/// - 每个 tick 结束后构建快照并广播
/// </summary>
internal sealed class SamplingScheduler : IDisposable
{
private readonly HardwareManager _hw;
private readonly CancellationTokenSource _cts = new();
private readonly Channel<SensorSnapshot> _broadcast;
private readonly SnapshotCache _cache;
private int _fastIntervalMs = 1000;
private int _slowIntervalMs = 5000;
public SnapshotCache Cache => _cache;
public int FastIntervalMs => _fastIntervalMs;
public int SlowIntervalMs => _slowIntervalMs;
public SamplingScheduler(HardwareManager hw, int fastIntervalMs, int slowIntervalMs)
{
_hw = hw;
_fastIntervalMs = Math.Max(200, fastIntervalMs);
_slowIntervalMs = Math.Max(_fastIntervalMs, slowIntervalMs);
// unbounded channel:快照丢失风险 < 内存爆涨风险(消费慢时丢弃最旧的)
// 这里用 bounded + DropOldest:保证 SSE 慢消费者不阻塞调度器
_broadcast = Channel.CreateBounded<SensorSnapshot>(new BoundedChannelOptions(8)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = false,
SingleWriter = true,
});
_cache = new SnapshotCache();
}
/// <summary>
/// 启动调度循环。立即推送首个快照(已由 HardwareManager 构造时 Update 过)。
/// </summary>
public Task StartAsync()
{
// 首个快照(含 ColdStartMs
var first = _hw.BuildSnapshot();
_cache.Update(first);
_broadcast.Writer.TryWrite(first);
// 快/慢通道并行循环
_ = Task.Run(() => FastLoopAsync(_cts.Token));
_ = Task.Run(() => SlowLoopAsync(_cts.Token));
return Task.CompletedTask;
}
private async Task FastLoopAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_fastIntervalMs));
while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false))
{
try
{
_hw.UpdateAll();
var snap = _hw.BuildSnapshot();
_cache.Update(snap);
_broadcast.Writer.TryWrite(snap);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[Scheduler] 快通道异常: {ex.Message}");
}
}
}
private async Task SlowLoopAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_slowIntervalMs));
while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false))
{
try
{
// 慢通道单独 Update,避免依赖快通道的 UpdateAll
// 注:下一次快通道 tick 构建的快照会包含本次慢通道更新后的值
_hw.UpdateSlowOnly();
}
catch (Exception ex)
{
Console.Error.WriteLine($"[Scheduler] 慢通道异常: {ex.Message}");
}
}
}
/// <summary>
/// 订阅快照流。每个 SSE 客户端调用一次。
/// 返回的 IAsyncEnumerable 会在调度器停止或订阅者取消时结束。
/// </summary>
public IAsyncEnumerable<SensorSnapshot> SubscribeAsync(CancellationToken ct) =>
_broadcast.Reader.ReadAllAsync(ct);
public void UpdateIntervals(int? fastMs, int? slowMs)
{
// 注意:PeriodicTimer 已启动后无法修改间隔,下次重启 Kernel 才生效。
// 阶段二简化处理:记录新值,实际生效需重启。阶段三若需热更新可重建 timer。
if (fastMs.HasValue && fastMs.Value >= 200) _fastIntervalMs = fastMs.Value;
if (slowMs.HasValue && slowMs.Value >= _fastIntervalMs) _slowIntervalMs = slowMs.Value;
}
public void Dispose()
{
_cts.Cancel();
_broadcast.Writer.TryComplete();
_cts.Dispose();
}
}
/// <summary>
/// 快照缓存:存储最新快照,供 GET /snapshot 直接返回,避免触发底层重扫描。
/// 线程安全:读写均加锁,快照对象本身不可变(每次 Update 替换引用)。
/// </summary>
internal sealed class SnapshotCache
{
private readonly object _lock = new();
private SensorSnapshot? _latest;
private int _sensorCount;
public void Update(SensorSnapshot snap)
{
lock (_lock)
{
_latest = snap;
_sensorCount = snap.Groups.Sum(g => g.Sensors.Count);
}
}
public SensorSnapshot? GetLatest()
{
lock (_lock)
{
return _latest;
}
}
public int SensorCount
{
get { lock (_lock) return _sensorCount; }
}
}