This commit is contained in:
xyy
2026-03-26 19:43:52 +08:00
parent 7bbf829224
commit b161e884e7
14 changed files with 452 additions and 268 deletions

View File

@@ -1,79 +1,105 @@
namespace MembranePoreTester.Communication
using System;
namespace MembranePoreTester.Communication
{
/// <summary>
/// PLC 配置类,用于存储从 appsettings.json 读取的 Modbus 参数和寄存器地址。
/// </summary>
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; // 流量量程系数
}
/// <summary>
/// PLC 服务接口,定义与 Modbus 设备通信的方法。
/// </summary>
public interface IPlcService
{
Task<float> ReadPressureAsync(int stationId); // 按工位读取压力
/// <summary> 读取指定工位的压力(浮点数) </summary>
/// <param name="stationId">工位号 1~3</param>
Task<float> ReadPressureAsync(int stationId);
/// <summary> 读取湿膜流量(浮点数) </summary>
Task<float> ReadWetFlowAsync();
/// <summary> 读取干膜流量(浮点数) </summary>
Task<float> ReadDryFlowAsync();
Task WriteCoilAsync(ushort coilAddress, bool value); // 写线圈
Task WriteRegisterAsync(ushort registerAddress, ushort value); // 写寄存器
Task<bool> ReadCoilAsync(ushort coilAddress); // 新增:读取线圈状态
/// <summary> 写入线圈(如 M 元件) </summary>
Task WriteCoilAsync(ushort coilAddress, bool value);
/// <summary> 写入单个寄存器16位 </summary>
Task WriteRegisterAsync(ushort registerAddress, ushort value);
/// <summary> 读取线圈状态(如 M 元件的 ON/OFF </summary>
Task<bool> ReadCoilAsync(ushort coilAddress);
/// <summary> 读取连续多个保持寄存器16位 </summary>
Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort count);
Task WriteSingleRegisterAsync(ushort registerAddress, ushort value);
}
/// <summary> 写入单个保持寄存器16位 </summary>
Task WriteSingleRegisterAsync(ushort registerAddress, ushort value);
Task WriteMultipleRegistersAsync(ushort registerAddress, float value);
float UshortToFloat(ushort P1, ushort P2);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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<float> 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<float> ReadPressureAsync() =>
await ReadFloatAsync(_config.PressureRegister) * (float)_config.PressureFactor;
await ReadFloatAsync(_config.PressureRegister);
public async Task<float> ReadWetFlowAsync() =>
await ReadFloatAsync(_config.WetFlowRegister) * (float)_config.WetFlowFactor;
await ReadFloatAsync(_config.WetFlowRegister);
public async Task<float> 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<bool> 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));
}
/// <summary>
/// Int转为ushort数组发送
/// </summary>
/// <param name="res"></param>
/// <returns>返回ushort数组</returns>
private ushort[] intToushorts(int res)
{
ushort ust1 = (ushort)(res >> 16);
ushort ust2 = (ushort)res;
return new ushort[] { ust2, ust1 };
}
/// <summary>
/// Float转为Ushort数组发送
/// </summary>
/// <param name="value"></param>
/// <returns>返回ushort数组</returns>
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;
}
/// <summary>
/// ushort转为float类型
/// </summary>
/// <param name="P1"></param>
/// <param name="P2"></param>
/// <returns>float型数据</returns>
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<float> ReadPressureAsync(int stationId)

View File

@@ -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)

View File

@@ -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
{

View File

@@ -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()
{

View File

@@ -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<Task> 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;
}
}
}

View File

@@ -58,7 +58,11 @@
<TextBox Text="{Binding Record.LiquidManufacturer}" Margin="5"/>-->
</StackPanel>
</GroupBox>
<GroupBox Header="当前压力" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Record.BubbleCurrentPressure}" Width="150" Margin="5"/>
</StackPanel>
</GroupBox>
<GroupBox Header="泡点压力" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Record.BubblePointPressure}" Width="150" Margin="5"/>

View File

@@ -29,7 +29,7 @@
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="5">
<ComboBox SelectedItem="{Binding HighLowPressure}" Width="80" Margin="5">
<ComboBoxItem IsSelected="True">低压</ComboBoxItem>
<ComboBoxItem>高压</ComboBoxItem>
<ComboBoxItem >高压</ComboBoxItem>
</ComboBox>
<Button Content="启动" Command="{Binding StartCommand}" Width="80" Margin="5"/>
<Button Content="停止" Command="{Binding StopCommand}" Width="80" Margin="5"/>

