njs-modbus 3.3.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.md CHANGED
@@ -1,430 +1,412 @@
1
1
  # njs-modbus
2
2
 
3
- English | [简体中文](./README.zh-CN.md)
4
-
5
- A pure JavaScript implementation of Modbus for Node.js.
6
-
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 -->
13
-
14
- ## Features
15
-
16
- - **Protocols:** Modbus TCP, RTU, ASCII
17
- - **Transports:** Serial, TCP client/server, UDP client/server
18
- - **Master:** FIFO or pipelined concurrent requests (protocol must be TCP)
19
- - **Slave:** Per-connection FIFO or concurrent processing (protocol must be TCP)
20
- - **Custom function codes** with pluggable framing
21
- - **Broadcasting** (unit = 0)
22
- - **Full TypeScript**
23
- - **Zero runtime dependencies** (peer-depends on `serialport` for serial)
24
-
25
- | Code | Name |
26
- |------|------|
27
- | 01 | Read Coils |
28
- | 02 | Read Discrete Inputs |
29
- | 03 | Read Holding Registers |
30
- | 04 | Read Input Registers |
31
- | 05 | Write Single Coil |
32
- | 06 | Write Single Register |
33
- | 15 | Write Multiple Coils |
34
- | 16 | Write Multiple Registers |
35
- | 17 | Report Server ID |
36
- | 22 | Mask Write Register |
37
- | 23 | Read/Write Multiple Registers |
38
- | 43/14 | Read Device Identification |
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/)
39
7
 
40
- ## Installation
8
+ **English** | [中文](README.zh-CN.md)
9
+
10
+ > A production-grade, zero-GC Modbus protocol stack for Node.js — TCP, RTU, and ASCII over TCP, UDP, TLS, serial, or any custom transport you can model as a byte pipeline.
11
+
12
+ `njs-modbus` is written in strict TypeScript and targets Node.js `>=18.19`. Its design is heavily informed by industrial field conditions: deterministic latency, streaming frame recovery, programmable access control, and audit logging are built in from the start.
13
+
14
+ Licensed under the **Business Source License 1.1 (BSL 1.1)**. Free for development, testing, and production use by individuals, educational institutions, non-profits, and companies with annual gross revenue below US$1M. A proprietary commercial license is available for larger organizations. Every version transitions to **Apache-2.0** on its Change Date.
15
+
16
+ ---
17
+
18
+ ## Table of Contents
41
19
 
