using NPOI.SS.Formula.Functions; using Sunny.UI; using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Check.Main.Common { /// /// 汇川 Easy 系列 PLC 客户端 (Modbus TCP) /// 支持 D/M/X/Y 地址读写,整数、浮点数、字符串 /// public class EasyPlcClient : IDisposable { private readonly TcpClient _tcpClient = new(); private NetworkStream _stream; private ushort _transactionId = 0; private readonly SemaphoreSlim _lock = new(1, 1); private readonly string _ip; private readonly int _port; private readonly byte _slaveId; public EasyPlcClient(string ip, int port = 502, byte slaveId = 1) { _ip = ip; _port = port; _slaveId = slaveId; } public bool IsConnected => _tcpClient != null && _tcpClient.Connected;//10.11添加 public async Task ConnectAsync() { await _tcpClient.ConnectAsync(_ip, _port); _stream = _tcpClient.GetStream(); } #region ---- 基础 Modbus ---- private ushort GetTransactionId() => unchecked((ushort)Interlocked.Increment(ref Unsafe.As(ref _transactionId))); private async Task SendAndReceiveAsync(byte[] pdu, ushort startAddr, ushort length = 1) { ushort tid = GetTransactionId(); ushort lengthField = (ushort)(pdu.Length + 1); byte[] mbap = { (byte)(tid >> 8), (byte)tid, 0x00, 0x00, // Protocol = 0 (byte)(lengthField >> 8), (byte)lengthField, _slaveId }; byte[] adu = new byte[mbap.Length + pdu.Length]; Buffer.BlockCopy(mbap, 0, adu, 0, mbap.Length); Buffer.BlockCopy(pdu, 0, adu, mbap.Length, pdu.Length); await _lock.WaitAsync(); try { await _stream.WriteAsync(adu, 0, adu.Length); byte[] header = new byte[7]; await _stream.ReadAsync(header, 0, 7); int respLength = (header[4] << 8) + header[5]; byte[] resp = new byte[respLength - 1]; await _stream.ReadAsync(resp, 0, resp.Length); return resp; } finally { _lock.Release(); } } #endregion #region ---- 地址解析 ---- private void ParseAddress(string address, out string area, out ushort offset) { var letters = Regex.Match(address, @"^[A-Za-z]+").Value.ToUpper(); var digits = Regex.Match(address, @"\d+").Value; if (string.IsNullOrEmpty(letters) || string.IsNullOrEmpty(digits)) throw new ArgumentException($"地址 {address} 无效"); area = letters; offset = (ushort)(ushort.Parse(digits) - 1); // 👉 注意:这里的偏移需根据你的 PLC Modbus 地址表调整 // 例如 D 区可能是 40001 起,M 区可能是 00001 起 // 目前假设 D -> Holding Register, M/Y -> Coil, X -> Discrete Input } #endregion #region ---- 整数读写 ---- public async Task WriteAsync(string address, int value) { ParseAddress(address, out string area, out ushort offset); switch (area) { case "D": // 写寄存器 byte[] pdu = { 0x06, (byte)(offset >> 8), (byte)offset, (byte)(value >> 8), (byte)value }; await SendAndReceiveAsync(pdu, offset); break; case "M": case "Y": // 写单线圈 byte[] coil = { 0x05, (byte)(offset >> 8), (byte)offset, (byte)(value != 0 ? 0xFF : 0x00), 0x00 }; await SendAndReceiveAsync(coil, offset); break; default: throw new NotSupportedException($"写 {area} 区未支持"); } } public async Task ReadAsync(string address) { ParseAddress(address, out string area, out ushort offset); switch (area) { case "D": // Holding Register byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 }; var resp = await SendAndReceiveAsync(pdu, offset); return (resp[1] << 8) + resp[2]; case "M": case "Y": // Coils byte[] pdu1 = { 0x01, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 }; var resp1 = await SendAndReceiveAsync(pdu1, offset); return (resp1[2] & 0x01) != 0 ? 1 : 0; case "X": // Inputs byte[] pdu2 = { 0x02, (byte)(offset >> 8), (byte)offset, 0x00, 0x01 }; var resp2 = await SendAndReceiveAsync(pdu2, offset); return (resp2[2] & 0x01) != 0 ? 1 : 0; default: throw new NotSupportedException($"读 {area} 区未支持"); } } #endregion #region ---- 浮点读写 (2寄存器) ---- public async Task WriteFloatAsync(string address, float value) { ParseAddress(address, out string area, out ushort offset); if (area != "D") throw new NotSupportedException("浮点仅支持 D 区"); byte[] bytes = BitConverter.GetBytes(value); if (BitConverter.IsLittleEndian) Array.Reverse(bytes); byte[] pdu = { 0x10, (byte)(offset >> 8), (byte)offset, 0x00, 0x02, 0x04, bytes[0], bytes[1], bytes[2], bytes[3] }; await SendAndReceiveAsync(pdu, offset, 2); } public async Task ReadFloatAsync(string address) { ParseAddress(address, out string area, out ushort offset); if (area != "D") throw new NotSupportedException("浮点仅支持 D 区"); byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, 0x00, 0x02 }; var resp = await SendAndReceiveAsync(pdu, offset, 2); byte[] data = { resp[1], resp[2], resp[3], resp[4] }; if (BitConverter.IsLittleEndian) Array.Reverse(data); return BitConverter.ToSingle(data, 0); } #endregion #region ---- 字符串读写 ---- public async Task WriteStringAsync(string address, string value, int maxLength) { ParseAddress(address, out string area, out ushort offset); if (area != "D") throw new NotSupportedException("字符串仅支持 D 区"); byte[] strBytes = Encoding.ASCII.GetBytes(value); if (strBytes.Length > maxLength) Array.Resize(ref strBytes, maxLength); // 每寄存器2字节 int regCount = (strBytes.Length + 1) / 2; byte[] data = new byte[regCount * 2]; Array.Copy(strBytes, data, strBytes.Length); byte[] pdu = new byte[7 + data.Length]; pdu[0] = 0x10; pdu[1] = (byte)(offset >> 8); pdu[2] = (byte)offset; pdu[3] = (byte)(regCount >> 8); pdu[4] = (byte)regCount; pdu[5] = (byte)(data.Length); Buffer.BlockCopy(data, 0, pdu, 6, data.Length); await SendAndReceiveAsync(pdu, offset, (ushort)regCount); } public async Task ReadStringAsync(string address, int length) { ParseAddress(address, out string area, out ushort offset); if (area != "D") throw new NotSupportedException("字符串仅支持 D 区"); int regCount = (length + 1) / 2; byte[] pdu = { 0x03, (byte)(offset >> 8), (byte)offset, (byte)(regCount >> 8), (byte)regCount }; var resp = await SendAndReceiveAsync(pdu, offset, (ushort)regCount); byte[] data = new byte[resp[0]]; Array.Copy(resp, 1, data, 0, data.Length); return Encoding.ASCII.GetString(data).TrimEnd('\0'); } #endregion public void Dispose() { _stream?.Dispose(); _tcpClient?.Close(); _lock?.Dispose(); } } }