PlcDataModel.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. using System.Collections.Concurrent;
  2. using CommunityToolkit.Mvvm.ComponentModel;
  3. using YZWater.Core.Services;
  4. namespace YZWater.Core.Models;
  5. /// <summary>
  6. /// PLC 数据模型 - 内存数据快照
  7. /// </summary>
  8. public partial class PlcDataModel : ObservableObject
  9. {
  10. // ═══════════════════════════════════════════════
  11. // 水箱液位
  12. // ═══════════════════════════════════════════════
  13. [ObservableProperty] private float _tank1Level;
  14. [ObservableProperty] private float _tank2Level;
  15. [ObservableProperty] private float _tank3Level;
  16. [ObservableProperty] private float _tank4Level;
  17. // ═══════════════════════════════════════════════
  18. // 流量
  19. // ═══════════════════════════════════════════════
  20. [ObservableProperty] private float _inflowRate;
  21. [ObservableProperty] private float _outflowRate;
  22. [ObservableProperty] private float _flowDelta;
  23. // ═══════════════════════════════════════════════
  24. // 泵状态 (0-4)
  25. // ═══════════════════════════════════════════════
  26. [ObservableProperty] private bool _pump1Running;
  27. [ObservableProperty] private bool _pump2Running;
  28. [ObservableProperty] private bool _pump3Running;
  29. [ObservableProperty] private bool _pump4Running;
  30. [ObservableProperty] private bool _pump5Running;
  31. // 泵故障
  32. [ObservableProperty] private bool _pump1Fault;
  33. [ObservableProperty] private bool _pump2Fault;
  34. [ObservableProperty] private bool _pump3Fault;
  35. [ObservableProperty] private bool _pump4Fault;
  36. [ObservableProperty] private bool _pump5Fault;
  37. // 泵频率
  38. [ObservableProperty] private float _pump1Freq;
  39. [ObservableProperty] private float _pump2Freq;
  40. [ObservableProperty] private float _pump3Freq;
  41. [ObservableProperty] private float _pump4Freq;
  42. [ObservableProperty] private float _pump5Freq;
  43. // 泵电流
  44. [ObservableProperty] private float _pump1Current;
  45. [ObservableProperty] private float _pump2Current;
  46. [ObservableProperty] private float _pump3Current;
  47. [ObservableProperty] private float _pump4Current;
  48. [ObservableProperty] private float _pump5Current;
  49. // ═══════════════════════════════════════════════
  50. // 风机状态
  51. // ═══════════════════════════════════════════════
  52. [ObservableProperty] private bool _fan1Running;
  53. [ObservableProperty] private bool _fan2Running;
  54. [ObservableProperty] private bool _fan1Fault;
  55. [ObservableProperty] private bool _fan2Fault;
  56. // ═══════════════════════════════════════════════
  57. // 阀门位置 (0-3, 0=100=全关, 100=全开)
  58. // ═══════════════════════════════════════════════
  59. [ObservableProperty] private int _valve1Position;
  60. [ObservableProperty] private int _valve2Position;
  61. [ObservableProperty] private int _valve3Position;
  62. [ObservableProperty] private int _valve4Position;
  63. // ═══════════════════════════════════════════════
  64. // 系统状态
  65. // ═══════════════════════════════════════════════
  66. [ObservableProperty] private bool _isAutoMode;
  67. // ═══════════════════════════════════════════════
  68. // 数据质量
  69. // ═══════════════════════════════════════════════
  70. [ObservableProperty] private DateTime _lastReadTime;
  71. [ObservableProperty] private bool _isDataValid;
  72. [ObservableProperty] private bool _isPlcConnected;
  73. // ═══════════════════════════════════════════════
  74. // 动态寄存器(由 ProjectProfile 驱动)
  75. // ═══════════════════════════════════════════════
  76. /// <summary>模拟量动态寄存器(名称 → 值),由项目档案驱动</summary>
  77. public ConcurrentDictionary<string, float> AnalogRegisters { get; } = new();
  78. /// <summary>数字量动态寄存器(名称 → 值),由项目档案驱动</summary>
  79. public ConcurrentDictionary<string, bool> DigitalRegisters { get; } = new();
  80. // ═══════════════════════════════════════════════
  81. // 测点级数据质量(OPC 风格的 Good/Bad)
  82. // ═══════════════════════════════════════════════
  83. /// <summary>
  84. /// 测点质量(名称 → 最近一次读取是否成功)。
  85. /// 读取失败的测点保留上一次有效值,只把质量位置为 Bad,
  86. /// 这样单个坏地址不会把整轮数据打成零,也不会有零被当成真实值使用。
  87. /// </summary>
  88. private readonly ConcurrentDictionary<string, bool> _tagQuality = new();
  89. /// <summary>
  90. /// 测点质量是否为 Bad(读不到/从未读到)。质量表里没有的键视为非 Bad:
  91. /// 那可能是旧版硬编码测点,或客户端模式(服务端只传寄存器值、不传质量表),
  92. /// 这种情况下由调用方自己的默认值语义负责。
  93. /// </summary>
  94. public bool IsTagBad(string key)
  95. => _tagQuality.TryGetValue(key, out var readable) && !readable;
  96. /// <summary>
  97. /// 记录测点质量。
  98. /// 返回本次是否发生跳变以及跳变前的质量,调用方据此决定是否需要告警
  99. /// (Good→Bad 才需要通知操作员,Bad→Good 由通信层记恢复日志)。
  100. /// </summary>
  101. public (bool Changed, bool WasReadable) SetTagQuality(string key, bool readable)
  102. {
  103. // 先取旧值再写:AddOrUpdate 返回的是"新值",拿它比较永远得不出跳变
  104. var known = _tagQuality.TryGetValue(key, out var previous);
  105. _tagQuality[key] = readable;
  106. return (known && previous != readable, previous);
  107. }
  108. /// <summary>
  109. /// 初始化动态寄存器(根据项目档案创建空条目)
  110. /// </summary>
  111. public void InitializeDynamicRegisters(ProjectProfile profile)
  112. {
  113. AnalogRegisters.Clear();
  114. DigitalRegisters.Clear();
  115. // 泵相关
  116. foreach (var pump in profile.Pumps)
  117. {
  118. if (!string.IsNullOrEmpty(pump.CurrentAddress))
  119. AnalogRegisters[$"{pump.Name}.Current"] = 0;
  120. if (!string.IsNullOrEmpty(pump.RunAddress))
  121. DigitalRegisters[$"{pump.Name}.Run"] = false;
  122. if (!string.IsNullOrEmpty(pump.FaultAddress))
  123. DigitalRegisters[$"{pump.Name}.Fault"] = false;
  124. if (!string.IsNullOrEmpty(pump.AutoStateAddress))
  125. DigitalRegisters[$"{pump.Name}.AutoMode"] = false;
  126. if (!string.IsNullOrEmpty(pump.DailyHoursAddress))
  127. AnalogRegisters[$"{pump.Name}.DailyHours"] = 0;
  128. if (!string.IsNullOrEmpty(pump.DailyMinutesAddress))
  129. AnalogRegisters[$"{pump.Name}.DailyMinutes"] = 0;
  130. }
  131. // 水箱/水池
  132. foreach (var tank in profile.Tanks)
  133. {
  134. if (!string.IsNullOrEmpty(tank.LevelAddress))
  135. AnalogRegisters[$"{tank.Name}.Level"] = 0;
  136. // 动态启泵液位:数量跟随项目泵数(至少检查 3 个以兼容旧配置)
  137. var pumpThresholdCount = Math.Max(3, profile.Pumps.Count);
  138. for (int i = 1; i <= pumpThresholdCount; i++)
  139. {
  140. var addr = tank.GetStartPumpLevelAddress(i);
  141. if (!string.IsNullOrEmpty(addr))
  142. AnalogRegisters[$"{tank.Name}.StartPump{i}Level"] = 0;
  143. }
  144. if (!string.IsNullOrEmpty(tank.StopLevelAddress))
  145. AnalogRegisters[$"{tank.Name}.StopLevel"] = 0;
  146. if (!string.IsNullOrEmpty(tank.ProtectionLevelAddress))
  147. AnalogRegisters[$"{tank.Name}.ProtectionLevel"] = 0;
  148. }
  149. // 仪表
  150. foreach (var gauge in profile.Gauges)
  151. {
  152. if (!string.IsNullOrEmpty(gauge.ValueAddress))
  153. AnalogRegisters[$"{gauge.Name}.Value"] = 0;
  154. if (!string.IsNullOrEmpty(gauge.HighSetAddress))
  155. AnalogRegisters[$"{gauge.Name}.HighSet"] = 0;
  156. if (!string.IsNullOrEmpty(gauge.LowSetAddress))
  157. AnalogRegisters[$"{gauge.Name}.LowSet"] = 0;
  158. }
  159. // 通用传感器
  160. foreach (var sensor in profile.Sensors)
  161. {
  162. if (!string.IsNullOrEmpty(sensor.Address))
  163. AnalogRegisters[$"{sensor.Name}.Value"] = 0;
  164. }
  165. // 通用数字量输入(如变频器故障)
  166. foreach (var di in profile.DigitalInputs)
  167. {
  168. if (!string.IsNullOrEmpty(di.Address))
  169. DigitalRegisters[$"DI.{di.Name}"] = false;
  170. }
  171. // 通用数字量输出(如复位按钮)
  172. foreach (var do_ in profile.DigitalOutputs)
  173. {
  174. if (!string.IsNullOrEmpty(do_.Address))
  175. DigitalRegisters[$"DO.{do_.Name}"] = false;
  176. }
  177. // 全局中控/屏控:所有泵共用一个地址,读键约定与 PlcPollingService 一致(挂在第一个泵名下)。
  178. // 不登记这个键的话它永远"未登记",IsTagBad 判不出 Bad,首轮就会把默认 false 当成"中控"放行写操作。
  179. if (!string.IsNullOrEmpty(profile.ModeSelectAddress) && profile.Pumps.Count > 0)
  180. DigitalRegisters[$"{profile.Pumps[0].Name}.ModeSelect"] = false;
  181. // 档案里的测点初始质量为 Bad:读到过一次成功才转 Good。
  182. // 地址填错/被轮询跳过的测点因此一直是 Bad,消费者不会拿零值去做判断。
  183. _tagQuality.Clear();
  184. foreach (var key in AnalogRegisters.Keys) _tagQuality[key] = false;
  185. foreach (var key in DigitalRegisters.Keys) _tagQuality[key] = false;
  186. }
  187. /// <summary>获取动态模拟量寄存器值</summary>
  188. public float GetAnalogRegister(string key, float defaultValue = 0)
  189. => AnalogRegisters.TryGetValue(key, out var value) ? value : defaultValue;
  190. /// <summary>获取动态数字量寄存器值</summary>
  191. public bool GetDigitalRegister(string key, bool defaultValue = false)
  192. => DigitalRegisters.TryGetValue(key, out var value) ? value : defaultValue;
  193. /// <summary>
  194. /// 复制另一份模型的动态寄存器与测点质量。
  195. /// UI 线程用快照取值,不能直接引用轮询线程正在写入的字典,
  196. /// 但档案驱动的数据都在这两个字典里,不复制就等于读到的全是默认零值。
  197. /// </summary>
  198. public void CopyDynamicFrom(PlcDataModel source)
  199. {
  200. AnalogRegisters.Clear();
  201. DigitalRegisters.Clear();
  202. _tagQuality.Clear();
  203. foreach (var (key, value) in source.AnalogRegisters) AnalogRegisters[key] = value;
  204. foreach (var (key, value) in source.DigitalRegisters) DigitalRegisters[key] = value;
  205. foreach (var (key, value) in source._tagQuality) _tagQuality[key] = value;
  206. }
  207. // ═══════════════════════════════════════════════
  208. // 计算属性
  209. // ═══════════════════════════════════════════════
  210. public int RunningPumpCount => (Pump1Running ? 1 : 0) + (Pump2Running ? 1 : 0) +
  211. (Pump3Running ? 1 : 0) + (Pump4Running ? 1 : 0) +
  212. (Pump5Running ? 1 : 0);
  213. public int RunningFanCount => (Fan1Running ? 1 : 0) + (Fan2Running ? 1 : 0);
  214. public bool HasAnyFault => Pump1Fault || Pump2Fault || Pump3Fault || Pump4Fault || Pump5Fault ||
  215. Fan1Fault || Fan2Fault;
  216. /// <summary>数据可用于记录/控制(Mock 模式视为始终有效;读取失败整轮作废时 IsDataValid=false)</summary>
  217. public bool HasValidData => MockPlcService.IsEnabled || (IsPlcConnected && IsDataValid);
  218. /// <summary>
  219. /// 获取泵运行状态
  220. /// </summary>
  221. public bool GetPumpRunning(int index) => index switch
  222. {
  223. 0 => Pump1Running,
  224. 1 => Pump2Running,
  225. 2 => Pump3Running,
  226. 3 => Pump4Running,
  227. 4 => Pump5Running,
  228. _ => false
  229. };
  230. /// <summary>
  231. /// 获取泵故障状态
  232. /// </summary>
  233. public bool GetPumpFault(int index) => index switch
  234. {
  235. 0 => Pump1Fault,
  236. 1 => Pump2Fault,
  237. 2 => Pump3Fault,
  238. 3 => Pump4Fault,
  239. 4 => Pump5Fault,
  240. _ => false
  241. };
  242. /// <summary>
  243. /// 获取泵频率
  244. /// </summary>
  245. public float GetPumpFreq(int index) => index switch
  246. {
  247. 0 => Pump1Freq,
  248. 1 => Pump2Freq,
  249. 2 => Pump3Freq,
  250. 3 => Pump4Freq,
  251. 4 => Pump5Freq,
  252. _ => 0
  253. };
  254. /// <summary>
  255. /// 获取泵电流
  256. /// </summary>
  257. public float GetPumpCurrent(int index) => index switch
  258. {
  259. 0 => Pump1Current,
  260. 1 => Pump2Current,
  261. 2 => Pump3Current,
  262. 3 => Pump4Current,
  263. 4 => Pump5Current,
  264. _ => 0
  265. };
  266. }