diff --git a/Cardiopulmonarybypasssystems/MainWindow.xaml b/Cardiopulmonarybypasssystems/MainWindow.xaml
index 9bed8db..8625b57 100644
--- a/Cardiopulmonarybypasssystems/MainWindow.xaml
+++ b/Cardiopulmonarybypasssystems/MainWindow.xaml
@@ -133,49 +133,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -197,12 +154,6 @@
-
-
-
-
-
-
@@ -210,143 +161,137 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ IsEnabled="{Binding DataContext.CanModifySession, RelativeSource={RelativeSource AncestorType=Window}}">
+
+
+
+
@@ -382,12 +338,8 @@
-
-
-
+
-
+ Text="8泵预设流量" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -593,13 +491,13 @@
-
+
-
+
@@ -623,25 +521,25 @@
-
+
-
+
-
+
-
+
@@ -673,7 +571,6 @@
IsEnabled="{Binding CanModifySession}" />
-
@@ -821,54 +718,21 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -1026,13 +890,9 @@
-
+
-
@@ -1070,27 +930,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -1109,7 +952,7 @@
Content="上一项"
Background="#FF6B8791" />
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1983,15 +1843,11 @@
Command="{Binding RefreshEngineeringRegistersCommand}"
Content="刷新寄存器"
Background="#FF4D8C72" />
-
-
diff --git a/Cardiopulmonarybypasssystems/MainWindow.xaml.cs b/Cardiopulmonarybypasssystems/MainWindow.xaml.cs
index 92cc416..6272321 100644
--- a/Cardiopulmonarybypasssystems/MainWindow.xaml.cs
+++ b/Cardiopulmonarybypasssystems/MainWindow.xaml.cs
@@ -49,6 +49,22 @@ public partial class MainWindow : Window
InspectionItemsGrid.Focus();
}
+ private void OpenEngineeringRegistersButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (DataContext is not MainViewModel viewModel)
+ {
+ return;
+ }
+
+ viewModel.EngineeringRegisterPanelVisible = true;
+ if (viewModel.RefreshEngineeringRegistersCommand.CanExecute(null))
+ {
+ viewModel.RefreshEngineeringRegistersCommand.Execute(null);
+ }
+
+ EngineeringRegistersTab.IsSelected = true;
+ }
+
private void ConfigureTrendBindings()
{
var converter = (IMultiValueConverter)Resources["TrendPointCollectionConverter"];
diff --git a/Cardiopulmonarybypasssystems/Models/PumpControlChannel.cs b/Cardiopulmonarybypasssystems/Models/PumpControlChannel.cs
index f995bfe..8883086 100644
--- a/Cardiopulmonarybypasssystems/Models/PumpControlChannel.cs
+++ b/Cardiopulmonarybypasssystems/Models/PumpControlChannel.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Cardiopulmonarybypasssystems.Models;
@@ -56,6 +58,9 @@ public partial class PumpControlChannel : ObservableObject
[ObservableProperty]
private double rs485RawOffset;
+ [ObservableProperty]
+ private bool rs485CalibrationConfirmed;
+
[ObservableProperty]
private double rs485MinFlowLpm;
@@ -98,14 +103,26 @@ public partial class PumpControlChannel : ObservableObject
[ObservableProperty]
private double confirmedSetpointFlowValue;
+ [ObservableProperty]
+ private bool? pendingRs485RunningState;
+
public string StartAddressDisplay => $"M{StartAddress}";
public string FlowAddressDisplay => FlowAddress.HasValue ? $"D{FlowAddress.Value}" : "-";
public bool HasFlowTelemetry => FlowAddress.HasValue;
public bool SupportsRs485Preset => HasFlowTelemetry && Rs485Enabled;
public bool SupportsRs485DirectControl => SupportsRs485Preset && Rs485MotorControlRegister > 0;
+ public bool UsesLegacyPlcDirectControl => Key == "NegativeAssistPump";
public bool HasSetpointCalibration => Rs485RawPerLitrePerMinute > 0;
+ public bool HasConfirmedSetpointCalibration => HasSetpointCalibration && Rs485CalibrationConfirmed;
public bool IsFlowEstablished => !HasFlowTelemetry || (FlowAvailable && FlowValue >= FlowEstablishedThreshold);
public string Rs485SlaveAddressDisplay => SupportsRs485Preset ? Rs485SlaveAddress.ToString() : "-";
+ public string CalibrationStatusText => !Rs485Enabled
+ ? "未启用"
+ : !HasSetpointCalibration
+ ? "未配置"
+ : Rs485CalibrationConfirmed
+ ? "已确认"
+ : "待确认";
public string SetpointReadbackDisplay => !SupportsRs485Preset
? "-"
: SetpointAvailable
@@ -124,11 +141,11 @@ public partial class PumpControlChannel : ObservableObject
1 => "运行",
2 => "暂停",
null => "--",
- _ => $"状态 {Rs485RunStatusCode}"
+ _ => $"状态{Rs485RunStatusCode}"
};
public string PumpGroupName => Key switch
{
- "PressureDropPump" => "压力降/抗塌陷",
+ "PressureDropPump" => "压力降",
"RecirculationMainPump" or "RecirculationReturnPump" or "RecirculationDrainagePump" => "再循环",
"KinkResistancePump" => "抗扭结",
"HemolysisDrainageSinglePump" or "HemolysisReturnSinglePump" or "HemolysisDualLumenPump" => "血细胞破坏",
@@ -144,7 +161,7 @@ public partial class PumpControlChannel : ObservableObject
public string StateHint => IsRs485Busy
? string.IsNullOrWhiteSpace(Rs485BusyOperation) ? "RS485 操作中" : $"RS485 {Rs485BusyOperation}中"
: !StateAvailable
- ? "未取得 PLC 状态"
+ ? "未收到状态反馈"
: !IsRunning
? "泵未启动"
: IsFlowEstablished
@@ -157,10 +174,15 @@ public partial class PumpControlChannel : ObservableObject
: IsFlowEstablished
? "#FF32B06A"
: "#FFD38A16";
+ public string CardPrimaryDisplay => UsesLegacyPlcDirectControl ? StateText : FlowDisplay;
public string FlowDisplay => !FlowAddress.HasValue ? "-" : FlowAvailable ? $"{FlowValue:F2} L/min" : "--";
public string ActionText => IsRs485Busy ? "处理中" : IsRunning ? "停止" : "启动";
+ public bool CanToggleRs485Action => IsRunning || HasConfirmedSetpointCalibration;
+ public string ToggleActionHint => CanToggleRs485Action ? string.Empty : "未完成流量换算标定确认";
public string Rs485ReadActionText => IsRs485Busy ? "处理中" : "读取";
public string Rs485WriteActionText => IsRs485Busy ? "处理中" : "写入";
+ public string SetpointStatusForeground => ResolveSetpointStatusForeground();
+ public string SetpointStatusBackground => ResolveSetpointStatusBackground();
partial void OnIsRunningChanged(bool value)
{
@@ -168,6 +190,9 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(ActionText));
+ OnPropertyChanged(nameof(CanToggleRs485Action));
+ OnPropertyChanged(nameof(ToggleActionHint));
+ OnPropertyChanged(nameof(CardPrimaryDisplay));
}
partial void OnFlowValueChanged(double value)
@@ -177,6 +202,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateText));
OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(IndicatorColor));
+ OnPropertyChanged(nameof(CardPrimaryDisplay));
}
partial void OnStateAvailableChanged(bool value)
@@ -184,6 +210,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateText));
OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(IndicatorColor));
+ OnPropertyChanged(nameof(CardPrimaryDisplay));
}
partial void OnFlowAvailableChanged(bool value)
@@ -193,6 +220,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateText));
OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(IndicatorColor));
+ OnPropertyChanged(nameof(CardPrimaryDisplay));
}
partial void OnRs485EnabledChanged(bool value)
@@ -200,6 +228,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(SupportsRs485Preset));
OnPropertyChanged(nameof(SupportsRs485DirectControl));
OnPropertyChanged(nameof(Rs485SlaveAddressDisplay));
+ OnPropertyChanged(nameof(CalibrationStatusText));
OnPropertyChanged(nameof(SetpointReadbackDisplay));
OnPropertyChanged(nameof(RawSetpointDisplay));
}
@@ -210,10 +239,32 @@ public partial class PumpControlChannel : ObservableObject
partial void OnRs485RawPerLitrePerMinuteChanged(double value)
{
+ Rs485CalibrationConfirmed = false;
OnPropertyChanged(nameof(HasSetpointCalibration));
+ OnPropertyChanged(nameof(HasConfirmedSetpointCalibration));
+ OnPropertyChanged(nameof(CalibrationStatusText));
+ OnPropertyChanged(nameof(CanToggleRs485Action));
+ OnPropertyChanged(nameof(ToggleActionHint));
OnPropertyChanged(nameof(SetpointReadbackDisplay));
}
+ partial void OnRs485RawOffsetChanged(double value)
+ {
+ Rs485CalibrationConfirmed = false;
+ OnPropertyChanged(nameof(HasConfirmedSetpointCalibration));
+ OnPropertyChanged(nameof(CalibrationStatusText));
+ OnPropertyChanged(nameof(CanToggleRs485Action));
+ OnPropertyChanged(nameof(ToggleActionHint));
+ }
+
+ partial void OnRs485CalibrationConfirmedChanged(bool value)
+ {
+ OnPropertyChanged(nameof(HasConfirmedSetpointCalibration));
+ OnPropertyChanged(nameof(CalibrationStatusText));
+ OnPropertyChanged(nameof(CanToggleRs485Action));
+ OnPropertyChanged(nameof(ToggleActionHint));
+ }
+
partial void OnSetpointFlowValueChanged(double value) => OnPropertyChanged(nameof(SetpointReadbackDisplay));
partial void OnRawSetpointValueChanged(int value) => OnPropertyChanged(nameof(RawSetpointDisplay));
@@ -231,6 +282,12 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(RawSetpointDisplay));
}
+ partial void OnSetpointStatusTextChanged(string value)
+ {
+ OnPropertyChanged(nameof(SetpointStatusForeground));
+ OnPropertyChanged(nameof(SetpointStatusBackground));
+ }
+
partial void OnRs485RunStatusCodeChanged(ushort? value) => OnPropertyChanged(nameof(Rs485RunStateText));
partial void OnIsRs485BusyChanged(bool value)
@@ -239,7 +296,64 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(ActionText));
OnPropertyChanged(nameof(Rs485ReadActionText));
OnPropertyChanged(nameof(Rs485WriteActionText));
+ OnPropertyChanged(nameof(SetpointStatusForeground));
+ OnPropertyChanged(nameof(SetpointStatusBackground));
}
partial void OnRs485BusyOperationChanged(string value) => OnPropertyChanged(nameof(StateHint));
+
+ private string ResolveSetpointStatusForeground()
+ {
+ if (IsRs485Busy)
+ {
+ return "#FF0F6C81";
+ }
+
+ var text = SetpointStatusText ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(text) || text.Contains("未读取", StringComparison.Ordinal))
+ {
+ return "#FF60737E";
+ }
+
+ if (ContainsAny(text, "成功", "完成", "已应用", "已更新", "已在运行", "已确认"))
+ {
+ return "#FF2B8F6A";
+ }
+
+ if (ContainsAny(text, "失败", "禁止", "超出", "未配置", "未找到", "未选择", "离线", "不可用", "未启用", "待确认"))
+ {
+ return "#FFCC4A42";
+ }
+
+ return "#FF123744";
+ }
+
+ private string ResolveSetpointStatusBackground()
+ {
+ if (IsRs485Busy)
+ {
+ return "#FFE8F4F7";
+ }
+
+ var text = SetpointStatusText ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(text) || text.Contains("未读取", StringComparison.Ordinal))
+ {
+ return "#FFF2F6F8";
+ }
+
+ if (ContainsAny(text, "成功", "完成", "已应用", "已更新", "已在运行", "已确认"))
+ {
+ return "#FFE7F5EF";
+ }
+
+ if (ContainsAny(text, "失败", "禁止", "超出", "未配置", "未找到", "未选择", "离线", "不可用", "未启用", "待确认"))
+ {
+ return "#FFFBE9E7";
+ }
+
+ return "#FFF3F7F8";
+ }
+
+ private static bool ContainsAny(string text, params string[] keywords) =>
+ keywords.Any(keyword => text.Contains(keyword, StringComparison.Ordinal));
}
diff --git a/Cardiopulmonarybypasssystems/Models/Rs485PumpBindingSettings.cs b/Cardiopulmonarybypasssystems/Models/Rs485PumpBindingSettings.cs
index 7bded32..c988b5b 100644
--- a/Cardiopulmonarybypasssystems/Models/Rs485PumpBindingSettings.cs
+++ b/Cardiopulmonarybypasssystems/Models/Rs485PumpBindingSettings.cs
@@ -4,6 +4,7 @@ public sealed class Rs485PumpBindingSettings
{
public string PumpKey { get; set; } = string.Empty;
public bool Enabled { get; set; } = true;
+ public bool CalibrationConfirmed { get; set; }
public byte SlaveAddress { get; set; } = 1;
public ushort ForwardSpeedRegister { get; set; } = 0x00A2;
public ushort ReverseSpeedRegister { get; set; } = 0x00A3;
diff --git a/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs
deleted file mode 100644
index 268434b..0000000
--- a/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs
+++ /dev/null
@@ -1,718 +0,0 @@
-using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Cardiopulmonarybypasssystems.Models;
-using NModbus;
-
-namespace Cardiopulmonarybypasssystems.Services;
-
-public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDisposable
-{
- private const string IpAddress = "192.168.1.10";
- private const int Port = 502;
- private const byte SlaveId = 1;
- private const ushort ProximalPressureRegister = 1330;
- private const ushort DistalPressureRegister = 1380;
- private const double FlowRegisterScale = 0.01d;
- private const double KpaToMmHg = 7.50061683d;
- private const ushort PressureRegisterBlockLength = (ushort)(DistalPressureRegister - ProximalPressureRegister + 2);
- private const int SmoothingWindowSize = 5;
- private static readonly TimeSpan ConnectionAttemptTimeout = TimeSpan.FromMilliseconds(300);
- private static readonly TimeSpan ConnectionRetryInterval = TimeSpan.FromSeconds(5);
- private static readonly IReadOnlyDictionary FlowRegisters = new Dictionary(StringComparer.Ordinal)
- {
- ["PressureDropPump"] = 1000,
- ["RecirculationMainPump"] = 1010,
- ["RecirculationReturnPump"] = 1020,
- ["RecirculationDrainagePump"] = 1030,
- ["KinkResistancePump"] = 1040,
- ["HemolysisDrainageSinglePump"] = 1050,
- ["HemolysisReturnSinglePump"] = 1060,
- ["HemolysisDualLumenPump"] = 1070
- };
-
- private static readonly IReadOnlyDictionary FlowChannelNames = new Dictionary(StringComparer.Ordinal)
- {
- ["PressureDropPump"] = "主泵流量",
- ["RecirculationMainPump"] = "再循环主泵流量",
- ["RecirculationReturnPump"] = "动脉回输流量",
- ["RecirculationDrainagePump"] = "静脉引流流量",
- ["KinkResistancePump"] = "抗扭结主泵流量",
- ["HemolysisDrainageSinglePump"] = "血细胞破坏-单腔引流/回输流量",
- ["HemolysisReturnSinglePump"] = "双腔插管试验回路流量",
- ["HemolysisDualLumenPump"] = "双腔插管试验回路流量(两个管腔)"
- };
-
- private readonly object _syncRoot = new();
- private readonly Random _random = new();
- private readonly ModbusFactory _factory = new();
- private readonly Dictionary> _channelWindows = new(StringComparer.Ordinal);
- private readonly Dictionary _engineeringFloatRegisters = new()
- {
- [1006] = 1.0f,
- [1016] = 1.0f,
- [1026] = 1.0f,
- [1036] = 1.0f,
- [1046] = 1.0f,
- [1056] = 1.0f,
- [1066] = 1.0f,
- [1076] = 1.0f,
- [1328] = 1.0f,
- [1378] = 1.0f
- };
- private readonly List _channels =
- [
- new() { Name = "主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
- new() { Name = "再循环主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
- new() { Name = "动脉回输流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
- new() { Name = "静脉引流流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
- new() { Name = "抗扭结主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
- new() { Name = "血细胞破坏-单腔引流/回输流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
- new() { Name = "双腔插管试验回路流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
- new() { Name = "双腔插管试验回路流量(两个管腔)", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
- new() { Name = "远端压力", Unit = "mmHg", Value = 68, Min = 40, Max = 180 },
- new() { Name = "近端压力", Unit = "mmHg", Value = 80, Min = 60, Max = 220 },
- new() { Name = "负压辅助引流", Unit = "kPa", Value = 0, Min = -20, Max = 0 },
- new() { Name = "模拟血液温度", Unit = "°C", Value = 37.1, Min = 34, Max = 40 },
- new() { Name = "再循环率", Unit = "%", Value = 0, Min = 0, Max = 20 },
- new() { Name = "游离血红蛋白", Unit = "g/L", Value = 0.028, Min = 0, Max = 0.08 },
- new() { Name = "白细胞减少率", Unit = "%", Value = 7.1, Min = 0, Max = 20 }
- ];
- // The 8 production pumps are actuator-controlled by servo RS485.
- // Pump StartAddress values are retained only for telemetry compatibility and historical mapping.
- private readonly List _pumpControls =
- [
- new() { Key = "NegativeAssistPump", Name = "负压泵", StartAddress = 0 },
- new() { Key = "PressureDropPump", Name = "压力降/抗塌陷泵", StartAddress = 1, FlowAddress = FlowRegisters["PressureDropPump"] },
- new() { Key = "RecirculationMainPump", Name = "再循环主泵", StartAddress = 2, FlowAddress = FlowRegisters["RecirculationMainPump"] },
- new() { Key = "RecirculationReturnPump", Name = "回流泵", StartAddress = 3, FlowAddress = FlowRegisters["RecirculationReturnPump"] },
- new() { Key = "RecirculationDrainagePump", Name = "引流泵", StartAddress = 4, FlowAddress = FlowRegisters["RecirculationDrainagePump"] },
- new() { Key = "KinkResistancePump", Name = "抗扭结泵", StartAddress = 5, FlowAddress = FlowRegisters["KinkResistancePump"] },
- new() { Key = "HemolysisDrainageSinglePump", Name = "血细胞破坏-单腔引流/回输泵", StartAddress = 6, FlowAddress = FlowRegisters["HemolysisDrainageSinglePump"] },
- new() { Key = "HemolysisReturnSinglePump", Name = "双腔插管试验回路泵", StartAddress = 7, FlowAddress = FlowRegisters["HemolysisReturnSinglePump"] },
- new() { Key = "HemolysisDualLumenPump", Name = "双腔插管试验回路泵(两个管腔)", StartAddress = 8, FlowAddress = FlowRegisters["HemolysisDualLumenPump"] }
- ];
- private readonly List _valveControls =
- [
- new() { Key = "TestLoopValve1", Name = "测试回路阀 1", StartAddress = 10 },
- new() { Key = "TestLoopValve2", Name = "测试回路阀 2", StartAddress = 11 }
- ];
-
- private TcpClient? _tcpClient;
- private IModbusMaster? _master;
- private bool _connectionInitialized;
- private Task? _connectionTask;
- private DateTime _nextConnectionAttemptUtc = DateTime.MinValue;
- private double? _proximalPressureRawKpa;
- private double? _distalPressureRawKpa;
- private double? _proximalPressureDecodedKpa;
- private double? _distalPressureDecodedKpa;
-
- private int HighestConfiguredCoilAddress => Math.Max(
- _pumpControls.Max(item => item.StartAddress),
- _valveControls.Max(item => item.StartAddress));
-
- public bool IsLiveConnected
- {
- get
- {
- lock (_syncRoot)
- {
- return _master is not null && _tcpClient?.Connected == true;
- }
- }
- }
-
- public string EndpointDescription => $"{IpAddress}:{Port} / Slave {SlaveId}";
- public DateTime? LastSuccessfulReadAt => null;
- public string LastErrorMessage => "未使用的兼容服务";
-
- public IReadOnlyList GetChannels()
- {
- EnsureConnectionScheduled();
- return _channels;
- }
-
- public IReadOnlyList GetPumpControls()
- {
- EnsureConnectionScheduled();
- return _pumpControls;
- }
-
- public IReadOnlyList GetValveControls()
- {
- EnsureConnectionScheduled();
- return _valveControls;
- }
-
- public TelemetryUpdateSnapshot UpdateChannels()
- {
- EnsureConnectionScheduled();
-
- lock (_syncRoot)
- {
- var liveReadSucceeded = TryReadPumpStatesAndFlows();
- TryReadPressureChannels(liveReadSucceeded);
- SimulateAuxiliaryChannels(liveReadSucceeded);
- SyncDerivedChannels();
- return BuildSnapshot(BuildAlarms());
- }
- }
-
- public ushort? ReadHoldingRegister(ushort address)
- {
- lock (_syncRoot)
- {
- if (!_engineeringFloatRegisters.TryGetValue(address, out var value))
- {
- return null;
- }
-
- return (ushort)Math.Clamp((int)Math.Round(value), ushort.MinValue, ushort.MaxValue);
- }
- }
-
- public float? ReadHoldingFloatRegister(ushort address)
- {
- lock (_syncRoot)
- {
- return address switch
- {
- ProximalPressureRegister => (float)(_proximalPressureRawKpa ?? (_channels.First(channel => channel.Name == "近端压力").Value / KpaToMmHg)),
- DistalPressureRegister => (float)(_distalPressureRawKpa ?? (_channels.First(channel => channel.Name == "远端压力").Value / KpaToMmHg)),
- _ when _engineeringFloatRegisters.TryGetValue(address, out var value) => value,
- _ => null
- };
- }
- }
-
- public bool WriteHoldingRegister(ushort address, ushort value)
- {
- lock (_syncRoot)
- {
- if (!_engineeringFloatRegisters.ContainsKey(address))
- {
- return false;
- }
-
- _engineeringFloatRegisters[address] = value;
- return true;
- }
- }
-
- public bool WriteHoldingFloatRegister(ushort address, float value)
- {
- lock (_syncRoot)
- {
- if (!_engineeringFloatRegisters.ContainsKey(address))
- {
- return false;
- }
-
- _engineeringFloatRegisters[address] = value;
- return true;
- }
- }
-
- public void SetPumpRunning(string pumpKey, bool isRunning)
- {
- lock (_syncRoot)
- {
- var pump = _pumpControls.FirstOrDefault(item => item.Key == pumpKey);
- if (pump is null)
- {
- return;
- }
-
- // Legacy PLC coil path retained only for non-RS485 pump compatibility.
- if (PumpActuationProfiles.IsServoRs485ManagedPump(pump.Key))
- {
- return;
- }
-
- pump.IsRunning = isRunning;
-
- if (_master is null)
- {
- return;
- }
-
- try
- {
- _master.WriteSingleCoil(SlaveId, (ushort)pump.StartAddress, isRunning);
- }
- catch
- {
- ReleaseConnection();
- _nextConnectionAttemptUtc = DateTime.MinValue;
- }
- }
- }
-
- public void SetValveOpen(string valveKey, bool isOpen)
- {
- lock (_syncRoot)
- {
- var valve = _valveControls.FirstOrDefault(item => item.Key == valveKey);
- if (valve is null)
- {
- return;
- }
-
- valve.IsOpen = isOpen;
-
- if (_master is null)
- {
- return;
- }
-
- try
- {
- _master.WriteSingleCoil(SlaveId, (ushort)valve.StartAddress, isOpen);
- }
- catch
- {
- ReleaseConnection();
- _nextConnectionAttemptUtc = DateTime.MinValue;
- }
- }
- }
-
- public void Dispose()
- {
- lock (_syncRoot)
- {
- ReleaseConnection();
- }
- }
-
- private void EnsureConnectionScheduled()
- {
- lock (_syncRoot)
- {
- if (_master is not null && _tcpClient?.Connected == true)
- {
- return;
- }
-
- if (_connectionTask is { IsCompleted: false })
- {
- return;
- }
-
- if (DateTime.UtcNow < _nextConnectionAttemptUtc)
- {
- return;
- }
-
- _nextConnectionAttemptUtc = DateTime.UtcNow.Add(ConnectionRetryInterval);
- _connectionTask = Task.Run(ConnectWithTimeout);
- }
- }
-
- private void ConnectWithTimeout()
- {
- TcpClient? tcpClient = null;
-
- try
- {
- tcpClient = new TcpClient();
- using var cancellation = new CancellationTokenSource(ConnectionAttemptTimeout);
- tcpClient.ConnectAsync(IpAddress, Port, cancellation.Token).GetAwaiter().GetResult();
- tcpClient.ReceiveTimeout = (int)ConnectionAttemptTimeout.TotalMilliseconds;
- tcpClient.SendTimeout = (int)ConnectionAttemptTimeout.TotalMilliseconds;
- var master = _factory.CreateMaster(tcpClient);
- ApplySafeStartupState(master);
-
- lock (_syncRoot)
- {
- ReleaseConnection();
- _tcpClient = tcpClient;
- _master = master;
- _connectionInitialized = true;
- tcpClient = null;
- }
- }
- catch
- {
- tcpClient?.Dispose();
-
- lock (_syncRoot)
- {
- ReleaseConnection();
- }
- }
- }
-
- private bool TryReadPumpStatesAndFlows()
- {
- if (_master is null)
- {
- SimulatePumpFlows();
- return false;
- }
-
- try
- {
- var coilStates = _master.ReadCoils(SlaveId, 0, (ushort)(HighestConfiguredCoilAddress + 1));
- foreach (var pump in _pumpControls)
- {
- if (!PumpActuationProfiles.IsServoRs485ManagedPump(pump.Key))
- {
- pump.IsRunning = coilStates[pump.StartAddress];
- }
- }
-
- foreach (var valve in _valveControls)
- {
- valve.IsOpen = coilStates[valve.StartAddress];
- }
-
- foreach (var pump in _pumpControls.Where(item => item.FlowAddress.HasValue))
- {
- var registerValue = _master.ReadHoldingRegisters(SlaveId, (ushort)pump.FlowAddress!.Value, 1)[0];
- var flowValue = ConvertRegisterToFlow(registerValue);
- pump.FlowValue = flowValue;
- SetSmoothedValue(FlowChannelNames[pump.Key], flowValue);
- }
-
- return true;
- }
- catch
- {
- ReleaseConnection();
- _nextConnectionAttemptUtc = DateTime.MinValue;
- SimulatePumpFlows();
- return false;
- }
- }
-
- private void TryReadPressureChannels(bool liveReadSucceeded)
- {
- if (_master is null || !liveReadSucceeded)
- {
- _proximalPressureRawKpa = null;
- _distalPressureRawKpa = null;
- _proximalPressureDecodedKpa = null;
- _distalPressureDecodedKpa = null;
- SimulatePressureChannels();
- return;
- }
-
- try
- {
- var pressureRegisters = _master.ReadHoldingRegisters(SlaveId, ProximalPressureRegister, PressureRegisterBlockLength);
- var distalOffset = DistalPressureRegister - ProximalPressureRegister;
- var proximalRawKpa = ConvertRegistersToPressureKpa(pressureRegisters[0], pressureRegisters[1]);
- var distalRawKpa = ConvertRegistersToPressureKpa(pressureRegisters[distalOffset], pressureRegisters[distalOffset + 1]);
- _proximalPressureRawKpa = proximalRawKpa;
- _distalPressureRawKpa = distalRawKpa;
- _proximalPressureDecodedKpa = proximalRawKpa;
- _distalPressureDecodedKpa = distalRawKpa;
- SetSmoothedValue("近端压力", ConvertPressureKpaToMmHg(proximalRawKpa));
- SetSmoothedValue("远端压力", ConvertPressureKpaToMmHg(distalRawKpa));
- }
- catch
- {
- _proximalPressureRawKpa = null;
- _distalPressureRawKpa = null;
- _proximalPressureDecodedKpa = null;
- _distalPressureDecodedKpa = null;
- ReleaseConnection();
- _nextConnectionAttemptUtc = DateTime.MinValue;
- SimulatePressureChannels();
- }
- }
-
- private void SimulatePumpFlows()
- {
- foreach (var pump in _pumpControls.Where(item => item.FlowAddress.HasValue))
- {
- var target = pump.IsRunning ? SimulatedRunningTarget(pump.Key) : 0d;
- var nextValue = pump.IsRunning
- ? target + Next(-0.10, 0.10)
- : Math.Max(0d, pump.FlowValue * 0.35 + Next(-0.02, 0.02));
-
- pump.FlowValue = Math.Clamp(nextValue, 0d, 7d);
- SetSmoothedValue(FlowChannelNames[pump.Key], pump.FlowValue);
- }
- }
-
- private void SimulatePressureChannels()
- {
- var pressurePumpRunning = Pump("PressureDropPump").IsRunning;
- var proximalTarget = pressurePumpRunning ? 112d : 80d;
- var distalTarget = pressurePumpRunning ? 94d : 68d;
-
- SetSmoothedValue("近端压力", proximalTarget + Next(-3.0, 3.0));
- SetSmoothedValue("远端压力", distalTarget + Next(-2.5, 2.5));
-
- var proximal = Channel("近端压力");
- var distal = Channel("远端压力");
- if (distal.Value > proximal.Value - 2)
- {
- SetSmoothedValue("远端压力", Math.Max(distal.Min, proximal.Value - Next(6, 18)));
- }
- }
-
- private void SimulateAuxiliaryChannels(bool liveReadSucceeded)
- {
- var negativePump = Pump("NegativeAssistPump");
- var negativeTarget = negativePump.IsRunning ? -6.67d : 0d;
- SetSmoothedValue("负压辅助引流", negativeTarget + Next(-0.5, 0.5));
-
- SetSmoothedValue("模拟血液温度", Channel("模拟血液温度").Value + Next(-0.15, 0.15));
- SetSmoothedValue("游离血红蛋白", Channel("游离血红蛋白").Value + Next(-0.003, 0.003));
- SetSmoothedValue("白细胞减少率", Channel("白细胞减少率").Value + Next(-0.4, 0.4));
-
- if (!liveReadSucceeded)
- {
- return;
- }
-
- foreach (var pump in _pumpControls.Where(item => item.FlowAddress.HasValue))
- {
- SetSmoothedValue(FlowChannelNames[pump.Key], pump.FlowValue);
- }
- }
-
- private void SyncDerivedChannels()
- {
- var returnFlow = Channel("动脉回输流量").Value;
- var drainageFlow = Channel("静脉引流流量").Value;
- var recirculationRate = returnFlow <= 0.01
- ? 0d
- : Math.Clamp((returnFlow - drainageFlow) / returnFlow * 100d, 0d, 100d);
- SetSmoothedValue("再循环率", recirculationRate);
- }
-
- private List BuildAlarms()
- {
- var alarms = new List();
- var deltaPressure = Channel("近端压力").Value - Channel("远端压力").Value;
- var recirculationRate = Channel("再循环率").Value;
-
- if (deltaPressure > 24)
- {
- alarms.Add(new AlarmMessage
- {
- Timestamp = DateTime.Now,
- Level = "高",
- Message = $"压力降 ΔP {deltaPressure:F1} mmHg 偏高,请复核 D{ProximalPressureRegister}/D{DistalPressureRegister}。"
- });
- }
-
- if (!_connectionInitialized || _master is null)
- {
- alarms.Add(new AlarmMessage
- {
- Timestamp = DateTime.Now,
- Level = "中",
- Message = $"ModbusTcp 未连接,当前 M/D 泵控与流量使用本地平滑模拟。目标 {IpAddress}:{Port}。"
- });
- }
-
- if (recirculationRate > 8)
- {
- alarms.Add(new AlarmMessage
- {
- Timestamp = DateTime.Now,
- Level = "中",
- Message = $"再循环率 {recirculationRate:F1}% 偏高,建议复核回路与泵状态。"
- });
- }
-
- foreach (var pump in _pumpControls.Where(item => item.IsRunning && item.FlowAddress.HasValue && item.FlowValue < 0.15d))
- {
- alarms.Add(new AlarmMessage
- {
- Timestamp = DateTime.Now,
- Level = "中",
- Message = $"{pump.Name} 已启动但流量未建立,请检查回路、泵头和传感器。"
- });
- }
-
- return alarms;
- }
-
- private TelemetryUpdateSnapshot BuildSnapshot(IReadOnlyList alarms) => new()
- {
- Channels = _channels.Select(channel => new TelemetryChannelSnapshot
- {
- Name = channel.Name,
- Value = channel.Value,
- IsAvailable = true
- }).ToList(),
- PumpControls = _pumpControls.Select(pump => new PumpControlSnapshot
- {
- Key = pump.Key,
- IsRunning = pump.IsRunning,
- FlowValue = pump.FlowValue,
- StateAvailable = true,
- FlowAvailable = pump.FlowAddress.HasValue
- }).ToList(),
- ValveControls = _valveControls.Select(valve => new ValveControlSnapshot
- {
- Key = valve.Key,
- IsOpen = valve.IsOpen,
- StateAvailable = true
- }).ToList(),
- Alarms = alarms
- .Select(alarm => new AlarmMessage
- {
- Timestamp = alarm.Timestamp,
- Level = alarm.Level,
- Message = alarm.Message
- })
- .ToList(),
- ProximalPressureRawKpa = _proximalPressureRawKpa,
- DistalPressureRawKpa = _distalPressureRawKpa,
- IsLiveConnected = _master is not null && _tcpClient?.Connected == true,
- EndpointDescription = EndpointDescription,
- LastSuccessfulReadAt = null,
- LastErrorMessage = LastErrorMessage
- };
-
- private void SetSmoothedValue(string channelName, double nextValue)
- {
- var channel = Channel(channelName);
- var clampedValue = ShouldClampChannelValue(channelName)
- ? Math.Clamp(nextValue, channel.Min, channel.Max)
- : nextValue;
- var window = Window(channelName);
- window.Enqueue(clampedValue);
-
- while (window.Count > SmoothingWindowSize)
- {
- window.Dequeue();
- }
-
- channel.Value = window.Average();
- }
-
- private Queue Window(string channelName)
- {
- if (_channelWindows.TryGetValue(channelName, out var window))
- {
- return window;
- }
-
- window = new Queue();
- _channelWindows[channelName] = window;
- return window;
- }
-
- private void ApplySafeStartupState(IModbusMaster master)
- {
- foreach (var pump in _pumpControls)
- {
- master.WriteSingleCoil(SlaveId, (ushort)pump.StartAddress, false);
- }
-
- foreach (var valve in _valveControls)
- {
- master.WriteSingleCoil(SlaveId, (ushort)valve.StartAddress, false);
- }
-
- ApplyLocalSafeState();
- }
-
- private void ApplyLocalSafeState()
- {
- foreach (var pump in _pumpControls)
- {
- pump.IsRunning = false;
- pump.FlowValue = 0d;
- }
-
- foreach (var valve in _valveControls)
- {
- valve.IsOpen = false;
- }
-
- foreach (var channelName in FlowChannelNames.Values)
- {
- SetChannelValueDirect(channelName, 0d);
- }
-
- SetChannelValueDirect("负压辅助引流", 0d);
- SetChannelValueDirect("再循环率", 0d);
- }
-
- private void SetChannelValueDirect(string channelName, double nextValue)
- {
- var channel = Channel(channelName);
- var clampedValue = ShouldClampChannelValue(channelName)
- ? Math.Clamp(nextValue, channel.Min, channel.Max)
- : nextValue;
- var window = Window(channelName);
- window.Clear();
- window.Enqueue(clampedValue);
- channel.Value = clampedValue;
- }
-
- private void ReleaseConnection()
- {
- _master?.Dispose();
- _tcpClient?.Dispose();
- _master = null;
- _tcpClient = null;
- }
-
- private static double SimulatedRunningTarget(string pumpKey) => pumpKey switch
- {
- "PressureDropPump" => 4.2d,
- "RecirculationMainPump" => 4.8d,
- "RecirculationReturnPump" => 4.7d,
- "RecirculationDrainagePump" => 4.4d,
- "KinkResistancePump" => 4.6d,
- "HemolysisDrainageSinglePump" => 4.3d,
- "HemolysisReturnSinglePump" => 4.3d,
- "HemolysisDualLumenPump" => 4.1d,
- _ => 4.0d
- };
-
- private static double ConvertRegisterToFlow(ushort rawValue) => rawValue * FlowRegisterScale;
-
- private static double ConvertRegistersToPressureKpa(ushort firstWord, ushort secondWord)
- {
- var lowWordFirst = DecodeFloat(firstWord, secondWord, lowWordFirst: true);
- var highWordFirst = DecodeFloat(firstWord, secondWord, lowWordFirst: false);
- var lowWordFirstValid = IsPlausiblePressureKpa(lowWordFirst);
- var highWordFirstValid = IsPlausiblePressureKpa(highWordFirst);
-
- if (lowWordFirstValid && !highWordFirstValid)
- {
- return lowWordFirst;
- }
-
- if (!lowWordFirstValid && highWordFirstValid)
- {
- return highWordFirst;
- }
-
- return lowWordFirst;
- }
-
- private static double ConvertPressureKpaToMmHg(double pressureKpa) => pressureKpa * KpaToMmHg;
-
- private static float DecodeFloat(ushort firstWord, ushort secondWord, bool lowWordFirst)
- {
- var bits = lowWordFirst
- ? ((uint)secondWord << 16) | firstWord
- : ((uint)firstWord << 16) | secondWord;
- return BitConverter.Int32BitsToSingle(unchecked((int)bits));
- }
-
- private static bool IsPlausiblePressureKpa(float value) =>
- !float.IsNaN(value) && !float.IsInfinity(value) && value is > -1000f and < 1000f;
-
- private PumpControlChannel Pump(string key) => _pumpControls.First(pump => pump.Key == key);
-
- private static bool ShouldClampChannelValue(string channelName) =>
- channelName is not "近端压力" and not "远端压力";
-
- private DeviceChannel Channel(string name) => _channels.First(channel => channel.Name == name);
-
- private double Next(double min, double max) => min + (_random.NextDouble() * (max - min));
-}
diff --git a/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
index 3800423..1bdf852 100644
--- a/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
+++ b/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
@@ -580,7 +580,7 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
{
Timestamp = DateTime.Now,
Level = "中",
- Message = $"ModbusTcp 未连接,当前不再使用本地模拟数据。目标 {_ipAddress}:{_port}。"
+ Message = $"ModbusTcp 未连接,请检查 PLC 通讯。目标 {_ipAddress}:{_port}。"
});
}
diff --git a/Cardiopulmonarybypasssystems/Services/Rs485PumpFlowService.cs b/Cardiopulmonarybypasssystems/Services/Rs485PumpFlowService.cs
index 362f4b2..96a037e 100644
--- a/Cardiopulmonarybypasssystems/Services/Rs485PumpFlowService.cs
+++ b/Cardiopulmonarybypasssystems/Services/Rs485PumpFlowService.cs
@@ -1,4 +1,5 @@
using System.IO.Ports;
+using System.Linq;
using System.Text;
using System.Threading;
using Cardiopulmonarybypasssystems.Models;
@@ -20,6 +21,7 @@ public sealed class Rs485PumpFlowService : IRs485PumpFlowService
private const int WriteRetryCount = 3;
private const int RetryDelayMs = 80;
private const int PostWriteSettleDelayMs = 60;
+ private const int PostCommandVerifyDelayMs = 120;
private readonly ModbusFactory _factory = new();
private readonly object _syncRoot = new();
@@ -129,7 +131,7 @@ public sealed class Rs485PumpFlowService : IRs485PumpFlowService
{
Success = false,
RawForwardSpeed = readbackForward,
- Message = $"RS485 写入未生效:目标值={rawForwardSpeed},回读值={readbackForward}。请确认驱动器上限或预设模式。"
+ Message = $"RS485 写入未生效:目标值 {rawForwardSpeed},回读值 {readbackForward}。请确认驱动器上限或预设模式。"
};
}
@@ -158,20 +160,19 @@ public sealed class Rs485PumpFlowService : IRs485PumpFlowService
return Execute(request, master =>
{
var slave = request.PumpSettings.SlaveAddress;
- var warnings = new List();
if (rawMotorSpeed == 0)
{
rawMotorSpeed = 1;
}
- if (!TryWriteRegister(master, slave, DirectControlModeRegister, DirectControlModeValue, out _))
+ if (!TryWriteRegister(master, slave, DirectControlModeRegister, DirectControlModeValue, out var modeError))
{
- warnings.Add("未切入 RS485 直接控制模式");
+ return Failure($"RS485 直启失败:未切入直接控制模式,{modeError}");
}
- if (!TryWriteRegister(master, slave, ReleaseRegister, ReleaseValue, out _))
+ if (!TryWriteRegister(master, slave, ReleaseRegister, ReleaseValue, out var releaseError))
{
- warnings.Add("未执行释放命令");
+ return Failure($"RS485 直启失败:未执行释放命令,{releaseError}");
}
if (!TryWriteSignedRegister(master, slave, request.PumpSettings.MotorControlRegister, rawMotorSpeed, out var writeError))
@@ -179,11 +180,30 @@ public sealed class Rs485PumpFlowService : IRs485PumpFlowService
return Failure($"RS485 直接启动失败:{writeError}");
}
+ if (PostCommandVerifyDelayMs > 0)
+ {
+ Thread.Sleep(PostCommandVerifyDelayMs);
+ }
+
+ if (TryReadRegister(master, slave, request.PumpSettings.RunStatusRegister, out var runStatusValue, out _))
+ {
+ if (runStatusValue != 1)
+ {
+ return Failure($"RS485 直启失败:运行状态返回 {runStatusValue},未确认启动");
+ }
+
+ return new Rs485PumpFlowOperationResult
+ {
+ Success = true,
+ Message = $"RS485 直接启动成功,控制值 {rawMotorSpeed}",
+ RunStatus = runStatusValue
+ };
+ }
+
return new Rs485PumpFlowOperationResult
{
Success = true,
- Message = BuildSuccessMessage($"RS485 直接启动成功,控制值={rawMotorSpeed}", warnings),
- RunStatus = 1
+ Message = $"RS485 启动命令已下发,控制值 {rawMotorSpeed},等待流量确认"
};
});
}
@@ -193,22 +213,40 @@ public sealed class Rs485PumpFlowService : IRs485PumpFlowService
return Execute(request, master =>
{
var slave = request.PumpSettings.SlaveAddress;
- var warnings = new List();
if (!TryWriteSignedRegister(master, slave, request.PumpSettings.MotorControlRegister, 0, out var writeError))
{
return Failure($"RS485 直接停止失败:{writeError}");
}
- if (!TryWriteRegister(master, slave, SelfLockRegister, SelfLockValue, out _))
+ if (!TryWriteRegister(master, slave, SelfLockRegister, SelfLockValue, out var selfLockError))
{
- warnings.Add("未执行自锁命令");
+ return Failure($"RS485 直停失败:未执行自锁命令,{selfLockError}");
+ }
+
+ if (PostCommandVerifyDelayMs > 0)
+ {
+ Thread.Sleep(PostCommandVerifyDelayMs);
+ }
+
+ if (TryReadRegister(master, slave, request.PumpSettings.RunStatusRegister, out var runStatusValue, out _))
+ {
+ if (runStatusValue != 0)
+ {
+ return Failure($"RS485 直停失败:运行状态返回 {runStatusValue},未确认停止");
+ }
+
+ return new Rs485PumpFlowOperationResult
+ {
+ Success = true,
+ Message = "RS485 直接停止成功",
+ RunStatus = runStatusValue
+ };
}
return new Rs485PumpFlowOperationResult
{
Success = true,
- Message = BuildSuccessMessage("RS485 直接停止成功", warnings),
- RunStatus = 0
+ Message = "RS485 停止命令已下发,等待流量确认"
};
});
}
diff --git a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.Rs485.cs b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.Rs485.cs
index 95a3ebe..381fe46 100644
--- a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.Rs485.cs
+++ b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.Rs485.cs
@@ -2,6 +2,7 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
+using System.Threading.Tasks;
using Cardiopulmonarybypasssystems.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -25,6 +26,7 @@ public partial class MainViewModel
private static readonly HashSet Rs485PumpKeySet = [.. Rs485PumpKeys];
private const double DefaultRs485RawPerLitrePerMinute = 1000d;
private const double DefaultRs485RawOffset = 0d;
+ private const int MaxRs485MotorCommand = short.MaxValue;
[ObservableProperty]
private string rs485PortName = "COM3";
@@ -56,15 +58,28 @@ public partial class MainViewModel
[ObservableProperty]
private bool rs485AutoStartPumpAfterWrite;
+ [ObservableProperty]
+ private bool rs485AdvancedSettingsVisible;
+
[ObservableProperty]
private string rs485StatusText = "RS485 待确认";
public ObservableCollection Rs485ParityOptions { get; } = new(["Even", "Odd", "None"]);
public ObservableCollection Rs485FlowPumpControls { get; } = [];
+ public ObservableCollection ActiveRs485FlowPumpControls { get; } = [];
+ public string Rs485AdvancedSettingsToggleText => Rs485AdvancedSettingsVisible ? "收起高级维护" : "展开高级维护";
public string Rs485ConnectionSummary =>
$"{Rs485PortName} / {Rs485BaudRate}bps / {Rs485Parity} / {Rs485DataBits}-{Rs485StopBits}";
+ public int Rs485EnabledPumpCount => Rs485FlowPumpControls.Count(item => item.Rs485Enabled);
+ public int Rs485CalibrationConfirmedPumpCount =>
+ Rs485FlowPumpControls.Count(item => item.Rs485Enabled && item.HasConfirmedSetpointCalibration);
+ public string Rs485CalibrationSummary => Rs485EnabledPumpCount == 0
+ ? "当前未启用 RS485 泵通道。"
+ : Rs485CalibrationConfirmedPumpCount == Rs485EnabledPumpCount
+ ? $"已确认全部 {Rs485EnabledPumpCount} 台泵的流量换算。"
+ : $"已确认 {Rs485CalibrationConfirmedPumpCount}/{Rs485EnabledPumpCount} 台泵的流量换算,其余仍待标定确认。";
public string Rs485ManualHint =>
- "8 泵启停由伺服器 RS485 主控;目标流量会按换算系数写入驱动器速度寄存器,PLC 仅保留实时采集。";
+ "主操作区按 8 泵批量预设设计,可逐行录入目标流量后一次批量写入;PLC 仅保留实时采集。";
partial void OnRs485PortNameChanged(string value) => UpdateAndPersistRs485Settings();
partial void OnRs485BaudRateChanged(int value) => UpdateAndPersistRs485Settings();
@@ -87,6 +102,7 @@ public partial class MainViewModel
partial void OnRs485AutoSwitchPresetModeChanged(bool value) => UpdateAndPersistRs485Settings();
partial void OnRs485PersistPresetAfterWriteChanged(bool value) => UpdateAndPersistRs485Settings();
partial void OnRs485AutoStartPumpAfterWriteChanged(bool value) => UpdateAndPersistRs485Settings();
+ partial void OnRs485AdvancedSettingsVisibleChanged(bool value) => OnPropertyChanged(nameof(Rs485AdvancedSettingsToggleText));
private void InitializeRs485FlowControl()
{
@@ -115,6 +131,7 @@ public partial class MainViewModel
pump.PropertyChanged += OnRs485PumpConfigurationChanged;
}
+ RefreshActiveRs485FlowPumpControls();
OnPropertyChanged(nameof(Rs485ConnectionSummary));
}
@@ -145,6 +162,8 @@ public partial class MainViewModel
ApplyRs485Binding(pump, binding);
}
}
+
+ RefreshActiveRs485FlowPumpControls();
}
private Rs485SerialSettings BuildRs485SerialSettings() => new()
@@ -167,6 +186,7 @@ public partial class MainViewModel
{
PumpKey = pump.Key,
Enabled = pump.Rs485Enabled,
+ CalibrationConfirmed = pump.Rs485CalibrationConfirmed,
SlaveAddress = pump.Rs485SlaveAddress,
ForwardSpeedRegister = pump.Rs485ForwardSpeedRegister,
ReverseSpeedRegister = pump.Rs485ReverseSpeedRegister,
@@ -191,6 +211,7 @@ public partial class MainViewModel
{
PumpKey = Rs485PumpKeys[index],
Enabled = true,
+ CalibrationConfirmed = false,
SlaveAddress = (byte)(index + 1),
MotorControlRegister = 0x0040,
RawPerLitrePerMinute = DefaultRs485RawPerLitrePerMinute,
@@ -209,6 +230,7 @@ public partial class MainViewModel
?? new Rs485PumpBindingSettings { PumpKey = pump.Key };
pump.Rs485Enabled = binding.Enabled;
+ pump.Rs485CalibrationConfirmed = binding.CalibrationConfirmed;
pump.Rs485SlaveAddress = binding.SlaveAddress;
pump.Rs485ForwardSpeedRegister = binding.ForwardSpeedRegister;
pump.Rs485ReverseSpeedRegister = binding.ReverseSpeedRegister;
@@ -241,6 +263,14 @@ public partial class MainViewModel
{
pump.SetpointStatusText = "未配置 L/min 与速度换算系数";
}
+ else if (!pump.Rs485CalibrationConfirmed)
+ {
+ pump.SetpointStatusText = "换算系数待标定确认";
+ }
+ else
+ {
+ pump.SetpointStatusText = "换算系数已确认";
+ }
}
private void OnRs485PumpConfigurationChanged(object? sender, PropertyChangedEventArgs e)
@@ -254,6 +284,7 @@ public partial class MainViewModel
and not nameof(PumpControlChannel.Rs485SlaveAddress)
and not nameof(PumpControlChannel.Rs485RawPerLitrePerMinute)
and not nameof(PumpControlChannel.Rs485RawOffset)
+ and not nameof(PumpControlChannel.Rs485CalibrationConfirmed)
and not nameof(PumpControlChannel.Rs485MinFlowLpm)
and not nameof(PumpControlChannel.Rs485MaxFlowLpm)
and not nameof(PumpControlChannel.Rs485ForwardSpeedRegister)
@@ -271,10 +302,48 @@ public partial class MainViewModel
{
pump.SetpointStatusText = "未配置 L/min 与速度换算系数";
}
+ else if (!pump.Rs485CalibrationConfirmed)
+ {
+ pump.SetpointStatusText = "换算系数待标定确认";
+ }
+ else
+ {
+ pump.SetpointStatusText = "换算系数已确认";
+ }
+ if (e.PropertyName == nameof(PumpControlChannel.Rs485Enabled))
+ {
+ RefreshActiveRs485FlowPumpControls();
+ }
+
+ RaiseRs485CalibrationSummaryChanges();
UpdateAndPersistRs485Settings();
}
+ private void RefreshActiveRs485FlowPumpControls()
+ {
+ ActiveRs485FlowPumpControls.Clear();
+ foreach (var pump in Rs485FlowPumpControls.Where(item => item.Rs485Enabled))
+ {
+ ActiveRs485FlowPumpControls.Add(pump);
+ }
+
+ RaiseRs485CalibrationSummaryChanges();
+ }
+
+ private void RaiseRs485CalibrationSummaryChanges()
+ {
+ OnPropertyChanged(nameof(Rs485EnabledPumpCount));
+ OnPropertyChanged(nameof(Rs485CalibrationConfirmedPumpCount));
+ OnPropertyChanged(nameof(Rs485CalibrationSummary));
+ }
+
+ [RelayCommand]
+ private void ToggleRs485AdvancedSettings()
+ {
+ Rs485AdvancedSettingsVisible = !Rs485AdvancedSettingsVisible;
+ }
+
private void UpdateAndPersistRs485Settings()
{
OnPropertyChanged(nameof(Rs485ConnectionSummary));
@@ -292,7 +361,7 @@ public partial class MainViewModel
Rs485WriteTimeoutMs = 500;
Rs485AutoSwitchPresetMode = false;
Rs485PersistPresetAfterWrite = false;
- Rs485AutoStartPumpAfterWrite = true;
+ Rs485AutoStartPumpAfterWrite = false;
var primaryPump = Rs485FlowPumpControls.FirstOrDefault();
foreach (var pump in Rs485FlowPumpControls)
{
@@ -307,6 +376,7 @@ public partial class MainViewModel
pump.Rs485MotorControlRegister = 0x0040;
pump.Rs485RawPerLitrePerMinute = DefaultRs485RawPerLitrePerMinute;
pump.Rs485RawOffset = DefaultRs485RawOffset;
+ pump.Rs485CalibrationConfirmed = false;
pump.Rs485MinFlowLpm = 0;
pump.Rs485MaxFlowLpm = 1.0;
pump.SetpointStatusText = pump.Rs485Enabled
@@ -321,30 +391,62 @@ public partial class MainViewModel
}
[RelayCommand]
- private void TestRs485Connection()
+ private async Task TestRs485Connection()
{
- var pump = Rs485FlowPumpControls.FirstOrDefault(item => item.SupportsRs485Preset);
- if (pump is null)
+ var pumps = Rs485FlowPumpControls
+ .Where(item => item.SupportsRs485Preset)
+ .ToList();
+
+ if (pumps.Count == 0)
{
Rs485StatusText = "未找到已启用的 RS485 泵通道";
return;
}
- var result = _rs485PumpFlowService.TestConnection(BuildRs485Request(pump));
- ApplyRs485OperationResult(pump, result, "RS485 通讯确认");
+ var successCount = 0;
+ foreach (var pump in pumps)
+ {
+ if (!TryBeginRs485PumpOperation(pump, "通讯确认"))
+ {
+ continue;
+ }
+
+ try
+ {
+ var request = BuildRs485Request(pump);
+ var result = await Task.Run(() => _rs485PumpFlowService.TestConnection(request));
+ ApplyRs485OperationResult(pump, result, "RS485 通讯确认");
+ if (result.Success)
+ {
+ successCount++;
+ }
+ }
+ finally
+ {
+ EndRs485PumpOperation(pump);
+ }
+ }
+
+ var failedCount = pumps.Count - successCount;
+ var summary = failedCount == 0
+ ? $"RS485 通讯确认完成:{successCount} 台全部正常。"
+ : $"RS485 通讯确认完成:成功 {successCount} 台,失败 {failedCount} 台。";
+ Rs485StatusText = summary;
+ LatestAction = summary;
+ TraceEvents.Insert(0, NewTrace("RS485 通讯确认", summary));
}
[RelayCommand]
- private void RefreshAllPumpSetpoints()
+ private async Task RefreshAllPumpSetpoints()
{
foreach (var pump in Rs485FlowPumpControls.Where(item => item.SupportsRs485Preset))
{
- ReadPumpSetpoint(pump);
+ await ReadPumpSetpoint(pump);
}
}
[RelayCommand]
- private void ReadPumpSetpoint(PumpControlChannel? pump)
+ private async Task ReadPumpSetpoint(PumpControlChannel? pump)
{
if (!TryPreparePumpForRs485(pump, requireCalibration: false, out var activePump, out _))
{
@@ -358,7 +460,8 @@ public partial class MainViewModel
try
{
- var result = _rs485PumpFlowService.ReadPumpPreset(BuildRs485Request(activePump));
+ var request = BuildRs485Request(activePump);
+ var result = await Task.Run(() => _rs485PumpFlowService.ReadPumpPreset(request));
ApplyRs485OperationResult(activePump, result, "读取泵预设");
}
finally
@@ -368,48 +471,100 @@ public partial class MainViewModel
}
[RelayCommand]
- private void WritePumpSetpoint(PumpControlChannel? pump)
+ private async Task WritePumpSetpoint(PumpControlChannel? pump)
{
- if (!EnsureSessionEditable("RS485 预设流量写入"))
+ await TryWritePumpSetpointCore(pump, "RS485 预设流量写入", allowAutoStartAfterWrite: true);
+ }
+
+ [RelayCommand]
+ private async Task WriteAllPumpSetpoints()
+ {
+ if (!EnsureSessionEditable("RS485 批量预设流量写入"))
{
return;
}
+ var candidates = Rs485FlowPumpControls
+ .Where(item => item.SupportsRs485Preset)
+ .ToList();
+
+ if (candidates.Count == 0)
+ {
+ Rs485StatusText = "未找到已启用的 RS485 泵通道";
+ return;
+ }
+
+ var successCount = 0;
+ var failedCount = 0;
+ foreach (var pump in candidates)
+ {
+ if (await TryWritePumpSetpointCore(pump, "RS485 批量预设流量写入", allowAutoStartAfterWrite: false))
+ {
+ successCount++;
+ }
+ else
+ {
+ failedCount++;
+ }
+ }
+
+ var summary = $"批量写入完成:成功 {successCount} 台,失败 {failedCount} 台。批量模式仅写入目标值,不自动启动。";
+ Rs485StatusText = summary;
+ LatestAction = summary;
+ TraceEvents.Insert(0, NewTrace("RS485 批量写入", summary));
+ }
+
+ private async Task TryWritePumpSetpointCore(
+ PumpControlChannel? pump,
+ string operationName,
+ bool allowAutoStartAfterWrite)
+ {
+ if (!EnsureSessionEditable(operationName))
+ {
+ return false;
+ }
+
if (!TryPreparePumpForRs485(pump, requireCalibration: true, out var activePump, out var targetFlow))
{
- return;
+ return false;
}
if (activePump.IsRunning)
{
activePump.SetpointStatusText = "泵运行中,禁止改写预设";
Rs485StatusText = $"{activePump.Name} 运行中,禁止改写预设";
- return;
+ return false;
}
var rawValue = ConvertFlowToRawSpeed(activePump, targetFlow!.Value);
- if (rawValue is < 0 or > ushort.MaxValue)
+ if (rawValue is < 0 or > MaxRs485MotorCommand)
{
- activePump.SetpointStatusText = "换算后的速度值超出 0~65535";
+ activePump.SetpointStatusText = $"换算后的速度值超出 0~{MaxRs485MotorCommand}";
Rs485StatusText = $"{activePump.Name} 写入失败:速度值超出范围";
- return;
+ return false;
}
if (!TryBeginRs485PumpOperation(activePump, "写入"))
{
- return;
+ return false;
}
try
{
- var result = _rs485PumpFlowService.WritePumpPreset(BuildRs485Request(activePump), (ushort)rawValue);
+ var request = BuildRs485Request(activePump);
+ var result = await Task.Run(() => _rs485PumpFlowService.WritePumpPreset(request, (ushort)rawValue));
ApplyRs485OperationResult(activePump, result, "写入泵预设");
if (result.Success)
{
activePump.PendingSetpointText = targetFlow.Value.ToString("F2", CultureInfo.InvariantCulture);
CacheConfirmedRs485Setpoint(activePump, rawValue, targetFlow.Value);
- TryAutoStartPumpAfterRs485Write(activePump, targetFlow.Value);
+ if (allowAutoStartAfterWrite)
+ {
+ await TryAutoStartPumpAfterRs485Write(activePump, targetFlow.Value);
+ }
}
+
+ return result.Success;
}
finally
{
@@ -417,7 +572,7 @@ public partial class MainViewModel
}
}
- private void TryAutoStartPumpAfterRs485Write(PumpControlChannel pump, double targetFlow)
+ private async Task TryAutoStartPumpAfterRs485Write(PumpControlChannel pump, double targetFlow)
{
if (!Rs485AutoStartPumpAfterWrite)
{
@@ -445,12 +600,14 @@ public partial class MainViewModel
return;
}
- var directResult = _pumpActuationService.StartPump(BuildRs485Request(pump), rawMotorSpeed);
+ var request = BuildRs485Request(pump);
+ var directResult = await Task.Run(() => _pumpActuationService.StartPump(request, rawMotorSpeed));
ApplyRs485OperationResult(pump, directResult, "RS485 直接启泵");
if (directResult.Success)
{
CacheResolvedRs485Setpoint(pump, rawMotorSpeed);
- ApplyOptimisticRs485PumpState(pump, isRunning: true);
+ ApplyPostCommandPumpState(pump, directResult, expectedRunning: true);
+ await RefreshTelemetryAsync();
}
return;
@@ -585,6 +742,14 @@ public partial class MainViewModel
pump.IsRunning = result.RunStatus.Value == 1;
}
}
+ else if (IsRs485ManagedPump(pump))
+ {
+ pump.Rs485RunStatusCode = null;
+ if (!pump.PendingRs485RunningState.HasValue)
+ {
+ pump.StateAvailable = false;
+ }
+ }
LatestAction = $"{traceCategory}:{pump.Name} / {result.Message}";
TraceEvents.Insert(0, NewTrace(traceCategory, $"{pump.Name} / {result.Message}"));
@@ -601,6 +766,12 @@ public partial class MainViewModel
rawMotorSpeed = 0;
message = string.Empty;
+ if (!pump.HasConfirmedSetpointCalibration)
+ {
+ message = $"{pump.Name} 尚未完成流量换算标定确认";
+ return false;
+ }
+
var candidateRaw = pump.ConfirmedSetpointAvailable ? pump.ConfirmedRawSetpointValue : 0;
if (candidateRaw <= 0)
{
@@ -608,9 +779,9 @@ public partial class MainViewModel
return false;
}
- if (candidateRaw > short.MaxValue)
+ if (candidateRaw > MaxRs485MotorCommand)
{
- message = $"{pump.Name} 的直接启停控制值超过 32767";
+ message = $"{pump.Name} 的直接启停控制值超过 {MaxRs485MotorCommand}";
return false;
}
@@ -669,14 +840,26 @@ public partial class MainViewModel
}
}
- private static void ApplyOptimisticRs485PumpState(PumpControlChannel pump, bool isRunning)
+ private static void ApplyPostCommandPumpState(
+ PumpControlChannel pump,
+ Rs485PumpFlowOperationResult result,
+ bool expectedRunning)
{
- pump.IsRunning = isRunning;
- pump.StateAvailable = true;
- pump.Rs485RunStatusCode = (ushort)(isRunning ? 1 : 0);
+ if (result.RunStatus.HasValue)
+ {
+ pump.PendingRs485RunningState = null;
+ pump.IsRunning = result.RunStatus.Value == 1;
+ pump.StateAvailable = result.RunStatus.Value is 0 or 1;
+ pump.Rs485RunStatusCode = result.RunStatus.Value;
+ return;
+ }
+
+ pump.PendingRs485RunningState = expectedRunning;
+ pump.StateAvailable = false;
+ pump.Rs485RunStatusCode = null;
}
- private bool TryTogglePumpControlViaRs485(PumpControlChannel pump, bool nextState)
+ private async Task TryTogglePumpControlViaRs485(PumpControlChannel pump, bool nextState)
{
if (!ShouldUseRs485DirectPumpControl(pump))
{
@@ -712,22 +895,26 @@ public partial class MainViewModel
return true;
}
- var startResult = _pumpActuationService.StartPump(BuildRs485Request(pump), rawMotorSpeed);
+ var request = BuildRs485Request(pump);
+ var startResult = await Task.Run(() => _pumpActuationService.StartPump(request, rawMotorSpeed));
ApplyRs485OperationResult(pump, startResult, "RS485 泵控");
if (startResult.Success)
{
CacheResolvedRs485Setpoint(pump, rawMotorSpeed);
- ApplyOptimisticRs485PumpState(pump, isRunning: true);
+ ApplyPostCommandPumpState(pump, startResult, expectedRunning: true);
+ await RefreshTelemetryAsync();
}
return true;
}
- var stopResult = _pumpActuationService.StopPump(BuildRs485Request(pump));
+ var stopRequest = BuildRs485Request(pump);
+ var stopResult = await Task.Run(() => _pumpActuationService.StopPump(stopRequest));
ApplyRs485OperationResult(pump, stopResult, "RS485 泵控");
if (stopResult.Success)
{
- ApplyOptimisticRs485PumpState(pump, isRunning: false);
+ ApplyPostCommandPumpState(pump, stopResult, expectedRunning: false);
+ await RefreshTelemetryAsync();
}
return true;
diff --git a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
index c5bbb43..bea6666 100644
--- a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
+++ b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
@@ -26,10 +26,12 @@ public partial class MainViewModel : ObservableObject, IDisposable
private const double AntiCollapseTargetNegativePressure = -6.67;
private const double PressureKpaToMmHg = 7.50061683d;
private const int TrendHistoryCapacity = 60;
- private static readonly string LimitSettingsPath = Path.Combine(
+ private const string LimitSettingsFileName = "manufacturer-limits.json";
+ private static readonly string LegacyLimitSettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Cardiopulmonarybypasssystems",
- "manufacturer-limits.json");
+ LimitSettingsFileName);
+ private static readonly string LimitSettingsPath = ResolveLimitSettingsPath();
private double? _antiCollapseBaselinePressureDrop;
private double? _antiCollapseBaselineFlow;
private DateTime? _antiCollapseBaselineCapturedAt;
@@ -49,6 +51,73 @@ public partial class MainViewModel : ObservableObject, IDisposable
private double? _proximalPressureRawKpa;
private double? _distalPressureRawKpa;
+ private static string ResolveLimitSettingsPath()
+ {
+ var applicationPath = Path.Combine(AppContext.BaseDirectory, LimitSettingsFileName);
+ var developmentProjectDirectory = ResolveDevelopmentProjectDirectory();
+ if (!string.IsNullOrWhiteSpace(developmentProjectDirectory))
+ {
+ var projectPath = Path.Combine(developmentProjectDirectory, LimitSettingsFileName);
+ if (File.Exists(projectPath))
+ {
+ return projectPath;
+ }
+ }
+
+ return applicationPath;
+ }
+
+ private static string? ResolveDevelopmentProjectDirectory()
+ {
+ var candidates = new[]
+ {
+ Directory.GetCurrentDirectory(),
+ AppContext.BaseDirectory
+ };
+
+ foreach (var candidate in candidates)
+ {
+ if (string.IsNullOrWhiteSpace(candidate))
+ {
+ continue;
+ }
+
+ var directory = new DirectoryInfo(candidate);
+ while (directory is not null)
+ {
+ if (File.Exists(Path.Combine(directory.FullName, "Cardiopulmonarybypasssystems.csproj")))
+ {
+ return directory.FullName;
+ }
+
+ directory = directory.Parent;
+ }
+ }
+
+ return null;
+ }
+
+ private static void MigrateLegacyLimitSettingsIfNeeded()
+ {
+ if (string.Equals(LegacyLimitSettingsPath, LimitSettingsPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ if (!File.Exists(LegacyLimitSettingsPath) || File.Exists(LimitSettingsPath))
+ {
+ return;
+ }
+
+ var directory = Path.GetDirectoryName(LimitSettingsPath);
+ if (!string.IsNullOrWhiteSpace(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ File.Copy(LegacyLimitSettingsPath, LimitSettingsPath, overwrite: false);
+ }
+
[ObservableProperty]
private bool engineeringRegisterPanelVisible;
@@ -234,8 +303,8 @@ public partial class MainViewModel : ObservableObject, IDisposable
}
HemolysisTestParameters.PropertyChanged += OnHemolysisTestParametersPropertyChanged;
- LoadManufacturerLimitSettings();
InitializeRs485FlowControl();
+ LoadManufacturerLimitSettings();
SelectedItem = InspectionItems.FirstOrDefault();
if (SelectedItem is not null)
@@ -259,6 +328,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
public ObservableCollection Channels { get; }
public ObservableCollection PumpControls { get; }
public ObservableCollection ValveControls { get; }
+ public IEnumerable NegativeAssistPumpControls => PumpControlsFor("NegativeAssistPump");
public IEnumerable PressureDropPumpControls => PumpControlsFor("NegativeAssistPump", "PressureDropPump");
public IEnumerable RecirculationPumpControls => PumpControlsFor("RecirculationMainPump", "RecirculationReturnPump", "RecirculationDrainagePump");
public IEnumerable KinkResistancePumpControls => PumpControlsFor("KinkResistancePump");
@@ -610,7 +680,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
}
[RelayCommand]
- private void TogglePumpControl(PumpControlChannel? pump)
+ private async Task TogglePumpControl(PumpControlChannel? pump)
{
if (!EnsureSessionEditable("泵控"))
{
@@ -623,7 +693,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
}
var nextState = !pump.IsRunning;
- if (TryTogglePumpControlViaRs485(pump, nextState))
+ if (await TryTogglePumpControlViaRs485(pump, nextState))
{
return;
}
@@ -1722,14 +1792,6 @@ public partial class MainViewModel : ObservableObject, IDisposable
private ObservableCollection BuildEngineeringRegisters() =>
[
- CreateEngineeringRegister("流量系数 1", 1006, true),
- CreateEngineeringRegister("流量系数 2", 1016, true),
- CreateEngineeringRegister("流量系数 3", 1026, true),
- CreateEngineeringRegister("流量系数 4", 1036, true),
- CreateEngineeringRegister("流量系数 5", 1046, true),
- CreateEngineeringRegister("流量系数 6", 1056, true),
- CreateEngineeringRegister("流量系数 7", 1066, true),
- CreateEngineeringRegister("流量系数 8", 1076, true),
CreateEngineeringRegister("近端压力系数", 1328, true),
CreateEngineeringRegister("远端压力系数", 1378, true),
CreateEngineeringRegister("近端压力显示", 1330, false, usesFloatDisplay: true),
@@ -2335,6 +2397,8 @@ public partial class MainViewModel : ObservableObject, IDisposable
{
try
{
+ MigrateLegacyLimitSettingsIfNeeded();
+
if (!File.Exists(LimitSettingsPath))
{
return;
@@ -2698,6 +2762,33 @@ public partial class MainViewModel : ObservableObject, IDisposable
if (IsRs485ManagedPump(pump))
{
+ if (pump.PendingRs485RunningState.HasValue)
+ {
+ if (pump.FlowAvailable)
+ {
+ var inferredRunning = pump.IsFlowEstablished;
+ if (inferredRunning == pump.PendingRs485RunningState.Value)
+ {
+ pump.IsRunning = inferredRunning;
+ pump.StateAvailable = true;
+ pump.PendingRs485RunningState = null;
+ }
+ else
+ {
+ pump.StateAvailable = false;
+ }
+ }
+ else
+ {
+ pump.StateAvailable = false;
+ }
+ }
+ else if (!pump.Rs485RunStatusCode.HasValue && pump.FlowAvailable)
+ {
+ pump.IsRunning = pump.IsFlowEstablished;
+ pump.StateAvailable = true;
+ }
+
continue;
}
diff --git a/Cardiopulmonarybypasssystems/manufacturer-limits.json b/Cardiopulmonarybypasssystems/manufacturer-limits.json
new file mode 100644
index 0000000..2a6e268
--- /dev/null
+++ b/Cardiopulmonarybypasssystems/manufacturer-limits.json
@@ -0,0 +1,162 @@
+{
+ "ProductModel": "24Fr/32Fr \u53CC\u8154",
+ "ApplicablePopulation": "\u6210\u4EBA",
+ "RatedMaxFlow": 6,
+ "KinkResistanceMinimumFlow": 1,
+ "KinkResistanceOuterDiameter": 8,
+ "PressureDropLimit50": 20,
+ "PressureDropLimit75": 22,
+ "PressureDropLimit100": 24,
+ "AntiCollapseAllowedIncreaseRate": 50,
+ "RecirculationAllowedLimit": 8,
+ "Rs485SerialSettings": {
+ "PortName": "COM3",
+ "BaudRate": 9600,
+ "Parity": "Even",
+ "DataBits": 8,
+ "StopBits": 1,
+ "ReadTimeoutMs": 500,
+ "WriteTimeoutMs": 500,
+ "AutoSwitchPresetMode": false,
+ "PersistPresetAfterWrite": false,
+ "AutoStartPumpAfterWrite": false
+ },
+ "Rs485PumpBindings": [
+ {
+ "PumpKey": "PressureDropPump",
+ "Enabled": true,
+ "CalibrationConfirmed": false,
+ "SlaveAddress": 1,
+ "ForwardSpeedRegister": 162,
+ "ReverseSpeedRegister": 163,
+ "RunStatusRegister": 243,
+ "DeviceAddressRegister": 250,
+ "PresetModeRegister": 251,
+ "SavePresetRegister": 416,
+ "MotorControlRegister": 64,
+ "RawPerLitrePerMinute": 1000,
+ "RawOffset": 0,
+ "MinFlowLpm": 0,
+ "MaxFlowLpm": 7
+ },
+ {
+ "PumpKey": "RecirculationMainPump",
+ "Enabled": true,
+ "CalibrationConfirmed": false,
+ "SlaveAddress": 2,
+ "ForwardSpeedRegister": 162,
+ "ReverseSpeedRegister": 163,
+ "RunStatusRegister": 243,
+ "DeviceAddressRegister": 250,
+ "PresetModeRegister": 251,
+ "SavePresetRegister": 416,
+ "MotorControlRegister": 64,
+ "RawPerLitrePerMinute": 1000,
+ "RawOffset": 0,
+ "MinFlowLpm": 0,
+ "MaxFlowLpm": 7
+ },
+ {
+ "PumpKey": "RecirculationReturnPump",
+ "Enabled": true,
+ "CalibrationConfirmed": false,
+ "SlaveAddress": 3,
+ "ForwardSpeedRegister": 162,
+ "ReverseSpeedRegister": 163,
+ "RunStatusRegister": 243,
+ "DeviceAddressRegister": 250,
+ "PresetModeRegister": 251,
+ "SavePresetRegister": 416,
+ "MotorControlRegister": 64,
+ "RawPerLitrePerMinute": 1000,
+ "RawOffset": 0,
+ "MinFlowLpm": 0,
+ "MaxFlowLpm": 7
+ },
+ {
+ "PumpKey": "RecirculationDrainagePump",
+ "Enabled": true,
+ "CalibrationConfirmed": false,
+ "SlaveAddress": 4,
+ "ForwardSpeedRegister": 162,
+ "ReverseSpeedRegister": 163,
+ "RunStatusRegister": 243,
+ "DeviceAddressRegister": 250,
+ "PresetModeRegister": 251,
+ "SavePresetRegister": 416,
+ "MotorControlRegister": 64,
+ "RawPerLitrePerMinute": 1000,
+ "RawOffset": 0,
+ "MinFlowLpm": 0,
+ "MaxFlowLpm": 7
+ },
+ {
+ "PumpKey": "KinkResistancePump",
+ "Enabled": true,
+ "CalibrationConfirmed": false,
+ "SlaveAddress": 5,
+ "ForwardSpeedRegister": 162,
+ "ReverseSpeedRegister": 163,
+ "RunStatusRegister": 243,
+ "DeviceAddressRegister": 250,
+ "PresetModeRegister": 251,
+ "SavePresetRegister": 416,
+ "MotorControlRegister": 64,
+ "RawPerLitrePerMinute": 1000,
+ "RawOffset": 0,
+ "MinFlowLpm": 0,
+ "MaxFlowLpm": 7
+ },
+ {
+ "PumpKey": "HemolysisDrainageSinglePump",
+ "Enabled": true,
+ "CalibrationConfirmed": false,
+ "SlaveAddress": 6,
+ "ForwardSpeedRegister": 162,
+ "ReverseSpeedRegister": 163,
+ "RunStatusRegister": 243,
+ "DeviceAddressRegister": 250,
+ "PresetModeRegister": 251,
+ "SavePresetRegister": 416,
+ "MotorControlRegister": 64,
+ "RawPerLitrePerMinute": 1000,
+ "RawOffset": 0,
+ "MinFlowLpm": 0,
+ "MaxFlowLpm": 7
+ },
+ {
+ "PumpKey": "HemolysisReturnSinglePump",
+ "Enabled": true,
+ "CalibrationConfirmed": false,
+ "SlaveAddress": 7,
+ "ForwardSpeedRegister": 162,
+ "ReverseSpeedRegister": 163,
+ "RunStatusRegister": 243,
+ "DeviceAddressRegister": 250,
+ "PresetModeRegister": 251,
+ "SavePresetRegister": 416,
+ "MotorControlRegister": 64,
+ "RawPerLitrePerMinute": 1000,
+ "RawOffset": 0,
+ "MinFlowLpm": 0,
+ "MaxFlowLpm": 7
+ },
+ {
+ "PumpKey": "HemolysisDualLumenPump",
+ "Enabled": true,
+ "CalibrationConfirmed": false,
+ "SlaveAddress": 8,
+ "ForwardSpeedRegister": 162,
+ "ReverseSpeedRegister": 163,
+ "RunStatusRegister": 243,
+ "DeviceAddressRegister": 250,
+ "PresetModeRegister": 251,
+ "SavePresetRegister": 416,
+ "MotorControlRegister": 64,
+ "RawPerLitrePerMinute": 1000,
+ "RawOffset": 0,
+ "MinFlowLpm": 0,
+ "MaxFlowLpm": 7
+ }
+ ]
+}