This commit is contained in:
GukSang.Jin
2026-04-14 12:01:15 +08:00
parent e3d6c69f88
commit 99bcf8d334
5 changed files with 1091 additions and 101 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -198,6 +198,11 @@ public partial class PumpControlChannel : ObservableObject
: PendingRs485RunningState == true || IsRunning
? "停止"
: "启动";
public string ToggleButtonText => IsRs485Busy
? $"{Name}处理中"
: PendingRs485RunningState == true || IsRunning
? $"停止{Name}"
: $"启动{Name}";
public bool CanStartRs485Action => !IsRs485Busy
&& PendingRs485RunningState != true
&& !IsRunning
@@ -228,6 +233,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(ActionText));
OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CanStartRs485Action));
OnPropertyChanged(nameof(CanStopRs485Action));
OnPropertyChanged(nameof(StartActionHint));
@@ -252,6 +258,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateText));
OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CardPrimaryDisplay));
}
@@ -342,6 +349,7 @@ public partial class PumpControlChannel : ObservableObject
OnPropertyChanged(nameof(StateText));
OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(ActionText));
OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CanStartRs485Action));
OnPropertyChanged(nameof(CanStopRs485Action));
OnPropertyChanged(nameof(StartActionHint));
@@ -354,6 +362,7 @@ public partial class PumpControlChannel : ObservableObject
{
OnPropertyChanged(nameof(StateHint));
OnPropertyChanged(nameof(ActionText));
OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CanStartRs485Action));
OnPropertyChanged(nameof(CanStopRs485Action));
OnPropertyChanged(nameof(StartActionHint));

View File

@@ -16,6 +16,12 @@ public partial class ValveControlChannel : ObservableObject
public string StateText => !StateAvailable ? "未知" : IsOpen ? "开启" : "关闭";
public string ActionText => IsOpen ? "关闭" : "开启";
public string ToggleButtonText => Key == "CirculatingWaterTemperature"
? (IsOpen ? "停止循环水温" : "启动循环水温")
: IsOpen
? $"停止{Name}"
: $"启动{Name}";
public string CirculatingWaterToggleText => IsOpen ? "停止循环水温" : "启动循环水温";
public string IndicatorColor => !StateAvailable ? "#FF94A6AE" : IsOpen ? "#FF32B06A" : "#FFC8D4DA";
public string StateHint => !StateAvailable ? "未取得 PLC 状态" : $"{Name}已{StateText}";
public bool HideRealtimeCardStateDescription => Key is "TestLoopValve1" or "TestLoopValve2" or "CirculatingWaterTemperature";
@@ -24,6 +30,8 @@ public partial class ValveControlChannel : ObservableObject
{
OnPropertyChanged(nameof(StateText));
OnPropertyChanged(nameof(ActionText));
OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CirculatingWaterToggleText));
OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(StateHint));
}
@@ -31,6 +39,8 @@ public partial class ValveControlChannel : ObservableObject
partial void OnStateAvailableChanged(bool value)
{
OnPropertyChanged(nameof(StateText));
OnPropertyChanged(nameof(ToggleButtonText));
OnPropertyChanged(nameof(CirculatingWaterToggleText));
OnPropertyChanged(nameof(IndicatorColor));
OnPropertyChanged(nameof(StateHint));
}

View File

@@ -41,6 +41,20 @@ public partial class MainViewModel
private const double DefaultRs485RawPerLitrePerMinute = 100d;
private const double DefaultRs485RawOffset = 0d;
private const int MaxRs485MotorCommand = short.MaxValue;
private const string PressureDropRs485PumpKey = "PressureDropPump";
private const string KinkResistanceRs485PumpKey = "KinkResistancePump";
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;
@@ -78,14 +92,28 @@ public partial class MainViewModel
[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);
@@ -121,6 +149,13 @@ public partial class MainViewModel
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()
{
@@ -290,14 +325,130 @@ public partial class MainViewModel
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)

View File

