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.md CHANGED
@@ -1,476 +1,412 @@
1
1
  # njs-modbus
2
2
 
3
- English | [简体中文](./README.zh-CN.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
- A pure JavaScript implementation of Modbus for Node.js, optimized for throughput and low GC pressure.
8
+ **English** | [中文](README.zh-CN.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
+ > 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.
13
11
 
14
- ## Highlights
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.
15
13
 
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)
21
- - **Custom function codes** with pluggable framing
22
- - **Broadcasting** (`unit = 0`)
23
- - **Full TypeScript**
24
- - **Zero runtime dependencies** (peer-depends on `serialport` for serial)
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.
25
15
 
26
- ## Supported Function Codes
16
+ ---
27
17
 
28
- | Code | Name |
29
- |------|------|
30
- | 01 | Read Coils |
31
- | 02 | Read Discrete Inputs |
32
- | 03 | Read Holding Registers |
33
- | 04 | Read Input Registers |
34
- | 05 | Write Single Coil |
35
- | 06 | Write Single Register |
36
- | 15 | Write Multiple Coils |
37
- | 16 | Write Multiple Registers |
38
- | 17 | Report Server ID |
39
- | 22 | Mask Write Register |
40
- | 23 | Read/Write Multiple Registers |
41
- | 43/14 | Read Device Identification |
18
+ ## Table of Contents
42
19
 
43
- ## Installation
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)
44
34
 
45
- Requires **Node.js ≥ 18.19**.
35
+ ---
46
36
 
47
- ```bash
48
- npm install njs-modbus
49
- ```
37
+ ## What is njs-modbus?
50
38
 
51
- For serial transport, also install the peer dependency:
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.
52
40
 
53
- ```bash
54
- npm install serialport
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
+ └─────────────────────────────────────────────┘
55
52
  ```
56
53
 
57
- ## Quick Start
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.
58
57
 
59
- ### TCP Master
58
+ ---
60
59
 
61
- ```typescript
62
- import { ModbusMaster } from 'njs-modbus';
60
+ ## Why njs-modbus?
63
61
 
64
- const master = new ModbusMaster({
65
- physical: { type: 'TCP_CLIENT' },
66
- protocol: { type: 'TCP' },
67
- });
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. |
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
+ ## Feature Matrix
73
74
 
74
- await master.close();
75
- ```
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 | ✅ | ✅ | ✅ |
76
83
 
77
- ### TCP Slave
84
+ \* RTU custom function codes require a `determineFrameLength` callback so the framing state machine can know the frame length without buffering.
78
85
 
79
- ```typescript
80
- import { ModbusSlave } from 'njs-modbus';
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.
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
+ ## Installation
93
91
 
94
- await slave.open({ port: 502 });
92
+ ```bash
93
+ npm install njs-modbus
95
94
  ```
96
95
 
97
- ### Serial RTU Master
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
+ Serial support is provided through an optional peer dependency:
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
+ Requires 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
- ## Architecture
106
+ ## Quick Start
123
107
 
124
- The library follows a strict two-layer design:
108
+ ### TCP Master
125
109
 
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
- ```
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
- Every transport is modeled as connection-oriented: physical layers emit `'connect'` with an `AbstractPhysicalConnection`, and a dedicated application layer instance is created per connection.
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
- ## Physical Layer
142
+ ### TCP Slave
148
143
 
