This commit is contained in:
GukSang.Jin
2026-05-07 16:23:59 +08:00
parent 126bba29c0
commit 246e16d45d
8 changed files with 203 additions and 172 deletions

View File

@@ -20,6 +20,8 @@ public interface ITcpDeviceConnectionService : IAsyncDisposable
bool TryWriteInt16(ushort registerAddress, short value);
bool TryWriteFloat(ushort registerAddress, float value);
bool TryReadCoil(ushort coilAddress, out bool value);
bool TryWriteCoil(ushort coilAddress, bool value);

View File

@@ -22,7 +22,7 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
private const ushort IrradianceRegister = 410;
private const ushort IgnitionSecondsRegister = 1014;
private const ushort TestSecondsRegister = 1015;
private const ushort FlameDetectedCoil = 3;
private const ushort M3FlameMonitorBit = 3;
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
@@ -40,7 +40,7 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
ConeTemperature: ReadFloatOrEmpty(ConeTemperatureRegister),
SampleTemperature: ReadFloatOrEmpty(SampleTemperatureRegister),
Irradiance: ReadFloatOrEmpty(IrradianceRegister),
FlameDetected: ReadCoilOrFalse(FlameDetectedCoil),
FlameDetected: ReadCoilOrFalse(M3FlameMonitorBit),
Oxygen: ReadFloatOrEmpty(OxygenRegister),
CarbonDioxide: ReadFloatOrEmpty(CarbonDioxideRegister),
CarbonMonoxide: ReadFloatOrEmpty(CarbonMonoxideRegister),

View File

