njs-modbus 3.2.0 → 3.4.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](./README.md) | 简体中文
4
4
 
5
- Node.js 的纯 JavaScript Modbus 实现。
5
+ Node.js 的纯 JavaScript Modbus 实现,针对高吞吐量和低 GC 压力优化。
6
6
 
7
7
  <!-- prettier-ignore-start -->
8
8
  [![npm download](http://img.shields.io/npm/dw/njs-modbus.svg?style=flat-square)](http://www.npm-stats.com/~packages/njs-modbus)
@@ -11,17 +11,20 @@ Node.js 的纯 JavaScript Modbus 实现。
11
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
12
  <!-- prettier-ignore-end -->
13
13
 
14
- ## 特性
14
+ ## 核心亮点
15
15
 
16
- - **协议:** Modbus TCP、RTU、ASCII
17
- - **传输:** 串口、TCP 客户端/服务端、UDP 客户端/服务端
18
- - **主站:** FIFO 或流水线并发请求(仅 TCP)
19
- - **从站:** 每连接 FIFO 或并发处理(仅 TCP)
16
+ - **协议** Modbus TCP、RTU、ASCII
17
+ - **传输** 串口、TCP 客户端/服务端、UDP 客户端/服务端,支持可插拔自定义传输层
18
+ - **性能优先** — 并行 FIFO 队列、惰性删除定时器堆、内联大端读写、零分配热路径
19
+ - **主站** FIFO 或流水线并发请求(仅 TCP)
20
+ - **从站** — 每连接 FIFO 或并发处理(仅 TCP)
20
21
  - **自定义功能码**,支持可插拔帧解析
21
- - **广播**(单元号 = 0
22
+ - **广播**(`unit = 0`)
22
23
  - **完整 TypeScript**
23
24
  - **零运行时依赖**(仅在使用串口时 peer-depends `serialport`)
24
25
 
26
+ ## 支持的功能码
27
+
25
28
  | 码值 | 名称 |
26
29
  |------|------|
27
30
  | 01 | 读取线圈 |
@@ -39,6 +42,8 @@ Node.js 的纯 JavaScript Modbus 实现。
39
42
 
40
43
  ## 安装
41
44
 
45
+ 需要 **Node.js ≥ 18.19**。
46
+
42
47
  ```bash
43
48
  npm install njs-modbus
44
49
  ```
@@ -114,20 +119,45 @@ const master = new ModbusMaster({
114
119
  });
115
120
  ```
116
121
 
122
+ ## 架构
123
+
124
+ 本库遵循严格的两层设计:
125
+
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
+ ```
144
+
145
+ 所有传输层都统一为面向连接的模型:物理层在就绪时触发 `'connect'` 事件并传入一个 `AbstractPhysicalConnection`,每个连接会独立创建一个应用层实例。
146
+
117
147
  ## 物理层
118
148
 
119
- 所有物理层暴露 `open()` / `close()`、`state` 属性,以及事件:`'open'`、`'connect'`、`'close'`、`'error'`。
149
+ 所有物理层暴露 `open()` / `close()`、`state` 属性,以及 [事件](#事件) 章节中列出的事件。
120
150
 
121
- | 类型 | 类 | `open(...)` 参数 |
122
- |------|-------|-----------------|
123
- | `SERIAL` | `SerialPhysicalLayer` | |
124
- | `TCP_CLIENT` | `TcpClientPhysicalLayer` | `SocketConnectOpts` |
125
- | `TCP_SERVER` | `TcpServerPhysicalLayer` | `ListenOptions` |
126
- | `UDP_CLIENT` | `UdpClientPhysicalLayer` | `{ port, address }` |
127
- | `UDP_SERVER` | `UdpServerPhysicalLayer` | `BindOptions` |
128
- | `CUSTOM` | *(用户自定义)* | *(自定义)* |
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` 实例 |
129
159
 
130
- ### 服务端配置
160
+ ### TCP 服务端配置
131
161
 
132
162
  ```typescript
133
163
  const slave = new ModbusSlave({
@@ -143,62 +173,44 @@ const slave = new ModbusSlave({
143
173
  });
144
174
  ```
145
175
 
146
- ### 物理层选项
176
+ ### 物理层选项参考
147
177
 
148
178
  | 选项 | 类型 | 说明 |
149
- |--------|------|-------------|
179
+ |------|------|------|
150
180
  | `whitelist` | `string[]` | 允许的客户端 IP。`::ffff:` 前缀自动剥离。 |
151
181
  | `maxConnections` | `number` | 最大并发连接数。超出后新连接静默丢弃。 |
152
182
  | `idleTimeout` | `number` | 空闲超时(毫秒),超时后驱逐连接。默认 `30000`,传 `0` 禁用。 |
153
183
  | `socketOpts` / `serverOpts` | `object` | 透传给 Node.js `createSocket()` / `createServer()`。 |
154
184
 
155
- ## 自定义物理层
156
-
157
- 任何面向字节的传输层都可以通过实现 `AbstractPhysicalLayer` 和 `AbstractPhysicalConnection` 来使用,然后传入 `{ type: 'CUSTOM', layer: yourLayer }`。
158
-
159
- ```typescript
160
- import { AbstractPhysicalLayer, AbstractPhysicalConnection } from 'njs-modbus';
161
-
162
- class MyPhysicalLayer extends AbstractPhysicalLayer { /* open / close */ }
163
- class MyConnection extends AbstractPhysicalConnection { /* write / destroy, emit 'data' */ }
164
-
165
- const master = new ModbusMaster({
166
- physical: { type: 'CUSTOM', layer: new MyPhysicalLayer() },
167
- protocol: { type: 'TCP' },
168
- });
169
- ```
170
-
171
- 查看 [`examples/websocket`](./examples/websocket) 获取完整的 **WebSocket 承载 Modbus TCP** 实现——包含客户端/服务端物理层及可运行的演示。
185
+ ## 协议层
172
186
 
173
- 查看 [`examples/bluetooth`](./examples/bluetooth) 获取 **BLE 承载 Modbus TCP** 实现(基于 Nordic UART Service),包含无需真实蓝牙硬件即可运行的回环测试。
187
+ | 协议 | 选项 | 说明 |
188
+ |------|------|------|
189
+ | `TCP` | 无 | Modbus TCP,带 MBAP 头 |
190
+ | `RTU` | `RtuProtocolOptions` | Modbus RTU,带 CRC16 |
191
+ | `ASCII` | `AsciiApplicationLayerOptions` | Modbus ASCII,带 LRC |
174
192
 
175
- ## 主站 API
176
-
177
- ```typescript
178
- new ModbusMaster({
179
- physical: { type: 'TCP_CLIENT' },
180
- protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
181
- timeout?: 1000, // 单次请求超时(毫秒)
182
- concurrent?: false, // 流水线模式(仅 TCP)
183
- })
184
- ```
185
-
186
- RTU 帧选项:
193
+ ### RTU 选项
187
194
 
188
195
  ```typescript
189
196
  protocol: {
190
197
  type: 'RTU',
191
198
  opts: {
192
- // 既可以传裸数字(毫秒),也可以传 `{ unit: 'bit' | 'ms', value: N }`。
193
- // `0` 显式禁用该定时器(适用于 RTU-over-TCP 等无丢包传输)。
194
- intervalBetweenFrames: 20, // 20 毫秒
195
- interCharTimeout: { unit: 'bit', value: 10 }, // 按 bit-time,需 baudRate
196
- poolSize: 1024,
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,
197
209
  },
198
210
  }
199
211
  ```
200
212
 
201
- ASCII 选项:
213
+ ### ASCII 选项
202
214
 
203
215
  ```typescript
204
216
  protocol: {
@@ -209,11 +221,24 @@ protocol: {
209
221
  }
210
222
  ```
211
223
 
224
+ ## 主站 API
225
+
226
+ ### 构造函数
227
+
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
+ ```
236
+
212
237
  ### 方法
213
238
 
214
239
  | 方法 | 说明 |
215
- |--------|-------------|
216
- | `open(...args)` | 打开物理层。只能调用一次;`close()` 后实例永久失效。 |
240
+ |------|------|
241
+ | `open(...args)` | 打开物理层。一次性:close() 后不可重新打开。 |
217
242
  | `close()` | 立即关闭。进行中和队列中的请求会被 reject。 |
218
243
  | `readCoils(unit, address, length, timeout?)` | FC 01 |
219
244
  | `readDiscreteInputs(unit, address, length, timeout?)` | FC 02 |
@@ -235,6 +260,8 @@ protocol: {
235
260
 
236
261
  ## 从站 API
237
262
 
263
+ ### 构造函数
264
+
238
265
  ```typescript
239
266
  new ModbusSlave({
240
267
  physical: { type: 'TCP_SERVER' },
@@ -252,10 +279,10 @@ slave.add({
252
279
  // 可选:在默认分发前拦截任意功能码
253
280
  interceptor?: (fc, data) => Buffer | undefined,
254
281
 
255
- readCoils?: (address, length) => boolean[],
256
- readDiscreteInputs?: (address, length) => boolean[],
257
- readHoldingRegisters?: (address, length) => number[],
258
- readInputRegisters?: (address, length) => number[],
282
+ readCoils?: (address, length) => Uint8Array,
283
+ readDiscreteInputs?: (address, length) => Uint8Array,
284
+ readHoldingRegisters?: (address, length) => number[] | Uint16Array,
285
+ readInputRegisters?: (address, length) => number[] | Uint16Array,
259
286
 
260
287
  writeSingleCoil?: (address, value) => void,
261
288
  writeMultipleCoils?: (address, values) => void,
@@ -280,29 +307,70 @@ slave.add({
280
307
 
281
308
  缺失的处理器返回 `ILLEGAL_FUNCTION`。越界地址返回 `ILLEGAL_DATA_ADDRESS`。处理器抛出的异常变为 `SERVER_DEVICE_FAILURE`,除非错误是 `ModbusError`。
282
309
 
310
+ ### 方法
311
+
283
312
  | 方法 | 说明 |
284
- |--------|-------------|
313
+ |------|------|
285
314
  | `add(model)` | 注册从站模型 |
286
- | `remove(unit)` | 移除从站模型 |
287
- | `open(...args)` | 打开物理层。一次性:close() 后不可重新打开。 |
315
+ | `remove(unit)` | 移除模型 |
316
+ | `open(...args)` | 打开并开始接受连接。一次性。 |
288
317
  | `close()` | 立即关闭 |
289
318
  | `addCustomFunctionCode(cfc)` | 注册自定义功能码 |
290
319
  | `removeCustomFunctionCode(fc)` | 注销自定义功能码 |
291
320
 
321
+ ## 事件
322
+
323
+ `ModbusMaster` 和 `ModbusSlave` 都是 `EventEmitter`,所有事件均带类型定义。
324
+
325
+ ### 物理层事件
326
+
327
+ | 事件 | 参数 | 说明 |
328
+ |------|------|------|
329
+ | `open` | `()` | 物理层就绪,可接受连接 |
330
+ | `connect` | `(connection)` | 新连接已建立 |
331
+ | `close` | `()` | 物理层已关闭 |
332
+ | `error` | `(error)` | 物理层错误 |
333
+
334
+ ### 连接调试事件
335
+
336
+ | 事件 | 参数 | 说明 |
337
+ |------|------|------|
338
+ | `tx` | `(buffer, connection)` | 原始数据已写入链路 |
339
+ | `rx` | `(buffer, connection)` | 从链路接收到原始数据 |
340
+
341
+ ### 应用层事件
342
+
343
+ | 事件 | 参数 | 说明 |
344
+ |------|------|------|
345
+ | `framing` | `(frame, connection)` | 完整帧已解码 |
346
+ | `framingError` | `(error, connection)` | 帧解析错误(CRC/LRC 失败、MBAP 格式错误、时序违规等) |
347
+
348
+ ```typescript
349
+ master.on('framing', (frame, connection) => {
350
+ console.log('frame:', frame.unit, frame.fc, frame.transaction);
351
+ });
352
+
353
+ master.on('framingError', (error, connection) => {
354
+ console.log('framing error:', error.message);
355
+ });
356
+ ```
357
+
292
358
  ## 自定义功能码
293
359
 
360
+ `predictRequestLength` / `predictResponseLength` 接收 `getByte` 访问器和当前可用字节数 `length`。请使用 `length` 做边界检查,不要直接访问缓冲区。
361
+
294
362
  ```typescript
295
363
  import type { CustomFunctionCode } from 'njs-modbus';
296
364
 
297
365
  const cfc: CustomFunctionCode = {
298
366
  fc: 0x50,
299
- predictRequestLength: (buffer) => {
300
- if (buffer.length < 2) return null;
301
- return 4 + buffer[1];
367
+ predictRequestLength: (getByte, length) => {
368
+ if (length < 2) return 0; // 需要更多字节
369
+ return 4 + getByte(1);
302
370
  },
303
- predictResponseLength: (buffer) => {
304
- if (buffer.length < 2) return null;
305
- return 4 + buffer[1];
371
+ predictResponseLength: (getByte, length) => {
372
+ if (length < 2) return 0;
373
+ return 4 + getByte(1);
306
374
  },
307
375
  handle: async (data, unit) => {
308
376
  return Buffer.from([data[1], data[0]]);
@@ -343,57 +411,65 @@ slave.add({
343
411
  });
344
412
  ```
345
413
 
346
- ## 性能
347
-
348
- 与 [jsmodbus](https://github.com/Cloud-Automation/node-modbus) 和 [modbus-serial](https://github.com/yaacov/node-modbus-serial) 的对比。
349
-
350
- | 指标 | njs-modbus | jsmodbus | modbus-serial |
351
- |------|-----------|----------|---------------|
352
- | TCP 吞吐量 | **5,527 ops/sec** | 3,239 (0.59x) | 371 (0.07x) |
353
- | TCP P99 延迟 | **2.71 ms** | 4.73 ms (1.75x) | 12.48 ms (4.61x) |
354
- | TCP CPU 效率 | **1,116 µs/op** | 1,950 (1.75x) | 16,715 (14.98x) |
355
- | 并发 (8 连接) | **5,274 ops/sec** | 3,416 (0.65x) | 1,815 (0.34x) |
356
- | RTU CPU 效率 | **1,762 µs/op** | 1,760 (1.00x) | 2,144 (1.22x) |
357
- | TCP 响应编码 | **2.04M ops/sec** | 373K (0.18x) | 411K (0.20x) |
358
- | TCP 响应解码 | **1.91M ops/sec** | 538K (0.28x) | 231K (0.12x) |
414
+ ## 自定义物理层
359
415
 
360
- <details>
361
- <summary>完整基准测试结果</summary>
416
+ 任何面向字节的传输层都可以通过实现 `AbstractPhysicalLayer` 和 `AbstractPhysicalConnection` 来使用,然后传入 `{ type: 'CUSTOM', layer: yourLayer }`。
362
417
 
363
- Node.js v24.15.0 · linux x64 · 3 次运行 × 300 秒 · [完整报告](./benchmark/RESULTS.md)
418
+ ```typescript
419
+ import { AbstractPhysicalLayer, AbstractPhysicalConnection } from 'njs-modbus';
364
420
 
365
- ### TCP 吞吐量(单连接)
421
+ class MyPhysicalLayer extends AbstractPhysicalLayer { /* open / close */ }
422
+ class MyConnection extends AbstractPhysicalConnection { /* write / destroy, emit 'data' */ }
366
423
 
367
- ```
368
- njs-modbus │ 5,527 ops/sec 🏆 CPU: 1,116 µs/op
369
- jsmodbus │ 3,239 ops/sec (0.59x) CPU: 1,950 µs/op
370
- modbus-serial │ 371 ops/sec (0.07x) CPU: 16,715 µs/op
424
+ const master = new ModbusMaster({
425
+ physical: { type: 'CUSTOM', layer: new MyPhysicalLayer() },
426
+ protocol: { type: 'TCP' },
427
+ });
371
428
  ```
372
429
 
373
- ### 并发(8 连接)
430
+ 查看 [`examples/websocket`](./examples/websocket) 获取完整的 **WebSocket 承载 Modbus TCP** 实现。
374
431
 
375
- ```
376
- njs-modbus │ 5,274 ops/sec 🏆 CPU: 1,392 µs/op
377
- jsmodbus │ 3,416 ops/sec (0.65x) CPU: 2,131 µs/op
378
- modbus-serial │ 1,815 ops/sec (0.34x) CPU: 3,962 µs/op
379
- ```
432
+ 查看 [`examples/bluetooth`](./examples/bluetooth) 获取基于 Nordic UART Service (NUS) 的 **BLE 承载 Modbus TCP** 实现。
380
433
 
381
- ### RTU 串口(模拟 115200 波特率)
434
+ ## 性能
382
435
 
383
- ```
384
- njs-modbus │ 44 ops/sec CPU: 1,762 µs/op
385
- jsmodbus │ 44 ops/sec CPU: 1,760 µs/op
386
- modbus-serial │ 44 ops/sec CPU: 2,144 µs/op
387
- ```
436
+ 与 [jsmodbus](https://github.com/Cloud-Automation/node-modbus) v4.0.10 和 [modbus-serial](https://github.com/yaacov/node-modbus-serial) v8.0.25 的对比。
388
437
 
389
- ### 编码 / 解码(CPU 微基准测试)
438
+ 测试环境:AMD Ryzen 7 9800X3D 8-Core Processor · Node.js v24.16.0 · [完整报告](./benchmark/report_presentation.md)
390
439
 
391
- ```
392
- tcpResEncode: njs-modbus 2.04M ops/sec (jsmodbus 0.18x, modbus-serial 0.20x)
393
- tcpResDecode: njs-modbus 1.91M ops/sec (jsmodbus 0.28x, modbus-serial 0.12x)
394
- ```
440
+ ### TCP 端到端
395
441
 
396
- </details>
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) |
447
+
448
+ ### 帧编码 / 解码(CPU 微基准)
449
+
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) |
458
+
459
+ ### 全功能码 TCP 吞吐(100 线圈 / 50 寄存器负载)
460
+
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) |
471
+
472
+ > FC17 / FC22 / FC23 / FC43 不受 jsmodbus 支持。
397
473
 
398
474
  ## 许可证
399
475