njs-modbus 2.0.1 → 3.0.2

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 (56) hide show
  1. package/README.md +265 -154
  2. package/README.zh-CN.md +314 -0
  3. package/dist/index.cjs +1887 -1074
  4. package/dist/index.d.ts +369 -217
  5. package/dist/index.mjs +1884 -1073
  6. package/dist/src/error-code.d.ts +2 -24
  7. package/dist/src/layers/application/abstract-application-layer.d.ts +12 -7
  8. package/dist/src/layers/application/ascii-application-layer.d.ts +8 -9
  9. package/dist/src/layers/application/rtu-application-layer.d.ts +21 -44
  10. package/dist/src/layers/application/tcp-application-layer.d.ts +8 -6
  11. package/dist/src/layers/physical/abstract-physical-layer.d.ts +40 -12
  12. package/dist/src/layers/physical/index.d.ts +9 -3
  13. package/dist/src/layers/physical/serial-physical-layer.d.ts +43 -13
  14. package/dist/src/layers/physical/tcp-client-physical-layer.d.ts +12 -10
  15. package/dist/src/layers/physical/tcp-physical-connection.d.ts +17 -0
  16. package/dist/src/layers/physical/tcp-server-physical-layer.d.ts +23 -12
  17. package/dist/src/layers/physical/udp-client-physical-layer.d.ts +35 -0
  18. package/dist/src/layers/physical/udp-server-physical-layer.d.ts +54 -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.d.ts +45 -18
  22. package/dist/src/slave/slave.d.ts +57 -33
  23. package/dist/src/types.d.ts +3 -3
  24. package/dist/src/utils/index.d.ts +4 -2
  25. package/dist/src/utils/rtu-timing.d.ts +49 -0
  26. package/dist/src/utils/whitelist.d.ts +11 -0
  27. package/package.json +9 -7
  28. package/dist/src/layers/physical/udp-physical-layer.d.ts +0 -42
  29. package/dist/src/utils/genConnectionId.d.ts +0 -2
  30. package/dist/test/adu-buffer.test.d.ts +0 -1
  31. package/dist/test/ascii-hex-sentry.test.d.ts +0 -1
  32. package/dist/test/ascii-hex-validation.test.d.ts +0 -1
  33. package/dist/test/ascii-tcp-fragmentation.test.d.ts +0 -1
  34. package/dist/test/check-range.test.d.ts +0 -1
  35. package/dist/test/fallback-atomic.test.d.ts +0 -1
  36. package/dist/test/fallback-serial.test.d.ts +0 -1
  37. package/dist/test/fc17-serverid-validation.test.d.ts +0 -1
  38. package/dist/test/fc43-conformity.test.d.ts +0 -1
  39. package/dist/test/fc43-utf8-objects.test.d.ts +0 -1
  40. package/dist/test/gen-connection-id.test.d.ts +0 -1
  41. package/dist/test/helpers/raw-tcp.d.ts +0 -38
  42. package/dist/test/master-concurrent.test.d.ts +0 -1
  43. package/dist/test/modbus-error.test.d.ts +0 -1
  44. package/dist/test/physical-lifecycle.test.d.ts +0 -1
  45. package/dist/test/predict-rtu.test.d.ts +0 -1
  46. package/dist/test/rtu-custom-fc.test.d.ts +0 -1
  47. package/dist/test/rtu-pool-overflow.test.d.ts +0 -1
  48. package/dist/test/rtu-t15-timing.test.d.ts +0 -1
  49. package/dist/test/rtu-t35-default.test.d.ts +0 -1
  50. package/dist/test/rtu-t35-strict.test.d.ts +0 -1
  51. package/dist/test/rtu-tcp-fragmentation.test.d.ts +0 -1
  52. package/dist/test/serial-e2e.test.d.ts +0 -1
  53. package/dist/test/slave-multi-connection.test.d.ts +0 -1
  54. package/dist/test/slave.test.d.ts +0 -1
  55. package/dist/test/tcp-fragmentation.test.d.ts +0 -1
  56. package/dist/test/udp-multi-client.test.d.ts +0 -1
