njs-modbus 2.1.0 → 3.1.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.
Files changed (62) hide show
  1. package/README.md +324 -147
  2. package/README.zh-CN.md +380 -0
  3. package/dist/index.cjs +2552 -2288
  4. package/dist/index.d.ts +400 -233
  5. package/dist/index.mjs +2548 -2287
  6. package/dist/src/error-code.d.ts +2 -24
  7. package/dist/src/layers/application/abstract-application-layer.d.ts +13 -8
  8. package/dist/src/layers/application/ascii-application-layer.d.ts +9 -11
  9. package/dist/src/layers/application/rtu-application-layer.d.ts +20 -47
  10. package/dist/src/layers/application/tcp-application-layer.d.ts +9 -8
  11. package/dist/src/layers/physical/abstract-physical-layer.d.ts +43 -14
  12. package/dist/src/layers/physical/index.d.ts +9 -3
  13. package/dist/src/layers/physical/serial-physical-layer.d.ts +43 -15
  14. package/dist/src/layers/physical/tcp-client-physical-layer.d.ts +13 -12
  15. package/dist/src/layers/physical/tcp-physical-connection.d.ts +16 -0
  16. package/dist/src/layers/physical/tcp-server-physical-layer.d.ts +24 -14
  17. package/dist/src/layers/physical/udp-client-physical-layer.d.ts +33 -0
  18. package/dist/src/layers/physical/udp-server-physical-layer.d.ts +50 -0
  19. package/dist/src/layers/physical/utils.d.ts +39 -0
  20. package/dist/src/layers/physical/vars.d.ts +11 -0
  21. package/dist/src/master/master-session.d.ts +3 -3
  22. package/dist/src/master/master.d.ts +55 -19
  23. package/dist/src/slave/slave.d.ts +58 -33
  24. package/dist/src/types.d.ts +2 -2
  25. package/dist/src/utils/callback.d.ts +8 -0
  26. package/dist/src/utils/crc.d.ts +1 -1
  27. package/dist/src/utils/index.d.ts +7 -4
  28. package/dist/src/utils/predictRtuFrameLength.d.ts +13 -16
  29. package/dist/src/utils/promisify-cb.d.ts +4 -0
  30. package/dist/src/utils/rtu-timing.d.ts +63 -0
  31. package/dist/src/utils/whitelist.d.ts +11 -0
  32. package/dist/src/vars.d.ts +2 -0
  33. package/package.json +15 -8
  34. package/dist/src/layers/physical/udp-physical-layer.d.ts +0 -42
  35. package/dist/src/utils/genConnectionId.d.ts +0 -2
  36. package/dist/test/adu-buffer.test.d.ts +0 -1
  37. package/dist/test/ascii-hex-sentry.test.d.ts +0 -1
  38. package/dist/test/ascii-hex-validation.test.d.ts +0 -1
  39. package/dist/test/ascii-tcp-fragmentation.test.d.ts +0 -1
  40. package/dist/test/check-range.test.d.ts +0 -1
  41. package/dist/test/fallback-atomic.test.d.ts +0 -1
  42. package/dist/test/fallback-serial.test.d.ts +0 -1
  43. package/dist/test/fc17-serverid-validation.test.d.ts +0 -1
  44. package/dist/test/fc43-conformity.test.d.ts +0 -1
  45. package/dist/test/fc43-utf8-objects.test.d.ts +0 -1
  46. package/dist/test/gen-connection-id.test.d.ts +0 -1
  47. package/dist/test/helpers/raw-tcp.d.ts +0 -38
  48. package/dist/test/master-concurrent.test.d.ts +0 -1
  49. package/dist/test/modbus-error.test.d.ts +0 -1
  50. package/dist/test/physical-lifecycle.test.d.ts +0 -1
  51. package/dist/test/predict-rtu.test.d.ts +0 -1
  52. package/dist/test/rtu-custom-fc.test.d.ts +0 -1
  53. package/dist/test/rtu-pool-overflow.test.d.ts +0 -1
  54. package/dist/test/rtu-t15-timing.test.d.ts +0 -1
  55. package/dist/test/rtu-t35-default.test.d.ts +0 -1
  56. package/dist/test/rtu-t35-strict.test.d.ts +0 -1
  57. package/dist/test/rtu-tcp-fragmentation.test.d.ts +0 -1
  58. package/dist/test/serial-e2e.test.d.ts +0 -1
  59. package/dist/test/slave-multi-connection.test.d.ts +0 -1
  60. package/dist/test/slave.test.d.ts +0 -1
  61. package/dist/test/tcp-fragmentation.test.d.ts +0 -1
  62. package/dist/test/udp-multi-client.test.d.ts +0 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # njs-modbus
