239 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			239 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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
 | ||
| {
 | ||
|     /// <summary>
 | ||
|     /// 汇川 Easy 系列 PLC 客户端 (Modbus TCP)
 | ||
|     /// 支持 D/M/X/Y 地址读写,整数、浮点数、字符串
 | ||
|     /// </summary>
 | ||
|     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<ushort, int>(ref _transactionId)));
 | ||
| 
 | ||
|         private async Task<byte[]> 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<int> 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<float> 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<string> 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();
 | ||
|         }
 | ||
|     }
 | ||
| }
 |