1622 lines
57 KiB
C#
1622 lines
57 KiB
C#
using System.Collections.Generic;
|
||
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;
|
||
|
||
namespace Cardiopulmonarybypasssystems.ViewModels;
|
||
|
||
public partial class MainViewModel
|
||
{
|
||
private enum Rs485StartExecutionResult
|
||
{
|
||
Failed,
|
||
PendingConfirmation,
|
||
Confirmed
|
||
}
|
||
|
||
private enum Rs485ToggleExecutionResult
|
||
{
|
||
NotHandled,
|
||
Failed,
|
||
Succeeded
|
||
}
|
||
|
||
private static readonly string[] Rs485PumpKeys =
|
||
[
|
||
"PressureDropPump",
|
||
"RecirculationMainPump",
|
||
"RecirculationReturnPump",
|
||
"RecirculationDrainagePump",
|
||
"KinkResistancePump",
|
||
"HemolysisDrainageSinglePump",
|
||
"HemolysisReturnSinglePump",
|
||
"HemolysisDualLumenPump"
|
||
];
|
||
|
||
private static readonly HashSet<string> Rs485PumpKeySet = [.. Rs485PumpKeys];
|
||
private const double DefaultRs485RawPerLitrePerMinute = 100d;
|
||
private const double DefaultRs485RawOffset = 0d;
|
||
private const int MaxRs485MotorCommand = short.MaxValue;
|
||
private const double FlowStabilizationDeadbandLpm = 0.05d;
|
||
private const int FlowStabilizationMaxRawStep = 5;
|
||
private const double FlowStabilizationMaxRelativeTrim = 0.20d;
|
||
private const string PressureDropRs485PumpKey = "PressureDropPump";
|
||
private const string KinkResistanceRs485PumpKey = "KinkResistancePump";
|
||
private static readonly TimeSpan FlowStabilizationAdjustmentInterval = TimeSpan.FromSeconds(2);
|
||
private static readonly string[] HemolysisRs485PumpKeys =
|
||
[
|
||
"HemolysisDrainageSinglePump",
|
||
"HemolysisReturnSinglePump",
|
||
"HemolysisDualLumenPump"
|
||
];
|
||
private static readonly string[] RecirculationRs485PumpKeys =
|
||
[
|
||
"RecirculationMainPump",
|
||
"RecirculationReturnPump",
|
||
"RecirculationDrainagePump"
|
||
];
|
||
private static readonly TimeSpan Rs485RuntimeRefreshInterval = TimeSpan.FromSeconds(4);
|
||
private DateTime _lastRs485RuntimeRefreshUtc = DateTime.MinValue;
|
||
private string _lastRs485RuntimeRefreshFailureMessage = string.Empty;
|
||
|
||
[ObservableProperty]
|
||
private string rs485PortName = "COM9";
|
||
|
||
[ObservableProperty]
|
||
private int rs485BaudRate = 9600;
|
||
|
||
[ObservableProperty]
|
||
private string rs485Parity = "Even";
|
||
|
||
[ObservableProperty]
|
||
private int rs485DataBits = 8;
|
||
|
||
[ObservableProperty]
|
||
private int rs485StopBits = 1;
|
||
|
||
[ObservableProperty]
|
||
private int rs485ReadTimeoutMs = 500;
|
||
|
||
[ObservableProperty]
|
||
private int rs485WriteTimeoutMs = 500;
|
||
|
||
[ObservableProperty]
|
||
private bool rs485AutoSwitchPresetMode = true;
|
||
|
||
[ObservableProperty]
|
||
private bool rs485PersistPresetAfterWrite;
|
||
|
||
[ObservableProperty]
|
||
private bool rs485AutoStartPumpAfterWrite;
|
||
|
||
[ObservableProperty]
|
||
private bool rs485AdvancedSettingsVisible;
|
||
|
||
[ObservableProperty]
|
||
private bool realtimeActuatorControlsVisible = true;
|
||
|
||
[ObservableProperty]
|
||
private string rs485StatusText = "RS485 待确认";
|
||
|
||
public ObservableCollection<string> Rs485ParityOptions { get; } = new(["Even", "Odd", "None"]);
|
||
public ObservableCollection<PumpControlChannel> Rs485FlowPumpControls { get; } = [];
|
||
public ObservableCollection<PumpControlChannel> ActiveRs485FlowPumpControls { get; } = [];
|
||
public ObservableCollection<PumpControlChannel> RealtimeRs485FlowPumpControls { get; } = [];
|
||
public ObservableCollection<PumpControlChannel> PressureDropRs485FlowPumpControls { get; } = [];
|
||
public ObservableCollection<PumpControlChannel> KinkResistanceRs485FlowPumpControls { get; } = [];
|
||
public ObservableCollection<PumpControlChannel> HemolysisRs485FlowPumpControls { get; } = [];
|
||
public ObservableCollection<PumpControlChannel> RecirculationRs485FlowPumpControls { get; } = [];
|
||
public bool HasRealtimeRs485FlowPumpControls => RealtimeRs485FlowPumpControls.Count > 0;
|
||
public bool HasPressureDropRs485FlowPumpControls => PressureDropRs485FlowPumpControls.Count > 0;
|
||
public bool HasKinkResistanceRs485FlowPumpControls => KinkResistanceRs485FlowPumpControls.Count > 0;
|
||
public bool HasHemolysisRs485FlowPumpControls => HemolysisRs485FlowPumpControls.Count > 0;
|
||
public bool HasRecirculationRs485FlowPumpControls => RecirculationRs485FlowPumpControls.Count > 0;
|
||
public int SelectedRs485PumpCount => ActiveRs485FlowPumpControls.Count(item => item.IsBatchSelected);
|
||
public string Rs485AdvancedSettingsToggleText => Rs485AdvancedSettingsVisible ? "收起高级维护" : "展开高级维护";
|
||
public string RealtimeActuatorControlsToggleText => RealtimeActuatorControlsVisible ? "隐藏执行机构控制" : "显示执行机构控制";
|
||
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 泵批量预设设计,可逐行录入目标流量后一次批量写入;PLC 仅保留实时采集。";
|
||
|
||
partial void OnRs485PortNameChanged(string value) => UpdateAndPersistRs485Settings();
|
||
partial void OnRs485BaudRateChanged(int value) => UpdateAndPersistRs485Settings();
|
||
|
||
partial void OnRs485ParityChanged(string value)
|
||
{
|
||
var targetStopBits = string.Equals(value, "None", StringComparison.OrdinalIgnoreCase) ? 2 : 1;
|
||
if (Rs485StopBits != targetStopBits)
|
||
{
|
||
Rs485StopBits = targetStopBits;
|
||
}
|
||
|
||
UpdateAndPersistRs485Settings();
|
||
}
|
||
|
||
partial void OnRs485DataBitsChanged(int value) => UpdateAndPersistRs485Settings();
|
||
partial void OnRs485StopBitsChanged(int value) => UpdateAndPersistRs485Settings();
|
||
partial void OnRs485ReadTimeoutMsChanged(int value) => UpdateAndPersistRs485Settings();
|
||
partial void OnRs485WriteTimeoutMsChanged(int value) => UpdateAndPersistRs485Settings();
|
||
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));
|
||
partial void OnRealtimeActuatorControlsVisibleChanged(bool value) => OnPropertyChanged(nameof(RealtimeActuatorControlsToggleText));
|
||
|
||
[RelayCommand]
|
||
private void ToggleRealtimeActuatorControls()
|
||
{
|
||
RealtimeActuatorControlsVisible = !RealtimeActuatorControlsVisible;
|
||
}
|
||
|
||
private void InitializeRs485FlowControl()
|
||
{
|
||
Rs485FlowPumpControls.Clear();
|
||
foreach (var pump in PumpControls.Where(item => Rs485PumpKeySet.Contains(item.Key)))
|
||
{
|
||
Rs485FlowPumpControls.Add(pump);
|
||
}
|
||
|
||
var defaults = BuildDefaultRs485PumpBindings();
|
||
foreach (var pump in Rs485FlowPumpControls)
|
||
{
|
||
ApplyRs485Binding(pump, defaults.FirstOrDefault(item => item.PumpKey == pump.Key));
|
||
|
||
if (string.IsNullOrWhiteSpace(pump.PendingSetpointText))
|
||
{
|
||
pump.PendingSetpointText = pump.FlowAvailable
|
||
? pump.FlowValue.ToString("F2", CultureInfo.InvariantCulture)
|
||
: pump.Rs485MinFlowLpm.ToString("F2", CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
pump.PropertyChanged -= OnRs485PumpConfigurationChanged;
|
||
pump.PropertyChanged += OnRs485PumpConfigurationChanged;
|
||
}
|
||
|
||
RefreshActiveRs485FlowPumpControls();
|
||
OnPropertyChanged(nameof(Rs485ConnectionSummary));
|
||
}
|
||
|
||
private Rs485SerialSettings BuildRs485SerialSettings() => new()
|
||
{
|
||
PortName = Rs485PortName,
|
||
BaudRate = Rs485BaudRate,
|
||
Parity = Rs485Parity,
|
||
DataBits = Rs485DataBits,
|
||
StopBits = Rs485StopBits,
|
||
ReadTimeoutMs = Rs485ReadTimeoutMs,
|
||
WriteTimeoutMs = Rs485WriteTimeoutMs,
|
||
AutoSwitchPresetMode = Rs485AutoSwitchPresetMode,
|
||
PersistPresetAfterWrite = Rs485PersistPresetAfterWrite,
|
||
AutoStartPumpAfterWrite = Rs485AutoStartPumpAfterWrite
|
||
};
|
||
|
||
private List<Rs485PumpBindingSettings> BuildDefaultRs485PumpBindings()
|
||
{
|
||
var bindings = new List<Rs485PumpBindingSettings>();
|
||
for (var index = 0; index < Rs485PumpKeys.Length; index++)
|
||
{
|
||
bindings.Add(new Rs485PumpBindingSettings
|
||
{
|
||
PumpKey = Rs485PumpKeys[index],
|
||
Enabled = true,
|
||
CalibrationConfirmed = true,
|
||
SlaveAddress = (byte)(index + 1),
|
||
ForwardSpeedRegister = 0x00A2,
|
||
ReverseSpeedRegister = 0x00A3,
|
||
RunStatusRegister = 0x00F3,
|
||
DeviceAddressRegister = 0x00FA,
|
||
PresetModeRegister = 0x00FB,
|
||
SavePresetRegister = 0x01A0,
|
||
MotorControlRegister = 0x0040,
|
||
RawPerLitrePerMinute = DefaultRs485RawPerLitrePerMinute,
|
||
RawOffset = DefaultRs485RawOffset,
|
||
MinFlowLpm = 0,
|
||
MaxFlowLpm = 20.0
|
||
});
|
||
}
|
||
|
||
return bindings;
|
||
}
|
||
|
||
private void ApplyRs485Binding(PumpControlChannel pump, Rs485PumpBindingSettings? binding)
|
||
{
|
||
binding ??= BuildDefaultRs485PumpBindings().FirstOrDefault(item => item.PumpKey == pump.Key)
|
||
?? 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;
|
||
pump.Rs485RunStatusRegister = binding.RunStatusRegister;
|
||
pump.Rs485DeviceAddressRegister = binding.DeviceAddressRegister;
|
||
pump.Rs485PresetModeRegister = binding.PresetModeRegister;
|
||
pump.Rs485SavePresetRegister = binding.SavePresetRegister;
|
||
pump.Rs485MotorControlRegister = binding.MotorControlRegister;
|
||
pump.Rs485RawPerLitrePerMinute = binding.RawPerLitrePerMinute > 0
|
||
? binding.RawPerLitrePerMinute
|
||
: DefaultRs485RawPerLitrePerMinute;
|
||
pump.Rs485RawOffset = binding.RawPerLitrePerMinute > 0
|
||
? binding.RawOffset
|
||
: DefaultRs485RawOffset;
|
||
pump.Rs485MinFlowLpm = binding.MinFlowLpm;
|
||
pump.Rs485MaxFlowLpm = binding.MaxFlowLpm <= 0 ? Math.Max(RatedMaxFlow, 20.0) : binding.MaxFlowLpm;
|
||
|
||
if (string.IsNullOrWhiteSpace(pump.PendingSetpointText))
|
||
{
|
||
pump.PendingSetpointText = pump.FlowAvailable
|
||
? pump.FlowValue.ToString("F2", CultureInfo.InvariantCulture)
|
||
: pump.Rs485MinFlowLpm.ToString("F2", CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
if (binding.RawPerLitrePerMinute <= 0)
|
||
{
|
||
pump.SetpointStatusText = $"已应用默认换算初值 {DefaultRs485RawPerLitrePerMinute:F0} 速度/Lmin";
|
||
}
|
||
else if (!pump.HasSetpointCalibration)
|
||
{
|
||
pump.SetpointStatusText = "未配置 L/min 与速度换算系数";
|
||
}
|
||
else
|
||
{
|
||
pump.SetpointStatusText = "换算系数已确认";
|
||
}
|
||
}
|
||
|
||
private void OnRs485PumpConfigurationChanged(object? sender, PropertyChangedEventArgs e)
|
||
{
|
||
if (sender is not PumpControlChannel pump)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (e.PropertyName is not nameof(PumpControlChannel.Rs485Enabled)
|
||
and not nameof(PumpControlChannel.IsBatchSelected)
|
||
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)
|
||
and not nameof(PumpControlChannel.Rs485ReverseSpeedRegister)
|
||
and not nameof(PumpControlChannel.Rs485RunStatusRegister)
|
||
and not nameof(PumpControlChannel.Rs485DeviceAddressRegister)
|
||
and not nameof(PumpControlChannel.Rs485PresetModeRegister)
|
||
and not nameof(PumpControlChannel.Rs485SavePresetRegister)
|
||
and not nameof(PumpControlChannel.Rs485MotorControlRegister))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (e.PropertyName == nameof(PumpControlChannel.IsBatchSelected))
|
||
{
|
||
OnPropertyChanged(nameof(SelectedRs485PumpCount));
|
||
return;
|
||
}
|
||
|
||
if (!pump.HasSetpointCalibration)
|
||
{
|
||
pump.SetpointStatusText = "未配置 L/min 与速度换算系数";
|
||
}
|
||
else
|
||
{
|
||
pump.SetpointStatusText = "换算系数已确认";
|
||
}
|
||
|
||
if (e.PropertyName == nameof(PumpControlChannel.Rs485Enabled))
|
||
{
|
||
RefreshActiveRs485FlowPumpControls();
|
||
}
|
||
|
||
RaiseRs485CalibrationSummaryChanges();
|
||
UpdateAndPersistRs485Settings();
|
||
}
|
||
|
||
private void RefreshActiveRs485FlowPumpControls()
|
||
{
|
||
ActiveRs485FlowPumpControls.Clear();
|
||
RealtimeRs485FlowPumpControls.Clear();
|
||
PressureDropRs485FlowPumpControls.Clear();
|
||
KinkResistanceRs485FlowPumpControls.Clear();
|
||
HemolysisRs485FlowPumpControls.Clear();
|
||
RecirculationRs485FlowPumpControls.Clear();
|
||
foreach (var pump in Rs485FlowPumpControls.Where(item => item.Rs485Enabled))
|
||
{
|
||
ActiveRs485FlowPumpControls.Add(pump);
|
||
if (string.Equals(pump.Key, PressureDropRs485PumpKey, StringComparison.Ordinal))
|
||
{
|
||
PressureDropRs485FlowPumpControls.Add(pump);
|
||
}
|
||
else if (string.Equals(pump.Key, KinkResistanceRs485PumpKey, StringComparison.Ordinal))
|
||
{
|
||
KinkResistanceRs485FlowPumpControls.Add(pump);
|
||
}
|
||
else if (HemolysisRs485PumpKeys.Contains(pump.Key, StringComparer.Ordinal))
|
||
{
|
||
HemolysisRs485FlowPumpControls.Add(pump);
|
||
}
|
||
else if (RecirculationRs485PumpKeys.Contains(pump.Key, StringComparer.Ordinal))
|
||
{
|
||
RecirculationRs485FlowPumpControls.Add(pump);
|
||
}
|
||
else
|
||
{
|
||
RealtimeRs485FlowPumpControls.Add(pump);
|
||
}
|
||
}
|
||
|
||
OnPropertyChanged(nameof(HasRealtimeRs485FlowPumpControls));
|
||
OnPropertyChanged(nameof(HasPressureDropRs485FlowPumpControls));
|
||
OnPropertyChanged(nameof(HasKinkResistanceRs485FlowPumpControls));
|
||
OnPropertyChanged(nameof(HasHemolysisRs485FlowPumpControls));
|
||
OnPropertyChanged(nameof(HasRecirculationRs485FlowPumpControls));
|
||
RaiseRs485CalibrationSummaryChanges();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StartRecirculationRs485Pumps()
|
||
{
|
||
if (!EnsureSessionEditable("再循环三泵统一启动"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var pumps = RecirculationRs485FlowPumpControls.ToList();
|
||
if (pumps.Count == 0)
|
||
{
|
||
Rs485StatusText = "当前未配置再循环 RS485 泵。";
|
||
LatestAction = Rs485StatusText;
|
||
return;
|
||
}
|
||
|
||
var issuedCount = 0;
|
||
var confirmedCount = 0;
|
||
var skippedCount = 0;
|
||
var failedCount = 0;
|
||
foreach (var pump in pumps)
|
||
{
|
||
var effectiveRunning = pump.PendingRs485RunningState ?? pump.IsRunning;
|
||
if (effectiveRunning)
|
||
{
|
||
skippedCount++;
|
||
continue;
|
||
}
|
||
|
||
var startResult = await TryWriteAndStartPumpCore(pump, "再循环三泵统一启动");
|
||
if (startResult is Rs485StartExecutionResult.PendingConfirmation or Rs485StartExecutionResult.Confirmed)
|
||
{
|
||
issuedCount++;
|
||
if (startResult == Rs485StartExecutionResult.Confirmed)
|
||
{
|
||
confirmedCount++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
var summary = $"再循环三泵启动完成:已下发 {issuedCount} 台,已确认运行 {confirmedCount} 台,跳过 {skippedCount} 台,失败 {failedCount} 台。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("再循环三泵启动", summary));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StopRecirculationRs485Pumps()
|
||
{
|
||
if (!EnsureSessionEditable("再循环三泵统一停止"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var pumps = RecirculationRs485FlowPumpControls.ToList();
|
||
if (pumps.Count == 0)
|
||
{
|
||
Rs485StatusText = "当前未配置再循环 RS485 泵。";
|
||
LatestAction = Rs485StatusText;
|
||
return;
|
||
}
|
||
|
||
var stoppedCount = 0;
|
||
var failedCount = 0;
|
||
foreach (var pump in pumps)
|
||
{
|
||
if (await TryTogglePumpControlViaRs485(pump, nextState: false) == Rs485ToggleExecutionResult.Succeeded)
|
||
{
|
||
stoppedCount++;
|
||
}
|
||
else
|
||
{
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
var summary = $"再循环三泵停止完成:已下发停止 {stoppedCount} 台,失败 {failedCount} 台。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("再循环三泵停止", summary));
|
||
}
|
||
|
||
private async Task RefreshRs485RuntimeStateSilentlyAsync()
|
||
{
|
||
if (DateTime.UtcNow - _lastRs485RuntimeRefreshUtc < Rs485RuntimeRefreshInterval)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var pumps = ActiveRs485FlowPumpControls
|
||
.Where(item => item.SupportsRs485Preset && !item.IsRs485Busy)
|
||
.OrderBy(item => item.Rs485SlaveAddress)
|
||
.ToList();
|
||
|
||
if (pumps.Count == 0 || string.IsNullOrWhiteSpace(Rs485PortName))
|
||
{
|
||
return;
|
||
}
|
||
|
||
_lastRs485RuntimeRefreshUtc = DateTime.UtcNow;
|
||
var pumpMap = pumps.ToDictionary(item => item.Key, StringComparer.Ordinal);
|
||
var requests = pumps
|
||
.Select(BuildRs485Request)
|
||
.ToList();
|
||
|
||
var snapshots = await Task.Run(() => _rs485PumpFlowService.ReadPumpRuntimeStates(requests));
|
||
var allFailed = true;
|
||
foreach (var snapshot in snapshots)
|
||
{
|
||
if (!pumpMap.TryGetValue(snapshot.PumpKey, out var pump))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
ApplySilentRs485RuntimeSnapshot(pump, snapshot.Result);
|
||
if (snapshot.Result.Success)
|
||
{
|
||
allFailed = false;
|
||
}
|
||
}
|
||
|
||
if (!allFailed)
|
||
{
|
||
_lastRs485RuntimeRefreshFailureMessage = string.Empty;
|
||
return;
|
||
}
|
||
|
||
var failureMessage = "RS485 状态周期刷新失败,请检查串口链路和从站连接。";
|
||
if (string.Equals(_lastRs485RuntimeRefreshFailureMessage, failureMessage, StringComparison.Ordinal))
|
||
{
|
||
return;
|
||
}
|
||
|
||
_lastRs485RuntimeRefreshFailureMessage = failureMessage;
|
||
Rs485StatusText = failureMessage;
|
||
LatestAction = failureMessage;
|
||
TraceEvents.Insert(0, NewTrace("RS485 状态刷新失败", failureMessage));
|
||
}
|
||
|
||
private static void ApplySilentRs485RuntimeSnapshot(PumpControlChannel pump, Rs485PumpFlowOperationResult result)
|
||
{
|
||
if (result.RawForwardSpeed.HasValue)
|
||
{
|
||
pump.RawSetpointValue = result.RawForwardSpeed.Value;
|
||
pump.SetpointAvailable = true;
|
||
if (pump.HasSetpointCalibration)
|
||
{
|
||
pump.SetpointFlowValue = (result.RawForwardSpeed.Value - pump.Rs485RawOffset) / pump.Rs485RawPerLitrePerMinute;
|
||
}
|
||
}
|
||
|
||
if (result.DeviceAddress.HasValue)
|
||
{
|
||
pump.Rs485DeviceAddress = result.DeviceAddress.Value;
|
||
}
|
||
|
||
if (result.RunStatus.HasValue)
|
||
{
|
||
pump.Rs485RunStatusCode = result.RunStatus.Value;
|
||
pump.PendingRs485RunningState = null;
|
||
if (result.RunStatus.Value == 1)
|
||
{
|
||
pump.IsRunning = true;
|
||
pump.StateAvailable = true;
|
||
}
|
||
else if (result.RunStatus.Value == 0)
|
||
{
|
||
pump.IsRunning = false;
|
||
pump.StateAvailable = true;
|
||
}
|
||
else
|
||
{
|
||
pump.StateAvailable = false;
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (!pump.PendingRs485RunningState.HasValue)
|
||
{
|
||
pump.Rs485RunStatusCode = null;
|
||
pump.StateAvailable = false;
|
||
}
|
||
}
|
||
|
||
private async Task MaintainRs485FlowStabilizationAsync()
|
||
{
|
||
var adjustablePumps = ActiveRs485FlowPumpControls
|
||
.Where(ShouldMaintainFlowStabilization)
|
||
.OrderBy(item => item.Rs485SlaveAddress)
|
||
.ToList();
|
||
|
||
foreach (var pump in adjustablePumps)
|
||
{
|
||
await TryAdjustRs485FlowStabilizationAsync(pump);
|
||
}
|
||
}
|
||
|
||
private bool ShouldMaintainFlowStabilization(PumpControlChannel pump)
|
||
{
|
||
if (!pump.IsFlowStabilizationEnabled)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!pump.CanUseFlowStabilization)
|
||
{
|
||
pump.FlowStabilizationStatusText = "当前项目要求固定转速";
|
||
return false;
|
||
}
|
||
|
||
if (pump.IsRs485Busy)
|
||
{
|
||
pump.FlowStabilizationStatusText = "等待当前 RS485 操作完成";
|
||
return false;
|
||
}
|
||
|
||
if (!pump.IsRunning || pump.PendingRs485RunningState == false)
|
||
{
|
||
pump.FlowStabilizationStatusText = "等待泵运行";
|
||
return false;
|
||
}
|
||
|
||
if (!pump.FlowAvailable)
|
||
{
|
||
pump.FlowStabilizationStatusText = "等待流量反馈";
|
||
return false;
|
||
}
|
||
|
||
if (!pump.ConfirmedSetpointAvailable || pump.ConfirmedSetpointFlowValue <= 0)
|
||
{
|
||
pump.FlowStabilizationStatusText = "等待确认目标流量";
|
||
return false;
|
||
}
|
||
|
||
if (DateTime.UtcNow - pump.LastFlowStabilizationAdjustmentUtc < FlowStabilizationAdjustmentInterval)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private async Task TryAdjustRs485FlowStabilizationAsync(PumpControlChannel pump)
|
||
{
|
||
var targetFlow = pump.ConfirmedSetpointFlowValue;
|
||
var flowError = targetFlow - pump.FlowValue;
|
||
if (Math.Abs(flowError) <= FlowStabilizationDeadbandLpm)
|
||
{
|
||
pump.FlowStabilizationStatusText = $"稳流保持:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
|
||
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
|
||
return;
|
||
}
|
||
|
||
var targetRaw = pump.ConfirmedRawSetpointValue > 0
|
||
? pump.ConfirmedRawSetpointValue
|
||
: ConvertFlowToRawSpeed(pump, targetFlow);
|
||
if (targetRaw <= 0)
|
||
{
|
||
pump.FlowStabilizationStatusText = "目标流量换算值无效";
|
||
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
|
||
return;
|
||
}
|
||
|
||
var currentRaw = pump.FlowStabilizationRawSetpoint > 0
|
||
? pump.FlowStabilizationRawSetpoint
|
||
: pump.RawSetpointValue > 0
|
||
? pump.RawSetpointValue
|
||
: targetRaw;
|
||
var maxTrim = Math.Max(FlowStabilizationMaxRawStep, (int)Math.Round(targetRaw * FlowStabilizationMaxRelativeTrim, MidpointRounding.AwayFromZero));
|
||
var minRaw = Math.Max(1, targetRaw - maxTrim);
|
||
var maxRaw = Math.Min(MaxRs485MotorCommand, targetRaw + maxTrim);
|
||
var rawStep = Math.Sign(flowError) * FlowStabilizationMaxRawStep;
|
||
var nextRaw = Math.Clamp(currentRaw + rawStep, minRaw, maxRaw);
|
||
|
||
if (nextRaw == currentRaw)
|
||
{
|
||
pump.FlowStabilizationStatusText = $"稳流已到调节限幅:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min";
|
||
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
|
||
return;
|
||
}
|
||
|
||
if (!TryBeginRs485PumpOperation(pump, "稳流调节"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var request = BuildRs485Request(pump);
|
||
var result = await Task.Run(() => _rs485PumpFlowService.WritePumpMotorCommand(request, (short)nextRaw));
|
||
if (!result.Success)
|
||
{
|
||
pump.FlowStabilizationStatusText = result.Message;
|
||
Rs485StatusText = result.Message;
|
||
TraceEvents.Insert(0, NewTrace("RS485 稳流调节失败", $"{pump.Name} / {result.Message}"));
|
||
return;
|
||
}
|
||
|
||
pump.FlowStabilizationRawSetpoint = nextRaw;
|
||
CacheResolvedRs485Setpoint(pump, nextRaw);
|
||
ApplyPostCommandPumpState(pump, result, expectedRunning: true);
|
||
pump.FlowStabilizationStatusText = $"稳流调节:目标 {targetFlow:F2} / 当前 {pump.FlowValue:F2} L/min / 控制值 {nextRaw}";
|
||
}
|
||
finally
|
||
{
|
||
pump.LastFlowStabilizationAdjustmentUtc = DateTime.UtcNow;
|
||
EndRs485PumpOperation(pump);
|
||
}
|
||
}
|
||
|
||
private void RaiseRs485CalibrationSummaryChanges()
|
||
{
|
||
OnPropertyChanged(nameof(Rs485EnabledPumpCount));
|
||
OnPropertyChanged(nameof(Rs485CalibrationConfirmedPumpCount));
|
||
OnPropertyChanged(nameof(Rs485CalibrationSummary));
|
||
OnPropertyChanged(nameof(SelectedRs485PumpCount));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void ToggleRs485AdvancedSettings()
|
||
{
|
||
Rs485AdvancedSettingsVisible = !Rs485AdvancedSettingsVisible;
|
||
}
|
||
|
||
private void UpdateAndPersistRs485Settings()
|
||
{
|
||
OnPropertyChanged(nameof(Rs485ConnectionSummary));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void SetAllRs485PumpSelection(string? parameter)
|
||
{
|
||
var isSelected = string.Equals(parameter, bool.TrueString, StringComparison.OrdinalIgnoreCase);
|
||
|
||
foreach (var pump in ActiveRs485FlowPumpControls)
|
||
{
|
||
pump.IsBatchSelected = isSelected;
|
||
}
|
||
|
||
var summary = isSelected
|
||
? $"已选中 {SelectedRs485PumpCount} 台泵。"
|
||
: "已清空泵选择。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
}
|
||
|
||
[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)
|
||
{
|
||
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
|
||
? "已应用当前配置"
|
||
: "当前未启用";
|
||
}
|
||
|
||
Rs485StatusText = primaryPump is null
|
||
? "未找到可配置的 RS485 泵通道"
|
||
: $"已完成伺服器快速配置:启用 {primaryPump.Name} / 从站 1";
|
||
UpdateAndPersistRs485Settings();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task TestRs485Connection()
|
||
{
|
||
var baselinePump = Rs485FlowPumpControls
|
||
.Where(item => item.SupportsRs485Preset)
|
||
.OrderBy(item => item.Rs485SlaveAddress)
|
||
.FirstOrDefault();
|
||
|
||
if (baselinePump is null)
|
||
{
|
||
Rs485StatusText = "未找到已启用的 RS485 泵通道";
|
||
return;
|
||
}
|
||
|
||
if (!TryBeginRs485PumpOperation(baselinePump, "基准通讯"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var request = BuildRs485Request(baselinePump);
|
||
var result = await Task.Run(() => _rs485PumpFlowService.TestConnection(request));
|
||
ApplyRs485OperationResult(baselinePump, result, "RS485 基准通讯");
|
||
var summary = result.Success
|
||
? $"RS485 基准通讯正常:{baselinePump.Name} / 从站 {baselinePump.Rs485SlaveAddress}"
|
||
: $"RS485 基准通讯失败:{baselinePump.Name} / 从站 {baselinePump.Rs485SlaveAddress} / {result.Message}";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("RS485 基准通讯", summary));
|
||
}
|
||
finally
|
||
{
|
||
EndRs485PumpOperation(baselinePump);
|
||
}
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task TestAllRs485Connections()
|
||
{
|
||
var pumps = Rs485FlowPumpControls
|
||
.Where(item => item.SupportsRs485Preset)
|
||
.OrderBy(item => item.Rs485SlaveAddress)
|
||
.ToList();
|
||
|
||
if (pumps.Count == 0)
|
||
{
|
||
Rs485StatusText = "未找到已启用的 RS485 泵通道";
|
||
return;
|
||
}
|
||
|
||
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 async Task RefreshAllPumpSetpoints()
|
||
{
|
||
foreach (var pump in Rs485FlowPumpControls.Where(item => item.SupportsRs485Preset))
|
||
{
|
||
await ReadPumpSetpoint(pump);
|
||
}
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task ReadPumpSetpoint(PumpControlChannel? pump)
|
||
{
|
||
if (!TryPreparePumpForRs485(pump, requireCalibration: false, out var activePump, out _))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!TryBeginRs485PumpOperation(activePump, "读取"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var request = BuildRs485Request(activePump);
|
||
var result = await Task.Run(() => _rs485PumpFlowService.ReadPumpPreset(request));
|
||
ApplyRs485OperationResult(activePump, result, "读取泵预设");
|
||
}
|
||
finally
|
||
{
|
||
EndRs485PumpOperation(activePump);
|
||
}
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task WritePumpSetpoint(PumpControlChannel? pump)
|
||
{
|
||
await TryWritePumpSetpointCore(pump, "RS485 预设流量写入", autoStartAfterWrite: false);
|
||
}
|
||
|
||
[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 批量预设流量写入", autoStartAfterWrite: false))
|
||
{
|
||
successCount++;
|
||
}
|
||
else
|
||
{
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
var summary = $"批量写入完成:成功 {successCount} 台,失败 {failedCount} 台。批量模式仅写入目标值,不自动启动。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("RS485 批量写入", summary));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void ApplyRs485FlowPreset(string? presetKey)
|
||
{
|
||
double ratio = presetKey switch
|
||
{
|
||
"50" => 0.50d,
|
||
"75" => 0.75d,
|
||
"100" => 1.00d,
|
||
_ => 0d
|
||
};
|
||
|
||
if (ratio <= 0d)
|
||
{
|
||
Rs485StatusText = "未识别的批量流量预设";
|
||
return;
|
||
}
|
||
|
||
var referenceFlow = Math.Max(RatedMaxFlow * ratio, 0d);
|
||
foreach (var pump in ActiveRs485FlowPumpControls)
|
||
{
|
||
var targetFlow = Math.Clamp(referenceFlow, pump.Rs485MinFlowLpm, pump.Rs485MaxFlowLpm);
|
||
pump.PendingSetpointText = targetFlow.ToString("F2", CultureInfo.InvariantCulture);
|
||
pump.SetpointStatusText = $"已应用 {presetKey}% 快捷目标 {targetFlow:F2} L/min";
|
||
}
|
||
|
||
var summary = $"已将 8 泵目标流量批量设为 {presetKey}% 参考值。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("RS485 批量目标", summary));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task QuickToggleRs485Pump(PumpControlChannel? pump)
|
||
{
|
||
if (pump is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var effectiveRunning = pump.PendingRs485RunningState ?? pump.IsRunning;
|
||
if (effectiveRunning)
|
||
{
|
||
await TogglePumpControl(pump);
|
||
return;
|
||
}
|
||
|
||
await TryWriteAndStartPumpCore(pump, "RS485 快捷启动");
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StartSingleRs485Pump(PumpControlChannel? pump)
|
||
{
|
||
if (pump is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
await TryWriteAndStartPumpCore(pump, "RS485 单泵启动");
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StopSingleRs485Pump(PumpControlChannel? pump)
|
||
{
|
||
if (pump is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!EnsureSessionEditable("RS485 单泵停止"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
await TryTogglePumpControlViaRs485(pump, nextState: false);
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StartAllRs485Pumps()
|
||
{
|
||
if (!EnsureSessionEditable("RS485 一键启动"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var issuedCount = 0;
|
||
var confirmedCount = 0;
|
||
var skippedCount = 0;
|
||
var failedCount = 0;
|
||
foreach (var pump in ActiveRs485FlowPumpControls)
|
||
{
|
||
var effectiveRunning = pump.PendingRs485RunningState ?? pump.IsRunning;
|
||
if (effectiveRunning)
|
||
{
|
||
skippedCount++;
|
||
continue;
|
||
}
|
||
|
||
var startResult = await TryWriteAndStartPumpCore(pump, "RS485 一键启动");
|
||
if (startResult is Rs485StartExecutionResult.PendingConfirmation or Rs485StartExecutionResult.Confirmed)
|
||
{
|
||
issuedCount++;
|
||
if (startResult == Rs485StartExecutionResult.Confirmed)
|
||
{
|
||
confirmedCount++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
var summary = $"一键启动完成:已下发 {issuedCount} 台,已确认运行 {confirmedCount} 台,跳过 {skippedCount} 台,失败 {failedCount} 台。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("RS485 一键启动", summary));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StartSelectedRs485Pumps()
|
||
{
|
||
if (!EnsureSessionEditable("RS485 选中启动"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var selectedPumps = ActiveRs485FlowPumpControls.Where(item => item.IsBatchSelected).ToList();
|
||
if (selectedPumps.Count == 0)
|
||
{
|
||
Rs485StatusText = "请先勾选需要启动的泵";
|
||
LatestAction = Rs485StatusText;
|
||
return;
|
||
}
|
||
|
||
var issuedCount = 0;
|
||
var confirmedCount = 0;
|
||
var skippedCount = 0;
|
||
var failedCount = 0;
|
||
foreach (var pump in selectedPumps)
|
||
{
|
||
var effectiveRunning = pump.PendingRs485RunningState ?? pump.IsRunning;
|
||
if (effectiveRunning)
|
||
{
|
||
skippedCount++;
|
||
continue;
|
||
}
|
||
|
||
var startResult = await TryWriteAndStartPumpCore(pump, "RS485 选中启动");
|
||
if (startResult is Rs485StartExecutionResult.PendingConfirmation or Rs485StartExecutionResult.Confirmed)
|
||
{
|
||
issuedCount++;
|
||
if (startResult == Rs485StartExecutionResult.Confirmed)
|
||
{
|
||
confirmedCount++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
var summary = $"选中启动完成:已下发 {issuedCount} 台,已确认运行 {confirmedCount} 台,跳过 {skippedCount} 台,失败 {failedCount} 台。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("RS485 选中启动", summary));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StopAllRs485Pumps()
|
||
{
|
||
if (!EnsureSessionEditable("RS485 一键停止"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var stoppedCount = 0;
|
||
var failedCount = 0;
|
||
foreach (var pump in ActiveRs485FlowPumpControls)
|
||
{
|
||
if (await TryTogglePumpControlViaRs485(pump, nextState: false) == Rs485ToggleExecutionResult.Succeeded)
|
||
{
|
||
stoppedCount++;
|
||
}
|
||
else
|
||
{
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
var summary = $"一键停止完成:已下发停止 {stoppedCount} 台,失败 {failedCount} 台。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("RS485 一键停止", summary));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StopSelectedRs485Pumps()
|
||
{
|
||
if (!EnsureSessionEditable("RS485 选中停止"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var selectedPumps = ActiveRs485FlowPumpControls.Where(item => item.IsBatchSelected).ToList();
|
||
if (selectedPumps.Count == 0)
|
||
{
|
||
Rs485StatusText = "请先勾选需要停止的泵";
|
||
LatestAction = Rs485StatusText;
|
||
return;
|
||
}
|
||
|
||
var stoppedCount = 0;
|
||
var failedCount = 0;
|
||
foreach (var pump in selectedPumps)
|
||
{
|
||
if (await TryTogglePumpControlViaRs485(pump, nextState: false) == Rs485ToggleExecutionResult.Succeeded)
|
||
{
|
||
stoppedCount++;
|
||
}
|
||
else
|
||
{
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
var summary = $"选中停止完成:已下发停止 {stoppedCount} 台,失败 {failedCount} 台。";
|
||
Rs485StatusText = summary;
|
||
LatestAction = summary;
|
||
TraceEvents.Insert(0, NewTrace("RS485 选中停止", summary));
|
||
}
|
||
|
||
private async Task<bool> TryWritePumpSetpointCore(
|
||
PumpControlChannel? pump,
|
||
string operationName,
|
||
bool autoStartAfterWrite)
|
||
{
|
||
if (!EnsureSessionEditable(operationName))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!TryPreparePumpForRs485(pump, requireCalibration: true, out var activePump, out var targetFlow))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (activePump.IsRunning)
|
||
{
|
||
activePump.SetpointStatusText = "泵运行中,禁止改写预设";
|
||
Rs485StatusText = $"{activePump.Name} 运行中,禁止改写预设";
|
||
return false;
|
||
}
|
||
|
||
var rawValue = ConvertFlowToRawSpeed(activePump, targetFlow!.Value);
|
||
if (rawValue is < 0 or > MaxRs485MotorCommand)
|
||
{
|
||
activePump.SetpointStatusText = $"换算后的速度值超出 0~{MaxRs485MotorCommand}";
|
||
Rs485StatusText = $"{activePump.Name} 写入失败:速度值超出范围";
|
||
return false;
|
||
}
|
||
|
||
if (!TryBeginRs485PumpOperation(activePump, "写入"))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
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);
|
||
if (autoStartAfterWrite)
|
||
{
|
||
var startResult = await TryAutoStartPumpAfterRs485Write(activePump, targetFlow.Value, forceAutoStart: true);
|
||
return startResult is Rs485StartExecutionResult.PendingConfirmation or Rs485StartExecutionResult.Confirmed;
|
||
}
|
||
}
|
||
|
||
return result.Success;
|
||
}
|
||
finally
|
||
{
|
||
EndRs485PumpOperation(activePump);
|
||
}
|
||
}
|
||
|
||
private async Task<Rs485StartExecutionResult> TryWriteAndStartPumpCore(PumpControlChannel? pump, string operationName)
|
||
{
|
||
if (!await TryWritePumpSetpointCore(pump, operationName, autoStartAfterWrite: false))
|
||
{
|
||
return Rs485StartExecutionResult.Failed;
|
||
}
|
||
|
||
if (pump is null)
|
||
{
|
||
return Rs485StartExecutionResult.Failed;
|
||
}
|
||
|
||
var targetFlow = pump.ConfirmedSetpointAvailable
|
||
? pump.ConfirmedSetpointFlowValue
|
||
: double.TryParse(
|
||
pump.PendingSetpointText,
|
||
NumberStyles.Float | NumberStyles.AllowThousands,
|
||
CultureInfo.InvariantCulture,
|
||
out var parsedFlow)
|
||
? parsedFlow
|
||
: 0d;
|
||
|
||
return await TryAutoStartPumpAfterRs485Write(pump, targetFlow, forceAutoStart: true);
|
||
}
|
||
|
||
private async Task<Rs485StartExecutionResult> TryAutoStartPumpAfterRs485Write(PumpControlChannel pump, double targetFlow, bool forceAutoStart = false)
|
||
{
|
||
if (!forceAutoStart && !Rs485AutoStartPumpAfterWrite)
|
||
{
|
||
return Rs485StartExecutionResult.Failed;
|
||
}
|
||
|
||
if (pump.IsRunning)
|
||
{
|
||
var runningMessage = $"{pump.Name} 预设已更新为 {targetFlow:F2} L/min,泵当前已在运行";
|
||
pump.SetpointStatusText = runningMessage;
|
||
Rs485StatusText = runningMessage;
|
||
LatestAction = runningMessage;
|
||
TraceEvents.Insert(0, NewTrace("RS485 自动启动", $"{pump.Name} 已在运行,无需重复启动"));
|
||
return Rs485StartExecutionResult.Confirmed;
|
||
}
|
||
|
||
if (ShouldUseRs485DirectPumpControl(pump))
|
||
{
|
||
if (!TryResolveRs485MotorCommand(pump, out var rawMotorSpeed, out var resolveMessage))
|
||
{
|
||
pump.SetpointStatusText = resolveMessage;
|
||
Rs485StatusText = resolveMessage;
|
||
LatestAction = resolveMessage;
|
||
TraceEvents.Insert(0, NewTrace("RS485 直接启泵", $"{pump.Name} / {resolveMessage}"));
|
||
return Rs485StartExecutionResult.Failed;
|
||
}
|
||
|
||
var request = BuildRs485Request(pump);
|
||
var directResult = await Task.Run(() => _pumpActuationService.StartPump(request, rawMotorSpeed));
|
||
ApplyRs485OperationResult(pump, directResult, "RS485 直接启泵");
|
||
if (!directResult.Success)
|
||
{
|
||
return Rs485StartExecutionResult.Failed;
|
||
}
|
||
|
||
CacheResolvedRs485Setpoint(pump, rawMotorSpeed);
|
||
ApplyPostCommandPumpState(pump, directResult, expectedRunning: true);
|
||
await RefreshTelemetryAsync();
|
||
return directResult.RunStatus == 1
|
||
? Rs485StartExecutionResult.Confirmed
|
||
: Rs485StartExecutionResult.PendingConfirmation;
|
||
}
|
||
|
||
var message = $"{pump.Name} 已写入目标流量,但当前通道未配置伺服器 RS485 启停寄存器";
|
||
pump.SetpointStatusText = message;
|
||
Rs485StatusText = message;
|
||
LatestAction = message;
|
||
TraceEvents.Insert(0, NewTrace("RS485 自动启动", $"{pump.Name} / 未配置伺服器启停寄存器"));
|
||
return Rs485StartExecutionResult.Failed;
|
||
}
|
||
|
||
private bool TryPreparePumpForRs485(
|
||
PumpControlChannel? pump,
|
||
bool requireCalibration,
|
||
out PumpControlChannel activePump,
|
||
out double? targetFlow)
|
||
{
|
||
activePump = pump!;
|
||
targetFlow = null;
|
||
|
||
if (pump is null)
|
||
{
|
||
Rs485StatusText = "未选择泵通道";
|
||
return false;
|
||
}
|
||
|
||
if (!pump.SupportsRs485Preset)
|
||
{
|
||
pump.SetpointStatusText = "该泵未启用 RS485 预设";
|
||
Rs485StatusText = $"{pump.Name} 未启用 RS485 预设";
|
||
return false;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(Rs485PortName))
|
||
{
|
||
pump.SetpointStatusText = "请先配置串口号";
|
||
Rs485StatusText = "RS485 串口号为空";
|
||
return false;
|
||
}
|
||
|
||
if (requireCalibration && !pump.HasSetpointCalibration)
|
||
{
|
||
pump.SetpointStatusText = "请先配置 L/min 与速度换算系数";
|
||
Rs485StatusText = $"{pump.Name} 未配置换算系数";
|
||
return false;
|
||
}
|
||
|
||
if (!requireCalibration)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (!double.TryParse(
|
||
pump.PendingSetpointText,
|
||
NumberStyles.Float | NumberStyles.AllowThousands,
|
||
CultureInfo.InvariantCulture,
|
||
out var parsedFlow))
|
||
{
|
||
pump.SetpointStatusText = "请输入有效的 L/min";
|
||
Rs485StatusText = $"{pump.Name} 输入的流量格式无效";
|
||
return false;
|
||
}
|
||
|
||
if (parsedFlow < pump.Rs485MinFlowLpm || parsedFlow > pump.Rs485MaxFlowLpm)
|
||
{
|
||
pump.SetpointStatusText = $"允许范围 {pump.Rs485MinFlowLpm:F2}~{pump.Rs485MaxFlowLpm:F2} L/min";
|
||
Rs485StatusText = $"{pump.Name} 超出预设流量范围";
|
||
return false;
|
||
}
|
||
|
||
targetFlow = parsedFlow;
|
||
return true;
|
||
}
|
||
|
||
private Rs485PumpFlowRequest BuildRs485Request(PumpControlChannel pump) => new()
|
||
{
|
||
SerialSettings = BuildRs485SerialSettings(),
|
||
PumpSettings = new Rs485PumpBindingSettings
|
||
{
|
||
PumpKey = pump.Key,
|
||
Enabled = pump.Rs485Enabled,
|
||
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
|
||
}
|
||
};
|
||
|
||
private int ConvertFlowToRawSpeed(PumpControlChannel pump, double flowLpm) =>
|
||
(int)Math.Round(flowLpm * pump.Rs485RawPerLitrePerMinute + pump.Rs485RawOffset, MidpointRounding.AwayFromZero);
|
||
|
||
private double ConvertRawSpeedToFlow(PumpControlChannel pump, int rawSpeed) =>
|
||
pump.HasSetpointCalibration
|
||
? Math.Max(0, (rawSpeed - pump.Rs485RawOffset) / pump.Rs485RawPerLitrePerMinute)
|
||
: 0;
|
||
|
||
private void ApplyRs485OperationResult(PumpControlChannel pump, Rs485PumpFlowOperationResult result, string traceCategory)
|
||
{
|
||
Rs485StatusText = result.Message;
|
||
pump.SetpointStatusText = result.Message;
|
||
|
||
if (result.RawForwardSpeed.HasValue)
|
||
{
|
||
pump.RawSetpointValue = result.RawForwardSpeed.Value;
|
||
pump.SetpointAvailable = true;
|
||
if (pump.HasSetpointCalibration)
|
||
{
|
||
pump.SetpointFlowValue = ConvertRawSpeedToFlow(pump, result.RawForwardSpeed.Value);
|
||
}
|
||
}
|
||
|
||
if (result.DeviceAddress.HasValue)
|
||
{
|
||
pump.Rs485DeviceAddress = result.DeviceAddress.Value;
|
||
}
|
||
|
||
if (result.RunStatus.HasValue)
|
||
{
|
||
pump.Rs485RunStatusCode = result.RunStatus.Value;
|
||
if (result.RunStatus.Value is 0 or 1)
|
||
{
|
||
pump.StateAvailable = true;
|
||
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}"));
|
||
}
|
||
|
||
private bool ShouldUseRs485DirectPumpControl(PumpControlChannel pump) =>
|
||
_pumpActuationService.CanHandleDirectControl(pump);
|
||
|
||
private bool IsRs485ManagedPump(PumpControlChannel pump) =>
|
||
_pumpActuationService.IsManagedPump(pump);
|
||
|
||
private bool TryResolveRs485MotorCommand(PumpControlChannel pump, out short rawMotorSpeed, out string message)
|
||
{
|
||
rawMotorSpeed = 0;
|
||
message = string.Empty;
|
||
|
||
if (!pump.HasSetpointCalibration)
|
||
{
|
||
message = $"{pump.Name} 尚未配置有效的流量换算系数";
|
||
return false;
|
||
}
|
||
|
||
var candidateRaw = pump.ConfirmedSetpointAvailable ? pump.ConfirmedRawSetpointValue : 0;
|
||
if (candidateRaw <= 0
|
||
&& double.TryParse(
|
||
pump.PendingSetpointText,
|
||
NumberStyles.Float | NumberStyles.AllowThousands,
|
||
CultureInfo.InvariantCulture,
|
||
out var pendingFlow)
|
||
&& pendingFlow >= pump.Rs485MinFlowLpm
|
||
&& pendingFlow <= pump.Rs485MaxFlowLpm)
|
||
{
|
||
candidateRaw = ConvertFlowToRawSpeed(pump, pendingFlow);
|
||
}
|
||
|
||
if (candidateRaw <= 0)
|
||
{
|
||
message = $"{pump.Name} 启动前请输入有效目标流量";
|
||
return false;
|
||
}
|
||
|
||
if (candidateRaw > MaxRs485MotorCommand)
|
||
{
|
||
message = $"{pump.Name} 的直接启停控制值超过 {MaxRs485MotorCommand}";
|
||
return false;
|
||
}
|
||
|
||
rawMotorSpeed = (short)candidateRaw;
|
||
return true;
|
||
}
|
||
|
||
private static void CacheConfirmedRs485Setpoint(PumpControlChannel pump, int rawMotorSpeed, double flowLpm)
|
||
{
|
||
if (rawMotorSpeed <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
pump.ConfirmedSetpointAvailable = true;
|
||
pump.ConfirmedRawSetpointValue = rawMotorSpeed;
|
||
pump.ConfirmedSetpointFlowValue = flowLpm;
|
||
pump.FlowStabilizationRawSetpoint = rawMotorSpeed;
|
||
}
|
||
|
||
private bool TryBeginRs485PumpOperation(PumpControlChannel pump, string operationName)
|
||
{
|
||
if (pump.IsRs485Busy)
|
||
{
|
||
var message = string.IsNullOrWhiteSpace(pump.Rs485BusyOperation)
|
||
? $"{pump.Name} 正在执行 RS485 操作,请稍候"
|
||
: $"{pump.Name} 正在{pump.Rs485BusyOperation},请稍候";
|
||
pump.SetpointStatusText = message;
|
||
Rs485StatusText = message;
|
||
LatestAction = message;
|
||
return false;
|
||
}
|
||
|
||
pump.IsRs485Busy = true;
|
||
pump.Rs485BusyOperation = operationName;
|
||
return true;
|
||
}
|
||
|
||
private static void EndRs485PumpOperation(PumpControlChannel pump)
|
||
{
|
||
pump.Rs485BusyOperation = string.Empty;
|
||
pump.IsRs485Busy = false;
|
||
}
|
||
|
||
private void CacheResolvedRs485Setpoint(PumpControlChannel pump, int rawMotorSpeed)
|
||
{
|
||
if (rawMotorSpeed <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
pump.RawSetpointValue = rawMotorSpeed;
|
||
pump.SetpointAvailable = true;
|
||
if (pump.HasSetpointCalibration)
|
||
{
|
||
pump.SetpointFlowValue = ConvertRawSpeedToFlow(pump, rawMotorSpeed);
|
||
}
|
||
}
|
||
|
||
private static void ApplyPostCommandPumpState(
|
||
PumpControlChannel pump,
|
||
Rs485PumpFlowOperationResult result,
|
||
bool expectedRunning)
|
||
{
|
||
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;
|
||
}
|
||
|
||
if (!expectedRunning)
|
||
{
|
||
pump.PendingRs485RunningState = null;
|
||
pump.IsRunning = false;
|
||
pump.StateAvailable = true;
|
||
pump.Rs485RunStatusCode = 0;
|
||
return;
|
||
}
|
||
|
||
pump.PendingRs485RunningState = expectedRunning;
|
||
pump.IsRunning = expectedRunning;
|
||
pump.StateAvailable = false;
|
||
pump.Rs485RunStatusCode = null;
|
||
}
|
||
|
||
private async Task<Rs485ToggleExecutionResult> TryTogglePumpControlViaRs485(PumpControlChannel pump, bool nextState)
|
||
{
|
||
if (!ShouldUseRs485DirectPumpControl(pump))
|
||
{
|
||
if (IsRs485ManagedPump(pump))
|
||
{
|
||
var message = $"{pump.Name} 当前未配置伺服器 RS485 启停寄存器";
|
||
pump.SetpointStatusText = message;
|
||
Rs485StatusText = message;
|
||
LatestAction = message;
|
||
TraceEvents.Insert(0, NewTrace("RS485 泵控", $"{pump.Name} / {message}"));
|
||
return Rs485ToggleExecutionResult.Failed;
|
||
}
|
||
|
||
return Rs485ToggleExecutionResult.NotHandled;
|
||
}
|
||
|
||
var operationName = nextState ? "启动" : "停止";
|
||
if (!TryBeginRs485PumpOperation(pump, operationName))
|
||
{
|
||
return Rs485ToggleExecutionResult.Failed;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (nextState)
|
||
{
|
||
if (!TryResolveRs485MotorCommand(pump, out var rawMotorSpeed, out var resolveMessage))
|
||
{
|
||
pump.SetpointStatusText = resolveMessage;
|
||
Rs485StatusText = resolveMessage;
|
||
LatestAction = resolveMessage;
|
||
TraceEvents.Insert(0, NewTrace("RS485 泵控", $"{pump.Name} / {resolveMessage}"));
|
||
return Rs485ToggleExecutionResult.Failed;
|
||
}
|
||
|
||
var request = BuildRs485Request(pump);
|
||
var startResult = await Task.Run(() => _pumpActuationService.StartPump(request, rawMotorSpeed));
|
||
ApplyRs485OperationResult(pump, startResult, "RS485 泵控");
|
||
if (!startResult.Success)
|
||
{
|
||
return Rs485ToggleExecutionResult.Failed;
|
||
}
|
||
|
||
CacheResolvedRs485Setpoint(pump, rawMotorSpeed);
|
||
ApplyPostCommandPumpState(pump, startResult, expectedRunning: true);
|
||
await RefreshTelemetryAsync();
|
||
return Rs485ToggleExecutionResult.Succeeded;
|
||
}
|
||
|
||
var stopRequest = BuildRs485Request(pump);
|
||
var stopResult = await Task.Run(() => _pumpActuationService.StopPump(stopRequest));
|
||
ApplyRs485OperationResult(pump, stopResult, "RS485 泵控");
|
||
if (!stopResult.Success)
|
||
{
|
||
return Rs485ToggleExecutionResult.Failed;
|
||
}
|
||
|
||
ApplyPostCommandPumpState(pump, stopResult, expectedRunning: false);
|
||
await RefreshTelemetryAsync();
|
||
return Rs485ToggleExecutionResult.Succeeded;
|
||
}
|
||
finally
|
||
{
|
||
EndRs485PumpOperation(pump);
|
||
}
|
||
}
|
||
}
|
||
|