无塌陷

This commit is contained in:
GukSang.Jin
2026-03-10 15:26:28 +08:00
parent 22fb201881
commit 1d6a1312c7
4 changed files with 389 additions and 46 deletions

View File

@@ -274,6 +274,27 @@
<StackPanel Grid.Column="2"> <StackPanel Grid.Column="2">
<TextBlock Style="{StaticResource CaptionStyle}" Text="&#x586B;&#x5199;&#x8BF4;&#x660E;" /> <TextBlock Style="{StaticResource CaptionStyle}" Text="&#x586B;&#x5199;&#x8BF4;&#x660E;" />
<TextBlock Margin="0,0,0,6" Foreground="{StaticResource MutedTextBrush}" FontSize="13" Text="{Binding RealtimeMeasurementHint}" TextWrapping="Wrap" /> <TextBlock Margin="0,0,0,6" Foreground="{StaticResource MutedTextBrush}" FontSize="13" Text="{Binding RealtimeMeasurementHint}" TextWrapping="Wrap" />
<Border Margin="0,0,0,8" Padding="12" Background="#FFF8F4EA" CornerRadius="14">
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsAntiCollapseSelected}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Style="{StaticResource CaptionStyle}" Text="&#x6297;&#x584C;&#x9677;&#x5FEB;&#x901F;&#x91C7;&#x6837;" />
<TextBlock FontSize="13" Text="{Binding AntiCollapseBaselineDisplay}" TextWrapping="Wrap" />
<TextBlock Margin="0,4,0,0" FontSize="13" Text="{Binding AntiCollapseComparisonDisplay}" TextWrapping="Wrap" />
<WrapPanel Margin="0,8,0,0">
<Button Command="{Binding CaptureAntiCollapseBaselineCommand}" Content="&#x91C7;&#x96C6;&#x57FA;&#x7EBF;" Background="#FF6B8791" />
<Button Command="{Binding CaptureAntiCollapseComparisonCommand}" Content="&#x91C7;&#x96C6;&#x8D1F;&#x538B;&#x6BD4;&#x8F83;" Background="#FFE0A14A" />
</WrapPanel>
</StackPanel>
</Border>
<TextBox Text="{Binding ResultValue, UpdateSourceTrigger=PropertyChanged}" MinHeight="104" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" IsReadOnly="{Binding SelectedItemUsesRealtimeValue}" /> <TextBox Text="{Binding ResultValue, UpdateSourceTrigger=PropertyChanged}" MinHeight="104" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" IsReadOnly="{Binding SelectedItemUsesRealtimeValue}" />
<TextBlock Style="{StaticResource CaptionStyle}" Text="&#x5224;&#x5B9A;" /> <TextBlock Style="{StaticResource CaptionStyle}" Text="&#x5224;&#x5B9A;" />
<ComboBox ItemsSource="{Binding ResultStatusOptions}" SelectedItem="{Binding SelectedResultStatusText, Mode=TwoWay}" /> <ComboBox ItemsSource="{Binding ResultStatusOptions}" SelectedItem="{Binding SelectedResultStatusText, Mode=TwoWay}" />

View File