@@ -11,6 +11,7 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
private const byte ReadHoldingRegistersFunction = 0x03;
private const byte WriteSingleCoilFunction = 0x05;
private const byte WriteSingleRegisterFunction = 0x06;
private const byte WriteMultipleRegistersFunction = 0x10;
private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(3);
private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5);
private static readonly TimeSpan ReadWriteTimeout = TimeSpan.FromSeconds(2);
@@ -197,6 +198,31 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
}
}
public bool TryWriteFloat(ushort registerAddress, float value)
{
lock (_syncRoot)
{
if (_client is null || !IsTcpClientConnected(_client))
{
CloseCurrentClientCore();
return false;
}
try
{
WriteFloat(_client, registerAddress, value);
return true;
}
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
{
Debug.WriteLine($"TCP device register {registerAddress} float write failed: {ex.Message}");
SetConnectionState(false, $"写入寄存器 {registerAddress} 失败:{ex.Message}");
CloseCurrentClientCore();
return false;
}
}
}
public bool TryReadCoil(ushort coilAddress, out bool value)
{
value = false;
@@ -407,6 +433,24 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
}
}
private void WriteFloat(TcpClient client, ushort registerAddress, float value)
{
Span<byte> payload = stackalloc byte[9];
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], 2);
payload[4] = 4;
BinaryPrimitives.WriteInt32BigEndian(payload[5..9], BitConverter.SingleToInt32Bits(value));
var pdu = SendModbusRequest(client, WriteMultipleRegistersFunction, payload);
if (pdu.Length != 5
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != registerAddress
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[3..5]) != 2)
{
throw new InvalidDataException("Invalid Modbus TCP float write response.");
}
}
private bool ReadCoil(TcpClient client, ushort coilAddress)
{
Span<byte> payload = stackalloc byte[4];

View File

@@ -1,14 +1,8 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ConeCalorimeter.Services;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
namespace ConeCalorimeter.ViewModels;
@@ -19,6 +13,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
private const ushort CurrentHeatFluxRegister = 32;
private const ushort SlopeRegister = 420;
private const ushort InterceptRegister = 422;
private const ushort AlarmCoil = 91;
private readonly Action _closeAction;
private readonly Action _helpAction;
@@ -32,6 +27,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
private string _interceptText = "";
private string _lastAction = "待机";
private bool _alarmActive;
private bool _isEditingConeParameters;
public ConeRadiationSettingsViewModel(
Action closeAction,
@@ -44,18 +40,7 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
CloseCommand = new RelayCommand(_closeAction);
HelpCommand = new RelayCommand(_helpAction);
ActionCommand = new RelayCommand<string>(ExecuteAction);
CalibrationActions =
[
new ConeRadiationActionViewModel("10KW标定", ActionCommand),
new ConeRadiationActionViewModel("25KW标定", ActionCommand),
new ConeRadiationActionViewModel("35KW标定", ActionCommand),
new ConeRadiationActionViewModel("50KW标定", ActionCommand),
new ConeRadiationActionViewModel("65KW标定", ActionCommand),
new ConeRadiationActionViewModel("75KW标定", ActionCommand)
];
HeatFluxPlot = CreatePlotModel();
SaveParametersCommand = new RelayCommand(SaveParameters);
RefreshDeviceValues();
_refreshTimer = new DispatcherTimer
@@ -66,15 +51,13 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
_refreshTimer.Start();
}
public ObservableCollection<ConeRadiationActionViewModel> CalibrationActions { get; }
public IRelayCommand CloseCommand { get; }
public IRelayCommand HelpCommand { get; }
public IRelayCommand<string> ActionCommand { get; }
public PlotModel HeatFluxPlot { get; }
public IRelayCommand SaveParametersCommand { get; }
public string CurrentTemperatureText
{
@@ -124,51 +107,14 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
private set => SetProperty(ref _alarmActive, value);
}
private static PlotModel CreatePlotModel()
public void BeginConeParameterEdit()
{
var model = new PlotModel
{
Title = "温度-热流关系曲线",
PlotAreaBorderColor = OxyColors.DimGray,
TextColor = OxyColors.Black,
TitleColor = OxyColors.Black
};
_isEditingConeParameters = true;
}
model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Inside,
LegendPosition = LegendPosition.TopRight,
LegendBackground = OxyColor.FromAColor(220, OxyColors.White),
LegendBorder = OxyColors.Gray
});
model.Axes.Add(new LinearAxis
{
Position = AxisPosition.Bottom,
Title = "温度 (°C)",
Minimum = 0,
Maximum = 1000,
MajorStep = 200,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot,
MajorGridlineColor = OxyColor.FromRgb(205, 205, 205),
MinorGridlineColor = OxyColor.FromRgb(232, 232, 232)
});
model.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "热通量 (KW)",
Minimum = 0,
Maximum = 90,
MajorStep = 15,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot,
MajorGridlineColor = OxyColor.FromRgb(205, 205, 205),
MinorGridlineColor = OxyColor.FromRgb(232, 232, 232)
});
return model;
public void EndConeParameterEdit()
{
_isEditingConeParameters = false;
}
private void ExecuteAction(string? action)
@@ -194,8 +140,13 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
{
CurrentTemperatureText = ReadFloatText(CurrentTemperatureRegister);
CurrentHeatFluxText = ReadFloatText(CurrentHeatFluxRegister);
SlopeText = ReadFloatText(SlopeRegister);
InterceptText = ReadFloatText(InterceptRegister);
if (!_isEditingConeParameters)
{
SlopeText = ReadFloatText(SlopeRegister);
InterceptText = ReadFloatText(InterceptRegister);
}
AlarmActive = _tcpDeviceConnectionService.TryReadCoil(AlarmCoil, out var alarmActive) && alarmActive;
}
private string ReadFloatText(ushort registerAddress)
@@ -220,6 +171,35 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
}
}
private void SaveParameters()
{
if (!float.TryParse(SlopeText, NumberStyles.Float, CultureInfo.InvariantCulture, out var slope))
{
LastAction = "斜率输入无效";
Debug.WriteLine($"Invalid cone radiation slope: {SlopeText}");
return;
}
if (!float.TryParse(InterceptText, NumberStyles.Float, CultureInfo.InvariantCulture, out var intercept))
{
LastAction = "截距输入无效";
Debug.WriteLine($"Invalid cone radiation intercept: {InterceptText}");
return;
}
var slopeWritten = _tcpDeviceConnectionService.TryWriteFloat(SlopeRegister, slope);
var interceptWritten = _tcpDeviceConnectionService.TryWriteFloat(InterceptRegister, intercept);
if (slopeWritten && interceptWritten)
{
LastAction = "参数保存成功";
return;
}
LastAction = "参数保存失败";
Debug.WriteLine($"Cone radiation parameters write failed. Slope: {slopeWritten}, intercept: {interceptWritten}.");
}
private void WriteActionCoil(string action)
{
if (!TryGetActionCoil(action, out var coilAddress))
@@ -238,24 +218,6 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
{
switch (action)
{
case "10KW标定":
coilAddress = 130;
return true;
case "25KW标定":
coilAddress = 131;
return true;
case "35KW标定":
coilAddress = 132;
return true;
case "50KW标定":
coilAddress = 133;
return true;
case "65KW标定":
coilAddress = 134;
return true;
case "75KW标定":
coilAddress = 135;
return true;
case "循环水":
coilAddress = 49;
return true;

View File

@@ -1,7 +1,6 @@
<UserControl x:Class="ConeCalorimeter.Views.ConeRadiationSettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:oxy="http://oxyplot.org/wpf">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<Style x:Key="ValueBoxTextStyle" TargetType="TextBlock">
<Setter Property="Height" Value="42" />
@@ -97,10 +96,12 @@
HorizontalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#D9252E" />
<Setter Property="Background" Value="#2E9D57" />
<Setter Property="BorderBrush" Value="#1D6939" />
<Style.Triggers>
<DataTrigger Binding="{Binding AlarmActive}" Value="True">
<Setter Property="Background" Value="#F01824" />
<Setter Property="BorderBrush" Value="#6B201F" />
</DataTrigger>
</Style.Triggers>
</Style>
@@ -129,38 +130,32 @@
VerticalAlignment="Bottom" />
<Grid Grid.Row="1"
Margin="24,8,24,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="380" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Margin="0"
Background="White"
Margin="24,24,24,0">
<Border Background="White"
BorderBrush="#D2D8D5"
BorderThickness="1,1,1,0"
CornerRadius="6,6,0,0">
<oxy:PlotView Model="{Binding HeatFluxPlot}"
Background="White"
Margin="8,4,10,0" />
</Border>
<Grid Grid.Column="1"
Margin="18,10,0,0">
BorderThickness="1"
CornerRadius="6"
Padding="34,28">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="56" />
<RowDefinition Height="190" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0">
<StackPanel Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Center"
Margin="0,0,24,24">
<TextBlock Text="当前辐射温度:"
FontSize="24"
FontWeight="SemiBold"
Foreground="#172321" />
<Grid Margin="0,4,0,10">
<Grid Margin="0,8,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="150" />
@@ -176,12 +171,17 @@
FontSize="24"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
<StackPanel Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Center"
Margin="24,0,0,24">
<TextBlock Text="当前热通量:"
FontSize="24"
FontWeight="SemiBold"
Foreground="#172321" />
<Grid Margin="0,4,0,10">
<Grid Margin="0,8,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="150" />
@@ -199,12 +199,15 @@
</Grid>
</StackPanel>
<StackPanel Grid.Row="1">
<StackPanel Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Center"
Margin="0,8,24,24">
<TextBlock Text="设置辐射温度:"
FontSize="24"
FontWeight="SemiBold"
Foreground="#172321" />
<Grid Margin="0,4,0,6">
<Grid Margin="0,8,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="150" />
@@ -220,6 +223,16 @@
FontSize="24"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
<StackPanel Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Center"
Margin="24,8,0,24">
<TextBlock Text="热传递:"
FontSize="24"
FontWeight="SemiBold"
Foreground="#172321" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -239,9 +252,10 @@
</StackPanel>
<StackPanel Grid.Row="2"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="0,7,0,7">
Margin="0,14,0,0">
<Button Content="开始升温"
Command="{Binding ActionCommand}"
CommandParameter="开始升温"
@@ -256,30 +270,8 @@
Height="42"
Style="{StaticResource InstrumentButtonStyle}" />
</StackPanel>
<ItemsControl Grid.Row="3"
ItemsSource="{Binding CalibrationActions}"
VerticalAlignment="Top">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" Rows="3" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Label}"
Command="{Binding Command}"
CommandParameter="{Binding Label}"
Width="140"
Height="42"
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource InstrumentButtonStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</Grid>
<Grid Grid.Row="2"
@@ -287,31 +279,44 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="150" />
<ColumnDefinition Width="72" />
<ColumnDefinition Width="34" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="150" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="24" />
<ColumnDefinition Width="150" />
<ColumnDefinition Width="16" />
<ColumnDefinition Width="150" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="斜率:"
FontSize="24"
FontWeight="SemiBold"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1"
Text="{Binding SlopeText}"
Style="{StaticResource ValueBoxTextStyle}"
VerticalAlignment="Center" />
<TextBox Grid.Column="1"
Text="{Binding SlopeText, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource ValueInputBoxStyle}"
GotKeyboardFocus="ConeParameterTextBox_GotKeyboardFocus"
LostKeyboardFocus="ConeParameterTextBox_LostKeyboardFocus"
VerticalAlignment="Center" />
<TextBlock Grid.Column="3"
Text="截距:"
FontSize="24"
FontWeight="SemiBold"
VerticalAlignment="Center" />
<TextBlock Grid.Column="4"
Text="{Binding InterceptText}"
Style="{StaticResource ValueBoxTextStyle}"
VerticalAlignment="Center" />
<TextBox Grid.Column="4"
Text="{Binding InterceptText, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource ValueInputBoxStyle}"
GotKeyboardFocus="ConeParameterTextBox_GotKeyboardFocus"
LostKeyboardFocus="ConeParameterTextBox_LostKeyboardFocus"
VerticalAlignment="Center" />
<Button Grid.Column="6"
Content="保存参数"
Command="{Binding SaveParametersCommand}"
Height="42"
Style="{StaticResource InstrumentButtonStyle}"
VerticalAlignment="Center" />
<Button Grid.Column="8"
Content="循环水"
Command="{Binding ActionCommand}"
CommandParameter="循环水"

View File

@@ -1,4 +1,6 @@
using System.Windows.Controls;
using System.Windows.Input;
using ConeCalorimeter.ViewModels;
namespace ConeCalorimeter.Views;
@@ -8,4 +10,20 @@ public partial class ConeRadiationSettingsView : UserControl
{
InitializeComponent();
}
private void ConeParameterTextBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (DataContext is ConeRadiationSettingsViewModel viewModel)
{
viewModel.BeginConeParameterEdit();
}
}
private void ConeParameterTextBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (DataContext is ConeRadiationSettingsViewModel viewModel)
{
viewModel.EndConeParameterEdit();
}
}
}

