This commit is contained in:
GukSang.Jin
2026-04-03 09:24:45 +08:00
parent 12275fad0f
commit fc79a49458
9 changed files with 740 additions and 2 deletions

View File

@@ -0,0 +1,97 @@
<Window x:Class="Cardiopulmonarybypasssystems.EngineeringRegistersWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="工程寄存器"
Width="920"
Height="640"
MinWidth="860"
MinHeight="560"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize"
ShowInTaskbar="False">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Padding="14" CornerRadius="16" Background="{StaticResource HeroBrush}">
<DockPanel LastChildFill="False">
<StackPanel DockPanel.Dock="Left">
<Border Width="90"
Height="24"
Background="Transparent"
HorizontalAlignment="Left"
MouseLeftButtonDown="CloseHotspot_OnMouseLeftButtonDown"
MouseLeftButtonUp="CloseHotspot_OnMouseLeftButtonUp"
MouseLeave="CloseHotspot_OnMouseLeave" />
<TextBlock FontSize="20" FontWeight="Bold" Foreground="White" Text="工程寄存器" />
<TextBlock Margin="0,6,0,0"
FontSize="12"
Foreground="#EFFAFC"
Text="长按左上角空白区可关闭弹窗。D1330 / D1380 为只读显示值;其余地址支持读写,写入后自动回读并刷新实时值。"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<Button Padding="12,6"
Command="{Binding RefreshEngineeringRegistersCommand}"
Content="刷新寄存器"
Background="#FFFFFFFF"
Foreground="{StaticResource HeaderBrush}" />
</StackPanel>
</DockPanel>
</Border>
<Border Grid.Row="2" Style="{StaticResource CardBorderStyle}">
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Top"
Margin="0,0,0,10"
FontSize="13"
FontWeight="SemiBold"
Text="{Binding EngineeringRegisterStatus}"
TextWrapping="Wrap" />
<DataGrid ItemsSource="{Binding EngineeringRegisters}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
HeadersVisibility="Column"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridTextColumn Header="名称" Binding="{Binding Name}" IsReadOnly="True" Width="180" />
<DataGridTextColumn Header="地址" Binding="{Binding AddressDisplay}" IsReadOnly="True" Width="80" />
<DataGridTextColumn Header="权限" Binding="{Binding AccessDisplay}" IsReadOnly="True" Width="80" />
<DataGridTemplateColumn Header="当前值 / 写入值" Width="200">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBox Text="{Binding PendingValue, UpdateSourceTrigger=PropertyChanged}"
Visibility="{Binding EditVisibility}" />
<TextBlock VerticalAlignment="Center"
Text="{Binding CurrentValue}"
Visibility="{Binding ReadOnlyVisibility}" />
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="PLC 当前值" Binding="{Binding CurrentValue}" IsReadOnly="True" Width="140" />
<DataGridTextColumn Header="状态" Binding="{Binding StatusText}" IsReadOnly="True" Width="120" />
<DataGridTemplateColumn Header="操作" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Padding="10,4"
Command="{Binding DataContext.WriteEngineeringRegisterCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Content="写入"
Visibility="{Binding EditVisibility}"
Background="#FF4D8C72" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,52 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Cardiopulmonarybypasssystems.ViewModels;
namespace Cardiopulmonarybypasssystems;
public partial class EngineeringRegistersWindow : Window
{
private readonly DispatcherTimer _closeHoldTimer = new() { Interval = TimeSpan.FromMilliseconds(900) };
public EngineeringRegistersWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
Loaded += OnLoaded;
_closeHoldTimer.Tick += CloseHoldTimer_OnTick;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (DataContext is MainViewModel viewModel
&& viewModel.RefreshEngineeringRegistersCommand.CanExecute(null))
{
viewModel.RefreshEngineeringRegistersCommand.Execute(null);
}
}
private void CloseHotspot_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_closeHoldTimer.Stop();
_closeHoldTimer.Start();
}
private void CloseHotspot_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) => _closeHoldTimer.Stop();
private void CloseHotspot_OnMouseLeave(object sender, MouseEventArgs e) => _closeHoldTimer.Stop();
private void CloseHoldTimer_OnTick(object? sender, EventArgs e)
{
_closeHoldTimer.Stop();
Close();
}
protected override void OnClosed(EventArgs e)
{
_closeHoldTimer.Stop();
_closeHoldTimer.Tick -= CloseHoldTimer_OnTick;
base.OnClosed(e);
}
}

