# ModbusLink **Repository Path**: ChangGengCN/modbus-link ## Basic Information - **Project Name**: ModbusLink - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-11-12 - **Last Updated**: 2026-07-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ModbusLink [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![.NET](https://img.shields.io/badge/.NET-Standard%202.0%20%7C%20.NET%20Framework%204.7.2%20%7C%20.NET%206%2B-green.svg)](https://dotnet.microsoft.com/) ModbusLink 是一个为工业自动化场景设计的 .NET Modbus 通信库,提供基于串口的 **Modbus RTU** 与基于套接字的 **Modbus TCP** 协议完整实现。库包含核心协议抽象与底层通信封装,支持主站(客户端)和从站(服务端)两种工作模式,可用于设备间的数据交换与远程控制。 > **2.0 重要变更**: > - **连接管理彻底独立化**:删除 `IConnect` 接口与 `SerialPortConfig`,连接的打开/关闭/释放完全由调用方通过 `ISerialPort` / `ISocket`(`SerialPortConnection` / `TouchSocketConnection`)自行管理 > - **接口层携带连接类型泛型**:`IModbusMaster` / `IModbusSlave` 在接口层携带连接对象类型泛型 TConn(串口库下为 `ISerialPort`,TCP 库下为 `ISocket` / `IServerSocket`),接口仅保留纯粹的 Modbus 功能,不再暴露 `Connection`/`IsConnected` > - **Master/Slave 构造注入**:通过构造函数注入已打开的连接对象,不再提供 `OpenConnect`/`CloseConnect`;从站监听服务通过独立的 `Start()` / `Stop()` 控制,与连接生命周期解耦 > - **抽象层统一移入 Core**:所有中立抽象(`ISerialPort` / `ISocket` / `IServerSocket` / `IModbusRtuMaster` / `IModbusRtuSlave` / `IModbusTcpMaster` / `IModbusTcpSlave` / `SlaveDataStore`)统一收归到 `ModbusLink.Core` 命名空间,实现项目只保留第三方实现类,第三方可替换(系统自带 `System.Net.Sockets` / `System.IO.Ports` 均可适配) > - **响应模型重构**:`ModbusResponse` 由布尔 `IsSuccess` 升级为 `ModbusResponseStatus` 枚举(一次覆盖成功/通信失败/全部 8 种 Modbus 协议异常码),移除冗余字段,`RegistersData` → `Data`,工厂方法改为 `Success` / `CommunicationError` / `ProtocolError` > - **新增 ModbusLink.Socket 项目**:基于 [TouchSocket](https://touchsocket.net/) 实现 Modbus TCP 主站与从站,从站采用 Server 模式支持多客户端并发 > > 1.x 旧版文档请见 [README_1.x.md](README_1.x.md)。 --- ## 目录 - [项目结构](#项目结构) - [特性](#特性) - [支持平台](#支持平台) - [安装](#安装) - [核心概念](#核心概念) - [连接模型](#连接模型) - [使用指南](#使用指南) - [RTU 主站(Master)](#rtu-主站master) - [RTU 从站(Slave)](#rtu-从站slave) - [TCP 主站(Master)](#tcp-主站master) - [TCP 从站(Slave)](#tcp-从站slave) - [数据存储(DataStore)](#数据存储datastore) - [自定义数据存储](#自定义数据存储) - [自定义连接](#自定义连接) - [支持的数据类型](#支持的数据类型) - [异常处理与错误码](#异常处理与错误码) - [API 参考](#api-参考) - [调试与测试](#调试与测试) - [从 1.x 升级](#从-1x-升级) - [贡献](#贡献) - [鸣谢](#鸣谢) - [许可证](#许可证) --- ## 项目结构 ``` ModbusLink/ ├── ModbusLink.Core/ # 核心协议层(.NET Standard 2.0,纯抽象,零第三方依赖) │ ├── Interfaces/ │ │ ├── Core/ │ │ │ ├── IModbusMaster.cs # 主站接口(泛型 IModbusMaster) │ │ │ ├── IModbusSlave.cs # 从站接口(泛型 IModbusSlave,含 Start/Stop) │ │ │ └── IModbusSlaveDataStore.cs # 从站数据存储接口 │ │ ├── Rtu/ │ │ │ ├── ISerialPort.cs # 串口连接抽象(可由系统自带或第三方实现) │ │ │ ├── IModbusRtuMaster.cs # RTU 主站接口(: IModbusMaster) │ │ │ └── IModbusRtuSlave.cs # RTU 从站接口(: IModbusSlave) │ │ └── Tcp/ │ │ ├── ISocket.cs # TCP 客户端连接抽象(对标 ISerialPort) │ │ ├── IServerSocket.cs # TCP 服务端监听抽象(含 SocketSessionEventArgs) │ │ ├── IModbusTcpMaster.cs # TCP 主站接口(: IModbusMaster) │ │ └── IModbusTcpSlave.cs # TCP 从站接口(: IModbusSlave) │ ├── Models/ │ │ ├── ModbusRequest.cs # 请求模型(泛型) │ │ ├── ModbusResponse.cs # 响应模型(泛型/非泛型) │ │ └── ModbusResponseStatus.cs # 响应状态枚举(成功/通信失败/8 种协议异常码) │ ├── SlaveDataStore.cs # 默认从站数据存储实现(RTU/TCP 共用) │ └── FFC.cs # Modbus 功能码常量定义 ├── ModbusLink.SerialPort/ # 串口 RTU 实现(多目标框架,仅实现类) │ ├── SerialPortConnection.cs # 串口连接默认实现(封装 RJCP.SerialPortStream) │ ├── ModbusRtuMaster.cs # RTU 主站实现 │ └── ModbusRtuSlave.cs # RTU 从站实现 ├── ModbusLink.Socket/ # 套接字 TCP 实现(多目标框架,仅实现类) │ ├── TouchSocketConnection.cs # TCP 客户端连接实现(封装 TouchSocket TcpClient) │ ├── TouchSocketServer.cs # TCP 服务端监听实现(封装 TouchSocket TcpService) │ ├── ModbusTcpMaster.cs # TCP 主站实现(MBAP + PDU,无 CRC) │ └── ModbusTcpSlave.cs # TCP 从站实现(Server 模式,多客户端并发) └── ModbusLink/ # 示例/演示程序 └── Program.cs ``` --- ## 特性 - **双协议栈支持**:同时支持 Modbus RTU(串口)与 Modbus TCP(套接字) - **双模式支持**:每种协议均支持主站(客户端)和从站(服务端)通信 - **完整功能码覆盖**:支持常用 Modbus 功能码 - `0x01` 读取线圈(Coils) - `0x02` 读取离散输入(Discrete Inputs) - `0x03` 读取保持寄存器(Holding Registers) - `0x04` 读取输入寄存器(Input Registers) - `0x05` 写入单个线圈 - `0x06` 写入单个保持寄存器 - `0x0F` 写入多个线圈 - `0x10` 写入多个保持寄存器 - **多数据类型原生支持**:支持 `bool`、`byte`、`short`、`ushort`、`int`、`uint`、`float`、`double` - **自动 CRC16 校验**:所有 RTU 帧自动进行 CRC16 校验与验证;TCP 帧使用 MBAP 头(无 CRC) - **异步编程模型**:基于 `async/await` 的异步通信,避免阻塞主线程 - **泛型 API 设计**:寄存器读写使用泛型约束 `where T : unmanaged`,类型安全且支持不同位宽数据 - **连接管理独立**:连接的打开/关闭/释放完全由调用方自行管理,Master/Slave 不持有连接生命周期,职责单一 - **接口层泛型约束**:`IModbusMaster` / `IModbusSlave` 在接口层携带连接类型 TConn(与方法级 TData 区分),编译期完成类型检查;接口仅保留纯粹 Modbus 功能 - **第三方可替换**:核心抽象(`ISerialPort` / `ISocket` / `IServerSocket`)中立,可由系统自带 API(`System.IO.Ports` / `System.Net.Sockets`)或第三方库实现,实现项目仅保留具体实现类 - **TCP 从站多客户端并发**:TCP 从站采用 Server 模式,内置多客户端会话管理与并发处理 - **精细化响应状态**:`ModbusResponseStatus` 枚举一次覆盖成功 / 通信失败 / 全部 8 种 Modbus 协议异常码,调用方可精确区分"网线断了"与"从站说地址非法"等不同失败 - **线程安全**:连接操作与从站数据存储均内置锁机制,支持多线程环境 - **广播支持**:从站支持地址 `0x00` 广播请求(只执行不回应) - **事务标识配对**:TCP 主站自动管理 MBAP 事务标识,请求/响应精确配对 --- ## 支持平台 | 包名 | 目标框架 | 说明 | |---|---|---| | `ModbusLink.Core` | .NET Standard 2.0 | 核心抽象与模型,零第三方依赖,可被任何 .NET Standard 2.0 兼容项目引用 | | `ModbusLink.SerialPort` | .NET Framework 4.7.2 / .NET 6.0 / .NET 8.0 / .NET 9.0 | 串口 RTU 实现,依赖 `RJCP.SerialPortStream` | | `ModbusLink.Socket` | .NET Framework 4.7.2 / .NET 6.0 | 套接字 TCP 实现,依赖 `TouchSocket` | --- ## 安装 ### 从 NuGet 安装(推荐) ```bash # 安装串口 RTU 包(自动包含核心库依赖) dotnet add package ModbusLink.SerialPort # 安装套接字 TCP 包(自动包含核心库依赖) dotnet add package ModbusLink.Socket ``` 或在 `.csproj` 中添加: ```xml ``` ### 从源码安装 ```bash git clone https://gitee.com/ChangGengCN/modbus-link.git cd modbus-link dotnet build ``` 将 `ModbusLink.Core` 和 `ModbusLink.SerialPort` / `ModbusLink.Socket` 添加为项目引用即可。 --- ## 核心概念 | 术语 | 说明 | |---|---| | **主站 (Master)** | 主动发起请求的设备,负责读取或写入从站数据 | | **从站 (Slave)** | 被动响应请求的设备,维护线圈和寄存器数据 | | **线圈 (Coil)** | 可读写布尔量,常用于开关、继电器状态 | | **离散输入 (Discrete Input)** | 只读布尔量,常用于传感器开关信号 | | **保持寄存器 (Holding Register)** | 可读写 16 位寄存器,常用于参数、设定值 | | **输入寄存器 (Input Register)** | 只读 16 位寄存器,常用于传感器模拟量值 | | **设备地址** | Modbus 总线上从站的唯一标识,范围 1–247(0 为广播地址) | | **连接对象 (Connection)** | 已封装好通信参数的底层连接实例(如 `ISerialPort` / `ISocket`),由调用方创建后传入 | | **MBAP 头** | Modbus TCP 的 7 字节应用层报文头(事务标识 2B + 协议标识 2B + 长度 2B + Unit ID 1B),替代 RTU 的 CRC | | **PDU** | 协议数据单元,MBAP 头之后的有效载荷,包含功能码与数据 | --- ## 连接模型 2.0 版本采用 **连接管理独立 + 构造注入** 设计: 1. 调用方创建并配置连接对象(如 `SerialPortConnection` / `TouchSocketConnection`),通信参数在创建时指定 2. 调用方自行调用 `connection.Open()` 打开连接(连接生命周期完全由调用方掌控) 3. 调用方通过构造函数将**已打开**的连接对象注入 Master/Slave 4. Master 直接使用连接进行读写;Slave 通过 `Start()` 启动监听服务、`Stop()` 停止监听服务 5. 连接的关闭/释放由调用方负责(`connection.Close()` / `Dispose`) **职责划分**: | 角色 | 职责 | |---|---| | 调用方 | 创建、打开、关闭、释放连接对象 | | Master | 仅协议层通信,不持有连接生命周期,无 `IDisposable` | | Slave | 协议层 + 监听服务(`Start`/`Stop`),`Dispose` 仅停止监听,不动连接 | **RTU 与 TCP 的连接类型差异**: | 协议 | Master 连接类型 | Slave 连接类型 | 说明 | |---|---|---|---| | RTU | `ISerialPort` | `ISerialPort` | 串口为点对点总线,主从共享同一连接 | | TCP | `ISocket`(客户端连接) | `IServerSocket`(服务端监听) | TCP 从站天然是 Server,需监听并接受多个客户端 | 这样设计的好处: - **职责单一**:Modbus 类只管协议,不管连接生命周期,避免资源归属混乱 - **编译期类型安全**:TConn 由 `IModbusMaster` / `IModbusSlave` 在接口层约束,接口不含 `Connection`/`IsConnected`,职责纯粹 - **可测试性**:可注入 mock 连接进行单元测试 - **可扩展性**:可替换为系统自带 API 或任意第三方库实现 --- ## 使用指南 ### RTU 主站(Master) 主站通过串口与从站通信,发起读写请求。 #### 基础连接 ```csharp using ModbusLink.Core; using ModbusLink.SerialPort; using RJCP.IO.Ports; // 1. 创建并配置串口连接对象 ISerialPort connection = new SerialPortConnection( portName: "COM1", baudRate: 9600, dataBits: 8, parity: Parity.None, stopBits: StopBits.One ); connection.ReadTimeout = 1000; connection.WriteTimeout = 1000; // 2. 由调用方打开连接 connection.Open(); // 3. 构造函数注入已打开的连接 var master = new ModbusRtuMaster(connection); // 4. 业务操作... // await master.ReadCoils(...); // 5. 由调用方关闭/释放连接 connection.Close(); connection.Dispose(); ``` > **注意**:Master 不持有连接的生命周期,也不实现 `IDisposable`。连接的打开/关闭/释放完全由调用方负责。Master 接口不暴露 `Connection`/`IsConnected`,仅提供纯粹的 Modbus 读写功能。 #### 读取线圈 ```csharp // 读取从站 1 的 10 个线圈,从地址 0 开始 var request = ModbusRequest.ReadCreate(deviceAdd: 1, startAdd: 0, length: 10); var response = await master.ReadCoils(request); if (response.IsSuccess) { bool[] coils = response.Data; Console.WriteLine($"读取成功: {string.Join(", ", coils)}"); } else { Console.WriteLine($"读取失败,状态:{response.Status},错误信息:{response.Message}"); } ``` #### 写入线圈 ```csharp // 写入单个线圈 var singleRequest = new ModbusRequest(deviceAdd: 1, startAdd: 0, dataValues: new[] { true }); var response = await master.WriteCoils(singleRequest); // 写入多个线圈 var multiRequest = new ModbusRequest(deviceAdd: 1, startAdd: 0, dataValues: new[] { true, false, true, true }); var response2 = await master.WriteCoils(multiRequest); ``` #### 读取保持寄存器 ```csharp // 读取 5 个 ushort 类型的保持寄存器 var request = ModbusRequest.ReadCreate(deviceAdd: 1, startAdd: 0, length: 5); var response = await master.ReadHoldingRegisters(request); if (response.IsSuccess) { ushort[] values = response.Data; // values 长度为 5 } ``` #### 写入保持寄存器 ```csharp // 写入单个 ushort var request = new ModbusRequest(deviceAdd: 1, startAdd: 0, dataValues: new[] { (ushort)1234 }); var response = await master.WriteHoldingRegisters(request); // 写入多个 float(每个 float 占 2 个寄存器) var floatRequest = new ModbusRequest(deviceAdd: 1, startAdd: 0, dataValues: new[] { 3.14f, 2.718f }); var floatResponse = await master.WriteHoldingRegisters(floatRequest); ``` #### 读取输入寄存器 ```csharp // 读取输入寄存器(只读) var request = ModbusRequest.ReadCreate(deviceAdd: 1, startAdd: 0, length: 4); var response = await master.ReadInputRegisters(request); ``` --- ### RTU 从站(Slave) 从站监听串口,响应主站的读写请求。 #### 快速启动 ```csharp using ModbusLink.Core; using ModbusLink.SerialPort; // 1. 创建数据存储 var store = new SlaveDataStore( coilCount: 1024, discreteInputCount: 1024, holdingRegisterCount: 1024, inputRegisterCount: 1024 ); // 2. 创建并打开串口连接 ISerialPort connection = new SerialPortConnection("COM2", 9600); connection.Open(); // 3. 构造函数注入连接,并配置从站参数 var slave = new ModbusRtuSlave(connection) .SetDeviceAddress(0x01) .SetSlaveDataStore(store); // 4. 启动监听服务(不打开连接,连接已由外部打开) var start = await slave.Start(); if (!start.IsSuccess) { Console.WriteLine($"从站启动失败,状态:{start.Status},错误信息:{start.Message}"); return; } Console.WriteLine("从站已启动,按任意键停止..."); Console.ReadKey(); // 5. 停止监听服务(不关闭连接) await slave.Stop(); // 6. 由调用方关闭/释放连接 connection.Close(); connection.Dispose(); ``` #### 链式配置 ```csharp var connection = new SerialPortConnection("COM2", 9600); connection.Open(); var slave = new ModbusRtuSlave(connection) .SetDeviceAddress(0x01) .SetSlaveDataStore(new SlaveDataStore()); await slave.Start(); ``` --- ### TCP 主站(Master) TCP 主站通过套接字连接远程从站,发起读写请求。 #### 基础连接 ```csharp using ModbusLink.Core; using ModbusLink.Socket; // 1. 创建并配置 TCP 客户端连接对象(封装 TouchSocket TcpClient) ISocket connection = new TouchSocketConnection("127.0.0.1", 502); connection.ReadTimeout = 1000; connection.WriteTimeout = 1000; // 2. 由调用方打开连接 connection.Open(); // 3. 构造函数注入已打开的连接 var master = new ModbusTcpMaster(connection); // 4. 业务操作... // await master.ReadCoils(...); // 5. 由调用方关闭/释放连接 connection.Close(); connection.Dispose(); ``` > **说明**:TCP 主站使用 MBAP 头(7 字节)+ PDU 的帧格式,无 CRC。事务标识自动递增管理,请求/响应精确配对。 #### 读写操作 TCP 主站的读写 API 与 RTU 主站完全一致(共用 `IModbusMaster` 接口),仅连接类型不同: ```csharp // 读取线圈 var request = ModbusRequest.ReadCreate(deviceAdd: 1, startAdd: 0, length: 10); var response = await master.ReadCoils(request); if (response.IsSuccess) { bool[] coils = response.Data; } else { Console.WriteLine($"读取失败,状态:{response.Status},错误信息:{response.Message}"); } ``` --- ### TCP 从站(Slave) TCP 从站采用 Server 模式,监听端口并接受多个客户端并发连接,响应主站的读写请求。 #### 快速启动 ```csharp using ModbusLink.Core; using ModbusLink.Socket; // 1. 创建数据存储 var store = new SlaveDataStore( coilCount: 1024, discreteInputCount: 1024, holdingRegisterCount: 1024, inputRegisterCount: 1024 ); // 2. 创建 TCP 服务端监听对象(封装 TouchSocket TcpService) IServerSocket serverSocket = new TouchSocketServer("0.0.0.0", 502); // 3. 构造函数注入监听对象,并配置从站参数 var slave = new ModbusTcpSlave(serverSocket) .SetDeviceAddress(0x01) .SetSlaveDataStore(store); // 4. 启动监听服务 var start = await slave.Start(); if (!start.IsSuccess) { Console.WriteLine($"从站启动失败,状态:{start.Status},错误信息:{start.Message}"); return; } Console.WriteLine("TCP 从站已启动,监听 0.0.0.0:502,按任意键停止..."); Console.ReadKey(); // 5. 停止监听服务 await slave.Stop(); // 6. 由调用方释放监听对象 serverSocket.Dispose(); ``` #### 多客户端并发说明 TCP 从站内部为每个连接的客户端启动独立的处理任务,通过 `ConcurrentDictionary` 管理会话上下文。多个主站可同时连接同一从站并发起请求,互不阻塞。广播地址 `0x00` 的请求只执行不回应。 --- ### 数据存储(DataStore) `SlaveDataStore` 是 `IModbusSlaveDataStore` 的默认实现,使用线程安全的内存数组。RTU 与 TCP 从站共用同一个实现(位于 `ModbusLink.Core`): ```csharp var store = new SlaveDataStore( coilCount: 2048, // 线圈数量 discreteInputCount: 512, // 离散输入数量 holdingRegisterCount: 1024, // 保持寄存器数量 inputRegisterCount: 512 // 输入寄存器数量 ); // 预置数据(从机启动前或运行中均可操作) await store.WriteSingleCoil(0, true); await store.WriteMultipleCoils(10, new[] { true, false, true }); await store.WriteSingleRegister(0, (ushort)100); await store.WriteMultipleRegisters(1, new[] { (ushort)200, (ushort)300 }); ``` | 方法 | 说明 | |---|---| | `ReadCoils(start, quantity)` | 读取线圈 | | `ReadDiscreteInputs(start, quantity)` | 读取离散输入 | | `ReadHoldingRegisters(start, quantity)` | 读取保持寄存器 | | `ReadInputRegisters(start, quantity)` | 读取输入寄存器 | | `WriteSingleCoil(address, value)` | 写入单个线圈 | | `WriteMultipleCoils(start, values)` | 写入多个线圈 | | `WriteSingleRegister(address, value)` | 写入单个寄存器 | | `WriteMultipleRegisters(start, values)` | 写入多个寄存器 | **注意**:越界访问或无效参数会抛出异常,从站内部会自动将其映射为 Modbus 异常响应。 --- ### 自定义数据存储 你可以实现 `IModbusSlaveDataStore` 接口,将数据持久化到数据库、文件或硬件设备: ```csharp public class CustomDataStore : IModbusSlaveDataStore { public Task ReadCoils(ushort startAddress, ushort quantity) { // 自定义读取逻辑 } public Task WriteSingleRegister(ushort address, ushort value) { // 自定义写入逻辑,例如写入 PLC 或数据库 } // ... 实现其余方法 } ``` --- ### 自定义连接 核心抽象(`ISerialPort` / `ISocket` / `IServerSocket`)是中立的,可由系统自带 API 或任意第三方库实现。 #### 自定义串口连接 ```csharp public class CustomSerialPort : ISerialPort { // 实现 Open / Close / Read / Write / ReadAsync 等方法 // ... } // 使用自定义连接 var connection = new CustomSerialPort(); connection.Open(); var master = new ModbusRtuMaster(connection); ``` 连接对象的类型由 `IModbusMaster` 在接口层约束,构造函数注入时编译器即完成类型检查。 #### 自定义套接字连接 ```csharp public class CustomSocket : ISocket { // 实现 Open / Close / Read / Write / ReadAsync 等方法 // 可基于 System.Net.Sockets.TcpClient + NetworkStream 实现 // ... } public class CustomServerSocket : IServerSocket { // 实现 Start / Stop / ClientConnected / ClientDisconnected // 可基于 System.Net.Sockets.TcpListener 实现 // ... } // 使用自定义连接 var connection = new CustomSocket(); connection.Open(); var master = new ModbusTcpMaster(connection); var serverSocket = new CustomServerSocket(); var slave = new ModbusTcpSlave(serverSocket) .SetDeviceAddress(0x01) .SetSlaveDataStore(new SlaveDataStore()); await slave.Start(); ``` > **说明**:当前 `ModbusLink.Socket` 项目内置基于 TouchSocket 的实现,但接口本身可由系统自带 `System.Net.Sockets` 适配,无需强依赖第三方。 --- ## 支持的数据类型 寄存器读写支持泛型类型,各类型占用的寄存器数量如下: | 数据类型 | 占用寄存器数 | 说明 | |---|---|---| | `bool` | — | 线圈/离散输入专用 | | `byte` / `ushort` / `short` | 1 | 16 位整型 | | `uint` / `int` / `float` | 2 | 32 位数据,按大端(AB CD)排列 | | `double` | 4 | 64 位浮点数,按大端排列 | **示例**:读取 3 个 `float` 实际上会读取 6 个保持寄存器。 ```csharp // 读取 3 个 float(共 6 个寄存器) var request = ModbusRequest.ReadCreate(deviceAdd: 1, startAdd: 0, length: 3); var response = await master.ReadHoldingRegisters(request); // response.Data 为 float[3] ``` --- ## 异常处理与错误码 ### 响应状态枚举 所有主站操作返回 `ModbusResponse`,通过 `Status` 属性(`ModbusResponseStatus` 枚举)判断结果。枚举值一次覆盖成功、通信失败与全部 8 种 Modbus 协议异常码: | 枚举值 | 数值 | 含义 | 类别 | |---|---|---|---| | `Success` | 0 | 操作成功 | 成功 | | `IllegalFunction` | 1 | 异常码 01 非法功能码 | 协议异常 | | `IllegalDataAddress` | 2 | 异常码 02 非法数据地址 | 协议异常 | | `IllegalDataValue` | 3 | 异常码 03 非法数据值 | 协议异常 | | `SlaveDeviceFailure` | 4 | 异常码 04 从站设备故障 | 协议异常 | | `Acknowledge` | 5 | 异常码 05 确认(长操作已启动) | 协议异常 | | `SlaveDeviceBusy` | 6 | 异常码 06 从站设备忙 | 协议异常 | | `NegativeAcknowledge` | 7 | 异常码 07 否定确认 | 协议异常 | | `MemoryParityError` | 8 | 异常码 08 存储奇偶校验错误 | 协议异常 | | `CommunicationError` | 0xFF | 通信失败(超时/断连/CRC/格式错误) | 通信失败 | > **设计要点**:协议异常码的枚举值与 Modbus 标准异常码 1~8 一一对应,可直接 `(ModbusResponseStatus)exceptionCode` 转换。 ### 主站响应对象属性 `ModbusResponse` 包含以下属性: | 属性 | 说明 | |---|---| | `Status` | 响应状态枚举(成功/通信失败/各类协议异常码) | | `IsSuccess` | 是否成功(便捷属性,等价于 `Status == Success`) | | `Message` | 描述信息(成功时为简要说明,失败时为错误描述) | | `Exception` | 异常对象(仅通信失败时携带,协议异常和成功时为 null) | | `RequestData` | 请求原始字节数组(便于调试) | | `ResponseData` | 响应原始字节数组(便于调试) | | `Data` | 解析后的数据数组(线圈为 `bool[]`,寄存器为对应数值类型数组) | ### 工厂方法 `ModbusResponse` 提供三个工厂方法,对应三种结果场景: | 工厂方法 | 适用场景 | |---|---| | `Success(data, request, response, message)` | 操作成功,携带解析后的数据 | | `CommunicationError(ex, request, message)` | 通信失败(超时/断连/CRC/格式错误),携带异常对象 | | `ProtocolError(errorStatus, request, response, message)` | 协议异常(从站返回异常响应,异常码 01~08) | ### 使用示例 ```csharp var response = await master.ReadHoldingRegisters(request); if (response.IsSuccess) { // 成功,访问 response.Data } else if (response.Status == ModbusResponseStatus.CommunicationError) { // 通信失败(网线断了/超时),访问 response.Exception Console.WriteLine($"通信失败: {response.Message},异常: {response.Exception}"); } else { // 协议异常(从站拒绝请求),response.Status 即异常码 Console.WriteLine($"协议异常: {response.Status}(异常码 {(int)response.Status:D2})"); } ``` --- ## API 参考 ### 主站接口 (`IModbusMaster`) ```csharp public interface IModbusMaster { // Modbus 操作(接口不含 Connection/IsConnected,连接状态由外部连接对象查询) Task> ReadCoils(ModbusRequest request); Task> WriteCoils(ModbusRequest request); Task> ReadHoldingRegisters(ModbusRequest request) where TData : unmanaged; Task> ReadInputRegisters(ModbusRequest request) where TData : unmanaged; Task> WriteHoldingRegisters(ModbusRequest request) where TData : unmanaged; } ``` > Master 接口仅包含纯粹的 Modbus 协议操作,不暴露 `Connection`/`IsConnected`,不包含连接的 Open/Close 方法,也不继承 `IDisposable`。连接由调用方通过构造函数注入并独立管理。 ### 从站接口 (`IModbusSlave`) ```csharp public interface IModbusSlave { byte DeviceAddress { get; } // 从站地址 bool IsRunning { get; } // 监听服务是否运行 Task Start(); // 启动监听服务(不打开连接) Task Stop(); // 停止监听服务(不关闭连接) } ``` > Slave 的 `Start`/`Stop` 仅控制监听服务,不触碰连接本身。`IDisposable.Dispose()` 等价于 `Stop()`。 ### 串口连接抽象 (`ISerialPort`) ```csharp public interface ISerialPort : IDisposable { bool IsOpen { get; } int ReadTimeout { get; set; } int WriteTimeout { get; set; } int BytesToRead { get; } void Open(); void Close(); void DiscardInBuffer(); void Write(byte[] buffer, int offset, int count); int Read(byte[] buffer, int offset, int count); Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); } ``` ### TCP 客户端连接抽象 (`ISocket`) ```csharp public interface ISocket : IDisposable { bool IsOpen { get; } int ReadTimeout { get; set; } int WriteTimeout { get; set; } void Open(); void Close(); void Write(byte[] buffer, int offset, int count); int Read(byte[] buffer, int offset, int count); Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); } ``` ### TCP 服务端监听抽象 (`IServerSocket`) ```csharp public interface IServerSocket : IDisposable { bool IsRunning { get; } void Start(); void Stop(); event EventHandler ClientConnected; event EventHandler ClientDisconnected; } public class SocketSessionEventArgs : EventArgs { public ISocket Session { get; } public SocketSessionEventArgs(ISocket session); } ``` ### 串口连接默认实现 (`SerialPortConnection`) | 构造函数 | 说明 | |---|---| | `SerialPortConnection(string portName, int baudRate, int dataBits = 8, Parity parity = Parity.None, StopBits stopBits = StopBits.One)` | 通过串口参数构造,内部创建 `SerialPortStream` | | `SerialPortConnection(SerialPortStream serialPortStream)` | 包装已配置好的 `SerialPortStream`(生命周期由本实例接管) | 读写超时通过 `ReadTimeout` / `WriteTimeout` 属性设置,默认均为 1000ms。 ### TCP 连接默认实现 (`TouchSocketConnection` / `TouchSocketServer`) | 类 | 构造函数 | 说明 | |---|---|---| | `TouchSocketConnection` | `TouchSocketConnection(string host, int port)` | TCP 客户端连接,封装 TouchSocket `TcpClient` | | `TouchSocketServer` | `TouchSocketServer(string host, int port)` | TCP 服务端监听,封装 TouchSocket `TcpService`,支持多客户端 | > **推送转拉取**:TouchSocket 是事件驱动(Received 委托)模型,而 `ISocket` 需要阻塞/异步 Read。实现内部使用 `SemaphoreSlim` + `List` 缓冲区将推送模型转换为拉取模型。 ### 协议接口汇总 ```csharp // RTU 主站:IModbusRtuMaster : IModbusMaster // 构造:new ModbusRtuMaster(ISerialPort connection) // 仅协议操作,无连接 Open/Close // RTU 从站:IModbusRtuSlave : IModbusSlave // 构造:new ModbusRtuSlave(ISerialPort connection) // 配置:SetDeviceAddress / SetSlaveDataStore(链式) // 服务:Start() / Stop() // TCP 主站:IModbusTcpMaster : IModbusMaster // 构造:new ModbusTcpMaster(ISocket connection) // 仅协议操作,MBAP + PDU,无 CRC // TCP 从站:IModbusTcpSlave : IModbusSlave // 构造:new ModbusTcpSlave(IServerSocket serverSocket) // 配置:SetDeviceAddress / SetSlaveDataStore(链式) // 服务:Start() / Stop(),Server 模式支持多客户端并发 ``` --- ## 调试与测试 ### 虚拟串口联调(RTU) 在没有物理设备的情况下,推荐使用虚拟串口对(Virtual Serial Port Pair)进行主从联调: - **Windows**: [com0com](http://com0com.sourceforge.net/) 或 Virtual Serial Port Driver - **创建虚拟串口对**(如 COM3 ↔ COM4) - **主站**绑定 COM3,**从站**绑定 COM4 ```csharp // 主站 var masterConn = new SerialPortConnection("COM3", 9600); masterConn.Open(); var master = new ModbusRtuMaster(masterConn); // 从站 var slaveConn = new SerialPortConnection("COM4", 9600); slaveConn.Open(); var slave = new ModbusRtuSlave(slaveConn) .SetDeviceAddress(0x01) .SetSlaveDataStore(new SlaveDataStore()); await slave.Start(); ``` ### 本地 TCP 联调(TCP) TCP 协议无需虚拟串口,直接使用本地回环地址即可联调: ```csharp // 从站(先启动) var serverSocket = new TouchSocketServer("0.0.0.0", 502); var slave = new ModbusTcpSlave(serverSocket) .SetDeviceAddress(0x01) .SetSlaveDataStore(new SlaveDataStore()); await slave.Start(); // 主站(连接本地从站) var clientConn = new TouchSocketConnection("127.0.0.1", 502); clientConn.Open(); var master = new ModbusTcpMaster(clientConn); ``` ### 单元测试(自定义连接) 得益于 `ISerialPort` / `ISocket` 抽象,可以注入 mock 对连接行为进行单元测试: ```csharp public class MockSerialPort : ISerialPort { private readonly Queue _responses = new(); public void EnqueueResponse(byte[] data) => _responses.Enqueue(data); public bool IsOpen { get; private set; } = true; public int ReadTimeout { get; set; } public int WriteTimeout { get; set; } public int BytesToRead => _responses.Count > 0 ? _responses.Peek().Length : 0; public void Open() => IsOpen = true; public void Close() => IsOpen = false; public void DiscardInBuffer() { } public void Write(byte[] buffer, int offset, int count) { /* 捕获请求 */ } public int Read(byte[] buffer, int offset, int count) { if (_responses.Count == 0) return 0; var data = _responses.Dequeue(); Array.Copy(data, 0, buffer, offset, data.Length); return data.Length; } public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) => Task.FromResult(Read(buffer, offset, count)); public void Dispose() { } } // 测试用例 var mock = new MockSerialPort(); // IsOpen 默认为 true,无需 Open mock.EnqueueResponse(new byte[] { 0x01, 0x03, 0x02, 0x00, 0x64, /* CRC */ }); var master = new ModbusRtuMaster(mock); ``` ### 日志与原始数据 主站响应对象包含 `RequestData` 和 `ResponseData` 字节数组,可用于打印和协议分析: ```csharp var response = await master.ReadHoldingRegisters(request); Console.WriteLine("请求: " + BitConverter.ToString(response.RequestData)); Console.WriteLine("响应: " + BitConverter.ToString(response.ResponseData)); ``` --- ## 从 1.x 升级 2.0 为破坏性变更,连接管理模型与响应模型均有重大调整,需调整调用代码: ### 1. 主站:连接改为构造注入 + 外部打开 ```csharp // 1.x var config = new SerialPortConfig { PortName = "COM1", BaudRate = 9600, ReadTimeout = 1000 }; var master = new ModbusRtuMaster(config); await master.OpenConnect(); // ... await master.CloseConnect(); // 2.0 ISerialPort connection = new SerialPortConnection("COM1", 9600) { ReadTimeout = 1000 }; connection.Open(); // 由调用方打开 var master = new ModbusRtuMaster(connection); // 构造注入 // ... connection.Close(); // 由调用方关闭 ``` ### 2. 从站:连接构造注入 + Start/Stop 控制监听 ```csharp // 1.x var slave = new ModbusRtuSlave() .SetDeviceAddress(0x01) .SetSerialPortConfig(new SerialPortConfig("COM2", 9600)) .SetSlaveDataStore(new SlaveDataStore()); await slave.OpenConnect(); // ... await slave.CloseConnect(); // 2.0 var connection = new SerialPortConnection("COM2", 9600); connection.Open(); var slave = new ModbusRtuSlave(connection) .SetDeviceAddress(0x01) .SetSlaveDataStore(new SlaveDataStore()); await slave.Start(); // 启动监听(不打开连接) // ... await slave.Stop(); // 停止监听(不关闭连接) connection.Close(); // 由调用方关闭连接 ``` ### 3. 响应模型:字段重命名与状态枚举化 ```csharp // 1.x if (response.IsSuccess) { var data = response.RegistersData; } else { Console.WriteLine(response.ResponseErrorInfo); } // 2.0 if (response.IsSuccess) // IsSuccess 便捷属性保留,迁移顺畅 { var data = response.Data; // RegistersData → Data } else { // 通过 Status 枚举精确判断失败类型 Console.WriteLine($"状态:{response.Status},错误信息:{response.Message}"); // ResponseErrorInfo → Message(统一描述) // ResponseErrorDetail → Exception(仅通信失败时携带) } ``` ### 4. 关键差异速查 | 维度 | 1.x | 2.0 | |---|---|---| | 连接配置 | `SerialPortConfig` | `SerialPortConnection`(`ISerialPort`) | | 连接打开 | `master.OpenConnect()` 内部打开 | 调用方 `connection.Open()` | | 连接关闭 | `master.CloseConnect()` 内部关闭 | 调用方 `connection.Close()` | | Master 创建 | `new ModbusRtuMaster(config)` | `new ModbusRtuMaster(connection)` | | Slave 启动监听 | `slave.OpenConnect()` | `slave.Start()` | | Slave 停止监听 | `slave.CloseConnect()` | `slave.Stop()` | | Master IDisposable | 是(Dispose 关连接) | 否(不持有连接) | | Slave IDisposable | 是(Dispose 关连接) | 是(Dispose 等价 Stop,不动连接) | | 响应成功判断 | `response.IsSuccess` | `response.IsSuccess`(保留)/ `response.Status == Success` | | 响应数据字段 | `response.RegistersData` | `response.Data` | | 响应错误描述 | `response.ResponseErrorInfo` | `response.Message` | | 响应异常对象 | `response.ResponseErrorDetail` | `response.Exception` | | 响应状态 | 仅 `IsSuccess` 布尔 | `Status` 枚举(成功/通信失败/8 种协议异常码) | | 抽象层位置 | 分散在各实现项目 | 统一收归 `ModbusLink.Core` | | 协议栈 | 仅 RTU | RTU + TCP | --- ## 贡献 欢迎提交代码、报告问题或提出改进建议! - 提交 Issue: [Gitee Issues](https://gitee.com/ChangGengCN/modbus-link/issues) - 提交 Pull Request: [Gitee Pull Requests](https://gitee.com/ChangGengCN/modbus-link/pulls) --- ## 鸣谢 - [RJCP.SerialPortStream](https://github.com/jcurl/SerialPortStream) — 底层跨平台串口通信库 - [TouchSocket](https://touchsocket.net/) — 高性能 .NET 套接字通信库 --- ## 许可证 本项目基于 [MIT](LICENSE) 许可证开源。