@@ -353,6 +353,45 @@ public partial class MainViewModel : ObservableObject, IDisposable
public ObservableCollection<DeviceChannel> Channels { get; }
public ObservableCollection<PumpControlChannel> PumpControls { get; }
public ObservableCollection<ValveControlChannel> ValveControls { get; }
public IEnumerable<ValveControlChannel> RealtimeValveControls =>
ValveControls.Where(item =>
!string.Equals(item.Key, "CirculatingWaterTemperature", StringComparison.Ordinal)
&& !string.Equals(item.Key, "TestLoopValve1", StringComparison.Ordinal)
&& !string.Equals(item.Key, "TestLoopValve2", StringComparison.Ordinal));
public ValveControlChannel? CirculatingWaterControl =>
ValveControls.FirstOrDefault(item => string.Equals(item.Key, "CirculatingWaterTemperature", StringComparison.Ordinal));
public bool HasCirculatingWaterControl => CirculatingWaterControl is not null;
public IEnumerable<ValveControlChannel> PressureDropAuxiliaryValveControls =>
ValveControls.Where(item =>
string.Equals(item.Key, "CirculatingWaterTemperature", StringComparison.Ordinal)
|| string.Equals(item.Key, "TestLoopValve1", StringComparison.Ordinal))
.OrderBy(item => string.Equals(item.Key, "CirculatingWaterTemperature", StringComparison.Ordinal) ? 0 : 1);
public bool HasPressureDropAuxiliaryValveControls => PressureDropAuxiliaryValveControls.Any();
public IEnumerable<ValveControlChannel> PressureDropSecondaryValveControls =>
ValveControls.Where(item => string.Equals(item.Key, "TestLoopValve1", StringComparison.Ordinal));
public bool HasPressureDropSecondaryValveControls => PressureDropSecondaryValveControls.Any();
public ValveControlChannel? RecirculationCirculatingWaterControl =>
ValveControls.FirstOrDefault(item => string.Equals(item.Key, "CirculatingWaterTemperature", StringComparison.Ordinal));
public bool HasRecirculationCirculatingWaterControl => RecirculationCirculatingWaterControl is not null;
public PumpControlChannel? AntiCollapseNegativeAssistPumpControl =>
NegativeAssistPumpControls.FirstOrDefault();
public bool HasAntiCollapseNegativeAssistPumpControl => AntiCollapseNegativeAssistPumpControl is not null;
public ValveControlChannel? AntiCollapseCirculatingWaterControl =>
ValveControls.FirstOrDefault(item => string.Equals(item.Key, "CirculatingWaterTemperature", StringComparison.Ordinal));
public bool HasAntiCollapseCirculatingWaterControl => AntiCollapseCirculatingWaterControl is not null;
public ValveControlChannel? AntiCollapseTestLoopValve2Control =>
ValveControls.FirstOrDefault(item => string.Equals(item.Key, "TestLoopValve2", StringComparison.Ordinal));
public bool HasAntiCollapseTestLoopValve2Control => AntiCollapseTestLoopValve2Control is not null;
public IEnumerable<ValveControlChannel> AntiCollapseAuxiliaryValveControls =>
ValveControls.Where(item =>
string.Equals(item.Key, "CirculatingWaterTemperature", StringComparison.Ordinal)
|| string.Equals(item.Key, "TestLoopValve2", StringComparison.Ordinal))
.OrderBy(item => string.Equals(item.Key, "CirculatingWaterTemperature", StringComparison.Ordinal) ? 0 : 1);
public bool HasAntiCollapseAuxiliaryValveControls => AntiCollapseAuxiliaryValveControls.Any();
public IEnumerable<ValveControlChannel> AntiCollapseSecondaryValveControls =>
ValveControls.Where(item => string.Equals(item.Key, "TestLoopValve2", StringComparison.Ordinal));
public bool HasAntiCollapseSecondaryValveControls => AntiCollapseSecondaryValveControls.Any();
public bool HasAntiCollapsePumpControls => NegativeAssistPumpControls.Any();
public IEnumerable<PumpControlChannel> NegativeAssistPumpControls => PumpControlsFor("NegativeAssistPump");
public IEnumerable<PumpControlChannel> PressureDropPumpControls => PumpControlsFor("NegativeAssistPump", "PressureDropPump");
public IEnumerable<PumpControlChannel> RecirculationPumpControls => PumpControlsFor("RecirculationMainPump", "RecirculationReturnPump", "RecirculationDrainagePump");
@@ -559,7 +598,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
public string PressureTrendCurrentSummary => $"近端 {ProximalPressureDisplay} / 远端 {DistalPressureDisplay} / ΔP {DeltaPressureDisplay}";
public string FlowTrendCurrentSummary => BuildFlowTrendCurrentSummary();
public string HemolysisRecordTemplate =>
"3.2 试验血液准备\n" +
"1. 试验血液准备\n" +
$"- 依据 GB/T 16886.4 进行血液预处理。\n" +
$"- 血液来源:{HemolysisTestParameters.BloodSource}\n" +
$"- 采血日期:{FormatHemolysisDate(HemolysisTestParameters.CollectionDate)}\n" +
@@ -569,13 +608,13 @@ public partial class MainViewModel : ObservableObject, IDisposable
$"- 血中葡萄糖:{FormatHemolysisValue(HemolysisTestParameters.Glucose, "F1")} mmol/L\n" +
$"- 血红蛋白:{FormatHemolysisValue(HemolysisTestParameters.TotalHemoglobin, "F1")} g/dL\n" +
$"- 初始游离血红蛋白 fHb{FormatHemolysisValue(GetHemolysisInitialFreeHemoglobin(), "F1")} mg/dL\n\n" +
"3.3 测试回路说明\n" +
"2. 测试回路说明\n" +
$"- 回路总充盈量:{FormatHemolysisValue(HemolysisTestParameters.CircuitPrimingVolume, "F0")} mL\n" +
$"- 回路初始血液通道试验液容积差:{FormatHemolysisValue(HemolysisTestParameters.CircuitVolumeDifference, "F2")} %(应 ≤ 1%\n" +
$"- 设定流量:{FormatHemolysisValue(HemolysisTestParameters.SetFlow, "F2")} L/min\n" +
$"- 设定运行时间:{FormatHemolysisValue(HemolysisTestParameters.RunTimeMinutes, "F0")} min\n" +
$"- 温度控制:{FormatHemolysisValue(HemolysisTestParameters.TargetTemperature, "F1")} ℃ ± 2 ℃\n\n" +
"4. 试验运行与取样记录";
"3. 试验运行与取样记录";
public string HemolysisRecordNoteTemplate => BuildHemolysisRecordNoteText();
public bool HasAntiCollapseBaseline => _antiCollapseBaselinePressureDrop.HasValue && _antiCollapseBaselineFlow.HasValue;
public string AntiCollapseBaselineDisplay => HasAntiCollapseBaseline
@@ -778,11 +817,35 @@ public partial class MainViewModel : ObservableObject, IDisposable
}
var nextState = !valve.IsOpen;
ValveControlChannel? pairedValve = null;
if (nextState)
{
pairedValve = valve.Key switch
{
"TestLoopValve1" => ValveControls.FirstOrDefault(item => string.Equals(item.Key, "TestLoopValve2", StringComparison.Ordinal)),
"TestLoopValve2" => ValveControls.FirstOrDefault(item => string.Equals(item.Key, "TestLoopValve1", StringComparison.Ordinal)),
_ => null
};
if (pairedValve?.IsOpen == true)
{
_telemetryService.SetValveOpen(pairedValve.Key, false);
}
}
_telemetryService.SetValveOpen(valve.Key, nextState);
var traceDetail = $"{valve.Name} => {(nextState ? "" : "")}";
if (pairedValve?.IsOpen == true)
{
traceDetail += $" / {pairedValve.Name} => 关闭";
}
LatestAction = IsTelemetryOnline
? $"{valve.Name} 已发送{(nextState ? "" : "")}指令。"
? pairedValve?.IsOpen == true
? $"{valve.Name} 已发送{(nextState ? "" : "")}指令,并自动关闭 {pairedValve.Name}。"
: $"{valve.Name} 已发送{(nextState ? "" : "")}指令。"
: $"{valve.Name} 指令未下发PLC 当前离线。";
TraceEvents.Insert(0, NewTrace("阀控", $"{valve.Name} => {(nextState ? "" : "")}"));
TraceEvents.Insert(0, NewTrace("阀控", traceDetail));
_ = RefreshTelemetryAsync();
}
@@ -1349,7 +1412,6 @@ public partial class MainViewModel : ObservableObject, IDisposable
RefreshTelemetryPanel();
RefreshDeviceStatus();
RefreshComputedState();
RefreshFilteredItemsView();
}
catch (Exception ex)
{
@@ -1760,7 +1822,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
var calculationHematocrit = GetHemolysisCalculationHematocrit();
return
"5. 结果计算\n" +
"4. 结果计算\n" +
$"- 标准取样点完成情况:{BuildHemolysisSamplingCompletionSummary()}\n" +
$"- ΔfHb (T360 - T0){FormatHemolysisDisplay(GetHemolysisDeltaFreeHemoglobin(), "F1", "mg/dL")}\n" +
"- NIH = ΔfHb × V(L) × (1-Hct) / (Q×T)\n" +