njs-modbus 3.2.0 → 3.3.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.md CHANGED
@@ -15,8 +15,8 @@ A pure JavaScript implementation of Modbus for Node.js.
15
15
 
16
16
  - **Protocols:** Modbus TCP, RTU, ASCII
17
17
  - **Transports:** Serial, TCP client/server, UDP client/server
18
- - **Master:** FIFO or pipelined concurrent requests (TCP only)
19
- - **Slave:** Per-connection FIFO or concurrent processing (TCP only)
18
+ - **Master:** FIFO or pipelined concurrent requests (protocol must be TCP)
19
+ - **Slave:** Per-connection FIFO or concurrent processing (protocol must be TCP)
20
20
  - **Custom function codes** with pluggable framing
21
21
  - **Broadcasting** (unit = 0)
22
22
  - **Full TypeScript**
@@ -39,6 +39,8 @@ A pure JavaScript implementation of Modbus for Node.js.
39
39
 
40
40
  ## Installation
41
41
 
42
+ Requires **Node.js ≥ 18.19**.
43
+
42
44
  ```bash
43
45
  npm install njs-modbus
44
46
  ```
@@ -179,7 +181,7 @@ new ModbusMaster({
179
181
  physical: { type: 'TCP_CLIENT' },
180
182
  protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
181
183
  timeout?: 1000, // per-request timeout (ms)
182
- concurrent?: false, // pipelined mode (TCP only)
184
+ concurrent?: false, // pipelined mode (protocol: TCP only)
183
185
  })
184
186
  ```
185
187
 
@@ -240,7 +242,7 @@ Broadcast (`unit = 0`) resolves `void` — slaves never respond.
240
242
  new ModbusSlave({
241
243
  physical: { type: 'TCP_SERVER' },
242
244
  protocol: { type: 'TCP' },
243
- concurrent?: false, // per-connection concurrent (TCP only)
245
+ concurrent?: false, // per-connection concurrent (protocol: TCP only)
244
246
  })
245
247
  ```
246
248
 
