diff --git a/Cardiopulmonarybypasssystems/MainWindow.xaml b/Cardiopulmonarybypasssystems/MainWindow.xaml
index d93d937..b85fa66 100644
--- a/Cardiopulmonarybypasssystems/MainWindow.xaml
+++ b/Cardiopulmonarybypasssystems/MainWindow.xaml
@@ -477,6 +477,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Cardiopulmonarybypasssystems/Models/ValveControlChannel.cs b/Cardiopulmonarybypasssystems/Models/ValveControlChannel.cs
new file mode 100644
index 0000000..08b4c0e
--- /dev/null
+++ b/Cardiopulmonarybypasssystems/Models/ValveControlChannel.cs
@@ -0,0 +1,26 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Cardiopulmonarybypasssystems.Models;
+
+public partial class ValveControlChannel : ObservableObject
+{
+ public required string Key { get; init; }
+ public required string Name { get; init; }
+ public int StartAddress { get; init; }
+
+ [ObservableProperty]
+ private bool isOpen;
+
+ public string StateText => IsOpen ? "开启" : "关闭";
+ public string ActionText => IsOpen ? "关闭阀门" : "开启阀门";
+ public string IndicatorColor => IsOpen ? "#FF32B06A" : "#FFC8D4DA";
+ public string StateHint => IsOpen ? "测试回路已导通" : "测试回路已关闭";
+
+ partial void OnIsOpenChanged(bool value)
+ {
+ OnPropertyChanged(nameof(StateText));
+ OnPropertyChanged(nameof(ActionText));
+ OnPropertyChanged(nameof(IndicatorColor));
+ OnPropertyChanged(nameof(StateHint));
+ }
+}
diff --git a/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs
index 1fde60c..478ffd1 100644
--- a/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs
+++ b/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs
@@ -6,6 +6,8 @@ public interface IModbusTelemetryService
{
IReadOnlyList GetChannels();
IReadOnlyList GetPumpControls();
+ IReadOnlyList GetValveControls();
IReadOnlyList UpdateChannels();
void SetPumpRunning(string pumpKey, bool isRunning);
+ void SetValveOpen(string valveKey, bool isOpen);
}
diff --git a/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs
index c984da9..d9c8cda 100644
--- a/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs
+++ b/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs
@@ -76,6 +76,11 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
new() { Key = "HemolysisReturnSinglePump", Name = "血细胞破坏-单腔回输泵", StartAddress = 7, FlowAddress = 1060 },
new() { Key = "HemolysisDualLumenPump", Name = "血细胞破坏-双腔泵", StartAddress = 8, FlowAddress = 1070 }
];
+ private readonly List _valveControls =
+ [
+ new() { Key = "TestLoopValve1", Name = "测试回路阀 1", StartAddress = 10 },
+ new() { Key = "TestLoopValve2", Name = "测试回路阀 2", StartAddress = 11 }
+ ];
private TcpClient? _tcpClient;
private IModbusMaster? _master;
@@ -95,6 +100,12 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
return _pumpControls;
}
+ public IReadOnlyList GetValveControls()
+ {
+ EnsureConnectionScheduled();
+ return _valveControls;
+ }
+
public IReadOnlyList UpdateChannels()
{
EnsureConnectionScheduled();
@@ -138,6 +149,35 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
}
}
+ public void SetValveOpen(string valveKey, bool isOpen)
+ {
+ lock (_syncRoot)
+ {
+ var valve = _valveControls.FirstOrDefault(item => item.Key == valveKey);
+ if (valve is null)
+ {
+ return;
+ }
+
+ valve.IsOpen = isOpen;
+
+ if (_master is null)
+ {
+ return;
+ }
+
+ try
+ {
+ _master.WriteSingleCoil(SlaveId, (ushort)valve.StartAddress, isOpen);
+ }
+ catch
+ {
+ ReleaseConnection();
+ _nextConnectionAttemptUtc = DateTime.MinValue;
+ }
+ }
+ }
+
public void Dispose()
{
lock (_syncRoot)
diff --git a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
index 29e1bd0..5b74b3e 100644
--- a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
+++ b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
@@ -137,6 +137,7 @@ public partial class MainViewModel : ObservableObject
FilteredItemsView.Filter = MatchesFilteredItem;
Channels = new ObservableCollection(telemetryService.GetChannels());
PumpControls = new ObservableCollection(telemetryService.GetPumpControls());
+ ValveControls = new ObservableCollection(telemetryService.GetValveControls());
TraceEvents = new ObservableCollection(repository.GetSeedTraceEvents());
AlarmMessages = new ObservableCollection();
ResultStatusOptions = new ObservableCollection(["待检", "合格", "预警", "不合格"]);
@@ -190,6 +191,7 @@ public partial class MainViewModel : ObservableObject
public ICollectionView FilteredItemsView { get; }
public ObservableCollection Channels { get; }
public ObservableCollection PumpControls { get; }
+ public ObservableCollection ValveControls { get; }
public IEnumerable PressureDropPumpControls => PumpControlsFor("NegativeAssistPump", "PressureDropPump");
public IEnumerable RecirculationPumpControls => PumpControlsFor("RecirculationMainPump", "RecirculationReturnPump", "RecirculationDrainagePump");
public IEnumerable KinkResistancePumpControls => PumpControlsFor("KinkResistancePump");
@@ -451,6 +453,21 @@ public partial class MainViewModel : ObservableObject
RefreshTelemetry();
}
+ [RelayCommand]
+ private void ToggleValveControl(ValveControlChannel? valve)
+ {
+ if (valve is null)
+ {
+ return;
+ }
+
+ var nextState = !valve.IsOpen;
+ _telemetryService.SetValveOpen(valve.Key, nextState);
+ valve.IsOpen = nextState;
+ LatestAction = $"{valve.Name} 已{(nextState ? "开启" : "关闭")}。";
+ TraceEvents.Insert(0, NewTrace("阀控", $"{valve.Name} => {(nextState ? "开启" : "关闭")}"));
+ }
+
[RelayCommand]
private void ClearTrendData()
{