2
2
 
3
- A pure JavaScript implementation of MODBUS for NodeJS.
3
+ English | [简体中文](./README.zh-CN.md)
4
4
 
5
- <div>
5
+ A pure JavaScript implementation of Modbus for Node.js.
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,194 +11,371 @@ A pure JavaScript implementation of MODBUS for NodeJS.
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
- </div>
15
-
16
- ## Introduction
17
-
18
- `njs-modbus` is designed as a layered architecture, including the physical layer and the application layer:
19
-
20
- - Physical layer implements Serial Port, TCP/IP and UDP/IP.
21
- - Application layer implements RTU, ASCII and TCP.
22
-
23
- `njs-modbus` provide both client and server.
24
-
25
14
  ## Features
26
15
 
27
- - Full modbus standard protocol implementation
28
- - Support for custom function codes
29
- - Support broadcasting
30
- - Very lightweight project
31
- - Full typescript
32
-
33
- ### Supported function codes
34
-
35
- | Code | |
36
- | ----- | ----------------------------- |
37
- | 01 | Read Coils |
38
- | 02 | Read Discrete Inputs |
39
- | 03 | Read Holding Registers |
40
- | 04 | Read Input Register |
41
- | 05 | Write Single Coil |
42
- | 06 | Write Single Register |
43
- | 15 | Write Multiple Coils |
44
- | 16 | Write Multiple Registers |
45
- | 17 | Report Server ID |
46
- | 22 | Mask Write Register |
47
- | 23 | Read/Write Multiple Registers |
48
- | 43/14 | Read device Identification |
49
-
50
- ### Supported protocols
51
-
52
- - Modbus RTU
53
- - Modbus ASCII
54
- - Modbus TCP/IP
55
- - Modbus UDP/IP
56
- - Modbus RTU/ASCII Over TCP/IP
57
- - Modbus RTU/ASCII Over UDP/IP
58
-
59
- #### Installation
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)
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 |
39
+
40
+ ## Installation
60
41
 
61
42
  ```bash
62
43
  npm install njs-modbus
63
44
  ```
64
45
 
65
- ## Examples
46
+ For serial transport, also install the peer dependency:
66
47
 
67
- ### Modbus RTU Master
48
+ ```bash
49
+ npm install serialport
50
+ ```
51
+
52
+ ## Quick Start
53
+
54
+ ### TCP Master
68
55
 
69
56
  ```typescript
70
- import { SerialPhysicalLayer, RtuApplicationLayer, ModbusMaster } from 'njs-modbus';
57
+ import { ModbusMaster } from 'njs-modbus';
71
58
 
72
- const physicalLayer = new SerialPhysicalLayer({ path: 'COM1', baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1 });
73
- const applicationLayer = new RtuApplicationLayer(physicalLayer);
59
+ const master = new ModbusMaster({
60
+ physical: { type: 'TCP_CLIENT' },
61
+ protocol: { type: 'TCP' },
62
+ });
74
63
 
75
- const modbusMaster = new ModbusMaster(applicationLayer, physicalLayer);
64
+ await master.open({ port: 502, host: '192.168.1.10' });
76
65
 
77
- modbusMaster
78
- .open()
79
- .then(() => {
80
- console.log('opened');
81
- modbusMaster.readHoldingRegisters(1, 0, 10).then((res) => {
82
- console.log(res);
83
- });
84
- })
85
- .catch((error) => {
86
- console.log(error);
87
- });
66
+ const res = await master.readHoldingRegisters(1, 0, 10);
67
+ console.log(res.data);
68
+
69
+ await master.close();
88
70
  ```
89
71
 
90
- ### Modbus RTU Slave
72
+ ### TCP Slave
91
73
 
