本文档描述 `DriverModbusRtu` 的设计、配置、接口和实现要点。该驱动实现位于 `Pascal.Edge.DeviceDriver.Modbus\DriverModbusRtu.cs`,用于通过串口或 RTU-over-TCP 与 Modbus 设备通信。

## 概览

- 驱动名:`DriverModbusRtu`

- 目的:提供轻量、可复用的 Modbus RTU 驱动,支持串口 RTU 与 RTU-over-TCP(RTU 帧通过 TCP,不使用 MBAP)。

- 能力:周期性读写、块读合并、数据类型/字节序转换、错误处理、重连与运行时快照发布。

## 快速开始

1. 在宿主中构造 `DriverModbusRtuParameter` 并传入 `StartDevice(...)`。

2. 使用 `UpdateAddresses(...)` 提供 `DeviceAddressItem` 列表。

3. 可注入 `DriverModbusRtu.AddressUpdatedCallback` 以接收地址更新回调并发布或存储变更。

4. 调用 `WriteAddressAsync` / `WriteAddressesAsync` 执行写操作。

## 配置示例

示例 JSON(可放在 `modbusRtu.json` 或由宿主传入):

```json

{

  "ComPort": "COM1",

  "BaudRate": 19200,

  "DataBits": 8,

  "Parity": "None",

  "StopBits": 1,

  "SlaveId": 1,

  "ReadTimeout": 3000,

  "ReadCycle": 5000,

  "UseTcp": false,

  "TcpHost": "192.168.0.10",

  "TcpPort": 502,

  "ConnectTimeout": 3000,

  "ReconnectInterval": 5000,

  "MaxBlockReadRegisterCount": 120,

  "MaxMergeAddressGap": 5,

  "IsBlockReadRequireAddressContinuous": false,

  "DataFormat": "ABCD",

  "IsEnable": true

}

```

常用字段说明:

- `ComPort` / `BaudRate` / `DataBits` / `Parity` / `StopBits`:串口参数。

- `SlaveId`:Modbus 从站地址(1..247)。

- `ReadTimeout`:单次请求超时(ms)。

- `ReadCycle`:读取循环周期(ms)。

- `UseTcp` / `TcpHost` / `TcpPort`:TCP-over-RTU 配置。

- `MaxBlockReadRegisterCount`:单次块读寄存器上限。

- `MaxMergeAddressGap`、`IsBlockReadRequireAddressContinuous`:块合并策略。

- `DataFormat`:寄存器字节/寄存器顺序(例如 `ABCD`, `BADC`)。

完整配置示例:

