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