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));
}
}