diff --git a/Cardiopulmonarybypasssystems/EngineeringRegistersWindow.xaml b/Cardiopulmonarybypasssystems/EngineeringRegistersWindow.xaml
index 639bc98..d355cda 100644
--- a/Cardiopulmonarybypasssystems/EngineeringRegistersWindow.xaml
+++ b/Cardiopulmonarybypasssystems/EngineeringRegistersWindow.xaml
@@ -61,12 +61,13 @@
+
diff --git a/Cardiopulmonarybypasssystems/Models/ManufacturerLimitSettings.cs b/Cardiopulmonarybypasssystems/Models/ManufacturerLimitSettings.cs
index 9b268d6..b5fa139 100644
--- a/Cardiopulmonarybypasssystems/Models/ManufacturerLimitSettings.cs
+++ b/Cardiopulmonarybypasssystems/Models/ManufacturerLimitSettings.cs
@@ -12,4 +12,6 @@ public sealed class ManufacturerLimitSettings
public double PressureDropLimit100 { get; set; } = 24;
public double AntiCollapseAllowedIncreaseRate { get; set; } = 50;
public double RecirculationAllowedLimit { get; set; } = 8;
+ public Rs485SerialSettings Rs485SerialSettings { get; set; } = new();
+ public List Rs485PumpBindings { get; set; } = [];
}
diff --git a/Cardiopulmonarybypasssystems/Models/PumpControlChannel.cs b/Cardiopulmonarybypasssystems/Models/PumpControlChannel.cs
index 203b9e8..0130c7c 100644
--- a/Cardiopulmonarybypasssystems/Models/PumpControlChannel.cs
+++ b/Cardiopulmonarybypasssystems/Models/PumpControlChannel.cs
@@ -121,6 +121,15 @@ public partial class PumpControlChannel : ObservableObject
[ObservableProperty]
private string flowStabilizationStatusText = "稳流未启用";
+ [ObservableProperty]
+ private int consecutiveFlowStabilizationFailureCount;
+
+ [ObservableProperty]
+ private int consecutiveFlowStabilizationLimitCount;
+
+ [ObservableProperty]
+ private int consecutiveFlowStabilizationUnavailableCount;
+
public string StartAddressDisplay => $"M{StartAddress}";
public string FlowAddressDisplay => FlowAddress.HasValue ? $"D{FlowAddress.Value}" : "-";
public bool HasFlowTelemetry => FlowAddress.HasValue;
@@ -129,20 +138,22 @@ public partial class PumpControlChannel : ObservableObject
public bool UsesLegacyPlcDirectControl => Key == "NegativeAssistPump";
public bool HideRealtimeCardStateDescription => Key == "NegativeAssistPump";
public bool HasSetpointCalibration => Rs485RawPerLitrePerMinute > 0;
- public bool HasConfirmedSetpointCalibration => HasSetpointCalibration;
+ public bool HasConfirmedSetpointCalibration => HasSetpointCalibration && Rs485CalibrationConfirmed;
public bool IsFlowEstablished => !HasFlowTelemetry || (FlowAvailable && FlowValue >= FlowEstablishedThreshold);
public string Rs485SlaveAddressDisplay => SupportsRs485Preset ? Rs485SlaveAddress.ToString() : "-";
public string CalibrationStatusText => !Rs485Enabled
? "未启用"
- : HasSetpointCalibration
+ : HasConfirmedSetpointCalibration
? "已确认"
- : "未配置";
+ : HasSetpointCalibration
+ ? "待确认"
+ : "未配置";
public string SetpointReadbackDisplay => !SupportsRs485Preset
? "-"
: SetpointAvailable
- ? HasSetpointCalibration
+ ? HasConfirmedSetpointCalibration
? $"{SetpointFlowValue:F2} L/min"
- : "未配置换算"
+ : "未确认换算"
: "--";
public string RawSetpointDisplay => !SupportsRs485Preset
? "-"
@@ -226,14 +237,14 @@ public partial class PumpControlChannel : ObservableObject
? "泵已在运行"
: HasConfirmedSetpointCalibration
? string.Empty
- : "未配置流量换算系数";
+ : "未确认流量换算系数";
public string StopActionHint => IsRs485Busy ? "RS485 操作中" : string.Empty;
public bool CanToggleRs485Action => PendingRs485RunningState == true || IsRunning || HasConfirmedSetpointCalibration;
public string ToggleActionHint => PendingRs485RunningState == true
? "启动确认中,可执行停止"
: CanToggleRs485Action
? string.Empty
- : "未配置流量换算系数";
+ : "未确认流量换算系数";
public string Rs485ReadActionText => IsRs485Busy ? "处理中" : "读取";
public string Rs485WriteActionText => IsRs485Busy ? "处理中" : "写入";
public bool CanUseFlowStabilization => SupportsRs485DirectControl && Key != "KinkResistancePump";
@@ -244,7 +255,9 @@ public partial class PumpControlChannel : ObservableObject
? "固定转速"
: IsFlowStabilizationEnabled
? FlowStabilizationStatusText
- : "稳流未启用";
+ : string.IsNullOrWhiteSpace(FlowStabilizationStatusText)
+ ? "稳流未启用"
+ : FlowStabilizationStatusText;
public string SetpointStatusForeground => ResolveSetpointStatusForeground();
public string SetpointStatusBackground => ResolveSetpointStatusBackground();
@@ -325,6 +338,9 @@ public partial class PumpControlChannel : ObservableObject
if (!value)
{
FlowStabilizationStatusText = "稳流未启用";
+ ConsecutiveFlowStabilizationFailureCount = 0;
+ ConsecutiveFlowStabilizationLimitCount = 0;
+ ConsecutiveFlowStabilizationUnavailableCount = 0;
}
OnPropertyChanged(nameof(FlowStabilizationStateText));
@@ -363,6 +379,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StartActionHint));
OnPropertyChanged(nameof(CanToggleRs485Action));
OnPropertyChanged(nameof(ToggleActionHint));
+ OnPropertyChanged(nameof(SetpointReadbackDisplay));
}
partial void OnSetpointFlowValueChanged(double value) => OnPropertyChanged(nameof(SetpointReadbackDisplay));
diff --git a/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
index 987dd68..5d39249 100644
--- a/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
+++ b/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
@@ -22,14 +22,14 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
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
+ ["PressureDropPump"] = 1008,
+ ["RecirculationMainPump"] = 1018,
+ ["RecirculationReturnPump"] = 1028,
+ ["RecirculationDrainagePump"] = 1038,
+ ["KinkResistancePump"] = 1048,
+ ["HemolysisDrainageSinglePump"] = 1058,
+ ["HemolysisReturnSinglePump"] = 1068,
+ ["HemolysisDualLumenPump"] = 1078
};
private static readonly IReadOnlyDictionary FlowChannelNames = new Dictionary(StringComparer.Ordinal)
@@ -698,9 +698,7 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
private void SetChannelValue(string channelName, double nextValue, bool isAvailable)
{
var channel = Channel(channelName);
- channel.Value = ShouldClampChannelValue(channelName)
- ? Math.Clamp(nextValue, channel.Min, channel.Max)
- : nextValue;
+ channel.Value = nextValue;
channel.IsAvailable = isAvailable;
}
@@ -778,8 +776,5 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
private static bool IsPlausiblePressureKpa(float value) =>
!float.IsNaN(value) && !float.IsInfinity(value) && value is > -1000f and < 1000f;
- private static bool ShouldClampChannelValue(string channelName) =>
- channelName is not "近端压力" and not "远端压力";
-
private DeviceChannel Channel(string name) => _channels.First(channel => channel.Name == name);
}
diff --git a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.Rs485.cs b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.Rs485.cs
index c7924e9..f5798b3 100644
--- a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.Rs485.cs
+++ b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.Rs485.cs
@@ -44,6 +44,9 @@ public partial class MainViewModel
private const double FlowStabilizationDeadbandLpm = 0.05d;
private const int FlowStabilizationMaxRawStep = 5;
private const double FlowStabilizationMaxRelativeTrim = 0.20d;
+ private const int FlowStabilizationMaxConsecutiveFailures = 3;
+ private const int FlowStabilizationMaxConsecutiveLimitHits = 3;
+ private const int FlowStabilizationMaxUnavailableCycles = 3;
private const string PressureDropRs485PumpKey = "PressureDropPump";
private const string KinkResistanceRs485PumpKey = "KinkResistancePump";
private static readonly TimeSpan FlowStabilizationAdjustmentInterval = TimeSpan.FromSeconds(2);
@@ -62,6 +65,7 @@ public partial class MainViewModel
private static readonly TimeSpan Rs485RuntimeRefreshInterval = TimeSpan.FromSeconds(4);
private DateTime _lastRs485RuntimeRefreshUtc = DateTime.MinValue;
private string _lastRs485RuntimeRefreshFailureMessage = string.Empty;
+ private bool _suppressRs485SettingsSave;
[ObservableProperty]
private string rs485PortName = "COM9";
@@ -203,6 +207,81 @@ public partial class MainViewModel
AutoStartPumpAfterWrite = Rs485AutoStartPumpAfterWrite
};
+ private void ApplyRs485SerialSettings(Rs485SerialSettings? settings)
+ {
+ if (settings is null)
+ {
+ return;
+ }
+
+ if (!string.IsNullOrWhiteSpace(settings.PortName))
+ {
+ Rs485PortName = settings.PortName;
+ }
+
+ if (settings.BaudRate > 0)
+ {
+ Rs485BaudRate = settings.BaudRate;
+ }
+
+ if (!string.IsNullOrWhiteSpace(settings.Parity))
+ {
+ Rs485Parity = settings.Parity;
+ }
+
+ if (settings.DataBits > 0)
+ {
+ Rs485DataBits = settings.DataBits;
+ }
+
+ Rs485StopBits = settings.StopBits <= 0 ? 1 : settings.StopBits;
+ Rs485ReadTimeoutMs = settings.ReadTimeoutMs > 0 ? settings.ReadTimeoutMs : Rs485ReadTimeoutMs;
+ Rs485WriteTimeoutMs = settings.WriteTimeoutMs > 0 ? settings.WriteTimeoutMs : Rs485WriteTimeoutMs;
+ Rs485AutoSwitchPresetMode = settings.AutoSwitchPresetMode;
+ Rs485PersistPresetAfterWrite = settings.PersistPresetAfterWrite;
+ Rs485AutoStartPumpAfterWrite = settings.AutoStartPumpAfterWrite;
+ }
+
+ private void ApplyRs485Bindings(IReadOnlyList? bindings)
+ {
+ var defaults = BuildDefaultRs485PumpBindings();
+ var bindingMap = (bindings ?? Array.Empty())
+ .Where(item => !string.IsNullOrWhiteSpace(item.PumpKey))
+ .GroupBy(item => item.PumpKey, StringComparer.Ordinal)
+ .ToDictionary(group => group.Key, group => group.Last(), StringComparer.Ordinal);
+
+ foreach (var pump in Rs485FlowPumpControls)
+ {
+ bindingMap.TryGetValue(pump.Key, out var binding);
+ ApplyRs485Binding(pump, binding ?? defaults.FirstOrDefault(item => item.PumpKey == pump.Key));
+ }
+
+ RefreshActiveRs485FlowPumpControls();
+ OnPropertyChanged(nameof(Rs485ConnectionSummary));
+ }
+
+ private List BuildCurrentRs485PumpBindings() =>
+ Rs485FlowPumpControls
+ .Select(pump => new Rs485PumpBindingSettings
+ {
+ PumpKey = pump.Key,
+ Enabled = pump.Rs485Enabled,
+ CalibrationConfirmed = pump.Rs485CalibrationConfirmed,
+ SlaveAddress = pump.Rs485SlaveAddress,
+ ForwardSpeedRegister = pump.Rs485ForwardSpeedRegister,
+ ReverseSpeedRegister = pump.Rs485ReverseSpeedRegister,
+ RunStatusRegister = pump.Rs485RunStatusRegister,
+ DeviceAddressRegister = pump.Rs485DeviceAddressRegister,
+ PresetModeRegister = pump.Rs485PresetModeRegister,
+ SavePresetRegister = pump.Rs485SavePresetRegister,
+ MotorControlRegister = pump.Rs485MotorControlRegister,
+ RawPerLitrePerMinute = pump.Rs485RawPerLitrePerMinute,
+ RawOffset = pump.Rs485RawOffset,
+ MinFlowLpm = pump.Rs485MinFlowLpm,
+ MaxFlowLpm = pump.Rs485MaxFlowLpm
+ })
+ .ToList();
+
private List BuildDefaultRs485PumpBindings()
{
var bindings = new List();
@@ -212,7 +291,7 @@ public partial class MainViewModel
{
PumpKey = Rs485PumpKeys[index],
Enabled = true,
- CalibrationConfirmed = true,
+ CalibrationConfirmed = false,
SlaveAddress = (byte)(index + 1),
ForwardSpeedRegister = 0x00A2,
ReverseSpeedRegister = 0x00A3,
@@ -270,6 +349,10 @@ public partial class MainViewModel
{
pump.SetpointStatusText = "未配置 L/min 与速度换算系数";
}
+ else if (!pump.Rs485CalibrationConfirmed)
+ {
+ pump.SetpointStatusText = "换算系数待确认";
+ }
else
{
pump.SetpointStatusText = "换算系数已确认";
@@ -312,6 +395,10 @@ public partial class MainViewModel
{
pump.SetpointStatusText = "未配置 L/min 与速度换算系数";
}
+ else if (!pump.Rs485CalibrationConfirmed)
+ {
+ pump.SetpointStatusText = "换算系数待确认";
+ }
else
{
pump.SetpointStatusText = "换算系数已确认";
@@ -579,6 +666,7 @@ public partial class MainViewModel
if (!pump.CanUseFlowStabilization)
{
pump.FlowStabilizationStatusText = "当前项目要求固定转速";
+ ResetFlowStabilizationProtectionCounters(pump);
return false;
}
@@ -588,21 +676,33 @@ public partial class MainViewModel
return false;
}
+ if (pump.PendingRs485RunningState == true)
+ {
+ pump.FlowStabilizationStatusText = "等待启动确认";
+ return false;
+ }
+
if (!pump.IsRunning || pump.PendingRs485RunningState == false)
{
pump.FlowStabilizationStatusText = "等待泵运行";
+ ResetFlowStabilizationProtectionCounters(pump);
return false;
}
if (!pump.FlowAvailable)
{
- pump.FlowStabilizationStatusText = "等待流量反馈";
+ if (RegisterFlowStabilizationUnavailable(pump, "等待流量反馈"))
+ {
+ return false;
+ }
+
return false;
}
if (!pump.ConfirmedSetpointAvailable || pump.ConfirmedSetpointFlowValue <= 0)
{
pump.FlowStabilizationStatusText = "等待确认目标流量";
+ ResetFlowStabilizationProtectionCounters(pump);
return false;
}
@@ -620,6 +720,7 @@ public partial class MainViewModel
var flowError = targetFlow - pump.FlowValue;
if (Math.Abs(flowError) <= FlowStabilizationDeadbandLpm)
{
+ ResetFlowStabilizationProtectionCounters(pump);
pump.FlowStabilizationStatusText = $"稳流保持:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
return;
@@ -630,7 +731,7 @@ public partial class MainViewModel
: ConvertFlowToRawSpeed(pump, targetFlow);
if (targetRaw <= 0)
{
- pump.FlowStabilizationStatusText = "目标流量换算值无效";
+ DisableFlowStabilizationForProtection(pump, "稳流保护:目标流量换算值无效");
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
return;
}
@@ -648,7 +749,18 @@ public partial class MainViewModel
if (nextRaw == currentRaw)
{
- pump.FlowStabilizationStatusText = $"稳流已到调节限幅:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
+ pump.ConsecutiveFlowStabilizationLimitCount++;
+ pump.ConsecutiveFlowStabilizationFailureCount = 0;
+ pump.ConsecutiveFlowStabilizationUnavailableCount = 0;
+ var message = $"稳流已到调节限幅:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
+ if (pump.ConsecutiveFlowStabilizationLimitCount >= FlowStabilizationMaxConsecutiveLimitHits)
+ {
+ DisableFlowStabilizationForProtection(pump, $"稳流保护:{message},已连续 {pump.ConsecutiveFlowStabilizationLimitCount} 次无法继续调节");
+ pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
+ return;
+ }
+
+ pump.FlowStabilizationStatusText = $"{message}({pump.ConsecutiveFlowStabilizationLimitCount}/{FlowStabilizationMaxConsecutiveLimitHits})";
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
return;
}
@@ -664,12 +776,26 @@ public partial class MainViewModel
var result = await Task.Run(() => _rs485PumpFlowService.WritePumpMotorCommand(request, (short)nextRaw));
if (!result.Success)
{
- pump.FlowStabilizationStatusText = result.Message;
+ if (RegisterFlowStabilizationCommandFailure(pump, result.Message))
+ {
+ return;
+ }
+
Rs485StatusText = result.Message;
TraceEvents.Insert(0, NewTrace("RS485 稳流调节失败", $"{pump.Name} / {result.Message}"));
return;
}
+ if (result.RunStatus.HasValue && result.RunStatus.Value != 1)
+ {
+ DisableFlowStabilizationForProtection(pump, $"稳流保护:{pump.Name} 运行状态返回 {result.RunStatus.Value},停止自动调节");
+ ApplyPostCommandPumpState(pump, result, expectedRunning: true);
+ return;
+ }
+
+ pump.ConsecutiveFlowStabilizationFailureCount = 0;
+ pump.ConsecutiveFlowStabilizationLimitCount = 0;
+ pump.ConsecutiveFlowStabilizationUnavailableCount = 0;
pump.FlowStabilizationRawSetpoint = nextRaw;
CacheResolvedRs485Setpoint(pump, nextRaw);
ApplyPostCommandPumpState(pump, result, expectedRunning: true);
@@ -682,6 +808,59 @@ public partial class MainViewModel
}
}
+ private bool RegisterFlowStabilizationUnavailable(PumpControlChannel pump, string message)
+ {
+ pump.ConsecutiveFlowStabilizationUnavailableCount++;
+ pump.ConsecutiveFlowStabilizationFailureCount = 0;
+ pump.ConsecutiveFlowStabilizationLimitCount = 0;
+
+ if (pump.ConsecutiveFlowStabilizationUnavailableCount >= FlowStabilizationMaxUnavailableCycles)
+ {
+ DisableFlowStabilizationForProtection(
+ pump,
+ $"稳流保护:{message},已连续 {pump.ConsecutiveFlowStabilizationUnavailableCount} 次无有效反馈");
+ return true;
+ }
+
+ pump.FlowStabilizationStatusText = $"{message}({pump.ConsecutiveFlowStabilizationUnavailableCount}/{FlowStabilizationMaxUnavailableCycles})";
+ return false;
+ }
+
+ private bool RegisterFlowStabilizationCommandFailure(PumpControlChannel pump, string message)
+ {
+ pump.ConsecutiveFlowStabilizationFailureCount++;
+ pump.ConsecutiveFlowStabilizationLimitCount = 0;
+ pump.ConsecutiveFlowStabilizationUnavailableCount = 0;
+
+ if (pump.ConsecutiveFlowStabilizationFailureCount >= FlowStabilizationMaxConsecutiveFailures)
+ {
+ DisableFlowStabilizationForProtection(
+ pump,
+ $"稳流保护:{message},已连续 {pump.ConsecutiveFlowStabilizationFailureCount} 次调节失败");
+ return true;
+ }
+
+ pump.FlowStabilizationStatusText = $"{message}({pump.ConsecutiveFlowStabilizationFailureCount}/{FlowStabilizationMaxConsecutiveFailures})";
+ return false;
+ }
+
+ private void DisableFlowStabilizationForProtection(PumpControlChannel pump, string message)
+ {
+ pump.IsFlowStabilizationEnabled = false;
+ pump.FlowStabilizationStatusText = message;
+ ResetFlowStabilizationProtectionCounters(pump);
+ Rs485StatusText = message;
+ LatestAction = message;
+ TraceEvents.Insert(0, NewTrace("RS485 稳流保护", $"{pump.Name} / {message}"));
+ }
+
+ private static void ResetFlowStabilizationProtectionCounters(PumpControlChannel pump)
+ {
+ pump.ConsecutiveFlowStabilizationFailureCount = 0;
+ pump.ConsecutiveFlowStabilizationLimitCount = 0;
+ pump.ConsecutiveFlowStabilizationUnavailableCount = 0;
+ }
+
private void RaiseRs485CalibrationSummaryChanges()
{
OnPropertyChanged(nameof(Rs485EnabledPumpCount));
@@ -699,6 +878,13 @@ public partial class MainViewModel
private void UpdateAndPersistRs485Settings()
{
OnPropertyChanged(nameof(Rs485ConnectionSummary));
+ RaiseRs485CalibrationSummaryChanges();
+ if (_suppressRs485SettingsSave)
+ {
+ return;
+ }
+
+ SaveManufacturerLimitSettings();
}
[RelayCommand]
@@ -721,40 +907,50 @@ public partial class MainViewModel
[RelayCommand]
private void ApplySingleDeviceRs485Profile()
{
- Rs485BaudRate = 9600;
- Rs485Parity = "Even";
- Rs485DataBits = 8;
- Rs485StopBits = 1;
- Rs485ReadTimeoutMs = 500;
- Rs485WriteTimeoutMs = 500;
- Rs485AutoSwitchPresetMode = false;
- Rs485PersistPresetAfterWrite = false;
- Rs485AutoStartPumpAfterWrite = false;
- var primaryPump = Rs485FlowPumpControls.FirstOrDefault();
- foreach (var pump in Rs485FlowPumpControls)
+ var previousSuppress = _suppressRs485SettingsSave;
+ _suppressRs485SettingsSave = true;
+ try
{
- pump.Rs485Enabled = ReferenceEquals(pump, primaryPump);
- pump.Rs485SlaveAddress = 1;
- pump.Rs485ForwardSpeedRegister = 0x00A2;
- pump.Rs485ReverseSpeedRegister = 0x00A3;
- pump.Rs485RunStatusRegister = 0x00F3;
- pump.Rs485DeviceAddressRegister = 0x00FA;
- pump.Rs485PresetModeRegister = 0x00FB;
- pump.Rs485SavePresetRegister = 0x01A0;
- pump.Rs485MotorControlRegister = 0x0040;
- pump.Rs485RawPerLitrePerMinute = DefaultRs485RawPerLitrePerMinute;
- pump.Rs485RawOffset = DefaultRs485RawOffset;
- pump.Rs485CalibrationConfirmed = true;
- pump.Rs485MinFlowLpm = 0;
- pump.Rs485MaxFlowLpm = 1.0;
- pump.SetpointStatusText = pump.Rs485Enabled
- ? "已应用当前配置"
- : "当前未启用";
+ Rs485BaudRate = 9600;
+ Rs485Parity = "Even";
+ Rs485DataBits = 8;
+ Rs485StopBits = 1;
+ Rs485ReadTimeoutMs = 500;
+ Rs485WriteTimeoutMs = 500;
+ Rs485AutoSwitchPresetMode = false;
+ Rs485PersistPresetAfterWrite = false;
+ Rs485AutoStartPumpAfterWrite = false;
+ var primaryPump = Rs485FlowPumpControls.FirstOrDefault();
+ foreach (var pump in Rs485FlowPumpControls)
+ {
+ pump.Rs485Enabled = ReferenceEquals(pump, primaryPump);
+ pump.Rs485SlaveAddress = 1;
+ pump.Rs485ForwardSpeedRegister = 0x00A2;
+ pump.Rs485ReverseSpeedRegister = 0x00A3;
+ pump.Rs485RunStatusRegister = 0x00F3;
+ pump.Rs485DeviceAddressRegister = 0x00FA;
+ pump.Rs485PresetModeRegister = 0x00FB;
+ pump.Rs485SavePresetRegister = 0x01A0;
+ pump.Rs485MotorControlRegister = 0x0040;
+ pump.Rs485RawPerLitrePerMinute = DefaultRs485RawPerLitrePerMinute;
+ pump.Rs485RawOffset = DefaultRs485RawOffset;
+ pump.Rs485CalibrationConfirmed = false;
+ pump.Rs485MinFlowLpm = 0;
+ pump.Rs485MaxFlowLpm = 1.0;
+ pump.SetpointStatusText = pump.Rs485Enabled
+ ? "已应用当前配置,待确认换算系数"
+ : "当前未启用";
+ }
+
+ Rs485StatusText = primaryPump is null
+ ? "未找到可配置的 RS485 泵通道"
+ : $"已完成伺服器快速配置:启用 {primaryPump.Name} / 从站 1";
+ }
+ finally
+ {
+ _suppressRs485SettingsSave = previousSuppress;
}
- Rs485StatusText = primaryPump is null
- ? "未找到可配置的 RS485 泵通道"
- : $"已完成伺服器快速配置:启用 {primaryPump.Name} / 从站 1";
UpdateAndPersistRs485Settings();
}
@@ -1324,6 +1520,13 @@ public partial class MainViewModel
return false;
}
+ if (requireCalibration && !pump.Rs485CalibrationConfirmed)
+ {
+ pump.SetpointStatusText = "请先确认 L/min 与速度换算系数";
+ Rs485StatusText = $"{pump.Name} 换算系数未确认";
+ return false;
+ }
+
if (!requireCalibration)
{
return true;
@@ -1440,6 +1643,12 @@ public partial class MainViewModel
return false;
}
+ if (!pump.Rs485CalibrationConfirmed)
+ {
+ message = $"{pump.Name} 尚未确认流量换算系数";
+ return false;
+ }
+
var candidateRaw = pump.ConfirmedSetpointAvailable ? pump.ConfirmedRawSetpointValue : 0;
if (candidateRaw <= 0
&& double.TryParse(
@@ -1519,6 +1728,8 @@ public partial class MainViewModel
{
pump.SetpointFlowValue = ConvertRawSpeedToFlow(pump, rawMotorSpeed);
}
+
+ pump.FlowStabilizationRawSetpoint = rawMotorSpeed;
}
private static void ApplyPostCommandPumpState(
@@ -1541,6 +1752,8 @@ public partial class MainViewModel
pump.IsRunning = false;
pump.StateAvailable = true;
pump.Rs485RunStatusCode = 0;
+ pump.FlowStabilizationRawSetpoint = 0;
+ ResetFlowStabilizationProtectionCounters(pump);
return;
}
diff --git a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
index 773e275..d622659 100644
--- a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
+++ b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
@@ -1007,6 +1007,11 @@ public partial class MainViewModel : ObservableObject, IDisposable
return;
}
+ if (!EnsureFlowStabilizationReadyForSample("抗塌陷基线采集", PressureDropRs485PumpKey))
+ {
+ return;
+ }
+
_antiCollapseBaselinePressureDrop = DeltaPressure;
_antiCollapseBaselineFlow = PumpFlow;
_antiCollapseBaselineCapturedAt = DateTime.Now;
@@ -1051,6 +1056,11 @@ public partial class MainViewModel : ObservableObject, IDisposable
return;
}
+ if (!EnsureFlowStabilizationReadyForSample("抗塌陷比较采集", PressureDropRs485PumpKey))
+ {
+ return;
+ }
+
var resultText = BuildAntiCollapseMeasuredText();
var noteText = BuildAntiCollapseRecordNote();
var comparison = GetAntiCollapseComparison();
@@ -2086,6 +2096,89 @@ public partial class MainViewModel : ObservableObject, IDisposable
private double ChannelValueOrDefault(string name) => TryGetChannel(name, out var channel) && channel.IsAvailable ? channel.Value : 0d;
private bool HasChannelTelemetry(params string[] names) => names.All(name => TryGetChannel(name, out var channel) && channel.IsAvailable);
+ private bool EnsureFlowStabilizationReadyForSample(string actionName, params string[] pumpKeys)
+ {
+ foreach (var pumpKey in pumpKeys.Distinct(StringComparer.Ordinal))
+ {
+ var pump = PumpControls.FirstOrDefault(item => string.Equals(item.Key, pumpKey, StringComparison.Ordinal));
+ if (pump is null)
+ {
+ continue;
+ }
+
+ var hasProtectionFault = pump.FlowStabilizationStatusText.StartsWith("稳流保护", StringComparison.Ordinal);
+ if (!pump.IsFlowStabilizationEnabled && !hasProtectionFault)
+ {
+ continue;
+ }
+
+ if (IsFlowStabilizationReadyForSample(pump, hasProtectionFault, out var reason))
+ {
+ continue;
+ }
+
+ var message = $"{actionName}已阻止:{pump.Name} {reason}";
+ LatestAction = message;
+ TraceEvents.Insert(0, NewTrace("稳流采样保护", message));
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool IsFlowStabilizationReadyForSample(
+ PumpControlChannel pump,
+ bool hasProtectionFault,
+ out string reason)
+ {
+ reason = string.Empty;
+
+ if (hasProtectionFault && !pump.IsFlowStabilizationEnabled)
+ {
+ reason = pump.FlowStabilizationStatusText;
+ return false;
+ }
+
+ if (pump.IsRs485Busy)
+ {
+ reason = "正在执行 RS485 操作,等待调节完成后再采样。";
+ return false;
+ }
+
+ if (pump.PendingRs485RunningState == true)
+ {
+ reason = "启动仍在确认中,等待运行/流量反馈稳定后再采样。";
+ return false;
+ }
+
+ if (!pump.IsRunning || pump.PendingRs485RunningState == false)
+ {
+ reason = "未确认运行,不能作为稳定流量采样。";
+ return false;
+ }
+
+ if (!pump.FlowAvailable)
+ {
+ reason = "无实时流量反馈,不能作为稳定流量采样。";
+ return false;
+ }
+
+ if (!pump.ConfirmedSetpointAvailable || pump.ConfirmedSetpointFlowValue <= 0)
+ {
+ reason = "无已确认目标流量,不能作为稳定流量采样。";
+ return false;
+ }
+
+ var error = Math.Abs(pump.FlowValue - pump.ConfirmedSetpointFlowValue);
+ if (error > FlowStabilizationDeadbandLpm)
+ {
+ reason = $"尚未进入稳流允差:目标 {pump.ConfirmedSetpointFlowValue:F2} / 当前 {pump.FlowValue:F2} L/min,偏差 {error:F2} L/min。";
+ return false;
+ }
+
+ return true;
+ }
+
private bool TryGetChannel(string name, out DeviceChannel channel)
{
channel = Channels.FirstOrDefault(item => item.Name == name)!;
@@ -2467,6 +2560,11 @@ public partial class MainViewModel : ObservableObject, IDisposable
return;
}
+ if (!EnsureFlowStabilizationReadyForSample("压力降采样", PressureDropRs485PumpKey))
+ {
+ return;
+ }
+
var entry = PressureDropEntries.First(item => item.Label == label);
entry.ActualPumpFlow = PumpFlow;
entry.ProximalPressure = ChannelValue("近端压力");
@@ -2639,6 +2737,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
private void LoadManufacturerLimitSettings()
{
+ var previousRs485Suppress = _suppressRs485SettingsSave;
try
{
MigrateLegacyLimitSettingsIfNeeded();
@@ -2656,6 +2755,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
}
_suppressLimitSettingsSave = true;
+ _suppressRs485SettingsSave = true;
ProductModel = settings.ProductModel;
ApplicablePopulation = settings.ApplicablePopulation;
RatedMaxFlow = settings.RatedMaxFlow;
@@ -2666,6 +2766,8 @@ public partial class MainViewModel : ObservableObject, IDisposable
PressureDropLimit100 = settings.PressureDropLimit100;
AntiCollapseAllowedIncreaseRate = settings.AntiCollapseAllowedIncreaseRate;
RecirculationAllowedLimit = settings.RecirculationAllowedLimit;
+ ApplyRs485SerialSettings(settings.Rs485SerialSettings);
+ ApplyRs485Bindings(settings.Rs485PumpBindings);
}
catch
{
@@ -2675,6 +2777,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
finally
{
_suppressLimitSettingsSave = false;
+ _suppressRs485SettingsSave = previousRs485Suppress;
RefreshSpecializedJudgements();
}
}
@@ -2700,7 +2803,9 @@ public partial class MainViewModel : ObservableObject, IDisposable
PressureDropLimit75 = PressureDropLimit75,
PressureDropLimit100 = PressureDropLimit100,
AntiCollapseAllowedIncreaseRate = AntiCollapseAllowedIncreaseRate,
- RecirculationAllowedLimit = RecirculationAllowedLimit
+ RecirculationAllowedLimit = RecirculationAllowedLimit,
+ Rs485SerialSettings = BuildRs485SerialSettings(),
+ Rs485PumpBindings = BuildCurrentRs485PumpBindings()
};
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
@@ -2811,6 +2916,11 @@ public partial class MainViewModel : ObservableObject, IDisposable
return;
}
+ if (!EnsureFlowStabilizationReadyForSample("再循环采样", RecirculationRs485PumpKeys))
+ {
+ return;
+ }
+
var entry = RecirculationEntries.First(item => item.Label == label);
entry.ActualPumpFlow = RecirculationPumpFlow;
entry.DrainageFlow = DrainageFlow;