View File

@@ -21,7 +21,14 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Grid.Row="0" Padding="12,10" CornerRadius="18" Background="{StaticResource HeroBrush}">
<Border Grid.Row="0"
Padding="12,10"
CornerRadius="18"
Background="{StaticResource HeroBrush}"
PreviewMouseLeftButtonDown="HeroHeader_OnPreviewMouseLeftButtonDown"
PreviewTouchDown="HeroHeader_OnPreviewTouchDown"
PreviewTouchUp="HeroHeader_OnPreviewTouchUp"
TouchLeave="HeroHeader_OnTouchLeave">
<Border.Resources>
<Style x:Key="HeroCompactPillStyle" TargetType="Border" BasedOn="{StaticResource PillBorderStyle}">
<Setter Property="Padding" Value="10,5" />
@@ -109,7 +116,7 @@
<Grid Grid.Row="2">
<Border Style="{StaticResource CardBorderStyle}" Margin="0">
<TabControl>
<TabControl x:Name="RootTabControl">
<TabItem Header="项目检测">
<ScrollViewer Margin="0,6,0,0" VerticalScrollBarVisibility="Auto" CanContentScroll="False">
<StackPanel>
@@ -1416,6 +1423,84 @@
</ScrollViewer>
</TabItem>
<TabItem x:Name="EngineeringRegistersTab"
Header="工程寄存器"
Visibility="{Binding EngineeringRegisterPanelVisibility}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="0,6,0,0">
<Border Style="{StaticResource CardBorderStyle}">
<StackPanel>
<DockPanel LastChildFill="False">
<TextBlock DockPanel.Dock="Left" Style="{StaticResource SectionTitleStyle}" Text="寄存器读写" />
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Button Margin="0,0,8,0"
Padding="12,6"
Command="{Binding RefreshEngineeringRegistersCommand}"
Content="刷新寄存器"
Background="#FF4D8C72" />
<Button Padding="12,6"
Command="{Binding ToggleEngineeringRegisterPanelCommand}"
Content="隐藏界面"
Background="#FF6B8791" />
</StackPanel>
</DockPanel>
<TextBlock Margin="0,8,0,0"
Style="{StaticResource CaptionStyle}"
Text="双击右上角可显示/隐藏此界面。D1330、D1380 为只读显示值;其余地址支持读写,写入后会立即回读刷新。"
TextWrapping="Wrap" />
<TextBlock Margin="0,6,0,0"
FontSize="13"
FontWeight="SemiBold"
Text="{Binding EngineeringRegisterStatus}"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<Border Style="{StaticResource CardBorderStyle}">
<DataGrid ItemsSource="{Binding EngineeringRegisters}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
HeadersVisibility="Column"
MinHeight="420">
<DataGrid.Columns>
<DataGridTextColumn Header="名称" Binding="{Binding Name}" IsReadOnly="True" Width="180" />
<DataGridTextColumn Header="地址" Binding="{Binding AddressDisplay}" IsReadOnly="True" Width="80" />
<DataGridTextColumn Header="权限" Binding="{Binding AccessDisplay}" IsReadOnly="True" Width="70" />
<DataGridTextColumn Header="当前值" Binding="{Binding CurrentValue}" IsReadOnly="True" Width="120" />
<DataGridTemplateColumn Header="写入值" Width="120">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBox Text="{Binding PendingValue, UpdateSourceTrigger=PropertyChanged}"
Visibility="{Binding EditVisibility}" />
<TextBlock VerticalAlignment="Center"
Text="只读"
Visibility="{Binding ReadOnlyVisibility}" />
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="状态" Binding="{Binding StatusText}" IsReadOnly="True" Width="120" />
<DataGridTemplateColumn Header="操作" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Padding="10,4"
Command="{Binding DataContext.WriteEngineeringRegisterCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Content="写入"
Visibility="{Binding EditVisibility}"
Background="#FF4D8C72" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="追溯">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="0,6,0,0">

