更新
This commit is contained in:
97
Cardiopulmonarybypasssystems/EngineeringRegistersWindow.xaml
Normal file
97
Cardiopulmonarybypasssystems/EngineeringRegistersWindow.xaml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user