View File

@@ -51,21 +51,27 @@
</DataTemplate>
<DataTemplate x:Key="SideMetricTemplate">
<Grid Margin="0,5">
<Border Margin="0,3"
Padding="8,5"
Background="#F8FAF9"
BorderBrush="#E1E6E3"
BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="112" />
<ColumnDefinition Width="70" />
<ColumnDefinition Width="64" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Label}"
FontSize="18"
FontSize="16"
FontWeight="SemiBold"
Foreground="#1B2826"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1"
Text="{Binding ValueText}"
FontFamily="Consolas"
FontSize="20"
FontSize="19"
FontWeight="SemiBold"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
@@ -76,6 +82,7 @@
Foreground="#34423E"
VerticalAlignment="Center" />
</Grid>
</Border>
</DataTemplate>
</UserControl.Resources>
@@ -134,7 +141,7 @@
</Ellipse>
<StackPanel Grid.Column="1"
VerticalAlignment="Center">
<TextBlock Text="火焰监测"
<TextBlock Text="M3火焰监测"
FontSize="18"
FontWeight="SemiBold"
Foreground="#162321" />
@@ -158,25 +165,12 @@
Style="{StaticResource PanelBorderStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="52" />
<RowDefinition Height="*" />
<RowDefinition Height="260" />
<RowDefinition Height="280" />
</Grid.RowDefinitions>
<Border Background="#F1F4F2"
BorderBrush="#D3DAD6"
BorderThickness="0,0,0,1"
CornerRadius="6,6,0,0">
<TextBlock Text="实验操作"
FontSize="22"
FontWeight="SemiBold"
Foreground="#1B2A27"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<ScrollViewer Grid.Row="1"
Padding="20,16"
<ScrollViewer Grid.Row="0"
Padding="12,12,12,8"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<ItemsControl ItemsSource="{Binding GasMetrics}"
@@ -203,7 +197,7 @@
</StackPanel>
</ScrollViewer>
<Border Grid.Row="2"
<Border Grid.Row="1"
Padding="10"
Background="#F8FAF9"
BorderBrush="#D3DAD6"
@@ -220,8 +214,8 @@
<Button Content="{Binding Label}"
Command="{Binding Command}"
CommandParameter="{Binding Label}"
Margin="5"
MinHeight="38"
Margin="5,3"
MinHeight="40"
Style="{StaticResource InstrumentButtonStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>

View File

@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.203",
"rollForward": "latestFeature"
}
}