diff --git a/Cardiopulmonarybypasssystems/MainWindow.xaml b/Cardiopulmonarybypasssystems/MainWindow.xaml
index 9e7c13b..c67825c 100644
--- a/Cardiopulmonarybypasssystems/MainWindow.xaml
+++ b/Cardiopulmonarybypasssystems/MainWindow.xaml
@@ -1342,6 +1342,24 @@
+
+
diff --git a/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs
index daa5615..1675742 100644
--- a/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs
+++ b/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs
@@ -16,6 +16,7 @@ public interface IModbusTelemetryService
float? ReadHoldingFloatRegister(ushort address);
bool WriteHoldingRegister(ushort address, ushort value);
bool WriteHoldingFloatRegister(ushort address, float value);
+ bool WriteCoil(ushort address, bool value, string operationName);
// Legacy PLC coil path retained only for non-RS485 pump compatibility.
void SetPumpRunning(string pumpKey, bool isRunning);
void SetValveOpen(string valveKey, bool isOpen);
diff --git a/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
index 2e6684f..d402598 100644
--- a/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
+++ b/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
@@ -386,6 +386,45 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
}
}
+ public bool WriteCoil(ushort address, bool value, string operationName)
+ {
+ lock (_syncRoot)
+ {
+ if (_master is null)
+ {
+ _lastErrorMessage = $"PLC 离线,未执行{operationName}线圈写入:M{address}";
+ Logger.Warning(
+ "PLC 离线,跳过线圈写入,Operation={Operation},Coil=M{CoilAddress},Value={Value}",
+ operationName,
+ address,
+ value);
+ return false;
+ }
+
+ try
+ {
+ _master.WriteSingleCoil(_slaveId, address, value);
+ Logger.Information(
+ "PLC 线圈写入成功,Operation={Operation},Coil=M{CoilAddress},Value={Value}",
+ operationName,
+ address,
+ value);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(
+ ex,
+ "PLC 线圈写入失败,Operation={Operation},Coil=M{CoilAddress},Value={Value}",
+ operationName,
+ address,
+ value);
+ HandleConnectionFailure($"{operationName}线圈 M{address} 写入失败:{ex.Message}");
+ return false;
+ }
+ }
+ }
+
public void SetValveOpen(string valveKey, bool isOpen)
{
lock (_syncRoot)
diff --git a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
index ef5888f..266dc47 100644
--- a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
+++ b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs
@@ -31,6 +31,9 @@ public partial class MainViewModel : ObservableObject, IDisposable
private const string SettingsDirectoryName = "Cardiopulmonarybypasssystems";
private const string LimitSettingsFileName = "manufacturer-limits.json";
private const float EngineeringFloatVerificationTolerance = 0.0001f;
+ private const ushort ProximalPressureCalibrationCoil = 1300;
+ private const ushort DistalPressureCalibrationCoil = 1301;
+ private static readonly TimeSpan PressureCalibrationPulseDuration = TimeSpan.FromMilliseconds(300);
private static readonly (string Name, ushort Address)[] FlowCoefficientRegisters =
[
("流量系数1", 1006),
@@ -62,6 +65,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
private string _lastTelemetryRefreshFailureMessage = string.Empty;
private double? _proximalPressureRawKpa;
private double? _distalPressureRawKpa;
+ private bool _pressureCalibrationPulseInProgress;
private static string ResolveLimitSettingsPath()
{
return Path.Combine(
@@ -875,6 +879,71 @@ public partial class MainViewModel : ObservableObject, IDisposable
_ = RefreshTelemetryAsync();
}
+ [RelayCommand]
+ private async Task CalibrateProximalPressure()
+ {
+ await PulsePressureCalibrationAsync("近端压力校准", ProximalPressureCalibrationCoil);
+ }
+
+ [RelayCommand]
+ private async Task CalibrateDistalPressure()
+ {
+ await PulsePressureCalibrationAsync("远端压力校准", DistalPressureCalibrationCoil);
+ }
+
+ private async Task PulsePressureCalibrationAsync(string calibrationName, ushort coilAddress)
+ {
+ if (!EnsureSessionEditable(calibrationName))
+ {
+ return;
+ }
+
+ if (_pressureCalibrationPulseInProgress)
+ {
+ LatestAction = "压力校准指令正在执行,请稍后再操作。";
+ return;
+ }
+
+ _pressureCalibrationPulseInProgress = true;
+ try
+ {
+ Logger.Information(
+ "执行压力校准脉冲,CalibrationName={CalibrationName},Coil=M{CoilAddress},TelemetryOnline={TelemetryOnline}",
+ calibrationName,
+ coilAddress,
+ IsTelemetryOnline);
+
+ if (!_telemetryService.WriteCoil(coilAddress, true, calibrationName))
+ {
+ LatestAction = IsTelemetryOnline
+ ? $"{calibrationName} 指令下发失败:{_telemetryService.LastErrorMessage}"
+ : $"{calibrationName} 指令未下发,PLC 当前离线。";
+ TraceEvents.Insert(0, NewTrace("压力校准", $"{calibrationName} / M{coilAddress}=1 下发失败"));
+ return;
+ }
+
+ LatestAction = $"{calibrationName} 已触发,M{coilAddress}=1。";
+ TraceEvents.Insert(0, NewTrace("压力校准", $"{calibrationName} / M{coilAddress}=1"));
+
+ await Task.Delay(PressureCalibrationPulseDuration);
+
+ if (!_telemetryService.WriteCoil(coilAddress, false, calibrationName))
+ {
+ LatestAction = $"{calibrationName} 释放失败,请检查 PLC 线圈 M{coilAddress}。";
+ TraceEvents.Insert(0, NewTrace("压力校准", $"{calibrationName} / M{coilAddress}=0 释放失败"));
+ return;
+ }
+
+ LatestAction = $"{calibrationName} 已完成校准脉冲。";
+ TraceEvents.Insert(0, NewTrace("压力校准", $"{calibrationName} / M{coilAddress}=0"));
+ }
+ finally
+ {
+ _pressureCalibrationPulseInProgress = false;
+ _ = RefreshTelemetryAsync();
+ }
+ }
+
[RelayCommand]
private void ClearTrendData()
{