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 { /// /// 提供基于 UDP 传输的 Modbus RTU 协议通信能力。 /// 该类将标准 Modbus RTU 帧(含 CRC16 校验)封装在 UDP 数据包中进行传输, /// 适用于支持此非标准模式的工业设备或仿真平台。 /// [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; /// /// 获取目标设备的 IP 地址。 /// public string RemoteIpAddress { get; private set; } /// /// 获取目标设备的 UDP 端口号。 /// public int RemotePort { get; private set; } /// /// 获取本地 UDP 端口号。如果为 0,则表示使用系统自动分配的端口。 /// public int LocalPort { get; private set; } = 0; // 0 表示自动分配 /// /// 获取或设置每次通信的超时时间(毫秒),默认为 3000 毫秒。 /// public int TimeoutMs { get; set; } = 3000; /// /// 获取或设置通信失败时的最大重试次数,默认为 3 次。 /// public int MaxRetries { get; set; } = 3; /// /// 初始化一个新的 Modbus RTU over UDP 实例。 /// /// 目标设备的 IPv4 地址。 /// 目标 UDP 端口。 /// 本地 UDP 端口。如果为 0,则使用系统自动分配的端口,默认为 0。 /// 通信超时时间(毫秒)。 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; } /// /// 修改 Modbus RTU over UDP 实例参数 /// /// ModbusRtu_Udp实例 /// IP地址 /// 远程端口号 /// 本地端口号。如果为 0,则使用系统自动分配的端口。 /// 超时时间(毫秒) 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; } } /// /// 异步初始化 UDP 客户端并标记为已连接状态。 /// 注意:UDP 是无连接协议,此处“连接”仅为逻辑状态初始化。 /// 如果 LocalPort 不为 0,则会绑定到指定的本地端口。 /// /// 支持中途取消操作 /// 连接结果 public async Task 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 } /// /// 初始化 UDP 客户端并标记为已连接状态。 /// 注意:UDP 是无连接协议,此处“连接”仅为逻辑状态初始化。 /// 如果 LocalPort 不为 0,则会绑定到指定的本地端口。 /// 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; } } /// /// 关闭 UDP 客户端并清除连接状态。 /// public void Disconnect() { lock (_lock) { _udpClient?.Close(); _udpClient = null; _isConnected = false; } } /// /// 执行设备初始化操作(当前为占位实现,UDP 无状态故无需特殊初始化)。 /// public void InitializeDevice() { // UDP 无状态,保留接口一致性 } /// /// 触发紧急停止:立即断开通信连接。 /// public void EmergencyStop() { Disconnect(); } /// /// 异步发送 Modbus RTU 请求帧并通过 UDP 接收响应,支持重试机制。 /// /// 完整的 Modbus RTU 请求帧(含 CRC)。 /// 用于取消操作的取消令牌。 /// 接收到的响应字节数组。 /// 在指定重试次数内未收到有效响应。 public async Task 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("通信失败。"); } /// /// 计算 Modbus RTU 帧的 CRC16 校验值(小端格式,多项式 0xA001)。 /// /// 待计算 CRC 的字节数组。 /// 16 位 CRC 校验值。 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; } /// /// 构建完整的 Modbus RTU 帧(含 CRC16 校验)。 /// /// 从站地址(1-247)。 /// 功能码(如 0x01、0x03 等)。 /// PDU 数据部分(不含地址和功能码)。 /// 完整的 RTU 帧字节数组。 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; } /// /// 验证接收到的 Modbus RTU 响应帧是否有效。 /// /// 接收到的完整响应帧。 /// 期望的从站地址。 /// 期望的功能码。 /// 若帧有效则返回 true;否则抛出异常。 /// 当收到异常响应或 CRC 校验失败时抛出。 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; } /// /// 计算指定范围数据的 CRC16 校验值。 /// /// 源字节数组。 /// 起始偏移量。 /// 数据长度。 /// 16 位 CRC 校验值。 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; } /// /// 异步读取从站的线圈状态(功能码 01)。 /// /// 从站地址。 /// 起始线圈地址(0-based)。 /// 要读取的线圈数量。 /// 取消令牌。 /// 布尔数组,表示每个线圈的 ON/OFF 状态。 public async Task 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; } /// /// 异步写入单个线圈状态(功能码 05)。 /// /// 从站地址。 /// 线圈地址(0-based)。 /// 目标值(true = ON, false = OFF)。 /// 取消令牌。 /// 任务完成表示写入成功。 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("写入线圈响应校验失败"); } /// /// 异步读取从站的离散输入状态(功能码 02)。 /// /// 从站地址。 /// 起始输入地址(0-based)。 /// 要读取的输入点数量。 /// 取消令牌。 /// 布尔数组,表示每个输入的状态。 public async Task 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; } /// /// 异步读取从站的保持寄存器值(功能码 03)。 /// /// 从站地址。 /// 起始寄存器地址(0-based)。 /// 要读取的寄存器数量。 /// 取消令牌。 /// 16 位无符号整数数组,表示寄存器值。 public async Task 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; } /// /// 异步写入单个保持寄存器(功能码 06)。 /// /// 从站地址。 /// 寄存器地址(0-based)。 /// 要写入的值。 /// 取消令牌。 /// 任务完成表示写入成功。 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("写入寄存器响应校验失败"); } /// /// 异步读取从站的输入寄存器值(功能码 04)。 /// /// 从站地址。 /// 起始寄存器地址(0-based)。 /// 要读取的寄存器数量。 /// 取消令牌。 /// 16 位无符号整数数组,表示输入寄存器值。 public async Task 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; } /// /// 释放 UDP 客户端资源并标记为已处置。 /// public void Dispose() { if (!_disposed) { Disconnect(); _disposed = true; } } } }