237 lines
8.5 KiB
C#
237 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 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();
|
||
}
|
||
}
|
||
}
|