92
74
  ```typescript
93
- import type { ModbusSlaveModel } from 'njs-modbus';
94
- import { SerialPhysicalLayer, RtuApplicationLayer, ModbusSlave } from 'njs-modbus';
75
+ import { ModbusSlave } from 'njs-modbus';
95
76
 
96
- const physicalLayer = new SerialPhysicalLayer({ path: 'COM1', baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1 });
97
- const applicationLayer = new RtuApplicationLayer(physicalLayer);
77
+ const slave = new ModbusSlave({
78
+ physical: { type: 'TCP_SERVER' },
79
+ protocol: { type: 'TCP' },
80
+ });
98
81
 
99
- const slave1Data = {
100
- discreteInputs: new Map<number, boolean>(),
101
- coils: new Map<number, boolean>(),
102
- inputRegisters: new Map<number, number>(),
103
- holdingRegisters: new Map<number, number>(),
104
- };
105
- const slave1: ModbusSlaveModel = {
106
- readDiscreteInputs: (address, length) => {
107
- return Array.from({ length }).map((_, i) => {
108
- const discreteInput = slave1Data.discreteInputs.get(address + i);
109
- if (typeof discreteInput === 'undefined') {
110
- return false;
111
- }
112
- return discreteInput;
113
- });
82
+ slave.add({
83
+ unit: 1,
84
+ readHoldingRegisters: (address, length) => {
85
+ return Array.from({ length }, (_, i) => address + i);
114
86
  },
87
+ });
115
88
 
116
- readCoils: (address, length) => {
117
- return Array.from({ length }).map((_, i) => {
118
- const coil = slave1Data.coils.get(address + i);
119
- if (typeof coil === 'undefined') {
120
- return false;
121
- }
122
- return coil;
123
- });
124
- },
125
- writeSingleCoil: (address, value) => {
126
- slave1Data.coils.set(address, value);
89
+ await slave.open({ port: 502 });
90
+ ```
91
+
92
+ ### Serial RTU Master
93
+
94
+ ```typescript
95
+ const master = new ModbusMaster({
96
+ physical: {
97
+ type: 'SERIAL',
98
+ opts: { path: '/dev/ttyUSB0', baudRate: 9600 },
127
99
  },
100
+ protocol: { type: 'RTU' },
101
+ });
102
+
103
+ await master.open();
104
+ const res = await master.readHoldingRegisters(1, 0, 10);
105
+ await master.close();
106
+ ```
107
+
108
+ ### RTU over TCP
109
+
110
+ ```typescript
111
+ const master = new ModbusMaster({
112
+ physical: { type: 'TCP_CLIENT' },
113
+ protocol: { type: 'RTU' },
114
+ });
115
+ ```
116
+
117
+ ## Physical Layers
118
+
119
+ All physical layers expose `open()` / `close()`, a `state` property, and events: `'open'`, `'connect'`, `'close'`, `'error'`.
120
+
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)* |
129
+
130
+ ### Server options
128
131
 
