njs-modbus 3.1.1 → 3.3.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 (44) hide show
  1. package/README.md +84 -35
  2. package/README.zh-CN.md +84 -35
  3. package/dist/index.cjs +1291 -497
  4. package/dist/index.d.ts +99 -31
  5. package/dist/index.mjs +1291 -498
  6. package/dist/utils.cjs +536 -0
  7. package/dist/utils.d.ts +163 -0
  8. package/dist/utils.mjs +522 -0
  9. package/package.json +22 -2
  10. package/dist/src/error-code.d.ts +0 -17
  11. package/dist/src/index.d.ts +0 -7
  12. package/dist/src/layers/application/abstract-application-layer.d.ts +0 -26
  13. package/dist/src/layers/application/ascii-application-layer.d.ts +0 -23
  14. package/dist/src/layers/application/index.d.ts +0 -6
  15. package/dist/src/layers/application/rtu-application-layer.d.ts +0 -34
  16. package/dist/src/layers/application/tcp-application-layer.d.ts +0 -16
  17. package/dist/src/layers/physical/abstract-physical-layer.d.ts +0 -50
  18. package/dist/src/layers/physical/index.d.ts +0 -12
  19. package/dist/src/layers/physical/serial-physical-layer.d.ts +0 -70
  20. package/dist/src/layers/physical/tcp-client-physical-layer.d.ts +0 -20
  21. package/dist/src/layers/physical/tcp-physical-connection.d.ts +0 -16
  22. package/dist/src/layers/physical/tcp-server-physical-layer.d.ts +0 -29
  23. package/dist/src/layers/physical/udp-client-physical-layer.d.ts +0 -34
  24. package/dist/src/layers/physical/udp-server-physical-layer.d.ts +0 -51
  25. package/dist/src/layers/physical/utils.d.ts +0 -39
  26. package/dist/src/layers/physical/vars.d.ts +0 -11
  27. package/dist/src/master/index.d.ts +0 -3
  28. package/dist/src/master/master-session.d.ts +0 -18
  29. package/dist/src/master/master.d.ts +0 -140
  30. package/dist/src/slave/index.d.ts +0 -2
  31. package/dist/src/slave/slave.d.ts +0 -119
  32. package/dist/src/types.d.ts +0 -54
  33. package/dist/src/utils/bitsToMs.d.ts +0 -13
  34. package/dist/src/utils/callback.d.ts +0 -8
  35. package/dist/src/utils/checkRange.d.ts +0 -1
  36. package/dist/src/utils/crc.d.ts +0 -1
  37. package/dist/src/utils/index.d.ts +0 -11
  38. package/dist/src/utils/isUint8.d.ts +0 -8
  39. package/dist/src/utils/lrc.d.ts +0 -1
  40. package/dist/src/utils/predictRtuFrameLength.d.ts +0 -17
  41. package/dist/src/utils/promisify-cb.d.ts +0 -4
  42. package/dist/src/utils/rtu-timing.d.ts +0 -63
  43. package/dist/src/utils/whitelist.d.ts +0 -11
  44. package/dist/src/vars.d.ts +0 -49
