using Modbus.Device; using System; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using TabletTester2025.Models; namespace TabletTester2025.Services { 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 readonly SemaphoreSlim _connectLock = new(1, 1); private TcpClient? _tcpClient; private IModbusMaster? _master; public ModbusTcpPlcService(PlcConfiguration config) { _config = config; } public Task ConnectAsync() => EnsureConnectedAsync(); public async Task CheckConnectionAsync() { try { if (_config.HardnessCompleteCoil != 0) await ReadCoilAsync(_config.HardnessCompleteCoil); else await ReadHoldingRegistersAsync(_config.HardnessPoSun, 1); return true; } catch { CloseConnection(); return false; } } public async Task EnsureConnectedAsync(int retryCount = DefaultRetryCount) { if (HasOpenConnection) return; await _connectLock.WaitAsync(); try { if (HasOpenConnection) return; Exception? lastError = null; for (int attempt = 1; attempt <= retryCount; attempt++) { 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(); } } public async Task ReadFloatAsync(ushort startAddress) { var registers = await ReadHoldingRegistersAsync(startAddress, 2); return UshortToFloat(registers[1], registers[0]); } public async Task ReadIntAsync(ushort startAddress) { var registers = await ReadHoldingRegistersAsync(startAddress, 1); return registers[0]; } public Task WriteCoilAsync(ushort coilAddress, bool value) { return ExecuteAsync(master => master.WriteSingleCoilAsync(_config.SlaveId, coilAddress, value)); } public async Task ReadCoilAsync(ushort coilAddress) { bool[] result = await ExecuteAsync(master => master.ReadCoilsAsync(_config.SlaveId, coilAddress, 1)); return result[0]; } public Task WriteRegisterAsync(ushort registerAddress, ushort value) { return ExecuteAsync(master => master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value)); } public Task WriteFloatAsync(ushort startAddress, float value) { byte[] bytes = BitConverter.GetBytes(value); ushort[] registers = { (ushort)((bytes[2] << 8) | bytes[3]), (ushort)((bytes[0] << 8) | bytes[1]) }; return ExecuteAsync(master => master.WriteMultipleRegistersAsync(_config.SlaveId, startAddress, registers)); } 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(); try { if (_master == null) throw new InvalidOperationException("PLC连接未初始化"); return await action(_master); } catch { CloseConnection(); throw; } } private static float UshortToFloat(ushort high, ushort low) { byte[] bytes = new byte[4]; bytes[0] = (byte)(high >> 8); bytes[1] = (byte)(high & 0xFF); bytes[2] = (byte)(low >> 8); bytes[3] = (byte)(low & 0xFF); 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() { CloseConnection(); _connectLock.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; } } }