@@ -1,10 +1,18 @@
using Cardiopulmonarybypasssystems.Models; using System.Net.Sockets;
using Cardiopulmonarybypasssystems.Models;
using NModbus; using NModbus;
namespace Cardiopulmonarybypasssystems.Services; namespace Cardiopulmonarybypasssystems.Services;
public sealed class MockModbusTelemetryService : IModbusTelemetryService public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDisposable
{ {
private const string IpAddress = "192.168.1.10";
private const int Port = 502;
private const byte SlaveId = 1;
private const ushort ProximalPressureRegister = 1330;
private const ushort DistalPressureRegister = 1380;
private readonly object _syncRoot = new();
private readonly Random _random = new(); private readonly Random _random = new();
private readonly ModbusFactory _factory = new(); private readonly ModbusFactory _factory = new();
private readonly List<DeviceChannel> _channels = private readonly List<DeviceChannel> _channels =
@@ -12,8 +20,8 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService
new() { Name = "主泵流量", Unit = "L/min", Value = 4.82, Min = 0, Max = 7 }, new() { Name = "主泵流量", Unit = "L/min", Value = 4.82, Min = 0, Max = 7 },
new() { Name = "静脉引流流量", Unit = "L/min", Value = 4.54, Min = 0, Max = 7 }, new() { Name = "静脉引流流量", Unit = "L/min", Value = 4.54, Min = 0, Max = 7 },
new() { Name = "动脉回输流量", Unit = "L/min", Value = 4.86, Min = 0, Max = 7 }, new() { Name = "动脉回输流量", Unit = "L/min", Value = 4.86, Min = 0, Max = 7 },
new() { Name = "入口压力", Unit = "mmHg", Value = 112, Min = 60, Max = 220 }, new() { Name = "远端压力", Unit = "mmHg", Value = 94, Min = 40, Max = 180 },
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 = -10.4, Min = -20, Max = 0 }, new() { Name = "负压辅助引流", Unit = "kPa", Value = -10.4, Min = -20, Max = 0 },
new() { Name = "模拟血液温度", Unit = "°C", Value = 37.1, Min = 34, Max = 40 }, 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 = 6.8, Min = 0, Max = 20 },
@@ -21,21 +29,117 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService
new() { Name = "白细胞减少率", Unit = "%", Value = 7.1, Min = 0, Max = 20 } new() { Name = "白细胞减少率", Unit = "%", Value = 7.1, Min = 0, Max = 20 }
]; ];
public IReadOnlyList<DeviceChannel> GetChannels() => _channels; private TcpClient? _tcpClient;
private IModbusMaster? _master;
private bool _connectionInitialized;
public IReadOnlyList<DeviceChannel> GetChannels()
{
EnsureConnected();
return _channels;
}
public IReadOnlyList<AlarmMessage> UpdateChannels() public IReadOnlyList<AlarmMessage> UpdateChannels()
{ {
_ = _factory; EnsureConnected();
foreach (var channel in _channels) lock (_syncRoot)
{
TryReadPressureChannels();
SimulateAuxiliaryChannels();
var pumpFlow = Channel("主泵流量");
var drainageFlow = Channel("静脉引流流量");
var returnFlow = Channel("动脉回输流量");
drainageFlow.Value = Math.Clamp(
Math.Min(drainageFlow.Value, returnFlow.Value - Next(0.12, 0.48)),
drainageFlow.Min,
drainageFlow.Max);
pumpFlow.Value = Math.Clamp(
(drainageFlow.Value + returnFlow.Value) / 2d + Next(-0.05, 0.05),
pumpFlow.Min,
pumpFlow.Max);
var recirculationRate = returnFlow.Value <= 0.01
? 0
: Math.Clamp((returnFlow.Value - drainageFlow.Value) / returnFlow.Value * 100d, 0d, 100d);
Channel("再循环率").Value = recirculationRate;
return BuildAlarms();
}
}
public void Dispose()
{
lock (_syncRoot)
{
_master?.Dispose();
_tcpClient?.Dispose();
_master = null;
_tcpClient = null;
}
}
private void EnsureConnected()
{
lock (_syncRoot)
{
if (_master is not null && _tcpClient?.Connected == true)
{
return;
}
Dispose();
try
{
_tcpClient = new TcpClient();
_tcpClient.Connect(IpAddress, Port);
_master = _factory.CreateMaster(_tcpClient);
_connectionInitialized = true;
}
catch
{
_master = null;
_tcpClient?.Dispose();
_tcpClient = null;
}
}
}
private void TryReadPressureChannels()
{
if (_master is null)
{
SimulatePressureChannels();
return;
}
try
{
var proximalRaw = _master.ReadHoldingRegisters(SlaveId, ProximalPressureRegister, 1)[0];
var distalRaw = _master.ReadHoldingRegisters(SlaveId, DistalPressureRegister, 1)[0];
Channel("近端压力").Value = ConvertRegisterToPressure(proximalRaw, Channel("近端压力"));
Channel("远端压力").Value = ConvertRegisterToPressure(distalRaw, Channel("远端压力"));
}
catch
{
Dispose();
SimulatePressureChannels();
}
}
private void SimulateAuxiliaryChannels()
{
foreach (var channel in _channels.Where(channel => channel.Name is not "近端压力" and not "远端压力"))
{ {
var offset = channel.Name switch var offset = channel.Name switch
{ {
"主泵流量" => Next(-0.08, 0.08), "主泵流量" => Next(-0.08, 0.08),
"静脉引流流量" => Next(-0.08, 0.08), "静脉引流流量" => Next(-0.08, 0.08),
"动脉回输流量" => Next(-0.06, 0.06), "动脉回输流量" => Next(-0.06, 0.06),
"入口压力" => Next(-3.0, 3.0),
"出口压力" => Next(-2.5, 2.5),
"负压辅助引流" => Next(-0.6, 0.6), "负压辅助引流" => Next(-0.6, 0.6),
"模拟血液温度" => Next(-0.15, 0.15), "模拟血液温度" => Next(-0.15, 0.15),
"游离血红蛋白" => Next(-0.003, 0.003), "游离血红蛋白" => Next(-0.003, 0.003),
@@ -44,35 +148,47 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService
channel.Value = Math.Clamp(channel.Value + offset, channel.Min, channel.Max); channel.Value = Math.Clamp(channel.Value + offset, channel.Min, channel.Max);
} }
}
var pumpFlow = Channel("主泵流量"); private void SimulatePressureChannels()
var drainageFlow = Channel("静脉引流流量"); {
var returnFlow = Channel("动脉回输流量"); var proximal = Channel("近端压力");
var distal = Channel("远端压力");
drainageFlow.Value = Math.Clamp( proximal.Value = Math.Clamp(proximal.Value + Next(-3.0, 3.0), proximal.Min, proximal.Max);
Math.Min(drainageFlow.Value, returnFlow.Value - Next(0.12, 0.48)), distal.Value = Math.Clamp(distal.Value + Next(-2.5, 2.5), distal.Min, distal.Max);
drainageFlow.Min,
drainageFlow.Max);
pumpFlow.Value = Math.Clamp(
(drainageFlow.Value + returnFlow.Value) / 2d + Next(-0.05, 0.05),
pumpFlow.Min,
pumpFlow.Max);
var recirculationRate = returnFlow.Value <= 0.01 if (distal.Value > proximal.Value - 2)
? 0 {
: Math.Clamp((returnFlow.Value - drainageFlow.Value) / returnFlow.Value * 100d, 0d, 100d); distal.Value = Math.Max(distal.Min, proximal.Value - Next(6, 18));
Channel("再循环率").Value = recirculationRate; }
}
private List<AlarmMessage> BuildAlarms()
{
var alarms = new List<AlarmMessage>(); var alarms = new List<AlarmMessage>();
var deltaPressure = Channel("近端压力").Value - Channel("远端压力").Value;
var recirculationRate = Channel("再循环率").Value;
var pumpFlow = Channel("主泵流量").Value;
var returnFlow = Channel("动脉回输流量").Value;
var deltaPressure = Channel("入口压力").Value - Channel("出口压力").Value;
if (deltaPressure > 24) if (deltaPressure > 24)
{ {
alarms.Add(new AlarmMessage alarms.Add(new AlarmMessage
{ {
Timestamp = DateTime.Now, Timestamp = DateTime.Now,
Level = "高", Level = "高",
Message = $"压 {deltaPressure:F1} mmHg,接近 4.3.1 压力降上限" Message = $"压力降 ΔP {deltaPressure:F1} mmHg 偏高,请复核近端/远端压力与流量点"
});
}
if (!_connectionInitialized || _master is null)
{
alarms.Add(new AlarmMessage
{
Timestamp = DateTime.Now,
Level = "中",
Message = $"ModbusTcp 未连接,当前使用本地模拟压力数据。目标 {IpAddress}:{Port}"
}); });
} }
@@ -86,13 +202,13 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService
}); });
} }
if (Math.Abs(pumpFlow.Value - returnFlow.Value) > 0.35) if (Math.Abs(pumpFlow - returnFlow) > 0.35)
{ {
alarms.Add(new AlarmMessage alarms.Add(new AlarmMessage
{ {
Timestamp = DateTime.Now, Timestamp = DateTime.Now,
Level = "中", Level = "中",
Message = $"主泵/回输流量差 {Math.Abs(pumpFlow.Value - returnFlow.Value):F2} L/min建议检查流量传感器标定" Message = $"主泵/回输流量差 {Math.Abs(pumpFlow - returnFlow):F2} L/min建议检查流量传感器标定"
}); });
} }
@@ -109,6 +225,12 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService
return alarms; return alarms;
} }
private static double ConvertRegisterToPressure(ushort rawValue, DeviceChannel channel)
{
var signedValue = rawValue > short.MaxValue ? rawValue - 65536 : rawValue;
return Math.Clamp(signedValue, channel.Min, channel.Max);
}
private DeviceChannel Channel(string name) => _channels.First(channel => channel.Name == name); private DeviceChannel Channel(string name) => _channels.First(channel => channel.Name == name);
private double Next(double min, double max) => min + (_random.NextDouble() * (max - min)); private double Next(double min, double max) => min + (_random.NextDouble() * (max - min));

