更新点位
This commit is contained in:
@@ -15,9 +15,11 @@ namespace ConeCalorimeter
|
|||||||
_tcpDeviceConnectionService = new TcpDeviceConnectionService();
|
_tcpDeviceConnectionService = new TcpDeviceConnectionService();
|
||||||
_ = _tcpDeviceConnectionService.StartAsync();
|
_ = _tcpDeviceConnectionService.StartAsync();
|
||||||
|
|
||||||
var experimentDataService = new ExperimentDataService(new DemoRealtimeDataService());
|
var experimentDataService = new ExperimentDataService(
|
||||||
|
new ModbusRealtimeDataService(_tcpDeviceConnectionService));
|
||||||
DataContext = new MainViewModel(
|
DataContext = new MainViewModel(
|
||||||
experimentDataService,
|
experimentDataService,
|
||||||
|
_tcpDeviceConnectionService,
|
||||||
new NpoiReportExportService(),
|
new NpoiReportExportService(),
|
||||||
new HelpDialogService());
|
new HelpDialogService());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ public sealed record RealtimeDataRecord(
|
|||||||
double OrificePressure,
|
double OrificePressure,
|
||||||
double OrificeTemperature,
|
double OrificeTemperature,
|
||||||
double ConeTemperature,
|
double ConeTemperature,
|
||||||
|
double SampleTemperature,
|
||||||
double Irradiance,
|
double Irradiance,
|
||||||
bool FlameDetected,
|
bool FlameDetected,
|
||||||
double Oxygen,
|
double Oxygen,
|
||||||
@@ -30,6 +31,7 @@ public sealed record RealtimeDataRecord(
|
|||||||
snapshot.OrificePressure,
|
snapshot.OrificePressure,
|
||||||
snapshot.OrificeTemperature,
|
snapshot.OrificeTemperature,
|
||||||
snapshot.ConeTemperature,
|
snapshot.ConeTemperature,
|
||||||
|
snapshot.SampleTemperature,
|
||||||
snapshot.Irradiance,
|
snapshot.Irradiance,
|
||||||
snapshot.FlameDetected,
|
snapshot.FlameDetected,
|
||||||
snapshot.Oxygen,
|
snapshot.Oxygen,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public sealed record RealtimeSnapshot(
|
|||||||
double OrificePressure,
|
double OrificePressure,
|
||||||
double OrificeTemperature,
|
double OrificeTemperature,
|
||||||
double ConeTemperature,
|
double ConeTemperature,
|
||||||
|
double SampleTemperature,
|
||||||
double Irradiance,
|
double Irradiance,
|
||||||
bool FlameDetected,
|
bool FlameDetected,
|
||||||
double Oxygen,
|
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 StartAsync();
|
||||||
|
|
||||||
Task StopAsync();
|
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.Diagnostics;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.IO;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
|
||||||
namespace ConeCalorimeter.Services;
|
namespace ConeCalorimeter.Services;
|
||||||
@@ -7,13 +9,20 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
|||||||
{
|
{
|
||||||
private const string Host = "192.168.1.10";
|
private const string Host = "192.168.1.10";
|
||||||
private const int Port = 502;
|
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 RetryDelay = TimeSpan.FromSeconds(3);
|
||||||
private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5);
|
private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5);
|
||||||
|
private static readonly TimeSpan ReadWriteTimeout = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
private readonly object _syncRoot = new();
|
private readonly object _syncRoot = new();
|
||||||
private CancellationTokenSource? _connectionLoopCancellation;
|
private CancellationTokenSource? _connectionLoopCancellation;
|
||||||
private Task? _connectionLoopTask;
|
private Task? _connectionLoopTask;
|
||||||
private TcpClient? _client;
|
private TcpClient? _client;
|
||||||
|
private ushort _transactionId;
|
||||||
private bool _isConnected;
|
private bool _isConnected;
|
||||||
|
|
||||||
public bool IsConnected
|
public bool IsConnected
|
||||||
@@ -90,6 +99,132 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
|||||||
await StopAsync();
|
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)
|
private async Task RunConnectionLoopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
@@ -138,9 +273,12 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await connectTask;
|
await connectTask;
|
||||||
|
client.ReceiveTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
|
||||||
|
client.SendTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
|
||||||
|
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
{
|
{
|
||||||
|
_client?.Dispose();
|
||||||
_client = client;
|
_client = client;
|
||||||
_isConnected = true;
|
_isConnected = true;
|
||||||
}
|
}
|
||||||
@@ -187,15 +325,159 @@ public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
|
|||||||
|
|
||||||
private void CloseCurrentClient()
|
private void CloseCurrentClient()
|
||||||
{
|
{
|
||||||
TcpClient? client;
|
|
||||||
|
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
{
|
{
|
||||||
client = _client;
|
CloseCurrentClientCore();
|
||||||
_client = null;
|
}
|
||||||
_isConnected = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CloseCurrentClientCore()
|
||||||
|
{
|
||||||
|
var client = _client;
|
||||||
|
_client = null;
|
||||||
|
_isConnected = false;
|
||||||
client?.Dispose();
|
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.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ConeCalorimeter.Services;
|
||||||
|
|
||||||
namespace ConeCalorimeter.ViewModels;
|
namespace ConeCalorimeter.ViewModels;
|
||||||
|
|
||||||
public sealed class CValueCalibrationViewModel : PageViewModel
|
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 _closeAction;
|
||||||
private readonly Action _helpAction;
|
private readonly Action _helpAction;
|
||||||
|
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||||
|
private readonly DispatcherTimer _refreshTimer;
|
||||||
private string _baselineOxygenText = "";
|
private string _baselineOxygenText = "";
|
||||||
private string _temperatureText = "";
|
private string _temperatureText = "";
|
||||||
private string _pressureDifferenceText = "";
|
private string _pressureDifferenceText = "";
|
||||||
@@ -15,10 +26,14 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
|||||||
private string _cValueText = "";
|
private string _cValueText = "";
|
||||||
private string _lastAction = "待机";
|
private string _lastAction = "待机";
|
||||||
|
|
||||||
public CValueCalibrationViewModel(Action closeAction, Action helpAction) : base("C值标定")
|
public CValueCalibrationViewModel(
|
||||||
|
Action closeAction,
|
||||||
|
Action helpAction,
|
||||||
|
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("C值标定")
|
||||||
{
|
{
|
||||||
_closeAction = closeAction;
|
_closeAction = closeAction;
|
||||||
_helpAction = helpAction;
|
_helpAction = helpAction;
|
||||||
|
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||||
CloseCommand = new RelayCommand(_closeAction);
|
CloseCommand = new RelayCommand(_closeAction);
|
||||||
HelpCommand = new RelayCommand(_helpAction);
|
HelpCommand = new RelayCommand(_helpAction);
|
||||||
ActionCommand = new RelayCommand<string>(ExecuteAction);
|
ActionCommand = new RelayCommand<string>(ExecuteAction);
|
||||||
@@ -36,6 +51,14 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
|||||||
new CValueCalibrationActionViewModel("标定开始", ActionCommand),
|
new CValueCalibrationActionViewModel("标定开始", ActionCommand),
|
||||||
new CValueCalibrationActionViewModel("基线采集", ActionCommand)
|
new CValueCalibrationActionViewModel("基线采集", ActionCommand)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
RefreshDeviceValues();
|
||||||
|
_refreshTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_refreshTimer.Tick += (_, _) => RefreshDeviceValues();
|
||||||
|
_refreshTimer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<CValueCalibrationActionViewModel> TopActions { get; }
|
public ObservableCollection<CValueCalibrationActionViewModel> TopActions { get; }
|
||||||
@@ -94,19 +117,65 @@ public sealed class CValueCalibrationViewModel : PageViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
LastAction = action;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action == "标定开始")
|
if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
|
||||||
{
|
{
|
||||||
TemperatureText = "298.15";
|
Debug.WriteLine($"C value calibration action '{action}' write failed.");
|
||||||
PressureDifferenceText = "12.40";
|
}
|
||||||
CalibrationOxygenText = "20.10";
|
}
|
||||||
CValueText = "0.045";
|
|
||||||
|
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.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ConeCalorimeter.Services;
|
||||||
using OxyPlot;
|
using OxyPlot;
|
||||||
using OxyPlot.Axes;
|
using OxyPlot.Axes;
|
||||||
using OxyPlot.Legends;
|
using OxyPlot.Legends;
|
||||||
@@ -10,21 +14,32 @@ namespace ConeCalorimeter.ViewModels;
|
|||||||
|
|
||||||
public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
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 _closeAction;
|
||||||
private readonly Action _helpAction;
|
private readonly Action _helpAction;
|
||||||
|
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||||
|
private readonly DispatcherTimer _refreshTimer;
|
||||||
private string _currentTemperatureText = "";
|
private string _currentTemperatureText = "";
|
||||||
private string _currentHeatFluxText = "";
|
private string _currentHeatFluxText = "";
|
||||||
private string _targetTemperatureText = "355";
|
private string _targetTemperatureText = "";
|
||||||
private string _heatTransferText = "";
|
private string _heatTransferText = "";
|
||||||
private string _slopeText = "";
|
private string _slopeText = "";
|
||||||
private string _interceptText = "";
|
private string _interceptText = "";
|
||||||
private string _lastAction = "待机";
|
private string _lastAction = "待机";
|
||||||
private bool _alarmActive;
|
private bool _alarmActive;
|
||||||
|
|
||||||
public ConeRadiationSettingsViewModel(Action closeAction, Action helpAction) : base("辐射锥设置")
|
public ConeRadiationSettingsViewModel(
|
||||||
|
Action closeAction,
|
||||||
|
Action helpAction,
|
||||||
|
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("辐射锥设置")
|
||||||
{
|
{
|
||||||
_closeAction = closeAction;
|
_closeAction = closeAction;
|
||||||
_helpAction = helpAction;
|
_helpAction = helpAction;
|
||||||
|
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||||
CloseCommand = new RelayCommand(_closeAction);
|
CloseCommand = new RelayCommand(_closeAction);
|
||||||
HelpCommand = new RelayCommand(_helpAction);
|
HelpCommand = new RelayCommand(_helpAction);
|
||||||
ActionCommand = new RelayCommand<string>(ExecuteAction);
|
ActionCommand = new RelayCommand<string>(ExecuteAction);
|
||||||
@@ -40,6 +55,14 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
];
|
];
|
||||||
|
|
||||||
HeatFluxPlot = CreatePlotModel();
|
HeatFluxPlot = CreatePlotModel();
|
||||||
|
|
||||||
|
RefreshDeviceValues();
|
||||||
|
_refreshTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
_refreshTimer.Tick += (_, _) => RefreshDeviceValues();
|
||||||
|
_refreshTimer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<ConeRadiationActionViewModel> CalibrationActions { get; }
|
public ObservableCollection<ConeRadiationActionViewModel> CalibrationActions { get; }
|
||||||
@@ -158,18 +181,89 @@ public sealed class ConeRadiationSettingsViewModel : PageViewModel
|
|||||||
|
|
||||||
if (action == "开始升温")
|
if (action == "开始升温")
|
||||||
{
|
{
|
||||||
CurrentTemperatureText = TargetTemperatureText;
|
WriteTargetTemperature();
|
||||||
CurrentHeatFluxText = "25.00";
|
WriteActionCoil(action);
|
||||||
HeatTransferText = "0.42";
|
|
||||||
SlopeText = "0.08";
|
|
||||||
InterceptText = "1.20";
|
|
||||||
return;
|
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;
|
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(
|
public MainViewModel(
|
||||||
IExperimentDataService experimentDataService,
|
IExperimentDataService experimentDataService,
|
||||||
|
ITcpDeviceConnectionService tcpDeviceConnectionService,
|
||||||
IReportExportService reportExportService,
|
IReportExportService reportExportService,
|
||||||
IHelpDialogService helpDialogService)
|
IHelpDialogService helpDialogService)
|
||||||
{
|
{
|
||||||
_helpDialogService = helpDialogService;
|
_helpDialogService = helpDialogService;
|
||||||
var testPage = new TestPageViewModel(experimentDataService);
|
var testPage = new TestPageViewModel(experimentDataService, tcpDeviceConnectionService);
|
||||||
var reportPage = new ReportPageViewModel(experimentDataService, reportExportService);
|
var reportPage = new ReportPageViewModel(experimentDataService, reportExportService);
|
||||||
NavigationItems = [];
|
NavigationItems = [];
|
||||||
|
|
||||||
@@ -25,13 +26,17 @@ public sealed class MainViewModel : ObservableObject
|
|||||||
"C值标定",
|
"C值标定",
|
||||||
new CValueCalibrationViewModel(
|
new CValueCalibrationViewModel(
|
||||||
() => SelectPage(testItem),
|
() => SelectPage(testItem),
|
||||||
ShowCValueCalibrationHelp));
|
ShowCValueCalibrationHelp,
|
||||||
|
tcpDeviceConnectionService));
|
||||||
var coneRadiationItem = new NavigationItemViewModel(
|
var coneRadiationItem = new NavigationItemViewModel(
|
||||||
"辐射锥设置",
|
"辐射锥设置",
|
||||||
new ConeRadiationSettingsViewModel(() => SelectPage(testItem), ShowConeRadiationHelp));
|
new ConeRadiationSettingsViewModel(
|
||||||
|
() => SelectPage(testItem),
|
||||||
|
ShowConeRadiationHelp,
|
||||||
|
tcpDeviceConnectionService));
|
||||||
var smokeDensityItem = new NavigationItemViewModel(
|
var smokeDensityItem = new NavigationItemViewModel(
|
||||||
"烟密度设置",
|
"烟密度设置",
|
||||||
new SmokeDensitySettingsViewModel(() => SelectPage(testItem)));
|
new SmokeDensitySettingsViewModel(() => SelectPage(testItem), tcpDeviceConnectionService));
|
||||||
var realtimeDataItem = new NavigationItemViewModel(
|
var realtimeDataItem = new NavigationItemViewModel(
|
||||||
"实时数据",
|
"实时数据",
|
||||||
new RealtimeDataViewModel(experimentDataService, () => SelectPage(testItem)));
|
new RealtimeDataViewModel(experimentDataService, () => SelectPage(testItem)));
|
||||||
|
|||||||
@@ -24,11 +24,23 @@ public sealed class MetricDisplayViewModel : ObservableObject
|
|||||||
|
|
||||||
public void SetValue(double value, string format = "0.00")
|
public void SetValue(double value, string format = "0.00")
|
||||||
{
|
{
|
||||||
|
if (!double.IsFinite(value))
|
||||||
|
{
|
||||||
|
ValueText = string.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ValueText = value.ToString(format);
|
ValueText = value.ToString(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetValue(int value)
|
public void SetValue(int value)
|
||||||
{
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
ValueText = string.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ValueText = value.ToString();
|
ValueText = value.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public sealed class RealtimeDataRowViewModel
|
|||||||
|
|
||||||
public RealtimeDataRowViewModel(RealtimeDataRecord record)
|
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);
|
OxygenText = Format(record.Oxygen);
|
||||||
CarbonDioxideText = Format(record.CarbonDioxide);
|
CarbonDioxideText = Format(record.CarbonDioxide);
|
||||||
CarbonMonoxideText = Format(record.CarbonMonoxide);
|
CarbonMonoxideText = Format(record.CarbonMonoxide);
|
||||||
@@ -25,7 +25,7 @@ public sealed class RealtimeDataRowViewModel
|
|||||||
HeatReleaseText = Format(record.HeatReleaseRate);
|
HeatReleaseText = Format(record.HeatReleaseRate);
|
||||||
EhcText = Format(record.EffectiveHeatOfCombustion);
|
EhcText = Format(record.EffectiveHeatOfCombustion);
|
||||||
MassLossText = Format(record.MassLoss);
|
MassLossText = Format(record.MassLoss);
|
||||||
SampleTemperatureText = Format(record.ConeTemperature, "0.0");
|
SampleTemperatureText = Format(record.SampleTemperature, "0.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string TimeText { get; init; } = string.Empty;
|
public string TimeText { get; init; } = string.Empty;
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ConeCalorimeter.Services;
|
||||||
|
|
||||||
namespace ConeCalorimeter.ViewModels;
|
namespace ConeCalorimeter.ViewModels;
|
||||||
|
|
||||||
public sealed class SmokeDensitySettingsViewModel : PageViewModel
|
public sealed class SmokeDensitySettingsViewModel : PageViewModel
|
||||||
{
|
{
|
||||||
private readonly Action _closeAction;
|
private readonly Action _closeAction;
|
||||||
|
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||||
private string _absorbanceText = "";
|
private string _absorbanceText = "";
|
||||||
private string _lastCalibration = "待校准";
|
private string _lastCalibration = "待校准";
|
||||||
|
|
||||||
public SmokeDensitySettingsViewModel(Action closeAction) : base("烟密度设置")
|
public SmokeDensitySettingsViewModel(
|
||||||
|
Action closeAction,
|
||||||
|
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("烟密度设置")
|
||||||
{
|
{
|
||||||
_closeAction = closeAction;
|
_closeAction = closeAction;
|
||||||
|
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||||
CloseCommand = new RelayCommand(_closeAction);
|
CloseCommand = new RelayCommand(_closeAction);
|
||||||
CalibrationCommand = new RelayCommand<string>(Calibrate);
|
CalibrationCommand = new RelayCommand<string>(Calibrate);
|
||||||
|
|
||||||
@@ -52,6 +58,40 @@ public sealed class SmokeDensitySettingsViewModel : PageViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
LastCalibration = label;
|
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.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ConeCalorimeter.Models;
|
using ConeCalorimeter.Models;
|
||||||
@@ -13,15 +14,19 @@ namespace ConeCalorimeter.ViewModels;
|
|||||||
public sealed class TestPageViewModel : PageViewModel
|
public sealed class TestPageViewModel : PageViewModel
|
||||||
{
|
{
|
||||||
private readonly IExperimentDataService _experimentDataService;
|
private readonly IExperimentDataService _experimentDataService;
|
||||||
|
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
|
||||||
private readonly LineSeries _heatReleaseSeries;
|
private readonly LineSeries _heatReleaseSeries;
|
||||||
private readonly LineSeries _totalHeatSeries;
|
private readonly LineSeries _totalHeatSeries;
|
||||||
private readonly LineSeries _totalSmokeSeries;
|
private readonly LineSeries _totalSmokeSeries;
|
||||||
private bool _flameDetected;
|
private bool _flameDetected;
|
||||||
private string _lastAction = "待机";
|
private string _lastAction = "待机";
|
||||||
|
|
||||||
public TestPageViewModel(IExperimentDataService experimentDataService) : base("测试界面")
|
public TestPageViewModel(
|
||||||
|
IExperimentDataService experimentDataService,
|
||||||
|
ITcpDeviceConnectionService tcpDeviceConnectionService) : base("测试界面")
|
||||||
{
|
{
|
||||||
_experimentDataService = experimentDataService;
|
_experimentDataService = experimentDataService;
|
||||||
|
_tcpDeviceConnectionService = tcpDeviceConnectionService;
|
||||||
|
|
||||||
TopMetrics =
|
TopMetrics =
|
||||||
[
|
[
|
||||||
@@ -42,8 +47,8 @@ public sealed class TestPageViewModel : PageViewModel
|
|||||||
HeatMetrics =
|
HeatMetrics =
|
||||||
[
|
[
|
||||||
new MetricDisplayViewModel("热释放速率", "kW/m²"),
|
new MetricDisplayViewModel("热释放速率", "kW/m²"),
|
||||||
new MetricDisplayViewModel("qa(180)", "MJ/m²"),
|
new MetricDisplayViewModel("热释放速率180", "MJ/m²"),
|
||||||
new MetricDisplayViewModel("qa(300)", "MJ/m²"),
|
new MetricDisplayViewModel("热释放速率300", "MJ/m²"),
|
||||||
new MetricDisplayViewModel("放热总量", "MJ/m²"),
|
new MetricDisplayViewModel("放热总量", "MJ/m²"),
|
||||||
new MetricDisplayViewModel("产烟量", "m³"),
|
new MetricDisplayViewModel("产烟量", "m³"),
|
||||||
new MetricDisplayViewModel("当前质量", "g"),
|
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),
|
||||||
new DeviceActionViewModel("风机关", ExecuteDeviceActionCommand),
|
new DeviceActionViewModel("风机关", ExecuteDeviceActionCommand),
|
||||||
new DeviceActionViewModel("点火开", ExecuteDeviceActionCommand)
|
new DeviceActionViewModel("点火关", ExecuteDeviceActionCommand),
|
||||||
|
new DeviceActionViewModel("复位", ExecuteDeviceActionCommand)
|
||||||
];
|
];
|
||||||
|
|
||||||
HeatReleasePlot = CreatePlotModel(out _heatReleaseSeries, out _totalHeatSeries, out _totalSmokeSeries);
|
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)
|
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));
|
series.Points.Add(new DataPoint(x, y));
|
||||||
|
|
||||||
if (series.Points.Count > 600)
|
if (series.Points.Count > 600)
|
||||||
@@ -237,9 +247,64 @@ public sealed class TestPageViewModel : PageViewModel
|
|||||||
|
|
||||||
private void ExecuteDeviceAction(string? action)
|
private void ExecuteDeviceAction(string? action)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(action))
|
if (string.IsNullOrWhiteSpace(action))
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LastAction = action;
|
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>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Grid.Row="2"
|
<StackPanel Grid.Row="2"
|
||||||
Content="开始升温"
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,7,0,7">
|
||||||
|
<Button Content="开始升温"
|
||||||
Command="{Binding ActionCommand}"
|
Command="{Binding ActionCommand}"
|
||||||
CommandParameter="开始升温"
|
CommandParameter="开始升温"
|
||||||
Width="150"
|
Width="140"
|
||||||
Height="42"
|
Height="42"
|
||||||
HorizontalAlignment="Center"
|
Margin="0,0,10,0"
|
||||||
Margin="0,7,0,7"
|
|
||||||
Style="{StaticResource InstrumentPrimaryButtonStyle}" />
|
Style="{StaticResource InstrumentPrimaryButtonStyle}" />
|
||||||
|
<Button Content="停止升温"
|
||||||
|
Command="{Binding ActionCommand}"
|
||||||
|
CommandParameter="停止升温"
|
||||||
|
Width="140"
|
||||||
|
Height="42"
|
||||||
|
Style="{StaticResource InstrumentButtonStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<ItemsControl Grid.Row="3"
|
<ItemsControl Grid.Row="3"
|
||||||
ItemsSource="{Binding CalibrationActions}"
|
ItemsSource="{Binding CalibrationActions}"
|
||||||
@@ -289,18 +298,18 @@
|
|||||||
FontSize="24"
|
FontSize="24"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
Text="{Binding SlopeText, UpdateSourceTrigger=PropertyChanged}"
|
Text="{Binding SlopeText}"
|
||||||
Style="{StaticResource ValueInputBoxStyle}"
|
Style="{StaticResource ValueBoxTextStyle}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<TextBlock Grid.Column="3"
|
<TextBlock Grid.Column="3"
|
||||||
Text="截距:"
|
Text="截距:"
|
||||||
FontSize="24"
|
FontSize="24"
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="4"
|
<TextBlock Grid.Column="4"
|
||||||
Text="{Binding InterceptText, UpdateSourceTrigger=PropertyChanged}"
|
Text="{Binding InterceptText}"
|
||||||
Style="{StaticResource ValueInputBoxStyle}"
|
Style="{StaticResource ValueBoxTextStyle}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<Button Grid.Column="6"
|
<Button Grid.Column="6"
|
||||||
Content="循环水"
|
Content="循环水"
|
||||||
|
|||||||
Reference in New Issue
Block a user