@@ -253,10 +255,10 @@ slave.add({
253
255
  // Optional: intercept any FC before default dispatch
254
256
  interceptor?: (fc, data) => Buffer | undefined,
255
257
 
256
- readCoils?: (address, length) => boolean[],
257
- readDiscreteInputs?: (address, length) => boolean[],
258
- readHoldingRegisters?: (address, length) => number[],
259
- readInputRegisters?: (address, length) => number[],
258
+ readCoils?: (address, length) => boolean[] | Uint8Array,
259
+ readDiscreteInputs?: (address, length) => boolean[] | Uint8Array,
260
+ readHoldingRegisters?: (address, length) => number[] | Uint16Array,
261
+ readInputRegisters?: (address, length) => number[] | Uint16Array,
260
262
 
261
263
  writeSingleCoil?: (address, value) => void,
262
264
  writeMultipleCoils?: (address, values) => void,
@@ -292,18 +294,20 @@ Missing handlers return `ILLEGAL_FUNCTION`. Out-of-range addresses return `ILLEG
292
294
 
293
295
  ## Custom Function Codes
294
296
 
297
+ `predictRequestLength` / `predictResponseLength` receive a shared pool buffer and byte offsets (`start`, `end`). Use `end - start` for bounds checks rather than `buffer.length`.
298
+
295
299
  ```typescript
296
300
  import type { CustomFunctionCode } from 'njs-modbus';
297
301
 
298
302
  const cfc: CustomFunctionCode = {
299
303
  fc: 0x50,
300
- predictRequestLength: (buffer) => {
301
- if (buffer.length < 2) return null;
302
- return 4 + buffer[1];
304
+ predictRequestLength: (buffer, start, end) => {
305
+ if (end - start < 2) return null;
306
+ return 4 + buffer[start + 1];
303
307
  },
304
- predictResponseLength: (buffer) => {
305
- if (buffer.length < 2) return null;
306
- return 4 + buffer[1];
308
+ predictResponseLength: (buffer, start, end) => {
309
+ if (end - start < 2) return null;
310
+ return 4 + buffer[start + 1];
307
311
  },
308
312
  handle: async (data, unit) => {
309
313
  return Buffer.from([data[1], data[0]]);
@@ -350,50 +354,75 @@ Benchmarked against [jsmodbus](https://github.com/Cloud-Automation/node-modbus)
350
354
 
351
355
  | Metric | njs-modbus | jsmodbus | modbus-serial |
352
356
  |--------|-----------|----------|---------------|
353
- | TCP Throughput | **5,527 ops/sec** | 3,239 (0.59x) | 371 (0.07x) |
354
- | TCP P99 Latency | **2.71 ms** | 4.73 ms (1.75x) | 12.48 ms (4.61x) |
355
- | TCP CPU Efficiency | **1,116 µs/op** | 1,950 (1.75x) | 16,715 (14.98x) |
356
- | Concurrent (8 conn) | **5,274 ops/sec** | 3,416 (0.65x) | 1,815 (0.34x) |
357
- | RTU CPU Efficiency | **1,762 µs/op** | 1,760 (1.00x) | 2,144 (1.22x) |
358
- | TCP Res Encode | **2.04M ops/sec** | 373K (0.18x) | 411K (0.20x) |
359
- | TCP Res Decode | **1.91M ops/sec** | 538K (0.28x) | 231K (0.12x) |
357
+ | TCP Throughput | **70,544 ops/sec** | 39,827 (0.56x) | 786 (0.01x) |
358
+ | TCP P99 Latency | **53 µs** | 98 µs (1.83x) | 2,331 µs (43.7x) |
359
+ | Concurrent (8 conn) | **75,810 ops/sec** | 41,754 (0.55x) | 5,620 (0.07x) |
360
+ | RTU Serial (115200 baud) | **44 ops/sec** | 44 ops/sec | 45 ops/sec |
361
+ | TCP Response Encode | **6.57M ops/sec** | 1.22M (0.19x) | 1.18M (0.18x) |
362
+ | TCP Response Decode | **6.78M ops/sec** | 1.94M (0.29x) | 0.67M (0.10x) |
363
+ | FC01 Read Coils | **85,170 ops/sec** | 617 (0.01x) | 861 (0.01x) |
360
364
 
361
365
  <details>
362
366
  <summary>Full benchmark results</summary>
363
367
 
364
- Node.js v24.15.0 · linux x64 · 3 runs × 300 s · [full report](./benchmark/RESULTS.md)
368
+ Node.js v24.16.0 · AMD Ryzen 7 9800X3D · linux x64 · 3 runs × 120 s · [full report](./benchmark/RESULTS.md)
365
369
 
366
370
  ### TCP Throughput (Single Connection)
367
371
 
368
372
  ```
369
- njs-modbus │ 5,527 ops/sec 🏆 CPU: 1,116 µs/op
370
- jsmodbus │ 3,239 ops/sec (0.59x) CPU: 1,950 µs/op
371
- modbus-serial │ 371 ops/sec (0.07x) CPU: 16,715 µs/op
373
+ njs-modbus │ ██████████████████████████████ 70,544 ops/sec 🏆 p99: 53 µs CPU: 91 µs/op
374
+ jsmodbus │ █████████████████░░░░░░░░░░░░░ 39,827 ops/sec (0.56x) p99: 98 µs CPU: 161 µs/op
375
+ modbus-serial │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 786 ops/sec (0.01x) p99: 2,331 µs CPU: 8,176 µs/op
372
376
  ```
373
377
 
374
378
  ### Concurrent (8 Connections)
375
379
 
376
380
  ```
377
- njs-modbus │ 5,274 ops/sec 🏆 CPU: 1,392 µs/op
378
- jsmodbus │ 3,416 ops/sec (0.65x) CPU: 2,131 µs/op
379
- modbus-serial │ 1,815 ops/sec (0.34x) CPU: 3,962 µs/op
381
+ njs-modbus │ ██████████████████████████████ 75,810 ops/sec 🏆 p99: 258 µs CPU: 91 µs/op
382
+ jsmodbus │ █████████████████░░░░░░░░░░░░░ 41,754 ops/sec (0.55x) p99: 543 µs CPU: 167 µs/op
383
+ modbus-serial │ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 5,620 ops/sec (0.07x) p99: 3,180 µs CPU: 1,237 µs/op
380
384
  ```
381
385
 
382
- ### RTU Serial (115200 baud simulated)
386
+ ### RTU Serial (115200 baud via socat PTY)
383
387
 
384
388
  ```
385
- njs-modbus │ 44 ops/sec CPU: 1,762 µs/op
386
- jsmodbus │ 44 ops/sec CPU: 1,760 µs/op
387
- modbus-serial │ 44 ops/sec CPU: 2,144 µs/op
389
+ njs-modbus │ 44 ops/sec p99: 993 µs CPU: 729 µs/op GC: 2,613 ns/op
390
+ jsmodbus │ 44 ops/sec p99: 1,019 µs CPU: 694 µs/op GC: 2,609 ns/op
391
+ modbus-serial │ 45 ops/sec p99: 3,228 µs CPU: 765 µs/op GC: 3,864 ns/op
388
392
  ```
389
393
 
390
- ### Encode / Decode (CPU Micro-benchmark)
394
+ ### Frame Encode / Decode (CPU Micro-benchmark)
391
395
 
392
396
  ```
393
- tcpResEncode: njs-modbus 2.04M ops/sec (jsmodbus 0.18x, modbus-serial 0.20x)
394
- tcpResDecode: njs-modbus 1.91M ops/sec (jsmodbus 0.28x, modbus-serial 0.12x)
397
+ tcpReqEncode: njs-modbus 6.90M ops/sec 🏆 jsmodbus 0.78x modbus-serial 0.19x
398
+ tcpResEncode: njs-modbus 6.57M ops/sec 🏆 jsmodbus 0.19x modbus-serial 0.18x
399
+ tcpReqDecode: njs-modbus 6.80M ops/sec 🏆 jsmodbus 0.81x modbus-serial 0.17x
400
+ tcpResDecode: njs-modbus 6.78M ops/sec 🏆 jsmodbus 0.29x modbus-serial 0.10x
401
+ rtuReqEncode: njs-modbus 7.26M ops/sec 🏆 jsmodbus 0.35x modbus-serial 0.76x
402
+ rtuReqDecode: njs-modbus 3.97M ops/sec jsmodbus 1.21x modbus-serial 1.78x 🏆
403
+ asciiReqEncode: njs-modbus 5.64M ops/sec 🏆 modbus-serial 0.57x
404
+ asciiReqDecode: njs-modbus 3.37M ops/sec 🏆 modbus-serial 0.34x
395
405
  ```
396
406
 
407
+ ### Per-Function-Code TCP Throughput (Normal Payload)
408
+
409
+ | Function Code | njs-modbus | jsmodbus | modbus-serial |
410
+ |---------------|-----------|----------|---------------|
411
+ | FC01 Read Coils | **85,170** 🏆 | 617 (0.01x) | 861 (0.01x) |
412
+ | FC02 Read Discrete Inputs | **85,570** 🏆 | 525 (0.01x) | 861 (0.01x) |
413
+ | FC03 Read Holding Registers | **75,344** 🏆 | 40,081 (0.53x) | 852 (0.01x) |
414
+ | FC04 Read Input Registers | **75,757** 🏆 | 49,524 (0.65x) | 866 (0.01x) |
415
+ | FC05 Write Single Coil | **92,852** 🏆 | 53,521 (0.58x) | 871 (0.01x) |
416
+ | FC06 Write Single Register | **92,864** 🏆 | 53,684 (0.58x) | 872 (0.01x) |
417
+ | FC15 Write Multiple Coils | **86,434** 🏆 | 319 (0.00x) | 869 (0.01x) |
418
+ | FC16 Write Multiple Registers | **76,053** 🏆 | 39,356 (0.52x) | 852 (0.01x) |
419
+ | FC17 Report Server ID | **74,697** 🏆 | — | — |
420
+ | FC22 Mask Write Register | **90,717** 🏆 | — | — |
421
+ | FC23 Read/Write Multiple Registers | **85,652** 🏆 | — | — |
422
+ | FC43 Read Device Identification | **78,030** 🏆 | — | 865 (0.01x) |
423
+
424
+ > FC17 / FC22 / FC23 are not supported by jsmodbus or modbus-serial.
425
+
397
426
  </details>
398
427
 
399
428
  ## License
package/README.zh-CN.md CHANGED
@@ -15,8 +15,8 @@ Node.js 的纯 JavaScript Modbus 实现。
15
15
 
16
16
  - **协议:** Modbus TCP、RTU、ASCII
17
17
  - **传输:** 串口、TCP 客户端/服务端、UDP 客户端/服务端
18
- - **主站:** FIFO 或流水线并发请求(仅 TCP)
19
- - **从站:** 每连接 FIFO 或并发处理(仅 TCP)
18
+ - **主站:** FIFO 或流水线并发请求(协议必须为 TCP)
19
+ - **从站:** 每连接 FIFO 或并发处理(协议必须为 TCP)
20
20
  - **自定义功能码**,支持可插拔帧解析
21
21
  - **广播**(单元号 = 0)
22
22
  - **完整 TypeScript**
@@ -39,6 +39,8 @@ Node.js 的纯 JavaScript Modbus 实现。
39
39
 
40
40
  ## 安装
41
41
 
42
+ 需要 **Node.js ≥ 18.19**。
43
+
42
44
  ```bash
43
45
  npm install njs-modbus
44
46
  ```
@@ -179,7 +181,7 @@ new ModbusMaster({
179
181
  physical: { type: 'TCP_CLIENT' },
180
182
  protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
181
183
  timeout?: 1000, // 单次请求超时(毫秒)
182
- concurrent?: false, // 流水线模式(仅 TCP)
184
+ concurrent?: false, // 流水线模式(协议:仅 TCP)
183
185
  })
184
186
  ```
185
187
 
@@ -239,7 +241,7 @@ protocol: {
239
241
  new ModbusSlave({
240
242
  physical: { type: 'TCP_SERVER' },
241
243
  protocol: { type: 'TCP' },
242
- concurrent?: false, // 每连接并发(仅 TCP)
244
+ concurrent?: false, // 每连接并发(协议:仅 TCP)
243
245
  })
244
246
  ```
245
247
 
@@ -252,10 +254,10 @@ slave.add({
252
254
  // 可选:在默认分发前拦截任意功能码
253
255
  interceptor?: (fc, data) => Buffer | undefined,
254
256
 
255
- readCoils?: (address, length) => boolean[],
256
- readDiscreteInputs?: (address, length) => boolean[],
257
- readHoldingRegisters?: (address, length) => number[],
258
- readInputRegisters?: (address, length) => number[],
257
+ readCoils?: (address, length) => boolean[] | Uint8Array,
258
+ readDiscreteInputs?: (address, length) => boolean[] | Uint8Array,
259
+ readHoldingRegisters?: (address, length) => number[] | Uint16Array,
260
+ readInputRegisters?: (address, length) => number[] | Uint16Array,
259
261
 
260
262
  writeSingleCoil?: (address, value) => void,
261
263
  writeMultipleCoils?: (address, values) => void,
@@ -291,18 +293,20 @@ slave.add({
291
293
 
292
294
  ## 自定义功能码
293
295
 
296
+ `predictRequestLength` / `predictResponseLength` 接收共享池缓冲区及字节偏移量(`start`、`end`)。边界检查应使用 `end - start` 而非 `buffer.length`。
297
+
294
298
  ```typescript
295
299
  import type { CustomFunctionCode } from 'njs-modbus';
296
300
 
297
301
  const cfc: CustomFunctionCode = {
298
302
  fc: 0x50,
299
- predictRequestLength: (buffer) => {
300
- if (buffer.length < 2) return null;
301
- return 4 + buffer[1];
303
+ predictRequestLength: (buffer, start, end) => {
304
+ if (end - start < 2) return null;
305
+ return 4 + buffer[start + 1];
302
306
  },
303
- predictResponseLength: (buffer) => {
304
- if (buffer.length < 2) return null;
305
- return 4 + buffer[1];
307
+ predictResponseLength: (buffer, start, end) => {
308
+ if (end - start < 2) return null;
309
+ return 4 + buffer[start + 1];
306
310
  },
307
311
  handle: async (data, unit) => {
308
312
  return Buffer.from([data[1], data[0]]);
@@ -349,50 +353,75 @@ slave.add({
349
353
 
350
354
  | 指标 | njs-modbus | jsmodbus | modbus-serial |
351
355
  |------|-----------|----------|---------------|
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) |
356
+ | TCP 吞吐量 | **70,544 ops/sec** | 39,827 (0.56x) | 786 (0.01x) |
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) |
359
363
 
360
364
  <details>
361
365
  <summary>完整基准测试结果</summary>
362
366
 
363
- Node.js v24.15.0 · linux x64 · 3 次运行 × 300 秒 · [完整报告](./benchmark/RESULTS.md)
367
+ Node.js v24.16.0 · AMD Ryzen 7 9800X3D · linux x64 · 3 次运行 × 120 秒 · [完整报告](./benchmark/RESULTS.md)
364
368
 
365
369
  ### TCP 吞吐量(单连接)
366
370
 
367
371
  ```
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
372
+ njs-modbus │ ██████████████████████████████ 70,544 ops/sec 🏆 p99: 53 µs CPU: 91 µs/op
373
+ jsmodbus │ █████████████████░░░░░░░░░░░░░ 39,827 ops/sec (0.56x) p99: 98 µs CPU: 161 µs/op
374
+ modbus-serial │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 786 ops/sec (0.01x) p99: 2,331 µs CPU: 8,176 µs/op
371
375
  ```
372
376
 
373
377
  ### 并发(8 连接)
374
378
 
375
379
  ```
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
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
379
383
  ```
380
384
 
381
- ### RTU 串口(模拟 115200 波特率)
385
+ ### RTU 串口(socat PTY 模拟 115200 波特率)
382
386
 
383
387
  ```
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
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
387
391
  ```
388
392
 
389
- ### 编码 / 解码(CPU 微基准测试)
393
+ ### 帧编码 / 解码(CPU 微基准测试)
390
394
 
391
395
  ```
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)
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
394
404
  ```
395
405
 
406
+ ### 全功能码 TCP 吞吐量(常规负载)
407
+
408
+ | 功能码 | njs-modbus | jsmodbus | modbus-serial |
409
+ |--------|-----------|----------|---------------|
410
+ | FC01 读取线圈 | **85,170** 🏆 | 617 (0.01x) | 861 (0.01x) |
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) |
422
+
423
+ > FC17 / FC22 / FC23 不受 jsmodbus 和 modbus-serial 支持。
424
+
396
425
  </details>
397
426
 
398
427
  ## 许可证