更新202606045
This commit is contained in:
@@ -61,12 +61,13 @@
|
||||
<DataGridTextColumn Header="速度偏置" Binding="{Binding Rs485RawOffset, UpdateSourceTrigger=PropertyChanged}" Width="90" />
|
||||
<DataGridTextColumn Header="最小 L/min" Binding="{Binding Rs485MinFlowLpm, UpdateSourceTrigger=PropertyChanged}" Width="90" />
|
||||
<DataGridTextColumn Header="最大 L/min" Binding="{Binding Rs485MaxFlowLpm, UpdateSourceTrigger=PropertyChanged}" Width="90" />
|
||||
<DataGridCheckBoxColumn Header="已确认" Binding="{Binding Rs485CalibrationConfirmed, UpdateSourceTrigger=PropertyChanged}" Width="76" />
|
||||
<DataGridTextColumn Header="备注" Binding="{Binding SetpointStatusText}" IsReadOnly="True" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<TextBlock Margin="0,10,0,0"
|
||||
Style="{StaticResource CaptionStyle}"
|
||||
Text="上方 8 泵默认配置只保留运行所需参数,寄存器地址不在界面显示;下方 8 路流量系数表会先读取 PLC 当前值回显,再支持人工修改和写入校正。"
|
||||
Text="上方 8 泵默认配置只保留运行所需参数,寄存器地址不在界面显示;速度换算核对后勾选已确认才允许启泵。下方 8 路流量系数表会先读取 PLC 当前值回显,再支持人工修改和写入校正。"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -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<Rs485PumpBindingSettings> Rs485PumpBindings { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -22,14 +22,14 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, ushort> FlowRegisters = new Dictionary<string, ushort>(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<string, string> FlowChannelNames = new Dictionary<string, string>(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);
|
||||
}
|
||||
|
||||
@@ -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<Rs485PumpBindingSettings>? bindings)
|
||||
{
|
||||
var defaults = BuildDefaultRs485PumpBindings();
|
||||
var bindingMap = (bindings ?? Array.Empty<Rs485PumpBindingSettings>())
|
||||
.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<Rs485PumpBindingSettings> 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<Rs485PumpBindingSettings> BuildDefaultRs485PumpBindings()
|
||||
{
|
||||
var bindings = new List<Rs485PumpBindingSettings>();
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user