Files
ACP/DeviceCommand/Flexible/FModbusRTU.cs
T

269 lines
11 KiB
C#

using Common.Attributes;
using NModbus;
using NModbus.Serial;
using System;
using System.Collections.Concurrent;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Flexible
{
/// <summary>
/// 灵活型 Modbus RTU 串口通信类(免实例化,每次调用临时创建串口连接)。
/// 支持保持寄存器与线圈的读写操作,内部使用按串口名称粒度的通信锁保证同一串口同一时刻只有一个事务在执行,
/// 不同串口之间互不阻塞,适用于偶发性、无需保持长连接的 Modbus RTU 设备读写场景。
/// </summary>
[ACPCommand]
public static class FModbusRTU
{
// 按串口名称粒度的通信锁:同一串口同一时刻只有一个事务在执行,不同串口互不阻塞
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
/// <summary>获取指定串口的通信锁(不存在则自动创建)</summary>
private static SemaphoreSlim GetLock(string portName)
=> _commLocks.GetOrAdd(portName, _ => new SemaphoreSlim(1, 1));
/// <summary>
/// 创建串口实例并配置超时参数。
/// </summary>
private static SerialPort CreatePort(
string portName,
int baudRate,
int dataBits,
StopBits stopBits,
Parity parity,
int readTimeout,
int writeTimeout)
{
return new SerialPort(portName, baudRate, parity, dataBits, stopBits)
{
ReadTimeout = readTimeout > 0 ? readTimeout : SerialPort.InfiniteTimeout,
WriteTimeout = writeTimeout > 0 ? writeTimeout : SerialPort.InfiniteTimeout
};
}
/// <summary>
/// 基于指定串口创建 Modbus RTU 主站。
/// </summary>
private static IModbusMaster CreateMaster(SerialPort port)
{
return new ModbusFactory().CreateRtuMaster(port);
}
#region Holding Register
/// <summary>
/// 读取保持寄存器(功能码 03),返回指定数量的寄存器值数组。
/// </summary>
/// <param name="portName">串口名称,如 "COM1"</param>
/// <param name="baudRate">波特率,如 9600、115200</param>
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
/// <param name="startAddress">起始寄存器地址</param>
/// <param name="numberOfPoints">读取的寄存器数量</param>
/// <param name="dataBits">数据位,默认 8</param>
/// <param name="stopBits">停止位,默认 1 位</param>
/// <param name="parity">校验位,默认无校验</param>
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>读取到的寄存器值数组</returns>
public static async Task<ushort[]> 读保持寄存器(
string portName,
int baudRate,
byte slaveAddress,
ushort startAddress,
ushort numberOfPoints,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000,
CancellationToken ct = default)
{
await GetLock(portName).WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);
port.Open();
var master = CreateMaster(port);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (readTimeout > 0)
cts.CancelAfter(readTimeout);
return await master
.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints)
.WaitAsync(TimeSpan.FromMilliseconds(readTimeout),cts.Token);
}
finally
{
GetLock(portName).Release();
}
}
/// <summary>
/// 写入单个保持寄存器(功能码 06)。
/// </summary>
/// <param name="portName">串口名称,如 "COM1"</param>
/// <param name="baudRate">波特率,如 9600、115200</param>
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
/// <param name="registerAddress">要写入的寄存器地址</param>
/// <param name="value">要写入的寄存器值 (0-65535)</param>
/// <param name="dataBits">数据位,默认 8</param>
/// <param name="stopBits">停止位,默认 1 位</param>
/// <param name="parity">校验位,默认无校验</param>
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
/// <param name="ct">异步取消令牌</param>
public static async Task 写单个寄存器(
string portName,
int baudRate,
byte slaveAddress,
ushort registerAddress,
ushort value,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000,
CancellationToken ct = default)
{
await GetLock(portName).WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);
port.Open();
var master = CreateMaster(port);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (writeTimeout > 0)
cts.CancelAfter(writeTimeout);
await master
.WriteSingleRegisterAsync(slaveAddress, registerAddress, value)
.WaitAsync(TimeSpan.FromMilliseconds(writeTimeout),cts.Token);
}
finally
{
GetLock(portName).Release();
}
}
#endregion
#region Coil
/// <summary>
/// 读取线圈状态(功能码 01),返回指定数量的线圈开关状态数组。
/// </summary>
/// <param name="portName">串口名称,如 "COM1"</param>
/// <param name="baudRate">波特率,如 9600、115200</param>
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
/// <param name="startAddress">起始线圈地址</param>
/// <param name="numberOfPoints">读取的线圈数量</param>
/// <param name="dataBits">数据位,默认 8</param>
/// <param name="stopBits">停止位,默认 1 位</param>
/// <param name="parity">校验位,默认无校验</param>
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
/// <param name="ct">异步取消令牌</param>
/// <returns>线圈状态数组,true 表示吸合/导通</returns>
public static async Task<bool[]> 读线圈(
string portName,
int baudRate,
byte slaveAddress,
ushort startAddress,
ushort numberOfPoints,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000,
CancellationToken ct = default)
{
await GetLock(portName).WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);
port.Open();
var master = CreateMaster(port);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (readTimeout > 0)
cts.CancelAfter(readTimeout);
return await master
.ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints)
.WaitAsync(TimeSpan.FromMilliseconds(readTimeout),cts.Token);
}
finally
{
GetLock(portName).Release();
}
}
/// <summary>
/// 写入单个线圈(功能码 05),控制单个开关量输出。
/// </summary>
/// <param name="portName">串口名称,如 "COM1"</param>
/// <param name="baudRate">波特率,如 9600、115200</param>
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
/// <param name="coilAddress">要写入的线圈地址</param>
/// <param name="value">线圈状态(true: 吸合/导通, false: 断开)</param>
/// <param name="dataBits">数据位,默认 8</param>
/// <param name="stopBits">停止位,默认 1 位</param>
/// <param name="parity">校验位,默认无校验</param>
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
/// <param name="ct">异步取消令牌</param>
public static async Task 写单个线圈(
string portName,
int baudRate,
byte slaveAddress,
ushort coilAddress,
bool value,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000,
CancellationToken ct = default)
{
await GetLock(portName).WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);
port.Open();
var master = CreateMaster(port);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (writeTimeout > 0)
cts.CancelAfter(writeTimeout);
await master
.WriteSingleCoilAsync(slaveAddress, coilAddress, value)
.WaitAsync(TimeSpan.FromMilliseconds(writeTimeout), cts.Token);
}
finally
{
GetLock(portName).Release();
}
}
#endregion
}
}