{
  "protocolCategory": "Generic",
  "driverType": "DriverModbusRtu",
  "description": "Modbus RTU driver",
  "supportedDataTypes": ["Boolean","Byte","Char","DateTime","Double","Int16","Int32","Int64","Sbyte","Float","String","UInt16","UInt32","UInt64"],
  "addressRegex": "^[1-4]:(0{1,5}|0{0,4}[1-9]|0{0,3}[1-9]\\d|0{0,2}[1-9]\\d{2}|0?[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$",
  "supportsPresets": true,
  "allowManualAddress": true,
  "hasBuiltInAddresses": true,
  "supportsDynamicBrowse": false,
  "addressList": [
  ],
  "addressExamples": {
    "coil": "1:00001",
    "discreteInput": "2:00001",
    "inputRegister": "3:00001",
    "holdingRegister": "4:00001",
    "note": "格式为 <类型前缀>:<五位地址>,类型 1=Coil 2=Discrete Input 3=Input Register 4=Holding Register"
  },
  "parameter": [
    {
      "defaultValue": "COM1",
      "description": "Serial COM Port (e.g., COM1, COM2)",
      "label": "串口号",
      "name": "comPort",
      "orderNo": 1,
      "type": "string",
      "category": "Connection",
      "controlType": "dynamic-select",
      "dataSource": "system:serial-ports"
    },
    {
      "defaultValue": 9600,
      "description": "Baud Rate for serial communication",
      "label": "波特率",
      "name": "baudRate",
      "orderNo": 2,
      "type": "int",
      "category": "Connection",
      "controlType": "select",
      "options": [9600, 19200, 38400, 57600, 115200]
    },
    {
      "defaultValue": 0,
      "description": "Parity setting (0=None,1=Odd,2=Even)",
      "label": "校验位",
      "name": "parity",
      "orderNo": 3,
      "type": "int",
      "category": "Connection",
      "controlType": "select",
      "options": [
        { "label": "None", "value": 0 },
        { "label": "Odd", "value": 1 },
        { "label": "Even", "value": 2 }
      ]
    },
    {
      "defaultValue": 8,
      "description": "Number of data bits",
      "label": "数据位",
      "name": "dataBits",
      "orderNo": 4,
      "type": "int",
      "category": "Connection",
      "controlType": "select",
      "options": [7, 8]
    },
    {
      "defaultValue": 1,
      "description": "Number of stop bits",
      "label": "停止位",
      "name": "stopBits",
      "orderNo": 5,
      "type": "int",
      "category": "Connection",
      "controlType": "select",
      "options": [
        { "label": "1", "value": 1 },
        { "label": "2", "value": 2 }
      ]
    },
    {
      "defaultValue": 1,
      "description": "Modbus Slave ID of the device",
      "label": "站号",
      "name": "slaveId",
      "orderNo": 6,
      "type": "int",
      "category": "Connection",
      "controlType": "number"
    },
    {
      "defaultValue": false,
      "description": "If true, use TCP transport to proxy RTU frames instead of serial",
      "label": "使用TCP透传",
      "name": "useTcp",
      "orderNo": 7,
      "type": "bool",
      "category": "Connection",
      "controlType": "switch"
    },
    {
      "defaultValue": "127.0.0.1",
      "description": "TCP host for RTU-over-TCP or TCP fallback",
      "label": "TCP主机",
      "name": "tcpHost",
      "orderNo": 8,
      "type": "string",
      "category": "Connection",
      "controlType": "text",
      "visibleWhen": { "useTcp": true }
    },
    {
      "defaultValue": 502,
      "description": "TCP port for RTU-over-TCP or TCP fallback",
      "label": "TCP端口",
      "name": "tcpPort",
      "orderNo": 9,
      "type": "int",
      "category": "Connection",
      "controlType": "number",
      "visibleWhen": { "useTcp": true }
    },
    {
      "defaultValue": "ABCD",
      "description": "Default data format for communication (e.g. ABCD)",
      "label": "数据格式",
      "name": "dataFormat",
      "orderNo": 1,
      "type": "string",
      "category": "Advanced",
      "controlType": "select",
      "options": ["ABCD", "BADC", "CDAB", "DCBA"]
    },
    {
      "defaultValue": 120,
      "description": "Maximum number of registers to read in a single block",
      "label": "单块最大寄存器数",
      "name": "maxBlockReadRegisterCount",
      "orderNo": 2,
      "type": "int",
      "category": "Advanced",
      "controlType": "number"
    },
    {
      "defaultValue": false,
      "description": "Indicates if block read requires continuous addresses",
      "label": "块读取地址需连续",
      "name": "isBlockReadRequireAddressContinuous",
      "orderNo": 3,
      "type": "bool",
      "category": "Advanced",
      "controlType": "switch"
    },
    {
      "defaultValue": 10,
      "description": "Maximum merge address gap when combining non-contiguous blocks",
      "label": "最大合并空洞",
      "name": "maxMergeAddressGap",
      "orderNo": 4,
      "type": "int",
      "category": "Advanced",
      "controlType": "number"
    }
  ]
}

## 地址格式

- 格式:`"[type:]address"`(`type` 可省略,默认 `hr`)。

- 支持类型:

  - `coil` / `c` / `1` — coils(位,可写)

  - `di` / `discreteinput` / `2` — discrete inputs(位,只读)

  - `hr` / `holding` / `3` — holding registers(寄存器,可写)

  - `ir` / `input` / `4` — input registers(寄存器,只读)

- 解析采用 `Split(':', 2)`,数字类型会映射到对应字符串。