149
- All physical layers expose `open()` / `close()`, a `state` property, and the events documented in [Events](#events).
144
+ ```typescript
145
+ import { ModbusSlave, TcpServerPhysicalLayer } from 'njs-modbus';
150
146
 
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 |
147
+ const physical = new TcpServerPhysicalLayer();
159
148
 
160
- ### TCP Server options
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 normalized automatically
168
- maxConnections: 10,
169
- idleTimeout: 30000, // evict inactive connections, 0 = disable
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
- ### Physical layer option reference
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
- | Option | Type | Description |
179
- |--------|------|-------------|
180
- | `whitelist` | `string[]` | Allowed client IPs. `::ffff:` prefix is stripped automatically. |
181
- | `maxConnections` | `number` | Max concurrent connections. New connections are silently dropped when exceeded. |
182
- | `idleTimeout` | `number` | Idle timeout in ms before evicting a connection. Default `30000`. Pass `0` to disable. |
183
- | `socketOpts` / `serverOpts` | `object` | Forwarded to Node.js `createSocket()` / `createServer()`. |
177
+ ### RTU over Serial
184
178
 
185
- ## Protocol Layer
179
+ ```typescript
180
+ import { ModbusMaster, SerialPhysicalLayer } from 'njs-modbus';
186
181
 
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 |
182
+ const physical = new SerialPhysicalLayer();
192
183
 
193
- ### RTU options
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 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,
209
- },
210
- }
211
- ```
192
+ const res = await master.readHoldingRegisters(1, 0, 10);
193
+ console.log('registers:', res.data);
212
194
 
213
- ### ASCII options
195
+ master.destroy();
196
+ physical.close();
197
+ });
214
198
 
215
- ```typescript
216
- protocol: {
217
- type: 'ASCII',
218
- opts: {
219
- lenientHex: true, // accept lowercase hex (a-f). Default: false (strict uppercase per spec)
220
- },
221
- }
199
+ physical.open({ path: '/dev/ttyUSB0', baudRate: 115200 });
222
200
  ```
223
201
 
224
- ## Master API
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.
225
203
 
226
- ### Constructor
204
+ ---
227
205
 
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
- ```
206
+ ## Architecture
236
207
 
237
- ### Methods
238
-
239
- | Method | Description |
240
- |--------|-------------|
241
- | `open(...args)` | Open the physical layer. One-shot: cannot reopen after `close()`. |
242
- | `close()` | Close immediately. In-flight and queued requests are rejected. |
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?)` | Send a user-defined function code |
256
- | `addCustomFunctionCode(cfc)` | Register custom FC for framing |
257
- | `removeCustomFunctionCode(fc)` | Unregister custom FC |
258
-
259
- Broadcast (`unit = 0`) resolves `void` — slaves never respond.
260
-
261
- ## Slave API
262
-
263
- ### Constructor
208
+ The library is organized into four layers. Each layer is independent, testable in isolation, and replaceable.
264
209
 
265
- ```typescript
266
- new ModbusSlave({
267
- physical: { type: 'TCP_SERVER' },
268
- protocol: { type: 'TCP' },
269
- concurrent?: false, // per-connection concurrent (TCP only)
270
- })
271
- ```
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` |
272
216
 
273
- ### Register a model
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.
274
218
 
275
- ```typescript
276
- slave.add({
277
- unit: 1,
219
+ ---
278
220
 
279
- // Optional: intercept any FC before default dispatch
280
- interceptor?: (fc, data) => Buffer | undefined,
221
+ ## Core Capabilities
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
+ ### Low-Allocation Codec Hot Paths
286
224
 
287
- writeSingleCoil?: (address, value) => void,
288
- writeMultipleCoils?: (address, values) => void,
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.
289
226
 
290
- writeSingleRegister?: (address, value) => void,
291
- writeMultipleRegisters?: (address, values) => void,
227
+ ### Streaming Frame Recovery
292
228
 
293
- maskWriteRegister?: (address, andMask, orMask) => void,
229
+ The framing layers parse incoming bytes as a stream. They recover from:
294
230
 
295
- reportServerId?: () => ServerId,
296
- readDeviceIdentification?: () => { [index: number]: string },
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).
297
236
 
298
- // Optional address range validation
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
+ Valid frames are emitted; invalid data is discarded without corrupting adjacent frames.
307
238
 
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`.
239
+ ### Queue Strategies
309
240
 
310
- ### Methods
241
+ Both `ModbusMaster` and `ModbusSlave` support four queue strategies:
311
242
 
312
- | Method | Description |
313
- |--------|-------------|
314
- | `add(model)` | Register a slave model |
315
- | `remove(unit)` | Remove a model |
316
- | `open(...args)` | Open and begin accepting connections. One-shot. |
317
- | `close()` | Close immediately |
318
- | `addCustomFunctionCode(cfc)` | Register custom FC |
319
- | `removeCustomFunctionCode(fc)` | Unregister custom FC |
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. |
320
249
 
