更新点位
This commit is contained in:
@@ -15,9 +15,11 @@ namespace ConeCalorimeter
|
||||
_tcpDeviceConnectionService = new TcpDeviceConnectionService();
|
||||
_ = _tcpDeviceConnectionService.StartAsync();
|
||||
|
||||
var experimentDataService = new ExperimentDataService(new DemoRealtimeDataService());
|
||||
var experimentDataService = new ExperimentDataService(
|
||||
new ModbusRealtimeDataService(_tcpDeviceConnectionService));
|
||||
DataContext = new MainViewModel(
|
||||
experimentDataService,
|
||||
_tcpDeviceConnectionService,
|
||||
new NpoiReportExportService(),
|
||||
new HelpDialogService());
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public sealed record RealtimeDataRecord(
|
||||
double OrificePressure,
|
||||
double OrificeTemperature,
|
||||
double ConeTemperature,
|
||||
double SampleTemperature,
|
||||
double Irradiance,
|
||||
bool FlameDetected,
|
||||
double Oxygen,
|
||||
@@ -30,6 +31,7 @@ public sealed record RealtimeDataRecord(
|
||||
snapshot.OrificePressure,
|
||||
snapshot.OrificeTemperature,
|
||||
snapshot.ConeTemperature,
|
||||
snapshot.SampleTemperature,
|
||||
snapshot.Irradiance,
|
||||
snapshot.FlameDetected,
|
||||
snapshot.Oxygen,
|
||||
|
||||
@@ -5,6 +5,7 @@ public sealed record RealtimeSnapshot(
|
||||
double OrificePressure,
|
||||
double OrificeTemperature,
|
||||
double ConeTemperature,
|
||||
double SampleTemperature,
|
||||
double Irradiance,
|
||||
bool FlameDetected,
|
||||
double Oxygen,
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using ConeCalorimeter.Models;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
|
||||
public sealed class DemoRealtimeDataService : IRealtimeDataService
|
||||
{
|
||||
private readonly Random _random = new(20260504);
|
||||
|
||||
public RealtimeSnapshot GetCurrentSnapshot(TimeSpan elapsed)
|
||||
{
|
||||
var seconds = elapsed.TotalSeconds;
|
||||
var heatBase = 28 + Math.Sin(seconds / 8) * 11 + Math.Sin(seconds / 2.7) * 4;
|
||||
var heatReleaseRate = Math.Max(0, heatBase + _random.NextDouble() * 2.5);
|
||||
var totalHeat = Math.Min(150, seconds * 0.42 + Math.Sin(seconds / 10) * 2);
|
||||
var totalSmoke = Math.Min(150, seconds * 0.34 + Math.Cos(seconds / 9) * 1.8);
|
||||
|
||||
return new RealtimeSnapshot(
|
||||
OrificeFlow: 2.35 + Math.Sin(seconds / 11) * 0.08,
|
||||
OrificePressure: 18.4 + Math.Cos(seconds / 7) * 0.35,
|
||||
OrificeTemperature: 296.2 + Math.Sin(seconds / 15) * 0.5,
|
||||
ConeTemperature: 751 + Math.Sin(seconds / 13) * 4,
|
||||
Irradiance: 50.0 + Math.Cos(seconds / 18) * 0.25,
|
||||
FlameDetected: seconds % 24 > 5,
|
||||
Oxygen: 20.95 - Math.Min(2.4, seconds * 0.006),
|
||||
CarbonDioxide: 0.04 + Math.Min(7.5, seconds * 0.018),
|
||||
CarbonMonoxide: 0.01 + Math.Min(1.3, seconds * 0.004),
|
||||
HeatReleaseRate: heatReleaseRate,
|
||||
Qa180: Math.Min(120, seconds * 0.21),
|
||||
Qa300: Math.Min(180, seconds * 0.18),
|
||||
TotalHeatRelease: totalHeat,
|
||||
SmokeProduction: 0.3 + Math.Min(45, seconds * 0.09),
|
||||
CurrentMass: Math.Max(0, 35.0 - seconds * 0.015),
|
||||
MassLoss: Math.Min(35.0, seconds * 0.015),
|
||||
IgnitionSeconds: (int)Math.Min(seconds, 999),
|
||||
TestSeconds: (int)Math.Min(seconds, 9999),
|
||||
TotalSmoke: totalSmoke);
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,14 @@ public interface ITcpDeviceConnectionService : IAsyncDisposable
|
||||
Task StartAsync();
|
||||
|
||||
Task StopAsync();
|
||||
|
||||
bool TryReadFloat(ushort registerAddress, out double value);
|
||||
|
||||
bool TryReadInt16(ushort registerAddress, out int value);
|
||||
|
||||
bool TryWriteInt16(ushort registerAddress, short value);
|
||||
|
||||
bool TryReadCoil(ushort coilAddress, out bool value);
|
||||
|
||||
bool TryWriteCoil(ushort coilAddress, bool value);
|
||||
}
|
||||
|
||||
74
ConeCalorimeter/Services/ModbusRealtimeDataService.cs
Normal file
74
ConeCalorimeter/Services/ModbusRealtimeDataService.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using ConeCalorimeter.Models;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
|
||||
public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
{
|
||||
private const ushort OxygenRegister = 10;
|
||||
private const ushort OrificeFlowRegister = 14;
|
||||
private const ushort OrificePressureRegister = 16;
|
||||
private const ushort CarbonMonoxideRegister = 18;
|
||||
private const ushort CarbonDioxideRegister = 20;
|
||||
private const ushort ConeTemperatureRegister = 26;
|
||||
private const ushort OrificeTemperatureRegister = 30;
|
||||
private const ushort SampleTemperatureRegister = 36;
|
||||
private const ushort HeatReleaseRateRegister = 354;
|
||||
private const ushort Qa180Register = 366;
|
||||
private const ushort Qa300Register = 370;
|
||||
private const ushort TotalHeatReleaseRegister = 372;
|
||||
private const ushort SmokeProductionRegister = 390;
|
||||
private const ushort IgnitionSecondsRegister = 1014;
|
||||
private const ushort TestSecondsRegister = 1015;
|
||||
private const ushort FlameDetectedCoil = 3;
|
||||
|
||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||
|
||||
public ModbusRealtimeDataService(ITcpDeviceConnectionService tcpDeviceConnectionService)
|
||||
{
|
||||
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||
}
|
||||
|
||||
public RealtimeSnapshot GetCurrentSnapshot(TimeSpan elapsed)
|
||||
{
|
||||
return new RealtimeSnapshot(
|
||||
OrificeFlow: ReadFloatOrEmpty(OrificeFlowRegister),
|
||||
OrificePressure: ReadFloatOrEmpty(OrificePressureRegister),
|
||||
OrificeTemperature: ReadFloatOrEmpty(OrificeTemperatureRegister),
|
||||
ConeTemperature: ReadFloatOrEmpty(ConeTemperatureRegister),
|
||||
SampleTemperature: ReadFloatOrEmpty(SampleTemperatureRegister),
|
||||
Irradiance: double.NaN,
|
||||
FlameDetected: ReadCoilOrFalse(FlameDetectedCoil),
|
||||
Oxygen: ReadFloatOrEmpty(OxygenRegister),
|
||||
CarbonDioxide: ReadFloatOrEmpty(CarbonDioxideRegister),
|
||||
CarbonMonoxide: ReadFloatOrEmpty(CarbonMonoxideRegister),
|
||||
HeatReleaseRate: ReadFloatOrEmpty(HeatReleaseRateRegister),
|
||||
Qa180: ReadFloatOrEmpty(Qa180Register),
|
||||
Qa300: ReadFloatOrEmpty(Qa300Register),
|
||||
TotalHeatRelease: ReadFloatOrEmpty(TotalHeatReleaseRegister),
|
||||
SmokeProduction: ReadFloatOrEmpty(SmokeProductionRegister),
|
||||
CurrentMass: double.NaN,
|
||||
MassLoss: double.NaN,
|
||||
IgnitionSeconds: ReadInt16OrEmpty(IgnitionSecondsRegister),
|
||||
TestSeconds: ReadInt16OrEmpty(TestSecondsRegister),
|
||||
TotalSmoke: ReadFloatOrEmpty(SmokeProductionRegister));
|
||||
}
|
||||
|
||||
private double ReadFloatOrEmpty(ushort registerAddress)
|
||||
{
|
||||
return _tcpDeviceConnectionService.TryReadFloat(registerAddress, out var value)
|
||||
? value
|
||||
: double.NaN;
|
||||
}
|
||||
|
||||
private int ReadInt16OrEmpty(ushort registerAddress)
|
||||
{
|
||||
return _tcpDeviceConnectionService.TryReadInt16(registerAddress, out var value)
|
||||
? value
|
||||
: -1;
|
||||
}
|
||||
|
||||
private bool ReadCoilOrFalse(ushort coilAddress)
|
||||
{
|
||||
return _tcpDeviceConnectionService.TryReadCoil(coilAddress, out var value) && value;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
@@ -7,13 +9,20 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
{
|
||||
private const string Host = "192.168.1.10";
|
||||
private const int Port = 502;
|
||||
private const byte UnitId = 1;
|
||||
private const byte ReadCoilsFunction = 0x01;
|
||||
private const byte ReadHoldingRegistersFunction = 0x03;
|
||||
private const byte WriteSingleCoilFunction = 0x05;
|
||||
private const byte WriteSingleRegisterFunction = 0x06;
|
||||
private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(3);
|
||||
private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan ReadWriteTimeout = TimeSpan.FromSeconds(2);
|
||||
|
||||
private readonly object _syncRoot = new();
|
||||
private CancellationTokenSource? _connectionLoopCancellation;
|
||||
private Task? _connectionLoopTask;
|
||||
private TcpClient? _client;
|
||||
private ushort _transactionId;
|
||||
private bool _isConnected;
|
||||
|
||||
public bool IsConnected
|
||||
@@ -90,6 +99,132 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
await StopAsync();
|
||||
}
|
||||
|
||||
public bool TryReadFloat(ushort registerAddress, out double value)
|
||||
{
|
||||
value = double.NaN;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_client is null || !IsTcpClientConnected(_client))
|
||||
{
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
value = ReadFloat(_client, registerAddress);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} read failed: {ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadInt16(ushort registerAddress, out int value)
|
||||
{
|
||||
value = 0;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_client is null || !IsTcpClientConnected(_client))
|
||||
{
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
value = ReadInt16(_client, registerAddress);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} read failed: {ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWriteInt16(ushort registerAddress, short value)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_client is null || !IsTcpClientConnected(_client))
|
||||
{
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WriteInt16(_client, registerAddress, value);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device register {registerAddress} write failed: {ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReadCoil(ushort coilAddress, out bool value)
|
||||
{
|
||||
value = false;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_client is null || !IsTcpClientConnected(_client))
|
||||
{
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
value = ReadCoil(_client, coilAddress);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device coil {coilAddress} read failed: {ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWriteCoil(ushort coilAddress, bool value)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_client is null || !IsTcpClientConnected(_client))
|
||||
{
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WriteCoil(_client, coilAddress, value);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
|
||||
{
|
||||
Debug.WriteLine($"TCP device coil {coilAddress} write failed: {ex.Message}");
|
||||
CloseCurrentClientCore();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunConnectionLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
@@ -138,9 +273,12 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
}
|
||||
|
||||
await connectTask;
|
||||
client.ReceiveTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
|
||||
client.SendTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_client?.Dispose();
|
||||
_client = client;
|
||||
_isConnected = true;
|
||||
}
|
||||
@@ -187,15 +325,159 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
||||
|
||||
private void CloseCurrentClient()
|
||||
{
|
||||
TcpClient? client;
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
client = _client;
|
||||
_client = null;
|
||||
_isConnected = false;
|
||||
CloseCurrentClientCore();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseCurrentClientCore()
|
||||
{
|
||||
var client = _client;
|
||||
_client = null;
|
||||
_isConnected = false;
|
||||
client?.Dispose();
|
||||
}
|
||||
|
||||
private double ReadFloat(TcpClient client, ushort registerAddress)
|
||||
{
|
||||
var pdu = ReadHoldingRegisters(client, registerAddress, 2);
|
||||
|
||||
if (pdu.Length != 6 || pdu[1] != 4)
|
||||
{
|
||||
throw new InvalidDataException("Invalid Modbus TCP float response.");
|
||||
}
|
||||
|
||||
var rawValue = BinaryPrimitives.ReadInt32BigEndian(pdu[2..6]);
|
||||
return BitConverter.Int32BitsToSingle(rawValue);
|
||||
}
|
||||
|
||||
private int ReadInt16(TcpClient client, ushort registerAddress)
|
||||
{
|
||||
var pdu = ReadHoldingRegisters(client, registerAddress, 1);
|
||||
|
||||
if (pdu.Length != 4 || pdu[1] != 2)
|
||||
{
|
||||
throw new InvalidDataException("Invalid Modbus TCP int16 response.");
|
||||
}
|
||||
|
||||
return BinaryPrimitives.ReadInt16BigEndian(pdu[2..4]);
|
||||
}
|
||||
|
||||
private void WriteInt16(TcpClient client, ushort registerAddress, short value)
|
||||
{
|
||||
Span<byte> payload = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
|
||||
BinaryPrimitives.WriteInt16BigEndian(payload[2..4], value);
|
||||
|
||||
var pdu = SendModbusRequest(client, WriteSingleRegisterFunction, payload);
|
||||
|
||||
if (pdu.Length != 5
|
||||
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != registerAddress
|
||||
|| BinaryPrimitives.ReadInt16BigEndian(pdu[3..5]) != value)
|
||||
{
|
||||
throw new InvalidDataException("Invalid Modbus TCP register write response.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool ReadCoil(TcpClient client, ushort coilAddress)
|
||||
{
|
||||
Span<byte> payload = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], coilAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], 1);
|
||||
|
||||
var pdu = SendModbusRequest(client, ReadCoilsFunction, payload);
|
||||
|
||||
if (pdu.Length != 3 || pdu[1] != 1)
|
||||
{
|
||||
throw new InvalidDataException("Invalid Modbus TCP coil response.");
|
||||
}
|
||||
|
||||
return (pdu[2] & 0x01) == 0x01;
|
||||
}
|
||||
|
||||
private void WriteCoil(TcpClient client, ushort coilAddress, bool value)
|
||||
{
|
||||
Span<byte> payload = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], coilAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], value ? (ushort)0xFF00 : (ushort)0x0000);
|
||||
|
||||
var pdu = SendModbusRequest(client, WriteSingleCoilFunction, payload);
|
||||
|
||||
if (pdu.Length != 5
|
||||
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != coilAddress
|
||||
|| BinaryPrimitives.ReadUInt16BigEndian(pdu[3..5]) != (value ? (ushort)0xFF00 : (ushort)0x0000))
|
||||
{
|
||||
throw new InvalidDataException("Invalid Modbus TCP coil write response.");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] ReadHoldingRegisters(TcpClient client, ushort registerAddress, ushort registerCount)
|
||||
{
|
||||
Span<byte> payload = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], registerCount);
|
||||
|
||||
return SendModbusRequest(client, ReadHoldingRegistersFunction, payload);
|
||||
}
|
||||
|
||||
private byte[] SendModbusRequest(TcpClient client, byte functionCode, ReadOnlySpan<byte> payload)
|
||||
{
|
||||
var transactionId = ++_transactionId;
|
||||
var pduLength = 1 + payload.Length;
|
||||
var request = new byte[7 + pduLength];
|
||||
|
||||
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(0, 2), transactionId);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(2, 2), 0);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(4, 2), (ushort)(1 + pduLength));
|
||||
request[6] = UnitId;
|
||||
request[7] = functionCode;
|
||||
payload.CopyTo(request.AsSpan(8));
|
||||
|
||||
var stream = client.GetStream();
|
||||
stream.Write(request);
|
||||
|
||||
Span<byte> header = stackalloc byte[7];
|
||||
ReadExactly(stream, header);
|
||||
|
||||
var responseTransactionId = BinaryPrimitives.ReadUInt16BigEndian(header[0..2]);
|
||||
var protocolId = BinaryPrimitives.ReadUInt16BigEndian(header[2..4]);
|
||||
var length = BinaryPrimitives.ReadUInt16BigEndian(header[4..6]);
|
||||
var unitId = header[6];
|
||||
|
||||
if (responseTransactionId != transactionId || protocolId != 0 || unitId != UnitId || length < 2)
|
||||
{
|
||||
throw new InvalidDataException("Invalid Modbus TCP response header.");
|
||||
}
|
||||
|
||||
var pdu = new byte[length - 1];
|
||||
ReadExactly(stream, pdu);
|
||||
|
||||
if (pdu[0] == (functionCode | 0x80))
|
||||
{
|
||||
throw new InvalidDataException($"Modbus exception code {pdu[1]}.");
|
||||
}
|
||||
|
||||
if (pdu[0] != functionCode)
|
||||
{
|
||||
throw new InvalidDataException("Unexpected Modbus TCP function code.");
|
||||
}
|
||||
|
||||
return pdu;
|
||||
}
|
||||
|
||||
private static void ReadExactly(NetworkStream stream, Span<byte> buffer)
|
||||
{
|
||||
var totalRead = 0;
|
||||
while (totalRead < buffer.Length)
|
||||
{
|
||||
var read = stream.Read(buffer[totalRead..]);
|
||||
if (read == 0)
|
||||
{
|
||||
throw new IOException("TCP device closed the connection.");
|
||||
}
|
||||
|
||||
totalRead += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
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;
|
||||
|
||||
namespace ConeCalorimeter.ViewModels;
|
||||
|
||||
public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
{
|
||||
private const ushort TemperatureRegister = 282;
|
||||
private const ushort PressureDifferenceRegister = 284;
|
||||
private const ushort OxygenRegister = 286;
|
||||
private const ushort CValueRegister = 308;
|
||||
|
||||
private readonly Action _closeAction;
|
||||
private readonly Action _helpAction;
|
||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||
private readonly DispatcherTimer _refreshTimer;
|
||||
private string _baselineOxygenText = "";
|
||||
private string _temperatureText = "";
|
||||
private string _pressureDifferenceText = "";
|
||||
@@ -15,10 +26,14 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
private string _cValueText = "";
|
||||
private string _lastAction = "待机";
|
||||
|
||||
public CValueCalibrationViewModel(Action closeAction, Action helpAction) : base("C值标定")
|
||||
public CValueCalibrationViewModel(
|
||||
Action closeAction,
|
||||
Action helpAction,
|
||||
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("C值标定")
|
||||
{
|
||||
_closeAction = closeAction;
|
||||
_helpAction = helpAction;
|
||||
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||
CloseCommand = new RelayCommand(_closeAction);
|
||||
HelpCommand = new RelayCommand(_helpAction);
|
||||
ActionCommand = new RelayCommand<string>(ExecuteAction);
|
||||
@@ -36,6 +51,14 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
new CValueCalibrationActionViewModel("标定开始", ActionCommand),
|
||||
new CValueCalibrationActionViewModel("基线采集", ActionCommand)
|
||||
];
|
||||
|
||||
RefreshDeviceValues();
|
||||
_refreshTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
_refreshTimer.Tick += (_, _) => RefreshDeviceValues();
|
||||
_refreshTimer.Start();
|
||||
}
|
||||
|
||||
public ObservableCollection<CValueCalibrationActionViewModel> TopActions { get; }
|
||||
@@ -94,19 +117,65 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
||||
}
|
||||
|
||||
LastAction = action;
|
||||
WriteActionCoil(action);
|
||||
}
|
||||
|
||||
if (action == "基线采集")
|
||||
private void RefreshDeviceValues()
|
||||
{
|
||||
var oxygenText = ReadFloatText(OxygenRegister);
|
||||
|
||||
BaselineOxygenText = oxygenText;
|
||||
TemperatureText = ReadFloatText(TemperatureRegister);
|
||||
PressureDifferenceText = ReadFloatText(PressureDifferenceRegister);
|
||||
CalibrationOxygenText = oxygenText;
|
||||
CValueText = ReadFloatText(CValueRegister);
|
||||
}
|
||||
|
||||
private string ReadFloatText(ushort registerAddress)
|
||||
{
|
||||
return _tcpDeviceConnectionService.TryReadFloat(registerAddress, out var value)
|
||||
? value.ToString("0.00", CultureInfo.InvariantCulture)
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private void WriteActionCoil(string action)
|
||||
{
|
||||
if (!TryGetActionCoil(action, out var coilAddress))
|
||||
{
|
||||
BaselineOxygenText = "20.95";
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == "标定开始")
|
||||
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
|
||||
{
|
||||
TemperatureText = "298.15";
|
||||
PressureDifferenceText = "12.40";
|
||||
CalibrationOxygenText = "20.10";
|
||||
CValueText = "0.045";
|
||||
Debug.WriteLine($"C value calibration action '{action}' write failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetActionCoil(string action, out ushort coilAddress)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "甲烷阀":
|
||||
coilAddress = 55;
|
||||
return true;
|
||||
case "风机":
|
||||
coilAddress = 54;
|
||||
return true;
|
||||
case "点火器":
|
||||
coilAddress = 53;
|
||||
return true;
|
||||
case "取样泵":
|
||||
coilAddress = 50;
|
||||
return true;
|
||||
case "基线采集":
|
||||
coilAddress = 60;
|
||||
return true;
|
||||
case "标定开始":
|
||||
coilAddress = 70;
|
||||
return true;
|
||||
default:
|
||||
coilAddress = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
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;
|
||||
@@ -10,21 +14,32 @@ namespace ConeCalorimeter.ViewModels;
|
||||
|
||||
public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
{
|
||||
private const ushort TargetTemperatureRegister = 400;
|
||||
private const ushort CurrentHeatFluxRegister = 410;
|
||||
private const ushort SlopeRegister = 420;
|
||||
private const ushort InterceptRegister = 422;
|
||||
|
||||
private readonly Action _closeAction;
|
||||
private readonly Action _helpAction;
|
||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||
private readonly DispatcherTimer _refreshTimer;
|
||||
private string _currentTemperatureText = "";
|
||||
private string _currentHeatFluxText = "";
|
||||
private string _targetTemperatureText = "355";
|
||||
private string _targetTemperatureText = "";
|
||||
private string _heatTransferText = "";
|
||||
private string _slopeText = "";
|
||||
private string _interceptText = "";
|
||||
private string _lastAction = "待机";
|
||||
private bool _alarmActive;
|
||||
|
||||
public ConeRadiationSettingsViewModel(Action closeAction, Action helpAction) : base("辐射锥设置")
|
||||
public ConeRadiationSettingsViewModel(
|
||||
Action closeAction,
|
||||
Action helpAction,
|
||||
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("辐射锥设置")
|
||||
{
|
||||
_closeAction = closeAction;
|
||||
_helpAction = helpAction;
|
||||
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||
CloseCommand = new RelayCommand(_closeAction);
|
||||
HelpCommand = new RelayCommand(_helpAction);
|
||||
ActionCommand = new RelayCommand<string>(ExecuteAction);
|
||||
@@ -40,6 +55,14 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
];
|
||||
|
||||
HeatFluxPlot = CreatePlotModel();
|
||||
|
||||
RefreshDeviceValues();
|
||||
_refreshTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
_refreshTimer.Tick += (_, _) => RefreshDeviceValues();
|
||||
_refreshTimer.Start();
|
||||
}
|
||||
|
||||
public ObservableCollection<ConeRadiationActionViewModel> CalibrationActions { get; }
|
||||
@@ -158,18 +181,89 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
||||
|
||||
if (action == "开始升温")
|
||||
{
|
||||
CurrentTemperatureText = TargetTemperatureText;
|
||||
CurrentHeatFluxText = "25.00";
|
||||
HeatTransferText = "0.42";
|
||||
SlopeText = "0.08";
|
||||
InterceptText = "1.20";
|
||||
WriteTargetTemperature();
|
||||
WriteActionCoil(action);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.EndsWith("标定", StringComparison.Ordinal))
|
||||
WriteActionCoil(action);
|
||||
}
|
||||
|
||||
private void RefreshDeviceValues()
|
||||
{
|
||||
CurrentHeatFluxText = action.Replace("KW标定", ".00", StringComparison.Ordinal);
|
||||
CurrentHeatFluxText = ReadFloatText(CurrentHeatFluxRegister);
|
||||
SlopeText = ReadFloatText(SlopeRegister);
|
||||
InterceptText = ReadFloatText(InterceptRegister);
|
||||
}
|
||||
|
||||
private string ReadFloatText(ushort registerAddress)
|
||||
{
|
||||
return _tcpDeviceConnectionService.TryReadFloat(registerAddress, out var value)
|
||||
? value.ToString("0.00", CultureInfo.InvariantCulture)
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private void WriteTargetTemperature()
|
||||
{
|
||||
if (!short.TryParse(TargetTemperatureText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
Debug.WriteLine($"Invalid cone radiation target temperature: {TargetTemperatureText}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_tcpDeviceConnectionService.TryWriteInt16(TargetTemperatureRegister, value))
|
||||
{
|
||||
Debug.WriteLine("Cone radiation target temperature write failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteActionCoil(string action)
|
||||
{
|
||||
if (!TryGetActionCoil(action, out var coilAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
|
||||
{
|
||||
Debug.WriteLine($"Cone radiation action '{action}' write failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetActionCoil(string action, out ushort coilAddress)
|
||||
{
|
||||
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;
|
||||
case "开始升温":
|
||||
coilAddress = 100;
|
||||
return true;
|
||||
case "停止升温":
|
||||
coilAddress = 101;
|
||||
return true;
|
||||
default:
|
||||
coilAddress = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,12 @@ public sealed class MainViewModel : ObservableObject
|
||||
|
||||
public MainViewModel(
|
||||
IExperimentDataService experimentDataService,
|
||||
ITcpDeviceConnectionService tcpDeviceConnectionService,
|
||||
IReportExportService reportExportService,
|
||||
IHelpDialogService helpDialogService)
|
||||
{
|
||||
_helpDialogService = helpDialogService;
|
||||
var testPage = new TestPageViewModel(experimentDataService);
|
||||
var testPage = new TestPageViewModel(experimentDataService, tcpDeviceConnectionService);
|
||||
var reportPage = new ReportPageViewModel(experimentDataService, reportExportService);
|
||||
NavigationItems = [];
|
||||
|
||||
@@ -25,13 +26,17 @@ public sealed class MainViewModel : ObservableObject
|
||||
"C值标定",
|
||||
new CValueCalibrationViewModel(
|
||||
() => SelectPage(testItem),
|
||||
ShowCValueCalibrationHelp));
|
||||
ShowCValueCalibrationHelp,
|
||||
tcpDeviceConnectionService));
|
||||
var coneRadiationItem = new NavigationItemViewModel(
|
||||
"辐射锥设置",
|
||||
new ConeRadiationSettingsViewModel(() => SelectPage(testItem), ShowConeRadiationHelp));
|
||||
new ConeRadiationSettingsViewModel(
|
||||
() => SelectPage(testItem),
|
||||
ShowConeRadiationHelp,
|
||||
tcpDeviceConnectionService));
|
||||
var smokeDensityItem = new NavigationItemViewModel(
|
||||
"烟密度设置",
|
||||
new SmokeDensitySettingsViewModel(() => SelectPage(testItem)));
|
||||
new SmokeDensitySettingsViewModel(() => SelectPage(testItem), tcpDeviceConnectionService));
|
||||
var realtimeDataItem = new NavigationItemViewModel(
|
||||
"实时数据",
|
||||
new RealtimeDataViewModel(experimentDataService, () => SelectPage(testItem)));
|
||||
|
||||
@@ -24,11 +24,23 @@ public sealed class MetricDisplayViewModel : ObservableObject
|
||||
|
||||
public void SetValue(double value, string format = "0.00")
|
||||
{
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
ValueText = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
ValueText = value.ToString(format);
|
||||
}
|
||||
|
||||
public void SetValue(int value)
|
||||
{
|
||||
if (value < 0)
|
||||
{
|
||||
ValueText = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
ValueText = value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class RealtimeDataRowViewModel
|
||||
|
||||
public RealtimeDataRowViewModel(RealtimeDataRecord record)
|
||||
{
|
||||
TimeText = record.TestSeconds.ToString(CultureInfo.InvariantCulture);
|
||||
TimeText = record.TestSeconds < 0 ? string.Empty : record.TestSeconds.ToString(CultureInfo.InvariantCulture);
|
||||
OxygenText = Format(record.Oxygen);
|
||||
CarbonDioxideText = Format(record.CarbonDioxide);
|
||||
CarbonMonoxideText = Format(record.CarbonMonoxide);
|
||||
@@ -25,7 +25,7 @@ public sealed class RealtimeDataRowViewModel
|
||||
HeatReleaseText = Format(record.HeatReleaseRate);
|
||||
EhcText = Format(record.EffectiveHeatOfCombustion);
|
||||
MassLossText = Format(record.MassLoss);
|
||||
SampleTemperatureText = Format(record.ConeTemperature, "0.0");
|
||||
SampleTemperatureText = Format(record.SampleTemperature, "0.0");
|
||||
}
|
||||
|
||||
public string TimeText { get; init; } = string.Empty;
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ConeCalorimeter.Services;
|
||||
|
||||
namespace ConeCalorimeter.ViewModels;
|
||||
|
||||
public sealed class SmokeDensitySettingsViewModel : PageViewModel
|
||||
{
|
||||
private readonly Action _closeAction;
|
||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||
private string _absorbanceText = "";
|
||||
private string _lastCalibration = "待校准";
|
||||
|
||||
public SmokeDensitySettingsViewModel(Action closeAction) : base("烟密度设置")
|
||||
public SmokeDensitySettingsViewModel(
|
||||
Action closeAction,
|
||||
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("烟密度设置")
|
||||
{
|
||||
_closeAction = closeAction;
|
||||
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||
CloseCommand = new RelayCommand(_closeAction);
|
||||
CalibrationCommand = new RelayCommand<string>(Calibrate);
|
||||
|
||||
@@ -52,6 +58,40 @@ public sealed class SmokeDensitySettingsViewModel : PageViewModel
|
||||
}
|
||||
|
||||
LastCalibration = label;
|
||||
AbsorbanceText = label.StartsWith("100%", StringComparison.Ordinal) ? "100.00" : "0.00";
|
||||
|
||||
if (!TryGetCalibrationCoil(label, out var coilAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
|
||||
{
|
||||
Debug.WriteLine($"Smoke density calibration '{label}' write failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetCalibrationCoil(string label, out ushort coilAddress)
|
||||
{
|
||||
switch (label)
|
||||
{
|
||||
case "0%校准":
|
||||
coilAddress = 28;
|
||||
return true;
|
||||
case "25%校准":
|
||||
coilAddress = 26;
|
||||
return true;
|
||||
case "50%校准":
|
||||
coilAddress = 24;
|
||||
return true;
|
||||
case "75%校准":
|
||||
coilAddress = 22;
|
||||
return true;
|
||||
case "100%校准":
|
||||
coilAddress = 20;
|
||||
return true;
|
||||
default:
|
||||
coilAddress = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ConeCalorimeter.Models;
|
||||
@@ -13,15 +14,19 @@ namespace ConeCalorimeter.ViewModels;
|
||||
public sealed class TestPageViewModel : PageViewModel
|
||||
{
|
||||
private readonly IExperimentDataService _experimentDataService;
|
||||
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||
private readonly LineSeries _heatReleaseSeries;
|
||||
private readonly LineSeries _totalHeatSeries;
|
||||
private readonly LineSeries _totalSmokeSeries;
|
||||
private bool _flameDetected;
|
||||
private string _lastAction = "待机";
|
||||
|
||||
public TestPageViewModel(IExperimentDataService experimentDataService) : base("测试界面")
|
||||
public TestPageViewModel(
|
||||
IExperimentDataService experimentDataService,
|
||||
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("测试界面")
|
||||
{
|
||||
_experimentDataService = experimentDataService;
|
||||
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||
|
||||
TopMetrics =
|
||||
[
|
||||
@@ -42,8 +47,8 @@ public sealed class TestPageViewModel : PageViewModel
|
||||
HeatMetrics =
|
||||
[
|
||||
new MetricDisplayViewModel("热释放速率", "kW/m²"),
|
||||
new MetricDisplayViewModel("qa(180)", "MJ/m²"),
|
||||
new MetricDisplayViewModel("qa(300)", "MJ/m²"),
|
||||
new MetricDisplayViewModel("热释放速率180", "MJ/m²"),
|
||||
new MetricDisplayViewModel("热释放速率300", "MJ/m²"),
|
||||
new MetricDisplayViewModel("放热总量", "MJ/m²"),
|
||||
new MetricDisplayViewModel("产烟量", "m³"),
|
||||
new MetricDisplayViewModel("当前质量", "g"),
|
||||
@@ -63,12 +68,12 @@ public sealed class TestPageViewModel : PageViewModel
|
||||
new DeviceActionViewModel("辐射锥降", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("称重台升", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("称重台降", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("复位", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("停止", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("测试", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("测试开始", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("测试结束", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("风机开", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("风机关", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("点火开", ExecuteDeviceActionCommand)
|
||||
new DeviceActionViewModel("点火关", ExecuteDeviceActionCommand),
|
||||
new DeviceActionViewModel("复位", ExecuteDeviceActionCommand)
|
||||
];
|
||||
|
||||
HeatReleasePlot = CreatePlotModel(out _heatReleaseSeries, out _totalHeatSeries, out _totalSmokeSeries);
|
||||
@@ -227,6 +232,11 @@ public sealed class TestPageViewModel : PageViewModel
|
||||
|
||||
private static void AppendPoint(LineSeries series, double x, double y)
|
||||
{
|
||||
if (!double.IsFinite(x) || x < 0 || !double.IsFinite(y))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
series.Points.Add(new DataPoint(x, y));
|
||||
|
||||
if (series.Points.Count > 600)
|
||||
@@ -237,9 +247,64 @@ public sealed class TestPageViewModel : PageViewModel
|
||||
|
||||
private void ExecuteDeviceAction(string? action)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(action))
|
||||
if (string.IsNullOrWhiteSpace(action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LastAction = action;
|
||||
|
||||
if (!TryGetDeviceActionCoil(action, out var coilAddress, out var value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, value))
|
||||
{
|
||||
Debug.WriteLine($"Device action '{action}' write failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetDeviceActionCoil(string action, out ushort coilAddress, out bool value)
|
||||
{
|
||||
value = true;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "称重台升":
|
||||
coilAddress = 93;
|
||||
return true;
|
||||
case "称重台降":
|
||||
coilAddress = 94;
|
||||
return true;
|
||||
case "辐射锥升":
|
||||
coilAddress = 83;
|
||||
return true;
|
||||
case "辐射锥降":
|
||||
coilAddress = 84;
|
||||
return true;
|
||||
case "复位":
|
||||
coilAddress = 88;
|
||||
return true;
|
||||
case "测试开始":
|
||||
coilAddress = 65;
|
||||
return true;
|
||||
case "测试结束":
|
||||
coilAddress = 67;
|
||||
return true;
|
||||
case "点火关":
|
||||
coilAddress = 53;
|
||||
return true;
|
||||
case "风机开":
|
||||
coilAddress = 54;
|
||||
return true;
|
||||
case "风机关":
|
||||
coilAddress = 54;
|
||||
value = false;
|
||||
return true;
|
||||
default:
|
||||
coilAddress = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,15 +238,24 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Row="2"
|
||||
Content="开始升温"
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,7,0,7">
|
||||
<Button Content="开始升温"
|
||||
Command="{Binding ActionCommand}"
|
||||
CommandParameter="开始升温"
|
||||
Width="150"
|
||||
Width="140"
|
||||
Height="42"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,7,0,7"
|
||||
Margin="0,0,10,0"
|
||||
Style="{StaticResource InstrumentPrimaryButtonStyle}" />
|
||||
<Button Content="停止升温"
|
||||
Command="{Binding ActionCommand}"
|
||||
CommandParameter="停止升温"
|
||||
Width="140"
|
||||
Height="42"
|
||||
Style="{StaticResource InstrumentButtonStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<ItemsControl Grid.Row="3"
|
||||
ItemsSource="{Binding CalibrationActions}"
|
||||
@@ -289,18 +298,18 @@
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding SlopeText, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource ValueInputBoxStyle}"
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding SlopeText}"
|
||||
Style="{StaticResource ValueBoxTextStyle}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3"
|
||||
Text="截距:"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="4"
|
||||
Text="{Binding InterceptText, UpdateSourceTrigger=PropertyChanged}"
|
||||
Style="{StaticResource ValueInputBoxStyle}"
|
||||
<TextBlock Grid.Column="4"
|
||||
Text="{Binding InterceptText}"
|
||||
Style="{StaticResource ValueBoxTextStyle}"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Grid.Column="6"
|
||||
Content="循环水"
|
||||
|
||||
Reference in New Issue
Block a user