MockPlcService.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. using Serilog;
  2. using YZWater.Core.Models;
  3. namespace YZWater.Core.Services;
  4. /// <summary>
  5. /// Mock PLC 服务 - 无真实 PLC 时生成模拟数据用于测试
  6. /// </summary>
  7. public static class MockPlcService
  8. {
  9. private static readonly Random Rng = new();
  10. private static bool _enabled;
  11. // 模拟状态
  12. private static float _tank1 = 50, _tank2 = 60, _tank3 = 45, _tank4 = 55;
  13. private static float _inflow = 30, _outflow = 28;
  14. private static bool _pump1 = true, _pump2 = false, _pump3 = true, _pump4 = false, _pump5 = false;
  15. private static bool _fan1 = true, _fan2 = false;
  16. private static float _pump1Freq = 45, _pump2Freq = 0, _pump3Freq = 40, _pump4Freq = 0, _pump5Freq = 0;
  17. private static bool _autoMode = true;
  18. // 项目化模拟用的故障状态(1# 泵永远不故障,2#/3# 偶尔故障演示跳过逻辑)
  19. private static bool _pump2Fault, _pump3Fault;
  20. /// <summary>
  21. /// 是否启用 Mock 模式
  22. /// </summary>
  23. public static bool IsEnabled => _enabled;
  24. /// <summary>
  25. /// 启用 Mock 模式(替代真实 PLC 连接)
  26. /// </summary>
  27. public static void Enable()
  28. {
  29. _enabled = true;
  30. Log.Information("Mock PLC 数据源已启用");
  31. }
  32. /// <summary>
  33. /// 生成一帧模拟数据并更新 PlcDataModel
  34. /// </summary>
  35. public static void UpdateData(PlcDataModel data)
  36. {
  37. if (!_enabled) return;
  38. // 模拟液位波动
  39. _tank1 = Clamp(_tank1 + RandomDelta(2f), 0, 100);
  40. _tank2 = Clamp(_tank2 + RandomDelta(1.5f), 0, 100);
  41. _tank3 = Clamp(_tank3 + RandomDelta(1f), 0, 100);
  42. _tank4 = Clamp(_tank4 + RandomDelta(1.5f), 0, 100);
  43. // 模拟流量波动
  44. _inflow = Clamp(_inflow + RandomDelta(3f), 0, 200);
  45. _outflow = Clamp(_outflow + RandomDelta(2.5f), 0, 200);
  46. // 模拟泵频率波动
  47. if (_pump1) _pump1Freq = Clamp(_pump1Freq + RandomDelta(1f), 0, 50);
  48. if (_pump3) _pump3Freq = Clamp(_pump3Freq + RandomDelta(1f), 0, 50);
  49. // 按液位逻辑自动控制泵启停(按编号顺序:1# → 2# → 3#)
  50. // 阈值对应模拟归一化后的液位 0~10m
  51. var stopLevel = 3.5f;
  52. var startPump1 = 5.5f;
  53. var startPump2 = 7.0f;
  54. var startPump3 = 8.5f;
  55. var level = _tank1 / 10f; // 归一化 0~10m
  56. if (level < stopLevel)
  57. {
  58. _pump1 = _pump2 = _pump3 = false;
  59. }
  60. else if (level >= startPump3)
  61. {
  62. _pump1 = _pump2 = _pump3 = true;
  63. }
  64. else if (level >= startPump2)
  65. {
  66. _pump1 = _pump2 = true;
  67. _pump3 = false;
  68. }
  69. else if (level >= startPump1)
  70. {
  71. _pump1 = true;
  72. _pump2 = _pump3 = false;
  73. }
  74. // 偶尔随机故障(演示故障跳过逻辑)
  75. if (Rng.NextDouble() < 0.0005) _pump2Fault = !_pump2Fault;
  76. if (Rng.NextDouble() < 0.0003) _pump3Fault = !_pump3Fault;
  77. // 更新数据模型
  78. data.Tank1Level = _tank1;
  79. data.Tank2Level = _tank2;
  80. data.Tank3Level = _tank3;
  81. data.Tank4Level = _tank4;
  82. data.InflowRate = _inflow;
  83. data.OutflowRate = _outflow;
  84. data.FlowDelta = _inflow - _outflow;
  85. data.Pump1Running = _pump1;
  86. data.Pump2Running = _pump2;
  87. data.Pump3Running = _pump3;
  88. data.Pump4Running = _pump4;
  89. data.Pump5Running = _pump5;
  90. data.Pump1Fault = false;
  91. data.Pump2Fault = _pump2Fault;
  92. data.Pump3Fault = _pump3Fault;
  93. data.Pump4Fault = false;
  94. data.Pump5Fault = false;
  95. data.Pump1Freq = _pump1Freq;
  96. data.Pump2Freq = _pump2Freq;
  97. data.Pump3Freq = _pump3Freq;
  98. data.Pump4Freq = _pump4Freq;
  99. data.Pump5Freq = _pump5Freq;
  100. data.Fan1Running = _fan1;
  101. data.Fan2Running = _fan2;
  102. data.Fan1Fault = false;
  103. data.Fan2Fault = false;
  104. data.IsAutoMode = _autoMode;
  105. data.LastReadTime = DateTime.Now;
  106. data.IsDataValid = true;
  107. data.IsPlcConnected = true;
  108. // 同步到项目化动态寄存器(ProjectViewModel 通过这些键读数据)
  109. SyncDynamicRegisters(data);
  110. }
  111. /// <summary>
  112. /// 把模拟数据同步到项目化动态寄存器(AnalogRegisters / DigitalRegisters)。
  113. /// 按键名后缀模式匹配:.Level / .Current / .Value / .DailyHours 等。
  114. /// 阈值类寄存器(.StartPumpNLevel / .StopLevel / .ProtectionLevel)保留合理默认值,
  115. /// 保证项目化视图(如泵站首页 sparkline、负荷条等)在 Mock 模式下也能看到变化。
  116. /// </summary>
  117. private static void SyncDynamicRegisters(PlcDataModel data)
  118. {
  119. if (data.AnalogRegisters.Count == 0 && data.DigitalRegisters.Count == 0) return;
  120. // 模拟量寄存器:按 key 后缀匹配,填充模拟值
  121. foreach (var key in data.AnalogRegisters.Keys)
  122. {
  123. data.AnalogRegisters[key] = SimulateAnalog(key);
  124. }
  125. // 数字量寄存器:按 key 后缀匹配
  126. foreach (var key in data.DigitalRegisters.Keys)
  127. {
  128. data.DigitalRegisters[key] = SimulateDigital(key);
  129. }
  130. }
  131. private static float SimulateAnalog(string key)
  132. {
  133. // ── 液位(所有 tank 的 .Level 都统一用 Tank1 模拟,
  134. // 对于单水箱项目如扬杰二厂刚好一一对应)──
  135. if (key.EndsWith(".Level")) return _tank1 / 10f; // 归一到 0~10m
  136. // ── 阈值寄存器:使用合理默认值(不覆盖配置)──
  137. if (key.EndsWith(".StartPump1Level")) return 5.5f;
  138. if (key.EndsWith(".StartPump2Level")) return 7.0f;
  139. if (key.EndsWith(".StartPump3Level")) return 8.5f;
  140. if (key.EndsWith(".StopLevel")) return 3.5f;
  141. if (key.EndsWith(".ProtectionLevel")) return 9.5f;
  142. // ── 泵电流:运行时 ~22A(接近额定 25A),停止时 0 ──
  143. if (key.EndsWith(".Current"))
  144. {
  145. var n = ExtractPumpIndex(key);
  146. return n switch
  147. {
  148. 1 => _pump1 ? 22f + RandomDelta(0.5f) : 0f,
  149. 2 => _pump2 ? 23f + RandomDelta(0.5f) : 0f,
  150. 3 => _pump3 ? 21f + RandomDelta(0.5f) : 0f,
  151. 4 => _pump4 ? 20f + RandomDelta(0.5f) : 0f,
  152. 5 => _pump5 ? 19f + RandomDelta(0.5f) : 0f,
  153. _ => 0f
  154. };
  155. }
  156. // ── 日台时(静态演示值,避免每帧变化让 UI 抖动)──
  157. if (key.EndsWith(".DailyHours")) return 4;
  158. if (key.EndsWith(".DailyMinutes")) return 30;
  159. // ── 仪表(按单位匹配)──
  160. if (key.EndsWith(".Value"))
  161. {
  162. if (key.StartsWith("压力")) return Clamp(_outflow * 0.003f + 0.25f + RandomDelta(0.02f), 0f, 1.6f);
  163. if (key.StartsWith("流量")) return Clamp(_inflow, 0f, 200f);
  164. return 0;
  165. }
  166. return 0;
  167. }
  168. private static bool SimulateDigital(string key)
  169. {
  170. if (key.EndsWith(".Run"))
  171. {
  172. var n = ExtractPumpIndex(key);
  173. return n switch
  174. {
  175. 1 => _pump1,
  176. 2 => _pump2,
  177. 3 => _pump3,
  178. 4 => _pump4,
  179. 5 => _pump5,
  180. _ => false
  181. };
  182. }
  183. if (key.EndsWith(".Fault"))
  184. {
  185. var n = ExtractPumpIndex(key);
  186. return n switch
  187. {
  188. 1 => false, // 1# 泵永远不故障(主泵)
  189. 2 => _pump2Fault, // 2# 偶尔故障
  190. 3 => _pump3Fault, // 3# 偶尔故障
  191. _ => false
  192. };
  193. }
  194. if (key.EndsWith(".AutoMode")) return _autoMode;
  195. if (key.EndsWith(".ModeSelect")) return true; // 1=屏控(默认屏控,上位机不可操作)
  196. if (key.StartsWith("DI.")) return false; // 默认 DI 正常
  197. return false;
  198. }
  199. /// <summary>从 "1#泵.Run" 之类的 key 中抽出泵编号(1..5)</summary>
  200. private static int ExtractPumpIndex(string key)
  201. {
  202. // 匹配 "1#..." / "2#..." 等前缀
  203. if (key.Length >= 2 && key[0] >= '1' && key[0] <= '5' && key[1] == '#')
  204. return key[0] - '0';
  205. return 0;
  206. }
  207. /// <summary>
  208. /// Mock 写入操作(总是成功)
  209. /// </summary>
  210. public static Task<bool> WriteBoolAsync(string address, bool value)
  211. {
  212. Log.Debug("Mock 写入: {Address} = {Value}", address, value);
  213. // 模拟泵启停
  214. if (address == PlcConfig.Pump1Run) _pump1 = value;
  215. else if (address == PlcConfig.Pump2Run) _pump2 = value;
  216. else if (address == PlcConfig.Pump3Run) _pump3 = value;
  217. else if (address == PlcConfig.Pump4Run) _pump4 = value;
  218. else if (address == PlcConfig.Pump5Run) _pump5 = value;
  219. else if (address == PlcConfig.Fan1Run) _fan1 = value;
  220. else if (address == PlcConfig.Fan2Run) _fan2 = value;
  221. return Task.FromResult(true);
  222. }
  223. public static Task<bool> WriteFloatAsync(string address, float value)
  224. {
  225. Log.Debug("Mock 写入: {Address} = {Value}", address, value);
  226. return Task.FromResult(true);
  227. }
  228. private static float RandomDelta(float maxDelta)
  229. {
  230. return (float)(Rng.NextDouble() * 2 - 1) * maxDelta;
  231. }
  232. private static float Clamp(float value, float min, float max)
  233. {
  234. return Math.Max(min, Math.Min(max, value));
  235. }
  236. }