129
- readInputRegisters: (address, length) => {
130
- return Array.from({ length }).map((_, i) => {
131
- const inputRegister = slave1Data.inputRegisters.get(address + i);
132
- if (typeof inputRegister === 'undefined') {
133
- return 0;
134
- }
135
- return inputRegister;
136
- });
132
+ ```typescript
133
+ const slave = new ModbusSlave({
134
+ physical: {
135
+ type: 'TCP_SERVER',
136
+ opts: {
137
+ whitelist: ['192.168.1.10', '10.0.0.5'], // IPv4-mapped IPv6 normalized automatically
138
+ maxConnections: 10,
139
+ idleTimeout: 30000, // evict inactive connections, 0 = disable
140
+ },
137
141
  },
142
+ protocol: { type: 'TCP' },
143
+ });
144
+ ```
138
145
 
139
- readHoldingRegisters: (address, length) => {
140
- return Array.from({ length }).map((_, i) => {
141
- const holdingRegister = slave1Data.holdingRegisters.get(address + i);
142
- if (typeof holdingRegister === 'undefined') {
143
- return 0;
144
- }
145
- return holdingRegister;
146
- });
146
+ ### Physical layer options
147
+
148
+ | Option | Type | Description |
149
+ |--------|------|-------------|
150
+ | `whitelist` | `string[]` | Allowed client IPs. `::ffff:` prefix is stripped automatically. |
151
+ | `maxConnections` | `number` | Max concurrent connections. New connections are silently dropped when exceeded. |
152
+ | `idleTimeout` | `number` | Idle timeout in ms before evicting a connection. Default `30000`. Pass `0` to disable. |
153
+ | `socketOpts` / `serverOpts` | `object` | Forwarded to Node.js `createSocket()` / `createServer()`. |
154
+
155
+ ## Master API
156
+
157
+ ```typescript
158
+ new ModbusMaster({
159
+ physical: { type: 'TCP_CLIENT' },
160
+ protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
161
+ timeout?: 1000, // per-request timeout (ms)
162
+ concurrent?: false, // pipelined mode (TCP only)
163
+ })
164
+ ```
165
+
166
+ RTU framing options:
167
+
168
+ ```typescript
169
+ protocol: {
170
+ type: 'RTU',
171
+ opts: {
172
+ // Either a bare number (milliseconds), or `{ unit: 'bit' | 'ms', value: N }`.
173
+ // Use `0` to disable the timer entirely (useful for lossless transports
174
+ // such as RTU-over-TCP).
175
+ intervalBetweenFrames: 20, // 20 ms
176
+ interCharTimeout: { unit: 'bit', value: 10 }, // bit-time, needs baudRate
177
+ poolSize: 1024,
147
178
  },
148
- writeSingleRegister: (address, value) => {
149
- slave1Data.holdingRegisters.set(address, value);
179
+ }
180
+ ```
181
+
182
+ ASCII options:
183
+
184
+ ```typescript
185
+ protocol: {
186
+ type: 'ASCII',
187
+ opts: {
188
+ lenientHex: true, // accept lowercase hex (a-f). Default: false (strict uppercase per spec)
150
189
  },
190
+ }
191
+ ```
192
+
193
+ ### Methods
194
+
195
+ | Method | Description |
196
+ |--------|-------------|
197
+ | `open(...args)` | Open the physical layer. Can only be called once; after `close()` the instance is dead. |
198
+ | `close()` | Close immediately. In-flight and queued requests are rejected. |
199
+ | `readCoils(unit, address, length, timeout?)` | FC 01 |
200
+ | `readDiscreteInputs(unit, address, length, timeout?)` | FC 02 |
201
+ | `readHoldingRegisters(unit, address, length, timeout?)` | FC 03 |
202
+ | `readInputRegisters(unit, address, length, timeout?)` | FC 04 |
203
+ | `writeSingleCoil(unit, address, value, timeout?)` | FC 05 |
204
+ | `writeSingleRegister(unit, address, value, timeout?)` | FC 06 |
205
+ | `writeMultipleCoils(unit, address, values, timeout?)` | FC 15 |
206
+ | `writeMultipleRegisters(unit, address, values, timeout?)` | FC 16 |
207
+ | `reportServerId(unit, serverIdLength?, timeout?)` | FC 17 |
208
+ | `maskWriteRegister(unit, address, andMask, orMask, timeout?)` | FC 22 |
209
+ | `readAndWriteMultipleRegisters(unit, read, write, timeout?)` | FC 23 |
210
+ | `readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout?)` | FC 43/14 |
211
+ | `sendCustomFC(unit, fc, data, timeout?)` | Send a user-defined function code |
212
+ | `addCustomFunctionCode(cfc)` | Register custom FC for framing |
213
+ | `removeCustomFunctionCode(fc)` | Unregister custom FC |
214
+
215
+ Broadcast (`unit = 0`) resolves `void` — slaves never respond.
216
+
217
+ ## Slave API
218
+
219
+ ```typescript
220
+ new ModbusSlave({
221
+ physical: { type: 'TCP_SERVER' },
222
+ protocol: { type: 'TCP' },
223
+ concurrent?: false, // per-connection concurrent (TCP only)
224
+ })
225
+ ```
226
+
227
+ ### Register a model
228
+
229
+ ```typescript
230
+ slave.add({
231
+ unit: 1,
232
+
233
+ // Optional: intercept any FC before default dispatch
234
+ interceptor?: (fc, data) => Buffer | undefined,
235
+
236
+ readCoils?: (address, length) => boolean[],
237
+ readDiscreteInputs?: (address, length) => boolean[],
238
+ readHoldingRegisters?: (address, length) => number[],
239
+ readInputRegisters?: (address, length) => number[],
240
+
241
+ writeSingleCoil?: (address, value) => void,
242
+ writeMultipleCoils?: (address, values) => void,
151
243
 
152
- reportServerId: () => ({ additionalData: [1, 2, 3] }),
153
-
154
- readDeviceIdentification: () => ({
155
- 0x00: 'Basic:VendorName',
156
- 0x01: 'Basic:ProductCode',
157
- 0x02: 'Basic:MajorMinorRevision',
158
- 0x03: 'Regular:VendorUrl',
159
- 0x04: 'Regular:ProductName',
160
- 0x05: 'Regular:ModelName',
161
- 0x06: 'Regular:UserApplicationName',
162
- 0x80: 'Extended:Extended',
163
- 0xff: 'Extended:Extended',
244
+ writeSingleRegister?: (address, value) => void,
245
+ writeMultipleRegisters?: (address, values) => void,
246
+
247
+ maskWriteRegister?: (address, andMask, orMask) => void,
248
+
249
+ reportServerId?: () => ServerId,
250
+ readDeviceIdentification?: () => { [index: number]: string },
251
+
252
+ // Optional address range validation
253
+ getAddressRange?: () => ({
254
+ discreteInputs?: [number, number] | [number, number][];
255
+ coils?: [number, number] | [number, number][];
256
+ inputRegisters?: [number, number] | [number, number][];
257
+ holdingRegisters?: [number, number] | [number, number][];
164
258
  }),
259
+ });
260
+ ```
261
+
262
+ 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`.
263
+
264
+ | Method | Description |
265
+ |--------|-------------|
266
+ | `add(model)` | Register a slave model |
267
+ | `remove(unit)` | Remove a slave model |
268
+ | `open(...args)` | Open the physical layer. One-shot: cannot reopen after `close()`. |
269
+ | `close()` | Close immediately |
270
+ | `addCustomFunctionCode(cfc)` | Register a custom FC |
271
+ | `removeCustomFunctionCode(fc)` | Unregister a custom FC |
272
+
273
+ ## Custom Function Codes
274
+
275
+ ```typescript
276
+ import type { CustomFunctionCode } from 'njs-modbus';
277
+
278
+ const cfc: CustomFunctionCode = {
279
+ fc: 0x50,
280
+ predictRequestLength: (buffer) => {
281
+ if (buffer.length < 2) return null;
282
+ return 4 + buffer[1];
283
+ },
284
+ predictResponseLength: (buffer) => {
285
+ if (buffer.length < 2) return null;
286
+ return 4 + buffer[1];
287
+ },
288
+ handle: async (data, unit) => {
289
+ return Buffer.from([data[1], data[0]]);
290
+ },
165
291
  };
166
292
 
167
- const modbusSlave = new ModbusSlave(applicationLayer, physicalLayer);
168
- modbusSlave.add(slave1);
293
+ // Slave: framing + dispatch
294
+ slave.addCustomFunctionCode(cfc);
169
295
 
170
- modbusSlave
171
- .open()
172
- .then(() => {
173
- console.log('opened');
174
- })
175
- .catch((error) => {
176
- console.log(error);
177
- });
296
+ // Master: framing only
297
+ master.addCustomFunctionCode(cfc);
298
+ const response = await master.sendCustomFC(1, 0x50, [0xab, 0xcd]);
178
299
  ```
179
300
 
180
- For more advanced examples, check out [examples](/examples) included in the repository. If you have created any utilities that meet a specific need, feel free to submit them so others can benefit.
301
+ ## Error Handling
181
302
 
182
- ## Broadcasts (unit = 0)
303
+ ```typescript
304
+ import { ErrorCode, ModbusError, getErrorByCode } from 'njs-modbus';
305
+
306
+ // ErrorCode.ILLEGAL_FUNCTION = 0x01
307
+ // ErrorCode.ILLEGAL_DATA_ADDRESS = 0x02
308
+ // ErrorCode.ILLEGAL_DATA_VALUE = 0x03
309
+ // ErrorCode.SERVER_DEVICE_FAILURE = 0x04
310
+ // ErrorCode.ACKNOWLEDGE = 0x05
311
+ // ErrorCode.SERVER_DEVICE_BUSY = 0x06
312
+ // ErrorCode.MEMORY_PARITY_ERROR = 0x08
313
+ // ErrorCode.GATEWAY_PATH_UNAVAILABLE = 0x0a
314
+ // ErrorCode.GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0b
315
+
316
+ slave.add({
317
+ unit: 1,
318
+ readHoldingRegisters: (address) => {
319
+ if (address > 100) {
320
+ throw getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS);
321
+ }
322
+ return [42];
323
+ },
324
+ });
325
+ ```
183
326
 
184
- Slaves never respond to broadcast requests, so the master's `write*(0, ...)` Promise resolves as soon as the bytes are flushed to the wire.
327
+ ## Performance
185
328
 
186
- If you broadcast over serial (RTU or ASCII), per Modbus over Serial Line V1.02 §2.4.1 you must wait a **turnaround delay** before sending the next request — slow slaves need time to apply the broadcast write that produced no response. The library does not insert this delay automatically because the right value is workload-specific (fast sensors vs. PLCs writing to flash differ by orders of magnitude). Insert it yourself in your call site:
329
+ Benchmarked against [jsmodbus](https://github.com/Cloud-Automation/node-modbus) and [modbus-serial](https://github.com/yaacov/node-modbus-serial).
187
330
 
188
- ```typescript
189
- const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
331
+ | Metric | njs-modbus | jsmodbus | modbus-serial |
332
+ |--------|-----------|----------|---------------|
333
+ | TCP Throughput | **5,527 ops/sec** | 3,239 (0.59x) | 371 (0.07x) |
334
+ | TCP P99 Latency | **2.71 ms** | 4.73 ms (1.75x) | 12.48 ms (4.61x) |
335
+ | TCP CPU Efficiency | **1,116 µs/op** | 1,950 (1.75x) | 16,715 (14.98x) |
336
+ | Concurrent (8 conn) | **5,274 ops/sec** | 3,416 (0.65x) | 1,815 (0.34x) |
337
+ | RTU CPU Efficiency | **1,762 µs/op** | 1,760 (1.00x) | 2,144 (1.22x) |
338
+ | TCP Res Encode | **2.04M ops/sec** | 373K (0.18x) | 411K (0.20x) |
339
+ | TCP Res Decode | **1.91M ops/sec** | 538K (0.28x) | 231K (0.12x) |
340
+
341
+ <details>
342
+ <summary>Full benchmark results</summary>
343
+
344
+ Node.js v24.15.0 · linux x64 · 3 runs × 300 s · [full report](./benchmark/RESULTS.md)
190
345
 
191
- await master.writeSingleRegister(0, 0x0000, 0x1234); // broadcast
192
- await sleep(100); // turnaround — tune per devices
193
- await master.writeSingleRegister(1, 0x0000, 0x5678); // unicast to next slave
346
+ ### TCP Throughput (Single Connection)
347
+
348
+ ```
349
+ njs-modbus │ 5,527 ops/sec 🏆 CPU: 1,116 µs/op
350
+ jsmodbus │ 3,239 ops/sec (0.59x) CPU: 1,950 µs/op
351
+ modbus-serial │ 371 ops/sec (0.07x) CPU: 16,715 µs/op
194
352
  ```
195
353
 
196
- A safe lower bound is the RTU t3.5 inter-frame silence (e.g. ~4 ms at 9600 baud, ~1.75 ms above 19200 baud). Many real-world PLCs need 50–100 ms after a broadcast write. Modbus TCP/UDP do not require this delay (TCP gives synchronous acks; broadcasting on TCP is uncommon anyway).
354
+ ### Concurrent (8 Connections)
197
355
 
198
- ## Contributing
356
+ ```
357
+ njs-modbus │ 5,274 ops/sec 🏆 CPU: 1,392 µs/op
358
+ jsmodbus │ 3,416 ops/sec (0.65x) CPU: 2,131 µs/op
359
+ modbus-serial │ 1,815 ops/sec (0.34x) CPU: 3,962 µs/op
360
+ ```
361
+
362
+ ### RTU Serial (115200 baud simulated)
363
+
364
+ ```
365
+ njs-modbus │ 44 ops/sec CPU: 1,762 µs/op
366
+ jsmodbus │ 44 ops/sec CPU: 1,760 µs/op
367
+ modbus-serial │ 44 ops/sec CPU: 2,144 µs/op
368
+ ```
369
+
370
+ ### Encode / Decode (CPU Micro-benchmark)
371
+
372
+ ```
373
+ tcpResEncode: njs-modbus 2.04M ops/sec (jsmodbus 0.18x, modbus-serial 0.20x)
374
+ tcpResDecode: njs-modbus 1.91M ops/sec (jsmodbus 0.28x, modbus-serial 0.12x)
375
+ ```
199
376
 
200
- Please read our [contributing guide](/CODE_OF_CONDUCT.md) first.
377
+ </details>
201
378
 
202
379
  ## License
203
380
 
204
- [![gitHub license](https://img.shields.io/github/license/xiejay97/njs-modbus?style=flat-square)](/LICENSE)
381
+ [![license](https://img.shields.io/github/license/xiejay97/njs-modbus?style=flat-square)](/LICENSE)