diff --git a/Helpers/PlcConfiguration.cs b/Helpers/PlcConfiguration.cs index 4d26440..7a744ef 100644 --- a/Helpers/PlcConfiguration.cs +++ b/Helpers/PlcConfiguration.cs @@ -1,79 +1,105 @@ -namespace MembranePoreTester.Communication +using System; + +namespace MembranePoreTester.Communication { + /// + /// PLC 配置类,用于存储从 appsettings.json 读取的 Modbus 参数和寄存器地址。 + /// public class PlcConfiguration { - public string IpAddress { get; set; } - public int Port { get; set; } - public byte SlaveId { get; set; } = 1; // 从站地址,默认1 - public ushort PressureRegister { get; set; } - public ushort WetFlowRegister { get; set; } - public ushort DryFlowRegister { get; set; } - public double PressureFactor { get; set; } = 1.0; - public double WetFlowFactor { get; set; } = 1.0; - public double DryFlowFactor { get; set; } = 1.0; + // ========== 网络连接参数 ========== + public string IpAddress { get; set; } // PLC IP 地址 + public int Port { get; set; } // Modbus TCP 端口 + public byte SlaveId { get; set; } = 1; // 从站地址(默认1) + // 以下属性用于与上位机交互(但实际按工位读取,此处保留兼容) + public ushort PressureRegister { get; set; } // 不再使用,保留兼容 + public ushort WetFlowRegister { get; set; } // 湿膜流量寄存器起始地址 + public ushort DryFlowRegister { get; set; } // 干膜流量寄存器起始地址 + public double PressureFactor { get; set; } = 1.0; // 压力系数(单位换算) + public double WetFlowFactor { get; set; } = 1.0; // 湿膜流量系数 + public double DryFlowFactor { get; set; } = 1.0; // 干膜流量系数 + // ========== 工位专用寄存器 ========== + public ushort PressureRegisterStation1 { get; set; } // 工位1 压力寄存器起始地址 + public ushort PressureRegisterStation2 { get; set; } // 工位2 + public ushort PressureRegisterStation3 { get; set; } // 工位3 - public ushort PressureRegisterStation1 { get; set; } - public ushort PressureRegisterStation2 { get; set; } - public ushort PressureRegisterStation3 { get; set; } - public ushort PressureModeRegister { get; set; } - public ushort PressCoil { get; set; } - public ushort StartCoil { get; set; } - public ushort EnableCoil { get; set; } - public ushort StopCoil { get; set; } + // ========== 控制线圈 ========== + public ushort PressureModeRegister { get; set; } // 高压/低压模式寄存器 + public ushort PressCoil { get; set; } // 加压线圈(M100) + public ushort StartCoil { get; set; } // 启动线圈(M20) + public ushort EnableCoil { get; set; } // 使能线圈(M21,只读状态) + public ushort StopCoil { get; set; } // 停止线圈(M7) + // ========== 运维参数(用户可设置) ========== + public ushort PressureUpperLimit { get; set; } = 300; // 加压上限 D300 + public ushort PressureRate { get; set; } = 280; // 加压速率 D280 + public ushort PressureCoeff { get; set; } = 282; // 加压系数 D282 - - - - public ushort PressureUpperLimit { get; set; } = 300; - public ushort PressureRate { get; set; } = 280; - public ushort PressureCoeff { get; set; } = 282; - public ushort HPCoeff1 { get; set; } = 3120; - public ushort LPCoeff1 { get; set; } = 3122; - public ushort HPCoeff2 { get; set; } = 3124; + // 高压/低压系数(每个工位独立) + public ushort HPCoeff1 { get; set; } = 3120; // 工位1 高压系数 + public ushort LPCoeff1 { get; set; } = 3122; // 工位1 低压系数 + public ushort HPCoeff2 { get; set; } = 3124; // 工位2 public ushort LPCoeff2 { get; set; } = 3126; - public ushort HPCoeff3 { get; set; } = 3128; + public ushort HPCoeff3 { get; set; } = 3128; // 工位3 public ushort LPCoeff3 { get; set; } = 3130; - public ushort LargeFlowCoeff1 { get; set; } = 3048; - public ushort SmallFlowCoeff1 { get; set; } = 380; - public ushort LargeFlowCoeff2 { get; set; } = 1218; + + // 大/小流量系数(每个工位独立) + public ushort LargeFlowCoeff1 { get; set; } = 3048; // 工位1 大流量系数 + public ushort SmallFlowCoeff1 { get; set; } = 380; // 工位1 小流量系数 + public ushort LargeFlowCoeff2 { get; set; } = 1218; // 工位2 public ushort SmallFlowCoeff2 { get; set; } = 1318; - public ushort LargeFlowCoeff3 { get; set; } = 1418; + public ushort LargeFlowCoeff3 { get; set; } = 1418; // 工位3 public ushort SmallFlowCoeff3 { get; set; } = 1468; - public ushort FlowModeRegister1 { get; set; } = 5; // 工位1流量模式 (0=大,1=小) + + // 流量模式选择寄存器(0=大流量,1=小流量) + + public ushort FlowModeRegister { get; set; } = 4; // 工位1 流量模式 + public ushort FlowModeRegister1 { get; set; } = 5; // 工位1 流量模式 public ushort FlowModeRegister2 { get; set; } = 6; public ushort FlowModeRegister3 { get; set; } = 7; - // 校准系数地址(需与PLC约定) - public ushort PressureCalibZero { get; set; } = 3200; - public ushort PressureCalibSpan { get; set; } = 3202; - public ushort FlowCalibZero { get; set; } = 3204; - public ushort FlowCalibSpan { get; set; } = 3206; - - - - + // 校准系数(零点/量程)寄存器地址 + public ushort PressureCalibZero { get; set; } = 3200; // 压力零点系数 + public ushort PressureCalibSpan { get; set; } = 3202; // 压力量程系数 + public ushort FlowCalibZero { get; set; } = 3204; // 流量零点系数 + public ushort FlowCalibSpan { get; set; } = 3206; // 流量量程系数 } - - - - + /// + /// PLC 服务接口,定义与 Modbus 设备通信的方法。 + /// public interface IPlcService { - Task ReadPressureAsync(int stationId); // 按工位读取压力 + /// 读取指定工位的压力(浮点数) + /// 工位号 1~3 + Task ReadPressureAsync(int stationId); + + /// 读取湿膜流量(浮点数) Task ReadWetFlowAsync(); + + /// 读取干膜流量(浮点数) Task ReadDryFlowAsync(); - Task WriteCoilAsync(ushort coilAddress, bool value); // 写线圈 - Task WriteRegisterAsync(ushort registerAddress, ushort value); // 写寄存器 - Task ReadCoilAsync(ushort coilAddress); // 新增:读取线圈状态 + /// 写入线圈(如 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); - Task WriteSingleRegisterAsync(ushort registerAddress, ushort value); - } + /// 写入单个保持寄存器(16位) + Task WriteSingleRegisterAsync(ushort registerAddress, ushort value); + + + Task WriteMultipleRegistersAsync(ushort registerAddress, float value); + + float UshortToFloat(ushort P1, ushort P2); + } } \ No newline at end of file diff --git a/Models/BubblePointRecord.cs b/Models/BubblePointRecord.cs index a73258f..6f5abcb 100644 --- a/Models/BubblePointRecord.cs +++ b/Models/BubblePointRecord.cs @@ -1,6 +1,9 @@ -namespace MembranePoreTester.Models +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace MembranePoreTester.Models { - public class BubblePointRecord + public class BubblePointRecord: INotifyPropertyChanged { public string SampleType { get; set; } // 平板膜/中空纤维膜 public string SampleSpec { get; set; } // 规格 @@ -8,7 +11,28 @@ public double SoakingTime { get; set; } // 浸润时间(小时) public TestLiquid Liquid { get; set; } // 测试液体 public string LiquidManufacturer { get; set; } // 生产厂家 - public double BubblePointPressure { get; set; } // 泡点压力(数值) + + + private double _bubblePointPressure; + + public double BubblePointPressure // 泡点压力(数值) + { + get => _bubblePointPressure; + set + { + if (_bubblePointPressure != value) + { + _bubblePointPressure = value; + OnPropertyChanged(); // 需要触发通知 + } + } + } + + + + + + public double BubbleCurrentPressure { get; set; } // 泡点压力(数值) public string PressureUnit { get; set; } // Pa/cmHg/psi public DateTime TestDate { get; set; } = DateTime.Now; public string Tester { get; set; } @@ -26,5 +50,14 @@ _ => 0 }; } + + + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } } \ No newline at end of file diff --git a/Service/ModbusTcpPlcService.cs b/Service/ModbusTcpPlcService.cs index d304d17..f72db32 100644 --- a/Service/ModbusTcpPlcService.cs +++ b/Service/ModbusTcpPlcService.cs @@ -1,7 +1,8 @@ -using System; +using Modbus.Device; +using System; +using System.Net; using System.Net.Sockets; using System.Threading.Tasks; -using Modbus.Device; namespace MembranePoreTester.Communication { @@ -30,31 +31,19 @@ namespace MembranePoreTester.Communication private async Task ReadFloatAsync(ushort startAddress) { await EnsureConnectedAsync(); - - // 读取两个寄存器(从站地址由配置指定) - ushort[] registers = await _master.ReadHoldingRegistersAsync(_config.SlaveId, startAddress, 2); - - // 将两个16位寄存器合并为32位浮点数(大端) - byte[] bytes = new byte[4]; - bytes[0] = (byte)(registers[0] >> 8); - bytes[1] = (byte)(registers[0] & 0xFF); - bytes[2] = (byte)(registers[1] >> 8); - bytes[3] = (byte)(registers[1] & 0xFF); - - if (BitConverter.IsLittleEndian) - Array.Reverse(bytes); // 如果系统是小端,需要反转字节顺序 - - return BitConverter.ToSingle(bytes, 0); + var registers = await ReadHoldingRegistersAsync(startAddress, 2); + return UshortToFloat(registers[1], registers[0]); } + public async Task ReadPressureAsync() => - await ReadFloatAsync(_config.PressureRegister) * (float)_config.PressureFactor; + await ReadFloatAsync(_config.PressureRegister); public async Task ReadWetFlowAsync() => - await ReadFloatAsync(_config.WetFlowRegister) * (float)_config.WetFlowFactor; + await ReadFloatAsync(_config.WetFlowRegister); public async Task ReadDryFlowAsync() => - await ReadFloatAsync(_config.DryFlowRegister) * (float)_config.DryFlowFactor; + await ReadFloatAsync(_config.DryFlowRegister); public async Task WriteCoilAsync(ushort coilAddress, bool value) @@ -73,7 +62,7 @@ namespace MembranePoreTester.Communication public async Task ReadCoilAsync(ushort coilAddress) { await EnsureConnectedAsync(); - bool[] result = await _master.ReadCoilsAsync(_config.SlaveId, coilAddress, 1); + bool[] result = await _master?.ReadCoilsAsync(_config.SlaveId, coilAddress, 1); return result[0]; } @@ -87,9 +76,65 @@ namespace MembranePoreTester.Communication public async Task WriteSingleRegisterAsync(ushort registerAddress, ushort value) { await EnsureConnectedAsync(); - await _master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value); + int val = (int)value; + await _master.WriteMultipleRegistersAsync(1, registerAddress, intToushorts(val)); } + public async Task WriteMultipleRegistersAsync(ushort registerAddress, float value) + { + await EnsureConnectedAsync(); + await _master.WriteMultipleRegistersAsync(_config.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 async Task ReadPressureAsync(int stationId) diff --git a/ViewModels/BubblePointViewModel.cs b/ViewModels/BubblePointViewModel.cs index 2086fd4..f7b76df 100644 --- a/ViewModels/BubblePointViewModel.cs +++ b/ViewModels/BubblePointViewModel.cs @@ -87,6 +87,10 @@ namespace MembranePoreTester.ViewModels public ICommand OpenPressureCalibCommand { get; } + + + private System.Windows.Threading.DispatcherTimer _timer; // 添加定时器字段 + public BubblePointViewModel() { @@ -99,15 +103,47 @@ namespace MembranePoreTester.ViewModels SaveCommand = new RelayCommand(SaveToDatabase); ExportCommand = new RelayCommand(ExportToExcel); OpenPressureCalibCommand = new RelayCommand(OpenPressureCalibration); + + + + // 启动定时器,每秒读取一次 + _timer = new System.Windows.Threading.DispatcherTimer(); + _timer.Interval = TimeSpan.FromSeconds(1); + _timer.Tick += async (s, e) => await ReadCurrentPlcAsync(); + _timer.Start(); + + // 立即读取一次 + Task.Run(async () => await ReadCurrentPlcAsync()); + } + + public override void Dispose() + { + _timer?.Stop(); + base.Dispose(); + } + + private async Task ReadCurrentPlcAsync() + { + try + { + float rawPressure = await _plcService.ReadPressureAsync(StationId); + Record.BubbleCurrentPressure = rawPressure; + OnPropertyChanged(nameof(Record.BubbleCurrentPressure)); + } + catch (Exception ex) + { + MessageBox.Show($"读取PLC失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } } private async Task ReadPlcAsync() { + if (IsDisposed) return; // 改3:添加这行检查 try { - float rawPressure = await _plcService.ReadPressureAsync(StationId); - Record.BubblePointPressure = rawPressure * _plcConfig.PressureFactor; + Record.BubblePointPressure = Record.BubbleCurrentPressure; + OnPropertyChanged(nameof(Record.BubblePointPressure)); } catch (Exception ex) diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 35c7c93..4624f57 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -1,9 +1,11 @@ using MembranePoreTester.Communication; using System.Collections.ObjectModel; using System.ComponentModel; // 用于 PropertyChanged +using System.Net; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; +using static OfficeOpenXml.ExcelErrorValue; namespace MembranePoreTester.ViewModels { @@ -117,15 +119,19 @@ namespace MembranePoreTester.ViewModels private async Task WritePressureModeAsync(string mode) { - ushort val = mode == "高压" ? (ushort)1 : (ushort)0; + if (IsDisposed) return; + + ushort val = mode.ToString().Contains("高压") ? (ushort)0 : (ushort)1; try { - await _plcService.WriteRegisterAsync(_plcConfig.PressureModeRegister, val); + + await _plcService.WriteSingleRegisterAsync(_plcConfig.PressureModeRegister, val); } catch (Exception ex) { MessageBox.Show($"写压力模式失败: {ex.Message}"); } + } @@ -144,7 +150,7 @@ namespace MembranePoreTester.ViewModels public MainViewModel() { - for (int i = 1; i <= 3; i++) + for (int i = 1; i <= 1; i++) { var station = new StationItem { diff --git a/ViewModels/PoreDistributionViewModel.cs b/ViewModels/PoreDistributionViewModel.cs index 1a5a625..ba7052a 100644 --- a/ViewModels/PoreDistributionViewModel.cs +++ b/ViewModels/PoreDistributionViewModel.cs @@ -347,22 +347,16 @@ namespace MembranePoreTester.ViewModels set => SetProperty(ref _testMode, value); } - private int _selectedFlowModeIndex; // 0=大流量,1=小流量 - public int SelectedFlowModeIndex + private string _selectedFlowModeIndex; // 0=大流量,1=小流量 + public string SelectedFlowModeIndex { get => _selectedFlowModeIndex; set { if (SetProperty(ref _selectedFlowModeIndex, value)) { - ushort reg = StationId switch - { - 1 => _plcConfig.FlowModeRegister1, - 2 => _plcConfig.FlowModeRegister2, - 3 => _plcConfig.FlowModeRegister3, - _ => 0 - }; - Task.Run(async () => await _plcService.WriteSingleRegisterAsync(reg, (ushort)value)); + // 当选择变化时,写入 PLC 压力模式寄存器 + Task.Run(async () => await WriteFlowModeAsync(value)); } } } @@ -375,6 +369,18 @@ namespace MembranePoreTester.ViewModels set => SetProperty(ref _plotModel, value); } + private async Task WriteFlowModeAsync(string mode) + { + float val = mode == "大流量" ? 0.0f : 1.0f; + try + { + await _plcService.WriteMultipleRegistersAsync(_plcConfig.PressureModeRegister, val); + } + catch (Exception ex) + { + MessageBox.Show($"写流量模式失败: {ex.Message}"); + } + } private void UpdatePlot() { diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs index 86f3e4f..b5219a1 100644 --- a/ViewModels/ViewModelBase.cs +++ b/ViewModels/ViewModelBase.cs @@ -4,8 +4,11 @@ using System.Runtime.CompilerServices; namespace MembranePoreTester.ViewModels { - public class ViewModelBase : INotifyPropertyChanged + public class ViewModelBase : INotifyPropertyChanged, IDisposable { + private bool _disposed = false; + protected CancellationTokenSource _cts = new CancellationTokenSource(); + public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) @@ -30,5 +33,35 @@ namespace MembranePoreTester.ViewModels await App.PlcService.WriteSingleRegisterAsync(address, high); await App.PlcService.WriteSingleRegisterAsync((ushort)(address + 1), low); } + + // 新增:安全执行异步方法(自动处理对象释放) + protected async Task SafeExecuteAsync(Func action) + { + if (_disposed) return; + try + { + await action(); + } + catch (OperationCanceledException) + { + // 正常取消 + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"异步执行失败: {ex.Message}"); + } + } + + // 新增:检查是否已释放 + protected bool IsDisposed => _disposed; + + public virtual void Dispose() + { + if (_disposed) return; + _disposed = true; + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + } } } \ No newline at end of file diff --git a/Views/BubblePointView.xaml b/Views/BubblePointView.xaml index 2d75cc8..40cd46d 100644 --- a/Views/BubblePointView.xaml +++ b/Views/BubblePointView.xaml @@ -58,7 +58,11 @@ --> - + + + + + diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml index 150711f..2edca57 100644 --- a/Views/MainWindow.xaml +++ b/Views/MainWindow.xaml @@ -29,7 +29,7 @@ 低压 - 高压 + 高压