View File

@@ -24,25 +24,25 @@ public sealed class StandardRepository : IStandardRepository
Category = "性能特征", Category = "性能特征",
Item = "压力降", Item = "压力降",
AcceptanceCriteria = "在模拟血液黏度 3.2 mPa·s ± 0.2 mPa·s、37 ℃ ± 2 ℃ 条件下,于最大标称流量的 50%、75%、100% 流量点测得的压力降应在制造商声明范围内。", AcceptanceCriteria = "在模拟血液黏度 3.2 mPa·s ± 0.2 mPa·s、37 ℃ ± 2 ℃ 条件下,于最大标称流量的 50%、75%、100% 流量点测得的压力降应在制造商声明范围内。",
TestMethod = "使用最终灭菌成品接入循环回路并排尽气泡;在模拟血液黏度 3.2 mPa·s ± 0.2 mPa·s、37 ℃ ± 2 ℃ 条件下,于 50%、75%、100% 最大标称流量分别测量入口和出口压力,计算压力降并评定。", TestMethod = "使用最终灭菌成品接入循环回路并排尽气泡;在模拟血液黏度 3.2 mPa·s ± 0.2 mPa·s、37 ℃ ± 2 ℃ 条件下,于 50%、75%、100% 最大标称流量分别测量近端和远端压力,计算压力降并评定。",
RecordFocus = "记录试验条件、流量点、入口/出口压力、压力降 ΔP 及判定结果", RecordFocus = "记录试验条件、流量点、近端/远端压力、压力降 ΔP 及判定结果",
CaptureMode = InspectionItemCaptureMode.RealtimeMonitor, CaptureMode = InspectionItemCaptureMode.RealtimeMonitor,
MeasurementSource = "主泵流量、入口/出口压力、模拟血液温度传感器", MeasurementSource = "主泵流量、近端/远端压力、模拟血液温度传感器",
ManualEntryHint = "该项目由实时压力信号自动采集,无需人工重复填写。", ManualEntryHint = "该项目由实时压力信号自动采集,无需人工重复填写。",
LiveDisplayHint = "实时显示试验温度、当前流量、50%/75%/100% 标称流量点及入口/出口压力,用于记录压力降试验。" LiveDisplayHint = "实时显示试验温度、当前流量、50%/75%/100% 标称流量点及近端/远端压力,用于记录压力降试验。"
}, },
new() new()
{ {
Clause = "4.3.2", Clause = "4.3.2",
Category = "性能特征", Category = "性能特征",
Item = "抗塌陷", Item = "抗塌陷",
AcceptanceCriteria = "负压后压降增量不超过 40%。", AcceptanceCriteria = "对引流插管,在远端施加 -6.67 kPa 负压后,由插管引起的压力降增加值不应超过基线值的 50%。",
TestMethod = "施加负压比较压差增量和流量变化;判定压降增量不超过限值。", TestMethod = "使用模拟血液2.0~3.5 mPa·s在 37 ℃ ± 2 ℃ 条件下,先于制造商规定的最大血流量测量基线压力降,再在相同流量条件下于远端施加 -6.67 kPa 负压比较压力降增加值。",
RecordFocus = "记录负压与压差变化", RecordFocus = "记录基线压力降、负压加载后压力降、增加值、增加率及比较结论",
CaptureMode = InspectionItemCaptureMode.RealtimeAssist, CaptureMode = InspectionItemCaptureMode.RealtimeAssist,
MeasurementSource = "负压辅助引流、入口/出口压差趋势", MeasurementSource = "负压辅助引流、近端/远端压力、主泵流量、模拟血液温度",
ManualEntryHint = "系统实时显示负压与压差趋势,最终结论仍由检测员根据检测记录手动填写。", ManualEntryHint = "系统自动给出基线、负压后压力降和增幅比较建议,最终结论仍由检测员确认保存。",
LiveDisplayHint = "建议结合负压、压差增量和流量变化综合判定是否塌陷。" LiveDisplayHint = "建议先采集无负压基线,再施加 -6.67 kPa 负压比较压力降增幅。"
}, },
new() new()
{ {

View File

@@ -13,6 +13,12 @@ public partial class MainViewModel : ObservableObject
{ {
private readonly IModbusTelemetryService _telemetryService; private readonly IModbusTelemetryService _telemetryService;
private readonly DispatcherTimer _timer; private readonly DispatcherTimer _timer;
private const double AntiCollapseTargetNegativePressure = -6.67;
private double? _antiCollapseBaselinePressureDrop;
private double? _antiCollapseBaselineFlow;
private DateTime? _antiCollapseBaselineCapturedAt;
private string _lastAutoAntiCollapseResult = string.Empty;
private string _lastAutoAntiCollapseNote = string.Empty;
[ObservableProperty] [ObservableProperty]
private string pageTitle = "心肺转流系统一次性使用动静脉插管检测"; private string pageTitle = "心肺转流系统一次性使用动静脉插管检测";
@@ -152,6 +158,21 @@ public partial class MainViewModel : ObservableObject
public string PressureDropConditionDisplay => $"模拟血液 3.2±0.2 mPa·s / {TemperatureDisplay} / 最终灭菌成品"; public string PressureDropConditionDisplay => $"模拟血液 3.2±0.2 mPa·s / {TemperatureDisplay} / 最终灭菌成品";
public string PressureDropFlowPointDisplay => public string PressureDropFlowPointDisplay =>
$"50%={PressureDropFlowPoint(0.50):F2} L/min / 75%={PressureDropFlowPoint(0.75):F2} L/min / 100%={PressureDropFlowPoint(1.00):F2} L/min"; $"50%={PressureDropFlowPoint(0.50):F2} L/min / 75%={PressureDropFlowPoint(0.75):F2} L/min / 100%={PressureDropFlowPoint(1.00):F2} L/min";
public bool IsAntiCollapseSelected => SelectedItem?.Clause == "4.3.2";
public bool HasAntiCollapseBaseline => _antiCollapseBaselinePressureDrop.HasValue && _antiCollapseBaselineFlow.HasValue;
public string AntiCollapseBaselineDisplay => HasAntiCollapseBaseline
? $"基线 ΔP {_antiCollapseBaselinePressureDrop:F1} mmHg / 流量 {_antiCollapseBaselineFlow:F2} L/min / 时间 {_antiCollapseBaselineCapturedAt:HH:mm:ss}"
: "尚未采集基线";
public string AntiCollapseComparisonDisplay
{
get
{
var comparison = GetAntiCollapseComparison();
return comparison.HasBaseline
? $"当前 ΔP {DeltaPressure:F1} mmHg较基线增加 {comparison.Increase:F1} mmHg ({comparison.IncreaseRate:F1}%){comparison.StatusText}"
: "请先在无负压条件下采集基线,再进行负压比较";
}
}
public double PumpFlow => ChannelValue("主泵流量"); public double PumpFlow => ChannelValue("主泵流量");
public double DrainageFlow => ChannelValue("静脉引流流量"); public double DrainageFlow => ChannelValue("静脉引流流量");
@@ -185,6 +206,10 @@ public partial class MainViewModel : ObservableObject
OnPropertyChanged(nameof(SelectedItemCaptureModeText)); OnPropertyChanged(nameof(SelectedItemCaptureModeText));
OnPropertyChanged(nameof(SelectedItemMeasurementSource)); OnPropertyChanged(nameof(SelectedItemMeasurementSource));
OnPropertyChanged(nameof(SelectedItemUsesRealtimeValue)); OnPropertyChanged(nameof(SelectedItemUsesRealtimeValue));
OnPropertyChanged(nameof(IsAntiCollapseSelected));
OnPropertyChanged(nameof(HasAntiCollapseBaseline));
OnPropertyChanged(nameof(AntiCollapseBaselineDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
OnPropertyChanged(nameof(RealtimeMeasurementHint)); OnPropertyChanged(nameof(RealtimeMeasurementHint));
OnPropertyChanged(nameof(SelectedItemLiveDisplay)); OnPropertyChanged(nameof(SelectedItemLiveDisplay));
OnPropertyChanged(nameof(SelectedItemLiveHint)); OnPropertyChanged(nameof(SelectedItemLiveHint));
@@ -211,6 +236,65 @@ public partial class MainViewModel : ObservableObject
ItemSearchText = string.Empty; ItemSearchText = string.Empty;
} }
[RelayCommand]
private void CaptureAntiCollapseBaseline()
{
if (!IsAntiCollapseSelected)
{
LatestAction = "当前选择的不是抗塌陷项目。";
return;
}
_antiCollapseBaselinePressureDrop = DeltaPressure;
_antiCollapseBaselineFlow = PumpFlow;
_antiCollapseBaselineCapturedAt = DateTime.Now;
_lastAutoAntiCollapseResult = string.Empty;
_lastAutoAntiCollapseNote = string.Empty;
var baselineText = BuildAntiCollapseMeasuredText();
var noteText = BuildAntiCollapseRecordNote();
ResultValue = baselineText;
ResultNote = noteText;
LatestAction = $"已采集抗塌陷基线ΔP {DeltaPressure:F1} mmHg流量 {PumpFlow:F2} L/min。";
TraceEvents.Insert(0, NewTrace("抗塌陷基线", $"ΔP {DeltaPressure:F1} mmHg / 流量 {PumpFlow:F2} L/min"));
OnPropertyChanged(nameof(HasAntiCollapseBaseline));
OnPropertyChanged(nameof(AntiCollapseBaselineDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
}
[RelayCommand]
private void CaptureAntiCollapseComparison()
{
if (!IsAntiCollapseSelected)
{
LatestAction = "当前选择的不是抗塌陷项目。";
return;
}
if (!HasAntiCollapseBaseline)
{
LatestAction = "请先采集抗塌陷基线。";
return;
}
var resultText = BuildAntiCollapseMeasuredText();
var noteText = BuildAntiCollapseRecordNote();
var comparison = GetAntiCollapseComparison();
ResultValue = resultText;
ResultNote = noteText;
SelectedResultStatusText = comparison.StatusText.StartsWith("合格", StringComparison.Ordinal) ? "合格"
: comparison.StatusText.StartsWith("预警", StringComparison.Ordinal) ? "预警"
: "不合格";
_lastAutoAntiCollapseResult = resultText;
_lastAutoAntiCollapseNote = noteText;
LatestAction = $"已采集抗塌陷负压比较:增幅 {comparison.IncreaseRate:F1}% {comparison.StatusText}。";
TraceEvents.Insert(0, NewTrace("抗塌陷比较", $"增量 {comparison.Increase:F1} mmHg / 增幅 {comparison.IncreaseRate:F1}%"));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
}
[RelayCommand] [RelayCommand]
private void ToggleAcquisition() private void ToggleAcquisition()
{ {
@@ -387,9 +471,9 @@ public partial class MainViewModel : ObservableObject
private void RefreshTelemetryPanel() private void RefreshTelemetryPanel()
{ {
var inletPressure = Channels.First(c => c.Name == "入口压力").Value; var distalPressure = Channels.First(c => c.Name == "远端压力").Value;
var outletPressure = Channels.First(c => c.Name == "出口压力").Value; var proximalPressure = Channels.First(c => c.Name == "近端压力").Value;
DeltaPressure = inletPressure - outletPressure; DeltaPressure = proximalPressure - distalPressure;
OnPropertyChanged(nameof(PumpFlow)); OnPropertyChanged(nameof(PumpFlow));
OnPropertyChanged(nameof(DrainageFlow)); OnPropertyChanged(nameof(DrainageFlow));
OnPropertyChanged(nameof(ReturnFlow)); OnPropertyChanged(nameof(ReturnFlow));
@@ -405,6 +489,7 @@ public partial class MainViewModel : ObservableObject
OnPropertyChanged(nameof(WhiteCellLossDisplay)); OnPropertyChanged(nameof(WhiteCellLossDisplay));
OnPropertyChanged(nameof(PressureDropConditionDisplay)); OnPropertyChanged(nameof(PressureDropConditionDisplay));
OnPropertyChanged(nameof(PressureDropFlowPointDisplay)); OnPropertyChanged(nameof(PressureDropFlowPointDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
OnPropertyChanged(nameof(PumpFlowLoadDisplay)); OnPropertyChanged(nameof(PumpFlowLoadDisplay));
OnPropertyChanged(nameof(DrainageFlowLoadDisplay)); OnPropertyChanged(nameof(DrainageFlowLoadDisplay));
OnPropertyChanged(nameof(ReturnFlowLoadDisplay)); OnPropertyChanged(nameof(ReturnFlowLoadDisplay));
@@ -489,6 +574,31 @@ public partial class MainViewModel : ObservableObject
pressureItem.Status = pressureStatus; pressureItem.Status = pressureStatus;
} }
var antiCollapseItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.2");
if (antiCollapseItem is not null)
{
UpdateAntiCollapseBaseline();
if (SelectedItem == antiCollapseItem)
{
var suggestedResult = BuildAntiCollapseMeasuredText();
var suggestedNote = BuildAntiCollapseRecordNote();
if (string.IsNullOrWhiteSpace(ResultValue) || ResultValue == _lastAutoAntiCollapseResult)
{
ResultValue = suggestedResult;
}
if (string.IsNullOrWhiteSpace(ResultNote) || ResultNote == _lastAutoAntiCollapseNote)
{
ResultNote = suggestedNote;
}
_lastAutoAntiCollapseResult = suggestedResult;
_lastAutoAntiCollapseNote = suggestedNote;
}
}
var recirculationItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.3"); var recirculationItem = InspectionItems.FirstOrDefault(item => item.Clause == "4.3.3");
if (recirculationItem is null) if (recirculationItem is null)
{ {
@@ -601,7 +711,7 @@ public partial class MainViewModel : ObservableObject
return (SelectedItem.Clause, SelectedItem.Item) switch return (SelectedItem.Clause, SelectedItem.Item) switch
{ {
("4.3.1", _) => BuildPressureDropLiveDisplay(), ("4.3.1", _) => BuildPressureDropLiveDisplay(),
("4.3.2", _) => $"负压 {ChannelValue(""):F1} kPa / 压差 {DeltaPressure:F1} mmHg / 流量差 {Math.Abs(PumpFlow - ReturnFlow):F2} L/min", ("4.3.2", _) => BuildAntiCollapseLiveDisplay(),
("4.3.3", _) => $"再循环率 {RecirculationRate:F1}% / 引流 {DrainageFlow:F2} / 回输 {ReturnFlow:F2} L/min", ("4.3.3", _) => $"再循环率 {RecirculationRate:F1}% / 引流 {DrainageFlow:F2} / 回输 {ReturnFlow:F2} L/min",
("4.3.4", "血细胞破坏") => $"游离血红蛋白 {ChannelValue(""):F3} g/L / 温度 {ChannelValue(""):F1} °C", ("4.3.4", "血细胞破坏") => $"游离血红蛋白 {ChannelValue(""):F3} g/L / 温度 {ChannelValue(""):F1} °C",
("4.3.4", "血小板/白细胞减少率") => $"白细胞减少率 {ChannelValue(""):F1}% / 温度 {ChannelValue(""):F1} °C", ("4.3.4", "血小板/白细胞减少率") => $"白细胞减少率 {ChannelValue(""):F1}% / 温度 {ChannelValue(""):F1} °C",
@@ -611,13 +721,13 @@ public partial class MainViewModel : ObservableObject
private string BuildPressureDropMeasuredText() private string BuildPressureDropMeasuredText()
{ {
var inletPressure = ChannelValue("入口压力"); var distalPressure = ChannelValue("远端压力");
var outletPressure = ChannelValue("出口压力"); var proximalPressure = ChannelValue("近端压力");
return return
$"试验条件:{PressureDropConditionDisplay}\n" + $"试验条件:{PressureDropConditionDisplay}\n" +
$"流量点:{PressureDropFlowPointDisplay};当前 {PumpFlow:F2} L/min\n" + $"流量点:{PressureDropFlowPointDisplay};当前 {PumpFlow:F2} L/min\n" +
$"压力测量:入口 {inletPressure:F1} mmHg出口 {outletPressure:F1} mmHg\n" + $"压力测量:近端 {proximalPressure:F1} mmHg远端 {distalPressure:F1} mmHg\n" +
$"压力降 ΔP{DeltaPressure:F1} mmHg"; $"压力降 ΔP{DeltaPressure:F1} mmHg";
} }
@@ -641,9 +751,99 @@ public partial class MainViewModel : ObservableObject
return return
$"条件:{PressureDropConditionDisplay}\n" + $"条件:{PressureDropConditionDisplay}\n" +
$"流量点:{PressureDropFlowPointDisplay}\n" + $"流量点:{PressureDropFlowPointDisplay}\n" +
$"当前:主泵 {PumpFlow:F2} L/min / 入口 {ChannelValue(""):F1} mmHg / 出口 {ChannelValue(""):F1} mmHg / ΔP {DeltaPressure:F1} mmHg"; $"当前:主泵 {PumpFlow:F2} L/min / 近端 {ChannelValue(""):F1} mmHg / 远端 {ChannelValue(""):F1} mmHg / ΔP {DeltaPressure:F1} mmHg";
}
private void UpdateAntiCollapseBaseline()
{
var negativePressure = ChannelValue("负压辅助引流");
if (_antiCollapseBaselinePressureDrop is null || negativePressure >= -1.0)
{
_antiCollapseBaselinePressureDrop = DeltaPressure;
_antiCollapseBaselineFlow = PumpFlow;
_antiCollapseBaselineCapturedAt = DateTime.Now;
OnPropertyChanged(nameof(HasAntiCollapseBaseline));
OnPropertyChanged(nameof(AntiCollapseBaselineDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
}
}
private string BuildAntiCollapseLiveDisplay()
{
var comparison = GetAntiCollapseComparison();
var baselineText = comparison.HasBaseline
? $"基线 ΔP {comparison.BaselinePressureDrop:F1} mmHg @ {comparison.BaselineFlow:F2} L/min"
: "基线待采集(请先在无负压条件下运行)";
var increaseText = comparison.HasBaseline
? $"增量 {comparison.Increase:F1} mmHg / 增幅 {comparison.IncreaseRate:F1}% / 判定 {comparison.StatusText}"
: "尚无法比较增幅";
return
$"条件:模拟血液 2.0~3.5 mPa·s / {TemperatureDisplay} / 目标负压 {AntiCollapseTargetNegativePressure:F2} kPa\n" +
$"当前:负压 {NegativeAssistPressureDisplay} / 主泵 {PumpFlow:F2} L/min / ΔP {DeltaPressure:F1} mmHg\n" +
$"{baselineText}\n" +
$"{increaseText}";
}
private string BuildAntiCollapseMeasuredText()
{
var comparison = GetAntiCollapseComparison();
var baselineText = comparison.HasBaseline
? $"{comparison.BaselinePressureDrop:F1} mmHg"
: "未采集";
var increaseText = comparison.HasBaseline
? $"{comparison.Increase:F1} mmHg ({comparison.IncreaseRate:F1}%)"
: "无法比较";
return
$"试验条件:模拟血液 2.0~3.5 mPa·s{TemperatureDisplay},有效长度处于 37±2 ℃ 条件\n" +
$"基线测量:最大血流量 {comparison.BaselineFlow:F2} L/min压力降 {baselineText}\n" +
$"负压加载:远端施加 {NegativeAssistPressureDisplay}(目标 {AntiCollapseTargetNegativePressure:F2} kPa\n" +
$"比较结果:当前压力降 {DeltaPressure:F1} mmHg较基线增加 {increaseText}\n" +
$"建议判定:{comparison.StatusText}";
}
private string BuildAntiCollapseRecordNote()
{
var comparison = GetAntiCollapseComparison();
var baselineCapturedText = _antiCollapseBaselineCapturedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "未采集";
return
$"录入建议:先在无负压条件下采集基线压力降,再在远端施加 -6.67 kPa 负压后记录比较值。\n" +
$"当前基线时间:{baselineCapturedText};基线流量 {comparison.BaselineFlow:F2} L/min当前流量 {PumpFlow:F2} L/min。\n" +
$"判定规则:负压后压力降增幅不超过基线 50% 为合格;当前比较结果为 {comparison.StatusText}。";
}
private AntiCollapseComparison GetAntiCollapseComparison()
{
if (_antiCollapseBaselinePressureDrop is null || _antiCollapseBaselineFlow is null)
{
return new AntiCollapseComparison(false, 0, 0, 0, 0, "等待基线");
}
var baseline = _antiCollapseBaselinePressureDrop.Value;
var increase = DeltaPressure - baseline;
var increaseRate = baseline <= 0 ? 0 : increase / baseline * 100d;
var statusText = increaseRate switch
{
<= 50 => "合格",
<= 60 => "预警,建议复测",
_ => "不合格,压降增幅超限"
};
return new AntiCollapseComparison(true, baseline, _antiCollapseBaselineFlow.Value, increase, increaseRate, statusText);
} }
private double PressureDropFlowPoint(double ratio) => private double PressureDropFlowPoint(double ratio) =>
Channels.First(channel => channel.Name == "主泵流量").Max * ratio; Channels.First(channel => channel.Name == "主泵流量").Max * ratio;
private readonly record struct AntiCollapseComparison(
bool HasBaseline,
double BaselinePressureDrop,
double BaselineFlow,
double Increase,
double IncreaseRate,
string StatusText);
} }