示例:`"coil:5"`、`"hr:100"`、`"200"`(默认视为 `hr:200`)。

## 接口契约(要点)

实现接口:`IDeviceRuntimeInstance`

主要方法与属性:

- `bool StartDevice(DeviceOrganizationParameter orgParam, ICommunicationParameter commParam, List<DeviceAddressItem> addresses)`

- `bool StopDevice()`

- `Task<(bool Success, string? Error)> WriteAddressAsync(string name, object? value)`

- `Task<(bool Success, string? Error)> WriteAddressesAsync(IEnumerable<(string Name, object? Value)> items)`

- `void UpdateAddresses(List<DeviceAddressItem> addresses)`

- `Action<DeviceAddressItem>? AddressUpdatedCallback`(回调)

- `DeviceDriverStatus DeviceDriverStatus`, `bool IsEnable`, `int DataItemCount` 等状态/统计字段

设计要点:驱动通过回调把地址更新通知宿主,宿主负责进一步发布或存储。

## 读取策略与块合并

1. 为每个地址计算所需寄存器数量:`GetQuantityByDataType(deviceType, registerType)`(如 `float`→2,`double`→4)。

2. 按 `type` 分组并按起始地址排序。

3. 合并规则:尝试扩展 block,直到超过 `MaxBlockReadRegisterCount`,或不满足连续/最大 gap 条件。

   - 若 `IsBlockReadRequireAddressContinuous` 为 true,仅允许严格连续合并。

   - 否则允许空隙 ≤ `MaxMergeAddressGap`。

4. 对超过最大块的区域分段读取。

5. 读取完成后将合并结果拆分到每个地址项,调用 `ConvertRegisterArray` 处理字节序和类型,必要时再调用 `DeviceDataTypeConverter.Convert` 做二次转换。

## 写操作

- 支持:

  - Coil 写:功能码 `0x05`(Write Single Coil)。

  - 单寄存器写:`0x06`。

  - 多寄存器写:`0x10`(优先),失败可回退为逐寄存器 `0x06`。

写流程:

1. 规范化输入 `NormalizeIncomingValue`(支持 `System.Text.Json.JsonElement` 与 `Newtonsoft.Json.Linq.JToken`)。

2. 如配置 `ConvertedDataType` 不同,先反向转换到设备类型。

3. 根据 `DeviceDataType` 打包为 `ushort[]`(注意 `float/int32` 的字节序)。

4. 通过 `_bus` 或 `_tcpBus` 发送 0x10(带 payload)或 0x06(逐寄存器)。

. 更新 `DeviceAddressItem.Status` 并触发回调。

## 字节序与数据类型处理

- 寄存器初始按大端(`r0_hi, r0_lo, r1_hi, r1_lo, ...`)构建为 `bytesBE`。

- `DataFormat`(如 `ABCD`, `BADC`)用于对 4 字节类型重排并最终转换为 little-endian 供 `BitConverter` 使用(`GetBytesFromRegisters`)。

- `ConvertRegisterArray` 根据设备数据类型返回 CLR 类型并应用 `DataMultiplier`。

## 总线抽象与并发控制

- 优先使用共享/池化实现:`ModbusSerialBus`(串口)与 `ModbusTcpBus`(TCP-over-RTU)。

- 备选:直接使用 `_serialPort` 与内部 RTU 帧实现(`SendReceiveRequest`)。

- 并发保护:

  - `_serialLock`:串口读/写互斥

  - `_addressLock`:保护地址集合与项状态更改

  - `_slaveTasksLock`:保护并发任务集合

## 错误处理与重连

- 超时或 CRC 校验失败会导致响应为 `null`,对应地址会被标记为 `Invalid` 并写入 `ErrorReason`。

- TCP-over-RTU 丢线时,`ReadLoopAsync` 会尝试 `_tcpBus.ReconnectAsync(...)`,并按 `ReconnectInterval` 重试。

- 日志通过静态注入 `DriverModbusRtu.Logger`(`ILogger`),关键事件和异常应记录以便诊断。

## 运行时快照发布

