diff --git a/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs index 478ffd1..b08d94d 100644 --- a/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs +++ b/Cardiopulmonarybypasssystems/Services/IModbusTelemetryService.cs @@ -4,6 +4,7 @@ namespace Cardiopulmonarybypasssystems.Services; public interface IModbusTelemetryService { + bool IsLiveConnected { get; } IReadOnlyList GetChannels(); IReadOnlyList GetPumpControls(); IReadOnlyList GetValveControls(); diff --git a/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs b/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs index e5d9bdf..1309176 100644 --- a/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs +++ b/Cardiopulmonarybypasssystems/Services/MockModbusTelemetryService.cs @@ -11,6 +11,7 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo private const string IpAddress = "192.168.1.10"; private const int Port = 502; private const byte SlaveId = 1; + // Keep distinct pressure registers until the distal PLC address is confirmed. private const ushort ProximalPressureRegister = 1330; private const ushort DistalPressureRegister = 1380; private const double FlowRegisterScale = 0.01d; @@ -48,33 +49,33 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo private readonly Dictionary> _channelWindows = new(StringComparer.Ordinal); private readonly List _channels = [ - new() { Name = "主泵流量", Unit = "L/min", Value = 4.32, Min = 0, Max = 7 }, - new() { Name = "再循环主泵流量", Unit = "L/min", Value = 4.86, Min = 0, Max = 7 }, - new() { Name = "动脉回输流量", Unit = "L/min", Value = 4.74, Min = 0, Max = 7 }, - new() { Name = "静脉引流流量", Unit = "L/min", Value = 4.46, Min = 0, Max = 7 }, - new() { Name = "抗扭结主泵流量", Unit = "L/min", Value = 4.68, Min = 0, Max = 7 }, - new() { Name = "血细胞破坏-单腔引流/回输流量", Unit = "L/min", Value = 4.25, Min = 0, Max = 7 }, - new() { Name = "双腔插管试验回路流量", Unit = "L/min", Value = 4.30, Min = 0, Max = 7 }, - new() { Name = "双腔插管试验回路流量(两个管腔)", Unit = "L/min", Value = 4.12, Min = 0, Max = 7 }, - new() { Name = "远端压力", Unit = "mmHg", Value = 94, Min = 40, Max = 180 }, - new() { Name = "近端压力", Unit = "mmHg", Value = 112, Min = 60, Max = 220 }, - new() { Name = "负压辅助引流", Unit = "kPa", Value = -6.7, Min = -20, Max = 0 }, + new() { Name = "主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 }, + new() { Name = "再循环主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 }, + new() { Name = "动脉回输流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 }, + new() { Name = "静脉引流流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 }, + new() { Name = "抗扭结主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 }, + new() { Name = "血细胞破坏-单腔引流/回输流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 }, + new() { Name = "双腔插管试验回路流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 }, + new() { Name = "双腔插管试验回路流量(两个管腔)", Unit = "L/min", Value = 0, Min = 0, Max = 7 }, + new() { Name = "远端压力", Unit = "mmHg", Value = 68, Min = 40, Max = 180 }, + new() { Name = "近端压力", Unit = "mmHg", Value = 80, Min = 60, Max = 220 }, + new() { Name = "负压辅助引流", Unit = "kPa", Value = 0, Min = -20, Max = 0 }, new() { Name = "模拟血液温度", Unit = "°C", Value = 37.1, Min = 34, Max = 40 }, - new() { Name = "再循环率", Unit = "%", Value = 6.8, Min = 0, Max = 20 }, + new() { Name = "再循环率", Unit = "%", Value = 0, Min = 0, Max = 20 }, new() { Name = "游离血红蛋白", Unit = "g/L", Value = 0.028, Min = 0, Max = 0.08 }, new() { Name = "白细胞减少率", Unit = "%", Value = 7.1, Min = 0, Max = 20 } ]; private readonly List _pumpControls = [ new() { Key = "NegativeAssistPump", Name = "负压泵", StartAddress = 0 }, - new() { Key = "PressureDropPump", Name = "压力降/抗塌陷泵", StartAddress = 1, FlowAddress = 1000, IsRunning = true }, - new() { Key = "RecirculationMainPump", Name = "再循环主泵", StartAddress = 2, FlowAddress = 1010, IsRunning = true }, - new() { Key = "RecirculationReturnPump", Name = "回流泵", StartAddress = 3, FlowAddress = 1020, IsRunning = true }, - new() { Key = "RecirculationDrainagePump", Name = "引流泵", StartAddress = 4, FlowAddress = 1030, IsRunning = true }, - new() { Key = "KinkResistancePump", Name = "抗扭结泵", StartAddress = 5, FlowAddress = 1040, IsRunning = true }, - new() { Key = "HemolysisDrainageSinglePump", Name = "血细胞破坏-单腔引流/回输泵", StartAddress = 6, FlowAddress = 1050 }, - new() { Key = "HemolysisReturnSinglePump", Name = "双腔插管试验回路泵", StartAddress = 7, FlowAddress = 1060 }, - new() { Key = "HemolysisDualLumenPump", Name = "双腔插管试验回路泵(两个管腔)", StartAddress = 8, FlowAddress = 1070 } + new() { Key = "PressureDropPump", Name = "压力降/抗塌陷泵", StartAddress = 1, FlowAddress = FlowRegisters["PressureDropPump"] }, + new() { Key = "RecirculationMainPump", Name = "再循环主泵", StartAddress = 2, FlowAddress = FlowRegisters["RecirculationMainPump"] }, + new() { Key = "RecirculationReturnPump", Name = "回流泵", StartAddress = 3, FlowAddress = FlowRegisters["RecirculationReturnPump"] }, + new() { Key = "RecirculationDrainagePump", Name = "引流泵", StartAddress = 4, FlowAddress = FlowRegisters["RecirculationDrainagePump"] }, + new() { Key = "KinkResistancePump", Name = "抗扭结泵", StartAddress = 5, FlowAddress = FlowRegisters["KinkResistancePump"] }, + new() { Key = "HemolysisDrainageSinglePump", Name = "血细胞破坏-单腔引流/回输泵", StartAddress = 6, FlowAddress = FlowRegisters["HemolysisDrainageSinglePump"] }, + new() { Key = "HemolysisReturnSinglePump", Name = "双腔插管试验回路泵", StartAddress = 7, FlowAddress = FlowRegisters["HemolysisReturnSinglePump"] }, + new() { Key = "HemolysisDualLumenPump", Name = "双腔插管试验回路泵(两个管腔)", StartAddress = 8, FlowAddress = FlowRegisters["HemolysisDualLumenPump"] } ]; private readonly List _valveControls = [ @@ -88,6 +89,21 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo private Task? _connectionTask; private DateTime _nextConnectionAttemptUtc = DateTime.MinValue; + private int HighestConfiguredCoilAddress => Math.Max( + _pumpControls.Max(item => item.StartAddress), + _valveControls.Max(item => item.StartAddress)); + + public bool IsLiveConnected + { + get + { + lock (_syncRoot) + { + return _master is not null && _tcpClient?.Connected == true; + } + } + } + public IReadOnlyList GetChannels() { EnsureConnectionScheduled(); @@ -222,6 +238,7 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo tcpClient.ReceiveTimeout = (int)ConnectionAttemptTimeout.TotalMilliseconds; tcpClient.SendTimeout = (int)ConnectionAttemptTimeout.TotalMilliseconds; var master = _factory.CreateMaster(tcpClient); + ApplySafeStartupState(master); lock (_syncRoot) { @@ -253,10 +270,15 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo try { - var coilStates = _master.ReadCoils(SlaveId, 0, (ushort)_pumpControls.Count); - for (var index = 0; index < _pumpControls.Count; index++) + var coilStates = _master.ReadCoils(SlaveId, 0, (ushort)(HighestConfiguredCoilAddress + 1)); + foreach (var pump in _pumpControls) { - _pumpControls[index].IsRunning = coilStates[index]; + pump.IsRunning = coilStates[pump.StartAddress]; + } + + foreach (var valve in _valveControls) + { + valve.IsOpen = coilStates[valve.StartAddress]; } foreach (var pump in _pumpControls.Where(item => item.FlowAddress.HasValue)) @@ -439,6 +461,53 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo return window; } + private void ApplySafeStartupState(IModbusMaster master) + { + foreach (var pump in _pumpControls) + { + master.WriteSingleCoil(SlaveId, (ushort)pump.StartAddress, false); + } + + foreach (var valve in _valveControls) + { + master.WriteSingleCoil(SlaveId, (ushort)valve.StartAddress, false); + } + + ApplyLocalSafeState(); + } + + private void ApplyLocalSafeState() + { + foreach (var pump in _pumpControls) + { + pump.IsRunning = false; + pump.FlowValue = 0d; + } + + foreach (var valve in _valveControls) + { + valve.IsOpen = false; + } + + foreach (var channelName in FlowChannelNames.Values) + { + SetChannelValueDirect(channelName, 0d); + } + + SetChannelValueDirect("负压辅助引流", 0d); + SetChannelValueDirect("再循环率", 0d); + } + + private void SetChannelValueDirect(string channelName, double nextValue) + { + var channel = Channel(channelName); + var clampedValue = Math.Clamp(nextValue, channel.Min, channel.Max); + var window = Window(channelName); + window.Clear(); + window.Enqueue(clampedValue); + channel.Value = clampedValue; + } + private void ReleaseConnection() { _master?.Dispose(); diff --git a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs index 71f55b2..8d52698 100644 --- a/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs +++ b/Cardiopulmonarybypasssystems/ViewModels/MainViewModel.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Text.Json; @@ -52,7 +52,7 @@ public partial class MainViewModel : ObservableObject private string batchNumber = $"LOT-{DateTime.Now:yyyyMMdd}-01"; [ObservableProperty] - private string deviceStatus = "在线"; + private string deviceStatus = "连接中"; [ObservableProperty] private bool acquisitionRunning = true; @@ -209,6 +209,7 @@ public partial class MainViewModel : ObservableObject } RefreshTelemetryPanel(); + RefreshDeviceStatus(); RefreshComputedState(); _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; @@ -668,7 +669,7 @@ public partial class MainViewModel : ObservableObject private void ToggleAcquisition() { AcquisitionRunning = !AcquisitionRunning; - DeviceStatus = AcquisitionRunning ? "在线" : "采集暂停"; + RefreshDeviceStatus(); LatestAction = AcquisitionRunning ? "继续采集实时数据,供检测参考。" : "已暂停实时采集。"; if (AcquisitionRunning) @@ -857,6 +858,7 @@ public partial class MainViewModel : ObservableObject } RefreshTelemetryPanel(); + RefreshDeviceStatus(); RefreshComputedState(); RefreshFilteredItemsView(); } @@ -915,6 +917,17 @@ public partial class MainViewModel : ObservableObject SyncRealtimeItems(); } + private void RefreshDeviceStatus() + { + if (!AcquisitionRunning) + { + DeviceStatus = DetectionCompleted ? "采集停止" : "采集暂停"; + return; + } + + DeviceStatus = _telemetryService.IsLiveConnected ? "PLC在线" : "PLC离线(模拟)"; + } + private void RefreshComputedState() { QualifiedCount = InspectionItems.Count(r => r.Status == InspectionItemStatus.Qualified);