View File

@@ -1,7 +1,10 @@
using System.Globalization;
using System.Windows.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Cardiopulmonarybypasssystems.ViewModels;
@@ -9,11 +12,18 @@ namespace Cardiopulmonarybypasssystems;
public partial class MainWindow : Window
{
private const double EngineeringHotspotWidth = 220d;
private const double EngineeringHotspotHeight = 72d;
private EngineeringRegistersWindow? _engineeringRegistersWindow;
private readonly DispatcherTimer _engineeringTouchHoldTimer = new() { Interval = TimeSpan.FromMilliseconds(700) };
private DateTime _lastEngineeringTouchDownUtc = DateTime.MinValue;
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
ConfigureTrendBindings();
_engineeringTouchHoldTimer.Tick += EngineeringTouchHoldTimer_OnTick;
}
private void ConfigureTrendBindings()
@@ -48,4 +58,104 @@ public partial class MainWindow : Window
BindingOperations.SetBinding(polyline, Polyline.PointsProperty, binding);
}
private void HeroHeader_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount != 2 || DataContext is not MainViewModel viewModel)
{
return;
}
if (!IsWithinEngineeringHotspot(sender as IInputElement, e.GetPosition(sender as IInputElement)))
{
return;
}
ToggleEngineeringWindow(viewModel);
e.Handled = true;
}
private void HeroHeader_OnPreviewTouchDown(object sender, TouchEventArgs e)
{
if (DataContext is not MainViewModel viewModel)
{
return;
}
var source = sender as IInputElement;
var position = e.GetTouchPoint(source).Position;
if (!IsWithinEngineeringHotspot(source, position))
{
_engineeringTouchHoldTimer.Stop();
return;
}
var now = DateTime.UtcNow;
if ((now - _lastEngineeringTouchDownUtc).TotalMilliseconds <= 500)
{
_engineeringTouchHoldTimer.Stop();
ToggleEngineeringWindow(viewModel);
_lastEngineeringTouchDownUtc = DateTime.MinValue;
e.Handled = true;
return;
}
_lastEngineeringTouchDownUtc = now;
_engineeringTouchHoldTimer.Stop();
_engineeringTouchHoldTimer.Start();
e.Handled = true;
}
private void HeroHeader_OnPreviewTouchUp(object sender, TouchEventArgs e)
{
_engineeringTouchHoldTimer.Stop();
e.Handled = true;
}
private void HeroHeader_OnTouchLeave(object sender, TouchEventArgs e)
{
_engineeringTouchHoldTimer.Stop();
e.Handled = true;
}
private void EngineeringTouchHoldTimer_OnTick(object? sender, EventArgs e)
{
_engineeringTouchHoldTimer.Stop();
if (DataContext is MainViewModel viewModel)
{
ToggleEngineeringWindow(viewModel);
}
}
private void ToggleEngineeringWindow(MainViewModel viewModel)
{
if (_engineeringRegistersWindow is not null)
{
_engineeringRegistersWindow.Close();
_engineeringRegistersWindow = null;
return;
}
if (viewModel.RefreshEngineeringRegistersCommand.CanExecute(null))
{
viewModel.RefreshEngineeringRegistersCommand.Execute(null);
}
_engineeringRegistersWindow = new EngineeringRegistersWindow(viewModel)
{
Owner = this
};
_engineeringRegistersWindow.Closed += (_, _) => _engineeringRegistersWindow = null;
_engineeringRegistersWindow.Show();
}
private static bool IsWithinEngineeringHotspot(IInputElement? source, Point position)
{
_ = source;
return position.X >= 0d
&& position.Y >= 0d
&& position.X <= EngineeringHotspotWidth
&& position.Y <= EngineeringHotspotHeight;
}
}

View File

