BDU/DeviceCommand/Base/ModbusRtu_Udp.cs

476 lines
20 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}
}