using DeviceCommand.Base; using Logger; using Model.Models; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; namespace UIShare.GlobalVariable { /// /// 设备健康监控器:独立于监控采样的心跳重连机制。 /// /// 设计原则——与检测值零冲突: /// /// 健康检查平时只读 (纯本地属性,零网络开销,不碰通信锁) /// 仅在 IsConnected==false 时才获取 _commLock 执行重连,与监控采样天然串行化 /// ModbusTcp 的 ConnectAsync 是幂等的(已连接时直接返回),不会中断正在进行的监控 /// TCP 的 ConnectAsync 会重置连接,但监控的 catch 容忍单次失败,下次 tick 自动恢复 /// /// /// public class DeviceHealthMonitor : IDisposable { private readonly IDictionary _deviceMap; private readonly SystemConfig _systemConfig; private readonly string _scopeName; /// 健康检查定时器 private Timer? _healthCheckTimer; /// 每个设备的连续失败计数 private readonly ConcurrentDictionary _failureCounts = new(); /// 每个设备的重连尝试次数(用于计算退避阈值) private readonly ConcurrentDictionary _reconnectAttempts = new(); /// 基础失败阈值(首次重连触发值) private const int BaseFailureThreshold = 3; /// 退避上限(最大阈值) private const int MaxBackoffThreshold = 15; /// 健康检查间隔(毫秒) private readonly int _checkIntervalMs; private bool _disposed; private readonly object _startStopLock = new(); /// /// 创建健康监控器实例。 /// /// 当前作用域的设备字典 /// 当前作用域的系统配置(用于更新 DeviceInfoVM.IsConnected) /// 作用域名称(日志标识) /// 健康检查间隔,默认 5000ms public DeviceHealthMonitor( IDictionary deviceMap, SystemConfig systemConfig, string scopeName, int checkIntervalMs = 5000) { _deviceMap = deviceMap; _systemConfig = systemConfig; _scopeName = scopeName; _checkIntervalMs = checkIntervalMs; } /// 启动健康监控(幂等:多次调用只启动一次) public void Start() { lock (_startStopLock) { if (_disposed || _healthCheckTimer != null) return; _healthCheckTimer = new Timer( OnHealthCheckTick, null, TimeSpan.FromSeconds(10), // 首次检查延迟 10 秒,避免启动时设备尚未连接完成 TimeSpan.FromMilliseconds(_checkIntervalMs)); LoggerHelper.Info($"[{_scopeName}] 设备健康监控已启动,检查间隔={_checkIntervalMs}ms,基础重连阈值={BaseFailureThreshold}次(指数退避上限={MaxBackoffThreshold})"); } } /// 停止健康监控 public void Stop() { lock (_startStopLock) { _healthCheckTimer?.Change(Timeout.Infinite, Timeout.Infinite); _healthCheckTimer?.Dispose(); _healthCheckTimer = null; _failureCounts.Clear(); _reconnectAttempts.Clear(); } } /// /// 健康检查核心逻辑:遍历设备,检查连接状态,失败计数超阈值则重连。 /// TCP 设备额外进行主动探活(*IDN?),以检测死连接(对端崩溃但 TCP 未收到 FIN)。 /// private async void OnHealthCheckTick(object? state) { if (_disposed || _deviceMap.Count == 0) return; // 快照避免枚举期间字典被修改 var snapshot = _deviceMap.ToArray(); foreach (var kvp in snapshot) { if (_disposed) return; string deviceName = kvp.Key; var device = kvp.Value; try { bool alive = device.IsConnected; // TCP 设备主动探活:IsConnected 只反映上次操作状态,无法检测死连接 if (alive && device is Tcp tcpDevice) { alive = await ProbeTcpDeviceAsync(tcpDevice); } if (alive) { // 连接正常:清零失败计数与退避 _failureCounts.TryRemove(deviceName, out _); _reconnectAttempts.TryRemove(deviceName, out _); continue; } // 连接断开:累加失败计数 int failures = _failureCounts.AddOrUpdate(deviceName, 1, (_, count) => count + 1); // 计算当前退避阈值:基础值 + 重连尝试次数 × 2,上限为 MaxBackoffThreshold int attempts = _reconnectAttempts.GetOrAdd(deviceName, 0); int currentThreshold = Math.Min(BaseFailureThreshold + attempts * 2, MaxBackoffThreshold); if (failures < currentThreshold) { LoggerHelper.Warn( $"[{_scopeName}] 设备 [{deviceName}] 连接断开,等待重连中 ({failures}/{currentThreshold})"); continue; } // 达到阈值:执行重连 await ReconnectDeviceAsync(deviceName, device); } catch (Exception ex) { LoggerHelper.Error($"[{_scopeName}] 设备 [{deviceName}] 健康检查异常:{ex.Message}"); } } } /// /// 对 TCP 设备执行轻量级主动探活(SCPI *IDN?),2 秒超时。 /// 通过 WriteReadAsync 内部获取 _commLock,与监控采样天然串行化。 /// /// true: 探活成功(连接确实存活); false: 探活失败(死连接) private async Task ProbeTcpDeviceAsync(Tcp tcpDevice) { try { using var probeCts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); string resp = await tcpDevice.WriteReadAsync("*IDN?\n", "\n", probeCts.Token); return !string.IsNullOrWhiteSpace(resp); } catch { return false; } } /// /// 重连单个设备。 /// /// 冲突避免机制:ConnectAsync 内部获取设备的 _commLock, /// 如果此时监控采样正在通信,重连会等待锁释放后再执行, /// 保证同一时刻只有一个操作在使用通信链路。 /// /// private async Task ReconnectDeviceAsync(string deviceName, IBaseInterface device) { try { int attempts = _reconnectAttempts.AddOrUpdate(deviceName, 1, (_, c) => c + 1); int nextThreshold = Math.Min(BaseFailureThreshold + attempts * 2, MaxBackoffThreshold); LoggerHelper.Info($"[{_scopeName}] 设备 [{deviceName}] 连续失败触发重连(第 {attempts} 次重连,下次阈值={nextThreshold})..."); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); bool ok = await device.ConnectAsync(cts.Token); // 同步更新 DeviceInfoVM 的 UI 状态 UpdateDeviceInfoState(deviceName, ok); if (ok) { _failureCounts.TryRemove(deviceName, out _); _reconnectAttempts.TryRemove(deviceName, out _); LoggerHelper.Info($"[{_scopeName}] 设备 [{deviceName}] 重连成功"); } else { LoggerHelper.Warn($"[{_scopeName}] 设备 [{deviceName}] 重连失败,将在下次检查时重试"); } } catch (OperationCanceledException) { UpdateDeviceInfoState(deviceName, false); LoggerHelper.Warn($"[{_scopeName}] 设备 [{deviceName}] 重连超时(10s),将在下次检查时重试"); } catch (Exception ex) { UpdateDeviceInfoState(deviceName, false); LoggerHelper.Error($"[{_scopeName}] 设备 [{deviceName}] 重连异常:{ex.Message}"); } } /// /// 通过 UI 线程更新 SystemConfig.DeviceList 中对应设备的 IsConnected 状态(驱动 UI 刷新)。 /// Timer 回调运行在线程池线程上,必须切回 UI 线程才能触发 PropertyChanged。 /// private void UpdateDeviceInfoState(string deviceName, bool isConnected) { var info = _systemConfig?.DeviceList? .FirstOrDefault(d => d != null && string.Equals(d.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase)); if (info == null) return; var dispatcher = Application.Current?.Dispatcher; if (dispatcher == null || dispatcher.CheckAccess()) { // 已在 UI 线程(或无 Dispatcher),直接赋值 info.IsConnected = isConnected; } else { // 切回 UI 线程赋值,避免跨线程 PropertyChanged 异常 dispatcher.BeginInvoke(() => info.IsConnected = isConnected); } } public void Dispose() { if (_disposed) return; _disposed = true; Stop(); } } }