From 48e079514526c5a9c08c3242df71a38e3b979dd8 Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Thu, 4 Jun 2026 18:41:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0202606045?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EngineeringRegistersWindow.xaml | 3 +- .../Models/ManufacturerLimitSettings.cs | 2 + .../Models/PumpControlChannel.cs | 33 +- .../Services/ModbusTelemetryService.cs | 23 +- .../ViewModels/MainViewModel.Rs485.cs | 285 +++++++++++++++--- .../ViewModels/MainViewModel.cs | 112 ++++++- 6 files changed, 398 insertions(+), 60 deletions(-) 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;