42
- Requires **Node.js 18.19**.
20
+ - [What is njs-modbus?](#what-is-njs-modbus)
21
+ - [Why njs-modbus?](#why-njs-modbus)
22
+ - [Feature Matrix](#feature-matrix)
23
+ - [Installation](#installation)
24
+ - [Quick Start](#quick-start)
25
+ - [TCP Master](#tcp-master)
26
+ - [TCP Slave](#tcp-slave)
27
+ - [RTU over Serial](#rtu-over-serial)
28
+ - [Architecture](#architecture)
29
+ - [Core Capabilities](#core-capabilities)
30
+ - [Supported Function Codes](#supported-function-codes)
31
+ - [Benchmarks](#benchmarks)
32
+ - [Security & Compliance](#security--compliance)
33
+ - [Commercial Support & License](#commercial-support--license)
34
+
35
+ ---
36
+
37
+ ## What is njs-modbus?
38
+
39
+ `njs-modbus` is a layered Modbus protocol library for Node.js. The protocol layer speaks only in buffers and knows nothing about the underlying physical device. That means the same master/slave logic runs over TCP, UDP, TLS, serial, WebSocket, an in-memory mock, or any other transport — you implement the wire once behind the `AbstractPipelineAdapter` interface and reuse the full protocol stack unchanged.
40
+
41
+ ```
42
+ ┌─────────────────────────────────────────────┐
43
+ │ Application: ModbusMaster / ModbusSlave │
44
+ ├─────────────────────────────────────────────┤
45
+ │ Protocol framing: TCP / RTU / ASCII │
46
+ ├─────────────────────────────────────────────┤
47
+ │ Pipeline: AbstractPipelineAdapter │
48
+ ├─────────────────────────────────────────────┤
49
+ │ Physical transport: TCP / UDP / TLS / │
50
+ │ Serial / WebSocket / custom │
51
+ └─────────────────────────────────────────────┘
52
+ ```
53
+
54
+ - **Strict TypeScript** — generic protocol literals (`'TCP' | 'RTU' | 'ASCII'`) and typed Promise APIs catch integration mistakes at compile time.
55
+ - **Zero-GC decode hot paths** — explicit finite-state-machine framing avoids allocating objects or buffers on the JS heap during steady-state operation.
56
+ - **Transport-agnostic** — one adapter interface, any wire.
57
+
58
+ ---
59
+
60
+ ## Why njs-modbus?
61
+
62
+ | Concern | What you get |
63
+ | --- | --- |
64
+ | **Deterministic latency** | GC-free decode paths, low-allocation encode, and sub-microsecond P50 codec latency. No garbage-collection pauses on the hot path. |
65
+ | **Production-hardened framing** | Streaming state machines recover from garbage bytes, sticky frames, truncation, cross-boundary chunks, and corrupted CRC/LRC — without leaking invalid data into adjacent frames. |
66
+ | **Type safety** | Strict TypeScript with typed Promise APIs; most integration mistakes surface at compile time. |
67
+ | **Access control & audit** | Policy hooks at unit, address, and runtime gates, plus structured `accessAudit` events on the slave for compliance and forensics. |
68
+ | **Transport freedom** | TCP, UDP, TLS, serial, or custom transports via `AbstractPipelineAdapter`. The protocol logic never changes. |
69
+ | **Commercial clarity** | BSL 1.1: free for individuals, non-profits, and small companies; commercial license available for larger organizations; Apache-2.0 after the Change Date. |
70
+
71
+ ---
72
+
73
+ ## Feature Matrix
74
+
75
+ | Capability | TCP | RTU | ASCII |
76
+ | --- | :---: | :---: | :---: |
77
+ | Master / client | ✅ | ✅ | ✅ |
78
+ | Slave / server | ✅ | ✅ | ✅ |
79
+ | Concurrent pipelining | ✅ | — | — |
80
+ | Broadcast (`unit === 0`) | ✅ | ✅ | ✅ |
81
+ | Custom function codes | ✅ | ✅* | ✅ |
82
+ | Streaming frame recovery | ✅ | ✅ | ✅ |
83
+
84
+ \* RTU custom function codes require a `determineFrameLength` callback so the framing state machine can know the frame length without buffering.
85
+
86
+ Because the protocol layer is transport-agnostic, any protocol (TCP / RTU / ASCII) can run over any transport for which you provide a pipeline adapter. Built-in transports include TCP, UDP, TLS (over TCP), and serial; the WebSocket example demonstrates a custom adapter.
87
+
88
+ ---
89
+
90
+ ## Installation
43
91
 
44
92
  ```bash
45
93
  npm install njs-modbus
46
94
  ```
47
95
 
48
- For serial transport, also install the peer dependency:
96
+ Serial support is provided through an optional peer dependency:
49
97
 
50
98
  ```bash
51
99
  npm install serialport
52
100
  ```
53
101
 
102
+ Requires Node.js `>=18.19`.
103
+
104
+ ---
105
+
54
106
  ## Quick Start
55
107
 
56
108
  ### TCP Master
57
109
 
58
110
  ```typescript
59
- import { ModbusMaster } from 'njs-modbus';
60
-
61
- const master = new ModbusMaster({
62
- physical: { type: 'TCP_CLIENT' },
63
- protocol: { type: 'TCP' },
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
+ }
64
132
  });
65
133
 
66
- await master.open({ port: 502, host: '192.168.1.10' });
67
-
68
- const res = await master.readHoldingRegisters(1, 0, 10);
69
- console.log(res.data);
70
-
71
- await master.close();
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
+ });
72
140
  ```
73
141
 
74
142
  ### TCP Slave
75
143
 
76
144
  ```typescript
77
- import { ModbusSlave } from 'njs-modbus';
145
+ import { ModbusSlave, TcpServerPhysicalLayer } from 'njs-modbus';
78
146
 
79
- const slave = new ModbusSlave({
80
- physical: { type: 'TCP_SERVER' },
81
- protocol: { type: 'TCP' },
82
- });
147
+ const physical = new TcpServerPhysicalLayer();
83
148
 
84
- slave.add({
85
- unit: 1,
86
- readHoldingRegisters: (address, length) => {
87
- return Array.from({ length }, (_, i) => address + i);
88
- },
149
+ physical.on('connect', (pipeline) => {
150
+ const slave = new ModbusSlave({
151
+ pipelineAdapter: pipeline,
152
+ protocol: { type: 'TCP' },
153
+ queueStrategy: 'drop-stale',
154
+ });
155
+
156
+ slave.addUnit(1, {
157
+ readHoldingRegisters: (address, length, callback) => {
158
+ const values = Array.from({ length }, (_, i) => (address + i) & 0xffff);
159
+ callback(null, values);
160
+ },
161
+ writeSingleRegister: (address, value, callback) => {
162
+ console.log(`write ${value} to ${address}`);
163
+ callback(null);
164
+ },
165
+ });
89
166
  });
90
167
 
91
- await slave.open({ port: 502 });
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
+ });
92
175
  ```
93
176
 
94
- ### Serial RTU Master
177
+ ### RTU over Serial
95
178
 
96
179
  ```typescript
97
- const master = new ModbusMaster({
98
- physical: {
99
- type: 'SERIAL',
100
- opts: { path: '/dev/ttyUSB0', baudRate: 9600 },
101
- },
102
- protocol: { type: 'RTU' },
103
- });
180
+ import { ModbusMaster, SerialPhysicalLayer } from 'njs-modbus';
104
181
 
105
- await master.open();
106
- const res = await master.readHoldingRegisters(1, 0, 10);
107
- await master.close();
108
- ```
182
+ const physical = new SerialPhysicalLayer();
109
183
 
110
- ### RTU over TCP
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
+ });
111
191
 
112
- ```typescript
113
- const master = new ModbusMaster({
114
- physical: { type: 'TCP_CLIENT' },
115
- protocol: { type: 'RTU' },
192
+ const res = await master.readHoldingRegisters(1, 0, 10);
193
+ console.log('registers:', res.data);
194
+
195
+ master.destroy();
196
+ physical.close();
116
197
  });
198
+
199
+ physical.open({ path: '/dev/ttyUSB0', baudRate: 115200 });
117
200
  ```
118
201
 
119
- ## Physical Layers
202
+ See [`examples/`](https://github.com/xiejay97/njs-modbus/tree/main/examples) for runnable master/slave pairs, including access control, audit logging, TLS, and custom transports such as WebSocket.
120
203
 
121
- All physical layers expose `open()` / `close()`, a `state` property, and events: `'open'`, `'connect'`, `'close'`, `'error'`.
204
+ ---
122
205
 
123
- | Type | Class | `open(...)` args |
124
- |------|-------|-----------------|
125
- | `SERIAL` | `SerialPhysicalLayer` | none |
126
- | `TCP_CLIENT` | `TcpClientPhysicalLayer` | `SocketConnectOpts` |
127
- | `TCP_SERVER` | `TcpServerPhysicalLayer` | `ListenOptions` |
128
- | `UDP_CLIENT` | `UdpClientPhysicalLayer` | `{ port, address }` |
129
- | `UDP_SERVER` | `UdpServerPhysicalLayer` | `BindOptions` |
130
- | `CUSTOM` | *(user-provided)* | *(user-defined)* |
206
+ ## Architecture
131
207
 
132
- ### Server options
208
+ The library is organized into four layers. Each layer is independent, testable in isolation, and replaceable.
133
209
 
134
- ```typescript
135
- const slave = new ModbusSlave({
136
- physical: {
137
- type: 'TCP_SERVER',
138
- opts: {
139
- whitelist: ['192.168.1.10', '10.0.0.5'], // IPv4-mapped IPv6 normalized automatically
140
- maxConnections: 10,
141
- idleTimeout: 30000, // evict inactive connections, 0 = disable
142
- },
143
- },
144
- protocol: { type: 'TCP' },
145
- });
146
- ```
210
+ | Layer | Responsibility | Public Contract |
211
+ | --- | --- | --- |
212
+ | **Physical** | Open/close the wire and emit a pipeline per connection. | `AbstractPhysicalLayer` |
213
+ | **Pipeline** | Move raw bytes, handle back-pressure, and expose a `write(data)` + `data` event surface. | `AbstractPipelineAdapter` / `AbstractPipelineLayer` |
214
+ | **Protocol** | Parse frames, validate CRC/LRC/MBAP, and emit complete ADUs. | `TcpProtocolLayer` / `RtuProtocolLayer` / `AsciiProtocolLayer` |
215
+ | **Application** | Orchestrate transactions, queues, access control, and expose the public Promise API. | `ModbusMaster` / `ModbusSlave` |
147
216
 
148
- ### Physical layer options
217
+ This separation is what makes custom transports trivial. The WebSocket example in [`examples/websocket/`](https://github.com/xiejay97/njs-modbus/tree/main/examples/websocket) implements a full pipeline layer in under 150 lines.
149
218
 
150
- | Option | Type | Description |
151
- |--------|------|-------------|
152
- | `whitelist` | `string[]` | Allowed client IPs. `::ffff:` prefix is stripped automatically. |
153
- | `maxConnections` | `number` | Max concurrent connections. New connections are silently dropped when exceeded. |
154
- | `idleTimeout` | `number` | Idle timeout in ms before evicting a connection. Default `30000`. Pass `0` to disable. |
155
- | `socketOpts` / `serverOpts` | `object` | Forwarded to Node.js `createSocket()` / `createServer()`. |
219
+ ---
156
220
 
157
- ## Custom Physical Layer
221
+ ## Core Capabilities
158
222
 
159
- Any byte-oriented transport can be used by implementing `AbstractPhysicalLayer` and `AbstractPhysicalConnection`, then passing `{ type: 'CUSTOM', layer: yourLayer }`.
223
+ ### Low-Allocation Codec Hot Paths
160
224
 
161
- ```typescript
162
- import { AbstractPhysicalLayer, AbstractPhysicalConnection } from 'njs-modbus';
225
+ The protocol framing layers for TCP, RTU, and ASCII are implemented as explicit finite state machines. Decode paths avoid allocating objects or buffers on the JavaScript heap during steady-state operation by using pre-allocated residual buffers and zero-copy views, which removes garbage-collection jitter from the protocol hot path. Encode paths perform a single bounded `Buffer.allocUnsafe()` per frame.
163
226
 
164
- class MyPhysicalLayer extends AbstractPhysicalLayer { /* open / close */ }
165
- class MyConnection extends AbstractPhysicalConnection { /* write / destroy, emit 'data' */ }
227
+ ### Streaming Frame Recovery
166
228
 
167
- const master = new ModbusMaster({
168
- physical: { type: 'CUSTOM', layer: new MyPhysicalLayer() },
169
- protocol: { type: 'TCP' },
170
- });
171
- ```
229
+ The framing layers parse incoming bytes as a stream. They recover from:
172
230
 
173
- See [`examples/websocket`](./examples/websocket) for a complete **Modbus TCP over WebSocket** implementation — client/server physical layers with a working demo.
231
+ - Garbage bytes injected on the wire.
232
+ - Multiple valid frames concatenated in one read (`sticky` frames).
233
+ - Truncated frames followed by valid frames.
234
+ - Cross-boundary chunks split across multiple reads.
235
+ - Corrupted CRC (RTU) or LRC (ASCII).
174
236
 
175
- 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.
237
+ Valid frames are emitted; invalid data is discarded without corrupting adjacent frames.
176
238
 
177
- ## Master API
239
+ ### Queue Strategies
178
240
 
179
- ```typescript
180
- new ModbusMaster({
181
- physical: { type: 'TCP_CLIENT' },
182
- protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
183
- timeout?: 1000, // per-request timeout (ms)
184
- concurrent?: false, // pipelined mode (protocol: TCP only)
185
- })
186
- ```
241
+ Both `ModbusMaster` and `ModbusSlave` support four queue strategies:
187
242
 
188
- RTU framing options:
243
+ | Strategy | Behavior | Best for |
244
+ | --- | --- | --- |
245
+ | `fifo` | Strict first-in-first-out execution. | Serial lines, deterministic ordering. |
246
+ | `drop-stale` | New requests clear all pending, unexecuted requests. | Telemetry collectors where only the latest value matters. |
247
+ | `deduplicate` | Pending requests with the same ADU fingerprint are dropped. | Polling loops that may overlap. |
248
+ | `concurrent` | Requests are dispatched concurrently. | Modbus TCP or multi-link masters/slaves. |
189
249
 
190
- ```typescript
191
- protocol: {
192
- type: 'RTU',
193
- opts: {
194
- // Either a bare number (milliseconds), or `{ unit: 'bit' | 'ms', value: N }`.
195
- // Use `0` to disable the timer entirely (useful for lossless transports
196
- // such as RTU-over-TCP).
197
- intervalBetweenFrames: 20, // 20 ms
198
- interCharTimeout: { unit: 'bit', value: 10 }, // bit-time, needs baudRate
199
- poolSize: 1024,
200
- },
201
- }
202
- ```
250
+ `drop-stale` is the default.
203
251
 
204
- ASCII options:
252
+ ### Per-Unit Write Range Lock
205
253
 
206
- ```typescript
207
- protocol: {
208
- type: 'ASCII',
209
- opts: {
210
- lenientHex: true, // accept lowercase hex (a-f). Default: false (strict uppercase per spec)
211
- },
212
- }
213
- ```
254
+ For the slave in `concurrent` mode, `enableWriteRangeLock` (default `true`) ensures that write requests (FC05/06/15/16/22/23) with overlapping address ranges on the same unit are serialized, preventing race conditions. This is critical for maintaining consistency when modifying shared registers or coils from multiple connections simultaneously. Set to `false` only for purely synchronous in-memory slaves that do not need the coordination overhead.
255
+
256
+ ### Access Control and Audit
214
257
 
215
- ### Methods
216
-
217
- | Method | Description |
218
- |--------|-------------|
219
- | `open(...args)` | Open the physical layer. Can only be called once; after `close()` the instance is dead. |
220
- | `close()` | Close immediately. In-flight and queued requests are rejected. |
221
- | `readCoils(unit, address, length, timeout?)` | FC 01 |
222
- | `readDiscreteInputs(unit, address, length, timeout?)` | FC 02 |
223
- | `readHoldingRegisters(unit, address, length, timeout?)` | FC 03 |
224
- | `readInputRegisters(unit, address, length, timeout?)` | FC 04 |
225
- | `writeSingleCoil(unit, address, value, timeout?)` | FC 05 |
226
- | `writeSingleRegister(unit, address, value, timeout?)` | FC 06 |
227
- | `writeMultipleCoils(unit, address, values, timeout?)` | FC 15 |
228
- | `writeMultipleRegisters(unit, address, values, timeout?)` | FC 16 |
229
- | `reportServerId(unit, serverIdLength?, timeout?)` | FC 17 |
230
- | `maskWriteRegister(unit, address, andMask, orMask, timeout?)` | FC 22 |
231
- | `readAndWriteMultipleRegisters(unit, read, write, timeout?)` | FC 23 |
232
- | `readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout?)` | FC 43/14 |
233
- | `sendCustomFC(unit, fc, data, timeout?)` | Send a user-defined function code |
234
- | `addCustomFunctionCode(cfc)` | Register custom FC for framing |
235
- | `removeCustomFunctionCode(fc)` | Unregister custom FC |
236
-
237
- Broadcast (`unit = 0`) resolves `void` — slaves never respond.
238
-
239
- ## Slave API
258
+ Install an `AccessAuthorizer` on either master or slave to enforce policies at three gates:
259
+
260
+ - `checkUnit` authorize the target unit address.
261
+ - `checkAddress` — authorize the address range touched by the request.
262
+ - `checkRuntime` last-chance authorization immediately before wire I/O.
263
+
264
+ Each hook may return `true`, `false`, or a numeric Modbus exception `ErrorCode`. On the slave, denied requests emit `accessAudit` events.
240
265
 
241
266
  ```typescript
242
- new ModbusSlave({
243
- physical: { type: 'TCP_SERVER' },
244
- protocol: { type: 'TCP' },
245
- concurrent?: false, // per-connection concurrent (protocol: TCP only)
246
- })
267
+ slave.setAccessAuthorizer({
268
+ checkUnit: (unit) => unit === 1,
269
+ checkAddress: (_unit, table, [start, end]) =>
270
+ table === 'holdingRegisters' && start >= 0 && end < 100,
271
+ });
272
+
273
+ slave.on('accessAudit', (event) => {
274
+ console.log('access denied:', event.type, event.message);
275
+ });
247
276
  ```
248
277
 
249
- ### Register a model
278
+ ### Custom Function Codes
250
279
 
251
- ```typescript
252
- slave.add({
253
- unit: 1,
280
+ Register non-standard function codes on the master or slave. The framing layer learns the request shape, and the application layer receives the raw PDU to parse and respond.
254
281
 
255
- // Optional: intercept any FC before default dispatch
256
- interceptor?: (fc, data) => Buffer | undefined,
282
+ ```typescript
283
+ slave.addCustomFunctionCode(
284
+ { fc: 0x65 },
285
+ (unit, fc, data, callback) => {
286
+ // produce response PDU bytes
287
+ callback(null, () => Buffer.from([0x00]));
288
+ },
289
+ );
290
+ ```
257
291
 
258
- readCoils?: (address, length) => boolean[] | Uint8Array,
259
- readDiscreteInputs?: (address, length) => boolean[] | Uint8Array,
260
- readHoldingRegisters?: (address, length) => number[] | Uint16Array,
261
- readInputRegisters?: (address, length) => number[] | Uint16Array,
292
+ For RTU (and ASCII when operating over byte-oriented transports), the descriptor must also provide `determineFrameLength` so the framing state machine can determine frame length without buffering.
262
293
 
263
- writeSingleCoil?: (address, value) => void,
264
- writeMultipleCoils?: (address, values) => void,
294
+ ---
265
295
 
266
- writeSingleRegister?: (address, value) => void,
267
- writeMultipleRegisters?: (address, values) => void,
296
+ ## Supported Function Codes
268
297
 
269
- maskWriteRegister?: (address, andMask, orMask) => void,
298
+ | FC | Name | Master | Slave |
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 | ✅ | ✅ |
270
313
 
271
- reportServerId?: () => ServerId,
272
- readDeviceIdentification?: () => { [index: number]: string },
314
+ ---
273
315
 
274
- // Optional address range validation
275
- getAddressRange?: () => ({
276
- discreteInputs?: [number, number] | [number, number][];
277
- coils?: [number, number] | [number, number][];
278
- inputRegisters?: [number, number] | [number, number][];
279
- holdingRegisters?: [number, number] | [number, number][];
280
- }),
281
- });
282
- ```
316
+ ## Benchmarks
283
317
 
284
- 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`.
318
+ All figures below were produced by the benchmark harness in this repository on an **AMD Ryzen 7 9800X3D** workstation running **Node.js v24.16.0**. See [`benchmark/report_presentation.md`](benchmark/report_presentation.md) for the full report, methodology, and reproducibility instructions.
285
319
 
286
- | Method | Description |
287
- |--------|-------------|
288
- | `add(model)` | Register a slave model |
289
- | `remove(unit)` | Remove a slave model |
290
- | `open(...args)` | Open the physical layer. One-shot: cannot reopen after `close()`. |
291
- | `close()` | Close immediately |
292
- | `addCustomFunctionCode(cfc)` | Register a custom FC |
293
- | `removeCustomFunctionCode(fc)` | Unregister a custom FC |
320
+ ### Codec Micro-Benchmark
294
321
 
295
- ## Custom Function Codes
322
+ Pure CPU encode/decode, no network I/O. Each op completes in sub-microsecond time, so `Ops/sec` and `CPU (µs/op)` are the reliable indicators; per-op latency at this scale is dominated by `process.hrtime` overhead and is omitted.
296
323
 
297
- `predictRequestLength` / `predictResponseLength` receive a shared pool buffer and byte offsets (`start`, `end`). Use `end - start` for bounds checks rather than `buffer.length`.
324
+ | Suite | Ops/sec | CPU (µs/op) |
325
+ | --- | ---: | ---: |
326
+ | TCP request encode | 9.37 M | 0.11 |
327
+ | TCP response encode | 7.83 M | 0.13 |
328
+ | TCP request decode | 8.83 M | 0.11 |
329
+ | TCP response decode | 8.90 M | 0.11 |
330
+ | RTU request encode | 9.12 M | 0.11 |
331
+ | RTU response encode | 1.91 M | 0.53 |
332
+ | RTU request decode | 8.53 M | 0.12 |
333
+ | RTU response decode | 1.98 M | 0.51 |
334
+ | ASCII request encode | 8.83 M | 0.11 |
335
+ | ASCII response encode | 2.44 M | 0.42 |
336
+ | ASCII request decode | 7.84 M | 0.13 |
337
+ | ASCII response decode | 2.53 M | 0.4 |
298
338
 
299
- ```typescript
300
- import type { CustomFunctionCode } from 'njs-modbus';
339
+ All codec suites report `0` ns/op of GC pressure because the decode hot paths do not allocate on the JavaScript heap during steady-state operation.
301
340
 
302
- const cfc: CustomFunctionCode = {
303
- fc: 0x50,
304
- predictRequestLength: (buffer, start, end) => {
305
- if (end - start < 2) return null;
306
- return 4 + buffer[start + 1];
307
- },
308
- predictResponseLength: (buffer, start, end) => {
309
- if (end - start < 2) return null;
310
- return 4 + buffer[start + 1];
311
- },
312
- handle: async (data, unit) => {
313
- return Buffer.from([data[1], data[0]]);
314
- },
315
- };
341
+ ### End-to-End Transport Throughput
316
342
 
317
- // Slave: framing + dispatch
318
- slave.addCustomFunctionCode(cfc);
343
+ FC 03 (read 50 holding registers) over loopback TCP and a 115200-baud `socat` PTY pair for serial transports.
319
344
 
320
- // Master: framing only
321
- master.addCustomFunctionCode(cfc);
322
- const response = await master.sendCustomFC(1, 0x50, [0xab, 0xcd]);
323
- ```
345
+ | Transport | Library | Ops/sec | P50 (µs) | P99 (µs) |
346
+ | --- | --- | ---: | ---: | ---: |
347
+ | TCP sequential | **njs-modbus** | **94.81 k** | **8.7** | **43.9** |
348
+ | TCP sequential | jsmodbus | 59.59 k | 13.6 | 65.7 |
349
+ | TCP sequential | modbus-serial | 867 | 1,150.5 | 1,244.5 |
350
+ | TCP 8 connections | **njs-modbus** | **109.23 k** | **60.7** | **179.8** |
351
+ | TCP 8 connections | jsmodbus | 63.84 k | 102.6 | 299.7 |
352
+ | TCP 8 connections | modbus-serial | 6.37 k | 1,241.3 | 1,437.6 |
353
+ | RTU sequential | **njs-modbus** | **104** | **514.5** | **852.8** |
354
+ | RTU sequential | jsmodbus | 104 | 546.4 | 927.3 |
355
+ | RTU sequential | modbus-serial | 31 | 31,903.6 | 32,219.3 |
356
+ | ASCII sequential | **njs-modbus** | **51** | **556.1** | **947.9** |
324
357
 
325
- ## Error Handling
358
+ ### Chaos Resilience
326
359
 
327
- ```typescript
328
- import { ErrorCode, ModbusError, getErrorByCode } from 'njs-modbus';
329
-
330
- // ErrorCode.ILLEGAL_FUNCTION = 0x01
331
- // ErrorCode.ILLEGAL_DATA_ADDRESS = 0x02
332
- // ErrorCode.ILLEGAL_DATA_VALUE = 0x03
333
- // ErrorCode.SERVER_DEVICE_FAILURE = 0x04
334
- // ErrorCode.ACKNOWLEDGE = 0x05
335
- // ErrorCode.SERVER_DEVICE_BUSY = 0x06
336
- // ErrorCode.MEMORY_PARITY_ERROR = 0x08
337
- // ErrorCode.GATEWAY_PATH_UNAVAILABLE = 0x0a
338
- // ErrorCode.GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0b
339
-
340
- slave.add({
341
- unit: 1,
342
- readHoldingRegisters: (address) => {
343
- if (address > 100) {
344
- throw getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS);
345
- }
346
- return [42];
347
- },
348
- });
349
- ```
360
+ The chaos suite injects corrupted, fragmented, sticky, and garbage-contaminated frames into live Modbus servers and verifies that valid frames are recovered without leakage.
350
361
 
351
- ## Performance
362
+ | Protocol | Scenarios passed |
363
+ | --- | ---: |
364
+ | TCP | 12 / 12 |
365
+ | RTU | 12 / 12 |
366
+ | ASCII | 14 / 14 |
352
367
 
353
- Benchmarked against [jsmodbus](https://github.com/Cloud-Automation/node-modbus) and [modbus-serial](https://github.com/yaacov/node-modbus-serial).
368
+ Re-run the full suite locally:
354
369
 
355
- | Metric | njs-modbus | jsmodbus | modbus-serial |
356
- |--------|-----------|----------|---------------|
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) |
370
+ ```bash
371
+ npm run benchmark:full
372
+ ```
364
373
 
365
- <details>
366
- <summary>Full benchmark results</summary>
374
+ ---
367
375
 
368
- Node.js v24.16.0 · AMD Ryzen 7 9800X3D · linux x64 · 3 runs × 120 s · [full report](./benchmark/RESULTS.md)
376
+ ## Security & Compliance
369
377
 
370
- ### TCP Throughput (Single Connection)
378
+ `njs-modbus` is a pure Modbus protocol stack. For regulated environments, it provides a protocol-level policy enforcement point through `AccessAuthorizer` and an auditable trail through `accessAudit` events on `ModbusSlave`:
371
379
 
372
- ```
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
376
- ```
380
+ - `checkUnit` — authorize the target unit address.
381
+ - `checkAddress` authorize the address range touched by the request.
382
+ - `checkRuntime` last-chance authorization immediately before wire I/O.
377
383
 
378
- ### Concurrent (8 Connections)
384
+ Denied requests emit structured `accessAudit` events that can be forwarded to SIEM or audit logs. This helps meet operational-technology (OT) security and compliance requirements without adding external proxies.
379
385
 
380
- ```
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
384
- ```
386
+ `njs-modbus` also ships a built-in **TLS transport plugin** (`TlsClientPhysicalLayer` / `TlsServerPhysicalLayer`) backed by `node:tls`, so encrypted Modbus TCP links and mutual TLS are available when you supply certificates and TLS options. Network identity beyond certificate validation, IP whitelisting, host hardening, and physical security remain the responsibility of the host application and infrastructure.
385
387
 
386
- ### RTU Serial (115200 baud via socat PTY)
388
+ - [`SECURITY.md`](SECURITY.md) vulnerability reporting, coordinated disclosure, and security update policy.
389
+ - [`docs/security/`](https://github.com/xiejay97/njs-modbus/tree/main/docs/security) — access control, audit events, TLS usage, deployment compensating controls, and SDL.
387
390
 
388
- ```
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
392
- ```
391
+ See `examples/security/` for runnable master/slave examples, including TLS and transport-layer security options.
393
392
 
394
- ### Frame Encode / Decode (CPU Micro-benchmark)
393
+ ---
395
394
 
396
- ```
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
405
- ```
395
+ ## Commercial Support & License
406
396
 
407
- ### Per-Function-Code TCP Throughput (Normal Payload)
397
+ `njs-modbus` is released under the [Business Source License 1.1 (BSL 1.1)](LICENSE).
408
398
 
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) |
399
+ - **Free production use** is granted for individuals, educational institutions, non-profit organizations, and companies with annual gross revenue below US$1,000,000.
400
+ - **Change Date**: 2029-06-24. On that date, this version transitions to the Apache License, Version 2.0.
401
+ - **Commercial license**: For OEMs, system integrators, and commercial products that need a predictable licensing path, guaranteed support, or cannot satisfy the BSL free-use conditions, we offer a separate proprietary commercial license.
423
402
 
424
- > FC17 / FC22 / FC23 are not supported by jsmodbus or modbus-serial.
403
+ A commercial license removes BSL restrictions for your product and our support offerings help you ship with confidence:
425
404
 
426
- </details>
405
+ - **Product integration license** — use `njs-modbus` in closed-source commercial products without copyleft obligations.
406
+ - **Professional technical support** — troubleshooting, performance tuning, migration guidance, and upgrade planning.
407
+ - **Enterprise support options** — response SLAs, long-term maintenance releases, priority bug fixes, and custom development.
427
408
 
428
- ## License
409
+ For licensing terms, pricing, and support options, please contact us:
429
410
 
430
- [![license](https://img.shields.io/github/license/xiejay97/njs-modbus?style=flat-square)](/LICENSE)
411
+ - Email: [xiejay97@gmail.com](mailto:xiejay97@gmail.com)
412
+ - GitHub Issues: [https://github.com/xiejay97/njs-modbus/issues](https://github.com/xiejay97/njs-modbus/issues)