From 1ffd92ce588457b397a915b0814d106cb81b551e Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Tue, 16 Jun 2026 18:06:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B020260616?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FootwearTest/App.axaml.cs | 2 +- FootwearTest/Services/DeviceSettingsStore.cs | 48 ++++++++++ FootwearTest/Services/HybridDeviceClient.cs | 95 +++++++++++++------ .../Services/ModbusTcpDeviceClient.cs | 12 +-- FootwearTest/ViewModels/SettingsViewModel.cs | 3 +- 5 files changed, 117 insertions(+), 43 deletions(-) create mode 100644 FootwearTest/Services/DeviceSettingsStore.cs diff --git a/FootwearTest/App.axaml.cs b/FootwearTest/App.axaml.cs index 517ac7c..66fff61 100644 --- a/FootwearTest/App.axaml.cs +++ b/FootwearTest/App.axaml.cs @@ -47,7 +47,7 @@ namespace FootwearTest { var services = new ServiceCollection(); services.AddLogging(builder => builder.AddSerilog(dispose: false)); - services.AddSingleton(); + services.AddSingleton(DeviceSettingsStore.Load()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/FootwearTest/Services/DeviceSettingsStore.cs b/FootwearTest/Services/DeviceSettingsStore.cs new file mode 100644 index 0000000..6ff25c4 --- /dev/null +++ b/FootwearTest/Services/DeviceSettingsStore.cs @@ -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(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"); + } +} diff --git a/FootwearTest/Services/HybridDeviceClient.cs b/FootwearTest/Services/HybridDeviceClient.cs index ceebaee..877d306 100644 --- a/FootwearTest/Services/HybridDeviceClient.cs +++ b/FootwearTest/Services/HybridDeviceClient.cs @@ -11,6 +11,7 @@ public sealed class HybridDeviceClient : IDeviceClient private readonly ModbusTcpDeviceClient _plcClient; private readonly ModbusRtuDeviceClient _instrumentClient; private readonly ILogger _logger; + private readonly SemaphoreSlim _communicationLock = new(1, 1); public HybridDeviceClient( ModbusTcpDeviceClient plcClient, @@ -33,41 +34,65 @@ public sealed class HybridDeviceClient : IDeviceClient public async Task ConnectAsync(CancellationToken cancellationToken = default) { - await _plcClient.ConnectAsync(cancellationToken); + await _communicationLock.WaitAsync(cancellationToken); try { - await _instrumentClient.ConnectAsync(cancellationToken); - } - catch - { - await _plcClient.DisconnectAsync(cancellationToken); - throw; - } + await _plcClient.ConnectAsync(cancellationToken); + try + { + await _instrumentClient.ConnectAsync(cancellationToken); + } + 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 仪表已连接" - }; - SnapshotUpdated?.Invoke(this, LastSnapshot); - _logger.LogInformation("Hybrid device communication connected. {ConnectionText}", ConnectionText); + _communicationLock.Release(); + } } public async Task DisconnectAsync(CancellationToken cancellationToken = default) { - await _instrumentClient.DisconnectAsync(cancellationToken); - await _plcClient.DisconnectAsync(cancellationToken); + await _communicationLock.WaitAsync(cancellationToken); + try + { + await _instrumentClient.DisconnectAsync(cancellationToken); + await _plcClient.DisconnectAsync(cancellationToken); + } + finally + { + _communicationLock.Release(); + } } public async Task ReadSnapshotAsync(CancellationToken cancellationToken = default) { - var plcSnapshot = await _plcClient.ReadSnapshotAsync(cancellationToken); - var instrumentSnapshot = await _instrumentClient.ReadSnapshotAsync(cancellationToken); - LastSnapshot = MergeSnapshots(plcSnapshot, instrumentSnapshot) with + await _communicationLock.WaitAsync(cancellationToken); + try { - AlarmText = "PLC TCP 与 485 仪表读取正常" - }; - SnapshotUpdated?.Invoke(this, LastSnapshot); - return LastSnapshot; + var plcSnapshot = await _plcClient.ReadSnapshotAsync(cancellationToken); + var instrumentSnapshot = await _instrumentClient.ReadSnapshotAsync(cancellationToken); + LastSnapshot = MergeSnapshots(plcSnapshot, instrumentSnapshot) with + { + AlarmText = "PLC TCP 与 485 仪表读取正常" + }; + SnapshotUpdated?.Invoke(this, LastSnapshot); + return LastSnapshot; + } + finally + { + _communicationLock.Release(); + } } public async Task SetOutputsAsync( @@ -76,16 +101,24 @@ public sealed class HybridDeviceClient : IDeviceClient bool heaterRunning, CancellationToken cancellationToken = default) { - await _plcClient.SetOutputsAsync(pumpRunning, fanRunning, heaterRunning, cancellationToken); - LastSnapshot = LastSnapshot with + await _communicationLock.WaitAsync(cancellationToken); + try { - Timestamp = DateTime.Now, - PumpRunning = pumpRunning, - FanRunning = fanRunning, - HeaterRunning = heaterRunning, - AlarmText = "PLC TCP 输出写入正常" - }; - SnapshotUpdated?.Invoke(this, LastSnapshot); + await _plcClient.SetOutputsAsync(pumpRunning, fanRunning, heaterRunning, cancellationToken); + LastSnapshot = LastSnapshot with + { + Timestamp = DateTime.Now, + PumpRunning = pumpRunning, + FanRunning = fanRunning, + HeaterRunning = heaterRunning, + AlarmText = "PLC TCP 输出写入正常" + }; + SnapshotUpdated?.Invoke(this, LastSnapshot); + } + finally + { + _communicationLock.Release(); + } } private static DeviceSnapshot MergeSnapshots(DeviceSnapshot plcSnapshot, DeviceSnapshot instrumentSnapshot) diff --git a/FootwearTest/Services/ModbusTcpDeviceClient.cs b/FootwearTest/Services/ModbusTcpDeviceClient.cs index 38a3c9b..3ec19d5 100644 --- a/FootwearTest/Services/ModbusTcpDeviceClient.cs +++ b/FootwearTest/Services/ModbusTcpDeviceClient.cs @@ -167,15 +167,7 @@ public sealed class ModbusTcpDeviceClient : IDeviceClient Span bytes = stackalloc byte[4]; BinaryPrimitives.WriteUInt16BigEndian(bytes[..2], registers[0]); BinaryPrimitives.WriteUInt16BigEndian(bytes[2..], registers[1]); - - if (BitConverter.IsLittleEndian) - { - return BitConverter.ToSingle(bytes); - } - - var buffer = bytes.ToArray(); - Array.Reverse(buffer); - return BitConverter.ToSingle(buffer, 0); + return BinaryPrimitives.ReadSingleLittleEndian(bytes); } private async Task ReadHoldingRegistersAsync( @@ -246,7 +238,7 @@ public sealed class ModbusTcpDeviceClient : IDeviceClient var responseTransactionId = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(0, 2)); var protocolId = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(2, 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 响应头不正确。"); } diff --git a/FootwearTest/ViewModels/SettingsViewModel.cs b/FootwearTest/ViewModels/SettingsViewModel.cs index 244d44c..303f7a6 100644 --- a/FootwearTest/ViewModels/SettingsViewModel.cs +++ b/FootwearTest/ViewModels/SettingsViewModel.cs @@ -152,7 +152,8 @@ public partial class SettingsViewModel : ViewModelBase _settings.PumpCoilAddress = PumpCoilAddress; _settings.FanCoilAddress = FanCoilAddress; _settings.HeaterCoilAddress = HeaterCoilAddress; - SaveStatus = "参数已保存,新的试验流程将使用当前设置"; + DeviceSettingsStore.Save(_settings); + SaveStatus = "参数已保存,重新连接设备后生效"; _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}", UseSimulator,