PlcPollingService.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. using CommunityToolkit.Mvvm.ComponentModel;
  2. using Serilog;
  3. using YZWater.Core.Models;
  4. namespace YZWater.Core.Services;
  5. /// <summary>
  6. /// PLC 轮询服务 - 负责实时数据采集、自动重连、数据分发
  7. /// </summary>
  8. public partial class PlcPollingService : ObservableObject, IDisposable, IServiceHealthCheck, IRestartableService
  9. {
  10. private static PlcPollingService? _instance;
  11. private static readonly object _instanceLock = new();
  12. public static PlcPollingService Instance { get { lock (_instanceLock) { return _instance ??= new PlcPollingService(); } } }
  13. /// <summary>
  14. /// 重置单例(用于登出后重新登录)
  15. /// </summary>
  16. public static void ResetInstance()
  17. {
  18. lock (_instanceLock)
  19. {
  20. var old = _instance;
  21. _instance = null; // 先置 null,确保并发访问 Instance 时创建新实例
  22. old?.Dispose();
  23. }
  24. }
  25. private readonly PlcDataModel _data = new();
  26. private CancellationTokenSource? _cts;
  27. private Task? _pollingTask;
  28. private bool _disposed;
  29. // PLC 读取并发限制(防止同时发起过多请求导致 PLC 过载)
  30. private static readonly SemaphoreSlim _plcReadSemaphore = new(4, 4);
  31. // ─── 配置 ───
  32. public int PollingIntervalMs { get; set; } = 1000;
  33. public float DeadbandPercent { get; set; } = 0.5f;
  34. public int MaxReconnectAttempts { get; set; } = 10;
  35. public int ReconnectBaseDelayMs { get; set; } = 1000;
  36. // ─── 数据变更事件 ───
  37. public event Action<PlcDataModel>? DataUpdated;
  38. public event Action<bool>? ConnectionStateChanged;
  39. public event Action<string>? ErrorOccurred;
  40. /// <summary>
  41. /// 当前数据模型
  42. /// </summary>
  43. public PlcDataModel Data => _data;
  44. private PlcPollingService() { }
  45. /// <summary>
  46. /// 启动轮询
  47. /// </summary>
  48. public void Start()
  49. {
  50. if (_pollingTask != null && !_pollingTask.IsCompleted)
  51. {
  52. Log.Warning("轮询已在运行中");
  53. return;
  54. }
  55. _cts = new CancellationTokenSource();
  56. _pollingTask = Task.Run(() => PollingLoopAsync(_cts.Token));
  57. Log.Information("PLC 轮询服务已启动");
  58. }
  59. /// <summary>
  60. /// 停止轮询
  61. /// </summary>
  62. public void Stop()
  63. {
  64. Log.Information("PlcPollingService 正在停止... _cts={HasCts}, _pollingTask={HasTask}",
  65. _cts != null, _pollingTask != null);
  66. _cts?.Cancel();
  67. if (_pollingTask != null)
  68. {
  69. var completed = _pollingTask.Wait(TimeSpan.FromSeconds(5));
  70. if (!completed)
  71. {
  72. Log.Warning("PlcPollingService 轮询任务在5秒内未完成,可能仍在运行");
  73. }
  74. else
  75. {
  76. Log.Information("PlcPollingService 轮询任务已完成");
  77. }
  78. }
  79. _cts?.Dispose();
  80. _cts = null;
  81. _pollingTask = null;
  82. Log.Information("PlcPollingService 已停止");
  83. }
  84. /// <summary>
  85. /// 轮询主循环
  86. /// </summary>
  87. private async Task PollingLoopAsync(CancellationToken ct)
  88. {
  89. var reconnectAttempts = 0;
  90. var isInDegradedMode = false; // 降级模式标志
  91. const int degradedModeRetryIntervalMs = 30000; // 降级模式下 30 秒重试一次
  92. while (!ct.IsCancellationRequested)
  93. {
  94. try
  95. {
  96. if (MockPlcService.IsEnabled)
  97. {
  98. // Mock 模式:直接生成模拟数据
  99. MockPlcService.UpdateData(_data);
  100. }
  101. else
  102. {
  103. // 真实模式:确保连接
  104. if (!PlcService.IsConnected)
  105. {
  106. var connected = await TryConnectWithRetryAsync(reconnectAttempts, ct);
  107. if (!connected)
  108. {
  109. reconnectAttempts++;
  110. // 进入或保持在降级模式
  111. if (reconnectAttempts >= MaxReconnectAttempts)
  112. {
  113. if (!isInDegradedMode)
  114. {
  115. // 首次进入降级模式
  116. isInDegradedMode = true;
  117. var errorMsg = $"PLC 连接失败,已重试 {MaxReconnectAttempts} 次,进入降级模式(每 30 秒重试)";
  118. Log.Error(errorMsg);
  119. ErrorOccurred?.Invoke(errorMsg);
  120. // 发送 PLC 断开通知
  121. _ = NotificationService.Instance.SendPlcConnectionNotificationAsync(false);
  122. }
  123. else
  124. {
  125. // 已在降级模式,定期记录日志
  126. if (reconnectAttempts % 10 == 0) // 每 10 次(5 分钟)记录一次
  127. {
  128. Log.Warning($"PLC 连接仍在尝试中,累计重试 {reconnectAttempts} 次");
  129. }
  130. }
  131. // 降级模式:使用更长的重试间隔
  132. await Task.Delay(degradedModeRetryIntervalMs, ct);
  133. continue;
  134. }
  135. // 正常模式:指数退避
  136. var delayMs = ReconnectBaseDelayMs * Math.Min(reconnectAttempts, 5);
  137. await Task.Delay(delayMs, ct);
  138. continue;
  139. }
  140. // 连接成功
  141. if (isInDegradedMode)
  142. {
  143. // 从降级模式恢复
  144. Log.Information($"PLC 连接恢复,退出降级模式(累计重试 {reconnectAttempts} 次)");
  145. ErrorOccurred?.Invoke($"PLC 连接已恢复");
  146. isInDegradedMode = false;
  147. // 发送 PLC 恢复通知
  148. _ = NotificationService.Instance.SendPlcConnectionNotificationAsync(true);
  149. }
  150. reconnectAttempts = 0;
  151. }
  152. // 读取数据
  153. await ReadAllDataAsync(ct);
  154. }
  155. // 数据更新事件
  156. DataUpdated?.Invoke(_data);
  157. // 等待下一次轮询(降级模式下使用更长间隔)
  158. var nextDelay = isInDegradedMode ? degradedModeRetryIntervalMs : PollingIntervalMs;
  159. await Task.Delay(nextDelay, ct);
  160. }
  161. catch (OperationCanceledException)
  162. {
  163. break;
  164. }
  165. catch (Exception ex)
  166. {
  167. Log.Error(ex, "轮询异常");
  168. ErrorOccurred?.Invoke($"轮询异常: {ex.Message}");
  169. // 连接断开,尝试重连
  170. if (!PlcService.IsConnected)
  171. {
  172. _data.IsPlcConnected = false;
  173. ConnectionStateChanged?.Invoke(false);
  174. }
  175. await Task.Delay(PollingIntervalMs * 2, ct);
  176. }
  177. }
  178. }
  179. /// <summary>
  180. /// 尝试连接(带重试)
  181. /// </summary>
  182. private async Task<bool> TryConnectWithRetryAsync(int attempt, CancellationToken ct)
  183. {
  184. for (int i = 0; i <= attempt && i < 3; i++)
  185. {
  186. if (ct.IsCancellationRequested) return false;
  187. var connected = await PlcService.ConnectAsync();
  188. if (connected)
  189. {
  190. _data.IsPlcConnected = true;
  191. ConnectionStateChanged?.Invoke(true);
  192. return true;
  193. }
  194. if (i < attempt)
  195. await Task.Delay(ReconnectBaseDelayMs * (i + 1), ct);
  196. }
  197. return false;
  198. }
  199. /// <summary>
  200. /// 批量读取所有 PLC 数据
  201. /// 根据是否加载了项目档案选择读取模式
  202. /// </summary>
  203. private async Task ReadAllDataAsync(CancellationToken ct)
  204. {
  205. if (ProjectService.Current != null)
  206. await ReadAllDataFromProfile(ct);
  207. else
  208. await ReadAllDataDefault(ct);
  209. }
  210. /// <summary>
  211. /// 默认模式:硬编码地址批量读取(向后兼容)
  212. /// </summary>
  213. private async Task ReadAllDataDefault(CancellationToken ct)
  214. {
  215. if (ct.IsCancellationRequested) return;
  216. // 并行发起所有批量读取(使用 PlcConfig 中的地址常量)
  217. var tankTask = PlcService.ReadBytesAsync(PlcConfig.Tank1Level, 16); // 4 floats: 水箱液位
  218. var flowTask = PlcService.ReadBytesAsync(PlcConfig.InflowRate, 8); // 2 floats: 流量
  219. var freqTask = PlcService.ReadBytesAsync(PlcConfig.Pump1Freq, 20); // 5 floats: 泵频率
  220. var qTask = PlcService.ReadBytesAsync("Q0", 1); // 1 byte: 泵/风机运行状态
  221. var iTask = PlcService.ReadBytesAsync("I0", 1); // 1 byte: 泵/风机故障状态
  222. var mTask = PlcService.ReadBytesAsync("M0", 1); // 1 byte: 系统模式
  223. await Task.WhenAll(tankTask, flowTask, freqTask, qTask, iTask, mTask);
  224. if (ct.IsCancellationRequested) return;
  225. var tankData = tankTask.Result;
  226. var flowData = flowTask.Result;
  227. var freqData = freqTask.Result;
  228. var qData = qTask.Result;
  229. var iData = iTask.Result;
  230. var mData = mTask.Result;
  231. // 任一批量读取失败则跳过本周期(全部 6 个都检查)
  232. if (tankData == null || flowData == null || freqData == null ||
  233. qData == null || iData == null || mData == null)
  234. {
  235. _data.IsDataValid = false;
  236. return;
  237. }
  238. // 解析水箱液位 (VD100-VD112, 4 floats)
  239. var tank1 = PlcService.ParseFloat(tankData, 0);
  240. var tank2 = PlcService.ParseFloat(tankData, 4);
  241. var tank3 = PlcService.ParseFloat(tankData, 8);
  242. var tank4 = PlcService.ParseFloat(tankData, 12);
  243. // 解析流量 (VD200-VD204, 2 floats)
  244. var inflow = PlcService.ParseFloat(flowData, 0);
  245. var outflow = PlcService.ParseFloat(flowData, 4);
  246. // 解析泵频率 (VD300-VD316, 5 floats)
  247. var f1 = PlcService.ParseFloat(freqData, 0);
  248. var f2 = PlcService.ParseFloat(freqData, 4);
  249. var f3 = PlcService.ParseFloat(freqData, 8);
  250. var f4 = PlcService.ParseFloat(freqData, 12);
  251. var f5 = PlcService.ParseFloat(freqData, 16);
  252. // 解析 Q0 字节: bit 0-4 = 泵运行, bit 5-6 = 风机运行
  253. var p1 = PlcService.ParseBit(qData, 0, 0);
  254. var p2 = PlcService.ParseBit(qData, 0, 1);
  255. var p3 = PlcService.ParseBit(qData, 0, 2);
  256. var p4 = PlcService.ParseBit(qData, 0, 3);
  257. var p5 = PlcService.ParseBit(qData, 0, 4);
  258. var fan1 = PlcService.ParseBit(qData, 0, 5);
  259. var fan2 = PlcService.ParseBit(qData, 0, 6);
  260. // 解析 I0 字节: bit 0-4 = 泵故障, bit 5-6 = 风机故障
  261. var pf1 = PlcService.ParseBit(iData, 0, 0);
  262. var pf2 = PlcService.ParseBit(iData, 0, 1);
  263. var pf3 = PlcService.ParseBit(iData, 0, 2);
  264. var pf4 = PlcService.ParseBit(iData, 0, 3);
  265. var pf5 = PlcService.ParseBit(iData, 0, 4);
  266. var fanf1 = PlcService.ParseBit(iData, 0, 5);
  267. var fanf2 = PlcService.ParseBit(iData, 0, 6);
  268. // 解析 M0 字节: bit 0 = 系统模式
  269. var autoMode = PlcService.ParseBit(mData, 0, 0);
  270. // 应用死区过滤后更新数据
  271. ApplyWithDeadband(_data.Tank1Level, tank1, v => _data.Tank1Level = v);
  272. ApplyWithDeadband(_data.Tank2Level, tank2, v => _data.Tank2Level = v);
  273. ApplyWithDeadband(_data.Tank3Level, tank3, v => _data.Tank3Level = v);
  274. ApplyWithDeadband(_data.Tank4Level, tank4, v => _data.Tank4Level = v);
  275. ApplyWithDeadband(_data.InflowRate, inflow, v => _data.InflowRate = v);
  276. ApplyWithDeadband(_data.OutflowRate, outflow, v => _data.OutflowRate = v);
  277. _data.Pump1Running = p1;
  278. _data.Pump2Running = p2;
  279. _data.Pump3Running = p3;
  280. _data.Pump4Running = p4;
  281. _data.Pump5Running = p5;
  282. _data.Pump1Fault = pf1;
  283. _data.Pump2Fault = pf2;
  284. _data.Pump3Fault = pf3;
  285. _data.Pump4Fault = pf4;
  286. _data.Pump5Fault = pf5;
  287. _data.Pump1Freq = f1;
  288. _data.Pump2Freq = f2;
  289. _data.Pump3Freq = f3;
  290. _data.Pump4Freq = f4;
  291. _data.Pump5Freq = f5;
  292. _data.Fan1Running = fan1;
  293. _data.Fan2Running = fan2;
  294. _data.Fan1Fault = fanf1;
  295. _data.Fan2Fault = fanf2;
  296. _data.IsAutoMode = autoMode;
  297. _data.LastReadTime = DateTime.Now;
  298. _data.IsDataValid = true;
  299. _data.IsPlcConnected = PlcService.IsConnected;
  300. _data.FlowDelta = _data.InflowRate - _data.OutflowRate;
  301. // 触发计算属性更新
  302. OnPropertyChanged(nameof(PlcDataModel.FlowDelta));
  303. }
  304. /// <summary>
  305. /// 项目档案模式:根据 ProjectProfile 动态读取 PLC 数据
  306. /// </summary>
  307. private async Task ReadAllDataFromProfile(CancellationToken ct)
  308. {
  309. if (ct.IsCancellationRequested) return;
  310. var profile = ProjectService.Current;
  311. if (profile == null) return;
  312. // 收集所有需要读取的模拟量地址 (REAL/float)
  313. var analogReads = new List<(string key, string addr)>();
  314. foreach (var pump in profile.Pumps)
  315. {
  316. if (!string.IsNullOrEmpty(pump.CurrentAddress))
  317. analogReads.Add(($"{pump.Name}.Current", pump.CurrentAddress));
  318. if (!string.IsNullOrEmpty(pump.DailyHoursAddress))
  319. analogReads.Add(($"{pump.Name}.DailyHours", pump.DailyHoursAddress));
  320. if (!string.IsNullOrEmpty(pump.DailyMinutesAddress))
  321. analogReads.Add(($"{pump.Name}.DailyMinutes", pump.DailyMinutesAddress));
  322. }
  323. foreach (var tank in profile.Tanks)
  324. {
  325. if (!string.IsNullOrEmpty(tank.LevelAddress))
  326. analogReads.Add(($"{tank.Name}.Level", tank.LevelAddress));
  327. if (!string.IsNullOrEmpty(tank.StartPump1LevelAddress))
  328. analogReads.Add(($"{tank.Name}.StartPump1Level", tank.StartPump1LevelAddress));
  329. if (!string.IsNullOrEmpty(tank.StartPump2LevelAddress))
  330. analogReads.Add(($"{tank.Name}.StartPump2Level", tank.StartPump2LevelAddress));
  331. if (!string.IsNullOrEmpty(tank.StartPump3LevelAddress))
  332. analogReads.Add(($"{tank.Name}.StartPump3Level", tank.StartPump3LevelAddress));
  333. if (!string.IsNullOrEmpty(tank.StopLevelAddress))
  334. analogReads.Add(($"{tank.Name}.StopLevel", tank.StopLevelAddress));
  335. if (!string.IsNullOrEmpty(tank.ProtectionLevelAddress))
  336. analogReads.Add(($"{tank.Name}.ProtectionLevel", tank.ProtectionLevelAddress));
  337. }
  338. foreach (var gauge in profile.Gauges)
  339. {
  340. if (!string.IsNullOrEmpty(gauge.ValueAddress))
  341. analogReads.Add(($"{gauge.Name}.Value", gauge.ValueAddress));
  342. if (!string.IsNullOrEmpty(gauge.HighSetAddress))
  343. analogReads.Add(($"{gauge.Name}.HighSet", gauge.HighSetAddress));
  344. if (!string.IsNullOrEmpty(gauge.LowSetAddress))
  345. analogReads.Add(($"{gauge.Name}.LowSet", gauge.LowSetAddress));
  346. }
  347. // 收集传感器读取(带数据类型)
  348. var sensorReads = new List<(string key, string addr, string dataType)>();
  349. foreach (var sensor in profile.Sensors)
  350. {
  351. if (string.IsNullOrEmpty(sensor.Address)) continue;
  352. sensorReads.Add(($"{sensor.Name}.Value", sensor.Address, (sensor.DataType ?? "REAL").ToUpper()));
  353. }
  354. // 按数据类型分类处理
  355. foreach (var sensor in sensorReads)
  356. {
  357. switch (sensor.dataType)
  358. {
  359. case "REAL":
  360. // 4字节浮点数
  361. analogReads.Add((sensor.key, sensor.addr));
  362. break;
  363. case "INT":
  364. case "WORD":
  365. case "DWORD":
  366. case "DINT":
  367. // 整数类型:读取字节后手动转换
  368. analogReads.Add(($"{sensor.key}#{sensor.dataType}", sensor.addr));
  369. break;
  370. default:
  371. // 默认按 REAL 处理
  372. analogReads.Add((sensor.key, sensor.addr));
  373. break;
  374. }
  375. }
  376. // 收集所有需要读取的数字量地址 (bool/bit)
  377. var digitalReads = new List<(string key, string addr)>();
  378. foreach (var pump in profile.Pumps)
  379. {
  380. if (!string.IsNullOrEmpty(pump.RunAddress))
  381. digitalReads.Add(($"{pump.Name}.Run", pump.RunAddress));
  382. if (!string.IsNullOrEmpty(pump.FaultAddress))
  383. digitalReads.Add(($"{pump.Name}.Fault", pump.FaultAddress));
  384. if (!string.IsNullOrEmpty(pump.AutoStateAddress))
  385. digitalReads.Add(($"{pump.Name}.AutoMode", pump.AutoStateAddress));
  386. if (!string.IsNullOrEmpty(pump.ModeSelectAddress))
  387. digitalReads.Add(($"{pump.Name}.ModeSelect", pump.ModeSelectAddress));
  388. }
  389. // 通用数字量输入(如变频器故障)
  390. foreach (var di in profile.DigitalInputs)
  391. {
  392. if (!string.IsNullOrEmpty(di.Address))
  393. digitalReads.Add(($"DI.{di.Name}", di.Address));
  394. }
  395. // 并行发起所有读取(使用信号量限制并发数,避免 PLC 过载)
  396. // 性能优化:S7AddressParser 已实现地址解析和分组算法
  397. // 对于连续地址(如 VD500, VD504, VD508),使用批量读取优化
  398. // 立即物化 Task 数组,避免 IEnumerable 双重枚举导致 PLC 读取量翻倍
  399. // 尝试批量读取优化
  400. var batchResults = await TryBatchReadAsync(analogReads, ct);
  401. var analogTasks = analogReads.Select(async r =>
  402. {
  403. await _plcReadSemaphore.WaitAsync(ct);
  404. try
  405. {
  406. float value;
  407. var key = r.key;
  408. // 优先使用批量读取结果
  409. if (batchResults != null && batchResults.TryGetValue(key, out var batchValue))
  410. {
  411. return (key, Value: batchValue);
  412. }
  413. // 检查是否为整数类型(key 格式:name#DATATYPE)
  414. if (key.Contains('#'))
  415. {
  416. var parts = key.Split('#');
  417. key = parts[0];
  418. var dataType = parts[1];
  419. // 读取字节后手动转换
  420. var bytes = await PlcService.ReadBytesAsync(r.addr, dataType is "INT" or "WORD" ? (ushort)2 : (ushort)4);
  421. if (bytes == null || bytes.Length == 0)
  422. {
  423. return (key, Value: 0f);
  424. }
  425. value = dataType switch
  426. {
  427. "INT" => (float)BitConverter.ToInt16(bytes, 0),
  428. "WORD" => (float)BitConverter.ToUInt16(bytes, 0),
  429. "DINT" => (float)BitConverter.ToInt32(bytes, 0),
  430. "DWORD" => (float)BitConverter.ToUInt32(bytes, 0),
  431. _ => PlcService.ParseFloat(bytes, 0)
  432. };
  433. }
  434. else
  435. {
  436. // REAL 类型直接读浮点数
  437. value = await PlcService.ReadFloatAsync(r.addr);
  438. }
  439. return (key, Value: value);
  440. }
  441. finally
  442. {
  443. _plcReadSemaphore.Release();
  444. }
  445. }).ToArray();
  446. var digitalTasks = digitalReads.Select(async r =>
  447. {
  448. await _plcReadSemaphore.WaitAsync(ct);
  449. try
  450. {
  451. var value = await PlcService.ReadBoolAsync(r.addr);
  452. return (r.key, Value: value);
  453. }
  454. finally
  455. {
  456. _plcReadSemaphore.Release();
  457. }
  458. }).ToArray();
  459. var allTasks = analogTasks.Cast<Task>().Concat(digitalTasks.Cast<Task>()).ToArray();
  460. if (allTasks.Length > 0)
  461. await Task.WhenAll(allTasks);
  462. if (ct.IsCancellationRequested) return;
  463. // 更新模拟量寄存器
  464. foreach (var task in analogTasks)
  465. {
  466. var (key, value) = await task;
  467. if (_data.AnalogRegisters.TryGetValue(key, out var currentValue))
  468. {
  469. ApplyWithDeadband(currentValue, value, v => _data.AnalogRegisters[key] = v);
  470. }
  471. }
  472. // 更新数字量寄存器
  473. foreach (var task in digitalTasks)
  474. {
  475. var (key, value) = await task;
  476. _data.DigitalRegisters[key] = value;
  477. }
  478. _data.LastReadTime = DateTime.Now;
  479. _data.IsDataValid = true;
  480. _data.IsPlcConnected = PlcService.IsConnected;
  481. }
  482. /// <summary>
  483. /// 尝试批量读取连续地址(优化性能)
  484. /// </summary>
  485. private async Task<Dictionary<string, float>?> TryBatchReadAsync(
  486. List<(string key, string addr)> analogReads, CancellationToken ct)
  487. {
  488. // 只对 REAL 类型的地址进行批量读取(排除带 #DATATYPE 后缀的整数类型)
  489. var realReads = analogReads
  490. .Where(r => !r.key.Contains('#'))
  491. .Select(r => (r.key, r.addr))
  492. .ToList();
  493. if (realReads.Count < 3) return null; // 地址太少,不值得批量
  494. try
  495. {
  496. // 分组连续地址
  497. var groups = S7AddressParser.GroupConsecutiveAddresses(realReads);
  498. // 只对包含多个地址的分组进行批量读取
  499. var batchGroups = groups.Where(g => g.Items.Count >= 2).ToList();
  500. if (batchGroups.Count == 0) return null;
  501. var results = new Dictionary<string, float>();
  502. foreach (var group in batchGroups)
  503. {
  504. if (ct.IsCancellationRequested) return results;
  505. // 构建批量读取地址
  506. var prefix = S7AddressParser.GetAddressPrefix(group.Area, group.TotalBytes switch
  507. {
  508. >= 4 => 4,
  509. >= 2 => 2,
  510. _ => 1
  511. });
  512. var batchAddr = $"{prefix}{group.StartByte}";
  513. await _plcReadSemaphore.WaitAsync(ct);
  514. try
  515. {
  516. var bytes = await PlcService.ReadBytesAsync(batchAddr, (ushort)group.TotalBytes);
  517. if (bytes == null) continue;
  518. // 解析每个子项
  519. foreach (var (key, offset) in group.Items)
  520. {
  521. if (offset + 4 <= bytes.Length)
  522. {
  523. results[key] = PlcService.ParseFloat(bytes, offset);
  524. }
  525. }
  526. }
  527. finally
  528. {
  529. _plcReadSemaphore.Release();
  530. }
  531. }
  532. if (results.Count > 0)
  533. {
  534. Log.Debug("批量读取优化: {Count} 个地址合并为 {Groups} 次读取",
  535. results.Count, batchGroups.Count);
  536. }
  537. return results.Count > 0 ? results : null;
  538. }
  539. catch (Exception ex)
  540. {
  541. Log.Warning(ex, "批量读取失败,回退到单独读取");
  542. return null;
  543. }
  544. }
  545. /// <summary>
  546. /// 死区过滤:只有变化超过阈值才更新
  547. /// </summary>
  548. private void ApplyWithDeadband(float currentValue, float newValue, Action<float> setter)
  549. {
  550. if (Math.Abs(currentValue) < 0.001f && Math.Abs(newValue) < 0.001f)
  551. return; // 都是零值,跳过
  552. var threshold = Math.Abs(currentValue) * DeadbandPercent / 100.0f;
  553. if (Math.Abs(newValue - currentValue) > threshold || currentValue == 0)
  554. {
  555. setter(newValue);
  556. }
  557. }
  558. /// <summary>
  559. /// 立即读取一次(手动刷新)
  560. /// </summary>
  561. public async Task RefreshNowAsync()
  562. {
  563. if (PlcService.IsConnected)
  564. {
  565. await ReadAllDataAsync(CancellationToken.None);
  566. DataUpdated?.Invoke(_data);
  567. }
  568. }
  569. /// <summary>
  570. /// 触发数据更新事件(供客户端模式使用)
  571. /// </summary>
  572. public void RaiseDataUpdated(PlcDataModel data)
  573. {
  574. DataUpdated?.Invoke(data);
  575. }
  576. public void Dispose()
  577. {
  578. if (_disposed) return;
  579. _disposed = true;
  580. Log.Information("PlcPollingService 正在释放...");
  581. Stop();
  582. Log.Information("PlcPollingService 已释放");
  583. }
  584. /// <summary>
  585. /// 健康检查(看门狗调用)
  586. /// </summary>
  587. public Task<(bool IsHealthy, string? Reason)> CheckHealthAsync()
  588. {
  589. // 检查轮询任务是否还在运行
  590. if (_pollingTask == null || _pollingTask.IsCompleted)
  591. {
  592. return Task.FromResult((false, "轮询任务未运行"));
  593. }
  594. // 检查数据更新时间(超过 60 秒无数据视为不健康)
  595. if (_data.LastReadTime > DateTime.MinValue)
  596. {
  597. var elapsed = (DateTime.Now - _data.LastReadTime).TotalSeconds;
  598. if (elapsed > 60)
  599. {
  600. return Task.FromResult((false, $"数据更新超时: {elapsed:F0} 秒"));
  601. }
  602. }
  603. // 检查取消令牌是否已释放
  604. if (_cts == null)
  605. {
  606. return Task.FromResult((false, "轮询服务已停止"));
  607. }
  608. return Task.FromResult((true, "运行正常"));
  609. }
  610. /// <summary>
  611. /// 重启服务(看门狗调用)
  612. /// </summary>
  613. public async Task RestartAsync()
  614. {
  615. Log.Warning("PLC 轮询服务正在重启...");
  616. Stop();
  617. await Task.Delay(1000); // 等待资源释放
  618. Start();
  619. Log.Information("PLC 轮询服务已重启");
  620. }
  621. }