diff --git a/App.xaml.cs b/App.xaml.cs index 4c48bc6..675bba2 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -17,7 +17,7 @@ namespace TabletTester2025 public static PlcConfiguration PlcConfig { get; private set; } public static PharmaParameters CurrentPharmaParams { get; set; } = new PharmaParameters(); - protected override async void OnStartup(StartupEventArgs e) + protected override void OnStartup(StartupEventArgs e) { ExcelPackage.LicenseContext = LicenseContext.NonCommercial; base.OnStartup(e); @@ -96,22 +96,7 @@ namespace TabletTester2025 if (PlcConfig == null) throw new InvalidOperationException("PLC配置缺失"); - // 创建PLC服务(真实或模拟) - if (configuration["Plc:Type"] == "ModbusTcp") - PlcService = new ModbusTcpPlcService(PlcConfig); - else - PlcService = new PlcSimulator(); - - try - { - await PlcService.ConnectAsync(); - } - catch (Exception ex) - { - MessageBox.Show($"PLC连接失败,将使用模拟模式。\n{ex.Message}", "警告", MessageBoxButton.OK, MessageBoxImage.Warning); - PlcService = new PlcSimulator(); - await PlcService.ConnectAsync(); - } + PlcService = new ModbusTcpPlcService(PlcConfig); // 业务服务 var dbService = new DatabaseService(connectionString); diff --git a/Helpers/ValueConverters.cs b/Helpers/ValueConverters.cs index 0bd7523..9b0ce9c 100644 --- a/Helpers/ValueConverters.cs +++ b/Helpers/ValueConverters.cs @@ -43,7 +43,12 @@ namespace TabletTester2025.Helpers public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { string status = value as string; - return (status == "已连接") ? "Green" : "Red"; + return status switch + { + "已连接" => "Green", + "连接中" => "Orange", + _ => "Red" + }; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); @@ -73,4 +78,4 @@ namespace TabletTester2025.Helpers } -} \ No newline at end of file +} diff --git a/Services/BalanceService.cs b/Services/BalanceService.cs index 41d6d51..9b15fb9 100644 --- a/Services/BalanceService.cs +++ b/Services/BalanceService.cs @@ -1,15 +1,14 @@ -using System; +using System; using System.Threading.Tasks; namespace TabletTester2025.Services { public class BalanceService { - // 实际项目中请使用串口通讯 public async Task ReadWeightAsync() { await Task.Delay(100); - return new Random().NextDouble() * 10; // 模拟重量 0-10g + throw new InvalidOperationException("天平真实通讯未接入,禁止返回模拟重量"); } } -} \ No newline at end of file +} diff --git a/Services/ModbusTcpPlcService.cs b/Services/ModbusTcpPlcService.cs index 3dbda21..493309b 100644 --- a/Services/ModbusTcpPlcService.cs +++ b/Services/ModbusTcpPlcService.cs @@ -1,4 +1,4 @@ -using Modbus.Device; +using Modbus.Device; using System; using System.Net.Sockets; using System.Threading; @@ -7,102 +7,151 @@ using TabletTester2025.Models; namespace TabletTester2025.Services { - public class ModbusTcpPlcService : IPlcService + public class ModbusTcpPlcService : IPlcService, IDisposable { + private const int ConnectTimeoutMs = 3000; + private const int RetryDelayMs = 1000; + private const int DefaultRetryCount = 3; + private readonly PlcConfiguration _config; - private TcpClient _tcpClient; - private IModbusMaster _master; + private readonly SemaphoreSlim _connectLock = new(1, 1); + private TcpClient? _tcpClient; + private IModbusMaster? _master; public ModbusTcpPlcService(PlcConfiguration config) { _config = config; } - public async Task ConnectAsync() => await EnsureConnectedAsync(); + public Task ConnectAsync() => EnsureConnectedAsync(); - public async Task EnsureConnectedAsync(int retryCount = 3) + public async Task EnsureConnectedAsync(int retryCount = DefaultRetryCount) { - if (_tcpClient != null && _tcpClient.Connected) + if (HasOpenConnection) return; - for (int i = 0; i < retryCount; i++) + await _connectLock.WaitAsync(); + try { - try - { - _tcpClient?.Close(); - _tcpClient = new TcpClient(); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); - await _tcpClient.ConnectAsync(_config.IpAddress, _config.Port).WithCancellation(cts.Token); - _master = ModbusIpMaster.CreateIp(_tcpClient); - _master.Transport.ReadTimeout = 1000; - _master.Transport.WriteTimeout = 1000; + if (HasOpenConnection) return; - } - catch (Exception ex) when (i < retryCount - 1) + + Exception? lastError = null; + for (int attempt = 1; attempt <= retryCount; attempt++) { - System.Diagnostics.Debug.WriteLine($"连接失败,500ms后重试... {ex.Message}"); - await Task.Delay(500); + try + { + CloseConnection(); + + var client = new TcpClient(); + using var cts = new CancellationTokenSource(ConnectTimeoutMs); + await client.ConnectAsync(_config.IpAddress, _config.Port).WithCancellation(cts.Token); + + var master = ModbusIpMaster.CreateIp(client); + master.Transport.ReadTimeout = 1000; + master.Transport.WriteTimeout = 1000; + + _tcpClient = client; + _master = master; + return; + } + catch (Exception ex) + { + lastError = ex; + CloseConnection(); + System.Diagnostics.Debug.WriteLine($"PLC连接失败,第{attempt}/{retryCount}次:{ex.Message}"); + + if (attempt < retryCount) + await Task.Delay(RetryDelayMs); + } } + + throw new InvalidOperationException($"无法连接到 PLC ({_config.IpAddress}:{_config.Port})", lastError); + } + finally + { + _connectLock.Release(); } - throw new Exception($"无法连接到 PLC ({_config.IpAddress}:{_config.Port})"); } - //读取寄存器返回浮点型 + public async Task ReadFloatAsync(ushort startAddress) { - await EnsureConnectedAsync(); var registers = await ReadHoldingRegistersAsync(startAddress, 2); return UshortToFloat(registers[1], registers[0]); } - //读取返回整型 + public async Task ReadIntAsync(ushort startAddress) { - await EnsureConnectedAsync(); var registers = await ReadHoldingRegistersAsync(startAddress, 1); return registers[0]; } - public async Task WriteCoilAsync(ushort coilAddress, bool value) + public Task WriteCoilAsync(ushort coilAddress, bool value) { - await EnsureConnectedAsync(); - await _master.WriteSingleCoilAsync(_config.SlaveId, coilAddress, value); + return ExecuteAsync(master => master.WriteSingleCoilAsync(_config.SlaveId, coilAddress, value)); } public async Task ReadCoilAsync(ushort coilAddress) { - await EnsureConnectedAsync(); - bool[] result = await _master.ReadCoilsAsync(_config.SlaveId, coilAddress, 1); + bool[] result = await ExecuteAsync(master => master.ReadCoilsAsync(_config.SlaveId, coilAddress, 1)); return result[0]; } - public async Task WriteRegisterAsync(ushort registerAddress, ushort value) + public Task WriteRegisterAsync(ushort registerAddress, ushort value) { - await EnsureConnectedAsync(); - await _master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value); + return ExecuteAsync(master => master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value)); } - public async Task WriteFloatAsync(ushort startAddress, float value) + public Task WriteFloatAsync(ushort startAddress, float value) { - await EnsureConnectedAsync(); byte[] bytes = BitConverter.GetBytes(value); ushort[] registers = { (ushort)((bytes[2] << 8) | bytes[3]), (ushort)((bytes[0] << 8) | bytes[1]) }; - await _master.WriteMultipleRegistersAsync(_config.SlaveId, startAddress, registers); + + return ExecuteAsync(master => master.WriteMultipleRegistersAsync(_config.SlaveId, startAddress, registers)); } - public async Task ReadHoldingRegistersAsync(ushort startAddress, ushort count) + public Task ReadHoldingRegistersAsync(ushort startAddress, ushort count) + { + return ExecuteAsync(master => master.ReadHoldingRegistersAsync(_config.SlaveId, startAddress, count)); + } + + public bool IsConnected => HasOpenConnection; + + private bool HasOpenConnection => _tcpClient?.Connected == true && _master != null; + + private async Task ExecuteAsync(Func action) + { + await ExecuteAsync(async master => + { + await action(master); + return true; + }); + } + + private async Task ExecuteAsync(Func> action) { await EnsureConnectedAsync(); - return await _master.ReadHoldingRegistersAsync(_config.SlaveId, startAddress, count); + + try + { + if (_master == null) + throw new InvalidOperationException("PLC连接未初始化"); + + return await action(_master); + } + catch + { + CloseConnection(); + throw; + } } - public bool IsConnected => _tcpClient != null && _tcpClient.Connected; - - private float UshortToFloat(ushort high, ushort low) + private static float UshortToFloat(ushort high, ushort low) { - // Modbus 大端模式:高16位在前,低16位在后 byte[] bytes = new byte[4]; bytes[0] = (byte)(high >> 8); bytes[1] = (byte)(high & 0xFF); @@ -111,11 +160,19 @@ namespace TabletTester2025.Services return BitConverter.ToSingle(bytes, 0); } + private void CloseConnection() + { + try { _master?.Dispose(); } catch { } + try { _tcpClient?.Close(); } catch { } + try { _tcpClient?.Dispose(); } catch { } + _master = null; + _tcpClient = null; + } + public void Dispose() { - _master?.Dispose(); - _tcpClient?.Close(); - _tcpClient?.Dispose(); + CloseConnection(); + _connectLock.Dispose(); } } @@ -124,11 +181,12 @@ namespace TabletTester2025.Services public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); - using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) + using (cancellationToken.Register(s => ((TaskCompletionSource)s!).TrySetResult(true), tcs)) { if (task != await Task.WhenAny(task, tcs.Task)) throw new OperationCanceledException(cancellationToken); } + await task; } } diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 08a1ad2..553710d 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -18,6 +18,8 @@ namespace TabletTester2025.ViewModels private readonly AlarmService _alarm; private readonly PlcConfiguration _plcConfig; private DispatcherTimer _timer; + private bool _isConnecting; + private bool _isUpdatingRealtime; @@ -48,7 +50,7 @@ namespace TabletTester2025.ViewModels ExportAllCommand = new AsyncRelayCommand(ExportAllAsync); - _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; + _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _timer.Tick += OnTimerTick; _ = ConnectToPlc(); _timer.Start(); @@ -73,15 +75,53 @@ namespace TabletTester2025.ViewModels private async Task ConnectToPlc() { - try { await _plc.ConnectAsync(); PlcStatus = "已连接"; } - catch { PlcStatus = "连接失败"; } + if (_isConnecting) + return; + + try + { + _isConnecting = true; + PlcStatus = "连接中"; + await _plc.ConnectAsync(); + PlcStatus = _plc.IsConnected ? "已连接" : "连接失败"; + } + catch + { + PlcStatus = "连接失败"; + } + finally + { + _isConnecting = false; + } } private async void OnTimerTick(object sender, EventArgs e) { CurrentTime = DateTime.Now.ToString("HH:mm:ss"); - if (PlcStatus != "已连接") return; - await Tester.UpdateRealTimeData(); + + if (!_plc.IsConnected) + { + await ConnectToPlc(); + return; + } + + PlcStatus = "已连接"; + if (_isUpdatingRealtime) + return; + + try + { + _isUpdatingRealtime = true; + await Tester.UpdateRealTimeData(); + } + catch + { + PlcStatus = "连接失败"; + } + finally + { + _isUpdatingRealtime = false; + } } private async Task ExportAllAsync() diff --git a/ViewModels/StationViewModel.cs b/ViewModels/StationViewModel.cs index 7fa16b4..419503f 100644 --- a/ViewModels/StationViewModel.cs +++ b/ViewModels/StationViewModel.cs @@ -21,7 +21,6 @@ namespace TabletTester2025.ViewModels private readonly DatabaseService _db; private readonly ExcelExportService _excel; private readonly AlarmService _alarm; - private readonly BalanceService _balance; // ✅ 新增天平服务 private DispatcherTimer _disintegrationTimer; private bool _isLoadingDissolution1Time; private bool _isLoadingDissolution2Time; @@ -149,7 +148,6 @@ namespace TabletTester2025.ViewModels _db = db; _excel = excel; _alarm = alarm; - _balance = new BalanceService(); // 实例化天平服务(模拟) StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync); StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync); @@ -632,6 +630,7 @@ namespace TabletTester2025.ViewModels CurrentTest = TestType.Friability; Phase = TestPhase.Running; FriabilityPass = false; + bool resultReady = false; try { @@ -641,7 +640,10 @@ namespace TabletTester2025.ViewModels { throw new InvalidOperationException("未配置脆碎度启动线圈地址"); } - WeightBefore = await _balance.ReadWeightAsync(); + WeightBefore = await ReadFriabilityWeightAsync(_plcConfig.WeightBefore, "脆碎前重量"); + if (WeightBefore <= 0) + throw new InvalidOperationException("脆碎前重量必须大于0"); + await _plc.WriteCoilAsync(startCoil, true); int totalRounds = 100; // 药典标准脆碎度总圈数:100圈 double rpm = FriabilityTargetRpm; // 界面设置的目标转速(r/min) @@ -664,11 +666,15 @@ namespace TabletTester2025.ViewModels // 等待100ms,再更新下一次 await Task.Delay(100); } - WeightAfter = await _balance.ReadWeightAsync(); + if (Phase != TestPhase.Running) + throw new InvalidOperationException("脆碎度测试已停止,未保存结果"); + + WeightAfter = await ReadFriabilityWeightAsync(_plcConfig.WeightAfter, "脆碎后重量"); FriabilityCurrentRpm = FriabilityTargetRpm; LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100;//失重率 FriabilityPass = LossPercent <= App.CurrentPharmaParams.FriabilityMaxLossPercent; //标准值 + resultReady = true; // 标记测试为已完成 Phase = TestPhase.Completed; } @@ -683,10 +689,23 @@ namespace TabletTester2025.ViewModels { Phase = TestPhase.Idle; FriabilityRemainingRounds = 100; - await SaveBatchResult(); + if (resultReady) + await SaveBatchResult(); } } + private async Task ReadFriabilityWeightAsync(ushort registerAddress, string label) + { + if (registerAddress == 0) + throw new InvalidOperationException($"{label}寄存器未配置"); + + double value = await _plc.ReadFloatAsync(registerAddress); + if (!double.IsFinite(value) || value < 0) + throw new InvalidOperationException($"{label}数据异常"); + + return value; + } + private async Task RunDisintegrationAsync() { if (Phase != TestPhase.Idle) return; diff --git a/appsettings.json b/appsettings.json index 3f70f67..5e35ffe 100644 --- a/appsettings.json +++ b/appsettings.json @@ -3,8 +3,8 @@ "DefaultConnection": "Data Source=TabletTests.db" }, "Plc": { - "Type": "Simulator", // "Simulator" 或 "ModbusTcp" - "IpAddress": "127.0.0.1", + "Type": "ModbusTcp", + "IpAddress": "192.168.1.10", "Port": 502, "SlaveId": 1, "HardnessValue": 100,