@@ -0,0 +1,29 @@
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Cardiopulmonarybypasssystems.Models;
public partial class EngineeringRegisterItem : ObservableObject
{
public required string Name { get; init; }
public required ushort Address { get; init; }
public required bool IsWritable { get; init; }
public bool UsesFloatDisplay { get; init; }
public bool UsesFloatRegister { get; init; }
public string ValueSuffix { get; init; } = string.Empty;
public string DisplayFormat { get; init; } = "F2";
public string AddressDisplay => $"D{Address}";
public string AccessDisplay => IsWritable ? "读/写" : "只读";
public bool IsFloatRegisterAddress => UsesFloatRegister || UsesFloatDisplay || Address is 1006 or 1016 or 1026 or 1036 or 1046 or 1056 or 1066 or 1076 or 1328 or 1378 or 1330 or 1380;
public Visibility EditVisibility => IsWritable ? Visibility.Visible : Visibility.Collapsed;
public Visibility ReadOnlyVisibility => IsWritable ? Visibility.Collapsed : Visibility.Visible;
[ObservableProperty]
private string currentValue = "--";
[ObservableProperty]
private string pendingValue = string.Empty;
[ObservableProperty]
private string statusText = string.Empty;
}

View File

@@ -12,6 +12,10 @@ public interface IModbusTelemetryService
IReadOnlyList<PumpControlChannel> GetPumpControls();
IReadOnlyList<ValveControlChannel> GetValveControls();
TelemetryUpdateSnapshot UpdateChannels();
ushort? ReadHoldingRegister(ushort address);
float? ReadHoldingFloatRegister(ushort address);
bool WriteHoldingRegister(ushort address, ushort value);
bool WriteHoldingFloatRegister(ushort address, float value);
void SetPumpRunning(string pumpKey, bool isRunning);
void SetValveOpen(string valveKey, bool isOpen);
}

View File