- `PublishDeviceSnapshot()` 会尝试通过反射查找 `Pascal.Edge.Tools.ProducerQueue` 并发布 `/real/{deviceCode}` 快照。

- 快照包括设备代码、驱动状态、读取周期与地址摘要,便于监控与上报。

## 测试建议

- 单元测试:

  - `ConvertRegisterArray` / `GetBytesFromRegisters`(不同 `DataFormat`)

  - `GetQuantityByDataType`

  - `NormalizeIncomingValue`(`JsonElement` 与 `JToken`)

  - `WriteAddressAsync`(使用替身替换 `_bus`/`_tcpBus` 模拟)

- 集成测试:使用 Modbus 模拟器验证块读合并、写帧与重连策略。

- 建议将串口/总线抽象化,以便注入测试替身进行 CI 测试。

## 扩展建议

- 抽象并注入串口/总线依赖以便单元测试。

- 增加对更多数据类型(BCD、字符串、24-bit)的支持。

- 写优化:先尝试 0x10,再回退 0x06,并提供写队列与节流。

- 增加度量(OpenTelemetry/Prometheus)与更细粒度的日志 Scope。

## 示例地址项

- `Name: "PumpSpeed", Address: "hr:100", DeviceDataType: "Int16", DataMultiplier: 0.1`

- `Name: "Energy", Address: "hr:200", DeviceDataType: "Float", DataFormat: "BADC"`

- `Name: "Alarm", Address: "coil:5", DeviceDataType: "Boolean"`

完整的驱动配置设备文件示例:

{
  "driverType": "DriverModbusRtu",
  "organization": {
    "deviceId": "307b6b7b-0038-4f1d-9b46-b63b39f5fc0d",
    "deviceCode": "Mod002",
    "deviceName": "Rtu Over Tcp dev2",
    "description": "mod002",
    "isEnable": false,
    "connectTimeout": 5000,
    "reconnectInterval": 10000,
    "readCycle": 1000,
    "createdAt": "2026-01-18T12:06:26.1657814Z",
    "lastModified": "2026-01-23T12:04:57.2450516Z",
    "metadata": {}
  },
  "communication": {
    "comPort": "COM1",
    "baudRate": 9600,
    "parity": 0,
    "dataBits": 8,
    "stopBits": 1,
    "slaveId": 2,
    "useTcp": true,
    "tcpHost": "127.0.0.1",
    "tcpPort": 1502,
    "dataFormat": "ABCD",
    "maxBlockReadRegisterCount": 120,
    "isBlockReadRequireAddressContinuous": false,
    "maxMergeAddressGap": 10
  },
  "addressList": [
    {
      "name": "pooled",
      "address": "3:00005",
      "deviceDataType": "Int16",
      "convertedDataType": "Float",
      "dataMultiplier": 0.1,
      "description": "\u538B\u529B\u4F20\u611F\u5668\uFF08pooled\uFF09",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9644719Z",
      "status": "Ok",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag1",
      "address": "3:0001",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9652456Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag2",
      "address": "3:0002",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9656367Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag3",
      "address": "3:0003",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9660106Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag4",
      "address": "3:0004",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9664144Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag5",
      "address": "3:0005",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9667858Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag6",
      "address": "3:0006",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.967148Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag7",
      "address": "3:0007",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9675363Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag8",
      "address": "3:0008",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9679047Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag9",
      "address": "3:0009",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9682792Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag10",
      "address": "3:0010",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9686359Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag11",
      "address": "3:0011",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9690033Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag12",
      "address": "3:0012",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:16:58.9694281Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    },
    {
      "name": "tag99",
      "address": "3:0099",
      "deviceDataType": "Int16",
      "convertedDataType": "",
      "dataMultiplier": 1,
      "description": "",
      "unit": "",
      "updateTs": "2026-01-18T12:22:30.962Z",
      "status": "Unknown",
      "isEnable": true,
      "addressChanged": true
    }
  ]
}

其他:Pascal.Edge物联网平台:生产企业设备数据采集与管理解决方案_设备数据采集平台解决方案-CSDN博客

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