更新
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="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</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>
|
<Border.Resources>
|
||||||
<Style x:Key="HeroCompactPillStyle" TargetType="Border" BasedOn="{StaticResource PillBorderStyle}">
|
<Style x:Key="HeroCompactPillStyle" TargetType="Border" BasedOn="{StaticResource PillBorderStyle}">
|
||||||
<Setter Property="Padding" Value="10,5" />
|
<Setter Property="Padding" Value="10,5" />
|
||||||
@@ -109,7 +116,7 @@
|
|||||||
|
|
||||||
<Grid Grid.Row="2">
|
<Grid Grid.Row="2">
|
||||||
<Border Style="{StaticResource CardBorderStyle}" Margin="0">
|
<Border Style="{StaticResource CardBorderStyle}" Margin="0">
|
||||||
<TabControl>
|
<TabControl x:Name="RootTabControl">
|
||||||
<TabItem Header="项目检测">
|
<TabItem Header="项目检测">
|
||||||
<ScrollViewer Margin="0,6,0,0" VerticalScrollBarVisibility="Auto" CanContentScroll="False">
|
<ScrollViewer Margin="0,6,0,0" VerticalScrollBarVisibility="Auto" CanContentScroll="False">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -1416,6 +1423,84 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</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="追溯">
|
<TabItem Header="追溯">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="0,6,0,0">
|
<StackPanel Margin="0,6,0,0">
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Windows.Threading;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
using Cardiopulmonarybypasssystems.ViewModels;
|
using Cardiopulmonarybypasssystems.ViewModels;
|
||||||
|
|
||||||
@@ -9,11 +12,18 @@ namespace Cardiopulmonarybypasssystems;
|
|||||||
|
|
||||||
public partial class MainWindow : Window
|
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)
|
public MainWindow(MainViewModel viewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
ConfigureTrendBindings();
|
ConfigureTrendBindings();
|
||||||
|
_engineeringTouchHoldTimer.Tick += EngineeringTouchHoldTimer_OnTick;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureTrendBindings()
|
private void ConfigureTrendBindings()
|
||||||
@@ -48,4 +58,104 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
BindingOperations.SetBinding(polyline, Polyline.PointsProperty, binding);
|
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<PumpControlChannel> GetPumpControls();
|
||||||
IReadOnlyList<ValveControlChannel> GetValveControls();
|
IReadOnlyList<ValveControlChannel> GetValveControls();
|
||||||
TelemetryUpdateSnapshot UpdateChannels();
|
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 SetPumpRunning(string pumpKey, bool isRunning);
|
||||||
void SetValveOpen(string valveKey, bool isOpen);
|
void SetValveOpen(string valveKey, bool isOpen);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,19 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
|
|||||||
private readonly Random _random = new();
|
private readonly Random _random = new();
|
||||||
private readonly ModbusFactory _factory = new();
|
private readonly ModbusFactory _factory = new();
|
||||||
private readonly Dictionary<string, Queue<double>> _channelWindows = new(StringComparer.Ordinal);
|
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 =
|
private readonly List<DeviceChannel> _channels =
|
||||||
[
|
[
|
||||||
new() { Name = "主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
|
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)
|
public void SetPumpRunning(string pumpKey, bool isRunning)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
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)
|
public void SetPumpRunning(string pumpKey, bool isRunning)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
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()
|
private void ConnectWithTimeout()
|
||||||
{
|
{
|
||||||
TcpClient? tcpClient = null;
|
TcpClient? tcpClient = null;
|
||||||
@@ -623,6 +753,16 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
|
|||||||
return BitConverter.Int32BitsToSingle(unchecked((int)bits));
|
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) =>
|
private static bool IsPlausiblePressureKpa(float value) =>
|
||||||
!float.IsNaN(value) && !float.IsInfinity(value) && value is > -1000f and < 1000f;
|
!float.IsNaN(value) && !float.IsInfinity(value) && value is > -1000f and < 1000f;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -46,6 +47,12 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
|||||||
private double? _proximalPressureRawKpa;
|
private double? _proximalPressureRawKpa;
|
||||||
private double? _distalPressureRawKpa;
|
private double? _distalPressureRawKpa;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool engineeringRegisterPanelVisible;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string engineeringRegisterStatus = "双击右上角显示工程寄存器界面";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string pageTitle = "心肺转流系统一次性使用动静脉插管检测";
|
private string pageTitle = "心肺转流系统一次性使用动静脉插管检测";
|
||||||
|
|
||||||
@@ -161,6 +168,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
|||||||
TraceEvents = new ObservableCollection<TraceEvent>(repository.GetInitialTraceEvents());
|
TraceEvents = new ObservableCollection<TraceEvent>(repository.GetInitialTraceEvents());
|
||||||
AlarmMessages = new ObservableCollection<AlarmMessage>();
|
AlarmMessages = new ObservableCollection<AlarmMessage>();
|
||||||
ResultStatusOptions = new ObservableCollection<string>(["待检", "合格", "预警", "不合格"]);
|
ResultStatusOptions = new ObservableCollection<string>(["待检", "合格", "预警", "不合格"]);
|
||||||
|
EngineeringRegisters = BuildEngineeringRegisters();
|
||||||
PressureDropEntries = new ObservableCollection<PressureDropPointEntry>(
|
PressureDropEntries = new ObservableCollection<PressureDropPointEntry>(
|
||||||
[
|
[
|
||||||
new() { Label = "50%", TargetFlow = 3.0 },
|
new() { Label = "50%", TargetFlow = 3.0 },
|
||||||
@@ -249,6 +257,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
|||||||
public ObservableCollection<TraceEvent> TraceEvents { get; }
|
public ObservableCollection<TraceEvent> TraceEvents { get; }
|
||||||
public ObservableCollection<AlarmMessage> AlarmMessages { get; }
|
public ObservableCollection<AlarmMessage> AlarmMessages { get; }
|
||||||
public ObservableCollection<string> ResultStatusOptions { get; }
|
public ObservableCollection<string> ResultStatusOptions { get; }
|
||||||
|
public ObservableCollection<EngineeringRegisterItem> EngineeringRegisters { get; }
|
||||||
public ObservableCollection<PressureDropPointEntry> PressureDropEntries { get; }
|
public ObservableCollection<PressureDropPointEntry> PressureDropEntries { get; }
|
||||||
public ObservableCollection<KinkResistancePointEntry> KinkResistanceEntries { get; }
|
public ObservableCollection<KinkResistancePointEntry> KinkResistanceEntries { get; }
|
||||||
public ObservableCollection<RecirculationPointEntry> RecirculationEntries { get; }
|
public ObservableCollection<RecirculationPointEntry> RecirculationEntries { get; }
|
||||||
@@ -270,6 +279,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
|||||||
public ObservableCollection<string> HemolysisAnticoagulantOptions { get; } = new(["肝素", "枸橼酸钠", "其他"]);
|
public ObservableCollection<string> HemolysisAnticoagulantOptions { get; } = new(["肝素", "枸橼酸钠", "其他"]);
|
||||||
public bool HasFilteredItems => !FilteredItemsView.IsEmpty;
|
public bool HasFilteredItems => !FilteredItemsView.IsEmpty;
|
||||||
public bool HasSelectedItem => SelectedItem is not null;
|
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> FlowSensorChannels => Channels.Where(IsFlowSensorChannel);
|
||||||
public IEnumerable<DeviceChannel> OtherChannels => Channels.Where(channel => !IsFlowSensorChannel(channel));
|
public IEnumerable<DeviceChannel> OtherChannels => Channels.Where(channel => !IsFlowSensorChannel(channel));
|
||||||
public bool IsTelemetryOnline => _isTelemetryOnline;
|
public bool IsTelemetryOnline => _isTelemetryOnline;
|
||||||
@@ -480,6 +490,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
|||||||
partial void OnPressureDropLimit100Changed(double value) => UpdateAndPersistLimitSettings();
|
partial void OnPressureDropLimit100Changed(double value) => UpdateAndPersistLimitSettings();
|
||||||
partial void OnAntiCollapseAllowedIncreaseRateChanged(double value) => UpdateAndPersistLimitSettings();
|
partial void OnAntiCollapseAllowedIncreaseRateChanged(double value) => UpdateAndPersistLimitSettings();
|
||||||
partial void OnRecirculationAllowedLimitChanged(double value) => UpdateAndPersistLimitSettings();
|
partial void OnRecirculationAllowedLimitChanged(double value) => UpdateAndPersistLimitSettings();
|
||||||
|
partial void OnEngineeringRegisterPanelVisibleChanged(bool value) => OnPropertyChanged(nameof(EngineeringRegisterPanelVisibility));
|
||||||
partial void OnItemSearchTextChanged(string value)
|
partial void OnItemSearchTextChanged(string value)
|
||||||
{
|
{
|
||||||
RefreshFilteredItemsView();
|
RefreshFilteredItemsView();
|
||||||
@@ -659,6 +670,93 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
|||||||
RaiseTrendPropertyChanges();
|
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]
|
[RelayCommand]
|
||||||
private void CapturePressureDrop50() => CapturePressureDropSample("50%");
|
private void CapturePressureDrop50() => CapturePressureDropSample("50%");
|
||||||
|
|
||||||
@@ -1607,6 +1705,61 @@ public partial class MainViewModel : ObservableObject, IDisposable
|
|||||||
_ => $"主泵 {PressureDropPumpFlowDisplay} / 流量偏差 {FlowImbalanceDisplay}"
|
_ => $"主泵 {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)
|
private string PressureDisplay(string channelName, double? rawKpa)
|
||||||
{
|
{
|
||||||
if (rawKpa.HasValue)
|
if (rawKpa.HasValue)
|
||||||
|
|||||||
Reference in New Issue
Block a user