@@ -48,6 +48,19 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
private readonly Random _random = new();
private readonly ModbusFactory _factory = new();
private readonly Dictionary<string, Queue<double>> _channelWindows = new(StringComparer.Ordinal);
private readonly Dictionary<ushort, float> _engineeringFloatRegisters = new()
{
[1006] = 1.0f,
[1016] = 1.0f,
[1026] = 1.0f,
[1036] = 1.0f,
[1046] = 1.0f,
[1056] = 1.0f,
[1066] = 1.0f,
[1076] = 1.0f,
[1328] = 1.0f,
[1378] = 1.0f
};
private readonly List<DeviceChannel> _channels =
[
new() { Name = "主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
@@ -145,6 +158,61 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
}
}
public ushort? ReadHoldingRegister(ushort address)
{
lock (_syncRoot)
{
if (!_engineeringFloatRegisters.TryGetValue(address, out var value))
{
return null;
}
return (ushort)Math.Clamp((int)Math.Round(value), ushort.MinValue, ushort.MaxValue);
}
}
public float? ReadHoldingFloatRegister(ushort address)
{
lock (_syncRoot)
{
return address switch
{
ProximalPressureRegister => (float)(_proximalPressureRawKpa ?? (_channels.First(channel => channel.Name == "近端压力").Value / KpaToMmHg)),
DistalPressureRegister => (float)(_distalPressureRawKpa ?? (_channels.First(channel => channel.Name == "远端压力").Value / KpaToMmHg)),
_ when _engineeringFloatRegisters.TryGetValue(address, out var value) => value,
_ => null
};
}
}
public bool WriteHoldingRegister(ushort address, ushort value)
{
lock (_syncRoot)
{
if (!_engineeringFloatRegisters.ContainsKey(address))
{
return false;
}
_engineeringFloatRegisters[address] = value;
return true;
}
}
public bool WriteHoldingFloatRegister(ushort address, float value)
{
lock (_syncRoot)
{
if (!_engineeringFloatRegisters.ContainsKey(address))
{
return false;
}
_engineeringFloatRegisters[address] = value;
return true;
}
}
public void SetPumpRunning(string pumpKey, bool isRunning)
{
lock (_syncRoot)

View File

@@ -182,6 +182,106 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
}
}
public ushort? ReadHoldingRegister(ushort address)
{
EnsureConnectionReadyForDirectAccess();
lock (_syncRoot)
{
if (_master is null)
{
return null;
}
try
{
return _master.ReadHoldingRegisters(_slaveId, address, 1)[0];
}
catch (Exception ex)
{
HandleConnectionFailure($"读取寄存器 D{address} 失败:{ex.Message}");
return null;
}
}
}
public float? ReadHoldingFloatRegister(ushort address)
{
EnsureConnectionReadyForDirectAccess();
lock (_syncRoot)
{
if (_master is null)
{
return null;
}
try
{
var registers = _master.ReadHoldingRegisters(_slaveId, address, 2);
return address is ProximalPressureRegister or DistalPressureRegister
? (float)ConvertRegistersToPressureKpa(registers[0], registers[1])
: DecodeFloat(registers[0], registers[1], lowWordFirst: true);
}
catch (Exception ex)
{
HandleConnectionFailure($"读取浮点寄存器 D{address} 失败:{ex.Message}");
return null;
}
}
}
public bool WriteHoldingRegister(ushort address, ushort value)
{
EnsureConnectionReadyForDirectAccess();
lock (_syncRoot)
{
if (_master is null)
{
_lastErrorMessage = $"PLC 离线未执行寄存器写入D{address}";
return false;
}
try
{
_master.WriteSingleRegister(_slaveId, address, value);
return true;
}
catch (Exception ex)
{
HandleConnectionFailure($"写入寄存器 D{address} 失败:{ex.Message}");
return false;
}
}
}
public bool WriteHoldingFloatRegister(ushort address, float value)
{
EnsureConnectionReadyForDirectAccess();
lock (_syncRoot)
{
if (_master is null)
{
_lastErrorMessage = $"PLC 离线未执行浮点寄存器写入D{address}";
return false;
}
try
{
var registers = EncodeFloat(value, lowWordFirst: true);
_master.WriteMultipleRegisters(_slaveId, address, registers);
return true;
}
catch (Exception ex)
{
HandleConnectionFailure($"写入浮点寄存器 D{address} 失败:{ex.Message}");
return false;
}
}
}
public void SetPumpRunning(string pumpKey, bool isRunning)
{
lock (_syncRoot)
@@ -272,6 +372,36 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
}
}
private void EnsureConnectionReadyForDirectAccess()
{
EnsureConnectionScheduled();
Task? connectionTask;
lock (_syncRoot)
{
if (_master is not null && _tcpClient?.Connected == true)
{
return;
}
connectionTask = _connectionTask;
}
if (connectionTask is null)
{
return;
}
try
{
connectionTask.Wait(ConnectionAttemptTimeout + TimeSpan.FromMilliseconds(300));
}
catch
{
// Read/write callers handle the offline state after the wait.
}
}
private void ConnectWithTimeout()
{
TcpClient? tcpClient = null;
@@ -623,6 +753,16 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
return BitConverter.Int32BitsToSingle(unchecked((int)bits));
}
private static ushort[] EncodeFloat(float value, bool lowWordFirst)
{
var bits = unchecked((uint)BitConverter.SingleToInt32Bits(value));
var lowWord = (ushort)(bits & 0xFFFF);
var highWord = (ushort)((bits >> 16) & 0xFFFF);
return lowWordFirst
? [lowWord, highWord]
: [highWord, lowWord];
}
private static bool IsPlausiblePressureKpa(float value) =>
!float.IsNaN(value) && !float.IsInfinity(value) && value is > -1000f and < 1000f;

View File

