njs-modbus 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +61 -21
- package/README.md +311 -329
- package/README.zh-CN.md +311 -328
- package/dist/index.cjs +7208 -4107
- package/dist/index.d.cts +2862 -0
- package/dist/index.d.mts +2862 -0
- package/dist/index.mjs +7164 -4092
- package/package.json +66 -68
- package/dist/index.d.ts +0 -842
- package/dist/utils.cjs +0 -536
- package/dist/utils.d.ts +0 -163
- package/dist/utils.mjs +0 -522
package/README.zh-CN.md
CHANGED
|
@@ -1,429 +1,412 @@
|
|
|
1
1
|
# njs-modbus
|
|
2
2
|
|
|
3
|
-
[
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<!-- prettier-ignore-start -->
|
|
8
|
-
[](http://www.npm-stats.com/~packages/njs-modbus)
|
|
9
|
-
[](https://www.npmjs.com/package/njs-modbus)
|
|
10
|
-
[](https://bundlephobia.com/package/njs-modbus)
|
|
11
|
-
[](https://github.com/xiejay97/njs-modbus/actions)
|
|
12
|
-
<!-- prettier-ignore-end -->
|
|
13
|
-
|
|
14
|
-
## 特性
|
|
15
|
-
|
|
16
|
-
- **协议:** Modbus TCP、RTU、ASCII
|
|
17
|
-
- **传输:** 串口、TCP 客户端/服务端、UDP 客户端/服务端
|
|
18
|
-
- **主站:** FIFO 或流水线并发请求(协议必须为 TCP)
|
|
19
|
-
- **从站:** 每连接 FIFO 或并发处理(协议必须为 TCP)
|
|
20
|
-
- **自定义功能码**,支持可插拔帧解析
|
|
21
|
-
- **广播**(单元号 = 0)
|
|
22
|
-
- **完整 TypeScript**
|
|
23
|
-
- **零运行时依赖**(仅在使用串口时 peer-depends `serialport`)
|
|
24
|
-
|
|
25
|
-
| 码值 | 名称 |
|
|
26
|
-
|------|------|
|
|
27
|
-
| 01 | 读取线圈 |
|
|
28
|
-
| 02 | 读取离散输入 |
|
|
29
|
-
| 03 | 读取保持寄存器 |
|
|
30
|
-
| 04 | 读取输入寄存器 |
|
|
31
|
-
| 05 | 写单个线圈 |
|
|
32
|
-
| 06 | 写单个寄存器 |
|
|
33
|
-
| 15 | 写多个线圈 |
|
|
34
|
-
| 16 | 写多个寄存器 |
|
|
35
|
-
| 17 | 报告服务器 ID |
|
|
36
|
-
| 22 | 掩码写寄存器 |
|
|
37
|
-
| 23 | 读/写多个寄存器 |
|
|
38
|
-
| 43/14 | 读取设备标识 |
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://modbus.org/)
|
|
39
7
|
|
|
40
|
-
|
|
8
|
+
[English](README.md) | **中文**
|
|
9
|
+
|
|
10
|
+
> 面向 Node.js 的生产级、热路径零 GC Modbus 协议栈 —— 支持 TCP、RTU、ASCII,可运行在 TCP、UDP、TLS、串口或任何可被建模为字节管道的自定义传输层之上。
|
|
11
|
+
|
|
12
|
+
`njs-modbus` 使用严格 TypeScript 编写,目标运行时为 Node.js `>=18.19`。其设计充分考虑了工业现场常见的 GC 抖动、静默帧损坏和非受控总线访问等实际约束:确定性时延、流式帧恢复、可编程访问控制与审计日志均内置于核心。
|
|
13
|
+
|
|
14
|
+
采用 **Business Source License 1.1(BSL 1.1)** 许可。个人、教育机构、非营利组织以及年营收低于 100 万美元的企业,可免费用于开发、测试及生产环境。更大规模组织可购买专有商业许可。每个版本在其 Change Date 之后自动转为 **Apache-2.0** 许可。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 目录
|
|
41
19
|
|
|
42
|
-
|
|
20
|
+
- [njs-modbus 是什么?](#njs-modbus-是什么)
|
|
21
|
+
- [为什么选择 njs-modbus?](#为什么选择-njs-modbus)
|
|
22
|
+
- [功能矩阵](#功能矩阵)
|
|
23
|
+
- [安装](#安装)
|
|
24
|
+
- [快速开始](#快速开始)
|
|
25
|
+
- [TCP 主站](#tcp-主站)
|
|
26
|
+
- [TCP 从站](#tcp-从站)
|
|
27
|
+
- [串口 RTU](#串口-rtu)
|
|
28
|
+
- [架构](#架构)
|
|
29
|
+
- [核心能力](#核心能力)
|
|
30
|
+
- [支持的功能码](#支持的功能码)
|
|
31
|
+
- [基准测试](#基准测试)
|
|
32
|
+
- [安全与合规](#安全与合规)
|
|
33
|
+
- [商业支持与许可](#商业支持与许可)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## njs-modbus 是什么?
|
|
38
|
+
|
|
39
|
+
`njs-modbus` 是一个面向 Node.js 的分层 Modbus 协议库。协议层只与 Buffer 打交道,不感知任何底层物理设备。这意味着同一套主站/从站逻辑可以跑在 TCP、UDP、TLS、串口、WebSocket、内存 mock 或任何其他传输层之上 —— 你只需在 `AbstractPipelineAdapter` 接口后实现一次传输层,即可无需改动地复用完整协议栈。
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
┌─────────────────────────────────────────────┐
|
|
43
|
+
│ 应用层:ModbusMaster / ModbusSlave │
|
|
44
|
+
├─────────────────────────────────────────────┤
|
|
45
|
+
│ 协议帧层:TCP / RTU / ASCII │
|
|
46
|
+
├─────────────────────────────────────────────┤
|
|
47
|
+
│ 管道层:AbstractPipelineAdapter │
|
|
48
|
+
├─────────────────────────────────────────────┤
|
|
49
|
+
│ 物理传输层:TCP / UDP / TLS / │
|
|
50
|
+
│ 串口 / WebSocket / 自定义 │
|
|
51
|
+
└─────────────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **严格 TypeScript** —— 协议类型字面量(`'TCP' | 'RTU' | 'ASCII'`)与类型化 Promise API,可在编译期发现大多数集成错误。
|
|
55
|
+
- **热路径零 GC 解码** —— 显式有限状态机帧同步,在稳态运行期间不在 JavaScript 堆上分配对象或 Buffer。
|
|
56
|
+
- **传输层无关** —— 一个适配器接口,任意物理链路。
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 为什么选择 njs-modbus?
|
|
61
|
+
|
|
62
|
+
| 关注点 | 你能获得什么 |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| **确定性性能** | 解码路径零 GC、编码路径低分配,编解码 P50 时延低于 1 微秒,热路径无垃圾回收停顿。 |
|
|
65
|
+
| **生产级帧同步** | 流式状态机可从脏数据、粘包、残帧、跨边界分片以及 CRC/LRC 损坏中自愈,不会把无效数据泄漏到相邻帧。 |
|
|
66
|
+
| **类型安全** | 严格 TypeScript + 类型化 Promise API,多数集成错误在编译期即可发现。 |
|
|
67
|
+
| **访问控制与审计** | unit、地址、运行时三道闸门策略钩子,以及从站结构化 `accessAudit` 事件,满足合规与溯源需求。 |
|
|
68
|
+
| **传输层自由** | TCP、UDP、TLS、串口或自定义传输,统一通过 `AbstractPipelineAdapter` 接入,协议逻辑始终不变。 |
|
|
69
|
+
| **商业许可清晰** | BSL 1.1:个人、非营利机构及小企业免费使用;大型组织可购买商业许可;Change Date 后自动转为 Apache-2.0。 |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 功能矩阵
|
|
74
|
+
|
|
75
|
+
| 能力 | TCP | RTU | ASCII |
|
|
76
|
+
| --- | :---: | :---: | :---: |
|
|
77
|
+
| 主站 / 客户端 | ✅ | ✅ | ✅ |
|
|
78
|
+
| 从站 / 服务端 | ✅ | ✅ | ✅ |
|
|
79
|
+
| 并发流水线 | ✅ | — | — |
|
|
80
|
+
| 广播(`unit === 0`) | ✅ | ✅ | ✅ |
|
|
81
|
+
| 自定义功能码 | ✅ | ✅* | ✅ |
|
|
82
|
+
| 流式帧恢复 | ✅ | ✅ | ✅ |
|
|
83
|
+
|
|
84
|
+
\* RTU 自定义功能码需要提供 `determineFrameLength` 回调,以便帧状态机在不缓冲的情况下确定帧长度。
|
|
85
|
+
|
|
86
|
+
由于协议层与传输层解耦,任意协议(TCP / RTU / ASCII)都可以运行在任意提供管道适配器的传输层之上。内置传输包括 TCP、UDP、TLS(基于 TCP)和串口;WebSocket 示例展示了一套自定义适配器。
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 安装
|
|
43
91
|
|
|
44
92
|
```bash
|
|
45
93
|
npm install njs-modbus
|
|
46
94
|
```
|
|
47
95
|
|
|
48
|
-
|
|
96
|
+
串口支持通过可选的对等依赖提供:
|
|
49
97
|
|
|
50
98
|
```bash
|
|
51
99
|
npm install serialport
|
|
52
100
|
```
|
|
53
101
|
|
|
102
|
+
需要 Node.js `>=18.19`。
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
54
106
|
## 快速开始
|
|
55
107
|
|
|
56
108
|
### TCP 主站
|
|
57
109
|
|
|
58
110
|
```typescript
|
|
59
|
-
import { ModbusMaster } from 'njs-modbus';
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
111
|
+
import { ModbusMaster, TcpClientPhysicalLayer } from 'njs-modbus';
|
|
112
|
+
|
|
113
|
+
const physical = new TcpClientPhysicalLayer();
|
|
114
|
+
|
|
115
|
+
physical.on('connect', async (pipeline) => {
|
|
116
|
+
const master = new ModbusMaster({
|
|
117
|
+
pipelineAdapter: pipeline,
|
|
118
|
+
protocol: { type: 'TCP' },
|
|
119
|
+
queueStrategy: 'concurrent',
|
|
120
|
+
timeout: 1000,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const response = await master.readHoldingRegisters(1, 0, 10);
|
|
125
|
+
console.log('registers:', response.data);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error('request failed:', (err as Error).message);
|
|
128
|
+
} finally {
|
|
129
|
+
master.destroy();
|
|
130
|
+
physical.close();
|
|
131
|
+
}
|
|
64
132
|
});
|
|
65
133
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
134
|
+
physical.open({ host: '127.0.0.1', port: 502 }, (err) => {
|
|
135
|
+
if (err) {
|
|
136
|
+
console.error('failed to connect:', err.message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
72
140
|
```
|
|
73
141
|
|
|
74
142
|
### TCP 从站
|
|
75
143
|
|
|
76
144
|
```typescript
|
|
77
|
-
import { ModbusSlave } from 'njs-modbus';
|
|
145
|
+
import { ModbusSlave, TcpServerPhysicalLayer } from 'njs-modbus';
|
|
78
146
|
|
|
79
|
-
const
|
|
80
|
-
physical: { type: 'TCP_SERVER' },
|
|
81
|
-
protocol: { type: 'TCP' },
|
|
82
|
-
});
|
|
147
|
+
const physical = new TcpServerPhysicalLayer();
|
|
83
148
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
149
|
+
physical.on('connect', (pipeline) => {
|
|
150
|
+
const slave = new ModbusSlave({
|
|
151
|
+
pipelineAdapter: pipeline,
|
|
152
|
+
protocol: { type: 'TCP' },
|
|
153
|
+
queueStrategy: 'drop-stale',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
slave.addUnit(1, {
|
|
157
|
+
readHoldingRegisters: (address, length, callback) => {
|
|
158
|
+
const values = Array.from({ length }, (_, i) => (address + i) & 0xffff);
|
|
159
|
+
callback(null, values);
|
|
160
|
+
},
|
|
161
|
+
writeSingleRegister: (address, value, callback) => {
|
|
162
|
+
console.log(`write ${value} to ${address}`);
|
|
163
|
+
callback(null);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
89
166
|
});
|
|
90
167
|
|
|
91
|
-
|
|
168
|
+
physical.open({ port: 502 }, (err) => {
|
|
169
|
+
if (err) {
|
|
170
|
+
console.error('failed to listen:', err.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
console.log('slave listening on port 502');
|
|
174
|
+
});
|
|
92
175
|
```
|
|
93
176
|
|
|
94
|
-
### 串口 RTU
|
|
177
|
+
### 串口 RTU
|
|
95
178
|
|
|
96
179
|
```typescript
|
|
97
|
-
|
|
98
|
-
physical: {
|
|
99
|
-
type: 'SERIAL',
|
|
100
|
-
opts: { path: '/dev/ttyUSB0', baudRate: 9600 },
|
|
101
|
-
},
|
|
102
|
-
protocol: { type: 'RTU' },
|
|
103
|
-
});
|
|
180
|
+
import { ModbusMaster, SerialPhysicalLayer } from 'njs-modbus';
|
|
104
181
|
|
|
105
|
-
|
|
106
|
-
const res = await master.readHoldingRegisters(1, 0, 10);
|
|
107
|
-
await master.close();
|
|
108
|
-
```
|
|
182
|
+
const physical = new SerialPhysicalLayer();
|
|
109
183
|
|
|
110
|
-
|
|
184
|
+
physical.on('connect', async (pipeline) => {
|
|
185
|
+
const master = new ModbusMaster({
|
|
186
|
+
pipelineAdapter: pipeline,
|
|
187
|
+
protocol: { type: 'RTU' },
|
|
188
|
+
queueStrategy: 'fifo',
|
|
189
|
+
timeout: 500,
|
|
190
|
+
});
|
|
111
191
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
192
|
+
const res = await master.readHoldingRegisters(1, 0, 10);
|
|
193
|
+
console.log('registers:', res.data);
|
|
194
|
+
|
|
195
|
+
master.destroy();
|
|
196
|
+
physical.close();
|
|
116
197
|
});
|
|
198
|
+
|
|
199
|
+
physical.open({ path: '/dev/ttyUSB0', baudRate: 115200 });
|
|
117
200
|
```
|
|
118
201
|
|
|
119
|
-
|
|
202
|
+
[`examples/`](https://github.com/xiejay97/njs-modbus/tree/main/examples) 目录包含可运行的主站/从站示例,涵盖访问控制、审计日志、TLS 以及 WebSocket 等自定义传输层。
|
|
120
203
|
|
|
121
|
-
|
|
204
|
+
---
|
|
122
205
|
|
|
123
|
-
|
|
124
|
-
|------|-------|-----------------|
|
|
125
|
-
| `SERIAL` | `SerialPhysicalLayer` | 无 |
|
|
126
|
-
| `TCP_CLIENT` | `TcpClientPhysicalLayer` | `SocketConnectOpts` |
|
|
127
|
-
| `TCP_SERVER` | `TcpServerPhysicalLayer` | `ListenOptions` |
|
|
128
|
-
| `UDP_CLIENT` | `UdpClientPhysicalLayer` | `{ port, address }` |
|
|
129
|
-
| `UDP_SERVER` | `UdpServerPhysicalLayer` | `BindOptions` |
|
|
130
|
-
| `CUSTOM` | *(用户自定义)* | *(自定义)* |
|
|
206
|
+
## 架构
|
|
131
207
|
|
|
132
|
-
|
|
208
|
+
库体被组织为四层。每层相互独立、可单独测试、可替换。
|
|
133
209
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
maxConnections: 10,
|
|
141
|
-
idleTimeout: 30000, // 驱逐不活跃连接,0 = 禁用
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
protocol: { type: 'TCP' },
|
|
145
|
-
});
|
|
146
|
-
```
|
|
210
|
+
| 层级 | 职责 | 公共契约 |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| **物理层** | 打开/关闭链路,并为每个连接抛出一个管道实例。 | `AbstractPhysicalLayer` |
|
|
213
|
+
| **管道层** | 搬运原始字节、处理背压,暴露 `write(data)` + `data` 事件接口。 | `AbstractPipelineAdapter` / `AbstractPipelineLayer` |
|
|
214
|
+
| **协议层** | 解析帧、校验 CRC/LRC/MBAP,并抛出完整 ADU。 | `TcpProtocolLayer` / `RtuProtocolLayer` / `AsciiProtocolLayer` |
|
|
215
|
+
| **应用层** | 编排事务、队列、访问控制,并对外暴露 Promise API。 | `ModbusMaster` / `ModbusSlave` |
|
|
147
216
|
|
|
148
|
-
|
|
217
|
+
这种分层让自定义传输层变得极为简单。[`examples/websocket/`](https://github.com/xiejay97/njs-modbus/tree/main/examples/websocket) 中的 WebSocket 示例在 150 行内实现了一套完整管道层。
|
|
149
218
|
|
|
150
|
-
|
|
151
|
-
|--------|------|-------------|
|
|
152
|
-
| `whitelist` | `string[]` | 允许的客户端 IP。`::ffff:` 前缀自动剥离。 |
|
|
153
|
-
| `maxConnections` | `number` | 最大并发连接数。超出后新连接静默丢弃。 |
|
|
154
|
-
| `idleTimeout` | `number` | 空闲超时(毫秒),超时后驱逐连接。默认 `30000`,传 `0` 禁用。 |
|
|
155
|
-
| `socketOpts` / `serverOpts` | `object` | 透传给 Node.js `createSocket()` / `createServer()`。 |
|
|
219
|
+
---
|
|
156
220
|
|
|
157
|
-
##
|
|
221
|
+
## 核心能力
|
|
158
222
|
|
|
159
|
-
|
|
223
|
+
### 低分配编解码热路径
|
|
160
224
|
|
|
161
|
-
|
|
162
|
-
import { AbstractPhysicalLayer, AbstractPhysicalConnection } from 'njs-modbus';
|
|
225
|
+
TCP、RTU、ASCII 的协议帧层均以显式有限状态机实现。解码路径在稳态运行期间不在 JavaScript 堆上分配对象或 Buffer(通过预分配残量缓冲与零拷贝视图实现),从而彻底消除热路径上的 GC 抖动断点。编码路径每帧执行一次有界的 `Buffer.allocUnsafe()` 分配。
|
|
163
226
|
|
|
164
|
-
|
|
165
|
-
class MyConnection extends AbstractPhysicalConnection { /* write / destroy, emit 'data' */ }
|
|
227
|
+
### 流式帧恢复
|
|
166
228
|
|
|
167
|
-
|
|
168
|
-
physical: { type: 'CUSTOM', layer: new MyPhysicalLayer() },
|
|
169
|
-
protocol: { type: 'TCP' },
|
|
170
|
-
});
|
|
171
|
-
```
|
|
229
|
+
帧层以流式方式解析输入字节,可在以下场景中自愈:
|
|
172
230
|
|
|
173
|
-
|
|
231
|
+
- 线路上注入的脏数据。
|
|
232
|
+
- 一次读取中多个有效帧粘连(`sticky`)。
|
|
233
|
+
- 残帧后接有效帧。
|
|
234
|
+
- 一个帧被拆分到多次读取中(跨边界分片)。
|
|
235
|
+
- CRC(RTU)或 LRC(ASCII)损坏。
|
|
174
236
|
|
|
175
|
-
|
|
237
|
+
有效帧会被正常抛出,无效数据被丢弃,不会污染相邻帧。
|
|
176
238
|
|
|
177
|
-
|
|
239
|
+
### 队列策略
|
|
178
240
|
|
|
179
|
-
|
|
180
|
-
new ModbusMaster({
|
|
181
|
-
physical: { type: 'TCP_CLIENT' },
|
|
182
|
-
protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
|
|
183
|
-
timeout?: 1000, // 单次请求超时(毫秒)
|
|
184
|
-
concurrent?: false, // 流水线模式(协议:仅 TCP)
|
|
185
|
-
})
|
|
186
|
-
```
|
|
241
|
+
`ModbusMaster` 与 `ModbusSlave` 均支持四种队列策略:
|
|
187
242
|
|
|
188
|
-
|
|
243
|
+
| 策略 | 行为 | 适用场景 |
|
|
244
|
+
| --- | --- | --- |
|
|
245
|
+
| `fifo` | 严格先进先出执行。 | 串口线路、确定性顺序。 |
|
|
246
|
+
| `drop-stale` | 新请求到达时清空所有未执行的旧请求。 | 只需最新值的遥测采集。 |
|
|
247
|
+
| `deduplicate` | 相同 ADU 指纹的待处理请求会被去重丢弃。 | 可能重叠的轮询循环。 |
|
|
248
|
+
| `concurrent` | 请求并发派发。 | Modbus TCP 或多链路主站/从站。 |
|
|
189
249
|
|
|
190
|
-
|
|
191
|
-
protocol: {
|
|
192
|
-
type: 'RTU',
|
|
193
|
-
opts: {
|
|
194
|
-
// 既可以传裸数字(毫秒),也可以传 `{ unit: 'bit' | 'ms', value: N }`。
|
|
195
|
-
// 传 `0` 显式禁用该定时器(适用于 RTU-over-TCP 等无丢包传输)。
|
|
196
|
-
intervalBetweenFrames: 20, // 20 毫秒
|
|
197
|
-
interCharTimeout: { unit: 'bit', value: 10 }, // 按 bit-time,需 baudRate
|
|
198
|
-
poolSize: 1024,
|
|
199
|
-
},
|
|
200
|
-
}
|
|
201
|
-
```
|
|
250
|
+
默认策略为 `drop-stale`。
|
|
202
251
|
|
|
203
|
-
|
|
252
|
+
### 每 unit 写范围锁
|
|
204
253
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
opts: {
|
|
209
|
-
lenientHex: true, // 接受小写十六进制 (a-f)。默认:false(按规范仅大写)
|
|
210
|
-
},
|
|
211
|
-
}
|
|
212
|
-
```
|
|
254
|
+
对于 `concurrent` 模式下的从站,`enableWriteRangeLock`(默认 `true`)确保同一 unit 上地址范围重叠的写请求(FC05/06/15/16/22/23)被序列化,防止竞态条件。这对于在多个连接同时修改共享寄存器或线圈时保持一致性至关重要。仅对于不需要协调开销的纯同步内存从站,才设置为 `false`。
|
|
255
|
+
|
|
256
|
+
### 访问控制与审计
|
|
213
257
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
| `readDiscreteInputs(unit, address, length, timeout?)` | FC 02 |
|
|
222
|
-
| `readHoldingRegisters(unit, address, length, timeout?)` | FC 03 |
|
|
223
|
-
| `readInputRegisters(unit, address, length, timeout?)` | FC 04 |
|
|
224
|
-
| `writeSingleCoil(unit, address, value, timeout?)` | FC 05 |
|
|
225
|
-
| `writeSingleRegister(unit, address, value, timeout?)` | FC 06 |
|
|
226
|
-
| `writeMultipleCoils(unit, address, values, timeout?)` | FC 15 |
|
|
227
|
-
| `writeMultipleRegisters(unit, address, values, timeout?)` | FC 16 |
|
|
228
|
-
| `reportServerId(unit, serverIdLength?, timeout?)` | FC 17 |
|
|
229
|
-
| `maskWriteRegister(unit, address, andMask, orMask, timeout?)` | FC 22 |
|
|
230
|
-
| `readAndWriteMultipleRegisters(unit, read, write, timeout?)` | FC 23 |
|
|
231
|
-
| `readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout?)` | FC 43/14 |
|
|
232
|
-
| `sendCustomFC(unit, fc, data, timeout?)` | 发送用户自定义功能码 |
|
|
233
|
-
| `addCustomFunctionCode(cfc)` | 注册自定义功能码用于帧解析 |
|
|
234
|
-
| `removeCustomFunctionCode(fc)` | 注销自定义功能码 |
|
|
235
|
-
|
|
236
|
-
广播(`unit = 0`)resolve `void` — 从站不响应。
|
|
237
|
-
|
|
238
|
-
## 从站 API
|
|
258
|
+
在主站或从站上安装 `AccessAuthorizer`,可在三道闸门上执行策略:
|
|
259
|
+
|
|
260
|
+
- `checkUnit` —— 授权目标单元地址。
|
|
261
|
+
- `checkAddress` —— 授权请求触及的地址区间。
|
|
262
|
+
- `checkRuntime` —— 在真正发起写 I/O 前的最后机会检查。
|
|
263
|
+
|
|
264
|
+
每个钩子可返回 `true`、`false` 或数字形式的 Modbus 异常 `ErrorCode`。在从站上,被拒绝的请求会触发 `accessAudit` 事件。
|
|
239
265
|
|
|
240
266
|
```typescript
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
})
|
|
267
|
+
slave.setAccessAuthorizer({
|
|
268
|
+
checkUnit: (unit) => unit === 1,
|
|
269
|
+
checkAddress: (_unit, table, [start, end]) =>
|
|
270
|
+
table === 'holdingRegisters' && start >= 0 && end < 100,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
slave.on('accessAudit', (event) => {
|
|
274
|
+
console.log('access denied:', event.type, event.message);
|
|
275
|
+
});
|
|
246
276
|
```
|
|
247
277
|
|
|
248
|
-
###
|
|
278
|
+
### 自定义功能码
|
|
249
279
|
|
|
250
|
-
|
|
251
|
-
slave.add({
|
|
252
|
-
unit: 1,
|
|
280
|
+
在主站或从站上注册非标准功能码。帧层会学习请求形状,应用层则拿到原始 PDU 进行解析与响应。
|
|
253
281
|
|
|
254
|
-
|
|
255
|
-
|
|
282
|
+
```typescript
|
|
283
|
+
slave.addCustomFunctionCode(
|
|
284
|
+
{ fc: 0x65 },
|
|
285
|
+
(unit, fc, data, callback) => {
|
|
286
|
+
// 构造响应 PDU 字节
|
|
287
|
+
callback(null, () => Buffer.from([0x00]));
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
```
|
|
256
291
|
|
|
257
|
-
|
|
258
|
-
readDiscreteInputs?: (address, length) => boolean[] | Uint8Array,
|
|
259
|
-
readHoldingRegisters?: (address, length) => number[] | Uint16Array,
|
|
260
|
-
readInputRegisters?: (address, length) => number[] | Uint16Array,
|
|
292
|
+
对于 RTU(以及面向字节传输的 ASCII),描述符还需提供 `determineFrameLength`,以便帧状态机在不缓冲的情况下确定帧长度。
|
|
261
293
|
|
|
262
|
-
|
|
263
|
-
writeMultipleCoils?: (address, values) => void,
|
|
294
|
+
---
|
|
264
295
|
|
|
265
|
-
|
|
266
|
-
writeMultipleRegisters?: (address, values) => void,
|
|
296
|
+
## 支持的功能码
|
|
267
297
|
|
|
268
|
-
|
|
298
|
+
| FC | 名称 | 主站 | 从站 |
|
|
299
|
+
| --: | --- | :---: | :---: |
|
|
300
|
+
| 01 | Read Coils | ✅ | ✅ |
|
|
301
|
+
| 02 | Read Discrete Inputs | ✅ | ✅ |
|
|
302
|
+
| 03 | Read Holding Registers | ✅ | ✅ |
|
|
303
|
+
| 04 | Read Input Registers | ✅ | ✅ |
|
|
304
|
+
| 05 | Write Single Coil | ✅ | ✅ |
|
|
305
|
+
| 06 | Write Single Register | ✅ | ✅ |
|
|
306
|
+
| 08/00 | Return Query Data | ✅ | ✅ |
|
|
307
|
+
| 15 | Write Multiple Coils | ✅ | ✅ |
|
|
308
|
+
| 16 | Write Multiple Registers | ✅ | ✅ |
|
|
309
|
+
| 17 | Report Server ID | ✅ | ✅ |
|
|
310
|
+
| 22 | Mask Write Register | ✅ | ✅ |
|
|
311
|
+
| 23 | Read/Write Multiple Registers | ✅ | ✅ |
|
|
312
|
+
| 43/14 | Read Device Identification | ✅ | ✅ |
|
|
269
313
|
|
|
270
|
-
|
|
271
|
-
readDeviceIdentification?: () => { [index: number]: string },
|
|
314
|
+
---
|
|
272
315
|
|
|
273
|
-
|
|
274
|
-
getAddressRange?: () => ({
|
|
275
|
-
discreteInputs?: [number, number] | [number, number][];
|
|
276
|
-
coils?: [number, number] | [number, number][];
|
|
277
|
-
inputRegisters?: [number, number] | [number, number][];
|
|
278
|
-
holdingRegisters?: [number, number] | [number, number][];
|
|
279
|
-
}),
|
|
280
|
-
});
|
|
281
|
-
```
|
|
316
|
+
## 基准测试
|
|
282
317
|
|
|
283
|
-
|
|
318
|
+
以下全部数据由本仓库的基准测试套件在 **AMD Ryzen 7 9800X3D** 工作站上、**Node.js v24.16.0** 环境下测得。完整报告、测试方法与复现说明见 [`benchmark/report_presentation.md`](benchmark/report_presentation.md)。
|
|
284
319
|
|
|
285
|
-
|
|
286
|
-
|--------|-------------|
|
|
287
|
-
| `add(model)` | 注册从站模型 |
|
|
288
|
-
| `remove(unit)` | 移除从站模型 |
|
|
289
|
-
| `open(...args)` | 打开物理层。一次性:close() 后不可重新打开。 |
|
|
290
|
-
| `close()` | 立即关闭 |
|
|
291
|
-
| `addCustomFunctionCode(cfc)` | 注册自定义功能码 |
|
|
292
|
-
| `removeCustomFunctionCode(fc)` | 注销自定义功能码 |
|
|
320
|
+
### 编解码微基准
|
|
293
321
|
|
|
294
|
-
|
|
322
|
+
纯 CPU 编解码测试,无网络 I/O。每次操作都在亚微秒级别完成,因此 `Ops/sec` 和 `CPU (µs/op)` 是可靠指标;此量级下的单次时延主要由 `process.hrtime` 开销主导,故省略。
|
|
295
323
|
|
|
296
|
-
|
|
324
|
+
| 测试项 | Ops/sec | CPU (µs/op) |
|
|
325
|
+
| --- | ---: | ---: |
|
|
326
|
+
| TCP 请求编码 | 9.37 M | 0.11 |
|
|
327
|
+
| TCP 响应编码 | 7.83 M | 0.13 |
|
|
328
|
+
| TCP 请求解码 | 8.83 M | 0.11 |
|
|
329
|
+
| TCP 响应解码 | 8.90 M | 0.11 |
|
|
330
|
+
| RTU 请求编码 | 9.12 M | 0.11 |
|
|
331
|
+
| RTU 响应编码 | 1.91 M | 0.53 |
|
|
332
|
+
| RTU 请求解码 | 8.53 M | 0.12 |
|
|
333
|
+
| RTU 响应解码 | 1.98 M | 0.51 |
|
|
334
|
+
| ASCII 请求编码 | 8.83 M | 0.11 |
|
|
335
|
+
| ASCII 响应编码 | 2.44 M | 0.42 |
|
|
336
|
+
| ASCII 请求解码 | 7.84 M | 0.13 |
|
|
337
|
+
| ASCII 响应解码 | 2.53 M | 0.4 |
|
|
297
338
|
|
|
298
|
-
|
|
299
|
-
import type { CustomFunctionCode } from 'njs-modbus';
|
|
339
|
+
所有编解码测试项的 `GC (ns/op)` 均为零,因为稳态运行期间解码路径不在 JavaScript 堆上分配内存。
|
|
300
340
|
|
|
301
|
-
|
|
302
|
-
fc: 0x50,
|
|
303
|
-
predictRequestLength: (buffer, start, end) => {
|
|
304
|
-
if (end - start < 2) return null;
|
|
305
|
-
return 4 + buffer[start + 1];
|
|
306
|
-
},
|
|
307
|
-
predictResponseLength: (buffer, start, end) => {
|
|
308
|
-
if (end - start < 2) return null;
|
|
309
|
-
return 4 + buffer[start + 1];
|
|
310
|
-
},
|
|
311
|
-
handle: async (data, unit) => {
|
|
312
|
-
return Buffer.from([data[1], data[0]]);
|
|
313
|
-
},
|
|
314
|
-
};
|
|
341
|
+
### 端到端传输吞吐
|
|
315
342
|
|
|
316
|
-
|
|
317
|
-
slave.addCustomFunctionCode(cfc);
|
|
343
|
+
FC 03(读取 50 个保持寄存器),TCP 走本地回环,串口通过 115200 波特率的 `socat` PTY 对进行测试。
|
|
318
344
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
345
|
+
| 传输层 | 协议栈 | Ops/sec | P50 (µs) | P99 (µs) |
|
|
346
|
+
| --- | --- | ---: | ---: | ---: |
|
|
347
|
+
| TCP 顺序 | **njs-modbus** | **94.81 k** | **8.7** | **43.9** |
|
|
348
|
+
| TCP 顺序 | jsmodbus | 59.59 k | 13.6 | 65.7 |
|
|
349
|
+
| TCP 顺序 | modbus-serial | 867 | 1,150.5 | 1,244.5 |
|
|
350
|
+
| TCP 8 连接 | **njs-modbus** | **109.23 k** | **60.7** | **179.8** |
|
|
351
|
+
| TCP 8 连接 | jsmodbus | 63.84 k | 102.6 | 299.7 |
|
|
352
|
+
| TCP 8 连接 | modbus-serial | 6.37 k | 1,241.3 | 1,437.6 |
|
|
353
|
+
| RTU 顺序 | **njs-modbus** | **104** | **514.5** | **852.8** |
|
|
354
|
+
| RTU 顺序 | jsmodbus | 104 | 546.4 | 927.3 |
|
|
355
|
+
| RTU 顺序 | modbus-serial | 31 | 31,903.6 | 32,219.3 |
|
|
356
|
+
| ASCII 顺序 | **njs-modbus** | **51** | **556.1** | **947.9** |
|
|
323
357
|
|
|
324
|
-
|
|
358
|
+
### 混沌弹性
|
|
325
359
|
|
|
326
|
-
|
|
327
|
-
import { ErrorCode, ModbusError, getErrorByCode } from 'njs-modbus';
|
|
328
|
-
|
|
329
|
-
// ErrorCode.ILLEGAL_FUNCTION = 0x01
|
|
330
|
-
// ErrorCode.ILLEGAL_DATA_ADDRESS = 0x02
|
|
331
|
-
// ErrorCode.ILLEGAL_DATA_VALUE = 0x03
|
|
332
|
-
// ErrorCode.SERVER_DEVICE_FAILURE = 0x04
|
|
333
|
-
// ErrorCode.ACKNOWLEDGE = 0x05
|
|
334
|
-
// ErrorCode.SERVER_DEVICE_BUSY = 0x06
|
|
335
|
-
// ErrorCode.MEMORY_PARITY_ERROR = 0x08
|
|
336
|
-
// ErrorCode.GATEWAY_PATH_UNAVAILABLE = 0x0a
|
|
337
|
-
// ErrorCode.GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0b
|
|
338
|
-
|
|
339
|
-
slave.add({
|
|
340
|
-
unit: 1,
|
|
341
|
-
readHoldingRegisters: (address) => {
|
|
342
|
-
if (address > 100) {
|
|
343
|
-
throw getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS);
|
|
344
|
-
}
|
|
345
|
-
return [42];
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
```
|
|
360
|
+
混沌套件向真实 Modbus 服务器注入损坏、分片、粘包及脏数据帧,验证有效帧能否在无泄漏的情况下被正确恢复。
|
|
349
361
|
|
|
350
|
-
|
|
362
|
+
| 协议 | 通过场景数 |
|
|
363
|
+
| --- | ---: |
|
|
364
|
+
| TCP | 12 / 12 |
|
|
365
|
+
| RTU | 12 / 12 |
|
|
366
|
+
| ASCII | 14 / 14 |
|
|
351
367
|
|
|
352
|
-
|
|
368
|
+
本地复现完整测试:
|
|
353
369
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
| TCP P99 延迟 | **53 µs** | 98 µs (1.83x) | 2,331 µs (43.7x) |
|
|
358
|
-
| 并发 (8 连接) | **75,810 ops/sec** | 41,754 (0.55x) | 5,620 (0.07x) |
|
|
359
|
-
| RTU 串口 (115200 波特率) | **44 ops/sec** | 44 ops/sec | 45 ops/sec |
|
|
360
|
-
| TCP 响应编码 | **6.57M ops/sec** | 1.22M (0.19x) | 1.18M (0.18x) |
|
|
361
|
-
| TCP 响应解码 | **6.78M ops/sec** | 1.94M (0.29x) | 0.67M (0.10x) |
|
|
362
|
-
| FC01 读取线圈 | **85,170 ops/sec** | 617 (0.01x) | 861 (0.01x) |
|
|
370
|
+
```bash
|
|
371
|
+
npm run benchmark:full
|
|
372
|
+
```
|
|
363
373
|
|
|
364
|
-
|
|
365
|
-
<summary>完整基准测试结果</summary>
|
|
374
|
+
---
|
|
366
375
|
|
|
367
|
-
|
|
376
|
+
## 安全与合规
|
|
368
377
|
|
|
369
|
-
|
|
378
|
+
`njs-modbus` 是一个纯 Modbus 协议栈。面向受监管环境,它通过 `AccessAuthorizer` 提供协议层策略执行点,并通过从站 `accessAudit` 事件提供可审计轨迹:
|
|
370
379
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
modbus-serial │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 786 ops/sec (0.01x) p99: 2,331 µs CPU: 8,176 µs/op
|
|
375
|
-
```
|
|
380
|
+
- `checkUnit` —— 授权目标单元地址。
|
|
381
|
+
- `checkAddress` —— 授权请求触及的地址区间。
|
|
382
|
+
- `checkRuntime` —— 在真正发起写 I/O 前的最后机会检查。
|
|
376
383
|
|
|
377
|
-
|
|
384
|
+
被拒绝的请求会触发结构化 `accessAudit` 事件,可转发至 SIEM 或审计日志。这有助于在不引入外部代理的情况下,满足工业控制(OT)安全与合规要求。
|
|
378
385
|
|
|
379
|
-
|
|
380
|
-
njs-modbus │ ██████████████████████████████ 75,810 ops/sec 🏆 p99: 258 µs CPU: 91 µs/op
|
|
381
|
-
jsmodbus │ █████████████████░░░░░░░░░░░░░ 41,754 ops/sec (0.55x) p99: 543 µs CPU: 167 µs/op
|
|
382
|
-
modbus-serial │ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 5,620 ops/sec (0.07x) p99: 3,180 µs CPU: 1,237 µs/op
|
|
383
|
-
```
|
|
386
|
+
`njs-modbus` 同时内置 **TLS 传输插件**(`TlsClientPhysicalLayer` / `TlsServerPhysicalLayer`),基于 `node:tls` 实现,因此在提供证书与 TLS 选项的前提下,可直接建立加密的 Modbus TCP 连接并支持双向 TLS。证书生命周期管理、网络身份、主机加固与物理安全仍由宿主应用和基础设施负责。
|
|
384
387
|
|
|
385
|
-
|
|
388
|
+
- [`SECURITY.md`](SECURITY.md) —— 漏洞报告、协调披露与安全更新策略。
|
|
389
|
+
- [`docs/security/`](https://github.com/xiejay97/njs-modbus/tree/main/docs/security) —— 访问控制、审计事件、TLS 使用、部署补偿控制与 SDL。
|
|
386
390
|
|
|
387
|
-
|
|
388
|
-
njs-modbus │ 44 ops/sec p99: 993 µs CPU: 729 µs/op GC: 2,613 ns/op
|
|
389
|
-
jsmodbus │ 44 ops/sec p99: 1,019 µs CPU: 694 µs/op GC: 2,609 ns/op
|
|
390
|
-
modbus-serial │ 45 ops/sec p99: 3,228 µs CPU: 765 µs/op GC: 3,864 ns/op
|
|
391
|
-
```
|
|
391
|
+
`examples/security/` 目录包含可运行的主站/从站示例,包括 TLS 与传输层安全选项。
|
|
392
392
|
|
|
393
|
-
|
|
393
|
+
---
|
|
394
394
|
|
|
395
|
-
|
|
396
|
-
tcpReqEncode: njs-modbus 6.90M ops/sec 🏆 jsmodbus 0.78x modbus-serial 0.19x
|
|
397
|
-
tcpResEncode: njs-modbus 6.57M ops/sec 🏆 jsmodbus 0.19x modbus-serial 0.18x
|
|
398
|
-
tcpReqDecode: njs-modbus 6.80M ops/sec 🏆 jsmodbus 0.81x modbus-serial 0.17x
|
|
399
|
-
tcpResDecode: njs-modbus 6.78M ops/sec 🏆 jsmodbus 0.29x modbus-serial 0.10x
|
|
400
|
-
rtuReqEncode: njs-modbus 7.26M ops/sec 🏆 jsmodbus 0.35x modbus-serial 0.76x
|
|
401
|
-
rtuReqDecode: njs-modbus 3.97M ops/sec jsmodbus 1.21x modbus-serial 1.78x 🏆
|
|
402
|
-
asciiReqEncode: njs-modbus 5.64M ops/sec 🏆 modbus-serial 0.57x
|
|
403
|
-
asciiReqDecode: njs-modbus 3.37M ops/sec 🏆 modbus-serial 0.34x
|
|
404
|
-
```
|
|
395
|
+
## 商业支持与许可
|
|
405
396
|
|
|
406
|
-
|
|
397
|
+
`njs-modbus` 采用 [Business Source License 1.1(BSL 1.1)](LICENSE) 发布。
|
|
407
398
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
| FC02 读取离散输入 | **85,570** 🏆 | 525 (0.01x) | 861 (0.01x) |
|
|
412
|
-
| FC03 读取保持寄存器 | **75,344** 🏆 | 40,081 (0.53x) | 852 (0.01x) |
|
|
413
|
-
| FC04 读取输入寄存器 | **75,757** 🏆 | 49,524 (0.65x) | 866 (0.01x) |
|
|
414
|
-
| FC05 写单个线圈 | **92,852** 🏆 | 53,521 (0.58x) | 871 (0.01x) |
|
|
415
|
-
| FC06 写单个寄存器 | **92,864** 🏆 | 53,684 (0.58x) | 872 (0.01x) |
|
|
416
|
-
| FC15 写多个线圈 | **86,434** 🏆 | 319 (0.00x) | 869 (0.01x) |
|
|
417
|
-
| FC16 写多个寄存器 | **76,053** 🏆 | 39,356 (0.52x) | 852 (0.01x) |
|
|
418
|
-
| FC17 报告服务器 ID | **74,697** 🏆 | — | — |
|
|
419
|
-
| FC22 掩码写寄存器 | **90,717** 🏆 | — | — |
|
|
420
|
-
| FC23 读/写多个寄存器 | **85,652** 🏆 | — | — |
|
|
421
|
-
| FC43 读取设备标识 | **78,030** 🏆 | — | 865 (0.01x) |
|
|
399
|
+
- **免费生产使用**:授予个人、教育机构、非营利组织以及年营收低于 100 万美元的企业。
|
|
400
|
+
- **Change Date**:2029-06-24。在该日期,本版本将转为 Apache License, Version 2.0。
|
|
401
|
+
- **商业许可**:面向 OEM、系统集成商以及需要可预测许可路径、有保障支持或无法满足 BSL 免费使用条件的商业产品,我们提供独立的专有商业许可。
|
|
422
402
|
|
|
423
|
-
|
|
403
|
+
商业许可可解除 BSL 对您产品的限制,同时我们的支持服务帮助您安心交付:
|
|
424
404
|
|
|
425
|
-
|
|
405
|
+
- **产品集成许可** —— 在闭源商业产品中使用 `njs-modbus`,无需承担 copyleft 义务。
|
|
406
|
+
- **专业技术支持** —— 故障排查、性能调优、迁移指导与升级规划。
|
|
407
|
+
- **企业级支持方案** —— 响应 SLA、长期维护版本、优先 Bug 修复与定制开发。
|
|
426
408
|
|
|
427
|
-
|
|
409
|
+
具体授权条款、报价及支持方案请联系:
|
|
428
410
|
|
|
429
|
-
[
|
|
411
|
+
- 邮箱:[xiejay97@gmail.com](mailto:xiejay97@gmail.com)
|
|
412
|
+
- GitHub Issues:[https://github.com/xiejay97/njs-modbus/issues](https://github.com/xiejay97/njs-modbus/issues)
|