package/README.md CHANGED
@@ -1,203 +1,314 @@
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)
9
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
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)
11
12
  <!-- prettier-ignore-end -->
12
13
 
13
- </div>
14
-
15
- ## Introduction
16
-
17
- `njs-modbus` is designed as a layered architecture, including the physical layer and the application layer:
18
-
19
- - Physical layer implements Serial Port, TCP/IP and UDP/IP.
20
- - Application layer implements RTU, ASCII and TCP.
21
-
22
- `njs-modbus` provide both client and server.
23
-
24
14
  ## Features
25
15
 
26
- - Full modbus standard protocol implementation
27
- - Support for custom function codes
28
- - Support broadcasting
29
- - Very lightweight project
30
- - Full typescript
31
-
32
- ### Supported function codes
33
-
34
- | Code | |
35
- | ----- | ----------------------------- |
36
- | 01 | Read Coils |
37
- | 02 | Read Discrete Inputs |
38
- | 03 | Read Holding Registers |
39
- | 04 | Read Input Register |
40
- | 05 | Write Single Coil |
41
- | 06 | Write Single Register |
42
- | 15 | Write Multiple Coils |
43
- | 16 | Write Multiple Registers |
44
- | 17 | Report Server ID |
45
- | 22 | Mask Write Register |
46
- | 23 | Read/Write Multiple Registers |
47
- | 43/14 | Read device Identification |
48
-
49
- ### Supported protocols
50
-
51
- - Modbus RTU
52
- - Modbus ASCII
53
- - Modbus TCP/IP
54
- - Modbus UDP/IP
55
- - Modbus RTU/ASCII Over TCP/IP
56
- - Modbus RTU/ASCII Over UDP/IP
57
-
58
- #### 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
59
41
 
60
42
  ```bash
61
43
  npm install njs-modbus
62
44
  ```
63
45
 
64
- ## Examples
46
+ For serial transport, also install the peer dependency:
47
+
48
+ ```bash
49
+ npm install serialport
50
+ ```
51
+
52
+ ## Quick Start
65
53
 
66
- ### Modbus RTU Master
54
+ ### TCP Master
67
55
 
68
56
  ```typescript
69
- import { SerialPhysicalLayer, RtuApplicationLayer, ModbusMaster } from 'njs-modbus';
70
-
71
- const physicalLayer = new SerialPhysicalLayer({ path: 'COM1', baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1 });
72
- const applicationLayer = new RtuApplicationLayer(physicalLayer);
73
-
74
- const modbusMaster = new ModbusMaster(applicationLayer, physicalLayer);
75
-
76
- modbusMaster
77
- .open()
78
- .then(() => {
79
- console.log('opened');
80
- modbusMaster.readHoldingRegisters(1, 0, 10).then((res) => {
81
- console.log(res);
82
- });
83
- })
84
- .catch((error) => {
85
- console.log(error);
86
- });
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();
87
70
  ```
88
71
 
89
- ### Modbus RTU Slave
72
+ ### TCP Slave
90
73
 