View File

@@ -87,7 +87,7 @@
</Grid>
</GroupBox>
<GroupBox Header="校准参数">
<!--<GroupBox Header="校准参数">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
@@ -108,11 +108,11 @@
<Label Grid.Row="3" Grid.Column="0">流量量程校准:</Label>
<TextBox x:Name="txtFlowSpan" Grid.Row="3" Grid.Column="1"/>
</Grid>
</GroupBox>
</GroupBox>-->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,20,0,0">
<!--<Button Content="读取参数" Click="ReadParameters_Click" Width="100" Margin="5"/>-->
<Button Content="写入参数" Click="WriteParameters_Click" Width="100" Margin="5"/>
<Button Content="关闭" Click="Close_Click" Width="100" Margin="5"/>
</StackPanel>
</StackPanel>

View File

@@ -1,10 +1,12 @@
using MembranePoreTester.Communication;
using MembranePoreTester.ViewModels;
using System;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using MembranePoreTester.Communication;
using MembranePoreTester.ViewModels;
namespace MembranePoreTester.Views
{
@@ -13,7 +15,9 @@ namespace MembranePoreTester.Views
private readonly IPlcService _plcService;
private readonly PlcConfiguration _config;
private DispatcherTimer _autoRefreshTimer;
private bool _isEditing = false; // 用户是否正在编辑任何文本框
// 用于快速查找文本框对应的写入地址和方法(可根据需要扩展)
private readonly Dictionary<TextBox, (ushort address, string paramName)> _textBoxMapping;
public ParameterWindow()
{
@@ -21,12 +25,39 @@ namespace MembranePoreTester.Views
_plcService = App.PlcService;
_config = App.PlcConfig;
// 为所有文本框注册焦点事件在XAML中设置事件或在此处统一查找
// 初始化文本框到寄存器地址的映射
_textBoxMapping = new Dictionary<TextBox, (ushort, string)>
{
[txtPressureUpper] = (_config.PressureUpperLimit, "加压上限"),
[txtPressureRate] = (_config.PressureRate, "加压速率"),
[txtPressureCoeff] = (_config.PressureCoeff, "加压系数"),
[txtHPCoeff1] = (_config.HPCoeff1, "工位1高压系数"),
[txtLPCoeff1] = (_config.LPCoeff1, "工位1低压系数"),
[txtHPCoeff2] = (_config.HPCoeff2, "工位2高压系数"),
[txtLPCoeff2] = (_config.LPCoeff2, "工位2低压系数"),
[txtHPCoeff3] = (_config.HPCoeff3, "工位3高压系数"),
[txtLPCoeff3] = (_config.LPCoeff3, "工位3低压系数"),
[txtLargeFlow1] = (_config.LargeFlowCoeff1, "工位1大流量系数"),
[txtSmallFlow1] = (_config.SmallFlowCoeff1, "工位1小流量系数"),
[txtLargeFlow2] = (_config.LargeFlowCoeff2, "工位2大流量系数"),
[txtSmallFlow2] = (_config.SmallFlowCoeff2, "工位2小流量系数"),
[txtLargeFlow3] = (_config.LargeFlowCoeff3, "工位3大流量系数"),
[txtSmallFlow3] = (_config.SmallFlowCoeff3, "工位3小流量系数"),
//[txtPressureZero] = (_config.PressureCalibZero, "压力零点"),
//[txtPressureSpan] = (_config.PressureCalibSpan, "压力量程"),
//[txtFlowZero] = (_config.FlowCalibZero, "流量零点"),
//[txtFlowSpan] = (_config.FlowCalibSpan, "流量量程"),
};
// 为所有文本框注册焦点事件
RegisterTextBoxEvents();
Loaded += async (s, e) =>
{
// 启动自动刷新定时器每秒1次
// 启动定时器
_autoRefreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
@@ -35,152 +66,109 @@ namespace MembranePoreTester.Views
_autoRefreshTimer.Start();
// 首次加载时读取一次
await ReadParametersAsync();
await LoadParameters();
};
Closed += (s, e) => _autoRefreshTimer?.Stop();
}
/// <summary>
/// 为所有文本框注册焦点事件,用于检测编辑状态
/// </summary>
private void RegisterTextBoxEvents()
{
// 查找当前窗口中所有文本框(可根据实际布局调整)
var textBoxes = FindVisualChildren<System.Windows.Controls.TextBox>(this);
foreach (var tb in textBoxes)
foreach (var tb in _textBoxMapping.Keys)
{
tb.GotFocus += (s, e) => _isEditing = true;
tb.LostFocus += (s, e) => _isEditing = false;
tb.GotFocus += (s, e) => { /* 什么都不做,只是为了让自动刷新跳过当前焦点的文本框 */ };
tb.LostFocus += async (s, e) =>
{
// 失去焦点时将当前文本框的值写入PLC
var (address, name) = _textBoxMapping[tb];
if (float.TryParse(tb.Text, out float value))
{
try
{
await WriteFloatAsync(address, value);
// 可选:显示短暂提示(避免频繁弹窗)
System.Diagnostics.Debug.WriteLine($"{name} 已写入: {value}");
}
catch (Exception ex)
{
MessageBox.Show($"写入 {name} 失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
else
{
// 输入无效,可提示或恢复原值
MessageBox.Show($"请输入有效的数值", "输入错误", MessageBoxButton.OK, MessageBoxImage.Warning);
// 重新从PLC读取该参数恢复原值
await UpdateSingleTextBox(tb, address);
}
};
}
}
/// <summary>
/// 定时器事件:如果用户未在编辑,则刷新参数
/// 更新单个文本框的值从PLC读取
/// </summary>
private async Task UpdateSingleTextBox(TextBox tb, ushort address)
{
try
{
float val = await ReadFloatAsync(address);
tb.Text = val.ToString("F3");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"更新文本框失败: {ex.Message}");
}
}
private async void AutoRefreshTimer_Tick(object sender, EventArgs e)
{
if (!_isEditing)
// 只更新当前没有获得焦点的文本框
await Dispatcher.InvokeAsync(async () =>
{
await ReadParametersAsync();
}
foreach (var kv in _textBoxMapping)
{
var tb = kv.Key;
var address = kv.Value.address;
if (!tb.IsFocused)
{
try
{
float val = await ReadFloatAsync(address);
tb.Text = val.ToString("F3");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"自动刷新 {kv.Value.paramName} 失败: {ex.Message}");
}
}
}
});
}
private async Task ReadParametersAsync()
/// <summary>
/// 一次性加载所有参数(用于窗口启动)
/// </summary>
private async Task LoadParameters()
{
try
foreach (var kv in _textBoxMapping)
{
// 加压控制
txtPressureUpper.Text = (await ReadFloatAsync(_config.PressureUpperLimit)).ToString("F3");
txtPressureRate.Text = (await ReadFloatAsync(_config.PressureRate)).ToString("F3");
txtPressureCoeff.Text = (await ReadFloatAsync(_config.PressureCoeff)).ToString("F3");
// 高压/低压系数
txtHPCoeff1.Text = (await ReadFloatAsync(_config.HPCoeff1)).ToString("F3");
txtLPCoeff1.Text = (await ReadFloatAsync(_config.LPCoeff1)).ToString("F3");
txtHPCoeff2.Text = (await ReadFloatAsync(_config.HPCoeff2)).ToString("F3");
txtLPCoeff2.Text = (await ReadFloatAsync(_config.LPCoeff2)).ToString("F3");
txtHPCoeff3.Text = (await ReadFloatAsync(_config.HPCoeff3)).ToString("F3");
txtLPCoeff3.Text = (await ReadFloatAsync(_config.LPCoeff3)).ToString("F3");
// 流量系数
txtLargeFlow1.Text = (await ReadFloatAsync(_config.LargeFlowCoeff1)).ToString("F3");
txtSmallFlow1.Text = (await ReadFloatAsync(_config.SmallFlowCoeff1)).ToString("F3");
txtLargeFlow2.Text = (await ReadFloatAsync(_config.LargeFlowCoeff2)).ToString("F3");
txtSmallFlow2.Text = (await ReadFloatAsync(_config.SmallFlowCoeff2)).ToString("F3");
txtLargeFlow3.Text = (await ReadFloatAsync(_config.LargeFlowCoeff3)).ToString("F3");
txtSmallFlow3.Text = (await ReadFloatAsync(_config.SmallFlowCoeff3)).ToString("F3");
// 校准参数
txtPressureZero.Text = (await ReadFloatAsync(_config.PressureCalibZero)).ToString("F3");
txtPressureSpan.Text = (await ReadFloatAsync(_config.PressureCalibSpan)).ToString("F3");
txtFlowZero.Text = (await ReadFloatAsync(_config.FlowCalibZero)).ToString("F3");
txtFlowSpan.Text = (await ReadFloatAsync(_config.FlowCalibSpan)).ToString("F3");
}
catch (Exception ex)
{
// 静默处理,避免频繁弹窗干扰(可记录日志)
System.Diagnostics.Debug.WriteLine($"自动读取参数失败: {ex.Message}");
await UpdateSingleTextBox(kv.Key, kv.Value.address);
}
}
private async void WriteParameters_Click(object sender, RoutedEventArgs e)
{
try
{
// 加压控制
await WriteFloatAsync(_config.PressureUpperLimit, ParseFloat(txtPressureUpper.Text));
await WriteFloatAsync(_config.PressureRate, ParseFloat(txtPressureRate.Text));
await WriteFloatAsync(_config.PressureCoeff, ParseFloat(txtPressureCoeff.Text));
// 高压/低压系数
await WriteFloatAsync(_config.HPCoeff1, ParseFloat(txtHPCoeff1.Text));
await WriteFloatAsync(_config.LPCoeff1, ParseFloat(txtLPCoeff1.Text));
await WriteFloatAsync(_config.HPCoeff2, ParseFloat(txtHPCoeff2.Text));
await WriteFloatAsync(_config.LPCoeff2, ParseFloat(txtLPCoeff2.Text));
await WriteFloatAsync(_config.HPCoeff3, ParseFloat(txtHPCoeff3.Text));
await WriteFloatAsync(_config.LPCoeff3, ParseFloat(txtLPCoeff3.Text));
// 流量系数
await WriteFloatAsync(_config.LargeFlowCoeff1, ParseFloat(txtLargeFlow1.Text));
await WriteFloatAsync(_config.SmallFlowCoeff1, ParseFloat(txtSmallFlow1.Text));
await WriteFloatAsync(_config.LargeFlowCoeff2, ParseFloat(txtLargeFlow2.Text));
await WriteFloatAsync(_config.SmallFlowCoeff2, ParseFloat(txtSmallFlow2.Text));
await WriteFloatAsync(_config.LargeFlowCoeff3, ParseFloat(txtLargeFlow3.Text));
await WriteFloatAsync(_config.SmallFlowCoeff3, ParseFloat(txtSmallFlow3.Text));
// 校准参数
await WriteFloatAsync(_config.PressureCalibZero, ParseFloat(txtPressureZero.Text));
await WriteFloatAsync(_config.PressureCalibSpan, ParseFloat(txtPressureSpan.Text));
await WriteFloatAsync(_config.FlowCalibZero, ParseFloat(txtFlowZero.Text));
await WriteFloatAsync(_config.FlowCalibSpan, ParseFloat(txtFlowSpan.Text));
MessageBox.Show("参数写入成功");
}
catch (Exception ex)
{
MessageBox.Show($"写入参数失败: {ex.Message}");
}
}
private float ParseFloat(string text) => float.TryParse(text, out var val) ? val : 0;
// 以下方法与之前相同但保留用于写入已在LostFocus中调用
private async Task<float> ReadFloatAsync(ushort address)
{
var registers = await ((ModbusTcpPlcService)_plcService).ReadHoldingRegistersAsync(address, 2);
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);
return ((ModbusTcpPlcService)_plcService).UshortToFloat(registers[1], registers[0]);
}
private async Task WriteFloatAsync(ushort address, float value)
{
byte[] bytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
ushort high = (ushort)((bytes[0] << 8) | bytes[1]);
ushort low = (ushort)((bytes[2] << 8) | bytes[3]);
await ((ModbusTcpPlcService)_plcService).WriteRegisterAsync(address, high);
await ((ModbusTcpPlcService)_plcService).WriteRegisterAsync((ushort)(address + 1), low);
await ((ModbusTcpPlcService)_plcService).WriteMultipleRegistersAsync(address, value);
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
/// <summary>
/// 辅助方法:查找视觉树中的所有指定类型子元素
/// </summary>
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) yield break;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
if (child is T t) yield return t;
foreach (var childOfChild in FindVisualChildren<T>(child)) yield return childOfChild;
}
}
}
}

