更新20260616
This commit is contained in:
@@ -47,7 +47,7 @@ namespace FootwearTest
|
|||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddLogging(builder => builder.AddSerilog(dispose: false));
|
services.AddLogging(builder => builder.AddSerilog(dispose: false));
|
||||||
services.AddSingleton<DeviceSettings>();
|
services.AddSingleton(DeviceSettingsStore.Load());
|
||||||
services.AddSingleton<TestFormulaService>();
|
services.AddSingleton<TestFormulaService>();
|
||||||
services.AddSingleton<TestRunRepository>();
|
services.AddSingleton<TestRunRepository>();
|
||||||
services.AddSingleton<ReportService>();
|
services.AddSingleton<ReportService>();
|
||||||
|
|||||||
48
FootwearTest/Services/DeviceSettingsStore.cs
Normal file
48
FootwearTest/Services/DeviceSettingsStore.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace FootwearTest.Services;
|
||||||
|
|
||||||
|
public static class DeviceSettingsStore
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public static DeviceSettings Load()
|
||||||
|
{
|
||||||
|
var path = GetSettingsPath();
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return new DeviceSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return JsonSerializer.Deserialize<DeviceSettings>(json, JsonOptions) ?? new DeviceSettings();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new DeviceSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Save(DeviceSettings settings)
|
||||||
|
{
|
||||||
|
var path = GetSettingsPath();
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
var json = JsonSerializer.Serialize(settings, JsonOptions);
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetSettingsPath()
|
||||||
|
{
|
||||||
|
var folder = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"FootwearTest");
|
||||||
|
return Path.Combine(folder, "device-settings.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ public sealed class HybridDeviceClient : IDeviceClient
|
|||||||
private readonly ModbusTcpDeviceClient _plcClient;
|
private readonly ModbusTcpDeviceClient _plcClient;
|
||||||
private readonly ModbusRtuDeviceClient _instrumentClient;
|
private readonly ModbusRtuDeviceClient _instrumentClient;
|
||||||
private readonly ILogger<HybridDeviceClient> _logger;
|
private readonly ILogger<HybridDeviceClient> _logger;
|
||||||
|
private readonly SemaphoreSlim _communicationLock = new(1, 1);
|
||||||
|
|
||||||
public HybridDeviceClient(
|
public HybridDeviceClient(
|
||||||
ModbusTcpDeviceClient plcClient,
|
ModbusTcpDeviceClient plcClient,
|
||||||
@@ -33,41 +34,65 @@ public sealed class HybridDeviceClient : IDeviceClient
|
|||||||
|
|
||||||
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _plcClient.ConnectAsync(cancellationToken);
|
await _communicationLock.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _instrumentClient.ConnectAsync(cancellationToken);
|
await _plcClient.ConnectAsync(cancellationToken);
|
||||||
}
|
try
|
||||||
catch
|
{
|
||||||
{
|
await _instrumentClient.ConnectAsync(cancellationToken);
|
||||||
await _plcClient.DisconnectAsync(cancellationToken);
|
}
|
||||||
throw;
|
catch
|
||||||
}
|
{
|
||||||
|
await _plcClient.DisconnectAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
LastSnapshot = MergeSnapshots(_plcClient.LastSnapshot, _instrumentClient.LastSnapshot) with
|
LastSnapshot = MergeSnapshots(_plcClient.LastSnapshot, _instrumentClient.LastSnapshot) with
|
||||||
|
{
|
||||||
|
AlarmText = "PLC TCP 与 485 仪表已连接"
|
||||||
|
};
|
||||||
|
SnapshotUpdated?.Invoke(this, LastSnapshot);
|
||||||
|
_logger.LogInformation("Hybrid device communication connected. {ConnectionText}", ConnectionText);
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
AlarmText = "PLC TCP 与 485 仪表已连接"
|
_communicationLock.Release();
|
||||||
};
|
}
|
||||||
SnapshotUpdated?.Invoke(this, LastSnapshot);
|
|
||||||
_logger.LogInformation("Hybrid device communication connected. {ConnectionText}", ConnectionText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DisconnectAsync(CancellationToken cancellationToken = default)
|
public async Task DisconnectAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _instrumentClient.DisconnectAsync(cancellationToken);
|
await _communicationLock.WaitAsync(cancellationToken);
|
||||||
await _plcClient.DisconnectAsync(cancellationToken);
|
try
|
||||||
|
{
|
||||||
|
await _instrumentClient.DisconnectAsync(cancellationToken);
|
||||||
|
await _plcClient.DisconnectAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_communicationLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DeviceSnapshot> ReadSnapshotAsync(CancellationToken cancellationToken = default)
|
public async Task<DeviceSnapshot> ReadSnapshotAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var plcSnapshot = await _plcClient.ReadSnapshotAsync(cancellationToken);
|
await _communicationLock.WaitAsync(cancellationToken);
|
||||||
var instrumentSnapshot = await _instrumentClient.ReadSnapshotAsync(cancellationToken);
|
try
|
||||||
LastSnapshot = MergeSnapshots(plcSnapshot, instrumentSnapshot) with
|
|
||||||
{
|
{
|
||||||
AlarmText = "PLC TCP 与 485 仪表读取正常"
|
var plcSnapshot = await _plcClient.ReadSnapshotAsync(cancellationToken);
|
||||||
};
|
var instrumentSnapshot = await _instrumentClient.ReadSnapshotAsync(cancellationToken);
|
||||||
SnapshotUpdated?.Invoke(this, LastSnapshot);
|
LastSnapshot = MergeSnapshots(plcSnapshot, instrumentSnapshot) with
|
||||||
return LastSnapshot;
|
{
|
||||||
|
AlarmText = "PLC TCP 与 485 仪表读取正常"
|
||||||
|
};
|
||||||
|
SnapshotUpdated?.Invoke(this, LastSnapshot);
|
||||||
|
return LastSnapshot;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_communicationLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetOutputsAsync(
|
public async Task SetOutputsAsync(
|
||||||
@@ -76,16 +101,24 @@ public sealed class HybridDeviceClient : IDeviceClient
|
|||||||
bool heaterRunning,
|
bool heaterRunning,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _plcClient.SetOutputsAsync(pumpRunning, fanRunning, heaterRunning, cancellationToken);
|
await _communicationLock.WaitAsync(cancellationToken);
|
||||||
LastSnapshot = LastSnapshot with
|
try
|
||||||
{
|
{
|
||||||
Timestamp = DateTime.Now,
|
await _plcClient.SetOutputsAsync(pumpRunning, fanRunning, heaterRunning, cancellationToken);
|
||||||
PumpRunning = pumpRunning,
|
LastSnapshot = LastSnapshot with
|
||||||
FanRunning = fanRunning,
|
{
|
||||||
HeaterRunning = heaterRunning,
|
Timestamp = DateTime.Now,
|
||||||
AlarmText = "PLC TCP 输出写入正常"
|
PumpRunning = pumpRunning,
|
||||||
};
|
FanRunning = fanRunning,
|
||||||
SnapshotUpdated?.Invoke(this, LastSnapshot);
|
HeaterRunning = heaterRunning,
|
||||||
|
AlarmText = "PLC TCP 输出写入正常"
|
||||||
|
};
|
||||||
|
SnapshotUpdated?.Invoke(this, LastSnapshot);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_communicationLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DeviceSnapshot MergeSnapshots(DeviceSnapshot plcSnapshot, DeviceSnapshot instrumentSnapshot)
|
private static DeviceSnapshot MergeSnapshots(DeviceSnapshot plcSnapshot, DeviceSnapshot instrumentSnapshot)
|
||||||
|
|||||||
@@ -167,15 +167,7 @@ public sealed class ModbusTcpDeviceClient : IDeviceClient
|
|||||||
Span<byte> bytes = stackalloc byte[4];
|
Span<byte> bytes = stackalloc byte[4];
|
||||||
BinaryPrimitives.WriteUInt16BigEndian(bytes[..2], registers[0]);
|
BinaryPrimitives.WriteUInt16BigEndian(bytes[..2], registers[0]);
|
||||||
BinaryPrimitives.WriteUInt16BigEndian(bytes[2..], registers[1]);
|
BinaryPrimitives.WriteUInt16BigEndian(bytes[2..], registers[1]);
|
||||||
|
return BinaryPrimitives.ReadSingleLittleEndian(bytes);
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
{
|
|
||||||
return BitConverter.ToSingle(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = bytes.ToArray();
|
|
||||||
Array.Reverse(buffer);
|
|
||||||
return BitConverter.ToSingle(buffer, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ushort[]> ReadHoldingRegistersAsync(
|
private async Task<ushort[]> ReadHoldingRegistersAsync(
|
||||||
@@ -246,7 +238,7 @@ public sealed class ModbusTcpDeviceClient : IDeviceClient
|
|||||||
var responseTransactionId = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(0, 2));
|
var responseTransactionId = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(0, 2));
|
||||||
var protocolId = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(2, 2));
|
var protocolId = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(2, 2));
|
||||||
var length = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4, 2));
|
var length = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(4, 2));
|
||||||
if (responseTransactionId != transactionId || protocolId != 0 || length == 0)
|
if (responseTransactionId != transactionId || protocolId != 0 || length == 0 || header[6] != _settings.PlcSlaveId)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException("PLC Modbus TCP MBAP 响应头不正确。");
|
throw new InvalidDataException("PLC Modbus TCP MBAP 响应头不正确。");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,8 @@ public partial class SettingsViewModel : ViewModelBase
|
|||||||
_settings.PumpCoilAddress = PumpCoilAddress;
|
_settings.PumpCoilAddress = PumpCoilAddress;
|
||||||
_settings.FanCoilAddress = FanCoilAddress;
|
_settings.FanCoilAddress = FanCoilAddress;
|
||||||
_settings.HeaterCoilAddress = HeaterCoilAddress;
|
_settings.HeaterCoilAddress = HeaterCoilAddress;
|
||||||
SaveStatus = "参数已保存,新的试验流程将使用当前设置";
|
DeviceSettingsStore.Save(_settings);
|
||||||
|
SaveStatus = "参数已保存,重新连接设备后生效";
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Settings saved. UseSimulator={UseSimulator}, PlcHost={PlcHost}, PlcPort={PlcPort}, SerialPort={SerialPort}, BaudRate={BaudRate}, FootAreaSquareMeters={FootArea}, PumpSpeed={PumpSpeed}, FootTemperatureRegister={FootTemperatureRegister}, VoltageRegister={VoltageRegister}, VoltageDecimalPointRegister={VoltageDecimalPointRegister}, CurrentRegister={CurrentRegister}, CurrentDecimalPointRegister={CurrentDecimalPointRegister}, BalanceRegister={BalanceRegister}, BalanceDecimalPointRegister={BalanceDecimalPointRegister}",
|
"Settings saved. UseSimulator={UseSimulator}, PlcHost={PlcHost}, PlcPort={PlcPort}, SerialPort={SerialPort}, BaudRate={BaudRate}, FootAreaSquareMeters={FootArea}, PumpSpeed={PumpSpeed}, FootTemperatureRegister={FootTemperatureRegister}, VoltageRegister={VoltageRegister}, VoltageDecimalPointRegister={VoltageDecimalPointRegister}, CurrentRegister={CurrentRegister}, CurrentDecimalPointRegister={CurrentDecimalPointRegister}, BalanceRegister={BalanceRegister}, BalanceDecimalPointRegister={BalanceDecimalPointRegister}",
|
||||||
UseSimulator,
|
UseSimulator,
|
||||||
|
|||||||
Reference in New Issue
Block a user