njs-modbus 3.2.0 → 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.
package/dist/index.mjs CHANGED
@@ -164,40 +164,75 @@ function bitsToMs(baudRate, bits) {
164
164
  * open / close / destroy callbacks.
165
165
  */
166
166
  function drainCbs(cbs, err) {
167
- if (!cbs)
167
+ if (!cbs) {
168
168
  return;
169
+ }
169
170
  for (const cb of cbs) {
170
171
  cb?.(err);
171
172
  }
172
173
  }
173
174
 
174
- function inRange(n, [min, max]) {
175
- return n >= min && n <= max;
176
- }
177
- function isRangeArray(range) {
178
- return Array.isArray(range[0]);
179
- }
180
175
  function checkRange(value, range) {
181
176
  if (!range || range.length === 0) {
182
177
  return true;
183
178
  }
184
- const values = Array.isArray(value) ? value : [value];
185
- if (isRangeArray(range)) {
186
- for (const r of range) {
187
- const [min, max] = r;
188
- const [lo, hi] = min <= max ? [min, max] : [max, min];
189
- if (values.every((n) => inRange(n, [lo, hi]))) {
179
+ const isMultiRange = Array.isArray(range[0]);
180
+ const isValueArray = Array.isArray(value);
181
+ if (!isValueArray && !isMultiRange) {
182
+ const r = range;
183
+ const min = r[0], max = r[1];
184
+ const v = value;
185
+ return min <= max ? v >= min && v <= max : v >= max && v <= min;
186
+ }
187
+ if (!isValueArray && isMultiRange) {
188
+ const ranges = range;
189
+ const v = value;
190
+ for (let i = 0; i < ranges.length; i++) {
191
+ const min = ranges[i][0], max = ranges[i][1];
192
+ const lo = min <= max ? min : max;
193
+ const hi = min <= max ? max : min;
194
+ if (v >= lo && v <= hi) {
190
195
  return true;
191
196
  }
192
197
  }
193
198
  return false;
194
199
  }
195
- const [min, max] = range;
196
- const [lo, hi] = min <= max ? [min, max] : [max, min];
197
- return values.every((n) => inRange(n, [lo, hi]));
200
+ const values = value;
201
+ if (values.length === 0) {
202
+ return true;
203
+ }
204
+ if (!isMultiRange) {
205
+ const r = range;
206
+ const min = r[0], max = r[1];
207
+ const lo = min <= max ? min : max;
208
+ const hi = min <= max ? max : min;
209
+ for (let i = 0; i < values.length; i++) {
210
+ if (values[i] < lo || values[i] > hi) {
211
+ return false;
212
+ }
213
+ }
214
+ return true;
215
+ }
216
+ const ranges = range;
217
+ for (let i = 0; i < ranges.length; i++) {
218
+ const min = ranges[i][0], max = ranges[i][1];
219
+ const lo = min <= max ? min : max;
220
+ const hi = min <= max ? max : min;
221
+ let allInRange = true;
222
+ for (let j = 0; j < values.length; j++) {
223
+ if (values[j] < lo || values[j] > hi) {
224
+ allInRange = false;
225
+ break;
226
+ }
227
+ }
228
+ if (allInRange) {
229
+ return true;
230
+ }
231
+ }
232
+ return false;
198
233
  }
199
234
 
200
- const TABLE = [
235
+ const TABLE = new Uint16Array([
201
236
  0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
202
237
  0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
203
238
  0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
@@ -214,11 +249,11 @@ const TABLE = [
214
249
  0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
215
250
  0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
216
251
  0x4040,
217
- ];
218
- function crc(data, start = 0, end = data.length) {
252
+ ]);
253
+ function crc(data, start, end) {
219
254
  let crc = 0xffff;
220
255
  for (let index = start; index < end; index++) {
221
- crc = (TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8)) & 0xffff;
256
+ crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
222
257
  }
223
258
  return crc;
224
259
  }
@@ -231,114 +266,104 @@ function crc(data, start = 0, end = data.length) {
231
266
  * Infinity, and out-of-range values uniformly.
232
267
  */
233
268
  function isUint8(n) {
234
- return Number.isInteger(n) && n >= 0 && n <= 255;
269
+ return (n & 0xff) === n;
235
270
  }
236
271
 
237
- function lrc(data, start = 0, end = data.length) {
272
+ function lrc(data, start, end) {
238
273
  let sum = 0;
239
274
  for (let i = start; i < end; i++) {
240
275
  sum += data[i];
241
276
  }
242
- return (~sum + 1) & 0xff;
277
+ return -sum & 0xff;
243
278
  }
244
279
 
245
- const REQUEST_FIXED_LENGTHS = {
246
- [FunctionCode.READ_COILS]: 8,
247
- [FunctionCode.READ_DISCRETE_INPUTS]: 8,
248
- [FunctionCode.READ_HOLDING_REGISTERS]: 8,
249
- [FunctionCode.READ_INPUT_REGISTERS]: 8,
250
- [FunctionCode.WRITE_SINGLE_COIL]: 8,
251
- [FunctionCode.WRITE_SINGLE_REGISTER]: 8,
252
- [FunctionCode.REPORT_SERVER_ID]: 4,
253
- [FunctionCode.MASK_WRITE_REGISTER]: 10,
254
- [FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
255
- };
256
- const REQUEST_BYTE_COUNT = {
257
- [FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
258
- [FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
259
- [FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
260
- };
261
- const RESPONSE_FIXED_LENGTHS = {
262
- [FunctionCode.WRITE_SINGLE_COIL]: 8,
263
- [FunctionCode.WRITE_SINGLE_REGISTER]: 8,
264
- [FunctionCode.WRITE_MULTIPLE_COILS]: 8,
265
- [FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
266
- [FunctionCode.MASK_WRITE_REGISTER]: 10,
267
- };
268
- const RESPONSE_BYTE_COUNT = {
269
- [FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
270
- [FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
271
- [FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
272
- [FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
273
- [FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
274
- [FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
275
- };
276
- /** Sentinel: caller needs to feed more bytes before length can be determined. */
277
280
  const PREDICT_NEED_MORE = 0;
278
- /** Sentinel: function code is not in the standard tables. */
279
281
  const PREDICT_UNKNOWN = -1;
280
- /**
281
- * Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
282
- *
283
- * Returns a sentinel-encoded number to avoid per-call object allocation on the
284
- * RTU decode hot path:
285
- * - Positive integer (>= 4): total frame length, function code is known.
286
- * - {@link PREDICT_NEED_MORE} (0): function code is known but more bytes are
287
- * required (typically waiting on the byteCount byte).
288
- * - {@link PREDICT_UNKNOWN} (-1): function code is not in the standard tables —
289
- * the framing layer must defer to a registered `CustomFunctionCode` or treat
290
- * this as a framing error.
291
- */
282
+ const REQ_TABLE = new Int32Array(256);
283
+ const RES_TABLE = new Int32Array(256);
284
+ (function initTables() {
285
+ REQ_TABLE[FunctionCode.READ_COILS] = 8;
286
+ REQ_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = 8;
287
+ REQ_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = 8;
288
+ REQ_TABLE[FunctionCode.READ_INPUT_REGISTERS] = 8;
289
+ REQ_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
290
+ REQ_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
291
+ REQ_TABLE[FunctionCode.REPORT_SERVER_ID] = 4;
292
+ REQ_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
293
+ REQ_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
294
+ REQ_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
295
+ REQ_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
296
+ REQ_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
297
+ RES_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
298
+ RES_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
299
+ RES_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = 8;
300
+ RES_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
301
+ RES_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
302
+ RES_TABLE[FunctionCode.READ_COILS] = -517;
303
+ RES_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = -517;
304
+ RES_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = -517;
305
+ RES_TABLE[FunctionCode.READ_INPUT_REGISTERS] = -517;
306
+ RES_TABLE[FunctionCode.REPORT_SERVER_ID] = -517;
307
+ RES_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
308
+ RES_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
309
+ })();
292
310
  function predictRtuFrameLength(buffer, start, end, isResponse) {
293
- if (end - start < 2) {
311
+ const len = end - start;
312
+ if (len < 2) {
294
313
  return PREDICT_NEED_MORE;
295
314
  }
296
315
  const fc = buffer[start + 1];
297
- if (isResponse && (fc & EXCEPTION_OFFSET) !== 0) {
298
- return 5;
299
- }
300
- const fixed = (isResponse ? RESPONSE_FIXED_LENGTHS : REQUEST_FIXED_LENGTHS)[fc];
301
- if (fixed !== undefined) {
302
- return fixed;
303
- }
304
- const bc = (isResponse ? RESPONSE_BYTE_COUNT : REQUEST_BYTE_COUNT)[fc];
305
- if (bc !== undefined) {
306
- if (end - start <= bc.offset) {
307
- return PREDICT_NEED_MORE;
316
+ if (isResponse) {
317
+ if ((fc & EXCEPTION_OFFSET) !== 0) {
318
+ return 5;
319
+ }
320
+ const val = RES_TABLE[fc];
321
+ if (val > 0) {
322
+ return val;
323
+ }
324
+ if (val < 0) {
325
+ if (val === -999) {
326
+ // FC 43 / MEI 14 response — inline to avoid function-call overhead on
327
+ // the framing hot path (even though this FC is uncommon).
328
+ if (end - start < 8) {
329
+ return PREDICT_NEED_MORE;
330
+ }
331
+ if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
332
+ return PREDICT_UNKNOWN;
333
+ }
334
+ const numObjs = buffer[start + 7];
335
+ let cursor = start + 8;
336
+ for (let i = 0; i < numObjs; i++) {
337
+ if (end < cursor + 2) {
338
+ return PREDICT_NEED_MORE;
339
+ }
340
+ cursor += 2 + buffer[cursor + 1];
341
+ }
342
+ return cursor - start + 2;
343
+ }
344
+ const decode = -val;
345
+ const offset = decode >> 8;
346
+ if (len <= offset) {
347
+ return PREDICT_NEED_MORE;
348
+ }
349
+ return (decode & 0xff) + buffer[start + offset];
308
350
  }
309
- return bc.extra + buffer[start + bc.offset];
310
351
  }
311
- if (isResponse && fc === FunctionCode.READ_DEVICE_IDENTIFICATION) {
312
- return predictFc43_14Response(buffer, start, end);
313
- }
314
- return PREDICT_UNKNOWN;
315
- }
316
- /**
317
- * Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
318
- * response structure per Modbus V1.1b3 §6.21.
319
- *
320
- * Layout (after unit and fc):
321
- * mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
322
- * [objId(1) objLen(1) objData(objLen)] × numObjs
323
- * CRC(2)
324
- */
325
- function predictFc43_14Response(buffer, start, end) {
326
- if (end - start < 8) {
327
- return PREDICT_NEED_MORE;
328
- }
329
- if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
330
- return PREDICT_UNKNOWN;
331
- }
332
- const numObjs = buffer[start + 7];
333
- let cursor = start + 8;
334
- for (let i = 0; i < numObjs; i++) {
335
- if (end < cursor + 2) {
336
- return PREDICT_NEED_MORE;
352
+ else {
353
+ const val = REQ_TABLE[fc];
354
+ if (val > 0) {
355
+ return val;
356
+ }
357
+ if (val < 0) {
358
+ const decode = -val;
359
+ const offset = decode >> 8;
360
+ if (len <= offset) {
361
+ return PREDICT_NEED_MORE;
362
+ }
363
+ return (decode & 0xff) + buffer[start + offset];
337
364
  }
338
- const objLen = buffer[cursor + 1];
339
- cursor += 2 + objLen;
340
365
  }
341
- return cursor - start + 2;
366
+ return PREDICT_UNKNOWN;
342
367
  }
343
368
 
344
369
  /**
@@ -347,10 +372,12 @@ function predictFc43_14Response(buffer, start, end) {
347
372
  function promisifyCb(fn) {
348
373
  return new Promise((resolve, reject) => {
349
374
  fn((err) => {
350
- if (err)
375
+ if (err) {
351
376
  reject(err);
352
- else
377
+ }
378
+ else {
353
379
  resolve();
380
+ }
354
381
  });
355
382
  });
356
383
  }
@@ -403,102 +430,172 @@ function resolveRtuTiming(opts = {}, baudRate) {
403
430
  return { intervalBetweenFrames, interCharTimeout };
404
431
  }
405
432
 
406
- /** @internal
407
- * Zero-allocation binary min-heap for coalescing per-request timeouts.
433
+ /**
434
+ * Hybrid timer manager: uses native `setTimeout` for low concurrency
435
+ * and switches to a binary min-heap when concurrency exceeds the threshold.
436
+ *
437
+ * Benchmarks (add + clear throughput, Node 24, x64):
438
+ * 1 concurrent: setTimeout ~1.7× faster than heap
439
+ * 2 concurrent: setTimeout ~1.6× faster than heap
440
+ * 5 concurrent: setTimeout ~1.5-1.9× faster than heap
441
+ * 10 concurrent: roughly equal
442
+ * 20 concurrent: heap ~1.3× faster than setTimeout[]
443
+ * 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
408
444
  *
409
- * Uses two parallel numeric arrays (no object allocation per entry).
410
- * Lazy deletion: callers never remove from the heap; expired entries
411
- * are silently dropped when they surface at the top.
445
+ * The crossover point is around 10 concurrent timers, so the default
446
+ * `concurrentThreshold = 2` keeps the common 1-2 request case on the
447
+ * fast direct path while delegating to the heap for larger batches.
412
448
  */
413
449
  class TimerHeap {
414
450
  _deadlines = [];
415
451
  _ids = [];
452
+ _seqs = [];
453
+ _counter = 0;
416
454
  _timer = null;
417
455
  _onFire;
418
- /** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
419
456
  _boundTick;
420
- constructor(onFire) {
457
+ _threshold;
458
+ _mode = 'direct';
459
+ _directTimers = new Map();
460
+ /**
461
+ * @param onFire Callback invoked with the timer id when it expires.
462
+ * @param concurrentThreshold Maximum number of timers kept as individual
463
+ * native `setTimeout` handles. Once exceeded, all timers migrate to
464
+ * the internal heap and share a single native timer. Default is 2.
465
+ */
466
+ constructor(onFire, concurrentThreshold = 2) {
421
467
  this._onFire = onFire;
422
468
  this._boundTick = this._onTick.bind(this);
469
+ this._threshold = concurrentThreshold;
423
470
  }
424
- /** Number of pending timers in the heap. */
425
471
  get size() {
426
- return this._deadlines.length;
472
+ return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
427
473
  }
428
- /** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
429
474
  add(id, ms) {
475
+ if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
476
+ const deadline = performance.now() + ms;
477
+ const handle = setTimeout(() => {
478
+ if (this._mode !== 'direct') {
479
+ return;
480
+ }
481
+ this._directTimers.delete(id);
482
+ this._onFire(id);
483
+ }, ms);
484
+ this._directTimers.set(id, { handle, deadline });
485
+ return;
486
+ }
487
+ if (this._mode === 'direct') {
488
+ this._mode = 'heap';
489
+ for (const [existingId, { handle, deadline }] of this._directTimers) {
490
+ clearTimeout(handle);
491
+ const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
492
+ if (remaining === 0) {
493
+ this._onFire(existingId);
494
+ }
495
+ else {
496
+ this._heapAdd(existingId, remaining);
497
+ }
498
+ }
499
+ this._directTimers.clear();
500
+ }
501
+ this._heapAdd(id, ms);
502
+ }
503
+ clear() {
504
+ for (const { handle } of this._directTimers.values()) {
505
+ clearTimeout(handle);
506
+ }
507
+ this._directTimers.clear();
508
+ this._mode = 'direct';
509
+ if (this._timer) {
510
+ clearTimeout(this._timer);
511
+ this._timer = null;
512
+ }
513
+ this._deadlines.length = 0;
514
+ this._ids.length = 0;
515
+ this._seqs.length = 0;
516
+ this._counter = 0;
517
+ }
518
+ _heapAdd(id, ms) {
430
519
  const deadline = performance.now() + ms;
520
+ const seq = this._counter++;
431
521
  let i = this._deadlines.length;
432
522
  this._deadlines.push(deadline);
433
523
  this._ids.push(id);
434
- // sift up
524
+ this._seqs.push(seq);
435
525
  while (i > 0) {
436
526
  const p = (i - 1) >> 1;
437
- if (this._deadlines[p] <= deadline)
527
+ const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
528
+ if (parentComesFirst) {
438
529
  break;
530
+ }
439
531
  this._deadlines[i] = this._deadlines[p];
440
532
  this._ids[i] = this._ids[p];
533
+ this._seqs[i] = this._seqs[p];
441
534
  i = p;
442
535
  }
443
536
  this._deadlines[i] = deadline;
444
537
  this._ids[i] = id;
445
- // Only reschedule when the new entry became the heap top.
446
- if (i === 0)
538
+ this._seqs[i] = seq;
539
+ if (i === 0) {
447
540
  this._refresh();
448
- }
449
- /** Dispose without firing callbacks. */
450
- clear() {
451
- if (this._timer) {
452
- clearTimeout(this._timer);
453
- this._timer = null;
454
541
  }
455
- this._deadlines.length = 0;
456
- this._ids.length = 0;
457
542
  }
458
543
  _refresh() {
459
544
  if (this._timer) {
460
545
  clearTimeout(this._timer);
461
546
  this._timer = null;
462
547
  }
463
- if (this._deadlines.length === 0)
548
+ if (this._deadlines.length === 0) {
464
549
  return;
550
+ }
465
551
  const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
466
- this._timer = setTimeout(this._boundTick, delay);
552
+ const safeDelay = Math.min(delay, 2147483647);
553
+ this._timer = setTimeout(this._boundTick, safeDelay);
467
554
  }
468
555
  _onTick() {
469
556
  this._timer = null;
470
557
  const now = performance.now();
471
- while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
472
- const id = this._pop();
473
- this._onFire(id);
558
+ try {
559
+ while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
560
+ const id = this._pop();
561
+ this._onFire(id);
562
+ }
563
+ }
564
+ finally {
565
+ this._refresh();
474
566
  }
475
- this._refresh();
476
567
  }
477
- /** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
478
568
  _pop() {
479
569
  const topId = this._ids[0];
480
570
  const lastId = this._ids.pop();
481
571
  const lastDeadline = this._deadlines.pop();
572
+ const lastSeq = this._seqs.pop();
482
573
  const n = this._deadlines.length;
483
574
  if (n > 0) {
484
575
  let i = 0;
485
- // sift down
486
- while (true) {
487
- let min = i;
488
- const l = i * 2 + 1;
489
- const r = l + 1;
490
- if (l < n && this._deadlines[l] < this._deadlines[min])
491
- min = l;
492
- if (r < n && this._deadlines[r] < this._deadlines[min])
493
- min = r;
494
- if (min === i)
576
+ const half = n >> 1;
577
+ while (i < half) {
578
+ let minChild = (i << 1) + 1;
579
+ const rightChild = minChild + 1;
580
+ if (rightChild < n) {
581
+ const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
582
+ (this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
583
+ if (rightComesFirst) {
584
+ minChild = rightChild;
585
+ }
586
+ }
587
+ const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
588
+ if (lastComesFirst) {
495
589
  break;
496
- this._deadlines[i] = this._deadlines[min];
497
- this._ids[i] = this._ids[min];
498
- i = min;
590
+ }
591
+ this._deadlines[i] = this._deadlines[minChild];
592
+ this._ids[i] = this._ids[minChild];
593
+ this._seqs[i] = this._seqs[minChild];
594
+ i = minChild;
499
595
  }
500
596
  this._deadlines[i] = lastDeadline;
501
597
  this._ids[i] = lastId;
598
+ this._seqs[i] = lastSeq;
502
599
  }
503
600
  return topId;
504
601
  }
@@ -1618,8 +1715,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1618
1715
  while (state.end - state.start > 0) {
1619
1716
  const available = state.end - state.start;
1620
1717
  if (available < MIN_FRAME_LENGTH) {
1621
- if (this._handleIncomplete(state, strict))
1718
+ if (this._handleIncomplete(state, strict)) {
1622
1719
  return;
1720
+ }
1623
1721
  break;
1624
1722
  }
1625
1723
  const fc = pool[state.start + 1];
@@ -1648,8 +1746,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1648
1746
  state.start += 1;
1649
1747
  continue;
1650
1748
  }
1651
- if (this._handleIncomplete(state, strict))
1749
+ if (this._handleIncomplete(state, strict)) {
1652
1750
  return;
1751
+ }
1653
1752
  break;
1654
1753
  }
1655
1754
  if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
@@ -1663,8 +1762,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1663
1762
  state.start += 1;
1664
1763
  continue;
1665
1764
  }
1666
- if (this._handleIncomplete(state, strict))
1765
+ if (this._handleIncomplete(state, strict)) {
1667
1766
  return;
1767
+ }
1668
1768
  break;
1669
1769
  }
1670
1770
  // CRC check inline: no helper call, no subarray for the CRC body.
@@ -1726,8 +1826,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1726
1826
  buffer[0] = unit;
1727
1827
  buffer[1] = fc;
1728
1828
  if (data.length <= 16) {
1729
- for (let i = 0; i < data.length; i++)
1829
+ for (let i = 0; i < data.length; i++) {
1730
1830
  buffer[2 + i] = data[i];
1831
+ }
1731
1832
  }
1732
1833
  else {
1733
1834
  buffer.set(data, 2);
@@ -1746,9 +1847,8 @@ const CHAR_CODE = {
1746
1847
  CR: '\r'.charCodeAt(0),
1747
1848
  LF: '\n'.charCodeAt(0),
1748
1849
  };
1749
- // Modbus ASCII frame encodes at most 256 PDU bytes as 512 hex chars between
1750
- // `:` and `\r`. Cap per-connection buffering so a peer that never sends `\r`
1751
- // cannot grow `state.frame` without bound.
1850
+ // Modbus ASCII frame body is capped well below the theoretical maximum so a
1851
+ // peer that never sends `\r` cannot grow `state.frame` without bound.
1752
1852
  const MAX_ASCII_PAYLOAD = 512;
1753
1853
  const HEX_DECODE = new Uint8Array(256);
1754
1854
  HEX_DECODE.fill(0xff);
@@ -1885,14 +1985,16 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1885
1985
  // dataLen may be 0 for a frame that is only unit + fc + lrc.
1886
1986
  const dataLen = byteLen - 3;
1887
1987
  const data = Buffer.allocUnsafe(dataLen);
1988
+ let hexOff = 4;
1888
1989
  for (let i = 0; i < dataLen; i++) {
1889
- const hi = HEX_DECODE[hexChars[4 + i * 2]];
1890
- const lo = HEX_DECODE[hexChars[4 + i * 2 + 1]];
1990
+ const hi = HEX_DECODE[hexChars[hexOff]];
1991
+ const lo = HEX_DECODE[hexChars[hexOff + 1]];
1891
1992
  if (hi === 0xff || lo === 0xff) {
1892
1993
  this.onFramingError(new Error('Invalid hex character'));
1893
1994
  return;
1894
1995
  }
1895
1996
  data[i] = (hi << 4) | lo;
1997
+ hexOff += 2;
1896
1998
  }
1897
1999
  // Compute LRC over unit + fc + data.
1898
2000
  let sum = unit + fc;
@@ -1923,13 +2025,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1923
2025
  buffer[0] = unit;
1924
2026
  buffer[1] = fc;
1925
2027
  buffer.set(data, 2);
1926
- buffer[buffer.length - 1] = lrc(buffer.subarray(0, -1));
2028
+ buffer[buffer.length - 1] = lrc(buffer, 0, buffer.length - 1);
1927
2029
  const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
1928
2030
  out[0] = CHAR_CODE.COLON;
2031
+ let outOff = 1;
1929
2032
  for (let i = 0; i < buffer.length; i++) {
1930
2033
  const byte = buffer[i];
1931
- out[1 + i * 2] = HEX_ENCODE[byte >> 4];
1932
- out[2 + i * 2] = HEX_ENCODE[byte & 0x0f];
2034
+ out[outOff] = HEX_ENCODE[byte >> 4];
2035
+ out[outOff + 1] = HEX_ENCODE[byte & 0x0f];
2036
+ outOff += 2;
1933
2037
  }
1934
2038
  out[out.length - 2] = CHAR_CODE.CR;
1935
2039
  out[out.length - 1] = CHAR_CODE.LF;
@@ -2032,7 +2136,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2032
2136
  const frame = {
2033
2137
  // Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
2034
2138
  // argument coercion + bounds check. Symmetric to the header writes in
2035
- // encode() above. Hits on every received TCP frame.
2139
+ // encode() below. Hits on every received TCP frame.
2036
2140
  transaction: (buffer[0] << 8) | buffer[1],
2037
2141
  unit: buffer[6],
2038
2142
  fc: buffer[7],
@@ -2061,8 +2165,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2061
2165
  // Small-payload fast path: avoid C++ TypedArray.prototype.set boundary
2062
2166
  // crossing when the copy is just a few bytes (common for FC 3/4/6 requests).
2063
2167
  if (data.length <= 16) {
2064
- for (let i = 0; i < data.length; i++)
2168
+ for (let i = 0; i < data.length; i++) {
2065
2169
  buffer[8 + i] = data[i];
2170
+ }
2066
2171
  }
2067
2172
  else {
2068
2173
  buffer.set(data, 8);
@@ -2110,26 +2215,33 @@ class MasterSession {
2110
2215
  }
2111
2216
 
2112
2217
  function validateResponse(frame, unit, fc) {
2113
- if (frame.unit !== unit || frame.fc !== fc)
2218
+ if (frame.unit !== unit || frame.fc !== fc) {
2114
2219
  throw new Error('Invalid response');
2220
+ }
2115
2221
  }
2116
2222
  function validateByteCountResponse(frame, unit, fc, byteCount) {
2117
2223
  validateResponse(frame, unit, fc);
2118
- if (frame.data.length < 1 + byteCount)
2224
+ if (frame.data.length < 1 + byteCount) {
2119
2225
  throw new Error('Insufficient data length');
2120
- if (frame.data.length !== 1 + byteCount)
2226
+ }
2227
+ if (frame.data.length !== 1 + byteCount) {
2121
2228
  throw new Error('Invalid response');
2122
- if (frame.data[0] !== byteCount)
2229
+ }
2230
+ if (frame.data[0] !== byteCount) {
2123
2231
  throw new Error('Invalid response');
2232
+ }
2124
2233
  }
2125
2234
  function validateEchoResponse(frame, unit, fc, expected) {
2126
2235
  validateResponse(frame, unit, fc);
2127
- if (frame.data.length < expected.length)
2236
+ if (frame.data.length < expected.length) {
2128
2237
  throw new Error('Insufficient data length');
2129
- if (frame.data.length !== expected.length)
2238
+ }
2239
+ if (frame.data.length !== expected.length) {
2130
2240
  throw new Error('Invalid response');
2131
- if (!frame.data.equals(expected))
2241
+ }
2242
+ if (!frame.data.equals(expected)) {
2132
2243
  throw new Error('Invalid response');
2244
+ }
2133
2245
  }
2134
2246
  class ModbusMaster extends EventEmitter {
2135
2247
  timeout;
@@ -2157,8 +2269,9 @@ class ModbusMaster extends EventEmitter {
2157
2269
  _pendingExchanges = new Map();
2158
2270
  _timerHeap = new TimerHeap((id) => {
2159
2271
  const pending = this._pendingExchanges.get(id);
2160
- if (!pending)
2161
- return; // lazy deletion: already handled
2272
+ if (!pending) {
2273
+ return;
2274
+ } // lazy deletion: already handled
2162
2275
  pending.settled = true;
2163
2276
  this._pendingExchanges.delete(id);
2164
2277
  if (pending.sessionKey !== null) {
@@ -2345,7 +2458,9 @@ class ModbusMaster extends EventEmitter {
2345
2458
  }
2346
2459
  // Lazy-deletion timer architecture:
2347
2460
  // 1. Assign an exchangeId and register in _pendingExchanges.
2348
- // 2. Push deadline into the global TimerHeap (no per-request setTimeout).
2461
+ // 2. Push deadline into the global TimerHeap (one native setTimeout under
2462
+ // load; a fast direct-timer path is used when only 1-2 exchanges are
2463
+ // pending).
2349
2464
  // 3. When the response arrives, delete from Map — the heap entry is left
2350
2465
  // behind and silently discarded when it surfaces at the top (lazy deletion).
2351
2466
  const exchangeId = this._nextExchangeId++;
@@ -2356,11 +2471,13 @@ class ModbusMaster extends EventEmitter {
2356
2471
  this._timerHeap.add(exchangeId, timeout);
2357
2472
  connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
2358
2473
  const p = this._pendingExchanges.get(exchangeId);
2359
- if (!p || p.settled)
2474
+ if (!p || p.settled) {
2360
2475
  return;
2476
+ }
2361
2477
  const cb = p.callback;
2362
- if (!cb)
2478
+ if (!cb) {
2363
2479
  return;
2480
+ }
2364
2481
  p.settled = true;
2365
2482
  p.callback = null;
2366
2483
  this._pendingExchanges.delete(exchangeId);
@@ -2389,8 +2506,9 @@ class ModbusMaster extends EventEmitter {
2389
2506
  this._timerHeap.add(exchangeId, timeout);
2390
2507
  connection.write(payload, (writeErr) => {
2391
2508
  const p = this._pendingExchanges.get(exchangeId);
2392
- if (!p || p.settled)
2509
+ if (!p || p.settled) {
2393
2510
  return;
2511
+ }
2394
2512
  if (writeErr) {
2395
2513
  const cb = p.callback;
2396
2514
  if (cb) {
@@ -2415,8 +2533,9 @@ class ModbusMaster extends EventEmitter {
2415
2533
  // Timeout is managed by the global timer heap above.
2416
2534
  this._masterSession.start(key, (err, frame) => {
2417
2535
  const p2 = this._pendingExchanges.get(exchangeId);
2418
- if (!p2 || p2.settled)
2536
+ if (!p2 || p2.settled) {
2419
2537
  return;
2538
+ }
2420
2539
  const cb = p2.callback;
2421
2540
  if (cb) {
2422
2541
  p2.settled = true;
@@ -2449,8 +2568,42 @@ class ModbusMaster extends EventEmitter {
2449
2568
  try {
2450
2569
  validateByteCountResponse(frame, unit, fc, byteCount);
2451
2570
  const data = new Array(length);
2452
- for (let i = 0; i < length; i++) {
2453
- data[i] = (frame.data[1 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
2571
+ let byteIdx = 1;
2572
+ let outIdx = 0;
2573
+ const fullBytes = length >> 3;
2574
+ for (let b = 0; b < fullBytes; b++) {
2575
+ const byte = frame.data[byteIdx++];
2576
+ data[outIdx++] = (byte & 0x01) > 0;
2577
+ data[outIdx++] = (byte & 0x02) > 0;
2578
+ data[outIdx++] = (byte & 0x04) > 0;
2579
+ data[outIdx++] = (byte & 0x08) > 0;
2580
+ data[outIdx++] = (byte & 0x10) > 0;
2581
+ data[outIdx++] = (byte & 0x20) > 0;
2582
+ data[outIdx++] = (byte & 0x40) > 0;
2583
+ data[outIdx++] = (byte & 0x80) > 0;
2584
+ }
2585
+ const rem = length & 7;
2586
+ if (rem) {
2587
+ const byte = frame.data[byteIdx];
2588
+ data[outIdx++] = (byte & 0x01) > 0;
2589
+ if (rem > 1) {
2590
+ data[outIdx++] = (byte & 0x02) > 0;
2591
+ }
2592
+ if (rem > 2) {
2593
+ data[outIdx++] = (byte & 0x04) > 0;
2594
+ }
2595
+ if (rem > 3) {
2596
+ data[outIdx++] = (byte & 0x08) > 0;
2597
+ }
2598
+ if (rem > 4) {
2599
+ data[outIdx++] = (byte & 0x10) > 0;
2600
+ }
2601
+ if (rem > 5) {
2602
+ data[outIdx++] = (byte & 0x20) > 0;
2603
+ }
2604
+ if (rem > 6) {
2605
+ data[outIdx++] = (byte & 0x40) > 0;
2606
+ }
2454
2607
  }
2455
2608
  // Mutate the frame in place rather than spread-copying — `frame` is freshly
2456
2609
  // allocated per request and not retained anywhere else.
@@ -2498,9 +2651,10 @@ class ModbusMaster extends EventEmitter {
2498
2651
  // on each call. Symmetric to the slave-side BE write inlining
2499
2652
  // in handleFC3/FC4. At length=125 (FC3 max) that's 250 saved
2500
2653
  // bounds-check pairs per response.
2654
+ let off = 0;
2501
2655
  for (let i = 0; i < length; i++) {
2502
- const off = i * 2;
2503
2656
  data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
2657
+ off += 2;
2504
2658
  }
2505
2659
  frame.data = data;
2506
2660
  resolve(frame);
@@ -2591,10 +2745,19 @@ class ModbusMaster extends EventEmitter {
2591
2745
  bufferTx[2] = (value.length >>> 8) & 0xff;
2592
2746
  bufferTx[3] = value.length & 0xff;
2593
2747
  bufferTx[4] = byteCount;
2748
+ let acc = 0;
2749
+ let out = 5;
2594
2750
  for (let i = 0; i < value.length; i++) {
2595
2751
  if (value[i]) {
2596
- bufferTx[5 + Math.floor(i / 8)] |= 1 << i % 8;
2752
+ acc |= 1 << (i & 7);
2597
2753
  }
2754
+ if ((i & 7) === 7) {
2755
+ bufferTx[out++] = acc;
2756
+ acc = 0;
2757
+ }
2758
+ }
2759
+ if ((value.length & 7) !== 0) {
2760
+ bufferTx[out] = acc;
2598
2761
  }
2599
2762
  return new Promise((resolve, reject) => {
2600
2763
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2628,11 +2791,12 @@ class ModbusMaster extends EventEmitter {
2628
2791
  bufferTx[2] = (value.length >>> 8) & 0xff;
2629
2792
  bufferTx[3] = value.length & 0xff;
2630
2793
  bufferTx[4] = byteCount;
2794
+ let off = 5;
2631
2795
  for (let i = 0; i < value.length; i++) {
2632
2796
  const v = value[i];
2633
- const off = 5 + i * 2;
2634
2797
  bufferTx[off] = (v >>> 8) & 0xff;
2635
2798
  bufferTx[off + 1] = v & 0xff;
2799
+ off += 2;
2636
2800
  }
2637
2801
  return new Promise((resolve, reject) => {
2638
2802
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2670,10 +2834,12 @@ class ModbusMaster extends EventEmitter {
2670
2834
  }
2671
2835
  try {
2672
2836
  validateResponse(frame, unit, fc);
2673
- if (frame.data.length < 2 + serverIdLength)
2837
+ if (frame.data.length < 2 + serverIdLength) {
2674
2838
  throw new Error('Insufficient data length');
2675
- if (frame.data.length !== 1 + frame.data[0])
2839
+ }
2840
+ if (frame.data.length !== 1 + frame.data[0]) {
2676
2841
  throw new Error('Invalid response');
2842
+ }
2677
2843
  const runStatusIndex = 1 + serverIdLength;
2678
2844
  frame.data = {
2679
2845
  serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
@@ -2736,11 +2902,12 @@ class ModbusMaster extends EventEmitter {
2736
2902
  bufferTx[6] = (write.value.length >>> 8) & 0xff;
2737
2903
  bufferTx[7] = write.value.length & 0xff;
2738
2904
  bufferTx[8] = byteCount;
2905
+ let off = 9;
2739
2906
  for (let i = 0; i < write.value.length; i++) {
2740
2907
  const v = write.value[i];
2741
- const off = 9 + i * 2;
2742
2908
  bufferTx[off] = (v >>> 8) & 0xff;
2743
2909
  bufferTx[off + 1] = v & 0xff;
2910
+ off += 2;
2744
2911
  }
2745
2912
  return new Promise((resolve, reject) => {
2746
2913
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2759,9 +2926,10 @@ class ModbusMaster extends EventEmitter {
2759
2926
  // closure + N readUInt16BE bounds-check pairs. See writeFC3Or4
2760
2927
  // response handler for the same optimization.
2761
2928
  const data = new Array(read.length);
2929
+ let off = 0;
2762
2930
  for (let i = 0; i < read.length; i++) {
2763
- const off = i * 2;
2764
2931
  data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
2932
+ off += 2;
2765
2933
  }
2766
2934
  frame.data = data;
2767
2935
  resolve(frame);
@@ -2787,10 +2955,12 @@ class ModbusMaster extends EventEmitter {
2787
2955
  }
2788
2956
  try {
2789
2957
  validateResponse(frame, unit, fc);
2790
- if (frame.data.length < 6)
2958
+ if (frame.data.length < 6) {
2791
2959
  throw new Error('Insufficient data length');
2792
- if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode)
2960
+ }
2961
+ if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode) {
2793
2962
  throw new Error('Invalid response');
2963
+ }
2794
2964
  const objects = [];
2795
2965
  let object = [];
2796
2966
  let totalBytes = 0;
@@ -2818,10 +2988,12 @@ class ModbusMaster extends EventEmitter {
2818
2988
  break;
2819
2989
  }
2820
2990
  }
2821
- if (objects.length !== frame.data[5])
2991
+ if (objects.length !== frame.data[5]) {
2822
2992
  throw new Error('Invalid response');
2823
- if (frame.data.length !== 6 + totalBytes)
2993
+ }
2994
+ if (frame.data.length !== 6 + totalBytes) {
2824
2995
  throw new Error('Invalid response');
2996
+ }
2825
2997
  frame.data = {
2826
2998
  readDeviceIDCode,
2827
2999
  conformityLevel: frame.data[2],
@@ -2868,7 +3040,7 @@ class ModbusMaster extends EventEmitter {
2868
3040
  });
2869
3041
  }
2870
3042
  /**
2871
- * Open the underlying physical layer and begin accepting connections.
3043
+ * Open the underlying physical layer and establish a connection.
2872
3044
  *
2873
3045
  * A `ModbusMaster` instance can only be opened once. Once {@link close}
2874
3046
  * is called — explicitly or because the physical layer disconnected —
@@ -2898,8 +3070,9 @@ class ModbusMaster extends EventEmitter {
2898
3070
  // (broadcasts have no session waiter; non-broadcasts still in the
2899
3071
  // pre-write-window haven't registered in session yet).
2900
3072
  for (const pending of this._pendingExchanges.values()) {
2901
- if (pending.settled)
3073
+ if (pending.settled) {
2902
3074
  continue;
3075
+ }
2903
3076
  pending.settled = true;
2904
3077
  const cb = pending.callback;
2905
3078
  if (cb) {
@@ -3057,23 +3230,68 @@ class ModbusSlave extends EventEmitter {
3057
3230
  const byteCount = (length + 7) >> 3;
3058
3231
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3059
3232
  pdu[0] = byteCount;
3060
- // Pack 8 booleans per byte without first zero-filling: accumulate into
3061
- // `acc` and write a full byte once each lane is finished. Saves the
3062
- // `pdu.fill(0, 1, ...)` pass and replaces N `|=` reads-modify-writes
3063
- // with N `read`s + ⌈N/8⌉ `write`s. Measured ~+5% on FC01 / ~+3% on FC02
3064
- // at max payload (2000 / 1968 coils) via benchmark/all-fcs.ts bisect.
3065
- let acc = 0;
3066
- let out = 1;
3067
- for (let i = 0; i < length; i++) {
3068
- if (coils[i])
3069
- acc |= 1 << (i & 7);
3070
- if ((i & 7) === 7) {
3071
- pdu[out++] = acc;
3072
- acc = 0;
3233
+ if (coils instanceof Uint8Array) {
3234
+ // Branchless fast path `coils[i]` is already 0/1, no boolean
3235
+ // coercion or conditional jumps. At max payload (2000 coils) this
3236
+ // avoids 2000 branch-predictor slots and boolean-to-number casts.
3237
+ let out = 1;
3238
+ const fullBytes = length >> 3;
3239
+ for (let i = 0; i < fullBytes; i++) {
3240
+ const base = i << 3;
3241
+ pdu[out++] =
3242
+ (coils[base] & 1) |
3243
+ ((coils[base + 1] & 1) << 1) |
3244
+ ((coils[base + 2] & 1) << 2) |
3245
+ ((coils[base + 3] & 1) << 3) |
3246
+ ((coils[base + 4] & 1) << 4) |
3247
+ ((coils[base + 5] & 1) << 5) |
3248
+ ((coils[base + 6] & 1) << 6) |
3249
+ ((coils[base + 7] & 1) << 7);
3250
+ }
3251
+ const rem = length & 7;
3252
+ if (rem) {
3253
+ const base = fullBytes << 3;
3254
+ let acc = coils[base] & 1;
3255
+ if (rem > 1) {
3256
+ acc |= (coils[base + 1] & 1) << 1;
3257
+ }
3258
+ if (rem > 2) {
3259
+ acc |= (coils[base + 2] & 1) << 2;
3260
+ }
3261
+ if (rem > 3) {
3262
+ acc |= (coils[base + 3] & 1) << 3;
3263
+ }
3264
+ if (rem > 4) {
3265
+ acc |= (coils[base + 4] & 1) << 4;
3266
+ }
3267
+ if (rem > 5) {
3268
+ acc |= (coils[base + 5] & 1) << 5;
3269
+ }
3270
+ if (rem > 6) {
3271
+ acc |= (coils[base + 6] & 1) << 6;
3272
+ }
3273
+ pdu[out] = acc;
3274
+ }
3275
+ }
3276
+ else {
3277
+ // Fallback for boolean[] — accumulate into `acc` and write a full byte
3278
+ // once each lane is finished. Saves N `|=` read-modify-writes on the
3279
+ // output buffer.
3280
+ let acc = 0;
3281
+ let out = 1;
3282
+ for (let i = 0; i < length; i++) {
3283
+ if (coils[i]) {
3284
+ acc |= 1 << (i & 7);
3285
+ }
3286
+ if ((i & 7) === 7) {
3287
+ pdu[out++] = acc;
3288
+ acc = 0;
3289
+ }
3290
+ }
3291
+ if ((length & 7) !== 0) {
3292
+ pdu[out] = acc;
3073
3293
  }
3074
3294
  }
3075
- if ((length & 7) !== 0)
3076
- pdu[out] = acc;
3077
3295
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3078
3296
  }
3079
3297
  catch (error) {
@@ -3104,19 +3322,62 @@ class ModbusSlave extends EventEmitter {
3104
3322
  const byteCount = (length + 7) >> 3;
3105
3323
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3106
3324
  pdu[0] = byteCount;
3107
- // Accumulator-based bit pack — see handleFC1 for the rationale.
3108
- let acc = 0;
3109
- let out = 1;
3110
- for (let i = 0; i < length; i++) {
3111
- if (discreteInputs[i])
3112
- acc |= 1 << (i & 7);
3113
- if ((i & 7) === 7) {
3114
- pdu[out++] = acc;
3115
- acc = 0;
3325
+ if (discreteInputs instanceof Uint8Array) {
3326
+ let out = 1;
3327
+ const fullBytes = length >> 3;
3328
+ for (let i = 0; i < fullBytes; i++) {
3329
+ const base = i << 3;
3330
+ pdu[out++] =
3331
+ (discreteInputs[base] & 1) |
3332
+ ((discreteInputs[base + 1] & 1) << 1) |
3333
+ ((discreteInputs[base + 2] & 1) << 2) |
3334
+ ((discreteInputs[base + 3] & 1) << 3) |
3335
+ ((discreteInputs[base + 4] & 1) << 4) |
3336
+ ((discreteInputs[base + 5] & 1) << 5) |
3337
+ ((discreteInputs[base + 6] & 1) << 6) |
3338
+ ((discreteInputs[base + 7] & 1) << 7);
3339
+ }
3340
+ const rem = length & 7;
3341
+ if (rem) {
3342
+ const base = fullBytes << 3;
3343
+ let acc = discreteInputs[base] & 1;
3344
+ if (rem > 1) {
3345
+ acc |= (discreteInputs[base + 1] & 1) << 1;
3346
+ }
3347
+ if (rem > 2) {
3348
+ acc |= (discreteInputs[base + 2] & 1) << 2;
3349
+ }
3350
+ if (rem > 3) {
3351
+ acc |= (discreteInputs[base + 3] & 1) << 3;
3352
+ }
3353
+ if (rem > 4) {
3354
+ acc |= (discreteInputs[base + 4] & 1) << 4;
3355
+ }
3356
+ if (rem > 5) {
3357
+ acc |= (discreteInputs[base + 5] & 1) << 5;
3358
+ }
3359
+ if (rem > 6) {
3360
+ acc |= (discreteInputs[base + 6] & 1) << 6;
3361
+ }
3362
+ pdu[out] = acc;
3363
+ }
3364
+ }
3365
+ else {
3366
+ let acc = 0;
3367
+ let out = 1;
3368
+ for (let i = 0; i < length; i++) {
3369
+ if (discreteInputs[i]) {
3370
+ acc |= 1 << (i & 7);
3371
+ }
3372
+ if ((i & 7) === 7) {
3373
+ pdu[out++] = acc;
3374
+ acc = 0;
3375
+ }
3376
+ }
3377
+ if ((length & 7) !== 0) {
3378
+ pdu[out] = acc;
3116
3379
  }
3117
3380
  }
3118
- if ((length & 7) !== 0)
3119
- pdu[out] = acc;
3120
3381
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3121
3382
  }
3122
3383
  catch (error) {
@@ -3149,11 +3410,12 @@ class ModbusSlave extends EventEmitter {
3149
3410
  // Inline big-endian write — `pdu[i] = v` is a direct typed-array store,
3150
3411
  // while `writeUInt16BE` runs argument validation + bounds checks on each
3151
3412
  // call. At length=125 (FC3 max) that's 250 saved checks per request.
3413
+ let off = 1;
3152
3414
  for (let i = 0; i < length; i++) {
3153
3415
  const v = registers[i];
3154
- const off = 1 + i * 2;
3155
3416
  pdu[off] = (v >>> 8) & 0xff;
3156
3417
  pdu[off + 1] = v & 0xff;
3418
+ off += 2;
3157
3419
  }
3158
3420
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3159
3421
  }
@@ -3185,11 +3447,12 @@ class ModbusSlave extends EventEmitter {
3185
3447
  const pdu = Buffer.allocUnsafe(length * 2 + 1);
3186
3448
  pdu[0] = length * 2;
3187
3449
  // Inline big-endian write — see handleFC3 for the rationale.
3450
+ let off = 1;
3188
3451
  for (let i = 0; i < length; i++) {
3189
3452
  const v = registers[i];
3190
- const off = 1 + i * 2;
3191
3453
  pdu[off] = (v >>> 8) & 0xff;
3192
3454
  pdu[off + 1] = v & 0xff;
3455
+ off += 2;
3193
3456
  }
3194
3457
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3195
3458
  }
@@ -3268,12 +3531,42 @@ class ModbusSlave extends EventEmitter {
3268
3531
  return;
3269
3532
  }
3270
3533
  const value = new Array(length);
3271
- for (let i = 0; i < length; i++) {
3272
- // Bit math: kept as `Math.floor(i / 8)` / `i % 8` — V8 pattern-matches
3273
- // both forms with `i` as an integer loop counter to direct shifts, but
3274
- // measured ~+3% throughput vs the bit-shift form on this exact loop
3275
- // (benchmark/all-fcs.ts FC15 slave A/B, 8/8 paired samples).
3276
- value[i] = (frame.data[5 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
3534
+ let byteIdx = 5;
3535
+ let outIdx = 0;
3536
+ const fullBytes = length >> 3;
3537
+ for (let b = 0; b < fullBytes; b++) {
3538
+ const byte = frame.data[byteIdx++];
3539
+ value[outIdx++] = (byte & 0x01) > 0;
3540
+ value[outIdx++] = (byte & 0x02) > 0;
3541
+ value[outIdx++] = (byte & 0x04) > 0;
3542
+ value[outIdx++] = (byte & 0x08) > 0;
3543
+ value[outIdx++] = (byte & 0x10) > 0;
3544
+ value[outIdx++] = (byte & 0x20) > 0;
3545
+ value[outIdx++] = (byte & 0x40) > 0;
3546
+ value[outIdx++] = (byte & 0x80) > 0;
3547
+ }
3548
+ const rem = length & 7;
3549
+ if (rem) {
3550
+ const byte = frame.data[byteIdx];
3551
+ value[outIdx++] = (byte & 0x01) > 0;
3552
+ if (rem > 1) {
3553
+ value[outIdx++] = (byte & 0x02) > 0;
3554
+ }
3555
+ if (rem > 2) {
3556
+ value[outIdx++] = (byte & 0x04) > 0;
3557
+ }
3558
+ if (rem > 3) {
3559
+ value[outIdx++] = (byte & 0x08) > 0;
3560
+ }
3561
+ if (rem > 4) {
3562
+ value[outIdx++] = (byte & 0x10) > 0;
3563
+ }
3564
+ if (rem > 5) {
3565
+ value[outIdx++] = (byte & 0x20) > 0;
3566
+ }
3567
+ if (rem > 6) {
3568
+ value[outIdx++] = (byte & 0x40) > 0;
3569
+ }
3277
3570
  }
3278
3571
  try {
3279
3572
  if (model.writeMultipleCoils) {
@@ -3311,8 +3604,10 @@ class ModbusSlave extends EventEmitter {
3311
3604
  return;
3312
3605
  }
3313
3606
  const value = new Array(length);
3607
+ let off = 5;
3314
3608
  for (let i = 0; i < length; i++) {
3315
- value[i] = (frame.data[5 + i * 2] << 8) | frame.data[6 + i * 2];
3609
+ value[i] = (frame.data[off] << 8) | frame.data[off + 1];
3610
+ off += 2;
3316
3611
  }
3317
3612
  try {
3318
3613
  if (model.writeMultipleRegisters) {
@@ -3423,8 +3718,10 @@ class ModbusSlave extends EventEmitter {
3423
3718
  return;
3424
3719
  }
3425
3720
  const value = new Array(length.write);
3721
+ let off = 9;
3426
3722
  for (let i = 0; i < length.write; i++) {
3427
- value[i] = (frame.data[9 + i * 2] << 8) | frame.data[10 + i * 2];
3723
+ value[i] = (frame.data[off] << 8) | frame.data[off + 1];
3724
+ off += 2;
3428
3725
  }
3429
3726
  try {
3430
3727
  await this._withIntervalLock(address.write, address.write + length.write, async () => {
@@ -3441,11 +3738,12 @@ class ModbusSlave extends EventEmitter {
3441
3738
  const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
3442
3739
  pdu[0] = length.read * 2;
3443
3740
  // Inline big-endian write — see handleFC3 for the rationale.
3741
+ let off = 1;
3444
3742
  for (let i = 0; i < length.read; i++) {
3445
3743
  const v = registers[i];
3446
- const off = 1 + i * 2;
3447
3744
  pdu[off] = (v >>> 8) & 0xff;
3448
3745
  pdu[off + 1] = v & 0xff;
3746
+ off += 2;
3449
3747
  }
3450
3748
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3451
3749
  }
@@ -3677,8 +3975,9 @@ class ModbusSlave extends EventEmitter {
3677
3975
  for (let i = 0; i < locks.length; i++) {
3678
3976
  const l = locks[i];
3679
3977
  if (l.lo < hi && lo < l.hi) {
3680
- if (overlap === null)
3978
+ if (overlap === null) {
3681
3979
  overlap = [];
3980
+ }
3682
3981
  overlap.push(l.promise);
3683
3982
  }
3684
3983
  }
@@ -3714,8 +4013,9 @@ class ModbusSlave extends EventEmitter {
3714
4013
  if (i !== -1) {
3715
4014
  // O(1) swap-and-pop since lock order doesn't matter for correctness.
3716
4015
  const last = locks.length - 1;
3717
- if (i !== last)
4016
+ if (i !== last) {
3718
4017
  locks[i] = locks[last];
4018
+ }
3719
4019
  locks.pop();
3720
4020
  }
3721
4021
  }