View File

@@ -1,58 +1,65 @@
{
"PlcSettings": {
"IpAddress": "192.168.1.10",
"Port": 502,
"SlaveId": 1,
"PressureRegisterStation1": 6,
"PressureRegisterStation2": 8,
"PressureRegisterStation3": 10,
"PressureModeRegister": 200, // 高压/低压选择寄存器0=低压1=高压
"PressCoil": 100, // 加压线圈M100
"StartCoil": 20, // 启动线圈M20
"EnableCoil": 21, // 使能线圈M21
"StopCoil": 7, // 停止线圈M7
// Modbus TCP 连接参数
"IpAddress": "192.168.1.10", // PLC 的 IP 地址
"Port": 502, // Modbus TCP 端口号默认502
"SlaveId": 1, // PLC 从站地址通常为1
// 压力寄存器地址(每个工位占用 2 个连续 D 寄存器,存储 32 位浮点数)
"PressureRegisterStation1": 6, // 工位1 压力寄存器起始地址D6D7
"PressureRegisterStation2": 8, // 工位2 压力寄存器起始地址D8D9
"PressureRegisterStation3": 10, // 工位3 压力寄存器起始地址D10D11
// 压力模式选择(高压/低压)写入寄存器
"PressureModeRegister": 2, // 压力模式寄存器0=低压1=高压)
// 线圈控制M 元件)
"PressCoil": 100, // 加压线圈M100ON 开始加压OFF 停止
"StartCoil": 20, // 启动线圈M20ON 启动测试程序
"EnableCoil": 21, // 使能线圈M21状态反馈只读
"StopCoil": 7, // 停止线圈M7ON 停止测试
// 压力值系数(用于单位转换,例如 kPa 转 Pa 时设为 1000
"PressureFactor": 1.0,
"WetFlowRegister": 2,
"DryFlowRegister": 4,
"WetFlowFactor": 1.0,
"DryFlowFactor": 1.0,
"PressureUpperLimit": 300,
"PressureRate": 280,
"PressureCoeff": 282,
"HPCoeff1": 3120,
"LPCoeff1": 3122,
"HPCoeff2": 3124,
"LPCoeff2": 3126,
"HPCoeff3": 3128,
"LPCoeff3": 3130,
"LargeFlowCoeff1": 3048,
"SmallFlowCoeff1": 380,
"LargeFlowCoeff2": 1218,
"SmallFlowCoeff2": 1318,
"LargeFlowCoeff3": 1418,
"SmallFlowCoeff3": 1468,
"FlowModeRegister1": 5,
"FlowModeRegister2": 6,
"FlowModeRegister3": 7,
"PressureCalibZero": 3200,
"PressureCalibSpan": 3202,
"FlowCalibZero": 3204,
"FlowCalibSpan": 3206
// 流量寄存器地址(每个工位共用,但需配合流量模式切换)
"WetFlowRegister": 2, // 湿膜流量寄存器起始地址D2D3
"DryFlowRegister": 4, // 干膜流量寄存器起始地址D4D5
"WetFlowFactor": 1.0, // 湿膜流量系数
"DryFlowFactor": 1.0, // 干膜流量系数
// ========== 运维参数设置(可读写) ==========
"PressureUpperLimit": 300, // 加压上限D300
"PressureRate": 280, // 加压速率D280
"PressureCoeff": 282, // 加压系数D282
// 高压/低压系数(每个工位独立)
"HPCoeff1": 3120, // 工位1 高压系数D3120
"LPCoeff1": 3122, // 工位1 低压系数D3122
"HPCoeff2": 3124, // 工位2 高压系数D3124
"LPCoeff2": 3126, // 工位2 低压系数D3126
"HPCoeff3": 3128, // 工位3 高压系数D3128
"LPCoeff3": 3130, // 工位3 低压系数D3130
// 大/小流量系数(每个工位独立)
"LargeFlowCoeff1": 3048, // 工位1 大流量系数D3048
"SmallFlowCoeff1": 380, // 工位1 小流量系数D380
"LargeFlowCoeff2": 1218, // 工位2 大流量系数D1218
"SmallFlowCoeff2": 1318, // 工位2 小流量系数D1318
"LargeFlowCoeff3": 1418, // 工位3 大流量系数D1418
"SmallFlowCoeff3": 1468, // 工位3 小流量系数D1468
"FlowModeRegister": 4,
// 流量模式选择(大流量/小流量)写入寄存器
"FlowModeRegister1": 5, // 工位1 流量模式0=大流量,1=小流量)
"FlowModeRegister2": 6, // 工位2 流量模式
"FlowModeRegister3": 7, // 工位3 流量模式
// 校准系数(零点/量程寄存器地址约定若PLC未分配可自定义
"PressureCalibZero": 3200, // 压力零点系数D3200D3201
"PressureCalibSpan": 3202, // 压力量程系数D3202D3203
"FlowCalibZero": 3204, // 流量零点系数D3204D3205
"FlowCalibSpan": 3206 // 流量量程系数D3206D3207
}
}

Binary file not shown.