|
@@ -51,6 +51,13 @@ public partial class ProjectViewModel : ObservableObject, IDisposable
|
|
|
/// <summary>是否有建议启动的泵</summary>
|
|
/// <summary>是否有建议启动的泵</summary>
|
|
|
public bool HasSuggestedPump => !string.IsNullOrEmpty(SuggestedPumpName);
|
|
public bool HasSuggestedPump => !string.IsNullOrEmpty(SuggestedPumpName);
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 故障状态区域是否有内容可显示
|
|
|
|
|
+ /// (有泵 / 有数字量输入 / 有数字量输出 / 有活跃报警 → 显示,否则隐藏空面板)
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public bool HasFaultContent =>
|
|
|
|
|
+ Pumps.Count > 0 || DigitalInputs.Count > 0 || DigitalOutputs.Count > 0 || HasActiveAlarm;
|
|
|
|
|
+
|
|
|
// ─── 动态 2D/3D 控件绑定用字典 ───
|
|
// ─── 动态 2D/3D 控件绑定用字典 ───
|
|
|
|
|
|
|
|
/// <summary>项目档案(供动态布局引擎使用)</summary>
|
|
/// <summary>项目档案(供动态布局引擎使用)</summary>
|
|
@@ -81,7 +88,11 @@ public partial class ProjectViewModel : ObservableObject, IDisposable
|
|
|
{
|
|
{
|
|
|
var operatorName = AuthService.CurrentUser?.DisplayName ?? "Operator";
|
|
var operatorName = AuthService.CurrentUser?.DisplayName ?? "Operator";
|
|
|
await AlarmService.Instance.ResetAllAlarmsAsync(operatorName);
|
|
await AlarmService.Instance.ResetAllAlarmsAsync(operatorName);
|
|
|
- RunOnUi(() => RecentAlarms.Clear());
|
|
|
|
|
|
|
+ RunOnUi(() =>
|
|
|
|
|
+ {
|
|
|
|
|
+ RecentAlarms.Clear();
|
|
|
|
|
+ OnPropertyChanged(nameof(HasFaultContent));
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -133,7 +144,7 @@ public partial class ProjectViewModel : ObservableObject, IDisposable
|
|
|
Pumps.Add(new PumpItemViewModel(pump));
|
|
Pumps.Add(new PumpItemViewModel(pump));
|
|
|
|
|
|
|
|
foreach (var tank in profile.Tanks)
|
|
foreach (var tank in profile.Tanks)
|
|
|
- Tanks.Add(new TankItemViewModel(tank));
|
|
|
|
|
|
|
+ Tanks.Add(new TankItemViewModel(tank, profile.Pumps.Count));
|
|
|
|
|
|
|
|
foreach (var gauge in profile.Gauges)
|
|
foreach (var gauge in profile.Gauges)
|
|
|
Gauges.Add(new GaugeItemViewModel(gauge));
|
|
Gauges.Add(new GaugeItemViewModel(gauge));
|
|
@@ -190,6 +201,7 @@ public partial class ProjectViewModel : ObservableObject, IDisposable
|
|
|
RecentAlarms.Insert(0, record);
|
|
RecentAlarms.Insert(0, record);
|
|
|
while (RecentAlarms.Count > 6)
|
|
while (RecentAlarms.Count > 6)
|
|
|
RecentAlarms.RemoveAt(RecentAlarms.Count - 1);
|
|
RecentAlarms.RemoveAt(RecentAlarms.Count - 1);
|
|
|
|
|
+ OnPropertyChanged(nameof(HasFaultContent));
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -208,6 +220,7 @@ public partial class ProjectViewModel : ObservableObject, IDisposable
|
|
|
{
|
|
{
|
|
|
HasActiveAlarm = AlarmService.Instance.HasActiveAlarm;
|
|
HasActiveAlarm = AlarmService.Instance.HasActiveAlarm;
|
|
|
ActiveAlarmCount = AlarmService.Instance.ActiveAlarmCount;
|
|
ActiveAlarmCount = AlarmService.Instance.ActiveAlarmCount;
|
|
|
|
|
+ OnPropertyChanged(nameof(HasFaultContent));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>SuggestedPumpName 变化时联动刷新 HasSuggestedPump</summary>
|
|
/// <summary>SuggestedPumpName 变化时联动刷新 HasSuggestedPump</summary>
|
|
@@ -486,10 +499,11 @@ public partial class ProjectViewModel : ObservableObject, IDisposable
|
|
|
private void OnConnectionStateChanged(bool connected)
|
|
private void OnConnectionStateChanged(bool connected)
|
|
|
{
|
|
{
|
|
|
var mode = ConfigService.GetConfig()?.RunMode ?? "Direct";
|
|
var mode = ConfigService.GetConfig()?.RunMode ?? "Direct";
|
|
|
- IsPlcConnected = mode == "Mock" || connected;
|
|
|
|
|
|
|
+ // Mock 模式下不伪装连接状态,诚实显示"模拟模式"
|
|
|
|
|
+ IsPlcConnected = mode != "Mock" && connected;
|
|
|
PlcStatusText = mode switch
|
|
PlcStatusText = mode switch
|
|
|
{
|
|
{
|
|
|
- "Mock" => "模拟模式",
|
|
|
|
|
|
|
+ "Mock" => "模拟模式(无 PLC 连接)",
|
|
|
_ => connected ? "PLC 已连接" : "PLC 未连接"
|
|
_ => connected ? "PLC 已连接" : "PLC 未连接"
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -836,12 +850,59 @@ public partial class PumpItemViewModel : ObservableObject
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// <summary>
|
|
|
|
|
+/// 水箱阈值行 — 每行对应一个可编辑的液位阈值(启泵N / 停泵 / 保护液位)
|
|
|
|
|
+/// </summary>
|
|
|
|
|
+public partial class TankThresholdRow : ObservableObject
|
|
|
|
|
+{
|
|
|
|
|
+ /// <summary>行标签(如 "启泵1"、"停泵液位"、"保护液位")</summary>
|
|
|
|
|
+ public string Label { get; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>PlcDataModel 寄存器键(如 "集水池.StartPump1Level")</summary>
|
|
|
|
|
+ public string RegisterKey { get; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>TankConfig 属性名(用于回写配置,如 "StartPump1Level")</summary>
|
|
|
|
|
+ public string? ConfigProperty { get; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>当前值(从 PLC 读取,只读显示)</summary>
|
|
|
|
|
+ [ObservableProperty] private float _value;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>可编辑值(用户输入,待同步到 PLC)</summary>
|
|
|
|
|
+ [ObservableProperty] private float _editValue;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>PLC 写入地址(空则隐藏该行)</summary>
|
|
|
|
|
+ public string Address { get; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>是否有可写入的地址</summary>
|
|
|
|
|
+ public bool HasAddress => !string.IsNullOrEmpty(Address);
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>文本颜色风格键(null = 默认,"warning" = 琥珀,"danger" = 红)</summary>
|
|
|
|
|
+ public string StyleKey { get; }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>行是否可见(有地址,或有非零值)</summary>
|
|
|
|
|
+ [ObservableProperty] private bool _isVisible = true;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>行是否可编辑(有地址才可编辑,否则只读显示)</summary>
|
|
|
|
|
+ public bool IsEditable => HasAddress;
|
|
|
|
|
+
|
|
|
|
|
+ public TankThresholdRow(string label, string registerKey, string address, string styleKey = "", string? configProperty = null)
|
|
|
|
|
+ {
|
|
|
|
|
+ Label = label;
|
|
|
|
|
+ RegisterKey = registerKey;
|
|
|
|
|
+ Address = address;
|
|
|
|
|
+ StyleKey = styleKey;
|
|
|
|
|
+ ConfigProperty = configProperty;
|
|
|
|
|
+ IsVisible = HasAddress; // 初始仅在有地址时可见
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 水箱/水池 ViewModel
|
|
/// 水箱/水池 ViewModel
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public partial class TankItemViewModel : ObservableObject
|
|
public partial class TankItemViewModel : ObservableObject
|
|
|
{
|
|
{
|
|
|
private readonly TankConfig _config;
|
|
private readonly TankConfig _config;
|
|
|
|
|
+ private readonly int _pumpCount;
|
|
|
private bool _editInitialized;
|
|
private bool _editInitialized;
|
|
|
|
|
|
|
|
public string Name => _config.Name;
|
|
public string Name => _config.Name;
|
|
@@ -853,24 +914,16 @@ public partial class TankItemViewModel : ObservableObject
|
|
|
[ObservableProperty] private float _stopLevel;
|
|
[ObservableProperty] private float _stopLevel;
|
|
|
[ObservableProperty] private float _protectionLevel;
|
|
[ObservableProperty] private float _protectionLevel;
|
|
|
|
|
|
|
|
- // ─── 可编辑阈值(供参数同步到 PLC)───
|
|
|
|
|
|
|
+ // ─── 动态阈值行(数量跟随项目泵数 + 停泵 + 保护液位)───
|
|
|
|
|
|
|
|
- [ObservableProperty] private float _editStartPump1Level;
|
|
|
|
|
- [ObservableProperty] private float _editStartPump2Level;
|
|
|
|
|
- [ObservableProperty] private float _editStartPump3Level;
|
|
|
|
|
- [ObservableProperty] private float _editStopLevel;
|
|
|
|
|
- [ObservableProperty] private float _editProtectionLevel;
|
|
|
|
|
|
|
+ /// <summary>可编辑阈值行集合(启泵1~N + 停泵液位 + 保护液位)</summary>
|
|
|
|
|
+ public ObservableCollection<TankThresholdRow> ThresholdRows { get; } = new();
|
|
|
|
|
|
|
|
[ObservableProperty] private bool _isSyncing;
|
|
[ObservableProperty] private bool _isSyncing;
|
|
|
[ObservableProperty] private string _syncStatusText = "";
|
|
[ObservableProperty] private string _syncStatusText = "";
|
|
|
|
|
|
|
|
- /// <summary>是否有可写入 PLC 的地址配置</summary>
|
|
|
|
|
- public bool HasWritableAddresses =>
|
|
|
|
|
- !string.IsNullOrEmpty(_config.StartPump1LevelAddress) ||
|
|
|
|
|
- !string.IsNullOrEmpty(_config.StartPump2LevelAddress) ||
|
|
|
|
|
- !string.IsNullOrEmpty(_config.StartPump3LevelAddress) ||
|
|
|
|
|
- !string.IsNullOrEmpty(_config.StopLevelAddress) ||
|
|
|
|
|
- !string.IsNullOrEmpty(_config.ProtectionLevelAddress);
|
|
|
|
|
|
|
+ /// <summary>是否有可写入 PLC 的地址配置(任一行有地址即可)</summary>
|
|
|
|
|
+ public bool HasWritableAddresses => ThresholdRows.Any(r => r.HasAddress);
|
|
|
|
|
|
|
|
/// <summary>当前液位工况档位(依据液位与阈值自动判定,供 UI 徽章展示)</summary>
|
|
/// <summary>当前液位工况档位(依据液位与阈值自动判定,供 UI 徽章展示)</summary>
|
|
|
[ObservableProperty] private TankStage _currentStage = TankStage.Stop;
|
|
[ObservableProperty] private TankStage _currentStage = TankStage.Stop;
|
|
@@ -886,9 +939,42 @@ public partial class TankItemViewModel : ObservableObject
|
|
|
private int _trendIndex;
|
|
private int _trendIndex;
|
|
|
private int _trendCount;
|
|
private int _trendCount;
|
|
|
|
|
|
|
|
- public TankItemViewModel(TankConfig config)
|
|
|
|
|
|
|
+ public TankItemViewModel(TankConfig config, int pumpCount)
|
|
|
{
|
|
{
|
|
|
_config = config;
|
|
_config = config;
|
|
|
|
|
+ _pumpCount = pumpCount;
|
|
|
|
|
+ BuildThresholdRows();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>根据泵数量动态构建阈值行(启泵1~N + 停泵 + 保护)</summary>
|
|
|
|
|
+ private void BuildThresholdRows()
|
|
|
|
|
+ {
|
|
|
|
|
+ ThresholdRows.Clear();
|
|
|
|
|
+ // 启泵阈值:数量 = max(3, pumpCount),兼容旧配置的 3 行
|
|
|
|
|
+ var thresholdCount = Math.Max(3, _pumpCount);
|
|
|
|
|
+ for (int i = 1; i <= thresholdCount; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ var addr = _config.GetStartPumpLevelAddress(i);
|
|
|
|
|
+ ThresholdRows.Add(new TankThresholdRow(
|
|
|
|
|
+ label: $"启泵{i}",
|
|
|
|
|
+ registerKey: $"{Name}.StartPump{i}Level",
|
|
|
|
|
+ address: addr,
|
|
|
|
|
+ styleKey: "",
|
|
|
|
|
+ configProperty: null
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+ // 停泵液位
|
|
|
|
|
+ ThresholdRows.Add(new TankThresholdRow(
|
|
|
|
|
+ label: "停泵液位",
|
|
|
|
|
+ registerKey: $"{Name}.StopLevel",
|
|
|
|
|
+ address: _config.StopLevelAddress,
|
|
|
|
|
+ styleKey: "warning"));
|
|
|
|
|
+ // 保护液位
|
|
|
|
|
+ ThresholdRows.Add(new TankThresholdRow(
|
|
|
|
|
+ label: "保护液位",
|
|
|
|
|
+ registerKey: $"{Name}.ProtectionLevel",
|
|
|
|
|
+ address: _config.ProtectionLevelAddress,
|
|
|
|
|
+ styleKey: "danger"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public void UpdateFromData(PlcDataModel data)
|
|
public void UpdateFromData(PlcDataModel data)
|
|
@@ -901,16 +987,17 @@ public partial class TankItemViewModel : ObservableObject
|
|
|
ProtectionLevel = data.GetAnalogRegister($"{Name}.ProtectionLevel");
|
|
ProtectionLevel = data.GetAnalogRegister($"{Name}.ProtectionLevel");
|
|
|
CurrentStage = CalculateStage();
|
|
CurrentStage = CalculateStage();
|
|
|
|
|
|
|
|
- // 首次更新时初始化编辑值(后续轮询不覆盖用户输入)
|
|
|
|
|
- if (!_editInitialized)
|
|
|
|
|
|
|
+ // 更新 ThresholdRows 的值和可见性
|
|
|
|
|
+ foreach (var row in ThresholdRows)
|
|
|
{
|
|
{
|
|
|
- EditStartPump1Level = StartPump1Level;
|
|
|
|
|
- EditStartPump2Level = StartPump2Level;
|
|
|
|
|
- EditStartPump3Level = StartPump3Level;
|
|
|
|
|
- EditStopLevel = StopLevel;
|
|
|
|
|
- EditProtectionLevel = ProtectionLevel;
|
|
|
|
|
- _editInitialized = true;
|
|
|
|
|
|
|
+ row.Value = data.GetAnalogRegister(row.RegisterKey);
|
|
|
|
|
+ // 可见条件:有地址 OR 有非零值(PLC 已读取到数据)
|
|
|
|
|
+ row.IsVisible = row.HasAddress || Math.Abs(row.Value) > 0.001f;
|
|
|
|
|
+ // 首次更新时初始化编辑值(后续轮询不覆盖用户输入)
|
|
|
|
|
+ if (!_editInitialized)
|
|
|
|
|
+ row.EditValue = row.Value;
|
|
|
}
|
|
}
|
|
|
|
|
+ _editInitialized = true;
|
|
|
|
|
|
|
|
// 追加趋势采样(环形缓冲)
|
|
// 追加趋势采样(环形缓冲)
|
|
|
_trendBuffer[_trendIndex] = Level;
|
|
_trendBuffer[_trendIndex] = Level;
|
|
@@ -927,15 +1014,19 @@ public partial class TankItemViewModel : ObservableObject
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 按泵站运行逻辑判定当前液位档位:
|
|
/// 按泵站运行逻辑判定当前液位档位:
|
|
|
- /// 保护液位(最高,紧急风险)→ 启泵3 → 启泵2 → 启泵1 → 停泵区。
|
|
|
|
|
|
|
+ /// 保护液位(最高,紧急风险)→ 启泵N → ... → 启泵1 → 停泵区。
|
|
|
/// 阈值优先取配置值,未配置(≤0)的档位自动跳过。
|
|
/// 阈值优先取配置值,未配置(≤0)的档位自动跳过。
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
private TankStage CalculateStage()
|
|
private TankStage CalculateStage()
|
|
|
{
|
|
{
|
|
|
if (ProtectionLevel > 0 && Level >= ProtectionLevel) return TankStage.Protection;
|
|
if (ProtectionLevel > 0 && Level >= ProtectionLevel) return TankStage.Protection;
|
|
|
- if (StartPump3Level > 0 && Level >= StartPump3Level) return TankStage.Pump3;
|
|
|
|
|
- if (StartPump2Level > 0 && Level >= StartPump2Level) return TankStage.Pump2;
|
|
|
|
|
- if (StartPump1Level > 0 && Level >= StartPump1Level) return TankStage.Pump1;
|
|
|
|
|
|
|
+ // 动态检查各启泵阈值(反向遍历,从高到低)
|
|
|
|
|
+ for (int i = Math.Min(3, ThresholdRows.Count(r => r.Label.StartsWith("启泵"))); i >= 1; i--)
|
|
|
|
|
+ {
|
|
|
|
|
+ var level = i switch { 1 => StartPump1Level, 2 => StartPump2Level, 3 => StartPump3Level, _ => 0f };
|
|
|
|
|
+ if (level > 0 && Level >= level)
|
|
|
|
|
+ return i switch { 1 => TankStage.Pump1, 2 => TankStage.Pump2, _ => TankStage.Pump3 };
|
|
|
|
|
+ }
|
|
|
return TankStage.Stop;
|
|
return TankStage.Stop;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -953,14 +1044,22 @@ public partial class TankItemViewModel : ObservableObject
|
|
|
IsSyncing = true;
|
|
IsSyncing = true;
|
|
|
try
|
|
try
|
|
|
{
|
|
{
|
|
|
- // 更新 config 中的值
|
|
|
|
|
- _config.StartPump1Level = EditStartPump1Level;
|
|
|
|
|
- _config.StartPump2Level = EditStartPump2Level;
|
|
|
|
|
- _config.StartPump3Level = EditStartPump3Level;
|
|
|
|
|
- _config.StopLevel = EditStopLevel;
|
|
|
|
|
- _config.ProtectionLevel = EditProtectionLevel;
|
|
|
|
|
-
|
|
|
|
|
- var result = await ParameterSyncService.Instance.SyncTankThresholdsAsync(_config);
|
|
|
|
|
|
|
+ // 更新 config 中的值(遍历 ThresholdRows 动态回写)
|
|
|
|
|
+ int pumpIndex = 0;
|
|
|
|
|
+ foreach (var row in ThresholdRows)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (row.Label.StartsWith("启泵"))
|
|
|
|
|
+ {
|
|
|
|
|
+ pumpIndex++;
|
|
|
|
|
+ _config.SetStartPumpLevel(pumpIndex, row.EditValue);
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (row.Label == "停泵液位")
|
|
|
|
|
+ _config.StopLevel = row.EditValue;
|
|
|
|
|
+ else if (row.Label == "保护液位")
|
|
|
|
|
+ _config.ProtectionLevel = row.EditValue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var result = await ParameterSyncService.Instance.SyncTankThresholdsAsync(_config, _pumpCount);
|
|
|
SyncStatusText = result.Summary;
|
|
SyncStatusText = result.Summary;
|
|
|
Log.Information("水箱参数同步: {Tank} — {Summary}", Name, result.Summary);
|
|
Log.Information("水箱参数同步: {Tank} — {Summary}", Name, result.Summary);
|
|
|
}
|
|
}
|