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/README.zh-CN.md CHANGED
@@ -1,476 +1,412 @@
1
1
  # njs-modbus
2
2
 
3
- [English](./README.md) | 简体中文
3
+ [![License](https://img.shields.io/badge/License-BSL%201.1-orange.svg)](LICENSE)
4
+ [![Node.js](https://img.shields.io/badge/Runtime-Node%20%3E%3D18.19-339933.svg?logo=nodedotjs)](https://nodejs.org/)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-3178C6.svg?logo=typescript)](https://www.typescriptlang.org/)
6
+ [![Modbus](https://img.shields.io/badge/Modbus-TCP%20%7C%20RTU%20%7C%20ASCII%20%7C%20TLS-555555.svg)](https://modbus.org/)
4
7
 
5
- Node.js 的纯 JavaScript Modbus 实现,针对高吞吐量和低 GC 压力优化。
8
+ [English](README.md) | **中文**
6
9
 
7
- <!-- prettier-ignore-start -->
8
- [![npm download](http://img.shields.io/npm/dw/njs-modbus.svg?style=flat-square)](http://www.npm-stats.com/~packages/njs-modbus)
9
- [![npm latest package](http://img.shields.io/npm/v/njs-modbus/latest.svg?style=flat-square)](https://www.npmjs.com/package/njs-modbus)
10
- [![npm bundle size](https://img.shields.io/bundlephobia/minzip/njs-modbus?style=flat-square)](https://bundlephobia.com/package/njs-modbus)
11
- [![CI](https://img.shields.io/github/actions/workflow/status/xiejay97/njs-modbus/ci.yml?branch=main&style=flat-square)](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
- - **协议** Modbus TCP、RTU、ASCII
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
- 需要 **Node.js ≥ 18.19**。
35
+ ---
46
36
 
47
- ```bash
48
- npm install njs-modbus
49
- ```
37
+ ## njs-modbus 是什么?
50
38
 
51
- 使用串口传输时,还需安装 peer dependency:
39
+ `njs-modbus` 是一个面向 Node.js 的分层 Modbus 协议库。协议层只与 Buffer 打交道,不感知任何底层物理设备。这意味着同一套主站/从站逻辑可以跑在 TCP、UDP、TLS、串口、WebSocket、内存 mock 或任何其他传输层之上 —— 你只需在 `AbstractPipelineAdapter` 接口后实现一次传输层,即可无需改动地复用完整协议栈。
52
40
 
53
- ```bash
54
- npm install serialport
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
- ### TCP 主站
58
+ ---
60
59
 
61
- ```typescript
62
- import { ModbusMaster } from 'njs-modbus';
60
+ ## 为什么选择 njs-modbus?
63
61
 
64
- const master = new ModbusMaster({
65
- physical: { type: 'TCP_CLIENT' },
66
- protocol: { type: 'TCP' },
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
- await master.open({ port: 502, host: '192.168.1.10' });
71
+ ---
70
72
 
71
- const res = await master.readHoldingRegisters(1, 0, 10);
72
- console.log(res.data);
73
+ ## 功能矩阵
73
74
 
74
- await master.close();
75
- ```
75
+ | 能力 | TCP | RTU | ASCII |
76
+ | --- | :---: | :---: | :---: |
77
+ | 主站 / 客户端 | ✅ | ✅ | ✅ |
78
+ | 从站 / 服务端 | ✅ | ✅ | ✅ |
79
+ | 并发流水线 | ✅ | — | — |
80
+ | 广播(`unit === 0`) | ✅ | ✅ | ✅ |
81
+ | 自定义功能码 | ✅ | ✅* | ✅ |
82
+ | 流式帧恢复 | ✅ | ✅ | ✅ |
76
83
 
77
- ### TCP 从站
84
+ \* RTU 自定义功能码需要提供 `determineFrameLength` 回调,以便帧状态机在不缓冲的情况下确定帧长度。
78
85
 
79
- ```typescript
80
- import { ModbusSlave } from 'njs-modbus';
86
+ 由于协议层与传输层解耦,任意协议(TCP / RTU / ASCII)都可以运行在任意提供管道适配器的传输层之上。内置传输包括 TCP、UDP、TLS(基于 TCP)和串口;WebSocket 示例展示了一套自定义适配器。
81
87
 
82
- const slave = new ModbusSlave({
83
- physical: { type: 'TCP_SERVER' },
84
- protocol: { type: 'TCP' },
85
- });
88
+ ---
86
89
 
87
- slave.add({
88
- unit: 1,
89
- readHoldingRegisters: (address, length) => {
90
- return Array.from({ length }, (_, i) => address + i);
91
- },
92
- });
90
+ ## 安装
93
91
 
94
- await slave.open({ port: 502 });
92
+ ```bash
93
+ npm install njs-modbus
95
94
  ```
96
95
 
97
- ### 串口 RTU 主站
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
- await master.open();
109
- const res = await master.readHoldingRegisters(1, 0, 10);
110
- await master.close();
98
+ ```bash
99
+ npm install serialport
111
100
  ```
112
101
 
113
- ### RTU over TCP
102
+ 需要 Node.js `>=18.19`。
114
103
 
115
- ```typescript
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
- │ ModbusMaster / ModbusSlave │ ← 用户 API,会话/队列管理
129
- ├─────────────────────────────────────────┤
130
- │ TcpApplicationLayer │ ← 协议帧解析(MBAP / CRC16 / LRC)
131
- │ RtuApplicationLayer │
132
- │ AsciiApplicationLayer │
133
- ├─────────────────────────────────────────┤
134
- │ AbstractPhysicalConnection │ ← 每连接 I/O
135
- ├─────────────────────────────────────────┤
136
- │ AbstractPhysicalLayer │ ← 资源管理(端口/套接字/服务端)
137
- │ ├── TcpClientPhysicalLayer │
138
- │ ├── TcpServerPhysicalLayer │
139
- │ ├── UdpClientPhysicalLayer │
140
- │ ├── UdpServerPhysicalLayer │
141
- │ └── SerialPhysicalLayer │
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
- 所有传输层都统一为面向连接的模型:物理层在就绪时触发 `'connect'` 事件并传入一个 `AbstractPhysicalConnection`,每个连接会独立创建一个应用层实例。
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
- 所有物理层暴露 `open()` / `close()`、`state` 属性,以及 [事件](#事件) 章节中列出的事件。
144
+ ```typescript
145
+ import { ModbusSlave, TcpServerPhysicalLayer } from 'njs-modbus';
150
146
 
151
- | 类型 | `open(...)` 参数 | 说明 |
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
- ### TCP 服务端配置
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
- ```typescript
163
- const slave = new ModbusSlave({
164
- physical: {
165
- type: 'TCP_SERVER',
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
- protocol: { type: 'TCP' },
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
- ### RTU 选项
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
- ```typescript
196
- protocol: {
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
- ### ASCII 选项
195
+ master.destroy();
196
+ physical.close();
197
+ });
214
198
 
215
- ```typescript
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
- ## 主站 API
202
+ [`examples/`](https://github.com/xiejay97/njs-modbus/tree/main/examples) 目录包含可运行的主站/从站示例,涵盖访问控制、审计日志、TLS 以及 WebSocket 等自定义传输层。
225
203
 
226
- ### 构造函数
204
+ ---
227
205
 
228
- ```typescript
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
- ```typescript
266
- new ModbusSlave({
267
- physical: { type: 'TCP_SERVER' },
268
- protocol: { type: 'TCP' },
269
- concurrent?: false, // 每连接并发(仅 TCP)
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
- ```typescript
276
- slave.add({
277
- unit: 1,
219
+ ---
278
220
 
279
- // 可选:在默认分发前拦截任意功能码
280
- interceptor?: (fc, data) => Buffer | undefined,
221
+ ## 核心能力
281
222
 
282
- readCoils?: (address, length) => Uint8Array,
283
- readDiscreteInputs?: (address, length) => Uint8Array,
284
- readHoldingRegisters?: (address, length) => number[] | Uint16Array,
285
- readInputRegisters?: (address, length) => number[] | Uint16Array,
223
+ ### 低分配编解码热路径
286
224
 
287
- writeSingleCoil?: (address, value) => void,
288
- writeMultipleCoils?: (address, values) => void,
225
+ TCP、RTU、ASCII 的协议帧层均以显式有限状态机实现。解码路径在稳态运行期间不在 JavaScript 堆上分配对象或 Buffer(通过预分配残量缓冲与零拷贝视图实现),从而彻底消除热路径上的 GC 抖动断点。编码路径每帧执行一次有界的 `Buffer.allocUnsafe()` 分配。
289
226
 
290
- writeSingleRegister?: (address, value) => void,
291
- writeMultipleRegisters?: (address, values) => void,
227
+ ### 流式帧恢复
292
228
 
293
- maskWriteRegister?: (address, andMask, orMask) => void,
229
+ 帧层以流式方式解析输入字节,可在以下场景中自愈:
294
230
 
295
- reportServerId?: () => ServerId,
296
- readDeviceIdentification?: () => { [index: number]: string },
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
- 缺失的处理器返回 `ILLEGAL_FUNCTION`。越界地址返回 `ILLEGAL_DATA_ADDRESS`。处理器抛出的异常变为 `SERVER_DEVICE_FAILURE`,除非错误是 `ModbusError`。
239
+ ### 队列策略
309
240
 
310
- ### 方法
241
+ `ModbusMaster` 与 `ModbusSlave` 均支持四种队列策略:
311
242
 
312
- | 方法 | 说明 |
313
- |------|------|
314
- | `add(model)` | 注册从站模型 |
315
- | `remove(unit)` | 移除模型 |
316
- | `open(...args)` | 打开并开始接受连接。一次性。 |
317
- | `close()` | 立即关闭 |
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
- `ModbusMaster` `ModbusSlave` 都是 `EventEmitter`,所有事件均带类型定义。
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
- | `tx` | `(buffer, connection)` | 原始数据已写入链路 |
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
- master.on('framing', (frame, connection) => {
350
- console.log('frame:', frame.unit, frame.fc, frame.transaction);
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
- master.on('framingError', (error, connection) => {
354
- console.log('framing error:', error.message);
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
- `predictRequestLength` / `predictResponseLength` 接收 `getByte` 访问器和当前可用字节数 `length`。请使用 `length` 做边界检查,不要直接访问缓冲区。
280
+ 在主站或从站上注册非标准功能码。帧层会学习请求形状,应用层则拿到原始 PDU 进行解析与响应。
361
281
 
362
282
  ```typescript
363
- import type { CustomFunctionCode } from 'njs-modbus';
364
-
365
- const cfc: CustomFunctionCode = {
366
- fc: 0x50,
367
- predictRequestLength: (getByte, length) => {
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
- predictResponseLength: (getByte, length) => {
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
- ```typescript
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
- 任何面向字节的传输层都可以通过实现 `AbstractPhysicalLayer` `AbstractPhysicalConnection` 来使用,然后传入 `{ type: 'CUSTOM', layer: yourLayer }`。
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
- ```typescript
419
- import { AbstractPhysicalLayer, AbstractPhysicalConnection } from 'njs-modbus';
370
+ ```bash
371
+ npm run benchmark:full
372
+ ```
420
373
 
421
- class MyPhysicalLayer extends AbstractPhysicalLayer { /* open / close */ }
422
- class MyConnection extends AbstractPhysicalConnection { /* write / destroy, emit 'data' */ }
374
+ ---
423
375
 
424
- const master = new ModbusMaster({
425
- physical: { type: 'CUSTOM', layer: new MyPhysicalLayer() },
426
- protocol: { type: 'TCP' },
427
- });
428
- ```
376
+ ## 安全与合规
429
377
 
430
- 查看 [`examples/websocket`](./examples/websocket) 获取完整的 **WebSocket 承载 Modbus TCP** 实现。
378
+ `njs-modbus` 是一个纯 Modbus 协议栈。面向受监管环境,它通过 `AccessAuthorizer` 提供协议层策略执行点,并通过从站 `accessAudit` 事件提供可审计轨迹:
431
379
 
432
- 查看 [`examples/bluetooth`](./examples/bluetooth) 获取基于 Nordic UART Service (NUS) 的 **BLE 承载 Modbus TCP** 实现。
380
+ - `checkUnit` —— 授权目标单元地址。
381
+ - `checkAddress` —— 授权请求触及的地址区间。
382
+ - `checkRuntime` —— 在真正发起写 I/O 前的最后机会检查。
433
383
 
434
- ## 性能
384
+ 被拒绝的请求会触发结构化 `accessAudit` 事件,可转发至 SIEM 或审计日志。这有助于在不引入外部代理的情况下,满足工业控制(OT)安全与合规要求。
435
385
 
436
- 与 [jsmodbus](https://github.com/Cloud-Automation/node-modbus) v4.0.10 [modbus-serial](https://github.com/yaacov/node-modbus-serial) v8.0.25 的对比。
386
+ `njs-modbus` 同时内置 **TLS 传输插件**(`TlsClientPhysicalLayer` / `TlsServerPhysicalLayer`),基于 `node:tls` 实现,因此在提供证书与 TLS 选项的前提下,可直接建立加密的 Modbus TCP 连接并支持双向 TLS。证书生命周期管理、网络身份、主机加固与物理安全仍由宿主应用和基础设施负责。
437
387
 
438
- 测试环境:AMD Ryzen 7 9800X3D 8-Core Processor · Node.js v24.16.0 · [完整报告](./benchmark/report_presentation.md)
388
+ - [`SECURITY.md`](SECURITY.md) —— 漏洞报告、协调披露与安全更新策略。
389
+ - [`docs/security/`](https://github.com/xiejay97/njs-modbus/tree/main/docs/security) —— 访问控制、审计事件、TLS 使用、部署补偿控制与 SDL。
439
390
 
440
- ### TCP 端到端
391
+ `examples/security/` 目录包含可运行的主站/从站示例,包括 TLS 与传输层安全选项。
441
392
 
442
- | 指标 | njs-modbus | jsmodbus | modbus-serial |
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
- ### 帧编码 / 解码(CPU 微基准)
395
+ ## 商业支持与许可
449
396
 
450
- | 基准测试 | njs-modbus | jsmodbus | modbus-serial |
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
- ### 全功能码 TCP 吞吐(100 线圈 / 50 寄存器负载)
399
+ - **免费生产使用**:授予个人、教育机构、非营利组织以及年营收低于 100 万美元的企业。
400
+ - **Change Date**:2029-06-24。在该日期,本版本将转为 Apache License, Version 2.0。
401
+ - **商业许可**:面向 OEM、系统集成商以及需要可预测许可路径、有保障支持或无法满足 BSL 免费使用条件的商业产品,我们提供独立的专有商业许可。
460
402
 
461
- | 功能码 | njs-modbus | jsmodbus | modbus-serial |
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
- > FC17 / FC22 / FC23 / FC43 不受 jsmodbus 支持。
405
+ - **产品集成许可** —— 在闭源商业产品中使用 `njs-modbus`,无需承担 copyleft 义务。
406
+ - **专业技术支持** —— 故障排查、性能调优、迁移指导与升级规划。
407
+ - **企业级支持方案** —— 响应 SLA、长期维护版本、优先 Bug 修复与定制开发。
473
408
 
474
- ## 许可证
409
+ 具体授权条款、报价及支持方案请联系:
475
410
 
476
- [![license](https://img.shields.io/github/license/xiejay97/njs-modbus?style=flat-square)](/LICENSE)
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)