321
- ## Events
250
+ `drop-stale` is the default.
322
251
 
323
- Both `ModbusMaster` and `ModbusSlave` are `EventEmitter`s. All events are typed.
252
+ ### Per-Unit Write Range Lock
324
253
 
325
- ### Physical layer events
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.
326
255
 
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 |
256
+ ### Access Control and Audit
333
257
 
334
- ### Connection debug events
258
+ Install an `AccessAuthorizer` on either master or slave to enforce policies at three gates:
335
259
 
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 |
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.
340
263
 
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.) |
264
+ Each hook may return `true`, `false`, or a numeric Modbus exception `ErrorCode`. On the slave, denied requests emit `accessAudit` events.
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
- ## Custom Function Codes
278
+ ### Custom Function Codes
359
279
 
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.
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.
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; // need more bytes
369
- return 4 + getByte(1);
283
+ slave.addCustomFunctionCode(
284
+ { fc: 0x65 },
285
+ (unit, fc, data, callback) => {
286
+ // produce response PDU bytes
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
- // Slave: framing + dispatch
381
- slave.addCustomFunctionCode(cfc);
382
-
383
- // Master: framing only
384
- master.addCustomFunctionCode(cfc);
385
- const response = await master.sendCustomFC(1, 0x50, [0xab, 0xcd]);
289
+ );
386
290
  ```
387
291
 
388
- ## Error Handling
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.
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
- ## Custom Physical Layer
296
+ ## Supported Function Codes
415
297
 
416
- Any byte-oriented transport can be used by implementing `AbstractPhysicalLayer` and `AbstractPhysicalConnection`, then passing `{ type: 'CUSTOM', layer: yourLayer }`.
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 | ✅ | ✅ |
313
+
314
+ ---
315
+
316
+ ## Benchmarks
317
+
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.
319
+
320
+ ### Codec Micro-Benchmark
321
+
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.
323
+
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 |
338
+
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.
340
+
341
+ ### End-to-End Transport Throughput
342
+
343
+ FC 03 (read 50 holding registers) over loopback TCP and a 115200-baud `socat` PTY pair for serial transports.
344
+
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** |
357
+
358
+ ### Chaos Resilience
359
+
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.
361
+
362
+ | Protocol | Scenarios passed |
363
+ | --- | ---: |
364
+ | TCP | 12 / 12 |
365
+ | RTU | 12 / 12 |
366
+ | ASCII | 14 / 14 |
367
+
368
+ Re-run the full suite locally:
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
+ ## Security & Compliance
429
377
 
430
- See [`examples/websocket`](./examples/websocket) for a complete **Modbus TCP over WebSocket** implementation.
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`:
431
379
 
432
- See [`examples/bluetooth`](./examples/bluetooth) for a **Modbus TCP over BLE** implementation using the Nordic UART Service (NUS).
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.
433
383
 
434
- ## Performance
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.
435
385
 
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.
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.
437
387
 
438
- Environment: AMD Ryzen 7 9800X3D 8-Core Processor · Node.js v24.16.0 · [full report](./benchmark/report_presentation.md)
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.
439
390
 
440
- ### End-to-end TCP
391
+ See `examples/security/` for runnable master/slave examples, including TLS and transport-layer security options.
441
392
 
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) |
393
+ ---
447
394
 
448
- ### Frame encode / decode (CPU micro-benchmark)
395
+ ## Commercial Support & License
449
396
 
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) |
397
+ `njs-modbus` is released under the [Business Source License 1.1 (BSL 1.1)](LICENSE).
458
398
 
459
- ### Per-function-code TCP throughput (100 coils / 50 registers)
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.
460
402
 
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) |
403
+ A commercial license removes BSL restrictions for your product and our support offerings help you ship with confidence:
471
404
 
472
- > FC17 / FC22 / FC23 / FC43 are not supported by jsmodbus.
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.
473
408
 
474
- ## License
409
+ For licensing terms, pricing, and support options, please contact us:
475
410
 
476
- [![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)