From 10f5cbfd535d5ae7c13fbe2dd29b22d904f6f385 Mon Sep 17 00:00:00 2001 From: xyy <544939200@qq.com> Date: Fri, 15 May 2026 20:39:11 +0800 Subject: [PATCH] --- ASTM D7896-19瞬态热线法.csproj | 12 +- App.xaml | 4 +- App.xaml.cs | 61 +++++++- Models/AppConfig.cs | 24 ++- Services/IPlcCommunicationService.cs | 11 -- Services/IPlcService.cs | 39 +++++ Services/PlcCommunicationService.cs | 51 ------- Services/PlcService.cs | 212 +++++++++++++++++++++++++++ ViewModels/ConfigViewModel.cs | 85 +++++++++++ ViewModels/D7896ViewModel.cs | 19 ++- Views/ConfigWindow.xaml | 44 ++++++ Views/ConfigWindow.xaml.cs | 18 +++ Views/D7896View.xaml.cs | 7 +- Views/MainWindow.xaml.cs | 38 ++--- appsettings.json | 10 +- 15 files changed, 534 insertions(+), 101 deletions(-) delete mode 100644 Services/IPlcCommunicationService.cs create mode 100644 Services/IPlcService.cs delete mode 100644 Services/PlcCommunicationService.cs create mode 100644 Services/PlcService.cs create mode 100644 ViewModels/ConfigViewModel.cs create mode 100644 Views/ConfigWindow.xaml create mode 100644 Views/ConfigWindow.xaml.cs diff --git a/ASTM D7896-19瞬态热线法.csproj b/ASTM D7896-19瞬态热线法.csproj index 6d8cff7..a9a0674 100644 --- a/ASTM D7896-19瞬态热线法.csproj +++ b/ASTM D7896-19瞬态热线法.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows + net10.0-windows7.0 ASTM_D7896_19瞬态热线法 enable enable @@ -11,6 +11,12 @@ + + + + + + @@ -20,4 +26,8 @@ + + + + diff --git a/App.xaml b/App.xaml index 486a5fc..cd660de 100644 --- a/App.xaml +++ b/App.xaml @@ -1,8 +1,8 @@  + xmlns:converters="clr-namespace:ASTM_D7896_Tester.Converters" + > diff --git a/App.xaml.cs b/App.xaml.cs index 737a2bc..3c3ad2f 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,14 +1,71 @@ -using System.Configuration; +using ASTM_D7896_Tester.Models; +using ASTM_D7896_Tester.Services; +using ASTM_D7896_Tester.Views; +using Microsoft.Extensions.Configuration; +using OfficeOpenXml; +using System; +using System.Configuration; using System.Data; +using System.IO; using System.Windows; -namespace ASTM_D7896_Tester.Views +namespace ASTM_D7896_Tester { /// /// Interaction logic for App.xaml /// public partial class App : Application { + public static IPlcService PlcService { get; private set; } + public static AppConfig PlcConfig { get; private set; } + protected override async void OnStartup(StartupEventArgs e) + { + ExcelPackage.LicenseContext = LicenseContext.NonCommercial; + + base.OnStartup(e); + + // 防止在登录窗口关闭时应用程序因没有窗口而自动退出 + ShutdownMode = ShutdownMode.OnExplicitShutdown; + + + + + + + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + + var configuration = builder.Build(); + + // appsettings.json in this project keeps PLC settings at the root of the file + // (not under a "PlcSettings" section). Bind the entire configuration to AppConfig. + PlcConfig = configuration.Get() ?? new AppConfig(); + + PlcService = new PlcService(PlcConfig); + + var plcService = App.PlcService as PlcService; + try + { + await plcService.EnsureConnectedAsync(); + } + catch (Exception ex) + { + MessageBox.Show($"PLC 连接失败:{ex.Message}"); + } + + // 启动主窗口,设置为应用程序的主窗口并恢复默认的退出模式 + var mainWindow = new MainWindow(); + MainWindow = mainWindow; + ShutdownMode = ShutdownMode.OnMainWindowClose; + mainWindow.Show(); + } + + protected override void OnExit(ExitEventArgs e) + { + (PlcService as IDisposable)?.Dispose(); + base.OnExit(e); + } } } diff --git a/Models/AppConfig.cs b/Models/AppConfig.cs index e83040e..bbfe8ee 100644 --- a/Models/AppConfig.cs +++ b/Models/AppConfig.cs @@ -8,6 +8,7 @@ public class AppConfig public PlcRegisterAddresses PlcRegisterAddresses { get; set; } = new(); public TestParameters TestParameters { get; set; } = new(); public AppSettings AppSettings { get; set; } = new(); + } public class PlcConnectionConfig @@ -15,15 +16,18 @@ public class PlcConnectionConfig public string IpAddress { get; set; } = "127.0.0.1"; public int Port { get; set; } = 502; public int TimeoutMs { get; set; } = 5000; + + public byte SlaveId { get; set; } = 1; // 从站地址(默认1) } public class PlcRegisterAddresses { - public int ThermalConductivity { get; set; } = 40001; - public int ThermalDiffusivity { get; set; } = 40003; - public int TestTemperature { get; set; } = 40005; - public int StartCommand { get; set; } = 40010; - public int ResetCommand { get; set; } = 40011; + public ushort ThermalConductivity { get; set; } = 40001; + public ushort ThermalDiffusivity { get; set; } = 40003; + public ushort TestTemperature { get; set; } = 40005; + public ushort StartCommand { get; set; } = 40010; + public ushort ResetCommand { get; set; } = 40011; + } public class TestParameters @@ -43,6 +47,8 @@ public class TestParameters public bool UsePressure { get; set; } = false; public string ReferenceLiquid { get; set; } = "蒸馏水"; public double ReferenceConductivity { get; set; } = 0.606; + + public CalibrationCoefficients CalibrationCoefficients { get; set; } = new(); } public class AppSettings @@ -50,4 +56,12 @@ public class AppSettings public int WindowWidth { get; set; } = 1024; public int WindowHeight { get; set; } = 768; public string ThemeColor { get; set; } = "Blue"; +} + +public class CalibrationCoefficients +{ + public ushort PressureCoefficient { get; set; } + public ushort PressureProtection { get; set; } + public ushort TemperatureCoefficient { get; set; } + public ushort ResistanceCoefficient { get; set; } } \ No newline at end of file diff --git a/Services/IPlcCommunicationService.cs b/Services/IPlcCommunicationService.cs deleted file mode 100644 index 2996f93..0000000 --- a/Services/IPlcCommunicationService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ASTM_D7896_Tester.Services; - -public interface IPlcCommunicationService -{ - Task ConnectAsync(); - Task DisconnectAsync(); - Task IsConnectedAsync(); - Task ReadFloatAsync(int address); - Task WriteSingleCoilAsync(int address, bool value); - Task WriteSingleRegisterAsync(int address, short value); -} \ No newline at end of file diff --git a/Services/IPlcService.cs b/Services/IPlcService.cs new file mode 100644 index 0000000..674f8a3 --- /dev/null +++ b/Services/IPlcService.cs @@ -0,0 +1,39 @@ +namespace ASTM_D7896_Tester.Services; + +public interface IPlcService +{ + Task ConnectAsync(); + Task DisconnectAsync(); + Task IsConnectedAsync(); + /// 读取指定工位的压力(浮点数) + /// 工位号 1~3 + //Task ReadPressureAsync(int stationId); + + /// 读取湿膜流量(浮点数) + Task ReadWetFlowAsync(int stationId); + + /// 读取干膜流量(浮点数) + Task ReadDryFlowAsync(int stationId); + + /// 写入线圈(如 M 元件) + Task WriteCoilAsync(ushort coilAddress, bool value); + + /// 写入单个寄存器(16位) + Task WriteRegisterAsync(ushort registerAddress, ushort value); + + /// 读取线圈状态(如 M 元件的 ON/OFF) + Task ReadCoilAsync(ushort coilAddress); + + /// 读取连续多个保持寄存器(16位) + Task ReadHoldingRegistersAsync(ushort startAddress, ushort count); + + /// 写入单个保持寄存器(16位) + Task WriteSingleRegisterAsync(ushort registerAddress, ushort value); + + + Task WriteMultipleRegistersAsync(ushort registerAddress, float value); + + float UshortToFloat(ushort P1, ushort P2); + + Task ReadFloatAsync(ushort startAddress); +} \ No newline at end of file diff --git a/Services/PlcCommunicationService.cs b/Services/PlcCommunicationService.cs deleted file mode 100644 index 007fda0..0000000 --- a/Services/PlcCommunicationService.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace ASTM_D7896_Tester.Services; - -public class PlcCommunicationService : IPlcCommunicationService -{ - private bool _isConnected = false; - private readonly Random _random = new(); - - public Task ConnectAsync() - { - // 模拟连接 - _isConnected = true; - return Task.FromResult(true); - } - - public Task DisconnectAsync() - { - _isConnected = false; - return Task.CompletedTask; - } - - public Task IsConnectedAsync() => Task.FromResult(_isConnected); - - public Task ReadFloatAsync(int address) - { - // 模拟读取PLC寄存器返回随机值(实际应通过协议读取) - // 热导率范围 0.1~1.0 W/m·K,热扩散率范围 0.05~1.5 ×10⁻⁶ m²/s - if (address == 40001) - return Task.FromResult((float)(0.2 + _random.NextDouble() * 0.8)); - if (address == 40003) - return Task.FromResult((float)(0.1 + _random.NextDouble() * 1.0)); - if (address == 40005) - return Task.FromResult(25.0f); // 测试温度 - return Task.FromResult(0.0f); - } - - public Task WriteSingleCoilAsync(int address, bool value) - { - // 模拟写入线圈 - System.Diagnostics.Debug.WriteLine($"Write coil {address} = {value}"); - return Task.CompletedTask; - } - - public Task WriteSingleRegisterAsync(int address, short value) - { - System.Diagnostics.Debug.WriteLine($"Write register {address} = {value}"); - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Services/PlcService.cs b/Services/PlcService.cs new file mode 100644 index 0000000..555f8c6 --- /dev/null +++ b/Services/PlcService.cs @@ -0,0 +1,212 @@ +using ASTM_D7896_Tester.Models; +using Modbus.Device; +using System; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace ASTM_D7896_Tester.Services; + +public class PlcService : IPlcService +{ + private bool _isConnected = false; + private readonly AppConfig _config; + private TcpClient _tcpClient; + private IModbusMaster _master; + + public PlcService(AppConfig config) + { + _config = config; + } + + public Task ConnectAsync() + { + // 模拟连接 + _isConnected = true; + return Task.FromResult(true); + } + + public Task DisconnectAsync() + { + _isConnected = false; + return Task.CompletedTask; + } + + public Task IsConnectedAsync() => Task.FromResult(_isConnected); + + public async Task EnsureConnectedAsync(int retryCount = 3) + { + if (_tcpClient != null && _tcpClient.Connected) + return; + + for (int i = 0; i < retryCount; i++) + { + try + { + _tcpClient?.Close(); + _tcpClient = new TcpClient(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + // 现在可以直接使用扩展方法 + await _tcpClient.ConnectAsync(_config.PlcConnection.IpAddress, _config.PlcConnection.Port).WithCancellation(cts.Token); + _master = ModbusIpMaster.CreateIp(_tcpClient); + return; + } + catch (Exception ex) when (i < retryCount - 1) + { + System.Diagnostics.Debug.WriteLine($"连接失败,{500}ms 后重试... {ex.Message}"); + await Task.Delay(500); + } + } + throw new Exception($"无法连接到 PLC ({_config.PlcConnection.IpAddress}:{_config.PlcConnection.Port}),请检查网络和 PLC 状态。"); + } + + // 读取两个连续的保持寄存器,转换为32位浮点数(假设大端模式) + public async Task ReadFloatAsync(ushort startAddress) + { + await EnsureConnectedAsync(); + var registers = await ReadHoldingRegistersAsync(startAddress, 2); + return UshortToFloat(registers[1], registers[0]); + } + + + //public async Task ReadPressureAsync() => + // await ReadFloatAsync(_config.PressureRegister); + + public async Task ReadWetFlowAsync(int stationId) + { + ushort startAddress = stationId switch + { + //1 => _config.WetFlowRegister, + //2 => _config.WetFlowRegister2, + //3 => _config.WetFlowRegister3, + }; + return await ReadFloatAsync(startAddress); + } + + public async Task ReadDryFlowAsync(int stationId) + { + ushort startAddress = stationId switch + { + _ => throw new ArgumentException("Invalid station") + }; + return await ReadFloatAsync(startAddress); + } + + + public async Task WriteCoilAsync(ushort coilAddress, bool value) + { + await EnsureConnectedAsync(); + await _master.WriteSingleCoilAsync(_config.PlcConnection.SlaveId, coilAddress, value); + } + + public async Task WriteRegisterAsync(ushort registerAddress, ushort value) + { + await EnsureConnectedAsync(); + await Task.Delay(100); + await _master.WriteSingleRegisterAsync(_config.PlcConnection.SlaveId, registerAddress, value); + } + + + public async Task ReadCoilAsync(ushort coilAddress) + { + await EnsureConnectedAsync(); + await Task.Delay(100); + bool[] result = await _master?.ReadCoilsAsync(_config.PlcConnection.SlaveId, coilAddress, 1); + return result[0]; + } + + public bool IsConnected => _tcpClient != null && _tcpClient.Connected; + + public async Task ReadHoldingRegistersAsync(ushort startAddress, ushort count) + { + await EnsureConnectedAsync(); + // await Task.Delay(100); + return await _master.ReadHoldingRegistersAsync(_config.PlcConnection.SlaveId, startAddress, count); + } + + public async Task WriteSingleRegisterAsync(ushort registerAddress, ushort value) + { + await EnsureConnectedAsync(); + int val = (int)value; + await Task.Delay(100); + await _master.WriteMultipleRegistersAsync(1, registerAddress, intToushorts(val)); + } + + public async Task WriteMultipleRegistersAsync(ushort registerAddress, float value) + { + await EnsureConnectedAsync(); + await Task.Delay(100); + await _master.WriteMultipleRegistersAsync(_config.PlcConnection.SlaveId, registerAddress, SplitFloatToUShortArray((float)value)); + } + + /// + /// Int转为ushort数组发送 + /// + /// + /// 返回ushort数组 + private ushort[] intToushorts(int res) + { + ushort ust1 = (ushort)(res >> 16); + ushort ust2 = (ushort)res; + return new ushort[] { ust2, ust1 }; + } + + + /// + /// Float转为Ushort数组发送 + /// + /// + /// 返回ushort数组 + public ushort[] SplitFloatToUShortArray(float value) + { + byte[] floatBytes = BitConverter.GetBytes(value); + ushort[] ushortArray = new ushort[floatBytes.Length / 2]; + + for (int i = 0, j = 0; i < floatBytes.Length; i += 2, j++) + { + ushortArray[j] = BitConverter.ToUInt16(floatBytes, i); + } + + return ushortArray; + } + + /// + /// ushort转为float类型 + /// + /// + /// + /// float型数据 + public float UshortToFloat(ushort P1, ushort P2) + { + int intSign, intSignRest, intExponent, intExponentRest; + float faResult, faDigit; + intSign = P1 / 32768; + intSignRest = P1 % 32768; + intExponent = intSignRest / 128; + intExponentRest = intSignRest % 128; + faDigit = (float)(intExponentRest * 65536 + P2) / 8388608; + faResult = (float)Math.Pow(-1, intSign) * (float)Math.Pow(2, intExponent - 127) * (faDigit + 1); + return faResult; + } + + public void Dispose() + { + _master?.Dispose(); + _tcpClient?.Close(); + _tcpClient?.Dispose(); + } +} + + +public static class TaskExtensions +{ + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) + { + if (task != await Task.WhenAny(task, tcs.Task)) + throw new OperationCanceledException(cancellationToken); + } + await task; + } +} \ No newline at end of file diff --git a/ViewModels/ConfigViewModel.cs b/ViewModels/ConfigViewModel.cs new file mode 100644 index 0000000..8c60260 --- /dev/null +++ b/ViewModels/ConfigViewModel.cs @@ -0,0 +1,85 @@ +using ASTM_D7896_Tester.Models; +using ASTM_D7896_Tester.Services; +using ASTM_D7896_Tester.Views; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; +using System.Threading.Tasks; +using System.Windows; +using static ASTM_D7896_Tester.Models.TestParameters; + +namespace ASTM_D7896_Tester.ViewModels; + +public partial class ConfigViewModel : ObservableObject +{ + [ObservableProperty] + private float _pressureCoefficient; + + [ObservableProperty] + private float _pressureProtection; + + [ObservableProperty] + private float _temperatureCoefficient; + + [ObservableProperty] + private float _resistanceCoefficient; + + private readonly IPlcService _plcService; + private readonly AppConfig _coefficientAddresses; + + public ConfigViewModel() + { + // 从 App 静态属性获取 PLC 服务和系数寄存器地址 + _plcService = ASTM_D7896_Tester.App.PlcService; + _coefficientAddresses = ASTM_D7896_Tester.App.PlcConfig; + + // 窗口打开时加载一次 + LoadFromPlc(); + } + + private async void LoadFromPlc() + { + try + { + PressureCoefficient = await _plcService.ReadFloatAsync(_coefficientAddresses.TestParameters.CalibrationCoefficients.PressureCoefficient); + PressureProtection = await _plcService.ReadFloatAsync(_coefficientAddresses.TestParameters.CalibrationCoefficients.PressureProtection); + TemperatureCoefficient = await _plcService.ReadFloatAsync(_coefficientAddresses.TestParameters.CalibrationCoefficients.TemperatureCoefficient); + ResistanceCoefficient = await _plcService.ReadFloatAsync(_coefficientAddresses.TestParameters.CalibrationCoefficients.ResistanceCoefficient); + } + catch (Exception ex) + { + MessageBox.Show($"从 PLC 读取系数失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + [RelayCommand] + private async Task Save() + { + try + { + // 写入 PLC(使用 WriteMultipleRegistersAsync,该方法支持 float) + await _plcService.WriteMultipleRegistersAsync(_coefficientAddresses.TestParameters.CalibrationCoefficients.PressureCoefficient, PressureCoefficient); + await _plcService.WriteMultipleRegistersAsync(_coefficientAddresses.TestParameters.CalibrationCoefficients.PressureProtection, PressureProtection); + await _plcService.WriteMultipleRegistersAsync(_coefficientAddresses.TestParameters.CalibrationCoefficients.TemperatureCoefficient, TemperatureCoefficient); + await _plcService.WriteMultipleRegistersAsync(_coefficientAddresses.TestParameters.CalibrationCoefficients.ResistanceCoefficient, ResistanceCoefficient); + + MessageBox.Show("系数已保存到 PLC。", "成功", MessageBoxButton.OK, MessageBoxImage.Information); + CloseWindow(); + } + catch (Exception ex) + { + MessageBox.Show($"保存失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + [RelayCommand] + private void Cancel() + { + CloseWindow(); + } + + private void CloseWindow() + { + Application.Current.Windows.OfType().FirstOrDefault()?.Close(); + } +} \ No newline at end of file diff --git a/ViewModels/D7896ViewModel.cs b/ViewModels/D7896ViewModel.cs index c3e22e4..484d2f4 100644 --- a/ViewModels/D7896ViewModel.cs +++ b/ViewModels/D7896ViewModel.cs @@ -17,9 +17,10 @@ namespace ASTM_D7896_Tester.ViewModels; public partial class D7896ViewModel : ObservableObject { - private readonly IPlcCommunicationService _plcService; - private readonly ReportService _reportService; + private readonly IPlcService _plcService; private AppConfig _config; + private readonly ReportService _reportService; + public ObservableCollection ReferenceLiquids { get; } = new ObservableCollection { "蒸馏水", "甲苯", "乙二醇" }; @@ -119,8 +120,10 @@ public partial class D7896ViewModel : ObservableObject public D7896ViewModel() { - _config = JsonConfigHelper.LoadConfig(); - _plcService = new PlcCommunicationService(); + // 获取应用全局配置并确保不为 null + _config = ASTM_D7896_Tester.App.PlcConfig ?? new Models.AppConfig(); + + _plcService = ASTM_D7896_Tester.App.PlcService; _reportService = new ReportService(_config.TestParameters.ReportOutputPath); // 加载配置中的默认值 @@ -198,9 +201,9 @@ public partial class D7896ViewModel : ObservableObject CurrentMeasurementIndex = i; StatusMessage = $"正在执行第 {i} 次测量..."; - await _plcService.WriteSingleCoilAsync(_config.PlcRegisterAddresses.StartCommand, true); + await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.StartCommand, true); await Task.Delay(500); - await _plcService.WriteSingleCoilAsync(_config.PlcRegisterAddresses.StartCommand, false); + await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.StartCommand, false); await Task.Delay(2000); @@ -461,9 +464,9 @@ public partial class D7896ViewModel : ObservableObject if (!await _plcService.IsConnectedAsync()) await _plcService.ConnectAsync(); - await _plcService.WriteSingleCoilAsync(_config.PlcRegisterAddresses.StartCommand, true); + await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.StartCommand, true); await Task.Delay(500); - await _plcService.WriteSingleCoilAsync(_config.PlcRegisterAddresses.StartCommand, false); + await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.StartCommand, false); await Task.Delay(2000); float lambda = await _plcService.ReadFloatAsync(_config.PlcRegisterAddresses.ThermalConductivity); diff --git a/Views/ConfigWindow.xaml b/Views/ConfigWindow.xaml new file mode 100644 index 0000000..f007ab8 --- /dev/null +++ b/Views/ConfigWindow.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +