using Serilog; using YZWater.Core.Models; namespace YZWater.Core.Services; /// /// Mock PLC 服务 - 无真实 PLC 时生成模拟数据用于测试 /// public static class MockPlcService { private static readonly Random Rng = new(); private static bool _enabled; // 模拟状态 private static float _tank1 = 50, _tank2 = 60, _tank3 = 45, _tank4 = 55; private static float _inflow = 30, _outflow = 28; private static bool _pump1 = true, _pump2 = false, _pump3 = true, _pump4 = false, _pump5 = false; private static bool _fan1 = true, _fan2 = false; private static float _pump1Freq = 45, _pump2Freq = 0, _pump3Freq = 40, _pump4Freq = 0, _pump5Freq = 0; private static bool _autoMode = true; // 项目化模拟用的故障状态(1# 泵永远不故障,2#/3# 偶尔故障演示跳过逻辑) private static bool _pump2Fault, _pump3Fault; /// /// 是否启用 Mock 模式 /// public static bool IsEnabled => _enabled; /// /// 启用 Mock 模式(替代真实 PLC 连接) /// public static void Enable() { _enabled = true; Log.Information("Mock PLC 数据源已启用"); } /// /// 生成一帧模拟数据并更新 PlcDataModel /// public static void UpdateData(PlcDataModel data) { if (!_enabled) return; // 模拟液位波动 _tank1 = Clamp(_tank1 + RandomDelta(2f), 0, 100); _tank2 = Clamp(_tank2 + RandomDelta(1.5f), 0, 100); _tank3 = Clamp(_tank3 + RandomDelta(1f), 0, 100); _tank4 = Clamp(_tank4 + RandomDelta(1.5f), 0, 100); // 模拟流量波动 _inflow = Clamp(_inflow + RandomDelta(3f), 0, 200); _outflow = Clamp(_outflow + RandomDelta(2.5f), 0, 200); // 模拟泵频率波动 if (_pump1) _pump1Freq = Clamp(_pump1Freq + RandomDelta(1f), 0, 50); if (_pump3) _pump3Freq = Clamp(_pump3Freq + RandomDelta(1f), 0, 50); // 按液位逻辑自动控制泵启停(按编号顺序:1# → 2# → 3#) // 阈值对应模拟归一化后的液位 0~10m var stopLevel = 3.5f; var startPump1 = 5.5f; var startPump2 = 7.0f; var startPump3 = 8.5f; var level = _tank1 / 10f; // 归一化 0~10m if (level < stopLevel) { _pump1 = _pump2 = _pump3 = false; } else if (level >= startPump3) { _pump1 = _pump2 = _pump3 = true; } else if (level >= startPump2) { _pump1 = _pump2 = true; _pump3 = false; } else if (level >= startPump1) { _pump1 = true; _pump2 = _pump3 = false; } // 偶尔随机故障(演示故障跳过逻辑) if (Rng.NextDouble() < 0.0005) _pump2Fault = !_pump2Fault; if (Rng.NextDouble() < 0.0003) _pump3Fault = !_pump3Fault; // 更新数据模型 data.Tank1Level = _tank1; data.Tank2Level = _tank2; data.Tank3Level = _tank3; data.Tank4Level = _tank4; data.InflowRate = _inflow; data.OutflowRate = _outflow; data.FlowDelta = _inflow - _outflow; data.Pump1Running = _pump1; data.Pump2Running = _pump2; data.Pump3Running = _pump3; data.Pump4Running = _pump4; data.Pump5Running = _pump5; data.Pump1Fault = false; data.Pump2Fault = _pump2Fault; data.Pump3Fault = _pump3Fault; data.Pump4Fault = false; data.Pump5Fault = false; data.Pump1Freq = _pump1Freq; data.Pump2Freq = _pump2Freq; data.Pump3Freq = _pump3Freq; data.Pump4Freq = _pump4Freq; data.Pump5Freq = _pump5Freq; data.Fan1Running = _fan1; data.Fan2Running = _fan2; data.Fan1Fault = false; data.Fan2Fault = false; data.IsAutoMode = _autoMode; data.LastReadTime = DateTime.Now; data.IsDataValid = true; data.IsPlcConnected = true; // 同步到项目化动态寄存器(ProjectViewModel 通过这些键读数据) SyncDynamicRegisters(data); } /// /// 把模拟数据同步到项目化动态寄存器(AnalogRegisters / DigitalRegisters)。 /// 按键名后缀模式匹配:.Level / .Current / .Value / .DailyHours 等。 /// 阈值类寄存器(.StartPumpNLevel / .StopLevel / .ProtectionLevel)保留合理默认值, /// 保证项目化视图(如泵站首页 sparkline、负荷条等)在 Mock 模式下也能看到变化。 /// private static void SyncDynamicRegisters(PlcDataModel data) { if (data.AnalogRegisters.Count == 0 && data.DigitalRegisters.Count == 0) return; // 模拟量寄存器:按 key 后缀匹配,填充模拟值 foreach (var key in data.AnalogRegisters.Keys) { data.AnalogRegisters[key] = SimulateAnalog(key); } // 数字量寄存器:按 key 后缀匹配 foreach (var key in data.DigitalRegisters.Keys) { data.DigitalRegisters[key] = SimulateDigital(key); } } private static float SimulateAnalog(string key) { // ── 液位(所有 tank 的 .Level 都统一用 Tank1 模拟, // 对于单水箱项目如扬杰二厂刚好一一对应)── if (key.EndsWith(".Level")) return _tank1 / 10f; // 归一到 0~10m // ── 阈值寄存器:使用合理默认值(不覆盖配置)── if (key.EndsWith(".StartPump1Level")) return 5.5f; if (key.EndsWith(".StartPump2Level")) return 7.0f; if (key.EndsWith(".StartPump3Level")) return 8.5f; if (key.EndsWith(".StopLevel")) return 3.5f; if (key.EndsWith(".ProtectionLevel")) return 9.5f; // ── 泵电流:运行时 ~22A(接近额定 25A),停止时 0 ── if (key.EndsWith(".Current")) { var n = ExtractPumpIndex(key); return n switch { 1 => _pump1 ? 22f + RandomDelta(0.5f) : 0f, 2 => _pump2 ? 23f + RandomDelta(0.5f) : 0f, 3 => _pump3 ? 21f + RandomDelta(0.5f) : 0f, 4 => _pump4 ? 20f + RandomDelta(0.5f) : 0f, 5 => _pump5 ? 19f + RandomDelta(0.5f) : 0f, _ => 0f }; } // ── 日台时(静态演示值,避免每帧变化让 UI 抖动)── if (key.EndsWith(".DailyHours")) return 4; if (key.EndsWith(".DailyMinutes")) return 30; // ── 仪表(按单位匹配)── if (key.EndsWith(".Value")) { if (key.StartsWith("压力")) return Clamp(_outflow * 0.003f + 0.25f + RandomDelta(0.02f), 0f, 1.6f); if (key.StartsWith("流量")) return Clamp(_inflow, 0f, 200f); return 0; } return 0; } private static bool SimulateDigital(string key) { if (key.EndsWith(".Run")) { var n = ExtractPumpIndex(key); return n switch { 1 => _pump1, 2 => _pump2, 3 => _pump3, 4 => _pump4, 5 => _pump5, _ => false }; } if (key.EndsWith(".Fault")) { var n = ExtractPumpIndex(key); return n switch { 1 => false, // 1# 泵永远不故障(主泵) 2 => _pump2Fault, // 2# 偶尔故障 3 => _pump3Fault, // 3# 偶尔故障 _ => false }; } if (key.EndsWith(".AutoMode")) return _autoMode; if (key.EndsWith(".ModeSelect")) return true; // 1=屏控(默认屏控,上位机不可操作) if (key.StartsWith("DI.")) return false; // 默认 DI 正常 return false; } /// 从 "1#泵.Run" 之类的 key 中抽出泵编号(1..5) private static int ExtractPumpIndex(string key) { // 匹配 "1#..." / "2#..." 等前缀 if (key.Length >= 2 && key[0] >= '1' && key[0] <= '5' && key[1] == '#') return key[0] - '0'; return 0; } /// /// Mock 写入操作(总是成功) /// public static Task WriteBoolAsync(string address, bool value) { Log.Debug("Mock 写入: {Address} = {Value}", address, value); // 模拟泵启停 if (address == PlcConfig.Pump1Run) _pump1 = value; else if (address == PlcConfig.Pump2Run) _pump2 = value; else if (address == PlcConfig.Pump3Run) _pump3 = value; else if (address == PlcConfig.Pump4Run) _pump4 = value; else if (address == PlcConfig.Pump5Run) _pump5 = value; else if (address == PlcConfig.Fan1Run) _fan1 = value; else if (address == PlcConfig.Fan2Run) _fan2 = value; return Task.FromResult(true); } public static Task WriteFloatAsync(string address, float value) { Log.Debug("Mock 写入: {Address} = {Value}", address, value); return Task.FromResult(true); } private static float RandomDelta(float maxDelta) { return (float)(Rng.NextDouble() * 2 - 1) * maxDelta; } private static float Clamp(float value, float min, float max) { return Math.Max(min, Math.Min(max, value)); } }