| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- using System.Collections.Concurrent;
- using CommunityToolkit.Mvvm.ComponentModel;
- using YZWater.Core.Services;
- namespace YZWater.Core.Models;
- /// <summary>
- /// PLC 数据模型 - 内存数据快照
- /// </summary>
- 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 驱动)
- // ═══════════════════════════════════════════════
- /// <summary>模拟量动态寄存器(名称 → 值),由项目档案驱动</summary>
- public ConcurrentDictionary<string, float> AnalogRegisters { get; } = new();
- /// <summary>数字量动态寄存器(名称 → 值),由项目档案驱动</summary>
- public ConcurrentDictionary<string, bool> DigitalRegisters { get; } = new();
- // ═══════════════════════════════════════════════
- // 测点级数据质量(OPC 风格的 Good/Bad)
- // ═══════════════════════════════════════════════
- /// <summary>
- /// 测点质量(名称 → 最近一次读取是否成功)。
- /// 读取失败的测点保留上一次有效值,只把质量位置为 Bad,
- /// 这样单个坏地址不会把整轮数据打成零,也不会有零被当成真实值使用。
- /// </summary>
- private readonly ConcurrentDictionary<string, bool> _tagQuality = new();
- /// <summary>
- /// 测点质量是否为 Bad(读不到/从未读到)。质量表里没有的键视为非 Bad:
- /// 那可能是旧版硬编码测点,或客户端模式(服务端只传寄存器值、不传质量表),
- /// 这种情况下由调用方自己的默认值语义负责。
- /// </summary>
- public bool IsTagBad(string key)
- => _tagQuality.TryGetValue(key, out var readable) && !readable;
- /// <summary>
- /// 记录测点质量。
- /// 返回本次是否发生跳变以及跳变前的质量,调用方据此决定是否需要告警
- /// (Good→Bad 才需要通知操作员,Bad→Good 由通信层记恢复日志)。
- /// </summary>
- 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);
- }
- /// <summary>
- /// 初始化动态寄存器(根据项目档案创建空条目)
- /// </summary>
- 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;
- }
- /// <summary>获取动态模拟量寄存器值</summary>
- public float GetAnalogRegister(string key, float defaultValue = 0)
- => AnalogRegisters.TryGetValue(key, out var value) ? value : defaultValue;
- /// <summary>获取动态数字量寄存器值</summary>
- public bool GetDigitalRegister(string key, bool defaultValue = false)
- => DigitalRegisters.TryGetValue(key, out var value) ? value : defaultValue;
- /// <summary>
- /// 复制另一份模型的动态寄存器与测点质量。
- /// UI 线程用快照取值,不能直接引用轮询线程正在写入的字典,
- /// 但档案驱动的数据都在这两个字典里,不复制就等于读到的全是默认零值。
- /// </summary>
- 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;
- /// <summary>数据可用于记录/控制(Mock 模式视为始终有效;读取失败整轮作废时 IsDataValid=false)</summary>
- public bool HasValidData => MockPlcService.IsEnabled || (IsPlcConnected && IsDataValid);
- /// <summary>
- /// 获取泵运行状态
- /// </summary>
- public bool GetPumpRunning(int index) => index switch
- {
- 0 => Pump1Running,
- 1 => Pump2Running,
- 2 => Pump3Running,
- 3 => Pump4Running,
- 4 => Pump5Running,
- _ => false
- };
- /// <summary>
- /// 获取泵故障状态
- /// </summary>
- public bool GetPumpFault(int index) => index switch
- {
- 0 => Pump1Fault,
- 1 => Pump2Fault,
- 2 => Pump3Fault,
- 3 => Pump4Fault,
- 4 => Pump5Fault,
- _ => false
- };
- /// <summary>
- /// 获取泵频率
- /// </summary>
- public float GetPumpFreq(int index) => index switch
- {
- 0 => Pump1Freq,
- 1 => Pump2Freq,
- 2 => Pump3Freq,
- 3 => Pump4Freq,
- 4 => Pump5Freq,
- _ => 0
- };
- /// <summary>
- /// 获取泵电流
- /// </summary>
- public float GetPumpCurrent(int index) => index switch
- {
- 0 => Pump1Current,
- 1 => Pump2Current,
- 2 => Pump3Current,
- 3 => Pump4Current,
- 4 => Pump5Current,
- _ => 0
- };
- }
|