BDU/DeviceCommand/Base/ModbusRtu_Tcp.cs

519 lines
22 KiB
C#
Raw 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 NModbus;
using NModbus.Device;
using NModbus.Serial;
//using DeviceCommand.Interface;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class ModbusRtu_Tcp
{
private TcpClient _tcpClient;
private NetworkStream _networkStream;
private readonly object _lock = new();
private bool _disposed = false;
/// <summary>
/// 获取目标设备的 IP 地址。
/// </summary>
public string RemoteIpAddress { get; private set; }
/// <summary>
/// 获取目标设备的 TCP 端口号,默认为 502。
/// </summary>
public int RemotePort { get; private set; } = 502;
/// <summary>
/// 接收和发送的超时时间(毫秒)。
/// </summary>
public int ReceiveTimeout { get; set; } = 3000;
/// <summary>
/// 发送超时时间
/// </summary>
public int SendTimeout { get; set; } = 3000;
public TcpClient TcpClient
{
get => _tcpClient;
set => _tcpClient = value;
}
/// <summary>
/// 获取内部的 NetworkStream 对象。
/// </summary>
public NetworkStream NetworkStream => _networkStream;
/// <summary>
/// 初始化一个新的 Modbus RTU over TCP 实例。
/// </summary>
/// <param name="ipAddress">目标设备的 IPv4 地址。</param>
/// <param name="port">目标 TCP 端口,默认为 502。</param>
/// <param name="sendTimeout">发送超时时间。</param>
/// <param name="receiveTimeout">接收超时时间。</param>
public ModbusRtu_Tcp CreateDevice(string ipAddress, int port = 502, int sendTimeout = 3000, int receiveTimeout = 3000)
{
RemoteIpAddress = ipAddress;
RemotePort = port;
return this;
}
/// <summary>
/// 连接到 Modbus RTU over TCP 设备。
/// </summary>
/// <summary>
/// 异步连接到 Modbus RTU over TCP 设备。
/// </summary>
/// <param name="modbusTcp">Modbus RTU over TCP 设备对象实例。</param>
/// <param name="ct">用于取消操作的取消令牌。</param>
/// <returns>连接结果,成功为 true失败为 false。</returns>
public static async Task<bool> ConnectAsync(ModbusRtu_Tcp modbusTcp, CancellationToken ct = default)
{
if (modbusTcp._disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Tcp));
lock (modbusTcp._lock) // 确保线程安全
{
if (modbusTcp._tcpClient == null)
{
modbusTcp._tcpClient = new TcpClient();
}
}
// 检查连接状态和端点匹配
if (!modbusTcp._tcpClient.Connected)
{
// 未连接,直接创建新连接
lock (modbusTcp._lock)
{
modbusTcp._tcpClient.Close(); // 关闭可能存在的旧连接
modbusTcp._tcpClient.Dispose();
modbusTcp._tcpClient = new TcpClient();
modbusTcp._tcpClient.ReceiveTimeout = modbusTcp.ReceiveTimeout;
modbusTcp._tcpClient.SendTimeout = modbusTcp.SendTimeout;
}
await modbusTcp._tcpClient.ConnectAsync(modbusTcp.RemoteIpAddress, modbusTcp.RemotePort, ct);
}
else
{
// 已连接,检查端点是否匹配
var remoteEndPoint = (IPEndPoint)modbusTcp._tcpClient.Client.RemoteEndPoint!;
var ip = remoteEndPoint.Address.MapToIPv4().ToString();
bool isSameAddress = ip.Equals(modbusTcp.RemoteIpAddress);
bool isSamePort = remoteEndPoint.Port == modbusTcp.RemotePort;
if (!isSameAddress || !isSamePort)
{
// 端点不匹配,断开旧连接并创建新连接
lock (modbusTcp._lock)
{
modbusTcp._tcpClient.Close();
modbusTcp._tcpClient.Dispose();
modbusTcp._tcpClient = new TcpClient();
modbusTcp._tcpClient.ReceiveTimeout = modbusTcp.ReceiveTimeout;
modbusTcp._tcpClient.SendTimeout = modbusTcp.SendTimeout;
}
await modbusTcp._tcpClient.ConnectAsync(modbusTcp.RemoteIpAddress, modbusTcp.RemotePort, ct);
}
// 如果端点匹配,则无需操作
}
// 连接成功后,初始化 NetworkStream
lock (modbusTcp._lock)
{
if (modbusTcp._tcpClient.Connected)
{
modbusTcp._networkStream = modbusTcp._tcpClient.GetStream();
return true;
}
}
return false; // 理论上不应到达此处,除非连接状态判断有误
}
/// <summary>
/// 修改 Modbus RTU over TCP 设备的连接配置。
/// /// </summary>
/// <param name="modbusTcp">Modbus RTU over TCP 设备对象实例。</param>
/// <param name="ipAddress">目标设备的 IPv4 地址。</param>
/// <param name="port">目标 TCP 端口,默认为 502。</param>
/// <param name="timeoutMs">通信超时时间(毫秒)。</param>
public static void ChangeDeviceConfig(ModbusRtu_Tcp modbusTcp, string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
modbusTcp.RemoteIpAddress = ipAddress;
modbusTcp.RemotePort = port;
modbusTcp.ReceiveTimeout = receiveTimeout;
modbusTcp.SendTimeout = sendTimeout;
}
/// <summary>
/// 断开与 Modbus RTU over TCP 设备的连接。
/// </summary>
public void Disconnect()
{
lock (_lock)
{
_networkStream?.Close();
_networkStream = null;
_tcpClient?.Close();
_tcpClient = null;
}
}
/// <summary>
/// 执行设备初始化操作当前为占位实现TCP 无状态故无需特殊初始化)。
/// </summary>
public void InitializeDevice()
{
// TCP 连接建立后通常无需额外初始化,保留接口一致性
}
/// <summary>
/// 触发紧急停止:立即断开 TCP 连接。
/// </summary>
public void EmergencyStop()
{
Disconnect();
}
/// <summary>
/// 异步发送 Modbus RTU 请求并通过 TCP 接收响应,支持重试机制。
/// </summary>
/// <param name="requestFrame">待发送的完整 Modbus RTU 请求帧(含 CRC。</param>
/// <param name="ct">用于取消操作的取消令牌。</param>
/// <returns>接收到的响应帧字节数组。</returns>
private async Task<byte[]> SendRequestAndReceiveFrameAsync(byte[] requestFrame, CancellationToken ct)
{
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Tcp));
if (_networkStream == null || !_tcpClient.Connected) throw new InvalidOperationException("TCP 连接未建立。");
for (int attempt = 0; attempt <= 3; attempt++)
{
try
{
await _networkStream.WriteAsync(requestFrame, 0, requestFrame.Length, ct);
// 读取响应长度(最小响应帧长度为 5: 从站地址 + 功能码 + 字节计数 + CRC(2)
// 先读取至少 5 个字节
byte[] initialBuffer = new byte[5];
int totalRead = 0;
while (totalRead < 5)
{
int n = await _networkStream.ReadAsync(initialBuffer, totalRead, 5 - totalRead, ct);
if (n == 0) throw new IOException("连接中断。");
totalRead += n;
}
// 解析功能码和字节计数
byte func = initialBuffer[1];
int pduDataLength = 0;
int expectedTotalLength = 0;
if ((func & 0x80) != 0) // 异常响应
{
expectedTotalLength = 5; // 异常响应固定长度
}
else
{
if (func == 0x01 || func == 0x02) // 读线圈/读离散输入
{
pduDataLength = initialBuffer[2];
}
else if (func == 0x03 || func == 0x04) // 读保持/输入寄存器
{
pduDataLength = initialBuffer[2];
}
// 其他功能码可以继续扩展
expectedTotalLength = 2 + 1 + pduDataLength + 2; // 从站地址 + 功能码 + 字节计数(或数据) + CRC(2)
}
byte[] response = new byte[expectedTotalLength];
Array.Copy(initialBuffer, response, 5);
if (expectedTotalLength > 5)
{
totalRead = 5;
while (totalRead < expectedTotalLength)
{
int n = await _networkStream.ReadAsync(response, totalRead, expectedTotalLength - totalRead, ct);
if (n == 0) throw new IOException("连接中断。");
totalRead += n;
}
}
return response;
}
catch (Exception ex) when (ex is SocketException || ex is TimeoutException || ex is IOException)
{
if (attempt == 3)
throw new TimeoutException($"Modbus RTU over TCP 通信超时,已重试 3 次。");
await Task.Delay(100, ct);
}
}
throw new InvalidOperationException("通信失败。");
}
/// <summary>
/// 计算 Modbus RTU 帧的 CRC16 校验值(小端格式,多项式 0xA001
/// </summary>
/// <param name="data">待计算 CRC 的字节数组。</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>
/// 构建完整的 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, 0, pdu.Length);
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) throw new InvalidDataException("响应帧长度不足。");
if (response[0] != expectedSlave) throw new InvalidDataException("从站地址不匹配。");
if (response[1] != expectedFunction && response[1] != (byte)(expectedFunction | 0x80))
throw new InvalidDataException("功能码不匹配。");
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);
if (receivedCrc != calculatedCrc)
throw new InvalidDataException("响应帧 CRC 校验失败。");
return true;
}
// ————————————————————————
// 公共 Modbus 功能方法 (编号 68-70)
// ————————————————————————
/// <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 SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x01);
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 SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x05);
}
/// <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 SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x02);
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 SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x03);
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 SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x06);
}
/// <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 SendRequestAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x04);
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>
/// 释放 TCP 客户端资源并标记为已处置。
/// </summary>
public void Dispose()
{
if (!_disposed)
{
Disconnect();
_disposed = true;
}
}
}
}