@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
@@ -46,6 +47,12 @@ public partial class MainViewModel : ObservableObject, IDisposable
private double? _proximalPressureRawKpa;
private double? _distalPressureRawKpa;
[ObservableProperty]
private bool engineeringRegisterPanelVisible;
[ObservableProperty]
private string engineeringRegisterStatus = "双击右上角显示工程寄存器界面";
[ObservableProperty]
private string pageTitle = "心肺转流系统一次性使用动静脉插管检测";
@@ -161,6 +168,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
TraceEvents = new ObservableCollection<TraceEvent>(repository.GetInitialTraceEvents());
AlarmMessages = new ObservableCollection<AlarmMessage>();
ResultStatusOptions = new ObservableCollection<string>(["待检", "合格", "预警", "不合格"]);
EngineeringRegisters = BuildEngineeringRegisters();
PressureDropEntries = new ObservableCollection<PressureDropPointEntry>(
[
new() { Label = "50%", TargetFlow = 3.0 },
@@ -249,6 +257,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
public ObservableCollection<TraceEvent> TraceEvents { get; }
public ObservableCollection<AlarmMessage> AlarmMessages { get; }
public ObservableCollection<string> ResultStatusOptions { get; }
public ObservableCollection<EngineeringRegisterItem> EngineeringRegisters { get; }
public ObservableCollection<PressureDropPointEntry> PressureDropEntries { get; }
public ObservableCollection<KinkResistancePointEntry> KinkResistanceEntries { get; }
public ObservableCollection<RecirculationPointEntry> RecirculationEntries { get; }
@@ -270,6 +279,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
public ObservableCollection<string> HemolysisAnticoagulantOptions { get; } = new(["肝素", "枸橼酸钠", "其他"]);
public bool HasFilteredItems => !FilteredItemsView.IsEmpty;
public bool HasSelectedItem => SelectedItem is not null;
public Visibility EngineeringRegisterPanelVisibility => EngineeringRegisterPanelVisible ? Visibility.Visible : Visibility.Collapsed;
public IEnumerable<DeviceChannel> FlowSensorChannels => Channels.Where(IsFlowSensorChannel);
public IEnumerable<DeviceChannel> OtherChannels => Channels.Where(channel => !IsFlowSensorChannel(channel));
public bool IsTelemetryOnline => _isTelemetryOnline;
@@ -480,6 +490,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
partial void OnPressureDropLimit100Changed(double value) => UpdateAndPersistLimitSettings();
partial void OnAntiCollapseAllowedIncreaseRateChanged(double value) => UpdateAndPersistLimitSettings();
partial void OnRecirculationAllowedLimitChanged(double value) => UpdateAndPersistLimitSettings();
partial void OnEngineeringRegisterPanelVisibleChanged(bool value) => OnPropertyChanged(nameof(EngineeringRegisterPanelVisibility));
partial void OnItemSearchTextChanged(string value)
{
RefreshFilteredItemsView();
@@ -659,6 +670,93 @@ public partial class MainViewModel : ObservableObject, IDisposable
RaiseTrendPropertyChanges();
}
[RelayCommand]
private void ToggleEngineeringRegisterPanel()
{
EngineeringRegisterPanelVisible = !EngineeringRegisterPanelVisible;
EngineeringRegisterStatus = EngineeringRegisterPanelVisible
? "工程寄存器界面已显示"
: "工程寄存器界面已隐藏";
if (EngineeringRegisterPanelVisible)
{
RefreshEngineeringRegisters();
}
}
[RelayCommand]
private void RefreshEngineeringRegisters()
{
var refreshedCount = 0;
foreach (var item in EngineeringRegisters)
{
if (RefreshEngineeringRegisterItem(item))
{
refreshedCount++;
}
}
EngineeringRegisterStatus = refreshedCount == EngineeringRegisters.Count
? $"已刷新 {refreshedCount} 个寄存器地址"
: $"已刷新 {refreshedCount}/{EngineeringRegisters.Count} 个寄存器地址";
}
[RelayCommand]
private void WriteEngineeringRegister(EngineeringRegisterItem? item)
{
if (item is null || !item.IsWritable)
{
return;
}
if (item.IsFloatRegisterAddress)
{
if (!float.TryParse(item.PendingValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var floatParsed))
{
item.StatusText = "请输入浮点数";
EngineeringRegisterStatus = $"{item.AddressDisplay} 输入格式无效";
return;
}
if (!_telemetryService.WriteHoldingFloatRegister(item.Address, floatParsed))
{
item.StatusText = "写入失败";
EngineeringRegisterStatus = $"{item.AddressDisplay} 写入失败";
return;
}
RefreshEngineeringRegisters();
item.StatusText = "写入成功";
EngineeringRegisterStatus = $"{item.AddressDisplay} 已写入 {floatParsed.ToString("F4", CultureInfo.InvariantCulture)}";
LatestAction = $"{item.Name} 已写入寄存器 {item.AddressDisplay} = {floatParsed.ToString("F4", CultureInfo.InvariantCulture)}";
TraceEvents.Insert(0, NewTrace("工程寄存器写入", $"{item.Name} / {item.AddressDisplay} = {floatParsed.ToString("F4", CultureInfo.InvariantCulture)}"));
_ = RefreshTelemetryAsync();
return;
}
if (!ushort.TryParse(item.PendingValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
{
item.StatusText = "请输入 0-65535 的整数";
EngineeringRegisterStatus = $"{item.AddressDisplay} 输入格式无效";
return;
}
if (!_telemetryService.WriteHoldingRegister(item.Address, parsed))
{
item.StatusText = "写入失败";
EngineeringRegisterStatus = $"{item.AddressDisplay} 写入失败";
return;
}
RefreshEngineeringRegisters();
item.StatusText = "写入成功";
EngineeringRegisterStatus = $"{item.AddressDisplay} 已写入 {parsed}";
LatestAction = $"{item.Name} 已写入寄存器 {item.AddressDisplay} = {parsed}";
TraceEvents.Insert(0, NewTrace("工程寄存器写入", $"{item.Name} / {item.AddressDisplay} = {parsed}"));
_ = RefreshTelemetryAsync();
}
[RelayCommand]
private void CapturePressureDrop50() => CapturePressureDropSample("50%");
@@ -1607,6 +1705,61 @@ public partial class MainViewModel : ObservableObject, IDisposable
_ => $"主泵 {PressureDropPumpFlowDisplay} / 流量偏差 {FlowImbalanceDisplay}"
};
private ObservableCollection<EngineeringRegisterItem> BuildEngineeringRegisters() =>
[
CreateEngineeringRegister("流量系数 1", 1006, true),
CreateEngineeringRegister("流量系数 2", 1016, true),
CreateEngineeringRegister("流量系数 3", 1026, true),
CreateEngineeringRegister("流量系数 4", 1036, true),
CreateEngineeringRegister("流量系数 5", 1046, true),
CreateEngineeringRegister("流量系数 6", 1056, true),
CreateEngineeringRegister("流量系数 7", 1066, true),
CreateEngineeringRegister("流量系数 8", 1076, true),
CreateEngineeringRegister("近端压力系数", 1328, true),
CreateEngineeringRegister("远端压力系数", 1378, true),
CreateEngineeringRegister("近端压力显示", 1330, false, usesFloatDisplay: true),
CreateEngineeringRegister("远端压力显示", 1380, false, usesFloatDisplay: true)
];
private static EngineeringRegisterItem CreateEngineeringRegister(string name, ushort address, bool isWritable, bool usesFloatDisplay = false) =>
new()
{
Name = name,
Address = address,
IsWritable = isWritable,
UsesFloatDisplay = usesFloatDisplay,
StatusText = isWritable ? "可读写" : "只读显示"
};
private bool RefreshEngineeringRegisterItem(EngineeringRegisterItem item)
{
if (item.IsFloatRegisterAddress)
{
var floatValue = _telemetryService.ReadHoldingFloatRegister(item.Address);
item.CurrentValue = floatValue.HasValue
? item.Address is 1330 or 1380
? $"{floatValue.Value:F2} kPa"
: floatValue.Value.ToString("F4", CultureInfo.InvariantCulture)
: "--";
if (item.IsWritable && floatValue.HasValue)
{
item.PendingValue = floatValue.Value.ToString("F4", CultureInfo.InvariantCulture);
}
item.StatusText = floatValue.HasValue ? "读取成功" : "读取失败";
return floatValue.HasValue;
}
var registerValue = _telemetryService.ReadHoldingRegister(item.Address);
item.CurrentValue = registerValue.HasValue
? registerValue.Value.ToString(CultureInfo.InvariantCulture)
: "--";
item.PendingValue = registerValue.HasValue
? registerValue.Value.ToString(CultureInfo.InvariantCulture)
: item.PendingValue;
item.StatusText = registerValue.HasValue ? "读取成功" : "读取失败";
return registerValue.HasValue;
}
private string PressureDisplay(string channelName, double? rawKpa)
{
if (rawKpa.HasValue)