From 246e16d45de42857d01c482b6f4e92d493cad2d4 Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Thu, 7 May 2026 16:23:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ITcpDeviceConnectionService.cs | 2 + .../Services/ModbusRealtimeDataService.cs | 4 +- .../Services/TcpDeviceConnectionService.cs | 44 ++++++ .../ConeRadiationSettingsViewModel.cs | 130 +++++++----------- .../Views/ConeRadiationSettingsView.xaml | 129 ++++++++--------- .../Views/ConeRadiationSettingsView.xaml.cs | 18 +++ ConeCalorimeter/Views/TestPageView.xaml | 42 +++--- ConeCalorimeter/global.json | 6 + 8 files changed, 203 insertions(+), 172 deletions(-) create mode 100644 ConeCalorimeter/global.json diff --git a/ConeCalorimeter/Services/ITcpDeviceConnectionService.cs b/ConeCalorimeter/Services/ITcpDeviceConnectionService.cs index 767e71b..80076d4 100644 --- a/ConeCalorimeter/Services/ITcpDeviceConnectionService.cs +++ b/ConeCalorimeter/Services/ITcpDeviceConnectionService.cs @@ -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); diff --git a/ConeCalorimeter/Services/ModbusRealtimeDataService.cs b/ConeCalorimeter/Services/ModbusRealtimeDataService.cs index ff6878a..daaa9b5 100644 --- a/ConeCalorimeter/Services/ModbusRealtimeDataService.cs +++ b/ConeCalorimeter/Services/ModbusRealtimeDataService.cs @@ -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), diff --git a/ConeCalorimeter/Services/TcpDeviceConnectionService.cs b/ConeCalorimeter/Services/TcpDeviceConnectionService.cs index 19f4333..56b5c36 100644 --- a/ConeCalorimeter/Services/TcpDeviceConnectionService.cs +++ b/ConeCalorimeter/Services/TcpDeviceConnectionService.cs @@ -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 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 payload = stackalloc byte[4]; diff --git a/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs b/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs index 359d95c..1e8e360 100644 --- a/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs +++ b/ConeCalorimeter/ViewModels/ConeRadiationSettingsViewModel.cs @@ -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(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 CalibrationActions { get; } - public IRelayCommand CloseCommand { get; } public IRelayCommand HelpCommand { get; } public IRelayCommand 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; diff --git a/ConeCalorimeter/Views/ConeRadiationSettingsView.xaml b/ConeCalorimeter/Views/ConeRadiationSettingsView.xaml index 178cfd4..fe1486b 100644 --- a/ConeCalorimeter/Views/ConeRadiationSettingsView.xaml +++ b/ConeCalorimeter/Views/ConeRadiationSettingsView.xaml @@ -1,7 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -129,38 +130,32 @@ VerticalAlignment="Bottom" /> - - - - - - + - - - - + BorderThickness="1" + CornerRadius="6" + Padding="34,28"> + - - + + + + + - + - + @@ -176,12 +171,17 @@ FontSize="24" VerticalAlignment="Center" /> + + - + @@ -199,12 +199,15 @@ - + - + @@ -220,6 +223,16 @@ FontSize="24" VerticalAlignment="Center" /> + + + + @@ -239,9 +252,10 @@ + Margin="0,14,0,0">