package/dist/utils.mjs ADDED
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Convert a number of bits to milliseconds at a given baud rate.
3
+ *
4
+ * Used to derive Modbus RTU timing intervals from bit counts — e.g. 38.5 bits
5
+ * = 3.5 character times at 11 bits/char (t3.5 inter-frame silence), or 16.5
6
+ * bits = 1.5 character times (t1.5 inter-character timeout), per Modbus V1.02
7
+ * §2.5.1.1.
8
+ *
9
+ * @param baudRate Serial port baud rate.
10
+ * @param bits Number of bits to convert.
11
+ * @returns Duration in milliseconds.
12
+ */
13
+ function bitsToMs(baudRate, bits) {
14
+ return (bits * 1000) / baudRate;
15
+ }
16
+
17
+ /**
18
+ * Drain a pending-callback array: invoke each callback with the given error (or null).
19
+ *
20
+ * Handles `null`/`undefined` entries (from optional `cb?` parameters) gracefully.
21
+ * Used by physical layers and connections to resolve queued
22
+ * open / close / destroy callbacks.
23
+ */
24
+ function drainCbs(cbs, err) {
25
+ if (!cbs) {
26
+ return;
27
+ }
28
+ for (const cb of cbs) {
29
+ cb?.(err);
30
+ }
31
+ }
32
+
33
+ function checkRange(value, range) {
34
+ if (!range || range.length === 0) {
35
+ return true;
36
+ }
37
+ const isMultiRange = Array.isArray(range[0]);
38
+ const isValueArray = Array.isArray(value);
39
+ if (!isValueArray && !isMultiRange) {
40
+ const r = range;
41
+ const min = r[0], max = r[1];
42
+ const v = value;
43
+ return min <= max ? v >= min && v <= max : v >= max && v <= min;
44
+ }
45
+ if (!isValueArray && isMultiRange) {
46
+ const ranges = range;
47
+ const v = value;
48
+ for (let i = 0; i < ranges.length; i++) {
49
+ const min = ranges[i][0], max = ranges[i][1];
50
+ const lo = min <= max ? min : max;
51
+ const hi = min <= max ? max : min;
52
+ if (v >= lo && v <= hi) {
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+ const values = value;
59
+ if (values.length === 0) {
60
+ return true;
61
+ }
62
+ if (!isMultiRange) {
63
+ const r = range;
64
+ const min = r[0], max = r[1];
65
+ const lo = min <= max ? min : max;
66
+ const hi = min <= max ? max : min;
67
+ for (let i = 0; i < values.length; i++) {
68
+ if (values[i] < lo || values[i] > hi) {
69
+ return false;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+ const ranges = range;
75
+ for (let i = 0; i < ranges.length; i++) {
76
+ const min = ranges[i][0], max = ranges[i][1];
77
+ const lo = min <= max ? min : max;
78
+ const hi = min <= max ? max : min;
79
+ let allInRange = true;
80
+ for (let j = 0; j < values.length; j++) {
81
+ if (values[j] < lo || values[j] > hi) {
82
+ allInRange = false;
83
+ break;
84
+ }
85
+ }
86
+ if (allInRange) {
87
+ return true;
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+
93
+ const TABLE = new Uint16Array([
94
+ 0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
95
+ 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
96
+ 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
97
+ 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141,
98
+ 0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01,
99
+ 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0,
100
+ 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681,
101
+ 0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
102
+ 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
103
+ 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0,
104
+ 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381,
105
+ 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741,
106
+ 0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901,
107
+ 0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
108
+ 0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
109
+ 0x4040,
110
+ ]);
111
+ function crc(data, start, end) {
112
+ let crc = 0xffff;
113
+ for (let index = start; index < end; index++) {
114
+ crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
115
+ }
116
+ return crc;
117
+ }
118
+
119
+ /**
120
+ * Returns true when `n` is an integer in the unsigned-byte range [0, 255].
121
+ *
122
+ * Used for byte-level Modbus payload validation (function-code values, raw
123
+ * byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
124
+ * Infinity, and out-of-range values uniformly.
125
+ */
126
+ function isUint8(n) {
127
+ return (n & 0xff) === n;
128
+ }
129
+
130
+ function lrc(data, start, end) {
131
+ let sum = 0;
132
+ for (let i = start; i < end; i++) {
133
+ sum += data[i];
134
+ }
135
+ return -sum & 0xff;
136
+ }
137
+
138
+ /**
139
+ * Standard Modbus function codes (V1.1b3 §6).
140
+ */
141
+ var FunctionCode;
142
+ (function (FunctionCode) {
143
+ FunctionCode[FunctionCode["READ_COILS"] = 1] = "READ_COILS";
144
+ FunctionCode[FunctionCode["READ_DISCRETE_INPUTS"] = 2] = "READ_DISCRETE_INPUTS";
145
+ FunctionCode[FunctionCode["READ_HOLDING_REGISTERS"] = 3] = "READ_HOLDING_REGISTERS";
146
+ FunctionCode[FunctionCode["READ_INPUT_REGISTERS"] = 4] = "READ_INPUT_REGISTERS";
147
+ FunctionCode[FunctionCode["WRITE_SINGLE_COIL"] = 5] = "WRITE_SINGLE_COIL";
148
+ FunctionCode[FunctionCode["WRITE_SINGLE_REGISTER"] = 6] = "WRITE_SINGLE_REGISTER";
149
+ FunctionCode[FunctionCode["WRITE_MULTIPLE_COILS"] = 15] = "WRITE_MULTIPLE_COILS";
150
+ FunctionCode[FunctionCode["WRITE_MULTIPLE_REGISTERS"] = 16] = "WRITE_MULTIPLE_REGISTERS";
151
+ FunctionCode[FunctionCode["REPORT_SERVER_ID"] = 17] = "REPORT_SERVER_ID";
152
+ FunctionCode[FunctionCode["MASK_WRITE_REGISTER"] = 22] = "MASK_WRITE_REGISTER";
153
+ FunctionCode[FunctionCode["READ_WRITE_MULTIPLE_REGISTERS"] = 23] = "READ_WRITE_MULTIPLE_REGISTERS";
154
+ FunctionCode[FunctionCode["READ_DEVICE_IDENTIFICATION"] = 43] = "READ_DEVICE_IDENTIFICATION";
155
+ })(FunctionCode || (FunctionCode = {}));
156
+ /** Exception response FC = request FC | EXCEPTION_OFFSET (V1.1b3 §7). */
157
+ const EXCEPTION_OFFSET = 0x80;
158
+ /** FC 0x2B MEI sub-function selecting Read Device Identification (V1.1b3 §6.21). */
159
+ const MEI_READ_DEVICE_ID = 0x0e;
160
+ /** Read Device ID code values inside an FC 0x2B / MEI 0x0E request. */
161
+ var ReadDeviceIDCode;
162
+ (function (ReadDeviceIDCode) {
163
+ ReadDeviceIDCode[ReadDeviceIDCode["BASIC_STREAM"] = 1] = "BASIC_STREAM";
164
+ ReadDeviceIDCode[ReadDeviceIDCode["REGULAR_STREAM"] = 2] = "REGULAR_STREAM";
165
+ ReadDeviceIDCode[ReadDeviceIDCode["EXTENDED_STREAM"] = 3] = "EXTENDED_STREAM";
166
+ ReadDeviceIDCode[ReadDeviceIDCode["SPECIFIC_ACCESS"] = 4] = "SPECIFIC_ACCESS";
167
+ })(ReadDeviceIDCode || (ReadDeviceIDCode = {}));
168
+ /** Conformity level reported in an FC 0x2B / MEI 0x0E response. */
169
+ var ConformityLevel;
170
+ (function (ConformityLevel) {
171
+ ConformityLevel[ConformityLevel["BASIC"] = 129] = "BASIC";
172
+ ConformityLevel[ConformityLevel["REGULAR"] = 130] = "REGULAR";
173
+ ConformityLevel[ConformityLevel["EXTENDED"] = 131] = "EXTENDED";
174
+ })(ConformityLevel || (ConformityLevel = {}));
175
+ /** Shared empty Buffer to avoid repeated allocations. */
176
+ Buffer.alloc(0);
177
+
178
+ const PREDICT_NEED_MORE = 0;
179
+ const PREDICT_UNKNOWN = -1;
180
+ const REQ_TABLE = new Int32Array(256);
181
+ const RES_TABLE = new Int32Array(256);
182
+ (function initTables() {
183
+ REQ_TABLE[FunctionCode.READ_COILS] = 8;
184
+ REQ_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = 8;
185
+ REQ_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = 8;
186
+ REQ_TABLE[FunctionCode.READ_INPUT_REGISTERS] = 8;
187
+ REQ_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
188
+ REQ_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
189
+ REQ_TABLE[FunctionCode.REPORT_SERVER_ID] = 4;
190
+ REQ_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
191
+ REQ_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
192
+ REQ_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
193
+ REQ_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
194
+ REQ_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
195
+ RES_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
196
+ RES_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
197
+ RES_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = 8;
198
+ RES_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
199
+ RES_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
200
+ RES_TABLE[FunctionCode.READ_COILS] = -517;
201
+ RES_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = -517;
202
+ RES_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = -517;
203
+ RES_TABLE[FunctionCode.READ_INPUT_REGISTERS] = -517;
204
+ RES_TABLE[FunctionCode.REPORT_SERVER_ID] = -517;
205
+ RES_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
206
+ RES_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
207
+ })();
208
+ function predictRtuFrameLength(buffer, start, end, isResponse) {
209
+ const len = end - start;
210
+ if (len < 2) {
211
+ return PREDICT_NEED_MORE;
212
+ }
213
+ const fc = buffer[start + 1];
214
+ if (isResponse) {
215
+ if ((fc & EXCEPTION_OFFSET) !== 0) {
216
+ return 5;
217
+ }
218
+ const val = RES_TABLE[fc];
219
+ if (val > 0) {
220
+ return val;
221
+ }
222
+ if (val < 0) {
223
+ if (val === -999) {
224
+ // FC 43 / MEI 14 response — inline to avoid function-call overhead on
225
+ // the framing hot path (even though this FC is uncommon).
226
+ if (end - start < 8) {
227
+ return PREDICT_NEED_MORE;
228
+ }
229
+ if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
230
+ return PREDICT_UNKNOWN;
231
+ }
232
+ const numObjs = buffer[start + 7];
233
+ let cursor = start + 8;
234
+ for (let i = 0; i < numObjs; i++) {
235
+ if (end < cursor + 2) {
236
+ return PREDICT_NEED_MORE;
237
+ }
238
+ cursor += 2 + buffer[cursor + 1];
239
+ }
240
+ return cursor - start + 2;
241
+ }
242
+ const decode = -val;
243
+ const offset = decode >> 8;
244
+ if (len <= offset) {
245
+ return PREDICT_NEED_MORE;
246
+ }
247
+ return (decode & 0xff) + buffer[start + offset];
248
+ }
249
+ }
250
+ else {
251
+ const val = REQ_TABLE[fc];
252
+ if (val > 0) {
253
+ return val;
254
+ }
255
+ if (val < 0) {
256
+ const decode = -val;
257
+ const offset = decode >> 8;
258
+ if (len <= offset) {
259
+ return PREDICT_NEED_MORE;
260
+ }
261
+ return (decode & 0xff) + buffer[start + offset];
262
+ }
263
+ }
264
+ return PREDICT_UNKNOWN;
265
+ }
266
+
267
+ /**
268
+ * Convert a callback-style `(cb: (err?) => void) => void` call into a Promise.
269
+ */
270
+ function promisifyCb(fn) {
271
+ return new Promise((resolve, reject) => {
272
+ fn((err) => {
273
+ if (err) {
274
+ reject(err);
275
+ }
276
+ else {
277
+ resolve();
278
+ }
279
+ });
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Resolve a single RTU timing parameter to milliseconds.
285
+ *
286
+ * Returns `undefined` when the caller did not supply the parameter at all
287
+ * (so the caller knows to fall back to a spec default). An explicit `0` or
288
+ * `{ value: 0 }` returns `0` — the parameter is set, just disabled.
289
+ */
290
+ function resolveOne(value, baudRate, fastBaudMs) {
291
+ if (value === undefined) {
292
+ return undefined;
293
+ }
294
+ if (typeof value === 'number') {
295
+ return value;
296
+ }
297
+ if (value.unit === 'ms') {
298
+ return value.value;
299
+ }
300
+ // unit === 'bit' — needs baudRate to convert
301
+ if (baudRate === undefined) {
302
+ return undefined;
303
+ }
304
+ return baudRate > 19200 ? fastBaudMs : Math.ceil(bitsToMs(baudRate, value.value));
305
+ }
306
+ /**
307
+ * Resolve Modbus RTU timing parameters from user options into milliseconds.
308
+ *
309
+ * - `intervalBetweenFrames` (t3.5): if omitted and `baudRate` is present,
310
+ * defaults to 38.5 bits per Modbus V1.02 §2.5.1.1. Pass `0` to disable.
311
+ * - `interCharTimeout` (t1.5): opt-in; only resolved when explicitly provided.
312
+ *
313
+ * Per the spec, at baud rates > 19200 fixed values are used
314
+ * (1.75 ms for t3.5, 0.75 ms for t1.5) regardless of the bit value.
315
+ */
316
+ function resolveRtuTiming(opts = {}, baudRate) {
317
+ let intervalBetweenFrames = resolveOne(opts.intervalBetweenFrames, baudRate, 1.75);
318
+ if (intervalBetweenFrames === undefined) {
319
+ // Spec default: t3.5 derived from baudRate, or 0 when neither option nor
320
+ // baudRate were supplied.
321
+ intervalBetweenFrames = baudRate !== undefined ? (baudRate > 19200 ? 1.75 : Math.ceil(bitsToMs(baudRate, 38.5))) : 0;
322
+ }
323
+ let interCharTimeout = resolveOne(opts.interCharTimeout, baudRate, 0.75);
324
+ if (interCharTimeout === undefined) {
325
+ // t1.5 is opt-in — no spec-default fallback.
326
+ interCharTimeout = 0;
327
+ }
328
+ return { intervalBetweenFrames, interCharTimeout };
329
+ }
330
+
331
+ /**
332
+ * Hybrid timer manager: uses native `setTimeout` for low concurrency
333
+ * and switches to a binary min-heap when concurrency exceeds the threshold.
334
+ *
335
+ * Benchmarks (add + clear throughput, Node 24, x64):
336
+ * 1 concurrent: setTimeout ~1.7× faster than heap
337
+ * 2 concurrent: setTimeout ~1.6× faster than heap
338
+ * 5 concurrent: setTimeout ~1.5-1.9× faster than heap
339
+ * 10 concurrent: roughly equal
340
+ * 20 concurrent: heap ~1.3× faster than setTimeout[]
341
+ * 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
342
+ *
343
+ * The crossover point is around 10 concurrent timers, so the default
344
+ * `concurrentThreshold = 2` keeps the common 1-2 request case on the
345
+ * fast direct path while delegating to the heap for larger batches.
346
+ */
347
+ class TimerHeap {
348
+ _deadlines = [];
349
+ _ids = [];
350
+ _seqs = [];
351
+ _counter = 0;
352
+ _timer = null;
353
+ _onFire;
354
+ _boundTick;
355
+ _threshold;
356
+ _mode = 'direct';
357
+ _directTimers = new Map();
358
+ /**
359
+ * @param onFire Callback invoked with the timer id when it expires.
360
+ * @param concurrentThreshold Maximum number of timers kept as individual
361
+ * native `setTimeout` handles. Once exceeded, all timers migrate to
362
+ * the internal heap and share a single native timer. Default is 2.
363
+ */
364
+ constructor(onFire, concurrentThreshold = 2) {
365
+ this._onFire = onFire;
366
+ this._boundTick = this._onTick.bind(this);
367
+ this._threshold = concurrentThreshold;
368
+ }
369
+ get size() {
370
+ return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
371
+ }
372
+ add(id, ms) {
373
+ if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
374
+ const deadline = performance.now() + ms;
375
+ const handle = setTimeout(() => {
376
+ if (this._mode !== 'direct') {
377
+ return;
378
+ }
379
+ this._directTimers.delete(id);
380
+ this._onFire(id);
381
+ }, ms);
382
+ this._directTimers.set(id, { handle, deadline });
383
+ return;
384
+ }
385
+ if (this._mode === 'direct') {
386
+ this._mode = 'heap';
387
+ for (const [existingId, { handle, deadline }] of this._directTimers) {
388
+ clearTimeout(handle);
389
+ const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
390
+ if (remaining === 0) {
391
+ this._onFire(existingId);
392
+ }
393
+ else {
394
+ this._heapAdd(existingId, remaining);
395
+ }
396
+ }
397
+ this._directTimers.clear();
398
+ }
399
+ this._heapAdd(id, ms);
400
+ }
401
+ clear() {
402
+ for (const { handle } of this._directTimers.values()) {
403
+ clearTimeout(handle);
404
+ }
405
+ this._directTimers.clear();
406
+ this._mode = 'direct';
407
+ if (this._timer) {
408
+ clearTimeout(this._timer);
409
+ this._timer = null;
410
+ }
411
+ this._deadlines.length = 0;
412
+ this._ids.length = 0;
413
+ this._seqs.length = 0;
414
+ this._counter = 0;
415
+ }
416
+ _heapAdd(id, ms) {
417
+ const deadline = performance.now() + ms;
418
+ const seq = this._counter++;
419
+ let i = this._deadlines.length;
420
+ this._deadlines.push(deadline);
421
+ this._ids.push(id);
422
+ this._seqs.push(seq);
423
+ while (i > 0) {
424
+ const p = (i - 1) >> 1;
425
+ const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
426
+ if (parentComesFirst) {
427
+ break;
428
+ }
429
+ this._deadlines[i] = this._deadlines[p];
430
+ this._ids[i] = this._ids[p];
431
+ this._seqs[i] = this._seqs[p];
432
+ i = p;
433
+ }
434
+ this._deadlines[i] = deadline;
435
+ this._ids[i] = id;
436
+ this._seqs[i] = seq;
437
+ if (i === 0) {
438
+ this._refresh();
439
+ }
440
+ }
441
+ _refresh() {
442
+ if (this._timer) {
443
+ clearTimeout(this._timer);
444
+ this._timer = null;
445
+ }
446
+ if (this._deadlines.length === 0) {
447
+ return;
448
+ }
449
+ const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
450
+ const safeDelay = Math.min(delay, 2147483647);
451
+ this._timer = setTimeout(this._boundTick, safeDelay);
452
+ }
453
+ _onTick() {
454
+ this._timer = null;
455
+ const now = performance.now();
456
+ try {
457
+ while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
458
+ const id = this._pop();
459
+ this._onFire(id);
460
+ }
461
+ }
462
+ finally {
463
+ this._refresh();
464
+ }
465
+ }
466
+ _pop() {
467
+ const topId = this._ids[0];
468
+ const lastId = this._ids.pop();
469
+ const lastDeadline = this._deadlines.pop();
470
+ const lastSeq = this._seqs.pop();
471
+ const n = this._deadlines.length;
472
+ if (n > 0) {
473
+ let i = 0;
474
+ const half = n >> 1;
475
+ while (i < half) {
476
+ let minChild = (i << 1) + 1;
477
+ const rightChild = minChild + 1;
478
+ if (rightChild < n) {
479
+ const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
480
+ (this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
481
+ if (rightComesFirst) {
482
+ minChild = rightChild;
483
+ }
484
+ }
485
+ const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
486
+ if (lastComesFirst) {
487
+ break;
488
+ }
489
+ this._deadlines[i] = this._deadlines[minChild];
490
+ this._ids[i] = this._ids[minChild];
491
+ this._seqs[i] = this._seqs[minChild];
492
+ i = minChild;
493
+ }
494
+ this._deadlines[i] = lastDeadline;
495
+ this._ids[i] = lastId;
496
+ this._seqs[i] = lastSeq;
497
+ }
498
+ return topId;
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Normalize an IP address by stripping the IPv4-mapped IPv6 prefix.
504
+ * This ensures consistent comparison of addresses like `::ffff:192.168.1.1`.
505
+ */
506
+ function normalizeAddress(address = '') {
507
+ return address.replace(/^::ffff:/, '');
508
+ }
509
+ /**
510
+ * Check whether a remote address is allowed by the given whitelist.
511
+ * IPv4-mapped IPv6 addresses are normalized before comparison.
512
+ * Returns `true` when whitelist is absent or empty.
513
+ */
514
+ function isWhitelisted(address, whitelist) {
515
+ if (!whitelist || whitelist.length === 0) {
516
+ return true;
517
+ }
518
+ const normalized = normalizeAddress(address);
519
+ return whitelist.includes(normalized);
520
+ }
521
+
522
+ export { PREDICT_NEED_MORE, PREDICT_UNKNOWN, TimerHeap, bitsToMs, checkRange, crc, drainCbs, isUint8, isWhitelisted, lrc, predictRtuFrameLength, promisifyCb, resolveRtuTiming };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "njs-modbus",
3
- "version": "3.1.1",
3
+ "version": "3.3.0",
4
4
  "description": "A pure JavaScript implementation of Modbus for Node.js.",
5
5
  "keywords": [
6
6
  "modbus",
@@ -26,15 +26,35 @@
26
26
  "module": "dist/index.mjs",
27
27
  "source": "src/index.ts",
28
28
  "types": "dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "import": "./dist/index.mjs",
32
+ "require": "./dist/index.cjs",
33
+ "types": "./dist/index.d.ts"
34
+ },
35
+ "./utils": {
36
+ "import": "./dist/utils.mjs",
37
+ "require": "./dist/utils.cjs",
38
+ "types": "./dist/utils.d.ts"
39
+ }
40
+ },
29
41
  "files": [
30
42
  "dist",
31
43
  "*.d.ts"
32
44
  ],
33
45
  "scripts": {
34
- "build": "node -e \"fs.rmSync('dist', {recursive: true, force: true})\" && rollup -c",
46
+ "build": "node -e \"fs.rmSync('dist', {recursive: true, force: true})\" && rollup -c && node -e \"fs.rmSync('dist/src', {recursive: true, force: true})\"",
35
47
  "test": "tsx --test test/**/*.test.ts",
36
48
  "benchmark": "tsx benchmark/encode-decode.ts && echo && tsx benchmark/tcp-throughput.ts && echo && tsx benchmark/concurrent.ts",
49
+ "benchmark:encode-decode": "tsx benchmark/encode-decode.ts",
50
+ "benchmark:tcp": "tsx benchmark/tcp-throughput.ts",
51
+ "benchmark:concurrent": "tsx benchmark/concurrent.ts",
52
+ "benchmark:rtu": "tsx benchmark/rtu-throughput.ts",
53
+ "benchmark:all-fcs": "tsx benchmark/all-fcs.ts",
37
54
  "benchmark:report": "tsx benchmark/generate-report.ts",
55
+ "benchmark:report:quick": "tsx benchmark/generate-report.ts -- --duration 10s --runs 2",
56
+ "benchmark:report:full": "tsx benchmark/generate-report.ts -- --duration 120s --runs 3 --max-payload",
57
+ "benchmark:profile": "tsx benchmark/all-fcs.ts -- --duration 30s --runs 1 --only fc03 --libs njs-modbus --profile",
38
58
  "util:sort-package-json": "sort-package-json"
39
59
  },
40
60
  "devDependencies": {
@@ -1,17 +0,0 @@
1
- export declare enum ErrorCode {
2
- ILLEGAL_FUNCTION = 1,
3
- ILLEGAL_DATA_ADDRESS = 2,
4
- ILLEGAL_DATA_VALUE = 3,
5
- SERVER_DEVICE_FAILURE = 4,
6
- ACKNOWLEDGE = 5,
7
- SERVER_DEVICE_BUSY = 6,
8
- MEMORY_PARITY_ERROR = 8,
9
- GATEWAY_PATH_UNAVAILABLE = 10,
10
- GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 11
11
- }
12
- export declare class ModbusError extends Error {
13
- readonly code: ErrorCode;
14
- constructor(code: ErrorCode, message?: string);
15
- }
16
- export declare function getErrorByCode(code: ErrorCode): ModbusError;
17
- export declare function getCodeByError(err: Error): ErrorCode;
@@ -1,7 +0,0 @@
1
- export * from './types';
2
- export * from './error-code';
3
- export * from './vars';
4
- export * from './layers/physical';
5
- export * from './layers/application';
6
- export * from './master';
7
- export * from './slave';
@@ -1,26 +0,0 @@
1
- import type { ApplicationDataUnit, CustomFunctionCode } from '../../types';
2
- import type { AbstractPhysicalConnection } from '../physical';
3
- import EventEmitter from 'node:events';
4
- interface AbstractApplicationLayerEvents {
5
- framing: [frame: ApplicationDataUnit & {
6
- buffer: Buffer;
7
- }];
8
- 'framing-error': [error: Error];
9
- }
10
- /**
11
- * Application-layer protocol handler bound to a single physical connection.
12
- *
13
- * Its lifetime follows the channel: created when the underlying connection is
14
- * established and discarded when the connection closes. Subclasses implement
15
- * ASCII, RTU, or TCP framing rules.
16
- */
17
- export declare abstract class AbstractApplicationLayer extends EventEmitter<AbstractApplicationLayerEvents> {
18
- abstract readonly PROTOCOL: 'ASCII' | 'RTU' | 'TCP';
19
- abstract ROLE: 'MASTER' | 'SLAVE';
20
- abstract readonly connection: AbstractPhysicalConnection;
21
- flush(): void;
22
- addCustomFunctionCode(cfc: CustomFunctionCode): void;
23
- removeCustomFunctionCode(fc: number): void;
24
- abstract encode(unit: number, fc: number, data: Buffer, transaction?: number): Buffer;
25
- }
26
- export {};
@@ -1,23 +0,0 @@
1
- import type { AbstractPhysicalConnection } from '../physical';
2
- import { AbstractApplicationLayer } from './abstract-application-layer';
3
- export interface AsciiApplicationLayerOptions {
4
- /**
5
- * Accept lowercase hex digits (`a-f`) in addition to uppercase (`A-F`).
6
- * Default false (strict per Modbus V1.1b3 §2.2 — uppercase only).
7
- * Non-hex characters are always rejected with a `framing-error`.
8
- */
9
- lenientHex?: boolean;
10
- }
11
- export declare class AsciiApplicationLayer extends AbstractApplicationLayer {
12
- readonly PROTOCOL: "ASCII";
13
- readonly ROLE: 'MASTER' | 'SLAVE';
14
- readonly lenientHex: boolean;
15
- private _connection;
16
- private _state;
17
- private _cleanupFns;
18
- get connection(): AbstractPhysicalConnection;
19
- constructor(role: 'MASTER' | 'SLAVE', connection: AbstractPhysicalConnection, options?: AsciiApplicationLayerOptions);
20
- private framing;
21
- flush(): void;
22
- encode(unit: number, fc: number, data: Buffer, transaction?: number): Buffer;
23
- }
@@ -1,6 +0,0 @@
1
- export type { AsciiApplicationLayerOptions } from './ascii-application-layer';
2
- export type { RtuApplicationLayerOptions } from './rtu-application-layer';
3
- export { AbstractApplicationLayer } from './abstract-application-layer';
4
- export { RtuApplicationLayer } from './rtu-application-layer';
5
- export { AsciiApplicationLayer } from './ascii-application-layer';
6
- export { TcpApplicationLayer } from './tcp-application-layer';
@@ -1,34 +0,0 @@
1
- import type { CustomFunctionCode } from '../../types';
2
- import type { AbstractPhysicalConnection } from '../physical';
3
- import { AbstractApplicationLayer } from './abstract-application-layer';
4
- export interface RtuApplicationLayerOptions {
5
- /** Inter-frame silence in milliseconds (Modbus RTU t3.5). 0 = disabled (immediate parse). */
6
- intervalBetweenFrames?: number;
7
- /** Inter-character timeout in milliseconds (Modbus RTU t1.5). Opt-in. */
8
- interCharTimeout?: number;
9
- /**
10
- * Buffer pool size per connection (bytes). Defaults to `MAX_FRAME_LENGTH * 2`
11
- * (512 bytes). Increase this if you expect frames larger than 256 bytes or
12
- * heavy pipelining on a single connection.
13
- */
14
- poolSize?: number;
15
- }
16
- export declare class RtuApplicationLayer extends AbstractApplicationLayer {
17
- readonly PROTOCOL: "RTU";
18
- readonly ROLE: 'MASTER' | 'SLAVE';
19
- private _connection;
20
- private _state;
21
- private _poolSize;
22
- private _threePointFiveT;
23
- private _onePointFiveT;
24
- private _customFunctionCodes;
25
- private _cleanupFns;
26
- get connection(): AbstractPhysicalConnection;
27
- constructor(role: 'MASTER' | 'SLAVE', connection: AbstractPhysicalConnection, options?: RtuApplicationLayerOptions);
28
- private clearStateTimers;
29
- private flushBuffer;
30
- flush(): void;
31
- addCustomFunctionCode(cfc: CustomFunctionCode): void;
32
- removeCustomFunctionCode(fc: number): void;
33
- encode(unit: number, fc: number, data: Buffer, transaction?: number): Buffer;
34
- }