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
@@ -0,0 +1,380 @@
1
+ # njs-modbus
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ Node.js 的纯 JavaScript Modbus 实现。
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
+ ## 特性
15
+
16
+ - **协议:** Modbus TCP、RTU、ASCII
17
+ - **传输:** 串口、TCP 客户端/服务端、UDP 客户端/服务端
18
+ - **主站:** FIFO 或流水线并发请求(仅 TCP)
19
+ - **从站:** 每连接 FIFO 或并发处理(仅 TCP)
20
+ - **自定义功能码**,支持可插拔帧解析
21
+ - **广播**(单元号 = 0)
22
+ - **完整 TypeScript**
23
+ - **零运行时依赖**(仅在使用串口时 peer-depends `serialport`)
24
+
25
+ | 码值 | 名称 |
26
+ |------|------|
27
+ | 01 | 读取线圈 |
28
+ | 02 | 读取离散输入 |
29
+ | 03 | 读取保持寄存器 |
30
+ | 04 | 读取输入寄存器 |
31
+ | 05 | 写单个线圈 |
32
+ | 06 | 写单个寄存器 |
33
+ | 15 | 写多个线圈 |
34
+ | 16 | 写多个寄存器 |
35
+ | 17 | 报告服务器 ID |
36
+ | 22 | 掩码写寄存器 |
37
+ | 23 | 读/写多个寄存器 |
38
+ | 43/14 | 读取设备标识 |
39
+
40
+ ## 安装
41
+
42
+ ```bash
43
+ npm install njs-modbus
44
+ ```
45
+
46
+ 使用串口传输时,还需安装 peer dependency:
47
+
48
+ ```bash
49
+ npm install serialport
50
+ ```
51
+
52
+ ## 快速开始
53
+
54
+ ### TCP 主站
55
+
56
+ ```typescript
57
+ import { ModbusMaster } from 'njs-modbus';
58
+
59
+ const master = new ModbusMaster({
60
+ physical: { type: 'TCP_CLIENT' },
61
+ protocol: { type: 'TCP' },
62
+ });
63
+
64
+ await master.open({ port: 502, host: '192.168.1.10' });
65
+
66
+ const res = await master.readHoldingRegisters(1, 0, 10);
67
+ console.log(res.data);
68
+
69
+ await master.close();
70
+ ```
71
+
72
+ ### TCP 从站
73
+
74
+ ```typescript
75
+ import { ModbusSlave } from 'njs-modbus';
76
+
77
+ const slave = new ModbusSlave({
78
+ physical: { type: 'TCP_SERVER' },
79
+ protocol: { type: 'TCP' },
80
+ });
81
+
82
+ slave.add({
83
+ unit: 1,
84
+ readHoldingRegisters: (address, length) => {
85
+ return Array.from({ length }, (_, i) => address + i);
86
+ },
87
+ });
88
+
89
+ await slave.open({ port: 502 });
90
+ ```
91
+
92
+ ### 串口 RTU 主站
93
+
94
+ ```typescript
95
+ const master = new ModbusMaster({
96
+ physical: {
97
+ type: 'SERIAL',
98
+ opts: { path: '/dev/ttyUSB0', baudRate: 9600 },
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
+ ## 物理层
118
+
119
+ 所有物理层暴露 `open()` / `close()`、`state` 属性,以及事件:`'open'`、`'connect'`、`'close'`、`'error'`。
120
+
121
+ | 类型 | 类 | `open(...)` 参数 |
122
+ |------|-------|-----------------|
123
+ | `SERIAL` | `SerialPhysicalLayer` | 无 |
124
+ | `TCP_CLIENT` | `TcpClientPhysicalLayer` | `SocketConnectOpts` |
125
+ | `TCP_SERVER` | `TcpServerPhysicalLayer` | `ListenOptions` |
126
+ | `UDP_CLIENT` | `UdpClientPhysicalLayer` | `{ port, address }` |
127
+ | `UDP_SERVER` | `UdpServerPhysicalLayer` | `BindOptions` |
128
+ | `CUSTOM` | *(用户自定义)* | *(自定义)* |
129
+
130
+ ### 服务端配置
131
+
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 自动规范化
138
+ maxConnections: 10,
139
+ idleTimeout: 30000, // 驱逐不活跃连接,0 = 禁用
140
+ },
141
+ },
142
+ protocol: { type: 'TCP' },
143
+ });
144
+ ```
145
+
146
+ ### 物理层选项
147
+
148
+ | 选项 | 类型 | 说明 |
149
+ |--------|------|-------------|
150
+ | `whitelist` | `string[]` | 允许的客户端 IP。`::ffff:` 前缀自动剥离。 |
151
+ | `maxConnections` | `number` | 最大并发连接数。超出后新连接静默丢弃。 |
152
+ | `idleTimeout` | `number` | 空闲超时(毫秒),超时后驱逐连接。默认 `30000`,传 `0` 禁用。 |
153
+ | `socketOpts` / `serverOpts` | `object` | 透传给 Node.js `createSocket()` / `createServer()`。 |
154
+
155
+ ## 主站 API
156
+
157
+ ```typescript
158
+ new ModbusMaster({
159
+ physical: { type: 'TCP_CLIENT' },
160
+ protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
161
+ timeout?: 1000, // 单次请求超时(毫秒)
162
+ concurrent?: false, // 流水线模式(仅 TCP)
163
+ })
164
+ ```
165
+
166
+ RTU 帧选项:
167
+
168
+ ```typescript
169
+ protocol: {
170
+ type: 'RTU',
171
+ opts: {
172
+ // 既可以传裸数字(毫秒),也可以传 `{ unit: 'bit' | 'ms', value: N }`。
173
+ // 传 `0` 显式禁用该定时器(适用于 RTU-over-TCP 等无丢包传输)。
174
+ intervalBetweenFrames: 20, // 20 毫秒
175
+ interCharTimeout: { unit: 'bit', value: 10 }, // 按 bit-time,需 baudRate
176
+ poolSize: 1024,
177
+ },
178
+ }
179
+ ```
180
+
181
+ ASCII 选项:
182
+
183
+ ```typescript
184
+ protocol: {
185
+ type: 'ASCII',
186
+ opts: {
187
+ lenientHex: true, // 接受小写十六进制 (a-f)。默认:false(按规范仅大写)
188
+ },
189
+ }
190
+ ```
191
+
192
+ ### 方法
193
+
194
+ | 方法 | 说明 |
195
+ |--------|-------------|
196
+ | `open(...args)` | 打开物理层。只能调用一次;`close()` 后实例永久失效。 |
197
+ | `close()` | 立即关闭。进行中和队列中的请求会被 reject。 |
198
+ | `readCoils(unit, address, length, timeout?)` | FC 01 |
199
+ | `readDiscreteInputs(unit, address, length, timeout?)` | FC 02 |
200
+ | `readHoldingRegisters(unit, address, length, timeout?)` | FC 03 |
201
+ | `readInputRegisters(unit, address, length, timeout?)` | FC 04 |
202
+ | `writeSingleCoil(unit, address, value, timeout?)` | FC 05 |
203
+ | `writeSingleRegister(unit, address, value, timeout?)` | FC 06 |
204
+ | `writeMultipleCoils(unit, address, values, timeout?)` | FC 15 |
205
+ | `writeMultipleRegisters(unit, address, values, timeout?)` | FC 16 |
206
+ | `reportServerId(unit, serverIdLength?, timeout?)` | FC 17 |
207
+ | `maskWriteRegister(unit, address, andMask, orMask, timeout?)` | FC 22 |
208
+ | `readAndWriteMultipleRegisters(unit, read, write, timeout?)` | FC 23 |
209
+ | `readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout?)` | FC 43/14 |
210
+ | `sendCustomFC(unit, fc, data, timeout?)` | 发送用户自定义功能码 |
211
+ | `addCustomFunctionCode(cfc)` | 注册自定义功能码用于帧解析 |
212
+ | `removeCustomFunctionCode(fc)` | 注销自定义功能码 |
213
+
214
+ 广播(`unit = 0`)resolve `void` — 从站不响应。
215
+
216
+ ## 从站 API
217
+
218
+ ```typescript
219
+ new ModbusSlave({
220
+ physical: { type: 'TCP_SERVER' },
221
+ protocol: { type: 'TCP' },
222
+ concurrent?: false, // 每连接并发(仅 TCP)
223
+ })
224
+ ```
225
+
226
+ ### 注册模型
227
+
228
+ ```typescript
229
+ slave.add({
230
+ unit: 1,
231
+
232
+ // 可选:在默认分发前拦截任意功能码
233
+ interceptor?: (fc, data) => Buffer | undefined,
234
+
235
+ readCoils?: (address, length) => boolean[],
236
+ readDiscreteInputs?: (address, length) => boolean[],
237
+ readHoldingRegisters?: (address, length) => number[],
238
+ readInputRegisters?: (address, length) => number[],
239
+
240
+ writeSingleCoil?: (address, value) => void,
241
+ writeMultipleCoils?: (address, values) => void,
242
+
243
+ writeSingleRegister?: (address, value) => void,
244
+ writeMultipleRegisters?: (address, values) => void,
245
+
246
+ maskWriteRegister?: (address, andMask, orMask) => void,
247
+
248
+ reportServerId?: () => ServerId,
249
+ readDeviceIdentification?: () => { [index: number]: string },
250
+
251
+ // 可选地址范围校验
252
+ getAddressRange?: () => ({
253
+ discreteInputs?: [number, number] | [number, number][];
254
+ coils?: [number, number] | [number, number][];
255
+ inputRegisters?: [number, number] | [number, number][];
256
+ holdingRegisters?: [number, number] | [number, number][];
257
+ }),
258
+ });
259
+ ```
260
+
261
+ 缺失的处理器返回 `ILLEGAL_FUNCTION`。越界地址返回 `ILLEGAL_DATA_ADDRESS`。处理器抛出的异常变为 `SERVER_DEVICE_FAILURE`,除非错误是 `ModbusError`。
262
+
263
+ | 方法 | 说明 |
264
+ |--------|-------------|
265
+ | `add(model)` | 注册从站模型 |
266
+ | `remove(unit)` | 移除从站模型 |
267
+ | `open(...args)` | 打开物理层。一次性:close() 后不可重新打开。 |
268
+ | `close()` | 立即关闭 |
269
+ | `addCustomFunctionCode(cfc)` | 注册自定义功能码 |
270
+ | `removeCustomFunctionCode(fc)` | 注销自定义功能码 |
271
+
272
+ ## 自定义功能码
273
+
274
+ ```typescript
275
+ import type { CustomFunctionCode } from 'njs-modbus';
276
+
277
+ const cfc: CustomFunctionCode = {
278
+ fc: 0x50,
279
+ predictRequestLength: (buffer) => {
280
+ if (buffer.length < 2) return null;
281
+ return 4 + buffer[1];
282
+ },
283
+ predictResponseLength: (buffer) => {
284
+ if (buffer.length < 2) return null;
285
+ return 4 + buffer[1];
286
+ },
287
+ handle: async (data, unit) => {
288
+ return Buffer.from([data[1], data[0]]);
289
+ },
290
+ };
291
+
292
+ // 从站:帧解析 + 分发
293
+ slave.addCustomFunctionCode(cfc);
294
+
295
+ // 主站:仅帧解析
296
+ master.addCustomFunctionCode(cfc);
297
+ const response = await master.sendCustomFC(1, 0x50, [0xab, 0xcd]);
298
+ ```
299
+
300
+ ## 错误处理
301
+
302
+ ```typescript
303
+ import { ErrorCode, ModbusError, getErrorByCode } from 'njs-modbus';
304
+
305
+ // ErrorCode.ILLEGAL_FUNCTION = 0x01
306
+ // ErrorCode.ILLEGAL_DATA_ADDRESS = 0x02
307
+ // ErrorCode.ILLEGAL_DATA_VALUE = 0x03
308
+ // ErrorCode.SERVER_DEVICE_FAILURE = 0x04
309
+ // ErrorCode.ACKNOWLEDGE = 0x05
310
+ // ErrorCode.SERVER_DEVICE_BUSY = 0x06
311
+ // ErrorCode.MEMORY_PARITY_ERROR = 0x08
312
+ // ErrorCode.GATEWAY_PATH_UNAVAILABLE = 0x0a
313
+ // ErrorCode.GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0b
314
+
315
+ slave.add({
316
+ unit: 1,
317
+ readHoldingRegisters: (address) => {
318
+ if (address > 100) {
319
+ throw getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS);
320
+ }
321
+ return [42];
322
+ },
323
+ });
324
+ ```
325
+
326
+ ## 性能
327
+
328
+ 与 [jsmodbus](https://github.com/Cloud-Automation/node-modbus) 和 [modbus-serial](https://github.com/yaacov/node-modbus-serial) 的对比。
329
+
330
+ | 指标 | njs-modbus | jsmodbus | modbus-serial |
331
+ |------|-----------|----------|---------------|
332
+ | TCP 吞吐量 | **5,527 ops/sec** | 3,239 (0.59x) | 371 (0.07x) |
333
+ | TCP P99 延迟 | **2.71 ms** | 4.73 ms (1.75x) | 12.48 ms (4.61x) |
334
+ | TCP CPU 效率 | **1,116 µs/op** | 1,950 (1.75x) | 16,715 (14.98x) |
335
+ | 并发 (8 连接) | **5,274 ops/sec** | 3,416 (0.65x) | 1,815 (0.34x) |
336
+ | RTU CPU 效率 | **1,762 µs/op** | 1,760 (1.00x) | 2,144 (1.22x) |
337
+ | TCP 响应编码 | **2.04M ops/sec** | 373K (0.18x) | 411K (0.20x) |
338
+ | TCP 响应解码 | **1.91M ops/sec** | 538K (0.28x) | 231K (0.12x) |
339
+
340
+ <details>
341
+ <summary>完整基准测试结果</summary>
342
+
343
+ Node.js v24.15.0 · linux x64 · 3 次运行 × 300 秒 · [完整报告](./benchmark/RESULTS.md)
344
+
345
+ ### TCP 吞吐量(单连接)
346
+
347
+ ```
348
+ njs-modbus │ 5,527 ops/sec 🏆 CPU: 1,116 µs/op
349
+ jsmodbus │ 3,239 ops/sec (0.59x) CPU: 1,950 µs/op
350
+ modbus-serial │ 371 ops/sec (0.07x) CPU: 16,715 µs/op
351
+ ```
352
+
353
+ ### 并发(8 连接)
354
+
355
+ ```
356
+ njs-modbus │ 5,274 ops/sec 🏆 CPU: 1,392 µs/op
357
+ jsmodbus │ 3,416 ops/sec (0.65x) CPU: 2,131 µs/op
358
+ modbus-serial │ 1,815 ops/sec (0.34x) CPU: 3,962 µs/op
359
+ ```
360
+
361
+ ### RTU 串口(模拟 115200 波特率)
362
+
363
+ ```
364
+ njs-modbus │ 44 ops/sec CPU: 1,762 µs/op
365
+ jsmodbus │ 44 ops/sec CPU: 1,760 µs/op
366
+ modbus-serial │ 44 ops/sec CPU: 2,144 µs/op
367
+ ```
368
+
369
+ ### 编码 / 解码(CPU 微基准测试)
370
+
371
+ ```
372
+ tcpResEncode: njs-modbus 2.04M ops/sec (jsmodbus 0.18x, modbus-serial 0.20x)
373
+ tcpResDecode: njs-modbus 1.91M ops/sec (jsmodbus 0.28x, modbus-serial 0.12x)
374
+ ```
375
+
376
+ </details>
377
+
378
+ ## 许可证
379
+
380
+ [![license](https://img.shields.io/github/license/xiejay97/njs-modbus?style=flat-square)](/LICENSE)