mirror of
https://gitee.com/langsisi_admin/serein-flow
synced 2026-03-02 15:50:47 +08:00
通过Emit优化Script脚本的解释执行;出于后期更新的方向,暂时隐藏表达式节点、条件表达式节点、全局数据节点;流程图转c#代码新增对于Script脚本的支持,Script脚本现在可以原生导出为C#代码。
This commit is contained in:
17
Library/Network/Modbus/HexExtensions.cs
Normal file
17
Library/Network/Modbus/HexExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public static class HexExtensions
|
||||
{
|
||||
public static string ToHexString(this byte[] data, string separator = " ")
|
||||
{
|
||||
if (data == null) return string.Empty;
|
||||
return BitConverter.ToString(data).Replace("-", separator);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Library/Network/Modbus/IModbusClient.cs
Normal file
61
Library/Network/Modbus/IModbusClient.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus 客户端通用接口 (TCP/RTU 通用)
|
||||
/// </summary>
|
||||
public interface IModbusClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 报文发送时
|
||||
/// </summary>
|
||||
Action<byte[]> OnTx { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收到报文时
|
||||
/// </summary>
|
||||
Action<byte[]> OnRx { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 读取线圈状态 (0x01)
|
||||
/// </summary>
|
||||
Task<bool[]> ReadCoils(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// 读取离散输入状态 (0x02)
|
||||
/// </summary>
|
||||
Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// 读取保持寄存器 (0x03)
|
||||
/// </summary>
|
||||
Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// 读取输入寄存器 (0x04)
|
||||
/// </summary>
|
||||
Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// 写单个线圈 (0x05)
|
||||
/// </summary>
|
||||
Task WriteSingleCoil(ushort address, bool value);
|
||||
|
||||
/// <summary>
|
||||
/// 写单个寄存器 (0x06)
|
||||
/// </summary>
|
||||
Task WriteSingleRegister(ushort address, ushort value);
|
||||
|
||||
/// <summary>
|
||||
/// 写多个线圈 (0x0F)
|
||||
/// </summary>
|
||||
Task WriteMultipleCoils(ushort startAddress, bool[] values);
|
||||
|
||||
/// <summary>
|
||||
/// 写多个寄存器 (0x10)
|
||||
/// </summary>
|
||||
Task WriteMultipleRegisters(ushort startAddress, ushort[] values);
|
||||
}
|
||||
}
|
||||
119
Library/Network/Modbus/ModbusClientFactory.cs
Normal file
119
Library/Network/Modbus/ModbusClientFactory.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public static class ModbusClientFactory
|
||||
{
|
||||
private static readonly char[] separator = new[] { ':' };
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Modbus 客户端实例
|
||||
/// </summary>
|
||||
/// <param name="connectionString">
|
||||
/// 连接字符串格式:
|
||||
/// TCP示例:"tcp:192.168.1.100:502"
|
||||
/// UCP示例:"ucp:192.168.1.100:502"
|
||||
/// RTU示例:"rtu:COM3:9600:1" (格式:rtu:串口名:波特率:从站地址)
|
||||
/// </param>
|
||||
public static IModbusClient Create(string connectionString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
throw new ArgumentException("connectionString 不能为空");
|
||||
var parts = connectionString.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
||||
//var parts = connectionString.Split(':',options: StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 2)
|
||||
throw new ArgumentException("connectionString 格式错误");
|
||||
|
||||
var protocol = parts[0].ToLower();
|
||||
|
||||
if (protocol == "tcp")
|
||||
{
|
||||
// tcp:host:port
|
||||
if (parts.Length < 3)
|
||||
throw new ArgumentException("TCP格式应为 tcp:host:port");
|
||||
|
||||
string host = parts[1];
|
||||
if (!int.TryParse(parts[2], out int port))
|
||||
port = 502; // 默认端口
|
||||
|
||||
return new ModbusTcpClient(host, port);
|
||||
}
|
||||
else if (protocol == "ucp")
|
||||
{
|
||||
// ucp:host:port
|
||||
if (parts.Length < 3)
|
||||
throw new ArgumentException("TCP格式应为 tcp:host:port");
|
||||
|
||||
string host = parts[1];
|
||||
if (!int.TryParse(parts[2], out int port))
|
||||
port = 502; // 默认端口
|
||||
|
||||
return new ModbusUdpClient(host, port);
|
||||
}
|
||||
else if (protocol == "rtu")
|
||||
{
|
||||
// rtu:portName:baudRate:slaveId
|
||||
if (parts.Length < 4)
|
||||
throw new ArgumentException("RTU格式应为 rtu:portName:baudRate:slaveId");
|
||||
|
||||
string portName = parts[1];
|
||||
if (!int.TryParse(parts[2], out int baudRate))
|
||||
baudRate = 9600;
|
||||
|
||||
if (!byte.TryParse(parts[3], out byte slaveId))
|
||||
slaveId = 1;
|
||||
|
||||
return new ModbusRtuClient(portName, baudRate, slaveId: slaveId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"不支持的协议类型: {protocol}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Modbus TCP 客户端
|
||||
/// </summary>
|
||||
/// <param name="host">服务器地址</param>
|
||||
/// <param name="port">端口,默认502</param>
|
||||
public static ModbusTcpClient CreateTcpClient(string host, int port = 502)
|
||||
{
|
||||
return new ModbusTcpClient(host, port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Modbus TCP 客户端
|
||||
/// </summary>
|
||||
/// <param name="host">服务器地址</param>
|
||||
/// <param name="port">端口,默认502</param>
|
||||
public static ModbusUdpClient CreateUdpClient(string host, int port = 502)
|
||||
{
|
||||
return new ModbusUdpClient(host, port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Modbus RTU 客户端
|
||||
/// </summary>
|
||||
/// <param name="portName">串口名,比如 "COM3"</param>
|
||||
/// <param name="baudRate">波特率,默认9600</param>
|
||||
/// <param name="parity">校验,默认None</param>
|
||||
/// <param name="dataBits">数据位,默认8</param>
|
||||
/// <param name="stopBits">停止位,默认1</param>
|
||||
/// <param name="slaveId">从站地址,默认1</param>
|
||||
public static ModbusRtuClient CreateRtuClient(string portName,
|
||||
int baudRate = 9600,
|
||||
Parity parity = Parity.None,
|
||||
int dataBits = 8,
|
||||
StopBits stopBits = StopBits.One,
|
||||
byte slaveId = 1)
|
||||
{
|
||||
return new ModbusRtuClient(portName, baudRate, parity, dataBits, stopBits, slaveId);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Library/Network/Modbus/ModbusException.cs
Normal file
35
Library/Network/Modbus/ModbusException.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public class ModbusException : Exception
|
||||
{
|
||||
public byte FunctionCode { get; }
|
||||
public byte ExceptionCode { get; }
|
||||
|
||||
public ModbusException(byte functionCode, byte exceptionCode)
|
||||
: base($"Modbus异常码=0x{functionCode:X2},0x{exceptionCode:X2}({GetExceptionMessage(exceptionCode)})")
|
||||
{
|
||||
FunctionCode = functionCode;
|
||||
ExceptionCode = exceptionCode;
|
||||
}
|
||||
|
||||
private static string GetExceptionMessage(byte code) => code switch
|
||||
{
|
||||
0x01 => "非法功能。确认功能码是否被目标设备支持;检查设备固件版本是否过低;修改主站请求为设备支持的功能码", // 功能码错误
|
||||
0x02 => "非法数据地址。检查主站请求的寄存器地址和长度是否越界;确保设备配置的寄存器数量正确", // 数据地址错误
|
||||
0x03 => "非法数据值。检查写入的数值是否在设备支持的范围内;核对协议文档中对应寄存器的取值要求", // 数据值错误
|
||||
0x04 => "从站设备故障。检查设备运行状态和日志;尝试重启设备;排查硬件或内部程序错误", // 从设备故障
|
||||
0x05 => "确认。主站需通过轮询或延时机制等待处理完成,再次查询结果", // 确认
|
||||
0x06 => "从站设备忙。增加请求重试延时;避免高频率发送编程指令", // 从设备忙
|
||||
0x08 => "存储奇偶性差错。尝试重新发送请求;如错误持续出现,检查存储器硬件或文件一致性", // 内存奇偶校验错误
|
||||
0x0A => "不可用网关路径。检查网关配置和负载;确认目标设备的网络连接可用性", // 网关路径不可用
|
||||
0x0B => "网关目标设备响应失败。检查目标设备是否在线;检查网关的路由配置与网络连接", // 网关目标设备未响应
|
||||
_ => $"未知错误" // 未知错误
|
||||
};
|
||||
}
|
||||
}
|
||||
25
Library/Network/Modbus/ModbusRequest.cs
Normal file
25
Library/Network/Modbus/ModbusRequest.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
using Serein.Library.Network.Modbus;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public class ModbusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 功能码
|
||||
/// </summary>
|
||||
public ModbusFunctionCode FunctionCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// PDU (Protocol Data Unit) 数据,不包括从站地址和CRC
|
||||
/// </summary>
|
||||
public byte[] PDU { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 异步任务完成源,用于等待响应
|
||||
/// </summary>
|
||||
public TaskCompletionSource<byte[]> Completion { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
323
Library/Network/Modbus/ModbusRtuClient.cs
Normal file
323
Library/Network/Modbus/ModbusRtuClient.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using Serein.Library.Network.Modbus;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
|
||||
|
||||
public class ModbusRtuClient : IModbusClient
|
||||
{
|
||||
public Action<byte[]> OnTx { get; set; }
|
||||
public Action<byte[]> OnRx { get; set; }
|
||||
|
||||
|
||||
private readonly SerialPort _serialPort;
|
||||
private readonly SemaphoreSlim _requestLock = new SemaphoreSlim(1, 1);
|
||||
private readonly byte _slaveId;
|
||||
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public ModbusRtuClient(string portName, int baudRate = 9600, Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One, byte slaveId = 1)
|
||||
{
|
||||
_slaveId = slaveId;
|
||||
_serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits)
|
||||
{
|
||||
ReadTimeout = 1000,
|
||||
WriteTimeout = 1000
|
||||
};
|
||||
_serialPort.Open();
|
||||
|
||||
}
|
||||
|
||||
#region 功能码封装
|
||||
|
||||
public async Task<bool[]> ReadCoils(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var response = await SendAsync(ModbusFunctionCode.ReadCoils, pdu);
|
||||
return ParseDiscreteBits(response, quantity);
|
||||
}
|
||||
|
||||
public async Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var response = await SendAsync(ModbusFunctionCode.ReadDiscreteInputs, pdu);
|
||||
return ParseDiscreteBits(response, quantity);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var response = await SendAsync(ModbusFunctionCode.ReadHoldingRegisters, pdu);
|
||||
return ParseRegisters(response, quantity);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var response = await SendAsync(ModbusFunctionCode.ReadInputRegisters, pdu);
|
||||
return ParseRegisters(response, quantity);
|
||||
}
|
||||
|
||||
public async Task WriteSingleCoil(ushort address, bool value)
|
||||
{
|
||||
var pdu = new byte[]
|
||||
{
|
||||
(byte)(address >> 8),
|
||||
(byte)(address & 0xFF),
|
||||
value ? (byte)0xFF : (byte)0x00,
|
||||
0x00
|
||||
};
|
||||
await SendAsync(ModbusFunctionCode.WriteSingleCoil, pdu);
|
||||
}
|
||||
|
||||
public async Task WriteSingleRegister(ushort address, ushort value)
|
||||
{
|
||||
var pdu = new byte[]
|
||||
{
|
||||
(byte)(address >> 8),
|
||||
(byte)(address & 0xFF),
|
||||
(byte)(value >> 8),
|
||||
(byte)(value & 0xFF)
|
||||
};
|
||||
await SendAsync(ModbusFunctionCode.WriteSingleRegister, pdu);
|
||||
}
|
||||
|
||||
|
||||
public async Task WriteMultipleCoils(ushort startAddress, bool[] values)
|
||||
{
|
||||
if (values == null || values.Length == 0)
|
||||
throw new ArgumentException("values 不能为空");
|
||||
|
||||
int byteCount = (values.Length + 7) / 8; // 需要多少字节
|
||||
byte[] coilData = new byte[byteCount];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i])
|
||||
coilData[i / 8] |= (byte)(1 << (i % 8)); // 设置对应位
|
||||
}
|
||||
|
||||
var pdu = new List<byte>
|
||||
{
|
||||
(byte)(startAddress >> 8), // 起始地址高字节
|
||||
(byte)(startAddress & 0xFF), // 起始地址低字节
|
||||
(byte)(values.Length >> 8), // 数量高字节
|
||||
(byte)(values.Length & 0xFF), // 数量低字节
|
||||
(byte)coilData.Length // 数据字节数
|
||||
};
|
||||
pdu.AddRange(coilData);
|
||||
|
||||
await SendAsync(ModbusFunctionCode.WriteMultipleCoils, pdu.ToArray());
|
||||
}
|
||||
|
||||
public async Task WriteMultipleRegisters(ushort startAddress, ushort[] values)
|
||||
{
|
||||
if (values == null || values.Length == 0)
|
||||
throw new ArgumentException("values 不能为空");
|
||||
|
||||
var arrlen = 5 + values.Length * 2;
|
||||
var pdu = new byte[arrlen];
|
||||
|
||||
pdu[0] = (byte)(startAddress >> 8); // 起始地址高字节
|
||||
pdu[1] = (byte)(startAddress & 0xFF); // 起始地址低字节
|
||||
pdu[2] = (byte)(values.Length >> 8); // 寄存器数量高字节
|
||||
pdu[3] = (byte)(values.Length & 0xFF); // 寄存器数量低字节
|
||||
pdu[4] = (byte)(values.Length * 2); // 数据字节数
|
||||
|
||||
// 添加寄存器数据(每个寄存器 2 字节:高字节在前)
|
||||
var index = 5;
|
||||
foreach(var val in values)
|
||||
{
|
||||
pdu[index++] = (byte)(val >> 8);
|
||||
pdu[index++] = (byte)(val & 0xFF);
|
||||
|
||||
}
|
||||
|
||||
/* var pdu = new List<byte>
|
||||
{
|
||||
(byte)(startAddress >> 8), // 起始地址高字节
|
||||
(byte)(startAddress & 0xFF), // 起始地址低字节
|
||||
(byte)(values.Length >> 8), // 寄存器数量高字节
|
||||
(byte)(values.Length & 0xFF), // 寄存器数量低字节
|
||||
(byte)(values.Length * 2) // 数据字节数
|
||||
};
|
||||
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
pdu.Add((byte)(val >> 8));
|
||||
pdu.Add((byte)(val & 0xFF));
|
||||
}*/
|
||||
|
||||
await SendAsync(ModbusFunctionCode.WriteMultipleRegister, pdu);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 核心通信
|
||||
|
||||
public async Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
|
||||
{
|
||||
await _requestLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// 构造 RTU 帧
|
||||
byte[] frame = BuildFrame(_slaveId, (byte)functionCode, pdu);
|
||||
OnTx?.Invoke(frame); // 触发发送日志
|
||||
await _serialPort.BaseStream.WriteAsync(frame, 0, frame.Length, _cts.Token);
|
||||
await _serialPort.BaseStream.FlushAsync(_cts.Token);
|
||||
|
||||
// 接收响应
|
||||
var response = await ReceiveResponseAsync();
|
||||
OnRx?.Invoke(response); // 触发接收日志
|
||||
// 检查功能码是否异常响应
|
||||
if ((response[1] & 0x80) != 0)
|
||||
{
|
||||
byte exceptionCode = response[2];
|
||||
throw new ModbusException(response[1], exceptionCode);
|
||||
}
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_requestLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 接收响应
|
||||
/// </summary>
|
||||
private async Task<byte[]> ReceiveResponseAsync()
|
||||
{
|
||||
var buffer = new byte[256];
|
||||
int offset = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int read = await _serialPort.BaseStream.ReadAsync(buffer, offset, buffer.Length - offset, _cts.Token);
|
||||
offset += read;
|
||||
|
||||
// 最小RTU帧:地址(1) + 功能码(1) + 数据(N) + CRC(2)
|
||||
if (offset >= 5)
|
||||
{
|
||||
int frameLength = offset;
|
||||
if (!ValidateCrc(buffer, 0, frameLength))
|
||||
throw new IOException("CRC 校验失败");
|
||||
|
||||
byte[] response = new byte[frameLength - 2];
|
||||
Array.Copy(buffer, 0, response, 0, frameLength - 2);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] BuildFrame(byte slaveAddr, byte functionCode, byte[] pdu)
|
||||
{
|
||||
var frame = new byte[2 + pdu.Length + 2]; // 地址 + 功能码 + PDU + CRC
|
||||
frame[0] = slaveAddr;
|
||||
frame[1] = functionCode;
|
||||
Array.Copy(pdu, 0, frame, 2, pdu.Length);
|
||||
ushort crc = Crc16(frame, 0, frame.Length - 2);
|
||||
frame[frame.Length - 2] = (byte)(crc & 0xFF);
|
||||
frame[frame.Length - 1] = (byte)(crc >> 8);
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static bool ValidateCrc(byte[] buffer, int offset, int length)
|
||||
{
|
||||
ushort crcCalc = Crc16(buffer, offset, length - 2);
|
||||
ushort crcRecv = (ushort)(buffer[length - 2] | (buffer[length - 1] << 8));
|
||||
return crcCalc == crcRecv;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PDU与解析
|
||||
|
||||
private byte[] BuildReadPdu(ushort startAddress, ushort quantity)
|
||||
{
|
||||
|
||||
byte[] buffer = new byte[4];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), startAddress); // 起始地址高低字节
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2, 2), quantity); // 读取数量高低字节
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private bool[] ParseDiscreteBits(byte[] pdu, ushort count)
|
||||
{
|
||||
int byteCount = pdu[2]; // 第2字节是后续的字节数量
|
||||
int dataIndex = 3; // 数据从第3字节开始(0-based)
|
||||
|
||||
var result = new bool[count];
|
||||
|
||||
for (int i = 0, bytePos = 0, bitPos = 0; i < count; i++, bitPos++)
|
||||
{
|
||||
if (bitPos == 8)
|
||||
{
|
||||
bitPos = 0;
|
||||
bytePos++;
|
||||
}
|
||||
result[i] = ((pdu[dataIndex + bytePos] >> bitPos) & 0x01) != 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ushort[] ParseRegisters(byte[] pdu, ushort count)
|
||||
{
|
||||
var result = new ushort[count];
|
||||
int dataStart = 3; // 数据从第3字节开始
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int offset = dataStart + i * 2;
|
||||
result[i] = (ushort)((pdu[offset] << 8) | pdu[offset + 1]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CRC16
|
||||
private static ushort Crc16(byte[] data, int offset, int length)
|
||||
{
|
||||
const ushort polynomial = 0xA001;
|
||||
ushort crc = 0xFFFF;
|
||||
|
||||
for (int i = offset; i < offset + length; i++)
|
||||
{
|
||||
crc ^= data[i];
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
if ((crc & 0x0001) != 0)
|
||||
crc = (ushort)((crc >> 1) ^ polynomial);
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_serialPort?.Close();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
19
Library/Network/Modbus/ModbusRtuRequest.cs
Normal file
19
Library/Network/Modbus/ModbusRtuRequest.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
using Serein.Library.Network.Modbus;
|
||||
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus RTU 请求实体(串口模式下无效)
|
||||
/// </summary>
|
||||
public sealed class ModbusRtuRequest : ModbusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 从站地址(1~247)
|
||||
/// </summary>
|
||||
public byte SlaveAddress { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,19 +4,21 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus TCP 客户端
|
||||
/// </summary>
|
||||
public class ModbusTcpClient : IDisposable
|
||||
public class ModbusTcpClient : IModbusClient
|
||||
{
|
||||
public Action<byte[]> OnTx { get; set; }
|
||||
public Action<byte[]> OnRx { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息通道
|
||||
/// </summary>
|
||||
@@ -264,10 +266,15 @@ namespace Serein.Library.Network.Modbus
|
||||
/// <returns></returns>
|
||||
private async Task ProcessQueueAsync()
|
||||
{
|
||||
var request = await _channel.Reader.ReadAsync();
|
||||
byte[] packet = BuildPacket(request.TransactionId, 0x01, (byte)request.FunctionCode, request.PDU);
|
||||
await _stream.WriteAsync(packet, 0, packet.Length);
|
||||
await _stream.FlushAsync();
|
||||
while (_tcpClient.Connected)
|
||||
{
|
||||
var request = await _channel.Reader.ReadAsync();
|
||||
byte[] packet = BuildPacket(request.TransactionId, 0x01, (byte)request.FunctionCode, request.PDU);
|
||||
OnTx?.Invoke(packet); // 触发发送日志
|
||||
await _stream.WriteAsync(packet, 0, packet.Length);
|
||||
await _stream.FlushAsync();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -275,6 +282,8 @@ namespace Serein.Library.Network.Modbus
|
||||
/// </summary>
|
||||
/// <param name="functionCode">功能码</param>
|
||||
/// <param name="pdu">内容</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <param name="maxRetries">最大重发次数</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="TimeoutException"></exception>
|
||||
public Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
|
||||
@@ -305,12 +314,8 @@ namespace Serein.Library.Network.Modbus
|
||||
var buffer = new byte[1024];
|
||||
while (true)
|
||||
{
|
||||
#if NET462
|
||||
int len = await _stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
#else
|
||||
int len = await _stream.ReadAsync(buffer.AsMemory(0, buffer.Length)).ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
//int len = await _stream.ReadAsync(buffer.AsMemory(0, buffer.Length)).ConfigureAwait(false);
|
||||
if (len == 0) return; // 连接关闭
|
||||
|
||||
if (len < 6)
|
||||
@@ -320,6 +325,7 @@ namespace Serein.Library.Network.Modbus
|
||||
}
|
||||
|
||||
|
||||
|
||||
ushort protocolId = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(2, 2));
|
||||
if (protocolId != 0x0000)
|
||||
{
|
||||
@@ -339,7 +345,22 @@ namespace Serein.Library.Network.Modbus
|
||||
if (_pendingRequests.TryRemove(transactionId, out var tcs))
|
||||
{
|
||||
var responsePdu = new ReadOnlySpan<byte>(buffer, 6, dataLength).ToArray();
|
||||
tcs.SetResult(responsePdu); // 如需 byte[] 则 ToArray
|
||||
if (OnRx is not null)
|
||||
{
|
||||
var packet = new ReadOnlySpan<byte>(buffer, 0, 6 + dataLength).ToArray();
|
||||
OnRx?.Invoke(packet); // 触发接收日志
|
||||
}
|
||||
|
||||
// 检查是否异常响应
|
||||
if ((responsePdu[1] & 0x80) != 0)
|
||||
{
|
||||
byte exceptionCode = responsePdu[2];
|
||||
tcs.SetException(new ModbusException(responsePdu[1], exceptionCode));
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.SetResult(responsePdu);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -400,4 +421,6 @@ namespace Serein.Library.Network.Modbus
|
||||
_tcpClient?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
/// <summary>
|
||||
/// Modbus TCP 请求实体
|
||||
/// </summary>
|
||||
public class ModbusTcpRequest
|
||||
public class ModbusTcpRequest : ModbusRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 事务ID
|
||||
/// </summary>
|
||||
public ushort TransactionId { get; set; }
|
||||
/// <summary>
|
||||
/// 功能码
|
||||
/// </summary>
|
||||
public ModbusFunctionCode FunctionCode { get; set; }
|
||||
/// <summary>
|
||||
/// PDU 数据
|
||||
/// </summary>
|
||||
public byte[] PDU { get; set; }
|
||||
/// <summary>
|
||||
/// 请求的完成源,用于异步等待响应
|
||||
/// </summary>
|
||||
public TaskCompletionSource<byte[]> Completion { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
260
Library/Network/Modbus/ModbusUdpClient.cs
Normal file
260
Library/Network/Modbus/ModbusUdpClient.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace Serein.Library.Network.Modbus
|
||||
{
|
||||
public class ModbusUdpClient : IModbusClient
|
||||
{
|
||||
public Action<byte[]> OnTx { get; set; }
|
||||
public Action<byte[]> OnRx { get; set; }
|
||||
|
||||
private readonly Channel<ModbusTcpRequest> _channel = Channel.CreateUnbounded<ModbusTcpRequest>();
|
||||
private readonly UdpClient _udpClient;
|
||||
private readonly IPEndPoint _remoteEndPoint;
|
||||
private readonly ConcurrentDictionary<ushort, TaskCompletionSource<byte[]>> _pendingRequests = new();
|
||||
private int _transactionId = 0;
|
||||
|
||||
public ModbusUdpClient(string host, int port = 502)
|
||||
{
|
||||
_remoteEndPoint = new IPEndPoint(IPAddress.Parse(host), port);
|
||||
_udpClient = new UdpClient();
|
||||
_udpClient.Connect(_remoteEndPoint);
|
||||
|
||||
_ = ProcessQueueAsync();
|
||||
_ = ReceiveLoopAsync();
|
||||
}
|
||||
|
||||
#region 功能码封装
|
||||
public async Task<bool[]> ReadCoils(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var responsePdu = await SendAsync(ModbusFunctionCode.ReadCoils, pdu);
|
||||
return ParseDiscreteBits(responsePdu, quantity);
|
||||
}
|
||||
|
||||
public async Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var responsePdu = await SendAsync(ModbusFunctionCode.ReadDiscreteInputs, pdu);
|
||||
return ParseDiscreteBits(responsePdu, quantity);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var responsePdu = await SendAsync(ModbusFunctionCode.ReadHoldingRegisters, pdu);
|
||||
return ParseRegisters(responsePdu, quantity);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity)
|
||||
{
|
||||
var pdu = BuildReadPdu(startAddress, quantity);
|
||||
var responsePdu = await SendAsync(ModbusFunctionCode.ReadInputRegisters, pdu);
|
||||
return ParseRegisters(responsePdu, quantity);
|
||||
}
|
||||
|
||||
public async Task WriteSingleCoil(ushort address, bool value)
|
||||
{
|
||||
var pdu = new byte[]
|
||||
{
|
||||
(byte)(address >> 8),
|
||||
(byte)(address & 0xFF),
|
||||
value ? (byte)0xFF : (byte)0x00,
|
||||
0x00
|
||||
};
|
||||
await SendAsync(ModbusFunctionCode.WriteSingleCoil, pdu);
|
||||
}
|
||||
|
||||
public async Task WriteSingleRegister(ushort address, ushort value)
|
||||
{
|
||||
var pdu = new byte[]
|
||||
{
|
||||
(byte)(address >> 8),
|
||||
(byte)(address & 0xFF),
|
||||
(byte)(value >> 8),
|
||||
(byte)(value & 0xFF)
|
||||
};
|
||||
await SendAsync(ModbusFunctionCode.WriteSingleRegister, pdu);
|
||||
}
|
||||
|
||||
public async Task WriteMultipleCoils(ushort startAddress, bool[] values)
|
||||
{
|
||||
int byteCount = (values.Length + 7) / 8;
|
||||
byte[] data = new byte[byteCount];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i])
|
||||
data[i / 8] |= (byte)(1 << (i % 8));
|
||||
}
|
||||
|
||||
var pdu = new List<byte>
|
||||
{
|
||||
(byte)(startAddress >> 8),
|
||||
(byte)(startAddress & 0xFF),
|
||||
(byte)(values.Length >> 8),
|
||||
(byte)(values.Length & 0xFF),
|
||||
(byte)data.Length
|
||||
};
|
||||
pdu.AddRange(data);
|
||||
|
||||
await SendAsync(ModbusFunctionCode.WriteMultipleCoils, pdu.ToArray());
|
||||
}
|
||||
|
||||
public async Task WriteMultipleRegisters(ushort startAddress, ushort[] values)
|
||||
{
|
||||
var pdu = new List<byte>
|
||||
{
|
||||
(byte)(startAddress >> 8),
|
||||
(byte)(startAddress & 0xFF),
|
||||
(byte)(values.Length >> 8),
|
||||
(byte)(values.Length & 0xFF),
|
||||
(byte)(values.Length * 2)
|
||||
};
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
pdu.Add((byte)(val >> 8));
|
||||
pdu.Add((byte)(val & 0xFF));
|
||||
}
|
||||
|
||||
await SendAsync(ModbusFunctionCode.WriteMultipleRegister, pdu.ToArray());
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 核心通信
|
||||
|
||||
public Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
|
||||
{
|
||||
int id = Interlocked.Increment(ref _transactionId);
|
||||
var transactionId = (ushort)(id % ushort.MaxValue);
|
||||
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var request = new ModbusTcpRequest
|
||||
{
|
||||
TransactionId = transactionId,
|
||||
FunctionCode = functionCode,
|
||||
PDU = pdu,
|
||||
Completion = tcs
|
||||
};
|
||||
|
||||
_pendingRequests[transactionId] = tcs;
|
||||
_channel.Writer.TryWrite(request);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private async Task ProcessQueueAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var request = await _channel.Reader.ReadAsync();
|
||||
byte[] packet = BuildPacket(request.TransactionId, 0x01, (byte)request.FunctionCode, request.PDU);
|
||||
OnTx?.Invoke(packet);
|
||||
await _udpClient.SendAsync(packet, packet.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
UdpReceiveResult result = await _udpClient.ReceiveAsync();
|
||||
var buffer = result.Buffer;
|
||||
|
||||
if (buffer.Length < 6) continue;
|
||||
|
||||
ushort transactionId = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(0, 2));
|
||||
if (_pendingRequests.TryRemove(transactionId, out var tcs))
|
||||
{
|
||||
OnRx?.Invoke(buffer);
|
||||
var responsePdu = new ReadOnlySpan<byte>(buffer, 6, buffer.Length - 6).ToArray();
|
||||
|
||||
if ((responsePdu[1] & 0x80) != 0)
|
||||
{
|
||||
byte exceptionCode = responsePdu[2];
|
||||
tcs.SetException(new ModbusException(responsePdu[1], exceptionCode));
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.SetResult(responsePdu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] BuildPacket(ushort transactionId, byte unitId, byte functionCode, byte[] pduData)
|
||||
{
|
||||
int pduLength = 1 + pduData.Length;
|
||||
int totalLength = 7 + pduLength;
|
||||
|
||||
Span<byte> packet = totalLength <= 256 ? stackalloc byte[totalLength] : new byte[totalLength];
|
||||
packet[0] = (byte)(transactionId >> 8);
|
||||
packet[1] = (byte)(transactionId);
|
||||
packet[2] = 0; packet[3] = 0;
|
||||
ushort length = (ushort)(pduLength + 1);
|
||||
packet[4] = (byte)(length >> 8);
|
||||
packet[5] = (byte)(length);
|
||||
packet[6] = unitId;
|
||||
packet[7] = functionCode;
|
||||
pduData.AsSpan().CopyTo(packet.Slice(8));
|
||||
return packet.ToArray();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private byte[] BuildReadPdu(ushort startAddress, ushort quantity)
|
||||
{
|
||||
byte[] buffer = new byte[4];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), startAddress);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2, 2), quantity);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private bool[] ParseDiscreteBits(byte[] pdu, ushort count)
|
||||
{
|
||||
var result = new bool[count];
|
||||
int byteCount = pdu[2];
|
||||
int dataIndex = 3;
|
||||
|
||||
for (int i = 0, bytePos = 0, bitPos = 0; i < count; i++, bitPos++)
|
||||
{
|
||||
if (bitPos == 8)
|
||||
{
|
||||
bitPos = 0;
|
||||
bytePos++;
|
||||
}
|
||||
result[i] = ((pdu[dataIndex + bytePos] >> bitPos) & 0x01) != 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ushort[] ParseRegisters(byte[] pdu, ushort count)
|
||||
{
|
||||
var result = new ushort[count];
|
||||
int dataStart = 3;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int offset = dataStart + i * 2;
|
||||
result[i] = (ushort)((pdu[offset] << 8) | pdu[offset + 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var tcs in _pendingRequests.Values)
|
||||
tcs.TrySetCanceled();
|
||||
|
||||
_udpClient?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user