njs-modbus 3.4.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 +303 -367
- package/README.zh-CN.md +303 -367
- package/dist/index.cjs +7206 -4301
- package/dist/index.d.cts +2862 -0
- package/dist/index.d.mts +2862 -0
- package/dist/index.mjs +7162 -4286
- package/package.json +66 -62
- package/dist/index.d.ts +0 -867
- package/dist/utils.cjs +0 -564
- package/dist/utils.d.ts +0 -164
- package/dist/utils.mjs +0 -547
package/README.zh-CN.md
CHANGED
|
@@ -1,476 +1,412 @@
|
|
|
1
1
|
# njs-modbus
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://modbus.org/)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
[English](README.md) | **中文**
|
|
6
9
|
|
|
7
|
-
|
|
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 -->
|
|
10
|
+
> 面向 Node.js 的生产级、热路径零 GC Modbus 协议栈 —— 支持 TCP、RTU、ASCII,可运行在 TCP、UDP、TLS、串口或任何可被建模为字节管道的自定义传输层之上。
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
`njs-modbus` 使用严格 TypeScript 编写,目标运行时为 Node.js `>=18.19`。其设计充分考虑了工业现场常见的 GC 抖动、静默帧损坏和非受控总线访问等实际约束:确定性时延、流式帧恢复、可编程访问控制与审计日志均内置于核心。
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
- **传输** — 串口、TCP 客户端/服务端、UDP 客户端/服务端,支持可插拔自定义传输层
|
|
18
|
-
- **性能优先** — 并行 FIFO 队列、惰性删除定时器堆、内联大端读写、零分配热路径
|
|
19
|
-
- **主站** — FIFO 或流水线并发请求(仅 TCP)
|
|
20
|
-
- **从站** — 每连接 FIFO 或并发处理(仅 TCP)
|
|
21
|
-
- **自定义功能码**,支持可插拔帧解析
|
|
22
|
-
- **广播**(`unit = 0`)
|
|
23
|
-
- **完整 TypeScript**
|
|
24
|
-
- **零运行时依赖**(仅在使用串口时 peer-depends `serialport`)
|
|
14
|
+
采用 **Business Source License 1.1(BSL 1.1)** 许可。个人、教育机构、非营利组织以及年营收低于 100 万美元的企业,可免费用于开发、测试及生产环境。更大规模组织可购买专有商业许可。每个版本在其 Change Date 之后自动转为 **Apache-2.0** 许可。
|
|
25
15
|
|
|
26
|
-
|
|
16
|
+
---
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|------|------|
|
|
30
|
-
| 01 | 读取线圈 |
|
|
31
|
-
| 02 | 读取离散输入 |
|
|
32
|
-
| 03 | 读取保持寄存器 |
|
|
33
|
-
| 04 | 读取输入寄存器 |
|
|
34
|
-
| 05 | 写单个线圈 |
|
|
35
|
-
| 06 | 写单个寄存器 |
|
|
36
|
-
| 15 | 写多个线圈 |
|
|
37
|
-
| 16 | 写多个寄存器 |
|
|
38
|
-
| 17 | 报告服务器 ID |
|
|
39
|
-
| 22 | 掩码写寄存器 |
|
|
40
|
-
| 23 | 读/写多个寄存器 |
|
|
41
|
-
| 43/14 | 读取设备标识 |
|
|
18
|
+
## 目录
|
|
42
19
|
|
|
43
|
-
|
|
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
|
+
- [商业支持与许可](#商业支持与许可)
|
|
44
34
|
|
|
45
|
-
|
|
35
|
+
---
|
|
46
36
|
|
|
47
|
-
|
|
48
|
-
npm install njs-modbus
|
|
49
|
-
```
|
|
37
|
+
## njs-modbus 是什么?
|
|
50
38
|
|
|
51
|
-
|
|
39
|
+
`njs-modbus` 是一个面向 Node.js 的分层 Modbus 协议库。协议层只与 Buffer 打交道,不感知任何底层物理设备。这意味着同一套主站/从站逻辑可以跑在 TCP、UDP、TLS、串口、WebSocket、内存 mock 或任何其他传输层之上 —— 你只需在 `AbstractPipelineAdapter` 接口后实现一次传输层,即可无需改动地复用完整协议栈。
|
|
52
40
|
|
|
53
|
-
```
|
|
54
|
-
|
|
41
|
+
```
|
|
42
|
+
┌─────────────────────────────────────────────┐
|
|
43
|
+
│ 应用层:ModbusMaster / ModbusSlave │
|
|
44
|
+
├─────────────────────────────────────────────┤
|
|
45
|
+
│ 协议帧层:TCP / RTU / ASCII │
|
|
46
|
+
├─────────────────────────────────────────────┤
|
|
47
|
+
│ 管道层:AbstractPipelineAdapter │
|
|
48
|
+
├─────────────────────────────────────────────┤
|
|
49
|
+
│ 物理传输层:TCP / UDP / TLS / │
|
|
50
|
+
│ 串口 / WebSocket / 自定义 │
|
|
51
|
+
└─────────────────────────────────────────────┘
|
|
55
52
|
```
|
|
56
53
|
|
|
57
|
-
|
|
54
|
+
- **严格 TypeScript** —— 协议类型字面量(`'TCP' | 'RTU' | 'ASCII'`)与类型化 Promise API,可在编译期发现大多数集成错误。
|
|
55
|
+
- **热路径零 GC 解码** —— 显式有限状态机帧同步,在稳态运行期间不在 JavaScript 堆上分配对象或 Buffer。
|
|
56
|
+
- **传输层无关** —— 一个适配器接口,任意物理链路。
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
---
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
import { ModbusMaster } from 'njs-modbus';
|
|
60
|
+
## 为什么选择 njs-modbus?
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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。 |
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
---
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
console.log(res.data);
|
|
73
|
+
## 功能矩阵
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
| 能力 | TCP | RTU | ASCII |
|
|
76
|
+
| --- | :---: | :---: | :---: |
|
|
77
|
+
| 主站 / 客户端 | ✅ | ✅ | ✅ |
|
|
78
|
+
| 从站 / 服务端 | ✅ | ✅ | ✅ |
|
|
79
|
+
| 并发流水线 | ✅ | — | — |
|
|
80
|
+
| 广播(`unit === 0`) | ✅ | ✅ | ✅ |
|
|
81
|
+
| 自定义功能码 | ✅ | ✅* | ✅ |
|
|
82
|
+
| 流式帧恢复 | ✅ | ✅ | ✅ |
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
\* RTU 自定义功能码需要提供 `determineFrameLength` 回调,以便帧状态机在不缓冲的情况下确定帧长度。
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
import { ModbusSlave } from 'njs-modbus';
|
|
86
|
+
由于协议层与传输层解耦,任意协议(TCP / RTU / ASCII)都可以运行在任意提供管道适配器的传输层之上。内置传输包括 TCP、UDP、TLS(基于 TCP)和串口;WebSocket 示例展示了一套自定义适配器。
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
physical: { type: 'TCP_SERVER' },
|
|
84
|
-
protocol: { type: 'TCP' },
|
|
85
|
-
});
|
|
88
|
+
---
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
unit: 1,
|
|
89
|
-
readHoldingRegisters: (address, length) => {
|
|
90
|
-
return Array.from({ length }, (_, i) => address + i);
|
|
91
|
-
},
|
|
92
|
-
});
|
|
90
|
+
## 安装
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
```bash
|
|
93
|
+
npm install njs-modbus
|
|
95
94
|
```
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
const master = new ModbusMaster({
|
|
101
|
-
physical: {
|
|
102
|
-
type: 'SERIAL',
|
|
103
|
-
opts: { path: '/dev/ttyUSB0', baudRate: 9600 },
|
|
104
|
-
},
|
|
105
|
-
protocol: { type: 'RTU' },
|
|
106
|
-
});
|
|
96
|
+
串口支持通过可选的对等依赖提供:
|
|
107
97
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
await master.close();
|
|
98
|
+
```bash
|
|
99
|
+
npm install serialport
|
|
111
100
|
```
|
|
112
101
|
|
|
113
|
-
|
|
102
|
+
需要 Node.js `>=18.19`。
|
|
114
103
|
|
|
115
|
-
|
|
116
|
-
const master = new ModbusMaster({
|
|
117
|
-
physical: { type: 'TCP_CLIENT' },
|
|
118
|
-
protocol: { type: 'RTU' },
|
|
119
|
-
});
|
|
120
|
-
```
|
|
104
|
+
---
|
|
121
105
|
|
|
122
|
-
##
|
|
106
|
+
## 快速开始
|
|
123
107
|
|
|
124
|
-
|
|
108
|
+
### TCP 主站
|
|
125
109
|
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
110
|
+
```typescript
|
|
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
|
+
}
|
|
132
|
+
});
|
|
144
133
|
|
|
145
|
-
|
|
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
|
+
});
|
|
140
|
+
```
|
|
146
141
|
|
|
147
|
-
|
|
142
|
+
### TCP 从站
|
|
148
143
|
|
|
149
|
-
|
|
144
|
+
```typescript
|
|
145
|
+
import { ModbusSlave, TcpServerPhysicalLayer } from 'njs-modbus';
|
|
150
146
|
|
|
151
|
-
|
|
152
|
-
|------|-----------------|------|
|
|
153
|
-
| `SERIAL` | `()` | 通过 `serialport` 包访问串口 |
|
|
154
|
-
| `TCP_CLIENT` | `SocketConnectOpts` | TCP 客户端套接字 |
|
|
155
|
-
| `TCP_SERVER` | `ListenOptions` | TCP 服务端 |
|
|
156
|
-
| `UDP_CLIENT` | `{ port?, address? }` | UDP 客户端 |
|
|
157
|
-
| `UDP_SERVER` | `BindOptions` | UDP 服务端 |
|
|
158
|
-
| `CUSTOM` | *(用户自定义)* | 用户提供的 `AbstractPhysicalLayer` 实例 |
|
|
147
|
+
const physical = new TcpServerPhysicalLayer();
|
|
159
148
|
|
|
160
|
-
|
|
149
|
+
physical.on('connect', (pipeline) => {
|
|
150
|
+
const slave = new ModbusSlave({
|
|
151
|
+
pipelineAdapter: pipeline,
|
|
152
|
+
protocol: { type: 'TCP' },
|
|
153
|
+
queueStrategy: 'drop-stale',
|
|
154
|
+
});
|
|
161
155
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
opts: {
|
|
167
|
-
whitelist: ['192.168.1.10', '10.0.0.5'], // IPv4-mapped IPv6 自动规范化
|
|
168
|
-
maxConnections: 10,
|
|
169
|
-
idleTimeout: 30000, // 驱逐不活跃连接,0 = 禁用
|
|
156
|
+
slave.addUnit(1, {
|
|
157
|
+
readHoldingRegisters: (address, length, callback) => {
|
|
158
|
+
const values = Array.from({ length }, (_, i) => (address + i) & 0xffff);
|
|
159
|
+
callback(null, values);
|
|
170
160
|
},
|
|
171
|
-
|
|
172
|
-
|
|
161
|
+
writeSingleRegister: (address, value, callback) => {
|
|
162
|
+
console.log(`write ${value} to ${address}`);
|
|
163
|
+
callback(null);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
173
166
|
});
|
|
174
|
-
```
|
|
175
167
|
|
|
176
|
-
|
|
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
|
+
});
|
|
175
|
+
```
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|------|------|------|
|
|
180
|
-
| `whitelist` | `string[]` | 允许的客户端 IP。`::ffff:` 前缀自动剥离。 |
|
|
181
|
-
| `maxConnections` | `number` | 最大并发连接数。超出后新连接静默丢弃。 |
|
|
182
|
-
| `idleTimeout` | `number` | 空闲超时(毫秒),超时后驱逐连接。默认 `30000`,传 `0` 禁用。 |
|
|
183
|
-
| `socketOpts` / `serverOpts` | `object` | 透传给 Node.js `createSocket()` / `createServer()`。 |
|
|
177
|
+
### 串口 RTU
|
|
184
178
|
|
|
185
|
-
|
|
179
|
+
```typescript
|
|
180
|
+
import { ModbusMaster, SerialPhysicalLayer } from 'njs-modbus';
|
|
186
181
|
|
|
187
|
-
|
|
188
|
-
|------|------|------|
|
|
189
|
-
| `TCP` | 无 | Modbus TCP,带 MBAP 头 |
|
|
190
|
-
| `RTU` | `RtuProtocolOptions` | Modbus RTU,带 CRC16 |
|
|
191
|
-
| `ASCII` | `AsciiApplicationLayerOptions` | Modbus ASCII,带 LRC |
|
|
182
|
+
const physical = new SerialPhysicalLayer();
|
|
192
183
|
|
|
193
|
-
|
|
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
|
+
});
|
|
194
191
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
type: 'RTU',
|
|
198
|
-
opts: {
|
|
199
|
-
// t3.5 帧间静默时间。
|
|
200
|
-
// 既可以传裸数字(毫秒),也可以传 { unit: 'bit' | 'ms', value: N }。
|
|
201
|
-
// 传 0 显式禁用(适用于 RTU-over-TCP 等无丢包传输)。
|
|
202
|
-
intervalBetweenFrames: 20,
|
|
203
|
-
|
|
204
|
-
// t1.5 字符间超时(可选)。
|
|
205
|
-
interCharTimeout: { unit: 'bit', value: 10 },
|
|
206
|
-
|
|
207
|
-
// 丢弃存在 t1.5 时序违规的帧。
|
|
208
|
-
strictTiming: true,
|
|
209
|
-
},
|
|
210
|
-
}
|
|
211
|
-
```
|
|
192
|
+
const res = await master.readHoldingRegisters(1, 0, 10);
|
|
193
|
+
console.log('registers:', res.data);
|
|
212
194
|
|
|
213
|
-
|
|
195
|
+
master.destroy();
|
|
196
|
+
physical.close();
|
|
197
|
+
});
|
|
214
198
|
|
|
215
|
-
|
|
216
|
-
protocol: {
|
|
217
|
-
type: 'ASCII',
|
|
218
|
-
opts: {
|
|
219
|
-
lenientHex: true, // 接受小写十六进制 (a-f)。默认:false(按规范仅大写)
|
|
220
|
-
},
|
|
221
|
-
}
|
|
199
|
+
physical.open({ path: '/dev/ttyUSB0', baudRate: 115200 });
|
|
222
200
|
```
|
|
223
201
|
|
|
224
|
-
|
|
202
|
+
[`examples/`](https://github.com/xiejay97/njs-modbus/tree/main/examples) 目录包含可运行的主站/从站示例,涵盖访问控制、审计日志、TLS 以及 WebSocket 等自定义传输层。
|
|
225
203
|
|
|
226
|
-
|
|
204
|
+
---
|
|
227
205
|
|
|
228
|
-
|
|
229
|
-
new ModbusMaster({
|
|
230
|
-
physical: { type: 'TCP_CLIENT' },
|
|
231
|
-
protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
|
|
232
|
-
timeout?: 1000, // 单次请求超时(毫秒)
|
|
233
|
-
concurrent?: false, // 流水线模式(仅 TCP)
|
|
234
|
-
})
|
|
235
|
-
```
|
|
206
|
+
## 架构
|
|
236
207
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
| 方法 | 说明 |
|
|
240
|
-
|------|------|
|
|
241
|
-
| `open(...args)` | 打开物理层。一次性:close() 后不可重新打开。 |
|
|
242
|
-
| `close()` | 立即关闭。进行中和队列中的请求会被 reject。 |
|
|
243
|
-
| `readCoils(unit, address, length, timeout?)` | FC 01 |
|
|
244
|
-
| `readDiscreteInputs(unit, address, length, timeout?)` | FC 02 |
|
|
245
|
-
| `readHoldingRegisters(unit, address, length, timeout?)` | FC 03 |
|
|
246
|
-
| `readInputRegisters(unit, address, length, timeout?)` | FC 04 |
|
|
247
|
-
| `writeSingleCoil(unit, address, value, timeout?)` | FC 05 |
|
|
248
|
-
| `writeSingleRegister(unit, address, value, timeout?)` | FC 06 |
|
|
249
|
-
| `writeMultipleCoils(unit, address, values, timeout?)` | FC 15 |
|
|
250
|
-
| `writeMultipleRegisters(unit, address, values, timeout?)` | FC 16 |
|
|
251
|
-
| `reportServerId(unit, serverIdLength?, timeout?)` | FC 17 |
|
|
252
|
-
| `maskWriteRegister(unit, address, andMask, orMask, timeout?)` | FC 22 |
|
|
253
|
-
| `readAndWriteMultipleRegisters(unit, read, write, timeout?)` | FC 23 |
|
|
254
|
-
| `readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout?)` | FC 43/14 |
|
|
255
|
-
| `sendCustomFC(unit, fc, data, timeout?)` | 发送用户自定义功能码 |
|
|
256
|
-
| `addCustomFunctionCode(cfc)` | 注册自定义功能码用于帧解析 |
|
|
257
|
-
| `removeCustomFunctionCode(fc)` | 注销自定义功能码 |
|
|
258
|
-
|
|
259
|
-
广播(`unit = 0`)resolve `void` — 从站不响应。
|
|
260
|
-
|
|
261
|
-
## 从站 API
|
|
262
|
-
|
|
263
|
-
### 构造函数
|
|
208
|
+
库体被组织为四层。每层相互独立、可单独测试、可替换。
|
|
264
209
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
```
|
|
210
|
+
| 层级 | 职责 | 公共契约 |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| **物理层** | 打开/关闭链路,并为每个连接抛出一个管道实例。 | `AbstractPhysicalLayer` |
|
|
213
|
+
| **管道层** | 搬运原始字节、处理背压,暴露 `write(data)` + `data` 事件接口。 | `AbstractPipelineAdapter` / `AbstractPipelineLayer` |
|
|
214
|
+
| **协议层** | 解析帧、校验 CRC/LRC/MBAP,并抛出完整 ADU。 | `TcpProtocolLayer` / `RtuProtocolLayer` / `AsciiProtocolLayer` |
|
|
215
|
+
| **应用层** | 编排事务、队列、访问控制,并对外暴露 Promise API。 | `ModbusMaster` / `ModbusSlave` |
|
|
272
216
|
|
|
273
|
-
|
|
217
|
+
这种分层让自定义传输层变得极为简单。[`examples/websocket/`](https://github.com/xiejay97/njs-modbus/tree/main/examples/websocket) 中的 WebSocket 示例在 150 行内实现了一套完整管道层。
|
|
274
218
|
|
|
275
|
-
|
|
276
|
-
slave.add({
|
|
277
|
-
unit: 1,
|
|
219
|
+
---
|
|
278
220
|
|
|
279
|
-
|
|
280
|
-
interceptor?: (fc, data) => Buffer | undefined,
|
|
221
|
+
## 核心能力
|
|
281
222
|
|
|
282
|
-
|
|
283
|
-
readDiscreteInputs?: (address, length) => Uint8Array,
|
|
284
|
-
readHoldingRegisters?: (address, length) => number[] | Uint16Array,
|
|
285
|
-
readInputRegisters?: (address, length) => number[] | Uint16Array,
|
|
223
|
+
### 低分配编解码热路径
|
|
286
224
|
|
|
287
|
-
|
|
288
|
-
writeMultipleCoils?: (address, values) => void,
|
|
225
|
+
TCP、RTU、ASCII 的协议帧层均以显式有限状态机实现。解码路径在稳态运行期间不在 JavaScript 堆上分配对象或 Buffer(通过预分配残量缓冲与零拷贝视图实现),从而彻底消除热路径上的 GC 抖动断点。编码路径每帧执行一次有界的 `Buffer.allocUnsafe()` 分配。
|
|
289
226
|
|
|
290
|
-
|
|
291
|
-
writeMultipleRegisters?: (address, values) => void,
|
|
227
|
+
### 流式帧恢复
|
|
292
228
|
|
|
293
|
-
|
|
229
|
+
帧层以流式方式解析输入字节,可在以下场景中自愈:
|
|
294
230
|
|
|
295
|
-
|
|
296
|
-
|
|
231
|
+
- 线路上注入的脏数据。
|
|
232
|
+
- 一次读取中多个有效帧粘连(`sticky`)。
|
|
233
|
+
- 残帧后接有效帧。
|
|
234
|
+
- 一个帧被拆分到多次读取中(跨边界分片)。
|
|
235
|
+
- CRC(RTU)或 LRC(ASCII)损坏。
|
|
297
236
|
|
|
298
|
-
|
|
299
|
-
getAddressRange?: () => ({
|
|
300
|
-
discreteInputs?: [number, number] | [number, number][];
|
|
301
|
-
coils?: [number, number] | [number, number][];
|
|
302
|
-
inputRegisters?: [number, number] | [number, number][];
|
|
303
|
-
holdingRegisters?: [number, number] | [number, number][];
|
|
304
|
-
}),
|
|
305
|
-
});
|
|
306
|
-
```
|
|
237
|
+
有效帧会被正常抛出,无效数据被丢弃,不会污染相邻帧。
|
|
307
238
|
|
|
308
|
-
|
|
239
|
+
### 队列策略
|
|
309
240
|
|
|
310
|
-
|
|
241
|
+
`ModbusMaster` 与 `ModbusSlave` 均支持四种队列策略:
|
|
311
242
|
|
|
312
|
-
|
|
|
313
|
-
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `addCustomFunctionCode(cfc)` | 注册自定义功能码 |
|
|
319
|
-
| `removeCustomFunctionCode(fc)` | 注销自定义功能码 |
|
|
243
|
+
| 策略 | 行为 | 适用场景 |
|
|
244
|
+
| --- | --- | --- |
|
|
245
|
+
| `fifo` | 严格先进先出执行。 | 串口线路、确定性顺序。 |
|
|
246
|
+
| `drop-stale` | 新请求到达时清空所有未执行的旧请求。 | 只需最新值的遥测采集。 |
|
|
247
|
+
| `deduplicate` | 相同 ADU 指纹的待处理请求会被去重丢弃。 | 可能重叠的轮询循环。 |
|
|
248
|
+
| `concurrent` | 请求并发派发。 | Modbus TCP 或多链路主站/从站。 |
|
|
320
249
|
|
|
321
|
-
|
|
250
|
+
默认策略为 `drop-stale`。
|
|
322
251
|
|
|
323
|
-
|
|
252
|
+
### 每 unit 写范围锁
|
|
324
253
|
|
|
325
|
-
|
|
254
|
+
对于 `concurrent` 模式下的从站,`enableWriteRangeLock`(默认 `true`)确保同一 unit 上地址范围重叠的写请求(FC05/06/15/16/22/23)被序列化,防止竞态条件。这对于在多个连接同时修改共享寄存器或线圈时保持一致性至关重要。仅对于不需要协调开销的纯同步内存从站,才设置为 `false`。
|
|
326
255
|
|
|
327
|
-
|
|
328
|
-
|------|------|------|
|
|
329
|
-
| `open` | `()` | 物理层就绪,可接受连接 |
|
|
330
|
-
| `connect` | `(connection)` | 新连接已建立 |
|
|
331
|
-
| `close` | `()` | 物理层已关闭 |
|
|
332
|
-
| `error` | `(error)` | 物理层错误 |
|
|
256
|
+
### 访问控制与审计
|
|
333
257
|
|
|
334
|
-
|
|
258
|
+
在主站或从站上安装 `AccessAuthorizer`,可在三道闸门上执行策略:
|
|
335
259
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
| `rx` | `(buffer, connection)` | 从链路接收到原始数据 |
|
|
260
|
+
- `checkUnit` —— 授权目标单元地址。
|
|
261
|
+
- `checkAddress` —— 授权请求触及的地址区间。
|
|
262
|
+
- `checkRuntime` —— 在真正发起写 I/O 前的最后机会检查。
|
|
340
263
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
| 事件 | 参数 | 说明 |
|
|
344
|
-
|------|------|------|
|
|
345
|
-
| `framing` | `(frame, connection)` | 完整帧已解码 |
|
|
346
|
-
| `framingError` | `(error, connection)` | 帧解析错误(CRC/LRC 失败、MBAP 格式错误、时序违规等) |
|
|
264
|
+
每个钩子可返回 `true`、`false` 或数字形式的 Modbus 异常 `ErrorCode`。在从站上,被拒绝的请求会触发 `accessAudit` 事件。
|
|
347
265
|
|
|
348
266
|
```typescript
|
|
349
|
-
|
|
350
|
-
|
|
267
|
+
slave.setAccessAuthorizer({
|
|
268
|
+
checkUnit: (unit) => unit === 1,
|
|
269
|
+
checkAddress: (_unit, table, [start, end]) =>
|
|
270
|
+
table === 'holdingRegisters' && start >= 0 && end < 100,
|
|
351
271
|
});
|
|
352
272
|
|
|
353
|
-
|
|
354
|
-
console.log('
|
|
273
|
+
slave.on('accessAudit', (event) => {
|
|
274
|
+
console.log('access denied:', event.type, event.message);
|
|
355
275
|
});
|
|
356
276
|
```
|
|
357
277
|
|
|
358
|
-
|
|
278
|
+
### 自定义功能码
|
|
359
279
|
|
|
360
|
-
|
|
280
|
+
在主站或从站上注册非标准功能码。帧层会学习请求形状,应用层则拿到原始 PDU 进行解析与响应。
|
|
361
281
|
|
|
362
282
|
```typescript
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (length < 2) return 0; // 需要更多字节
|
|
369
|
-
return 4 + getByte(1);
|
|
283
|
+
slave.addCustomFunctionCode(
|
|
284
|
+
{ fc: 0x65 },
|
|
285
|
+
(unit, fc, data, callback) => {
|
|
286
|
+
// 构造响应 PDU 字节
|
|
287
|
+
callback(null, () => Buffer.from([0x00]));
|
|
370
288
|
},
|
|
371
|
-
|
|
372
|
-
if (length < 2) return 0;
|
|
373
|
-
return 4 + getByte(1);
|
|
374
|
-
},
|
|
375
|
-
handle: async (data, unit) => {
|
|
376
|
-
return Buffer.from([data[1], data[0]]);
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
// 从站:帧解析 + 分发
|
|
381
|
-
slave.addCustomFunctionCode(cfc);
|
|
382
|
-
|
|
383
|
-
// 主站:仅帧解析
|
|
384
|
-
master.addCustomFunctionCode(cfc);
|
|
385
|
-
const response = await master.sendCustomFC(1, 0x50, [0xab, 0xcd]);
|
|
289
|
+
);
|
|
386
290
|
```
|
|
387
291
|
|
|
388
|
-
|
|
292
|
+
对于 RTU(以及面向字节传输的 ASCII),描述符还需提供 `determineFrameLength`,以便帧状态机在不缓冲的情况下确定帧长度。
|
|
389
293
|
|
|
390
|
-
|
|
391
|
-
import { ErrorCode, ModbusError, getErrorByCode } from 'njs-modbus';
|
|
392
|
-
|
|
393
|
-
// ErrorCode.ILLEGAL_FUNCTION = 0x01
|
|
394
|
-
// ErrorCode.ILLEGAL_DATA_ADDRESS = 0x02
|
|
395
|
-
// ErrorCode.ILLEGAL_DATA_VALUE = 0x03
|
|
396
|
-
// ErrorCode.SERVER_DEVICE_FAILURE = 0x04
|
|
397
|
-
// ErrorCode.ACKNOWLEDGE = 0x05
|
|
398
|
-
// ErrorCode.SERVER_DEVICE_BUSY = 0x06
|
|
399
|
-
// ErrorCode.MEMORY_PARITY_ERROR = 0x08
|
|
400
|
-
// ErrorCode.GATEWAY_PATH_UNAVAILABLE = 0x0a
|
|
401
|
-
// ErrorCode.GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0b
|
|
402
|
-
|
|
403
|
-
slave.add({
|
|
404
|
-
unit: 1,
|
|
405
|
-
readHoldingRegisters: (address) => {
|
|
406
|
-
if (address > 100) {
|
|
407
|
-
throw getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS);
|
|
408
|
-
}
|
|
409
|
-
return [42];
|
|
410
|
-
},
|
|
411
|
-
});
|
|
412
|
-
```
|
|
294
|
+
---
|
|
413
295
|
|
|
414
|
-
##
|
|
296
|
+
## 支持的功能码
|
|
415
297
|
|
|
416
|
-
|
|
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 | ✅ | ✅ |
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 基准测试
|
|
317
|
+
|
|
318
|
+
以下全部数据由本仓库的基准测试套件在 **AMD Ryzen 7 9800X3D** 工作站上、**Node.js v24.16.0** 环境下测得。完整报告、测试方法与复现说明见 [`benchmark/report_presentation.md`](benchmark/report_presentation.md)。
|
|
319
|
+
|
|
320
|
+
### 编解码微基准
|
|
321
|
+
|
|
322
|
+
纯 CPU 编解码测试,无网络 I/O。每次操作都在亚微秒级别完成,因此 `Ops/sec` 和 `CPU (µs/op)` 是可靠指标;此量级下的单次时延主要由 `process.hrtime` 开销主导,故省略。
|
|
323
|
+
|
|
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 |
|
|
338
|
+
|
|
339
|
+
所有编解码测试项的 `GC (ns/op)` 均为零,因为稳态运行期间解码路径不在 JavaScript 堆上分配内存。
|
|
340
|
+
|
|
341
|
+
### 端到端传输吞吐
|
|
342
|
+
|
|
343
|
+
FC 03(读取 50 个保持寄存器),TCP 走本地回环,串口通过 115200 波特率的 `socat` PTY 对进行测试。
|
|
344
|
+
|
|
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** |
|
|
357
|
+
|
|
358
|
+
### 混沌弹性
|
|
359
|
+
|
|
360
|
+
混沌套件向真实 Modbus 服务器注入损坏、分片、粘包及脏数据帧,验证有效帧能否在无泄漏的情况下被正确恢复。
|
|
361
|
+
|
|
362
|
+
| 协议 | 通过场景数 |
|
|
363
|
+
| --- | ---: |
|
|
364
|
+
| TCP | 12 / 12 |
|
|
365
|
+
| RTU | 12 / 12 |
|
|
366
|
+
| ASCII | 14 / 14 |
|
|
367
|
+
|
|
368
|
+
本地复现完整测试:
|
|
417
369
|
|
|
418
|
-
```
|
|
419
|
-
|
|
370
|
+
```bash
|
|
371
|
+
npm run benchmark:full
|
|
372
|
+
```
|
|
420
373
|
|
|
421
|
-
|
|
422
|
-
class MyConnection extends AbstractPhysicalConnection { /* write / destroy, emit 'data' */ }
|
|
374
|
+
---
|
|
423
375
|
|
|
424
|
-
|
|
425
|
-
physical: { type: 'CUSTOM', layer: new MyPhysicalLayer() },
|
|
426
|
-
protocol: { type: 'TCP' },
|
|
427
|
-
});
|
|
428
|
-
```
|
|
376
|
+
## 安全与合规
|
|
429
377
|
|
|
430
|
-
|
|
378
|
+
`njs-modbus` 是一个纯 Modbus 协议栈。面向受监管环境,它通过 `AccessAuthorizer` 提供协议层策略执行点,并通过从站 `accessAudit` 事件提供可审计轨迹:
|
|
431
379
|
|
|
432
|
-
|
|
380
|
+
- `checkUnit` —— 授权目标单元地址。
|
|
381
|
+
- `checkAddress` —— 授权请求触及的地址区间。
|
|
382
|
+
- `checkRuntime` —— 在真正发起写 I/O 前的最后机会检查。
|
|
433
383
|
|
|
434
|
-
|
|
384
|
+
被拒绝的请求会触发结构化 `accessAudit` 事件,可转发至 SIEM 或审计日志。这有助于在不引入外部代理的情况下,满足工业控制(OT)安全与合规要求。
|
|
435
385
|
|
|
436
|
-
|
|
386
|
+
`njs-modbus` 同时内置 **TLS 传输插件**(`TlsClientPhysicalLayer` / `TlsServerPhysicalLayer`),基于 `node:tls` 实现,因此在提供证书与 TLS 选项的前提下,可直接建立加密的 Modbus TCP 连接并支持双向 TLS。证书生命周期管理、网络身份、主机加固与物理安全仍由宿主应用和基础设施负责。
|
|
437
387
|
|
|
438
|
-
|
|
388
|
+
- [`SECURITY.md`](SECURITY.md) —— 漏洞报告、协调披露与安全更新策略。
|
|
389
|
+
- [`docs/security/`](https://github.com/xiejay97/njs-modbus/tree/main/docs/security) —— 访问控制、审计事件、TLS 使用、部署补偿控制与 SDL。
|
|
439
390
|
|
|
440
|
-
|
|
391
|
+
`examples/security/` 目录包含可运行的主站/从站示例,包括 TLS 与传输层安全选项。
|
|
441
392
|
|
|
442
|
-
|
|
443
|
-
|------|-----------|----------|---------------|
|
|
444
|
-
| 单连接 | **74,964 ops/sec** 🏆 | 55,996 (0.75x) | 853 (0.01x) |
|
|
445
|
-
| 8 并发客户端 | **10,188 ops/sec** 🏆 | 5,918 (0.58x) | 728 (0.07x) |
|
|
446
|
-
| P99 延迟(单连接) | **17 µs** 🏆 | 15 µs (0.88x) | 1,206 µs (70.9x) |
|
|
393
|
+
---
|
|
447
394
|
|
|
448
|
-
|
|
395
|
+
## 商业支持与许可
|
|
449
396
|
|
|
450
|
-
|
|
451
|
-
|----------|-----------|----------|---------------|
|
|
452
|
-
| TCP 请求编码 | **9.61M ops/sec** 🏆 | 6.32M (0.66x) | 0.68M (0.07x) |
|
|
453
|
-
| TCP 响应编码 | **7.51M ops/sec** 🏆 | 1.18M (0.16x) | 0.44M (0.06x) |
|
|
454
|
-
| TCP 响应解码 | **8.55M ops/sec** 🏆 | 2.04M (0.24x) | 0.70M (0.08x) |
|
|
455
|
-
| RTU 请求编码 | **9.42M ops/sec** 🏆 | 3.03M (0.32x) | 6.57M (0.70x) |
|
|
456
|
-
| ASCII 请求编码 | **7.09M ops/sec** 🏆 | — | 4.11M (0.58x) |
|
|
457
|
-
| ASCII 请求解码 | **4.91M ops/sec** 🏆 | — | 1.19M (0.24x) |
|
|
397
|
+
`njs-modbus` 采用 [Business Source License 1.1(BSL 1.1)](LICENSE) 发布。
|
|
458
398
|
|
|
459
|
-
|
|
399
|
+
- **免费生产使用**:授予个人、教育机构、非营利组织以及年营收低于 100 万美元的企业。
|
|
400
|
+
- **Change Date**:2029-06-24。在该日期,本版本将转为 Apache License, Version 2.0。
|
|
401
|
+
- **商业许可**:面向 OEM、系统集成商以及需要可预测许可路径、有保障支持或无法满足 BSL 免费使用条件的商业产品,我们提供独立的专有商业许可。
|
|
460
402
|
|
|
461
|
-
|
|
462
|
-
|--------|-----------|----------|---------------|
|
|
463
|
-
| FC01 读取线圈 | **75,430** 🏆 | 488 (0.01x) | 859 (0.01x) |
|
|
464
|
-
| FC03 读取保持寄存器 | **82,028** 🏆 | 46,596 (0.57x) | 864 (0.01x) |
|
|
465
|
-
| FC06 写单个寄存器 | **86,110** 🏆 | 51,084 (0.59x) | 871 (0.01x) |
|
|
466
|
-
| FC15 写多个线圈 | **78,766** 🏆 | — | 867 (0.01x) |
|
|
467
|
-
| FC17 报告服务器 ID | **79,185** 🏆 | — | — |
|
|
468
|
-
| FC22 掩码写寄存器 | **73,060** 🏆 | — | — |
|
|
469
|
-
| FC23 读/写多个寄存器 | **68,429** 🏆 | — | — |
|
|
470
|
-
| FC43 读取设备标识 | **62,176** 🏆 | — | 868 (0.01x) |
|
|
403
|
+
商业许可可解除 BSL 对您产品的限制,同时我们的支持服务帮助您安心交付:
|
|
471
404
|
|
|
472
|
-
|
|
405
|
+
- **产品集成许可** —— 在闭源商业产品中使用 `njs-modbus`,无需承担 copyleft 义务。
|
|
406
|
+
- **专业技术支持** —— 故障排查、性能调优、迁移指导与升级规划。
|
|
407
|
+
- **企业级支持方案** —— 响应 SLA、长期维护版本、优先 Bug 修复与定制开发。
|
|
473
408
|
|
|
474
|
-
|
|
409
|
+
具体授权条款、报价及支持方案请联系:
|
|
475
410
|
|
|
476
|
-
[
|
|
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)
|