91
74
  ```typescript
92
- import type { ModbusSlaveModel } from 'njs-modbus';
93
- import { SerialPhysicalLayer, RtuApplicationLayer, ModbusSlave } from 'njs-modbus';
75
+ import { ModbusSlave } from 'njs-modbus';
94
76
 
95
- const physicalLayer = new SerialPhysicalLayer({ path: 'COM1', baudRate: 9600, dataBits: 8, parity: 'none', stopBits: 1 });
96
- const applicationLayer = new RtuApplicationLayer(physicalLayer);
77
+ const slave = new ModbusSlave({
78
+ physical: { type: 'TCP_SERVER' },
79
+ protocol: { type: 'TCP' },
80
+ });
97
81
 
98
- const slave1Data = {
99
- discreteInputs: new Map<number, boolean>(),
100
- coils: new Map<number, boolean>(),
101
- inputRegisters: new Map<number, number>(),
102
- holdingRegisters: new Map<number, number>(),
103
- };
104
- const slave1: ModbusSlaveModel = {
105
- readDiscreteInputs: (address, length) => {
106
- return Array.from({ length }).map((_, i) => {
107
- const discreteInput = slave1Data.discreteInputs.get(address + i);
108
- if (typeof discreteInput === 'undefined') {
109
- return false;
110
- }
111
- return discreteInput;
112
- });
82
+ slave.add({
83
+ unit: 1,
84
+ readHoldingRegisters: (address, length) => {
85
+ return Array.from({ length }, (_, i) => address + i);
113
86
  },
87
+ });
114
88
 
115
- readCoils: (address, length) => {
116
- return Array.from({ length }).map((_, i) => {
117
- const coil = slave1Data.coils.get(address + i);
118
- if (typeof coil === 'undefined') {
119
- return false;
120
- }
121
- return coil;
122
- });
123
- },
124
- writeSingleCoil: (address, value) => {
125
- slave1Data.coils.set(address, value);
126
- },
89
+ await slave.open({ port: 502 });
90
+ ```
127
91
 
128
- readInputRegisters: (address, length) => {
129
- return Array.from({ length }).map((_, i) => {
130
- const inputRegister = slave1Data.inputRegisters.get(address + i);
131
- if (typeof inputRegister === 'undefined') {
132
- return 0;
133
- }
134
- return inputRegister;
135
- });
136
- },
92
+ ### Serial RTU Master
137
93
 
