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.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](./README.zh-CN.md)
4
4
 
5
- A pure JavaScript implementation of Modbus for Node.js.
5
+ A pure JavaScript implementation of Modbus for Node.js, optimized for throughput and low GC pressure.
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 @@ A pure JavaScript implementation of Modbus for Node.js.
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
- ## Features
14
+ ## Highlights
15
15
 
16
- - **Protocols:** Modbus TCP, RTU, ASCII
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)
16
+ - **Protocols** Modbus TCP, RTU, ASCII
17
+ - **Transports** Serial, TCP client/server, UDP client/server, plus pluggable custom layers
18
+ - **Performance-first** — Parallel FIFO queues, lazy-deletion timer heap, inline BE reads/writes, zero-allocation hot paths
19
+ - **Master** FIFO or pipelined concurrent requests (TCP only)
20
+ - **Slave** — Per-connection FIFO or concurrent processing (TCP only)
20
21
  - **Custom function codes** with pluggable framing
21
- - **Broadcasting** (unit = 0)
22
+ - **Broadcasting** (`unit = 0`)
22
23
  - **Full TypeScript**
23
24
  - **Zero runtime dependencies** (peer-depends on `serialport` for serial)
24
25
 
26
+ ## Supported Function Codes
27
+
25
28
  | Code | Name |
26
29
  |------|------|
27
30
  | 01 | Read Coils |
@@ -39,6 +42,8 @@ A pure JavaScript implementation of Modbus for Node.js.
39
42
 
40
43
  ## Installation
41
44
 
45
+ Requires **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
 
117
- ## Physical Layers
122
+ ## Architecture
123
+
124
+ The library follows a strict two-layer design:
125
+
126
+ ```
127
+ ┌─────────────────────────────────────────┐
128
+ │ ModbusMaster / ModbusSlave │ ← User API, session/queue management
129
+ ├─────────────────────────────────────────┤
130
+ │ TcpApplicationLayer │ ← Protocol framing (MBAP / CRC16 / LRC)
131
+ │ RtuApplicationLayer │
132
+ │ AsciiApplicationLayer │
133
+ ├─────────────────────────────────────────┤
134
+ │ AbstractPhysicalConnection │ ← Per-connection I/O
135
+ ├─────────────────────────────────────────┤
136
+ │ AbstractPhysicalLayer │ ← Resource ownership (port/socket/server)
137
+ │ ├── TcpClientPhysicalLayer │
138
+ │ ├── TcpServerPhysicalLayer │
139
+ │ ├── UdpClientPhysicalLayer │
140
+ │ ├── UdpServerPhysicalLayer │
141
+ │ └── SerialPhysicalLayer │
142
+ └─────────────────────────────────────────┘
143
+ ```
144
+
145
+ Every transport is modeled as connection-oriented: physical layers emit `'connect'` with an `AbstractPhysicalConnection`, and a dedicated application layer instance is created per connection.
146
+
147
+ ## Physical Layer
118
148
 
