476 lines
20 KiB
C#
476 lines
20 KiB
C#
using Common.Attributes;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Net.Sockets;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using static Common.Attributes.ATSCommandAttribute;
|
||
|
||
namespace DeviceCommand.Base
|
||
{
|
||
/// <summary>
|
||
/// 提供基于 UDP 传输的 Modbus RTU 协议通信能力。
|
||
/// 该类将标准 Modbus RTU 帧(含 CRC16 校验)封装在 UDP 数据包中进行传输,
|
||
/// 适用于支持此非标准模式的工业设备或仿真平台。
|
||
/// </summary>
|
||
[ATSCommand]
|
||
[DeviceCategory("全部驱动")] // 添加分类属性
|
||
public class ModbusRtu_Udp : IDisposable
|
||
{
|
||
private UdpClient _udpClient;
|
||
private IPEndPoint _remoteEndPoint;
|
||
private bool _isConnected = false;
|
||
private readonly object _lock = new();
|
||
private bool _disposed = false;
|
||
|
||
/// <summary>
|
||
/// 获取目标设备的 IP 地址。
|
||
/// </summary>
|
||
public string RemoteIpAddress { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 获取目标设备的 UDP 端口号。
|
||
/// </summary>
|
||
public int RemotePort { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 获取本地 UDP 端口号。如果为 0,则表示使用系统自动分配的端口。
|
||
/// </summary>
|
||
public int LocalPort { get; private set; } = 0; // 0 表示自动分配
|
||
|
||
/// <summary>
|
||
/// 获取或设置每次通信的超时时间(毫秒),默认为 3000 毫秒。
|
||
/// </summary>
|
||
public int TimeoutMs { get; set; } = 3000;
|
||
|
||
/// <summary>
|
||
/// 获取或设置通信失败时的最大重试次数,默认为 3 次。
|
||
/// </summary>
|
||
public int MaxRetries { get; set; } = 3;
|
||
|
||
/// <summary>
|
||
/// 初始化一个新的 Modbus RTU over UDP 实例。
|
||
/// </summary>
|
||
/// <param name="ipAddress">目标设备的 IPv4 地址。</param>
|
||
/// <param name="remotePort">目标 UDP 端口。</param>
|
||
/// <param name="localPort">本地 UDP 端口。如果为 0,则使用系统自动分配的端口,默认为 0。</param>
|
||
/// <param name="timeoutMs">通信超时时间(毫秒)。</param>
|
||
public ModbusRtu_Udp CreateDevice(string ipAddress, int remotePort, int localPort = 0)
|
||
{
|
||
RemoteIpAddress = ipAddress;
|
||
RemotePort = remotePort;
|
||
LocalPort = localPort; // 存储本地端口
|
||
_remoteEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), remotePort);
|
||
return this;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 修改 Modbus RTU over UDP 实例参数
|
||
/// </summary>
|
||
/// <param name="modbusUdp">ModbusRtu_Udp实例</param>
|
||
/// <param name="ipAddress">IP地址</param>
|
||
/// <param name="remotePort">远程端口号</param>
|
||
/// <param name="localPort">本地端口号。如果为 0,则使用系统自动分配的端口。</param>
|
||
/// <param name="timeoutMs">超时时间(毫秒)</param>
|
||
public static void ChangeDeviceConfig(ModbusRtu_Udp modbusUdp, string ipAddress, int remotePort, int localPort = 0, int timeoutMs = 3000)
|
||
{
|
||
modbusUdp.RemoteIpAddress = ipAddress;
|
||
modbusUdp.RemotePort = remotePort;
|
||
modbusUdp.LocalPort = localPort; // 更新本地端口
|
||
if (timeoutMs > 0)
|
||
{
|
||
modbusUdp.TimeoutMs = timeoutMs;
|
||
}
|
||
// 更新远程端点
|
||
modbusUdp._remoteEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), remotePort);
|
||
|
||
// 如果客户端已连接,需要重新配置
|
||
if (modbusUdp._udpClient != null && modbusUdp._isConnected)
|
||
{
|
||
modbusUdp._udpClient.Client.ReceiveTimeout = modbusUdp.TimeoutMs;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步初始化 UDP 客户端并标记为已连接状态。
|
||
/// 注意:UDP 是无连接协议,此处“连接”仅为逻辑状态初始化。
|
||
/// 如果 LocalPort 不为 0,则会绑定到指定的本地端口。
|
||
/// </summary>
|
||
/// <param name="ct">支持中途取消操作</param>
|
||
/// <returns>连接结果</returns>
|
||
public async Task<bool> ConnectAsync(CancellationToken ct = default)
|
||
{
|
||
await Task.Run(() =>
|
||
{
|
||
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Udp));
|
||
Disconnect();
|
||
lock (_lock)
|
||
{
|
||
// 修改:根据 LocalPort 创建 UdpClient
|
||
if (LocalPort != 0)
|
||
{
|
||
_udpClient = new UdpClient(LocalPort); // 绑定到指定本地端口
|
||
}
|
||
else
|
||
{
|
||
_udpClient = new UdpClient(); // 使用系统自动分配端口
|
||
}
|
||
_udpClient.Client.ReceiveTimeout = TimeoutMs;
|
||
_isConnected = true;
|
||
}
|
||
}, ct);
|
||
|
||
return true; // UdpClient 初始化成功即返回 true
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化 UDP 客户端并标记为已连接状态。
|
||
/// 注意:UDP 是无连接协议,此处“连接”仅为逻辑状态初始化。
|
||
/// 如果 LocalPort 不为 0,则会绑定到指定的本地端口。
|
||
/// </summary>
|
||
public void Connect()
|
||
{
|
||
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Udp));
|
||
Disconnect();
|
||
lock (_lock)
|
||
{
|
||
// 修改:根据 LocalPort 创建 UdpClient
|
||
if (LocalPort != 0)
|
||
{
|
||
_udpClient = new UdpClient(LocalPort); // 绑定到指定本地端口
|
||
}
|
||
else
|
||
{
|
||
|
||
_udpClient = new UdpClient(); // 使用系统自动分配端口
|
||
}
|
||
_udpClient.Client.ReceiveTimeout = TimeoutMs;
|
||
_isConnected = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭 UDP 客户端并清除连接状态。
|
||
/// </summary>
|
||
public void Disconnect()
|
||
{
|
||
lock (_lock)
|
||
{
|
||
_udpClient?.Close();
|
||
_udpClient = null;
|
||
_isConnected = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行设备初始化操作(当前为占位实现,UDP 无状态故无需特殊初始化)。
|
||
/// </summary>
|
||
public void InitializeDevice()
|
||
{
|
||
// UDP 无状态,保留接口一致性
|
||
}
|
||
|
||
/// <summary>
|
||
/// 触发紧急停止:立即断开通信连接。
|
||
/// </summary>
|
||
public void EmergencyStop()
|
||
{
|
||
Disconnect();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步发送 Modbus RTU 请求帧并通过 UDP 接收响应,支持重试机制。
|
||
/// </summary>
|
||
/// <param name="request">完整的 Modbus RTU 请求帧(含 CRC)。</param>
|
||
/// <param name="ct">用于取消操作的取消令牌。</param>
|
||
/// <returns>接收到的响应字节数组。</returns>
|
||
/// <exception cref="TimeoutException">在指定重试次数内未收到有效响应。</exception>
|
||
public async Task<byte[]> SendRequestAndReceiveAsync(byte[] request, CancellationToken ct)
|
||
{
|
||
if (!_isConnected) throw new InvalidOperationException("设备未连接。");
|
||
|
||
for (int attempt = 0; attempt <= MaxRetries; attempt++)
|
||
{
|
||
try
|
||
{
|
||
await _udpClient.SendAsync(request, request.Length, _remoteEndPoint);
|
||
var result = await _udpClient.ReceiveAsync().WaitAsync(TimeSpan.FromMilliseconds(TimeoutMs), ct);
|
||
return result.Buffer;
|
||
}
|
||
catch (Exception ex) when (ex is SocketException || ex is TimeoutException)
|
||
{
|
||
if (attempt == MaxRetries)
|
||
throw new TimeoutException($"Modbus RTU over UDP 通信超时,已重试 {MaxRetries} 次。");
|
||
await Task.Delay(100, ct);
|
||
}
|
||
}
|
||
throw new InvalidOperationException("通信失败。");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算 Modbus RTU 帧的 CRC16 校验值(小端格式,多项式 0xA001)。
|
||
/// </summary>
|
||
/// <param name="data">待计算 CRC 的字节数组。</param>
|
||
/// <returns>16 位 CRC 校验值。</returns>
|
||
private static ushort CalculateCRC16(byte[] data)
|
||
{
|
||
ushort crc = 0xFFFF;
|
||
for (int i = 0; i < data.Length; i++)
|
||
{
|
||
crc ^= data[i];
|
||
for (int j = 0; j < 8; j++)
|
||
{
|
||
if ((crc & 0x0001) != 0)
|
||
{
|
||
crc >>= 1;
|
||
crc ^= 0xA001;
|
||
}
|
||
else
|
||
crc >>= 1;
|
||
}
|
||
}
|
||
return crc;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建完整的 Modbus RTU 帧(含 CRC16 校验)。
|
||
/// </summary>
|
||
/// <param name="slaveAddress">从站地址(1-247)。</param>
|
||
/// <param name="functionCode">功能码(如 0x01、0x03 等)。</param>
|
||
/// <param name="data">PDU 数据部分(不含地址和功能码)。</param>
|
||
/// <returns>完整的 RTU 帧字节数组。</returns>
|
||
private static byte[] BuildRtuFrame(byte slaveAddress, byte functionCode, byte[] data)
|
||
{
|
||
byte[] pdu = new byte[2 + data.Length];
|
||
pdu[0] = slaveAddress;
|
||
pdu[1] = functionCode;
|
||
Array.Copy(data, 0, pdu, 2, data.Length);
|
||
|
||
ushort crc = CalculateCRC16(pdu);
|
||
byte[] frame = new byte[pdu.Length + 2];
|
||
Array.Copy(pdu, frame, pdu.Length);
|
||
frame[pdu.Length] = (byte)(crc & 0xFF);
|
||
frame[pdu.Length + 1] = (byte)(crc >> 8);
|
||
return frame;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证接收到的 Modbus RTU 响应帧是否有效。
|
||
/// </summary>
|
||
/// <param name="response">接收到的完整响应帧。</param>
|
||
/// <param name="expectedSlave">期望的从站地址。</param>
|
||
/// <param name="expectedFunction">期望的功能码。</param>
|
||
/// <returns>若帧有效则返回 true;否则抛出异常。</returns>
|
||
/// <exception cref="Exception">当收到异常响应或 CRC 校验失败时抛出。</exception>
|
||
private static bool ValidateResponse(byte[] response, byte expectedSlave, byte expectedFunction)
|
||
{
|
||
if (response.Length < 4) return false;
|
||
if (response[0] != expectedSlave) return false;
|
||
if (response[1] != expectedFunction && response[1] != (byte)(expectedFunction | 0x80)) return false;
|
||
if ((response[1] & 0x80) != 0)
|
||
throw new Exception($"Modbus 异常响应:功能码 {response[1] & 0x7F},错误码 {response[2]}");
|
||
|
||
ushort receivedCrc = (ushort)(response[response.Length - 2] | (response[response.Length - 1] << 8));
|
||
ushort calculatedCrc = CalculateCRC16(response, 0, response.Length - 2);
|
||
return receivedCrc == calculatedCrc;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算指定范围数据的 CRC16 校验值。
|
||
/// </summary>
|
||
/// <param name="data">源字节数组。</param>
|
||
/// <param name="offset">起始偏移量。</param>
|
||
/// <param name="length">数据长度。</param>
|
||
/// <returns>16 位 CRC 校验值。</returns>
|
||
private static ushort CalculateCRC16(byte[] data, int offset, int length)
|
||
{
|
||
ushort crc = 0xFFFF;
|
||
for (int i = 0; i < length; i++)
|
||
{
|
||
crc ^= data[offset + i];
|
||
for (int j = 0; j < 8; j++)
|
||
{
|
||
if ((crc & 0x0001) != 0)
|
||
{
|
||
crc >>= 1;
|
||
crc ^= 0xA001;
|
||
}
|
||
else
|
||
crc >>= 1;
|
||
}
|
||
}
|
||
return crc;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步读取从站的线圈状态(功能码 01)。
|
||
/// </summary>
|
||
/// <param name="slaveAddress">从站地址。</param>
|
||
/// <param name="startAddress">起始线圈地址(0-based)。</param>
|
||
/// <param name="numberOfPoints">要读取的线圈数量。</param>
|
||
/// <param name="ct">取消令牌。</param>
|
||
/// <returns>布尔数组,表示每个线圈的 ON/OFF 状态。</returns>
|
||
public async Task<bool[]> ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
|
||
{
|
||
byte[] data = {
|
||
(byte)(startAddress >> 8), (byte)startAddress,
|
||
(byte)(numberOfPoints >> 8), (byte)numberOfPoints
|
||
};
|
||
byte[] request = BuildRtuFrame(slaveAddress, 0x01, data);
|
||
byte[] response = await SendRequestAndReceiveAsync(request, ct);
|
||
|
||
if (!ValidateResponse(response, slaveAddress, 0x01))
|
||
throw new InvalidDataException("响应校验失败");
|
||
|
||
int byteCount = response[2];
|
||
bool[] result = new bool[numberOfPoints];
|
||
for (int i = 0; i < numberOfPoints; i++)
|
||
{
|
||
int byteIndex = i / 8;
|
||
int bitIndex = i % 8;
|
||
result[i] = (response[3 + byteIndex] & (1 << bitIndex)) != 0;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步写入单个线圈状态(功能码 05)。
|
||
/// </summary>
|
||
/// <param name="slaveAddress">从站地址。</param>
|
||
/// <param name="coilAddress">线圈地址(0-based)。</param>
|
||
/// <param name="value">目标值(true = ON, false = OFF)。</param>
|
||
/// <param name="ct">取消令牌。</param>
|
||
/// <returns>任务完成表示写入成功。</returns>
|
||
public async Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default)
|
||
{
|
||
ushort coilValue = value ? (ushort)0xFF00 : (ushort)0x0000;
|
||
byte[] data = {
|
||
(byte)(coilAddress >> 8), (byte)coilAddress,
|
||
(byte)(coilValue >> 8), (byte)coilValue
|
||
};
|
||
byte[] request = BuildRtuFrame(slaveAddress, 0x05, data);
|
||
byte[] response = await SendRequestAndReceiveAsync(request, ct);
|
||
if (!ValidateResponse(response, slaveAddress, 0x05))
|
||
throw new InvalidDataException("写入线圈响应校验失败");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步读取从站的离散输入状态(功能码 02)。
|
||
/// </summary>
|
||
/// <param name="slaveAddress">从站地址。</param>
|
||
/// <param name="startAddress">起始输入地址(0-based)。</param>
|
||
/// <param name="numberOfPoints">要读取的输入点数量。</param>
|
||
/// <param name="ct">取消令牌。</param>
|
||
/// <returns>布尔数组,表示每个输入的状态。</returns>
|
||
public async Task<bool[]> ReadDiscreteInputsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
|
||
{
|
||
byte[] data = {
|
||
(byte)(startAddress >> 8), (byte)startAddress,
|
||
(byte)(numberOfPoints >> 8), (byte)numberOfPoints
|
||
};
|
||
byte[] request = BuildRtuFrame(slaveAddress, 0x02, data);
|
||
byte[] response = await SendRequestAndReceiveAsync(request, ct);
|
||
if (!ValidateResponse(response, slaveAddress, 0x02))
|
||
throw new InvalidDataException("读取离散输入响应校验失败");
|
||
|
||
int byteCount = response[2];
|
||
bool[] result = new bool[numberOfPoints];
|
||
for (int i = 0; i < numberOfPoints; i++)
|
||
{
|
||
int byteIndex = i / 8;
|
||
int bitIndex = i % 8;
|
||
result[i] = (response[3 + byteIndex] & (1 << bitIndex)) != 0;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步读取从站的保持寄存器值(功能码 03)。
|
||
/// </summary>
|
||
/// <param name="slaveAddress">从站地址。</param>
|
||
/// <param name="startAddress">起始寄存器地址(0-based)。</param>
|
||
/// <param name="numberOfRegisters">要读取的寄存器数量。</param>
|
||
/// <param name="ct">取消令牌。</param>
|
||
/// <returns>16 位无符号整数数组,表示寄存器值。</returns>
|
||
public async Task<ushort[]> ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfRegisters, CancellationToken ct = default)
|
||
{
|
||
byte[] data = {
|
||
(byte)(startAddress >> 8), (byte)startAddress,
|
||
(byte)(numberOfRegisters >> 8), (byte)numberOfRegisters
|
||
};
|
||
byte[] request = BuildRtuFrame(slaveAddress, 0x03, data);
|
||
byte[] response = await SendRequestAndReceiveAsync(request, ct);
|
||
if (!ValidateResponse(response, slaveAddress, 0x03))
|
||
throw new InvalidDataException("读取保持寄存器响应校验失败");
|
||
|
||
ushort[] result = new ushort[numberOfRegisters];
|
||
for (int i = 0; i < numberOfRegisters; i++)
|
||
{
|
||
result[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步写入单个保持寄存器(功能码 06)。
|
||
/// </summary>
|
||
/// <param name="slaveAddress">从站地址。</param>
|
||
/// <param name="registerAddress">寄存器地址(0-based)。</param>
|
||
/// <param name="value">要写入的值。</param>
|
||
/// <param name="ct">取消令牌。</param>
|
||
/// <returns>任务完成表示写入成功。</returns>
|
||
public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default)
|
||
{
|
||
byte[] data = {
|
||
(byte)(registerAddress >> 8), (byte)registerAddress,
|
||
(byte)(value >> 8), (byte)value
|
||
};
|
||
byte[] request = BuildRtuFrame(slaveAddress, 0x06, data);
|
||
byte[] response = await SendRequestAndReceiveAsync(request, ct);
|
||
if (!ValidateResponse(response, slaveAddress, 0x06))
|
||
throw new InvalidDataException("写入寄存器响应校验失败");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步读取从站的输入寄存器值(功能码 04)。
|
||
/// </summary>
|
||
/// <param name="slaveAddress">从站地址。</param>
|
||
/// <param name="startAddress">起始寄存器地址(0-based)。</param>
|
||
/// <param name="numberOfRegisters">要读取的寄存器数量。</param>
|
||
/// <param name="ct">取消令牌。</param>
|
||
/// <returns>16 位无符号整数数组,表示输入寄存器值。</returns>
|
||
public async Task<ushort[]> ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfRegisters, CancellationToken ct = default)
|
||
{
|
||
byte[] data = {
|
||
(byte)(startAddress >> 8), (byte)startAddress,
|
||
(byte)(numberOfRegisters >> 8), (byte)numberOfRegisters
|
||
};
|
||
byte[] request = BuildRtuFrame(slaveAddress, 0x04, data);
|
||
byte[] response = await SendRequestAndReceiveAsync(request, ct);
|
||
if (!ValidateResponse(response, slaveAddress, 0x04))
|
||
throw new InvalidDataException("读取输入寄存器响应校验失败");
|
||
|
||
ushort[] result = new ushort[numberOfRegisters];
|
||
for (int i = 0; i < numberOfRegisters; i++)
|
||
{
|
||
result[i] = (ushort)((response[3 + i * 2] << 8) | response[4 + i * 2]);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放 UDP 客户端资源并标记为已处置。
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
if (!_disposed)
|
||
{
|
||
Disconnect();
|
||
_disposed = true;
|
||
}
|
||
}
|
||
}
|
||
} |