138
- readHoldingRegisters: (address, length) => {
139
- return Array.from({ length }).map((_, i) => {
140
- const holdingRegister = slave1Data.holdingRegisters.get(address + i);
141
- if (typeof holdingRegister === 'undefined') {
142
- return 0;
143
- }
144
- return holdingRegister;
145
- });
94
+ ```typescript
95
+ const master = new ModbusMaster({
96
+ physical: {
97
+ type: 'SERIAL',
98
+ opts: { path: '/dev/ttyUSB0', baudRate: 9600 },
146
99
  },
147
- writeSingleRegister: (address, value) => {
148
- slave1Data.holdingRegisters.set(address, value);
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
+
129
+ ### Server options
130
+
131
+ ```typescript
132
+ const slave = new ModbusSlave({
133
+ physical: {
134
+ type: 'TCP_SERVER',
135
+ opts: {
136
+ whitelist: ['192.168.1.10', '10.0.0.5'], // IPv4-mapped IPv6 normalized automatically
137
+ maxConnections: 10,
138
+ idleTimeout: 30000, // evict inactive connections, 0 = disable
139
+ },
149
140
  },
141
+ protocol: { type: 'TCP' },
142
+ });
143
+ ```
150
144
 
151
- reportServerId: () => ({ additionalData: [1, 2, 3] }),
152
-
153
- readDeviceIdentification: () => ({
154
- 0x00: 'Basic:VendorName',
155
- 0x01: 'Basic:ProductCode',
156
- 0x02: 'Basic:MajorMinorRevision',
157
- 0x03: 'Regular:VendorUrl',
158
- 0x04: 'Regular:ProductName',
159
- 0x05: 'Regular:ModelName',
160
- 0x06: 'Regular:UserApplicationName',
161
- 0x80: 'Extended:Extended',
162
- 0xff: 'Extended:Extended',
163
- }),
164
- };
145
+ ### Physical layer options
146
+
147
+ | Option | Type | Description |
148
+ |--------|------|-------------|
149
+ | `whitelist` | `string[]` | Allowed client IPs. `::ffff:` prefix is stripped automatically. |
150
+ | `maxConnections` | `number` | Max concurrent connections. New connections are silently dropped when exceeded. |
151
+ | `idleTimeout` | `number` | Idle timeout in ms before evicting a connection. Default `30000`. Pass `0` to disable. |
152
+ | `socketOpts` / `serverOpts` | `object` | Forwarded to Node.js `createSocket()` / `createServer()`. |
165
153
 
166
- const modbusSlave = new ModbusSlave(applicationLayer, physicalLayer);
167
- modbusSlave.add(slave1);
168
-
169
- modbusSlave
170
- .open()
171
- .then(() => {
172
- console.log('opened');
173
- })
174
- .catch((error) => {
175
- console.log(error);
176
- });
154
+ ## Master API
155
+
156
+ ```typescript
157
+ new ModbusMaster({
158
+ physical: { type: 'TCP_CLIENT' },
159
+ protocol: { type: 'TCP' }, // 'TCP' | 'RTU' | 'ASCII'
160
+ timeout?: 1000, // per-request timeout (ms)
161
+ concurrent?: false, // pipelined mode (TCP only)
162
+ })
177
163
  ```
178
164
 
179
- 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.
165
+ RTU / ASCII framing options:
180
166
 
181
- ## Broadcasts (unit = 0)
167
+ ```typescript
168
+ protocol: {
169
+ type: 'RTU',
170
+ opts: {
171
+ intervalBetweenFrames: { unit: 'ms', value: 20 },
172
+ interCharTimeout: { unit: 'bit', value: 10 },
173
+ poolSize: 1024,
174
+ },
175
+ }
176
+ ```
182
177
 
183
- Slaves never respond to broadcast requests, so the master's `write*(0, ...)` Promise resolves as soon as the bytes are flushed to the wire.
178
+ ### Methods
179
+
180
+ | Method | Description |
181
+ |--------|-------------|
182
+ | `open(...args)` | Open the physical layer. Can only be called once; after `close()` the instance is dead. |
183
+ | `close()` | Close immediately. In-flight and queued requests are rejected. |
184
+ | `readCoils(unit, address, length, timeout?)` | FC 01 |
185
+ | `readDiscreteInputs(unit, address, length, timeout?)` | FC 02 |
186
+ | `readHoldingRegisters(unit, address, length, timeout?)` | FC 03 |
187
+ | `readInputRegisters(unit, address, length, timeout?)` | FC 04 |
188
+ | `writeSingleCoil(unit, address, value, timeout?)` | FC 05 |
189
+ | `writeSingleRegister(unit, address, value, timeout?)` | FC 06 |
190
+ | `writeMultipleCoils(unit, address, values, timeout?)` | FC 15 |
191
+ | `writeMultipleRegisters(unit, address, values, timeout?)` | FC 16 |
192
+ | `reportServerId(unit, serverIdLength?, timeout?)` | FC 17 |
193
+ | `maskWriteRegister(unit, address, andMask, orMask, timeout?)` | FC 22 |
194
+ | `readAndWriteMultipleRegisters(unit, read, write, timeout?)` | FC 23 |
195
+ | `readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout?)` | FC 43/14 |
196
+ | `sendCustomFC(unit, fc, data, timeout?)` | Send a user-defined function code |
197
+ | `addCustomFunctionCode(cfc)` | Register custom FC for framing |
198
+ | `removeCustomFunctionCode(fc)` | Unregister custom FC |
199
+
200
+ Broadcast (`unit = 0`) resolves `void` — slaves never respond.
201
+
202
+ ## Slave API
184
203
 
185
- 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:
204
+ ```typescript
205
+ new ModbusSlave({
206
+ physical: { type: 'TCP_SERVER' },
207
+ protocol: { type: 'TCP' },
208
+ concurrent?: false, // per-connection concurrent (TCP only)
209
+ })
210
+ ```
211
+
212
+ ### Register a model
186
213
 
187
214
  ```typescript
