using System.Collections.Concurrent; using CommunityToolkit.Mvvm.ComponentModel; using YZWater.Core.Services; namespace YZWater.Core.Models; /// /// PLC 数据模型 - 内存数据快照 /// public partial class PlcDataModel : ObservableObject { // ═══════════════════════════════════════════════ // 水箱液位 // ═══════════════════════════════════════════════ [ObservableProperty] private float _tank1Level; [ObservableProperty] private float _tank2Level; [ObservableProperty] private float _tank3Level; [ObservableProperty] private float _tank4Level; // ═══════════════════════════════════════════════ // 流量 // ═══════════════════════════════════════════════ [ObservableProperty] private float _inflowRate; [ObservableProperty] private float _outflowRate; [ObservableProperty] private float _flowDelta; // ═══════════════════════════════════════════════ // 泵状态 (0-4) // ═══════════════════════════════════════════════ [ObservableProperty] private bool _pump1Running; [ObservableProperty] private bool _pump2Running; [ObservableProperty] private bool _pump3Running; [ObservableProperty] private bool _pump4Running; [ObservableProperty] private bool _pump5Running; // 泵故障 [ObservableProperty] private bool _pump1Fault; [ObservableProperty] private bool _pump2Fault; [ObservableProperty] private bool _pump3Fault; [ObservableProperty] private bool _pump4Fault; [ObservableProperty] private bool _pump5Fault; // 泵频率 [ObservableProperty] private float _pump1Freq; [ObservableProperty] private float _pump2Freq; [ObservableProperty] private float _pump3Freq; [ObservableProperty] private float _pump4Freq; [ObservableProperty] private float _pump5Freq; // 泵电流 [ObservableProperty] private float _pump1Current; [ObservableProperty] private float _pump2Current; [ObservableProperty] private float _pump3Current; [ObservableProperty] private float _pump4Current; [ObservableProperty] private float _pump5Current; // ═══════════════════════════════════════════════ // 风机状态 // ═══════════════════════════════════════════════ [ObservableProperty] private bool _fan1Running; [ObservableProperty] private bool _fan2Running; [ObservableProperty] private bool _fan1Fault; [ObservableProperty] private bool _fan2Fault; // ═══════════════════════════════════════════════ // 阀门位置 (0-3, 0=100=全关, 100=全开) // ═══════════════════════════════════════════════ [ObservableProperty] private int _valve1Position; [ObservableProperty] private int _valve2Position; [ObservableProperty] private int _valve3Position; [ObservableProperty] private int _valve4Position; // ═══════════════════════════════════════════════ // 系统状态 // ═══════════════════════════════════════════════ [ObservableProperty] private bool _isAutoMode; // ═══════════════════════════════════════════════ // 数据质量 // ═══════════════════════════════════════════════ [ObservableProperty] private DateTime _lastReadTime; [ObservableProperty] private bool _isDataValid; [ObservableProperty] private bool _isPlcConnected; // ═══════════════════════════════════════════════ // 动态寄存器(由 ProjectProfile 驱动) // ═══════════════════════════════════════════════ /// 模拟量动态寄存器(名称 → 值),由项目档案驱动 public ConcurrentDictionary AnalogRegisters { get; } = new(); /// 数字量动态寄存器(名称 → 值),由项目档案驱动 public ConcurrentDictionary DigitalRegisters { get; } = new(); // ═══════════════════════════════════════════════ // 测点级数据质量(OPC 风格的 Good/Bad) // ═══════════════════════════════════════════════ /// /// 测点质量(名称 → 最近一次读取是否成功)。 /// 读取失败的测点保留上一次有效值,只把质量位置为 Bad, /// 这样单个坏地址不会把整轮数据打成零,也不会有零被当成真实值使用。 /// private readonly ConcurrentDictionary _tagQuality = new(); /// /// 测点质量是否为 Bad(读不到/从未读到)。质量表里没有的键视为非 Bad: /// 那可能是旧版硬编码测点,或客户端模式(服务端只传寄存器值、不传质量表), /// 这种情况下由调用方自己的默认值语义负责。 /// public bool IsTagBad(string key) => _tagQuality.TryGetValue(key, out var readable) && !readable; /// /// 记录测点质量。 /// 返回本次是否发生跳变以及跳变前的质量,调用方据此决定是否需要告警 /// (Good→Bad 才需要通知操作员,Bad→Good 由通信层记恢复日志)。 /// public (bool Changed, bool WasReadable) SetTagQuality(string key, bool readable) { // 先取旧值再写:AddOrUpdate 返回的是"新值",拿它比较永远得不出跳变 var known = _tagQuality.TryGetValue(key, out var previous); _tagQuality[key] = readable; return (known && previous != readable, previous); } /// /// 初始化动态寄存器(根据项目档案创建空条目) /// public void InitializeDynamicRegisters(ProjectProfile profile) { AnalogRegisters.Clear(); DigitalRegisters.Clear(); // 泵相关 foreach (var pump in profile.Pumps) { if (!string.IsNullOrEmpty(pump.CurrentAddress)) AnalogRegisters[$"{pump.Name}.Current"] = 0; if (!string.IsNullOrEmpty(pump.RunAddress)) DigitalRegisters[$"{pump.Name}.Run"] = false; if (!string.IsNullOrEmpty(pump.FaultAddress)) DigitalRegisters[$"{pump.Name}.Fault"] = false; if (!string.IsNullOrEmpty(pump.AutoStateAddress)) DigitalRegisters[$"{pump.Name}.AutoMode"] = false; if (!string.IsNullOrEmpty(pump.DailyHoursAddress)) AnalogRegisters[$"{pump.Name}.DailyHours"] = 0; if (!string.IsNullOrEmpty(pump.DailyMinutesAddress)) AnalogRegisters[$"{pump.Name}.DailyMinutes"] = 0; } // 水箱/水池 foreach (var tank in profile.Tanks) { if (!string.IsNullOrEmpty(tank.LevelAddress)) AnalogRegisters[$"{tank.Name}.Level"] = 0; // 动态启泵液位:数量跟随项目泵数(至少检查 3 个以兼容旧配置) var pumpThresholdCount = Math.Max(3, profile.Pumps.Count); for (int i = 1; i <= pumpThresholdCount; i++) { var addr = tank.GetStartPumpLevelAddress(i); if (!string.IsNullOrEmpty(addr)) AnalogRegisters[$"{tank.Name}.StartPump{i}Level"] = 0; } if (!string.IsNullOrEmpty(tank.StopLevelAddress)) AnalogRegisters[$"{tank.Name}.StopLevel"] = 0; if (!string.IsNullOrEmpty(tank.ProtectionLevelAddress)) AnalogRegisters[$"{tank.Name}.ProtectionLevel"] = 0; } // 仪表 foreach (var gauge in profile.Gauges) { if (!string.IsNullOrEmpty(gauge.ValueAddress)) AnalogRegisters[$"{gauge.Name}.Value"] = 0; if (!string.IsNullOrEmpty(gauge.HighSetAddress)) AnalogRegisters[$"{gauge.Name}.HighSet"] = 0; if (!string.IsNullOrEmpty(gauge.LowSetAddress)) AnalogRegisters[$"{gauge.Name}.LowSet"] = 0; } // 通用传感器 foreach (var sensor in profile.Sensors) { if (!string.IsNullOrEmpty(sensor.Address)) AnalogRegisters[$"{sensor.Name}.Value"] = 0; } // 通用数字量输入(如变频器故障) foreach (var di in profile.DigitalInputs) { if (!string.IsNullOrEmpty(di.Address)) DigitalRegisters[$"DI.{di.Name}"] = false; } // 通用数字量输出(如复位按钮) foreach (var do_ in profile.DigitalOutputs) { if (!string.IsNullOrEmpty(do_.Address)) DigitalRegisters[$"DO.{do_.Name}"] = false; } // 全局中控/屏控:所有泵共用一个地址,读键约定与 PlcPollingService 一致(挂在第一个泵名下)。 // 不登记这个键的话它永远"未登记",IsTagBad 判不出 Bad,首轮就会把默认 false 当成"中控"放行写操作。 if (!string.IsNullOrEmpty(profile.ModeSelectAddress) && profile.Pumps.Count > 0) DigitalRegisters[$"{profile.Pumps[0].Name}.ModeSelect"] = false; // 档案里的测点初始质量为 Bad:读到过一次成功才转 Good。 // 地址填错/被轮询跳过的测点因此一直是 Bad,消费者不会拿零值去做判断。 _tagQuality.Clear(); foreach (var key in AnalogRegisters.Keys) _tagQuality[key] = false; foreach (var key in DigitalRegisters.Keys) _tagQuality[key] = false; } /// 获取动态模拟量寄存器值 public float GetAnalogRegister(string key, float defaultValue = 0) => AnalogRegisters.TryGetValue(key, out var value) ? value : defaultValue; /// 获取动态数字量寄存器值 public bool GetDigitalRegister(string key, bool defaultValue = false) => DigitalRegisters.TryGetValue(key, out var value) ? value : defaultValue; /// /// 复制另一份模型的动态寄存器与测点质量。 /// UI 线程用快照取值,不能直接引用轮询线程正在写入的字典, /// 但档案驱动的数据都在这两个字典里,不复制就等于读到的全是默认零值。 /// public void CopyDynamicFrom(PlcDataModel source) { AnalogRegisters.Clear(); DigitalRegisters.Clear(); _tagQuality.Clear(); foreach (var (key, value) in source.AnalogRegisters) AnalogRegisters[key] = value; foreach (var (key, value) in source.DigitalRegisters) DigitalRegisters[key] = value; foreach (var (key, value) in source._tagQuality) _tagQuality[key] = value; } // ═══════════════════════════════════════════════ // 计算属性 // ═══════════════════════════════════════════════ public int RunningPumpCount => (Pump1Running ? 1 : 0) + (Pump2Running ? 1 : 0) + (Pump3Running ? 1 : 0) + (Pump4Running ? 1 : 0) + (Pump5Running ? 1 : 0); public int RunningFanCount => (Fan1Running ? 1 : 0) + (Fan2Running ? 1 : 0); public bool HasAnyFault => Pump1Fault || Pump2Fault || Pump3Fault || Pump4Fault || Pump5Fault || Fan1Fault || Fan2Fault; /// 数据可用于记录/控制(Mock 模式视为始终有效;读取失败整轮作废时 IsDataValid=false) public bool HasValidData => MockPlcService.IsEnabled || (IsPlcConnected && IsDataValid); /// /// 获取泵运行状态 /// public bool GetPumpRunning(int index) => index switch { 0 => Pump1Running, 1 => Pump2Running, 2 => Pump3Running, 3 => Pump4Running, 4 => Pump5Running, _ => false }; /// /// 获取泵故障状态 /// public bool GetPumpFault(int index) => index switch { 0 => Pump1Fault, 1 => Pump2Fault, 2 => Pump3Fault, 3 => Pump4Fault, 4 => Pump5Fault, _ => false }; /// /// 获取泵频率 /// public float GetPumpFreq(int index) => index switch { 0 => Pump1Freq, 1 => Pump2Freq, 2 => Pump3Freq, 3 => Pump4Freq, 4 => Pump5Freq, _ => 0 }; /// /// 获取泵电流 /// public float GetPumpCurrent(int index) => index switch { 0 => Pump1Current, 1 => Pump2Current, 2 => Pump3Current, 3 => Pump4Current, 4 => Pump5Current, _ => 0 }; }