119
- All physical layers expose `open()` / `close()`, a `state` property, and events: `'open'`, `'connect'`, `'close'`, `'error'`.
149
+ All physical layers expose `open()` / `close()`, a `state` property, and the events documented in [Events](#events).
120
150
 
121
- | Type | Class | `open(...)` args |
122
- |------|-------|-----------------|
123
- | `SERIAL` | `SerialPhysicalLayer` | none |
124
- | `TCP_CLIENT` | `TcpClientPhysicalLayer` | `SocketConnectOpts` |
125
- | `TCP_SERVER` | `TcpServerPhysicalLayer` | `ListenOptions` |
126
- | `UDP_CLIENT` | `UdpClientPhysicalLayer` | `{ port, address }` |
127
- | `UDP_SERVER` | `UdpServerPhysicalLayer` | `BindOptions` |
128
- | `CUSTOM` | *(user-provided)* | *(user-defined)* |
151
+ | Type | `open(...)` Args | Description |
152
+ |------|-----------------|-------------|
153
+ | `SERIAL` | `()` | Serial port via `serialport` package |
154
+ | `TCP_CLIENT` | `SocketConnectOpts` | TCP client socket |
155
+ | `TCP_SERVER` | `ListenOptions` | TCP server |
156
+ | `UDP_CLIENT` | `{ port?, address? }` | UDP client |
157
+ | `UDP_SERVER` | `BindOptions` | UDP server |
158
+ | `CUSTOM` | *(user-defined)* | User-provided `AbstractPhysicalLayer` instance |
129
159
 
130
- ### Server options
160
+ ### TCP Server options
131
161
 
132
162
  ```typescript
133
163
  const slave = new ModbusSlave({
@@ -143,7 +173,7 @@ const slave = new ModbusSlave({
143
173
  });
144
174
  ```
145
175
 
146
- ### Physical layer options
176
+ ### Physical layer option reference
147
177
 
148
178
  | Option | Type | Description |
149
179
  |--------|------|-------------|
@@ -152,54 +182,35 @@ const slave = new ModbusSlave({
152
182
  | `idleTimeout` | `number` | Idle timeout in ms before evicting a connection. Default `30000`. Pass `0` to disable. |
153
183
  | `socketOpts` / `serverOpts` | `object` | Forwarded to Node.js `createSocket()` / `createServer()`. |
154
184
 
155
- ## Custom Physical Layer
156
-
157
- Any byte-oriented transport can be used by implementing `AbstractPhysicalLayer` and `AbstractPhysicalConnection`, then passing `{ 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
- See [`examples/websocket`](./examples/websocket) for a complete **Modbus TCP over WebSocket** implementation — client/server physical layers with a working demo.
185
+ ## Protocol Layer
172
186
 
173
- See [`examples/bluetooth`](./examples/bluetooth) for a **Modbus TCP over BLE** implementation using the Nordic UART Service (NUS) — includes a loopback test that runs without real BLE hardware.
187
+ | Protocol | Options | Description |
188
+ |----------|---------|-------------|
189
+ | `TCP` | none | Modbus TCP with MBAP header |
190
+ | `RTU` | `RtuProtocolOptions` | Modbus RTU with CRC16 |
191
+ | `ASCII` | `AsciiApplicationLayerOptions` | Modbus ASCII with LRC |
174
192
 
175
- ## Master API
176
-
177
- ```typescript
178
- new ModbusMaster({
179
- physical: { type: 'TCP_CLIENT' },
180
- protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
181
- timeout?: 1000, // per-request timeout (ms)
182
- concurrent?: false, // pipelined mode (TCP only)
183
- })
184
- ```
185
-
186
- RTU framing options:
193
+ ### RTU options
187
194
 
188
195
  ```typescript
189
196
  protocol: {
190
197
  type: 'RTU',
191
198
  opts: {
192
- // Either a bare number (milliseconds), or `{ unit: 'bit' | 'ms', value: N }`.
193
- // Use `0` to disable the timer entirely (useful for lossless transports
194
- // such as RTU-over-TCP).
195
- intervalBetweenFrames: 20, // 20 ms
196
- interCharTimeout: { unit: 'bit', value: 10 }, // bit-time, needs baudRate
197
- poolSize: 1024,
199
+ // t3.5 inter-frame silence.
200
+ // Either a bare number (ms) or { unit: 'bit' | 'ms', value: N }.
201
+ // Use 0 to disable (useful for lossless transports like RTU-over-TCP).
202
+ intervalBetweenFrames: 20,
203
+
204
+ // t1.5 inter-character timeout (opt-in).
205
+ interCharTimeout: { unit: 'bit', value: 10 },
206
+
207
+ // Discard frames with t1.5 timing violations.
208
+ strictTiming: true,
198
209
  },
199
210
  }
200
211
  ```
201
212
 
202
- ASCII options:
213
+ ### ASCII options
203
214
 
204
215
  ```typescript
205
216
  protocol: {
@@ -210,11 +221,24 @@ protocol: {
210
221
  }
211
222
  ```
212
223
 
224
+ ## Master API
225
+
226
+ ### Constructor
227
+
228
+ ```typescript
229
+ new ModbusMaster({
230
+ physical: { type: 'TCP_CLIENT' },
231
+ protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
232
+ timeout?: 1000, // per-request timeout (ms)
233
+ concurrent?: false, // pipelined mode (TCP only)
234
+ })
235
+ ```
236
+
213
237
  ### Methods
214
238
 
215
239
  | Method | Description |
216
240
  |--------|-------------|
217
- | `open(...args)` | Open the physical layer. Can only be called once; after `close()` the instance is dead. |
241
+ | `open(...args)` | Open the physical layer. One-shot: cannot reopen after `close()`. |
218
242
  | `close()` | Close immediately. In-flight and queued requests are rejected. |
219
243
  | `readCoils(unit, address, length, timeout?)` | FC 01 |
220
244
  | `readDiscreteInputs(unit, address, length, timeout?)` | FC 02 |
@@ -236,6 +260,8 @@ Broadcast (`unit = 0`) resolves `void` — slaves never respond.
236
260
 
237
261
  ## Slave API
238
262
 
263
+ ### Constructor
264
+
239
265
  ```typescript
240
266
  new ModbusSlave({
241
267
  physical: { type: 'TCP_SERVER' },
@@ -253,10 +279,10 @@ slave.add({
253
279
  // Optional: intercept any FC before default dispatch
254
280
  interceptor?: (fc, data) => Buffer | undefined,
255
281
 
256
- readCoils?: (address, length) => boolean[],
257
- readDiscreteInputs?: (address, length) => boolean[],
258
- readHoldingRegisters?: (address, length) => number[],
259
- 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,
260
286
 
261
287
  writeSingleCoil?: (address, value) => void,
262
288
  writeMultipleCoils?: (address, values) => void,
@@ -281,29 +307,70 @@ slave.add({
281
307
 
282
308
  Missing handlers return `ILLEGAL_FUNCTION`. Out-of-range addresses return `ILLEGAL_DATA_ADDRESS`. Handler throws become `SERVER_DEVICE_FAILURE` unless the error is a `ModbusError`.
283
309
 
310
+ ### Methods
311
+
284
312
  | Method | Description |
285
313
  |--------|-------------|
286
314
  | `add(model)` | Register a slave model |
287
- | `remove(unit)` | Remove a slave model |
288
- | `open(...args)` | Open the physical layer. One-shot: cannot reopen after `close()`. |
315
+ | `remove(unit)` | Remove a model |
316
+ | `open(...args)` | Open and begin accepting connections. One-shot. |
289
317
  | `close()` | Close immediately |
290
- | `addCustomFunctionCode(cfc)` | Register a custom FC |
291
- | `removeCustomFunctionCode(fc)` | Unregister a custom FC |
318
+ | `addCustomFunctionCode(cfc)` | Register custom FC |
319
+ | `removeCustomFunctionCode(fc)` | Unregister custom FC |
320
+
321
+ ## Events
322
+
323
+ Both `ModbusMaster` and `ModbusSlave` are `EventEmitter`s. All events are typed.
324
+
325
+ ### Physical layer events
326
+
327
+ | Event | Arguments | Description |
328
+ |-------|-----------|-------------|
329
+ | `open` | `()` | Physical layer is ready to accept connections |
330
+ | `connect` | `(connection)` | A new connection has been established |
331
+ | `close` | `()` | Physical layer has closed |
332
+ | `error` | `(error)` | Physical layer error |
333
+
334
+ ### Connection debug events
335
+
336
+ | Event | Arguments | Description |
337
+ |-------|-----------|-------------|
338
+ | `tx` | `(buffer, connection)` | Raw data written to the wire |
339
+ | `rx` | `(buffer, connection)` | Raw data received from the wire |
340
+
341
+ ### Application layer events
342
+
343
+ | Event | Arguments | Description |
344
+ |-------|-----------|-------------|
345
+ | `framing` | `(frame, connection)` | A complete frame has been decoded |
346
+ | `framingError` | `(error, connection)` | Framing error (CRC/LRC failure, malformed MBAP, timing violation, etc.) |
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
+ ```
292
357
 
293
358
  ## Custom Function Codes
294
359
 
360
+ `predictRequestLength` / `predictResponseLength` receive a `getByte` accessor and the number of available `length` bytes. Use `length` for bounds checks — do not access the buffer directly.
361
+
295
362
  ```typescript
296
363
  import type { CustomFunctionCode } from 'njs-modbus';
297
364
 
298
365
  const cfc: CustomFunctionCode = {
299
366
  fc: 0x50,
300
- predictRequestLength: (buffer) => {
301
- if (buffer.length < 2) return null;
302
- return 4 + buffer[1];
367
+ predictRequestLength: (getByte, length) => {
368
+ if (length < 2) return 0; // need more bytes
369
+ return 4 + getByte(1);
303
370
  },
304
- predictResponseLength: (buffer) => {
305
- if (buffer.length < 2) return null;
306
- return 4 + buffer[1];
371
+ predictResponseLength: (getByte, length) => {
372
+ if (length < 2) return 0;
373
+ return 4 + getByte(1);
307
374
  },
308
375
  handle: async (data, unit) => {
309
376
  return Buffer.from([data[1], data[0]]);
@@ -344,57 +411,65 @@ slave.add({
344
411
  });
345
412
  ```
346
413
 
347
- ## Performance
348
-
349
- Benchmarked against [jsmodbus](https://github.com/Cloud-Automation/node-modbus) and [modbus-serial](https://github.com/yaacov/node-modbus-serial).
350
-
351
- | Metric | njs-modbus | jsmodbus | modbus-serial |
352
- |--------|-----------|----------|---------------|
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) |
414
+ ## Custom Physical Layer
360
415
 
361
- <details>
362
- <summary>Full benchmark results</summary>
416
+ Any byte-oriented transport can be used by implementing `AbstractPhysicalLayer` and `AbstractPhysicalConnection`, then passing `{ type: 'CUSTOM', layer: yourLayer }`.
363
417
 
364
- Node.js v24.15.0 · linux x64 · 3 runs × 300 s · [full report](./benchmark/RESULTS.md)
418
+ ```typescript
419
+ import { AbstractPhysicalLayer, AbstractPhysicalConnection } from 'njs-modbus';
365
420
 
366
- ### TCP Throughput (Single Connection)
421
+ class MyPhysicalLayer extends AbstractPhysicalLayer { /* open / close */ }
422
+ class MyConnection extends AbstractPhysicalConnection { /* write / destroy, emit 'data' */ }
367
423
 
368
- ```
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
424
+ const master = new ModbusMaster({
425
+ physical: { type: 'CUSTOM', layer: new MyPhysicalLayer() },
426
+ protocol: { type: 'TCP' },
427
+ });
372
428
  ```
373
429
 
374
- ### Concurrent (8 Connections)
430
+ See [`examples/websocket`](./examples/websocket) for a complete **Modbus TCP over WebSocket** implementation.
375
431
 
376
- ```
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
380
- ```
432
+ See [`examples/bluetooth`](./examples/bluetooth) for a **Modbus TCP over BLE** implementation using the Nordic UART Service (NUS).
381
433
 
382
- ### RTU Serial (115200 baud simulated)
434
+ ## Performance
383
435
 
384
- ```
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
388
- ```
436
+ Benchmarked against [jsmodbus](https://github.com/Cloud-Automation/node-modbus) v4.0.10 and [modbus-serial](https://github.com/yaacov/node-modbus-serial) v8.0.25.
389
437
 
390
- ### Encode / Decode (CPU Micro-benchmark)
438
+ Environment: AMD Ryzen 7 9800X3D 8-Core Processor · Node.js v24.16.0 · [full report](./benchmark/report_presentation.md)
391
439
 
392
- ```
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)
395
- ```
440
+ ### End-to-end TCP
396
441
 
397
- </details>
442
+ | Metric | njs-modbus | jsmodbus | modbus-serial |
443
+ |--------|-----------|----------|---------------|
444
+ | Single connection | **74,964 ops/sec** 🏆 | 55,996 (0.75x) | 853 (0.01x) |
445
+ | 8 concurrent clients | **10,188 ops/sec** 🏆 | 5,918 (0.58x) | 728 (0.07x) |
446
+ | P99 latency (single) | **17 µs** 🏆 | 15 µs (0.88x) | 1,206 µs (70.9x) |
447
+
448
+ ### Frame encode / decode (CPU micro-benchmark)
449
+
450
+ | Benchmark | njs-modbus | jsmodbus | modbus-serial |
451
+ |-----------|-----------|----------|---------------|
452
+ | TCP request encode | **9.61M ops/sec** 🏆 | 6.32M (0.66x) | 0.68M (0.07x) |
453
+ | TCP response encode | **7.51M ops/sec** 🏆 | 1.18M (0.16x) | 0.44M (0.06x) |
454
+ | TCP response decode | **8.55M ops/sec** 🏆 | 2.04M (0.24x) | 0.70M (0.08x) |
455
+ | RTU request encode | **9.42M ops/sec** 🏆 | 3.03M (0.32x) | 6.57M (0.70x) |
456
+ | ASCII request encode | **7.09M ops/sec** 🏆 | — | 4.11M (0.58x) |
457
+ | ASCII request decode | **4.91M ops/sec** 🏆 | — | 1.19M (0.24x) |
458
+
459
+ ### Per-function-code TCP throughput (100 coils / 50 registers)
460
+
461
+ | Function Code | njs-modbus | jsmodbus | modbus-serial |
462
+ |---------------|-----------|----------|---------------|
463
+ | FC01 Read Coils | **75,430** 🏆 | 488 (0.01x) | 859 (0.01x) |
464
+ | FC03 Read Holding Registers | **82,028** 🏆 | 46,596 (0.57x) | 864 (0.01x) |
465
+ | FC06 Write Single Register | **86,110** 🏆 | 51,084 (0.59x) | 871 (0.01x) |
466
+ | FC15 Write Multiple Coils | **78,766** 🏆 | — | 867 (0.01x) |
467
+ | FC17 Report Server ID | **79,185** 🏆 | — | — |
468
+ | FC22 Mask Write Register | **73,060** 🏆 | — | — |
469
+ | FC23 Read/Write Multiple Registers | **68,429** 🏆 | — | — |
470
+ | FC43 Read Device Identification | **62,176** 🏆 | — | 868 (0.01x) |
471
+
472
+ > FC17 / FC22 / FC23 / FC43 are not supported by jsmodbus.
398
473
 
399
474
  ## License
400
475