188
- const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
215
+ slave.add({
216
+ unit: 1,
217
+
218
+ // Optional: intercept any FC before default dispatch
219
+ interceptor?: (fc, data) => Buffer | undefined,
220
+
221
+ readCoils?: (address, length) => boolean[],
222
+ readDiscreteInputs?: (address, length) => boolean[],
223
+ readHoldingRegisters?: (address, length) => number[],
224
+ readInputRegisters?: (address, length) => number[],
225
+
226
+ writeSingleCoil?: (address, value) => void,
227
+ writeMultipleCoils?: (address, values) => void,
228
+
229
+ writeSingleRegister?: (address, value) => void,
230
+ writeMultipleRegisters?: (address, values) => void,
231
+
232
+ maskWriteRegister?: (address, andMask, orMask) => void,
233
+
234
+ reportServerId?: () => ServerId,
235
+ readDeviceIdentification?: () => { [index: number]: string },
189
236
 
190
- await master.writeSingleRegister(0, 0x0000, 0x1234); // broadcast
191
- await sleep(100); // turnaround — tune per devices
192
- await master.writeSingleRegister(1, 0x0000, 0x5678); // unicast to next slave
237
+ // Optional address range validation
238
+ getAddressRange?: () => ({
239
+ discreteInputs?: [number, number] | [number, number][];
240
+ coils?: [number, number] | [number, number][];
241
+ inputRegisters?: [number, number] | [number, number][];
242
+ holdingRegisters?: [number, number] | [number, number][];
243
+ }),
244
+ });
193
245
  ```
194
246
 
195
- 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).
247
+ 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`.
248
+
249
+ | Method | Description |
250
+ |--------|-------------|
251
+ | `add(model)` | Register a slave model |
252
+ | `remove(unit)` | Remove a slave model |
253
+ | `open(...args)` | Open the physical layer. One-shot: cannot reopen after `close()`. |
254
+ | `close()` | Close immediately |
255
+ | `addCustomFunctionCode(cfc)` | Register a custom FC |
256
+ | `removeCustomFunctionCode(fc)` | Unregister a custom FC |
257
+
258
+ ## Custom Function Codes
259
+
260
+ ```typescript
261
+ import type { CustomFunctionCode } from 'njs-modbus';
262
+
263
+ const cfc: CustomFunctionCode = {
264
+ fc: 0x50,
265
+ predictRequestLength: (buffer) => {
266
+ if (buffer.length < 2) return null;
267
+ return 4 + buffer[1];
268
+ },
269
+ predictResponseLength: (buffer) => {
270
+ if (buffer.length < 2) return null;
271
+ return 4 + buffer[1];
272
+ },
273
+ handle: async (data, unit) => {
274
+ return Buffer.from([data[1], data[0]]);
275
+ },
276
+ };
277
+
278
+ // Slave: framing + dispatch
279
+ slave.addCustomFunctionCode(cfc);
280
+
281
+ // Master: framing only
282
+ master.addCustomFunctionCode(cfc);
283
+ const response = await master.sendCustomFC(1, 0x50, [0xab, 0xcd]);
284
+ ```
196
285
 
197
- ## Contributing
286
+ ## Error Handling
198
287
 
199
- Please read our [contributing guide](/CODE_OF_CONDUCT.md) first.
288
+ ```typescript
289
+ import { ErrorCode, ModbusError, getErrorByCode } from 'njs-modbus';
290
+
291
+ // ErrorCode.ILLEGAL_FUNCTION = 0x01
292
+ // ErrorCode.ILLEGAL_DATA_ADDRESS = 0x02
293
+ // ErrorCode.ILLEGAL_DATA_VALUE = 0x03
294
+ // ErrorCode.SERVER_DEVICE_FAILURE = 0x04
295
+ // ErrorCode.ACKNOWLEDGE = 0x05
296
+ // ErrorCode.SERVER_DEVICE_BUSY = 0x06
297
+ // ErrorCode.MEMORY_PARITY_ERROR = 0x08
298
+ // ErrorCode.GATEWAY_PATH_UNAVAILABLE = 0x0a
299
+ // ErrorCode.GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0b
300
+
301
+ slave.add({
302
+ unit: 1,
303
+ readHoldingRegisters: (address) => {
304
+ if (address > 100) {
305
+ throw getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS);
306
+ }
307
+ return [42];
308
+ },
309
+ });
310
+ ```
200
311
 
201
312
  ## License
202
313
 
203
- [![gitHub license](https://img.shields.io/github/license/xiejay97/njs-modbus?style=flat-square)](/LICENSE)
314
+ [![license](https://img.shields.io/github/license/xiejay97/njs-modbus?style=flat-square)](/LICENSE)