BDU/DeviceCommand/Base/ModbusRtu_Serial.cs

610 lines
27 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.IO;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
using static Common.Attributes.ATSCommandAttribute;
namespace DeviceCommand.Base
{
/// <summary>
/// 提供标准串口 Modbus RTU 协议通信能力。
/// 该类直接管理 SerialPort并处理 Modbus RTU 帧的构建、发送、接收与 CRC16 校验。
/// </summary>
[ATSCommand]
[DeviceCategory("全部驱动")] // 添加分类属性
public class ModbusRtu_Serial : IDisposable
{
private SerialPort _serialPort;
private readonly object _lock = new();
private bool _disposed = false;
protected internal SemaphoreSlim semaphoreSlimLock { get; set; } = new(1, 1);
/// <summary>
/// 获取或设置串口名称(如 "COM1")。
/// </summary>
public string PortName { get; set; }
/// <summary>
/// 获取或设置波特率。
/// </summary>
public int BaudRate { get; set; }
/// <summary>
/// 获取或设置数据位数,默认为 8。
/// </summary>
public int DataBits { get; set; } = 8;
/// <summary>
/// 获取或设置停止位,默认为 One。
/// </summary>
public StopBits StopBits { get; set; } = StopBits.One;
/// <summary>
/// 获取或设置奇偶校验位,默认为 None。
/// </summary>
public Parity Parity { get; set; } = Parity.None;
/// <summary>
/// 获取或设置读取超时时间(毫秒),默认为 3000。
/// </summary>
public int ReadTimeout { get; set; } = 3000;
/// <summary>
/// 获取或设置写入超时时间(毫秒),默认为 3000。
/// </summary>
public int WriteTimeout { get; set; } = 3000;
/// <summary>
/// 获取或设置通信失败时的最大重试次数,默认为 3 次。
/// </summary>
public int MaxRetries { get; set; } = 3;
public SerialPort _SerialPort { get; set; } = new SerialPort();
/// <summary>
/// 初始化一个新的 Modbus RTU 串口实例。
/// </summary>
/// <param name="portName">串口名称,如 "COM1"。</param>
/// <param name="baudRate">波特率。</param>
/// <param name="dataBits">数据位,默认 8。</param>
/// <param name="stopBits">停止位,默认 One。</param>
/// <param name="parity">奇偶校验,默认 None。</param>
/// <param name="readTimeout">读取超时(毫秒)。</param>
/// <param name="writeTimeout">写入超时(毫秒)。</param>
public ModbusRtu_Serial CreateDevice(string portName, int baudRate
, int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None
, int readTimeout = 3000, int writeTimeout = 3000)
{
PortName = portName;
BaudRate = baudRate;
DataBits = dataBits;
StopBits = stopBits;
Parity = parity;
//ReadTimeout = readTimeout;
//WriteTimeout = writeTimeout;
return this;
}
/// <summary>
/// 修改串口实例参数
/// </summary>
/// <param name="modbusRtu">ModbusRtu串口实例</param>
/// <param name="portName">串口名称(如"COM1"</param>
/// <param name="baudRate">波特率</param>
/// <param name="dataBits">数据位</param>
/// <param name="stopBits">停止位</param>
/// <param name="parity">校验位(根据设备需求设置)</param>
/// <param name="readTimeout">读取超时</param>
/// <param name="writeTimeout">写入超时</param>
public static void ChangeDeviceConfig(ModbusRtu_Serial modbusRtu, string portName, int baudRate,
int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None,
int readTimeout = 3000, int writeTimeout = 3000)
{
// 更新配置实例参数
modbusRtu.PortName = portName;
modbusRtu.BaudRate = baudRate;
modbusRtu.DataBits = dataBits;
modbusRtu.StopBits = stopBits;
modbusRtu.Parity = parity;
if (readTimeout > 0)
modbusRtu.ReadTimeout = readTimeout;
if (writeTimeout > 0)
modbusRtu.WriteTimeout = writeTimeout;
// 如果串口已打开,需要重新配置 SerialPort 对象
if (modbusRtu._serialPort != null && modbusRtu._serialPort.IsOpen)
{
modbusRtu._serialPort.Close();
modbusRtu._serialPort.PortName = modbusRtu.PortName;
modbusRtu._serialPort.BaudRate = modbusRtu.BaudRate;
modbusRtu._serialPort.Parity = modbusRtu.Parity;
modbusRtu._serialPort.DataBits = modbusRtu.DataBits;
modbusRtu._serialPort.StopBits = modbusRtu.StopBits;
modbusRtu._serialPort.ReadTimeout = modbusRtu.ReadTimeout;
modbusRtu._serialPort.WriteTimeout = modbusRtu.WriteTimeout;
// 注意DtrEnable, RtsEnable 等其他属性也可以在此更新,如果需要的话
}
}
/// <summary>
/// 异步打开串口连接。
/// </summary>
/// <param name="ct">支持中途取消操作</param>
/// <returns>连接结果</returns>
public static Task<bool> ConnectAsync(ModbusRtu_Serial modbusRtu_Serial, CancellationToken ct = default)
{
if (modbusRtu_Serial._SerialPort.PortName != modbusRtu_Serial.PortName
|| modbusRtu_Serial._SerialPort.BaudRate != modbusRtu_Serial.BaudRate
|| modbusRtu_Serial._SerialPort.Parity != modbusRtu_Serial.Parity
|| modbusRtu_Serial._SerialPort.DataBits != modbusRtu_Serial.DataBits
|| modbusRtu_Serial._SerialPort.StopBits != modbusRtu_Serial.StopBits
|| modbusRtu_Serial._SerialPort.ReadTimeout != modbusRtu_Serial.ReadTimeout
|| modbusRtu_Serial._SerialPort.WriteTimeout != modbusRtu_Serial.WriteTimeout)
{
// 关闭现有连接并重新配置
modbusRtu_Serial._SerialPort.Close();
//更新串口配置
modbusRtu_Serial._SerialPort.PortName = modbusRtu_Serial.PortName;
modbusRtu_Serial._SerialPort.BaudRate = modbusRtu_Serial.BaudRate;
modbusRtu_Serial._SerialPort.Parity = modbusRtu_Serial.Parity;
modbusRtu_Serial._SerialPort.DataBits = modbusRtu_Serial.DataBits;
modbusRtu_Serial._SerialPort.StopBits = modbusRtu_Serial.StopBits;
modbusRtu_Serial._SerialPort.ReadTimeout = modbusRtu_Serial.ReadTimeout;
modbusRtu_Serial._SerialPort.WriteTimeout = modbusRtu_Serial.WriteTimeout;
// 重新打开串口
modbusRtu_Serial._SerialPort.Open();
}
else
{
// 检查串口是否已打开
if (!modbusRtu_Serial._SerialPort.IsOpen)
{
// 打开串口
modbusRtu_Serial._SerialPort.Open();
}
}
return Task.FromResult(true);
}
/// <summary>
/// 打开串口连接(同步方法)。
/// </summary>
public void Connect()
{
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Serial));
lock (_lock)
{
if (_serialPort == null)
{
_serialPort = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits)
{
ReadTimeout = ReadTimeout,
WriteTimeout = WriteTimeout,
// DtrEnable = true, // 可选:根据设备要求设置
// RtsEnable = true // 可选:根据设备要求设置
};
}
if (!_serialPort.IsOpen)
{
_serialPort.Open();
}
}
}
/// <summary>
/// 关闭串口连接。
/// </summary>
public void Disconnect()
{
lock (_lock)
{
if (_serialPort != null && _serialPort.IsOpen)
{
_serialPort.Close();
}
}
}
/// <summary>
/// 异步发送请求帧并接收响应帧,支持重试。
/// </summary>
/// <param name="requestFrame">待发送的完整 Modbus RTU 请求帧(含 CRC。</param>
/// <param name="ct">用于取消操作的取消令牌。</param>
/// <returns>接收到的响应帧字节数组。</returns>
private async Task<byte[]> SendAndReceiveFrameAsync(byte[] requestFrame, CancellationToken ct)
{
if (_disposed) throw new ObjectDisposedException(nameof(ModbusRtu_Serial));
if (_serialPort == null || !_serialPort.IsOpen) throw new InvalidOperationException("串口未连接。");
for (int attempt = 0; attempt <= MaxRetries; attempt++)
{
try
{
lock (_lock)
{
_serialPort.DiscardInBuffer(); // 清空接收缓冲区
_serialPort.Write(requestFrame, 0, requestFrame.Length);
}
// 等待响应,使用超时
await Task.Delay(50, ct); // 短暂等待设备响应
byte[] responseBuffer = new byte[256]; // 预分配足够大的缓冲区
int receivedBytes = 0;
int totalBytesToRead = 0;
// 等待第一个字节,实现超时
var start = DateTime.Now;
while (_serialPort.BytesToRead == 0 && (DateTime.Now - start).TotalMilliseconds < ReadTimeout)
{
await Task.Delay(10, ct);
}
if (_serialPort.BytesToRead == 0)
{
if (attempt == MaxRetries)
throw new TimeoutException("读取响应超时。");
continue; // 重试
}
// 读取响应
while ((DateTime.Now - start).TotalMilliseconds < ReadTimeout)
{
int bytesAvailable = _serialPort.BytesToRead;
if (bytesAvailable > 0)
{
int read = _serialPort.Read(responseBuffer, receivedBytes, responseBuffer.Length - receivedBytes);
receivedBytes += read;
if (receivedBytes >= 5) // 最小响应帧长度 (地址 + 功能 + 数据 + CRC)
{
// 尝试解析帧长度(根据功能码和数据长度)
byte func = responseBuffer[1];
if ((func & 0x80) != 0) // 异常响应
{
totalBytesToRead = 5; // 异常响应固定长度
}
else
{
if (func == 0x01 || func == 0x02) // 读线圈/读离散输入
{
totalBytesToRead = 3 + responseBuffer[2] + 2; // 地址 + 功能 + 字节计数 + 数据 + CRC
}
else if (func == 0x03 || func == 0x04) // 读保持/输入寄存器
{
totalBytesToRead = 3 + (responseBuffer[2] * 2) + 2; // 地址 + 功能 + 字节计数 + (数据 * 2) + CRC
}
else if (func == 0x05 || func == 0x06) // 写单个线圈/寄存器
{
totalBytesToRead = 6; // 响应长度固定 (地址 + 功能 + 起始地址 + CRC)
}
else if (func == 0x10) // 写多个寄存器
{
totalBytesToRead = 6; // 响应长度固定 (地址 + 功能 + 起始地址 + 寄存器数量 + CRC)
}
else
{
totalBytesToRead = receivedBytes; // 无法预知长度,先读到当前为止
}
}
if (receivedBytes >= totalBytesToRead)
{
break; // 接收完成
}
}
}
await Task.Delay(10, ct);
}
if (receivedBytes == 0)
{
if (attempt == MaxRetries)
throw new TimeoutException("读取响应超时。");
continue; // 重试
}
byte[] response = new byte[receivedBytes];
Array.Copy(responseBuffer, response, receivedBytes);
return response;
}
catch (TimeoutException)
{
if (attempt == MaxRetries)
throw;
}
catch (Exception)
{
if (attempt == MaxRetries)
throw;
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 功能方法
// ————————————————————————
/// <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 SendAndReceiveFrameAsync(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 SendAndReceiveFrameAsync(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 SendAndReceiveFrameAsync(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 SendAndReceiveFrameAsync(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 SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x06);
}
/// <summary>
/// 异步写入多个保持寄存器(功能码 10 (0x10))。
/// </summary>
/// <param name="slaveAddress">从站地址。</param>
/// <param name="startAddress">起始寄存器地址0-based。</param>
/// <param name="values">要写入的值数组。</param>
/// <param name="ct">取消令牌。</param>
/// <returns>任务完成表示写入成功。</returns>
public async Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default)
{
if (values == null || values.Length == 0)
throw new ArgumentException("写入值数组不能为空。", nameof(values));
int byteCount = values.Length * 2;
byte[] data = new byte[4 + byteCount]; // 起始地址(2) + 寄存器数量(2) + 字节计数(1) + 数据(N)
data[0] = (byte)(startAddress >> 8);
data[1] = (byte)(startAddress & 0xFF);
data[2] = (byte)(values.Length >> 8);
data[3] = (byte)(values.Length & 0xFF);
data[4] = (byte)byteCount; // 字节计数
for (int i = 0; i < values.Length; i++)
{
data[5 + i * 2] = (byte)(values[i] >> 8); // High byte
data[5 + i * 2 + 1] = (byte)(values[i] & 0xFF); // Low byte
}
byte[] request = BuildRtuFrame(slaveAddress, 0x10, data);
byte[] response = await SendAndReceiveFrameAsync(request, ct);
ValidateResponse(response, slaveAddress, 0x10);
}
/// <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 SendAndReceiveFrameAsync(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>
/// 释放串口资源并标记为已处置。
/// </summary>
public void Dispose()
{
if (!_disposed)
{
Disconnect();
if (_serialPort != null)
{
_serialPort.Dispose();
_serialPort = null;
}
_disposed = true;
}
}
}
}