njs-modbus 3.2.0 → 3.4.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.cjs CHANGED
@@ -166,40 +166,75 @@ function bitsToMs(baudRate, bits) {
166
166
  * open / close / destroy callbacks.
167
167
  */
168
168
  function drainCbs(cbs, err) {
169
- if (!cbs)
169
+ if (!cbs) {
170
170
  return;
171
+ }
171
172
  for (const cb of cbs) {
172
173
  cb?.(err);
173
174
  }
174
175
  }
175
176
 
176
- function inRange(n, [min, max]) {
177
- return n >= min && n <= max;
178
- }
179
- function isRangeArray(range) {
180
- return Array.isArray(range[0]);
181
- }
182
177
  function checkRange(value, range) {
183
178
  if (!range || range.length === 0) {
184
179
  return true;
185
180
  }
186
- const values = Array.isArray(value) ? value : [value];
187
- if (isRangeArray(range)) {
188
- for (const r of range) {
189
- const [min, max] = r;
190
- const [lo, hi] = min <= max ? [min, max] : [max, min];
191
- if (values.every((n) => inRange(n, [lo, hi]))) {
181
+ const isMultiRange = Array.isArray(range[0]);
182
+ const isValueArray = Array.isArray(value);
183
+ if (!isValueArray && !isMultiRange) {
184
+ const r = range;
185
+ const min = r[0], max = r[1];
186
+ const v = value;
187
+ return min <= max ? v >= min && v <= max : v >= max && v <= min;
188
+ }
189
+ if (!isValueArray && isMultiRange) {
190
+ const ranges = range;
191
+ const v = value;
192
+ for (let i = 0; i < ranges.length; i++) {
193
+ const min = ranges[i][0], max = ranges[i][1];
194
+ const lo = min <= max ? min : max;
195
+ const hi = min <= max ? max : min;
196
+ if (v >= lo && v <= hi) {
192
197
  return true;
193
198
  }
194
199
  }
195
200
  return false;
196
201
  }
197
- const [min, max] = range;
198
- const [lo, hi] = min <= max ? [min, max] : [max, min];
199
- return values.every((n) => inRange(n, [lo, hi]));
202
+ const values = value;
203
+ if (values.length === 0) {
204
+ return true;
205
+ }
206
+ if (!isMultiRange) {
207
+ const r = range;
208
+ const min = r[0], max = r[1];
209
+ const lo = min <= max ? min : max;
210
+ const hi = min <= max ? max : min;
211
+ for (let i = 0; i < values.length; i++) {
212
+ if (values[i] < lo || values[i] > hi) {
213
+ return false;
214
+ }
215
+ }
216
+ return true;
217
+ }
218
+ const ranges = range;
219
+ for (let i = 0; i < ranges.length; i++) {
220
+ const min = ranges[i][0], max = ranges[i][1];
221
+ const lo = min <= max ? min : max;
222
+ const hi = min <= max ? max : min;
223
+ let allInRange = true;
224
+ for (let j = 0; j < values.length; j++) {
225
+ if (values[j] < lo || values[j] > hi) {
226
+ allInRange = false;
227
+ break;
228
+ }
229
+ }
230
+ if (allInRange) {
231
+ return true;
232
+ }
233
+ }
234
+ return false;
200
235
  }
201
236
 
202
- const TABLE = [
237
+ const CRC_TABLE = new Uint16Array([
203
238
  0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
204
239
  0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
205
240
  0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
@@ -216,131 +251,127 @@ const TABLE = [
216
251
  0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
217
252
  0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
218
253
  0x4040,
219
- ];
220
- function crc(data, start = 0, end = data.length) {
254
+ ]);
255
+ /** CRC-16 (Modbus) over a single contiguous buffer. */
256
+ function crcFixed(data, start, end) {
221
257
  let crc = 0xffff;
222
258
  for (let index = start; index < end; index++) {
223
- crc = (TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8)) & 0xffff;
259
+ crc = CRC_TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
224
260
  }
225
261
  return crc;
226
262
  }
227
-
228
263
  /**
229
- * Returns true when `n` is an integer in the unsigned-byte range [0, 255].
230
- *
231
- * Used for byte-level Modbus payload validation (function-code values, raw
232
- * byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
233
- * Infinity, and out-of-range values uniformly.
264
+ * CRC-16 (Modbus) over two contiguous buffer segments.
265
+ * Computes CRC(head[headOff:headOff+headLen]) followed by CRC(tail[tailOff:tailOff+tailLen]).
234
266
  */
235
- function isUint8(n) {
236
- return Number.isInteger(n) && n >= 0 && n <= 255;
267
+ function crcDual(head, headOff, headLen, tail, tailOff, tailLen) {
268
+ let crc = 0xffff;
269
+ const headEnd = headOff + headLen;
270
+ for (let i = headOff; i < headEnd; i++) {
271
+ crc = CRC_TABLE[(crc ^ head[i]) & 0xff] ^ (crc >> 8);
272
+ }
273
+ const tailEnd = tailOff + tailLen;
274
+ for (let i = tailOff; i < tailEnd; i++) {
275
+ crc = CRC_TABLE[(crc ^ tail[i]) & 0xff] ^ (crc >> 8);
276
+ }
277
+ return crc;
237
278
  }
238
279
 
239
- function lrc(data, start = 0, end = data.length) {
280
+ function lrc(data, start, end) {
240
281
  let sum = 0;
241
282
  for (let i = start; i < end; i++) {
242
283
  sum += data[i];
243
284
  }
244
- return (~sum + 1) & 0xff;
285
+ return -sum & 0xff;
245
286
  }
246
287
 
247
- const REQUEST_FIXED_LENGTHS = {
248
- [exports.FunctionCode.READ_COILS]: 8,
249
- [exports.FunctionCode.READ_DISCRETE_INPUTS]: 8,
250
- [exports.FunctionCode.READ_HOLDING_REGISTERS]: 8,
251
- [exports.FunctionCode.READ_INPUT_REGISTERS]: 8,
252
- [exports.FunctionCode.WRITE_SINGLE_COIL]: 8,
253
- [exports.FunctionCode.WRITE_SINGLE_REGISTER]: 8,
254
- [exports.FunctionCode.REPORT_SERVER_ID]: 4,
255
- [exports.FunctionCode.MASK_WRITE_REGISTER]: 10,
256
- [exports.FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
257
- };
258
- const REQUEST_BYTE_COUNT = {
259
- [exports.FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
260
- [exports.FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
261
- [exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
262
- };
263
- const RESPONSE_FIXED_LENGTHS = {
264
- [exports.FunctionCode.WRITE_SINGLE_COIL]: 8,
265
- [exports.FunctionCode.WRITE_SINGLE_REGISTER]: 8,
266
- [exports.FunctionCode.WRITE_MULTIPLE_COILS]: 8,
267
- [exports.FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
268
- [exports.FunctionCode.MASK_WRITE_REGISTER]: 10,
269
- };
270
- const RESPONSE_BYTE_COUNT = {
271
- [exports.FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
272
- [exports.FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
273
- [exports.FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
274
- [exports.FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
275
- [exports.FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
276
- [exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
277
- };
278
- /** Sentinel: caller needs to feed more bytes before length can be determined. */
279
288
  const PREDICT_NEED_MORE = 0;
280
- /** Sentinel: function code is not in the standard tables. */
281
289
  const PREDICT_UNKNOWN = -1;
282
- /**
283
- * Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
284
- *
285
- * Returns a sentinel-encoded number to avoid per-call object allocation on the
286
- * RTU decode hot path:
287
- * - Positive integer (>= 4): total frame length, function code is known.
288
- * - {@link PREDICT_NEED_MORE} (0): function code is known but more bytes are
289
- * required (typically waiting on the byteCount byte).
290
- * - {@link PREDICT_UNKNOWN} (-1): function code is not in the standard tables —
291
- * the framing layer must defer to a registered `CustomFunctionCode` or treat
292
- * this as a framing error.
293
- */
294
- function predictRtuFrameLength(buffer, start, end, isResponse) {
295
- if (end - start < 2) {
290
+ const REQ_TABLE = new Int32Array(256);
291
+ const RES_TABLE = new Int32Array(256);
292
+ (function initTables() {
293
+ REQ_TABLE[exports.FunctionCode.READ_COILS] = 8;
294
+ REQ_TABLE[exports.FunctionCode.READ_DISCRETE_INPUTS] = 8;
295
+ REQ_TABLE[exports.FunctionCode.READ_HOLDING_REGISTERS] = 8;
296
+ REQ_TABLE[exports.FunctionCode.READ_INPUT_REGISTERS] = 8;
297
+ REQ_TABLE[exports.FunctionCode.WRITE_SINGLE_COIL] = 8;
298
+ REQ_TABLE[exports.FunctionCode.WRITE_SINGLE_REGISTER] = 8;
299
+ REQ_TABLE[exports.FunctionCode.REPORT_SERVER_ID] = 4;
300
+ REQ_TABLE[exports.FunctionCode.MASK_WRITE_REGISTER] = 10;
301
+ REQ_TABLE[exports.FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
302
+ REQ_TABLE[exports.FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
303
+ REQ_TABLE[exports.FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
304
+ REQ_TABLE[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
305
+ RES_TABLE[exports.FunctionCode.WRITE_SINGLE_COIL] = 8;
306
+ RES_TABLE[exports.FunctionCode.WRITE_SINGLE_REGISTER] = 8;
307
+ RES_TABLE[exports.FunctionCode.WRITE_MULTIPLE_COILS] = 8;
308
+ RES_TABLE[exports.FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
309
+ RES_TABLE[exports.FunctionCode.MASK_WRITE_REGISTER] = 10;
310
+ RES_TABLE[exports.FunctionCode.READ_COILS] = -517;
311
+ RES_TABLE[exports.FunctionCode.READ_DISCRETE_INPUTS] = -517;
312
+ RES_TABLE[exports.FunctionCode.READ_HOLDING_REGISTERS] = -517;
313
+ RES_TABLE[exports.FunctionCode.READ_INPUT_REGISTERS] = -517;
314
+ RES_TABLE[exports.FunctionCode.REPORT_SERVER_ID] = -517;
315
+ RES_TABLE[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
316
+ RES_TABLE[exports.FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
317
+ })();
318
+ function predictRtuFrameLength(residual, data, residualLen, start, end, isResponse) {
319
+ const len = end - start;
320
+ if (len < 2) {
296
321
  return PREDICT_NEED_MORE;
297
322
  }
298
- const fc = buffer[start + 1];
299
- if (isResponse && (fc & EXCEPTION_OFFSET) !== 0) {
300
- return 5;
301
- }
302
- const fixed = (isResponse ? RESPONSE_FIXED_LENGTHS : REQUEST_FIXED_LENGTHS)[fc];
303
- if (fixed !== undefined) {
304
- return fixed;
305
- }
306
- const bc = (isResponse ? RESPONSE_BYTE_COUNT : REQUEST_BYTE_COUNT)[fc];
307
- if (bc !== undefined) {
308
- if (end - start <= bc.offset) {
309
- return PREDICT_NEED_MORE;
323
+ const fc = start + 1 < residualLen ? residual[start + 1] : data[start + 1 - residualLen];
324
+ if (isResponse) {
325
+ if ((fc & EXCEPTION_OFFSET) !== 0) {
326
+ return 5;
327
+ }
328
+ const val = RES_TABLE[fc];
329
+ if (val > 0) {
330
+ return val;
331
+ }
332
+ if (val < 0) {
333
+ if (val === -999) {
334
+ // FC 43 / MEI 14 response — inline to avoid function-call overhead on
335
+ // the framing hot path (even though this FC is uncommon).
336
+ if (end - start < 8) {
337
+ return PREDICT_NEED_MORE;
338
+ }
339
+ if ((start + 2 < residualLen ? residual[start + 2] : data[start + 2 - residualLen]) !== MEI_READ_DEVICE_ID) {
340
+ return PREDICT_UNKNOWN;
341
+ }
342
+ const numObjs = start + 7 < residualLen ? residual[start + 7] : data[start + 7 - residualLen];
343
+ let cursor = start + 8;
344
+ for (let i = 0; i < numObjs; i++) {
345
+ if (end < cursor + 2) {
346
+ return PREDICT_NEED_MORE;
347
+ }
348
+ cursor += 2 + (cursor + 1 < residualLen ? residual[cursor + 1] : data[cursor + 1 - residualLen]);
349
+ }
350
+ return cursor - start + 2;
351
+ }
352
+ const decode = -val;
353
+ const offset = decode >> 8;
354
+ if (len <= offset) {
355
+ return PREDICT_NEED_MORE;
356
+ }
357
+ return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
310
358
  }
311
- return bc.extra + buffer[start + bc.offset];
312
- }
313
- if (isResponse && fc === exports.FunctionCode.READ_DEVICE_IDENTIFICATION) {
314
- return predictFc43_14Response(buffer, start, end);
315
359
  }
316
- return PREDICT_UNKNOWN;
317
- }
318
- /**
319
- * Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
320
- * response structure per Modbus V1.1b3 §6.21.
321
- *
322
- * Layout (after unit and fc):
323
- * mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
324
- * [objId(1) objLen(1) objData(objLen)] × numObjs
325
- * CRC(2)
326
- */
327
- function predictFc43_14Response(buffer, start, end) {
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;
360
+ else {
361
+ const val = REQ_TABLE[fc];
362
+ if (val > 0) {
363
+ return val;
364
+ }
365
+ if (val < 0) {
366
+ const decode = -val;
367
+ const offset = decode >> 8;
368
+ if (len <= offset) {
369
+ return PREDICT_NEED_MORE;
370
+ }
371
+ return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
339
372
  }
340
- const objLen = buffer[cursor + 1];
341
- cursor += 2 + objLen;
342
373
  }
343
- return cursor - start + 2;
374
+ return PREDICT_UNKNOWN;
344
375
  }
345
376
 
346
377
  /**
@@ -349,10 +380,12 @@ function predictFc43_14Response(buffer, start, end) {
349
380
  function promisifyCb(fn) {
350
381
  return new Promise((resolve, reject) => {
351
382
  fn((err) => {
352
- if (err)
383
+ if (err) {
353
384
  reject(err);
354
- else
385
+ }
386
+ else {
355
387
  resolve();
388
+ }
356
389
  });
357
390
  });
358
391
  }
@@ -378,7 +411,12 @@ function resolveOne(value, baudRate, fastBaudMs) {
378
411
  if (baudRate === undefined) {
379
412
  return undefined;
380
413
  }
381
- return baudRate > 19200 ? fastBaudMs : Math.ceil(bitsToMs(baudRate, value.value));
414
+ if (baudRate > 19200) {
415
+ return fastBaudMs;
416
+ }
417
+ const ms = bitsToMs(baudRate, value.value);
418
+ const trunc = ms | 0;
419
+ return trunc + (ms > trunc ? 1 : 0);
382
420
  }
383
421
  /**
384
422
  * Resolve Modbus RTU timing parameters from user options into milliseconds.
@@ -395,7 +433,17 @@ function resolveRtuTiming(opts = {}, baudRate) {
395
433
  if (intervalBetweenFrames === undefined) {
396
434
  // Spec default: t3.5 derived from baudRate, or 0 when neither option nor
397
435
  // baudRate were supplied.
398
- intervalBetweenFrames = baudRate !== undefined ? (baudRate > 19200 ? 1.75 : Math.ceil(bitsToMs(baudRate, 38.5))) : 0;
436
+ if (baudRate === undefined) {
437
+ intervalBetweenFrames = 0;
438
+ }
439
+ else if (baudRate > 19200) {
440
+ intervalBetweenFrames = 1.75;
441
+ }
442
+ else {
443
+ const ms = bitsToMs(baudRate, 38.5);
444
+ const trunc = ms | 0;
445
+ intervalBetweenFrames = trunc + (ms > trunc ? 1 : 0);
446
+ }
399
447
  }
400
448
  let interCharTimeout = resolveOne(opts.interCharTimeout, baudRate, 0.75);
401
449
  if (interCharTimeout === undefined) {
@@ -405,102 +453,176 @@ function resolveRtuTiming(opts = {}, baudRate) {
405
453
  return { intervalBetweenFrames, interCharTimeout };
406
454
  }
407
455
 
408
- /** @internal
409
- * Zero-allocation binary min-heap for coalescing per-request timeouts.
456
+ /**
457
+ * Hybrid timer manager: uses native `setTimeout` for low concurrency
458
+ * and switches to a binary min-heap when concurrency exceeds the threshold.
410
459
  *
411
- * Uses two parallel numeric arrays (no object allocation per entry).
412
- * Lazy deletion: callers never remove from the heap; expired entries
413
- * are silently dropped when they surface at the top.
460
+ * Benchmarks (add + clear throughput, Node 24, x64):
461
+ * 1 concurrent: setTimeout ~1.7× faster than heap
462
+ * 2 concurrent: setTimeout ~1.6× faster than heap
463
+ * 5 concurrent: setTimeout ~1.5-1.9× faster than heap
464
+ * 10 concurrent: roughly equal
465
+ * 20 concurrent: heap ~1.3× faster than setTimeout[]
466
+ * 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
467
+ *
468
+ * The crossover point is around 10 concurrent timers, so the default
469
+ * `concurrentThreshold = 2` keeps the common 1-2 request case on the
470
+ * fast direct path while delegating to the heap for larger batches.
414
471
  */
415
472
  class TimerHeap {
416
473
  _deadlines = [];
417
474
  _ids = [];
475
+ _seqs = [];
476
+ _counter = 0;
418
477
  _timer = null;
419
478
  _onFire;
420
- /** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
421
479
  _boundTick;
422
- constructor(onFire) {
480
+ _threshold;
481
+ _mode = 'direct';
482
+ _directTimers = new Map();
483
+ /**
484
+ * @param onFire Callback invoked with the timer id when it expires.
485
+ * @param concurrentThreshold Maximum number of timers kept as individual
486
+ * native `setTimeout` handles. Once exceeded, all timers migrate to
487
+ * the internal heap and share a single native timer. Default is 2.
488
+ */
489
+ constructor(onFire, concurrentThreshold = 2) {
423
490
  this._onFire = onFire;
424
491
  this._boundTick = this._onTick.bind(this);
492
+ this._threshold = concurrentThreshold;
425
493
  }
426
- /** Number of pending timers in the heap. */
427
494
  get size() {
428
- return this._deadlines.length;
495
+ return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
429
496
  }
430
- /** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
431
497
  add(id, ms) {
498
+ if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
499
+ const deadline = performance.now() + ms;
500
+ const handle = setTimeout(() => {
501
+ if (this._mode !== 'direct') {
502
+ return;
503
+ }
504
+ this._directTimers.delete(id);
505
+ this._onFire(id);
506
+ }, ms);
507
+ this._directTimers.set(id, { handle, deadline });
508
+ return;
509
+ }
510
+ if (this._mode === 'direct') {
511
+ this._mode = 'heap';
512
+ for (const [existingId, { handle, deadline }] of this._directTimers) {
513
+ clearTimeout(handle);
514
+ const diff = deadline - performance.now();
515
+ const trunc = diff | 0;
516
+ const remaining = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
517
+ if (remaining === 0) {
518
+ this._onFire(existingId);
519
+ }
520
+ else {
521
+ this._heapAdd(existingId, remaining);
522
+ }
523
+ }
524
+ this._directTimers.clear();
525
+ }
526
+ this._heapAdd(id, ms);
527
+ }
528
+ clear() {
529
+ for (const { handle } of this._directTimers.values()) {
530
+ clearTimeout(handle);
531
+ }
532
+ this._directTimers.clear();
533
+ this._mode = 'direct';
534
+ if (this._timer) {
535
+ clearTimeout(this._timer);
536
+ this._timer = null;
537
+ }
538
+ this._deadlines.length = 0;
539
+ this._ids.length = 0;
540
+ this._seqs.length = 0;
541
+ this._counter = 0;
542
+ }
543
+ _heapAdd(id, ms) {
432
544
  const deadline = performance.now() + ms;
545
+ const seq = this._counter++;
433
546
  let i = this._deadlines.length;
434
547
  this._deadlines.push(deadline);
435
548
  this._ids.push(id);
436
- // sift up
549
+ this._seqs.push(seq);
437
550
  while (i > 0) {
438
551
  const p = (i - 1) >> 1;
439
- if (this._deadlines[p] <= deadline)
552
+ const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
553
+ if (parentComesFirst) {
440
554
  break;
555
+ }
441
556
  this._deadlines[i] = this._deadlines[p];
442
557
  this._ids[i] = this._ids[p];
558
+ this._seqs[i] = this._seqs[p];
443
559
  i = p;
444
560
  }
445
561
  this._deadlines[i] = deadline;
446
562
  this._ids[i] = id;
447
- // Only reschedule when the new entry became the heap top.
448
- if (i === 0)
563
+ this._seqs[i] = seq;
564
+ if (i === 0) {
449
565
  this._refresh();
450
- }
451
- /** Dispose without firing callbacks. */
452
- clear() {
453
- if (this._timer) {
454
- clearTimeout(this._timer);
455
- this._timer = null;
456
566
  }
457
- this._deadlines.length = 0;
458
- this._ids.length = 0;
459
567
  }
460
568
  _refresh() {
461
569
  if (this._timer) {
462
570
  clearTimeout(this._timer);
463
571
  this._timer = null;
464
572
  }
465
- if (this._deadlines.length === 0)
573
+ if (this._deadlines.length === 0) {
466
574
  return;
467
- const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
468
- this._timer = setTimeout(this._boundTick, delay);
575
+ }
576
+ const diff = this._deadlines[0] - performance.now();
577
+ const trunc = diff | 0;
578
+ const delay = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
579
+ const safeDelay = delay < 2147483647 ? delay : 2147483647;
580
+ this._timer = setTimeout(this._boundTick, safeDelay);
469
581
  }
470
582
  _onTick() {
471
583
  this._timer = null;
472
584
  const now = performance.now();
473
- while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
474
- const id = this._pop();
475
- this._onFire(id);
585
+ try {
586
+ while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
587
+ const id = this._pop();
588
+ this._onFire(id);
589
+ }
590
+ }
591
+ finally {
592
+ this._refresh();
476
593
  }
477
- this._refresh();
478
594
  }
479
- /** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
480
595
  _pop() {
481
596
  const topId = this._ids[0];
482
597
  const lastId = this._ids.pop();
483
598
  const lastDeadline = this._deadlines.pop();
599
+ const lastSeq = this._seqs.pop();
484
600
  const n = this._deadlines.length;
485
601
  if (n > 0) {
486
602
  let i = 0;
487
- // sift down
488
- while (true) {
489
- let min = i;
490
- const l = i * 2 + 1;
491
- const r = l + 1;
492
- if (l < n && this._deadlines[l] < this._deadlines[min])
493
- min = l;
494
- if (r < n && this._deadlines[r] < this._deadlines[min])
495
- min = r;
496
- if (min === i)
603
+ const half = n >> 1;
604
+ while (i < half) {
605
+ let minChild = (i << 1) + 1;
606
+ const rightChild = minChild + 1;
607
+ if (rightChild < n) {
608
+ const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
609
+ (this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
610
+ if (rightComesFirst) {
611
+ minChild = rightChild;
612
+ }
613
+ }
614
+ const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
615
+ if (lastComesFirst) {
497
616
  break;
498
- this._deadlines[i] = this._deadlines[min];
499
- this._ids[i] = this._ids[min];
500
- i = min;
617
+ }
618
+ this._deadlines[i] = this._deadlines[minChild];
619
+ this._ids[i] = this._ids[minChild];
620
+ this._seqs[i] = this._seqs[minChild];
621
+ i = minChild;
501
622
  }
502
623
  this._deadlines[i] = lastDeadline;
503
624
  this._ids[i] = lastId;
625
+ this._seqs[i] = lastSeq;
504
626
  }
505
627
  return topId;
506
628
  }
@@ -538,6 +660,9 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
538
660
  get physicalLayer() {
539
661
  return this._physicalLayer;
540
662
  }
663
+ get serialport() {
664
+ return this._serialport;
665
+ }
541
666
  constructor(physicalLayer, serialport) {
542
667
  super();
543
668
  this._physicalLayer = physicalLayer;
@@ -546,6 +671,7 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
546
671
  if (this.state !== exports.PhysicalConnectionState.CONNECTED) {
547
672
  return;
548
673
  }
674
+ this.emit('rx', chunk);
549
675
  this.emit('data', chunk);
550
676
  };
551
677
  serialport.on('data', onData);
@@ -574,7 +700,12 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
574
700
  }
575
701
  write(data, cb) {
576
702
  if (this.state === exports.PhysicalConnectionState.CONNECTED) {
577
- this._serialport.write(data, cb);
703
+ this._serialport.write(data, (err) => {
704
+ if (!err) {
705
+ this.emit('tx', data);
706
+ }
707
+ cb?.(err);
708
+ });
578
709
  }
579
710
  else {
580
711
  cb?.(new Error('Connection is not connected'));
@@ -735,6 +866,9 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
735
866
  get physicalLayer() {
736
867
  return this._physicalLayer;
737
868
  }
869
+ get socket() {
870
+ return this._socket;
871
+ }
738
872
  constructor(physicalLayer, socket) {
739
873
  super();
740
874
  this._physicalLayer = physicalLayer;
@@ -743,6 +877,7 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
743
877
  if (this.state !== exports.PhysicalConnectionState.CONNECTED) {
744
878
  return;
745
879
  }
880
+ this.emit('rx', chunk);
746
881
  this.emit('data', chunk);
747
882
  };
748
883
  socket.on('data', onData);
@@ -771,7 +906,12 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
771
906
  }
772
907
  write(data, cb) {
773
908
  if (this.state === exports.PhysicalConnectionState.CONNECTED) {
774
- this._socket.write(data, cb);
909
+ this._socket.write(data, (err) => {
910
+ if (!err) {
911
+ this.emit('tx', data);
912
+ }
913
+ cb?.(err);
914
+ });
775
915
  }
776
916
  else {
777
917
  cb?.(new Error('Connection is not connected'));
@@ -1053,6 +1193,9 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1053
1193
  get physicalLayer() {
1054
1194
  return this._physicalLayer;
1055
1195
  }
1196
+ get socket() {
1197
+ return this._socket;
1198
+ }
1056
1199
  constructor(physicalLayer, socket) {
1057
1200
  super();
1058
1201
  this._physicalLayer = physicalLayer;
@@ -1061,6 +1204,7 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1061
1204
  if (this.state !== exports.PhysicalConnectionState.CONNECTED) {
1062
1205
  return;
1063
1206
  }
1207
+ this.emit('rx', msg);
1064
1208
  this.emit('data', msg);
1065
1209
  };
1066
1210
  socket.on('message', onMessage);
@@ -1089,7 +1233,12 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1089
1233
  }
1090
1234
  write(data, cb) {
1091
1235
  if (this.state === exports.PhysicalConnectionState.CONNECTED) {
1092
- this._socket.send(data, cb);
1236
+ this._socket.send(data, (err) => {
1237
+ if (!err) {
1238
+ this.emit('tx', data);
1239
+ }
1240
+ cb?.(err);
1241
+ });
1093
1242
  }
1094
1243
  else {
1095
1244
  cb?.(new Error('Connection is not connected'));
@@ -1229,6 +1378,12 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1229
1378
  get physicalLayer() {
1230
1379
  return this._physicalLayer;
1231
1380
  }
1381
+ get socket() {
1382
+ return this._socket;
1383
+ }
1384
+ get remote() {
1385
+ return this._remote;
1386
+ }
1232
1387
  constructor(physicalLayer, socket, remote, idleTimeout, messageEventDelegation) {
1233
1388
  super();
1234
1389
  this._physicalLayer = physicalLayer;
@@ -1247,6 +1402,7 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1247
1402
  }, idleTimeout);
1248
1403
  }
1249
1404
  }
1405
+ this.emit('rx', msg);
1250
1406
  this.emit('data', msg);
1251
1407
  };
1252
1408
  messageEventDelegation.add(onMessage);
@@ -1259,7 +1415,12 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1259
1415
  }
1260
1416
  write(data, cb) {
1261
1417
  if (this.state === exports.PhysicalConnectionState.CONNECTED) {
1262
- this._socket.send(data, this._remote.port, this._remote.address, cb);
1418
+ this._socket.send(data, this._remote.port, this._remote.address, (err) => {
1419
+ if (!err) {
1420
+ this.emit('tx', data);
1421
+ }
1422
+ cb?.(err);
1423
+ });
1263
1424
  }
1264
1425
  else {
1265
1426
  cb?.(new Error('Connection is not connected'));
@@ -1462,16 +1623,23 @@ class AbstractApplicationLayer {
1462
1623
  removeCustomFunctionCode(fc) { }
1463
1624
  }
1464
1625
 
1465
- const MAX_FRAME_LENGTH = 256;
1626
+ const MAX_FRAME_LENGTH$1 = 256;
1466
1627
  const MIN_FRAME_LENGTH = 4;
1467
1628
  class RtuApplicationLayer extends AbstractApplicationLayer {
1468
1629
  PROTOCOL = 'RTU';
1469
1630
  ROLE;
1470
1631
  _connection;
1471
- _state;
1472
- _poolSize;
1473
- _threePointFiveT;
1474
- _onePointFiveT;
1632
+ // Stores leftover data between parse rounds
1633
+ _residual = Buffer.alloc(MAX_FRAME_LENGTH$1);
1634
+ _residualLen = 0;
1635
+ _expectedLen = PREDICT_NEED_MORE;
1636
+ _t15Time;
1637
+ _t35Time;
1638
+ _t15Strict;
1639
+ _t15Timer;
1640
+ _t35Timer;
1641
+ // t1.5 cursor: 0 = not triggered; > 0 = virtual index where the gap occurred
1642
+ _t15Marker = 0;
1475
1643
  _customFunctionCodes = new Map();
1476
1644
  _cleanupCbs = [];
1477
1645
  get connection() {
@@ -1481,89 +1649,246 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1481
1649
  super();
1482
1650
  this.ROLE = role;
1483
1651
  this._connection = connection;
1484
- const { intervalBetweenFrames, interCharTimeout, poolSize } = options;
1485
- this._poolSize = poolSize ?? MAX_FRAME_LENGTH * 2;
1486
- this._state = { pool: Buffer.alloc(this._poolSize), start: 0, end: 0 };
1487
- this._threePointFiveT = intervalBetweenFrames ?? 0;
1488
- this._onePointFiveT = interCharTimeout ?? 0;
1652
+ const { intervalBetweenFrames, interCharTimeout, strictTiming } = options;
1653
+ this._t35Time = intervalBetweenFrames ?? 0;
1654
+ this._t15Time = this._t35Time === 0 ? 0 : (interCharTimeout ?? 0);
1655
+ if (this._t35Time < this._t15Time) {
1656
+ throw new Error('t3.5 cannot be less than t1.5');
1657
+ }
1658
+ this._t15Strict = strictTiming ?? false;
1659
+ const isResponse = role === 'MASTER';
1660
+ const timingEnabled = this._t35Time > 0;
1489
1661
  const onData = (data) => {
1490
- const state = this._state;
1491
- if (state.t15Expired && state.end > state.start) {
1492
- this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
1493
- state.start = 0;
1494
- state.end = 0;
1495
- }
1496
- state.t15Expired = false;
1497
1662
  const dataLen = data.length;
1498
- const space = state.pool.length - state.end;
1499
- if (dataLen <= space) {
1500
- // Fast path: incoming chunk fits the current pool tail; one bulk copy,
1501
- // no per-iteration bookkeeping. This is the typical case.
1502
- data.copy(state.pool, state.end);
1503
- state.end += dataLen;
1663
+ const residualLen = this._residualLen;
1664
+ const totalAvailable = residualLen + dataLen;
1665
+ // ========================================================================
1666
+ // 1. Timing reset: any bus activity unconditionally kills all silence timers
1667
+ // ========================================================================
1668
+ if (timingEnabled) {
1669
+ if (this._t15Timer !== undefined) {
1670
+ clearTimeout(this._t15Timer);
1671
+ this._t15Timer = undefined;
1672
+ }
1673
+ if (this._t35Timer !== undefined) {
1674
+ clearTimeout(this._t35Timer);
1675
+ this._t35Timer = undefined;
1676
+ }
1504
1677
  }
1505
- else {
1506
- // Slow path: incoming chunk does NOT fit the pool's remaining tail
1507
- // space (not necessarily larger than the whole pool — typically the
1508
- // pool already holds undrained residue from earlier ticks).
1509
- // Strategy drain & absorb in chunks:
1510
- // 1. Copy as much of `data` as fits the current tail.
1511
- // 2. When the pool fills mid-chunk, run flushBuffer(false) to emit
1512
- // any complete frames and collapse the leftover bytes to offset
1513
- // 0, which reclaims tail space.
1514
- // 3. Repeat until `data` is fully ingested.
1515
- // The mid-chunk flush is NEVER strict — t3.5 has not actually elapsed,
1516
- // the rest of `data` is still in hand. Only the t3.5 timer callback
1517
- // below passes strict=true.
1518
- let dataOffset = 0;
1519
- let currentState = state;
1520
- while (dataOffset < dataLen) {
1521
- const room = currentState.pool.length - currentState.end;
1522
- if (room === 0) {
1523
- // Pool full mid-ingest. Try to extract any complete frames and
1524
- // reclaim tail space so the rest of `data` has somewhere to go.
1525
- this.clearStateTimers();
1526
- this.flushBuffer(false);
1527
- // flushBuffer may emit; user callbacks could in principle replace
1528
- // _state. Re-read to stay correct across that boundary.
1529
- currentState = this._state;
1530
- if (currentState.pool.length - currentState.end === 0) {
1531
- // flushBuffer freed nothing — the entire pool is unparseable
1532
- // residue (typically a misconfigured poolSize for the wire's
1533
- // frame size). Hard reset; we cannot recover automatically.
1534
- this.onFramingError(new Error('Frame buffer exhausted before complete frame received'));
1535
- currentState.start = 0;
1536
- currentState.end = 0;
1537
- currentState.t15Expired = false;
1678
+ // ========================================================================
1679
+ // 2. Fast path: no residual data and the new chunk is exactly one frame
1680
+ // ========================================================================
1681
+ if (residualLen === 0 && dataLen >= MIN_FRAME_LENGTH) {
1682
+ const fc = data[1];
1683
+ let frameLen = PREDICT_NEED_MORE;
1684
+ const cfc = this._customFunctionCodes.size > 0 ? this._customFunctionCodes.get(fc) : undefined;
1685
+ if (cfc) {
1686
+ const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
1687
+ frameLen = predictor((idx) => data[idx], dataLen);
1688
+ }
1689
+ else if (isResponse) {
1690
+ if ((fc & 0x80) !== 0) {
1691
+ frameLen = 5;
1692
+ }
1693
+ else {
1694
+ const val = RES_TABLE[fc];
1695
+ if (val > 0) {
1696
+ frameLen = val;
1697
+ }
1698
+ else if (val < 0 && val !== -999) {
1699
+ const decode = -val;
1700
+ const offset = decode >> 8;
1701
+ if (dataLen > offset) {
1702
+ frameLen = (decode & 0xff) + data[offset];
1703
+ }
1538
1704
  }
1539
- continue;
1540
1705
  }
1541
- // Copy the next chunk into the freshly-available tail space.
1542
- const toCopy = room < dataLen - dataOffset ? room : dataLen - dataOffset;
1543
- data.copy(currentState.pool, currentState.end, dataOffset, dataOffset + toCopy);
1544
- currentState.end += toCopy;
1545
- dataOffset += toCopy;
1706
+ }
1707
+ else {
1708
+ const val = REQ_TABLE[fc];
1709
+ if (val > 0) {
1710
+ frameLen = val;
1711
+ }
1712
+ else if (val < 0) {
1713
+ const decode = -val;
1714
+ const offset = decode >> 8;
1715
+ if (dataLen > offset) {
1716
+ frameLen = (decode & 0xff) + data[offset];
1717
+ }
1718
+ }
1719
+ }
1720
+ if (frameLen === dataLen) {
1721
+ const expectedCrc = data[frameLen - 2] | (data[frameLen - 1] << 8);
1722
+ // Inline CRC for the hot single-buffer path — local table reference helps V8 IC.
1723
+ const table = CRC_TABLE;
1724
+ let crc = 0xffff;
1725
+ const crcEnd = frameLen - 2;
1726
+ for (let i = 0; i < crcEnd; i++) {
1727
+ crc = table[(crc ^ data[i]) & 0xff] ^ (crc >> 8);
1728
+ }
1729
+ if (expectedCrc === crc) {
1730
+ const dropFrame = timingEnabled && this._t15Strict && this._t15Marker > 0;
1731
+ if (!dropFrame) {
1732
+ const frame = {
1733
+ unit: data[0],
1734
+ fc: data[1],
1735
+ data: data.subarray(2, frameLen - 2),
1736
+ buffer: data,
1737
+ };
1738
+ this.onFraming(frame);
1739
+ }
1740
+ this._expectedLen = PREDICT_NEED_MORE;
1741
+ this._t15Marker = 0;
1742
+ return;
1743
+ }
1546
1744
  }
1547
1745
  }
1548
- this.clearStateTimers();
1549
- const liveState = this._state;
1550
- if (liveState.end - liveState.start >= MAX_FRAME_LENGTH) {
1551
- this.flushBuffer(this._threePointFiveT > 0);
1552
- }
1553
- else if (this._threePointFiveT) {
1554
- if (this._onePointFiveT > 0) {
1555
- liveState.interCharTimer = setTimeout(() => {
1556
- liveState.interCharTimer = undefined;
1557
- liveState.t15Expired = true;
1558
- }, this._onePointFiveT);
1746
+ // ========================================================================
1747
+ // 3. Extract frames in a loop until no complete frame remains (skip if data length below prediction)
1748
+ // ========================================================================
1749
+ let index = 0;
1750
+ if (!(this._expectedLen > 0 && totalAvailable < this._expectedLen)) {
1751
+ while (index <= totalAvailable - MIN_FRAME_LENGTH) {
1752
+ const fc = index + 1 < residualLen ? this._residual[index + 1] : data[index + 1 - residualLen];
1753
+ const cfc = this._customFunctionCodes.get(fc);
1754
+ let frameLen = PREDICT_NEED_MORE;
1755
+ if (cfc) {
1756
+ const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
1757
+ frameLen = predictor((idx) => {
1758
+ const pos = index + idx;
1759
+ return pos < residualLen ? this._residual[pos] : data[pos - residualLen];
1760
+ }, totalAvailable - index);
1761
+ }
1762
+ else {
1763
+ frameLen = predictRtuFrameLength(this._residual, data, residualLen, index, totalAvailable, isResponse);
1764
+ }
1765
+ if (frameLen === PREDICT_UNKNOWN) {
1766
+ index++;
1767
+ continue;
1768
+ }
1769
+ if (frameLen === PREDICT_NEED_MORE) {
1770
+ break;
1771
+ }
1772
+ if (frameLen > MAX_FRAME_LENGTH$1 || frameLen < MIN_FRAME_LENGTH) {
1773
+ index++;
1774
+ continue;
1775
+ }
1776
+ if (totalAvailable - index < frameLen) {
1777
+ this._expectedLen = index + frameLen;
1778
+ break;
1779
+ }
1780
+ const expectedCrc = (index + frameLen - 2 < residualLen ? this._residual[index + frameLen - 2] : data[index + frameLen - 2 - residualLen]) |
1781
+ ((index + frameLen - 1 < residualLen ? this._residual[index + frameLen - 1] : data[index + frameLen - 1 - residualLen]) << 8);
1782
+ const crcEnd = index + frameLen - 2;
1783
+ let actualCrc;
1784
+ if (crcEnd <= residualLen) {
1785
+ // Entire CRC range sits in the old residual buffer
1786
+ actualCrc = crcFixed(this._residual, index, crcEnd);
1787
+ }
1788
+ else if (index >= residualLen) {
1789
+ // Entire CRC range sits in the new data chunk
1790
+ actualCrc = crcFixed(data, index - residualLen, crcEnd - residualLen);
1791
+ }
1792
+ else {
1793
+ // CRC range spans both buffers
1794
+ actualCrc = crcDual(this._residual, index, residualLen - index, data, 0, crcEnd - residualLen);
1795
+ }
1796
+ if (expectedCrc !== actualCrc) {
1797
+ index++;
1798
+ continue;
1799
+ }
1800
+ // A complete frame has been received; CRC verification still required
1801
+ this._expectedLen = PREDICT_NEED_MORE;
1802
+ const dropFrame = this._t15Strict && this._t15Marker > index && this._t15Marker < index + frameLen;
1803
+ if (!dropFrame) {
1804
+ // Build contiguous raw buffer — one alloc, zero-copy subarray for data.
1805
+ const raw = Buffer.allocUnsafe(frameLen);
1806
+ if (index + frameLen <= residualLen) {
1807
+ this._residual.copy(raw, 0, index, index + frameLen);
1808
+ }
1809
+ else if (index >= residualLen) {
1810
+ data.copy(raw, 0, index - residualLen, index - residualLen + frameLen);
1811
+ }
1812
+ else {
1813
+ const headLen = residualLen - index;
1814
+ this._residual.copy(raw, 0, index, residualLen);
1815
+ data.copy(raw, headLen, 0, frameLen - headLen);
1816
+ }
1817
+ const frame = {
1818
+ unit: raw[0],
1819
+ fc: raw[1],
1820
+ data: raw.subarray(2, frameLen - 2),
1821
+ buffer: raw,
1822
+ };
1823
+ this.onFraming(frame);
1824
+ }
1825
+ index += frameLen;
1559
1826
  }
1560
- liveState.timer = setTimeout(() => {
1561
- this.clearStateTimers();
1562
- this.flushBuffer(true);
1563
- }, this._threePointFiveT);
1827
+ }
1828
+ // ========================================================================
1829
+ // 4. Compact residual buffer and rebuild silence timers
1830
+ // ========================================================================
1831
+ const newFrameStart = index;
1832
+ const finalRestLen = totalAvailable - newFrameStart;
1833
+ if (finalRestLen === 0) {
1834
+ this._residualLen = 0;
1835
+ this._expectedLen = PREDICT_NEED_MORE;
1836
+ this._t15Marker = 0;
1564
1837
  }
1565
1838
  else {
1566
- this.flushBuffer(false);
1839
+ const keepLen = finalRestLen < MAX_FRAME_LENGTH$1 ? finalRestLen : MAX_FRAME_LENGTH$1;
1840
+ const discardLen = totalAvailable - keepLen;
1841
+ if (discardLen >= residualLen) {
1842
+ // Kept portion lies entirely within the new `data`
1843
+ data.copy(this._residual, 0, discardLen - residualLen, dataLen);
1844
+ }
1845
+ else if (discardLen > 0) {
1846
+ // Kept portion spans both buffers, or physical left-shift truncation occurred
1847
+ for (let i = 0; i < keepLen; i++) {
1848
+ this._residual[i] = discardLen + i < residualLen ? this._residual[discardLen + i] : data[discardLen + i - residualLen];
1849
+ }
1850
+ }
1851
+ else {
1852
+ // discardLen === 0 (old data not consumed, truncation limit not hit) — simple append
1853
+ data.copy(this._residual, residualLen, 0, dataLen);
1854
+ }
1855
+ this._residualLen = keepLen;
1856
+ // Unify physical coordinate system translation
1857
+ if (discardLen > 0) {
1858
+ if (this._expectedLen > 0) {
1859
+ const newExpectedLen = this._expectedLen - discardLen;
1860
+ this._expectedLen = newExpectedLen > PREDICT_NEED_MORE ? newExpectedLen : PREDICT_NEED_MORE;
1861
+ }
1862
+ if (this._t15Marker > 0) {
1863
+ const newT15Marker = this._t15Marker - discardLen;
1864
+ this._t15Marker = newT15Marker > 0 ? newT15Marker : 0;
1865
+ }
1866
+ }
1867
+ if (timingEnabled) {
1868
+ let hasErrorEmitted = false;
1869
+ // Establish t3.5 absolute deadline
1870
+ this._t35Timer = setTimeout(() => {
1871
+ this._t35Timer = undefined;
1872
+ // No complete frame parsed within t3.5: circuit-break, discard all data
1873
+ this._residualLen = 0;
1874
+ this._expectedLen = PREDICT_NEED_MORE;
1875
+ this._t15Marker = 0;
1876
+ if (!hasErrorEmitted) {
1877
+ this.onFramingError(new Error('Incomplete frame at t3.5'));
1878
+ }
1879
+ }, this._t35Time);
1880
+ if (this._t15Time > 0) {
1881
+ // Establish t1.5 inter-character gap monitor
1882
+ this._t15Timer = setTimeout(() => {
1883
+ this._t15Timer = undefined;
1884
+ this._t15Marker = this._residualLen; // Record the residual boundary where the gap occurred
1885
+ if (this._t15Strict) {
1886
+ hasErrorEmitted = true;
1887
+ this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
1888
+ }
1889
+ }, this._t15Time);
1890
+ }
1891
+ }
1567
1892
  }
1568
1893
  };
1569
1894
  connection.on('data', onData);
@@ -1573,147 +1898,33 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1573
1898
  fn();
1574
1899
  }
1575
1900
  this._cleanupCbs.length = 0;
1576
- this.clearStateTimers();
1901
+ if (this._t15Timer !== undefined) {
1902
+ clearTimeout(this._t15Timer);
1903
+ this._t15Timer = undefined;
1904
+ }
1905
+ if (this._t35Timer !== undefined) {
1906
+ clearTimeout(this._t35Timer);
1907
+ this._t35Timer = undefined;
1908
+ }
1577
1909
  };
1578
1910
  connection.on('close', onClose);
1579
1911
  this._cleanupCbs.push(() => connection.off('close', onClose));
1580
1912
  }
1581
- clearStateTimers() {
1582
- const state = this._state;
1583
- if (state.timer) {
1584
- clearTimeout(state.timer);
1585
- state.timer = undefined;
1586
- }
1587
- if (state.interCharTimer) {
1588
- clearTimeout(state.interCharTimer);
1589
- state.interCharTimer = undefined;
1590
- }
1591
- }
1592
- /**
1593
- * Shared handler for every "frame is not yet complete" exit in `flushBuffer`.
1594
- * Returns `true` when the caller should `return` (strict reset), `false` to
1595
- * `break` the parse loop. Hot path never reaches here — only error/incomplete
1596
- * edges. Extracted as a method so it is not recreated on every `flushBuffer`
1597
- * call.
1598
- */
1599
- _handleIncomplete(state, strict) {
1600
- if (strict) {
1601
- this.onFramingError(new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
1602
- state.start = 0;
1603
- state.end = 0;
1604
- state.t15Expired = false;
1605
- return true;
1606
- }
1607
- if (state.t15Expired) {
1608
- this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
1609
- state.start = 0;
1610
- state.end = 0;
1611
- state.t15Expired = false;
1612
- }
1613
- return false;
1614
- }
1615
- flushBuffer(strict) {
1616
- const state = this._state;
1617
- const isResponse = this.ROLE === 'MASTER';
1618
- const pool = state.pool;
1619
- const customFCs = this._customFunctionCodes;
1620
- while (state.end - state.start > 0) {
1621
- const available = state.end - state.start;
1622
- if (available < MIN_FRAME_LENGTH) {
1623
- if (this._handleIncomplete(state, strict))
1624
- return;
1625
- break;
1626
- }
1627
- const fc = pool[state.start + 1];
1628
- const cfc = customFCs.get(fc);
1629
- let expected;
1630
- if (cfc) {
1631
- const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
1632
- const predicted = predictor(pool, state.start, state.end);
1633
- // Normalize custom predictor's `null` to the std sentinel so both
1634
- // paths share the same NEED_MORE tail below.
1635
- expected = predicted ?? PREDICT_NEED_MORE;
1636
- }
1637
- else {
1638
- // Standard FC path. predictRtuFrameLength uses sentinel returns to
1639
- // avoid per-call object allocation on the decode hot path.
1640
- expected = predictRtuFrameLength(pool, state.start, state.end, isResponse);
1641
- if (expected === PREDICT_UNKNOWN) {
1642
- this.onFramingError(new Error(`Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`));
1643
- state.start = 0;
1644
- state.end = 0;
1645
- return;
1646
- }
1647
- }
1648
- if (expected === PREDICT_NEED_MORE) {
1649
- if (available >= MAX_FRAME_LENGTH) {
1650
- state.start += 1;
1651
- continue;
1652
- }
1653
- if (this._handleIncomplete(state, strict))
1654
- return;
1655
- break;
1656
- }
1657
- if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
1658
- this.onFramingError(new Error('Invalid data'));
1659
- state.start = 0;
1660
- state.end = 0;
1661
- return;
1662
- }
1663
- if (available < expected) {
1664
- if (available >= MAX_FRAME_LENGTH) {
1665
- state.start += 1;
1666
- continue;
1667
- }
1668
- if (this._handleIncomplete(state, strict))
1669
- return;
1670
- break;
1671
- }
1672
- // CRC check inline: no helper call, no subarray for the CRC body.
1673
- const crcStart = state.start;
1674
- const crcEnd = crcStart + expected - 2;
1675
- const expectedCrc = pool.readUInt16LE(crcEnd);
1676
- const actualCrc = crc(pool, crcStart, crcEnd);
1677
- if (expectedCrc !== actualCrc) {
1678
- if (strict) {
1679
- this.onFramingError(new Error('CRC mismatch'));
1680
- state.start = 0;
1681
- state.end = 0;
1682
- state.t15Expired = false;
1683
- return;
1684
- }
1685
- state.start += 1;
1686
- continue;
1687
- }
1688
- // Frame located. Copy it out of the pool so the emitted buffer remains
1689
- // valid even if the consumer queues the frame across `onData` ticks.
1690
- // `Buffer.copyBytesFrom` is a native fast-path (Node 18.19+) — measurably
1691
- // faster than `Buffer.from(buffer)` for this size.
1692
- const frameBuf = Buffer.copyBytesFrom(pool, crcStart, expected);
1693
- state.start += expected;
1694
- const frame = {
1695
- unit: frameBuf[0],
1696
- fc: frameBuf[1],
1697
- data: frameBuf.subarray(2, expected - 2),
1698
- buffer: frameBuf,
1699
- };
1700
- this.onFraming(frame);
1913
+ flush() {
1914
+ this._residualLen = 0;
1915
+ this._expectedLen = PREDICT_NEED_MORE;
1916
+ this._t15Marker = 0;
1917
+ if (this._t15Timer !== undefined) {
1918
+ clearTimeout(this._t15Timer);
1919
+ this._t15Timer = undefined;
1701
1920
  }
1702
- if (state.start > 0) {
1703
- if (state.start < state.end) {
1704
- state.pool.copy(state.pool, 0, state.start, state.end);
1705
- }
1706
- state.end -= state.start;
1707
- state.start = 0;
1921
+ if (this._t35Timer !== undefined) {
1922
+ clearTimeout(this._t35Timer);
1923
+ this._t35Timer = undefined;
1708
1924
  }
1709
1925
  }
1710
- flush() {
1711
- this.clearStateTimers();
1712
- this._state.start = 0;
1713
- this._state.end = 0;
1714
- }
1715
1926
  addCustomFunctionCode(cfc) {
1716
- if (!isUint8(cfc.fc)) {
1927
+ if ((cfc.fc & 0xff) !== cfc.fc) {
1717
1928
  throw new Error(`fc must be an integer in 0..255, got ${cfc.fc}`);
1718
1929
  }
1719
1930
  this._customFunctionCodes.set(cfc.fc, cfc);
@@ -1728,14 +1939,15 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1728
1939
  buffer[0] = unit;
1729
1940
  buffer[1] = fc;
1730
1941
  if (data.length <= 16) {
1731
- for (let i = 0; i < data.length; i++)
1942
+ for (let i = 0; i < data.length; i++) {
1732
1943
  buffer[2 + i] = data[i];
1944
+ }
1733
1945
  }
1734
1946
  else {
1735
1947
  buffer.set(data, 2);
1736
1948
  }
1737
1949
  const crcEnd = buffer.length - 2;
1738
- const c = crc(buffer, 0, crcEnd);
1950
+ const c = crcFixed(buffer, 0, crcEnd);
1739
1951
  // Little-endian inline write of CRC trailer.
1740
1952
  buffer[crcEnd] = c & 0xff;
1741
1953
  buffer[crcEnd + 1] = (c >>> 8) & 0xff;
@@ -1748,9 +1960,8 @@ const CHAR_CODE = {
1748
1960
  CR: '\r'.charCodeAt(0),
1749
1961
  LF: '\n'.charCodeAt(0),
1750
1962
  };
1751
- // Modbus ASCII frame encodes at most 256 PDU bytes as 512 hex chars between
1752
- // `:` and `\r`. Cap per-connection buffering so a peer that never sends `\r`
1753
- // cannot grow `state.frame` without bound.
1963
+ // Modbus ASCII frame body is capped well below the theoretical maximum so a
1964
+ // peer that never sends `\r` cannot grow `state.frame` without bound.
1754
1965
  const MAX_ASCII_PAYLOAD = 512;
1755
1966
  const HEX_DECODE = new Uint8Array(256);
1756
1967
  HEX_DECODE.fill(0xff);
@@ -1763,11 +1974,16 @@ for (let i = 0x41; i <= 0x46; i++) {
1763
1974
  for (let i = 0x61; i <= 0x66; i++) {
1764
1975
  HEX_DECODE[i] = i - 0x61 + 10;
1765
1976
  }
1977
+ // Strict variant: lowercase hex digits (a-f) are treated as invalid so the
1978
+ // hot-path validation loop needs only one table lookup instead of two checks.
1979
+ const HEX_DECODE_STRICT = new Uint8Array(HEX_DECODE);
1980
+ for (let i = 0x61; i <= 0x66; i++) {
1981
+ HEX_DECODE_STRICT[i] = 0xff;
1982
+ }
1766
1983
  const HEX_ENCODE = new Uint8Array('0123456789ABCDEF'.split('').map((c) => c.charCodeAt(0)));
1767
1984
  class AsciiApplicationLayer extends AbstractApplicationLayer {
1768
1985
  PROTOCOL = 'ASCII';
1769
1986
  ROLE;
1770
- lenientHex;
1771
1987
  _connection;
1772
1988
  _state = { status: 'idle', frame: new Uint8Array(MAX_ASCII_PAYLOAD), frameLen: 0 };
1773
1989
  _cleanupCbs = [];
@@ -1778,23 +1994,30 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1778
1994
  super();
1779
1995
  this.ROLE = role;
1780
1996
  this._connection = connection;
1781
- this.lenientHex = options.lenientHex ?? false;
1782
- const lenientHex = this.lenientHex;
1783
- const isHexChar = (value) => {
1784
- if (value >= 0x30 && value <= 0x39) {
1785
- return true;
1786
- }
1787
- if (value >= 0x41 && value <= 0x46) {
1788
- return true;
1789
- }
1790
- if (lenientHex && value >= 0x61 && value <= 0x66) {
1791
- return true;
1792
- }
1793
- return false;
1794
- };
1997
+ const lenientHex = options.lenientHex ?? false;
1998
+ const hexTable = lenientHex ? HEX_DECODE : HEX_DECODE_STRICT;
1795
1999
  const onData = (data) => {
1796
2000
  const state = this._state;
1797
- for (let i = 0; i < data.length; i++) {
2001
+ const dataLen = data.length;
2002
+ // Fast path: idle state and the chunk is exactly one complete frame
2003
+ // ASCII frame: length >= 9 and always odd (9 + 2n)
2004
+ if (state.status === 'idle' && dataLen >= 9 && dataLen % 2 !== 0) {
2005
+ if (data[0] === CHAR_CODE.COLON && data[dataLen - 2] === CHAR_CODE.CR && data[dataLen - 1] === CHAR_CODE.LF) {
2006
+ let valid = true;
2007
+ for (let i = 1; i < dataLen - 2; i++) {
2008
+ if (hexTable[data[i]] > 15) {
2009
+ valid = false;
2010
+ break;
2011
+ }
2012
+ }
2013
+ if (valid) {
2014
+ const hexView = data.subarray(1, dataLen - 2);
2015
+ this.framing(hexView, hexView.length, hexTable);
2016
+ return;
2017
+ }
2018
+ }
2019
+ }
2020
+ for (let i = 0; i < dataLen; i++) {
1798
2021
  const value = data[i];
1799
2022
  switch (state.status) {
1800
2023
  case 'idle': {
@@ -1816,7 +2039,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1816
2039
  state.frameLen = 0;
1817
2040
  this.onFramingError(new Error('Invalid data'));
1818
2041
  }
1819
- else if (!isHexChar(value)) {
2042
+ else if (hexTable[value] > 15) {
1820
2043
  state.status = 'idle';
1821
2044
  state.frameLen = 0;
1822
2045
  this.onFramingError(new Error('Invalid hex character'));
@@ -1834,7 +2057,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1834
2057
  else {
1835
2058
  state.status = 'idle';
1836
2059
  if (value === CHAR_CODE.LF) {
1837
- this.framing(state.frame, state.frameLen);
2060
+ this.framing(state.frame, state.frameLen, hexTable);
1838
2061
  }
1839
2062
  }
1840
2063
  break;
@@ -1853,7 +2076,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1853
2076
  connection.on('close', onClose);
1854
2077
  this._cleanupCbs.push(() => connection.off('close', onClose));
1855
2078
  }
1856
- framing(hexChars, hexLen) {
2079
+ framing(hexChars, hexLen, hexTable) {
1857
2080
  if (hexLen < 6) {
1858
2081
  this.onFramingError(new Error('Insufficient data length'));
1859
2082
  return;
@@ -1865,10 +2088,10 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1865
2088
  const byteLen = hexLen >> 1;
1866
2089
  // Decode unit and fc directly from the first 4 hex characters —
1867
2090
  // avoids allocating a full decoded buffer just to read two bytes.
1868
- const unitHi = HEX_DECODE[hexChars[0]];
1869
- const unitLo = HEX_DECODE[hexChars[1]];
1870
- const fcHi = HEX_DECODE[hexChars[2]];
1871
- const fcLo = HEX_DECODE[hexChars[3]];
2091
+ const unitHi = hexTable[hexChars[0]];
2092
+ const unitLo = hexTable[hexChars[1]];
2093
+ const fcHi = hexTable[hexChars[2]];
2094
+ const fcLo = hexTable[hexChars[3]];
1872
2095
  if (unitHi === 0xff || unitLo === 0xff || fcHi === 0xff || fcLo === 0xff) {
1873
2096
  this.onFramingError(new Error('Invalid hex character'));
1874
2097
  return;
@@ -1876,30 +2099,31 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1876
2099
  const unit = (unitHi << 4) | unitLo;
1877
2100
  const fc = (fcHi << 4) | fcLo;
1878
2101
  // Decode LRC from the last 2 hex characters.
1879
- const lrcHi = HEX_DECODE[hexChars[hexLen - 2]];
1880
- const lrcLo = HEX_DECODE[hexChars[hexLen - 1]];
2102
+ const lrcHi = hexTable[hexChars[hexLen - 2]];
2103
+ const lrcLo = hexTable[hexChars[hexLen - 1]];
1881
2104
  if (lrcHi === 0xff || lrcLo === 0xff) {
1882
2105
  this.onFramingError(new Error('Invalid hex character'));
1883
2106
  return;
1884
2107
  }
1885
2108
  const lrcIn = (lrcHi << 4) | lrcLo;
1886
- // Decode data portion (between unit/fc and lrc) into a right-sized buffer.
2109
+ // Decode data portion (between unit/fc and lrc) into a right-sized buffer,
2110
+ // while simultaneously accumulating the LRC sum — one pass instead of two.
1887
2111
  // dataLen may be 0 for a frame that is only unit + fc + lrc.
1888
2112
  const dataLen = byteLen - 3;
1889
2113
  const data = Buffer.allocUnsafe(dataLen);
2114
+ let hexOff = 4;
2115
+ let sum = unit + fc;
1890
2116
  for (let i = 0; i < dataLen; i++) {
1891
- const hi = HEX_DECODE[hexChars[4 + i * 2]];
1892
- const lo = HEX_DECODE[hexChars[4 + i * 2 + 1]];
2117
+ const hi = hexTable[hexChars[hexOff]];
2118
+ const lo = hexTable[hexChars[hexOff + 1]];
1893
2119
  if (hi === 0xff || lo === 0xff) {
1894
2120
  this.onFramingError(new Error('Invalid hex character'));
1895
2121
  return;
1896
2122
  }
1897
- data[i] = (hi << 4) | lo;
1898
- }
1899
- // Compute LRC over unit + fc + data.
1900
- let sum = unit + fc;
1901
- for (let i = 0; i < dataLen; i++) {
1902
- sum += data[i];
2123
+ const byte = (hi << 4) | lo;
2124
+ data[i] = byte;
2125
+ sum += byte;
2126
+ hexOff += 2;
1903
2127
  }
1904
2128
  const lrcComputed = (~sum + 1) & 0xff;
1905
2129
  if (lrcIn !== lrcComputed) {
@@ -1925,13 +2149,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1925
2149
  buffer[0] = unit;
1926
2150
  buffer[1] = fc;
1927
2151
  buffer.set(data, 2);
1928
- buffer[buffer.length - 1] = lrc(buffer.subarray(0, -1));
2152
+ buffer[buffer.length - 1] = lrc(buffer, 0, buffer.length - 1);
1929
2153
  const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
1930
2154
  out[0] = CHAR_CODE.COLON;
2155
+ let outOff = 1;
1931
2156
  for (let i = 0; i < buffer.length; i++) {
1932
2157
  const byte = buffer[i];
1933
- out[1 + i * 2] = HEX_ENCODE[byte >> 4];
1934
- out[2 + i * 2] = HEX_ENCODE[byte & 0x0f];
2158
+ out[outOff] = HEX_ENCODE[byte >> 4];
2159
+ out[outOff + 1] = HEX_ENCODE[byte & 0x0f];
2160
+ outOff += 2;
1935
2161
  }
1936
2162
  out[out.length - 2] = CHAR_CODE.CR;
1937
2163
  out[out.length - 1] = CHAR_CODE.LF;
@@ -1939,13 +2165,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1939
2165
  }
1940
2166
  }
1941
2167
 
1942
- const MAX_TCP_FRAME = 260;
2168
+ const MAX_FRAME_LENGTH = 260;
1943
2169
  class TcpApplicationLayer extends AbstractApplicationLayer {
1944
2170
  PROTOCOL = 'TCP';
1945
2171
  ROLE;
1946
2172
  _connection;
1947
2173
  _transactionId = 1;
1948
- _buffer = EMPTY_BUFFER;
2174
+ _residual = Buffer.alloc(MAX_FRAME_LENGTH);
2175
+ _residualLen = 0;
2176
+ _expectedLen = PREDICT_NEED_MORE;
1949
2177
  _cleanupCbs = [];
1950
2178
  get connection() {
1951
2179
  return this._connection;
@@ -1955,51 +2183,108 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
1955
2183
  this.ROLE = role;
1956
2184
  this._connection = connection;
1957
2185
  const onData = (data) => {
1958
- // Fast path: _buffer is empty and data is a single, complete frame.
1959
- // This avoids the tryExtract subarray allocations + while loop
1960
- // for the overwhelmingly common case (one frame per TCP packet).
1961
- if (this._buffer.length === 0 && data.length >= 8) {
1962
- const length = (data[4] << 8) | data[5]; // inline BE read
1963
- const total = 6 + length;
1964
- if (data[2] === 0 && data[3] === 0 && total <= MAX_TCP_FRAME && length >= 2 && data.length === total) {
1965
- this.processFrame(data);
1966
- return;
2186
+ const dataLen = data.length;
2187
+ const residualLen = this._residualLen;
2188
+ const totalAvailable = residualLen + dataLen;
2189
+ // Fast path: no residual data and the new chunk is exactly one frame
2190
+ if (residualLen === 0 && dataLen >= 8) {
2191
+ if (data[2] === 0 && data[3] === 0) {
2192
+ const length = (data[4] << 8) | data[5];
2193
+ const frameLen = 6 + length;
2194
+ if (frameLen === dataLen && frameLen <= MAX_FRAME_LENGTH && length >= 2) {
2195
+ const frame = {
2196
+ transaction: (data[0] << 8) | data[1],
2197
+ unit: data[6],
2198
+ fc: data[7],
2199
+ data: data.subarray(8),
2200
+ buffer: data,
2201
+ };
2202
+ this.onFraming(frame);
2203
+ this._expectedLen = PREDICT_NEED_MORE;
2204
+ return;
2205
+ }
1967
2206
  }
1968
2207
  }
1969
- let buffer = this._buffer;
1970
- if (buffer.length === 0) {
1971
- buffer = data;
2208
+ let index = 0;
2209
+ if (!(this._expectedLen > 0 && totalAvailable < this._expectedLen)) {
2210
+ while (index <= totalAvailable - 6) {
2211
+ // Validate MBAP protocol ID (bytes 2-3 must be 0x0000)
2212
+ if ((index + 2 < residualLen ? this._residual[index + 2] : data[index + 2 - residualLen]) !== 0 ||
2213
+ (index + 3 < residualLen ? this._residual[index + 3] : data[index + 3 - residualLen]) !== 0) {
2214
+ this.onFramingError(new Error('Invalid data'));
2215
+ this._residualLen = 0;
2216
+ this._expectedLen = PREDICT_NEED_MORE;
2217
+ return;
2218
+ }
2219
+ const length = ((index + 4 < residualLen ? this._residual[index + 4] : data[index + 4 - residualLen]) << 8) |
2220
+ (index + 5 < residualLen ? this._residual[index + 5] : data[index + 5 - residualLen]);
2221
+ const frameLen = 6 + length;
2222
+ if (frameLen > MAX_FRAME_LENGTH || length < 2) {
2223
+ this.onFramingError(new Error('Invalid data'));
2224
+ this._residualLen = 0;
2225
+ this._expectedLen = PREDICT_NEED_MORE;
2226
+ return;
2227
+ }
2228
+ if (totalAvailable - index < frameLen) {
2229
+ this._expectedLen = index + frameLen;
2230
+ break;
2231
+ }
2232
+ // A complete frame has been received
2233
+ this._expectedLen = PREDICT_NEED_MORE;
2234
+ const raw = Buffer.allocUnsafe(frameLen);
2235
+ if (index + frameLen <= residualLen) {
2236
+ this._residual.copy(raw, 0, index, index + frameLen);
2237
+ }
2238
+ else if (index >= residualLen) {
2239
+ data.copy(raw, 0, index - residualLen, index - residualLen + frameLen);
2240
+ }
2241
+ else {
2242
+ const headLen = residualLen - index;
2243
+ this._residual.copy(raw, 0, index, residualLen);
2244
+ data.copy(raw, headLen, 0, frameLen - headLen);
2245
+ }
2246
+ const frame = {
2247
+ transaction: (raw[0] << 8) | raw[1],
2248
+ unit: raw[6],
2249
+ fc: raw[7],
2250
+ data: raw.subarray(8),
2251
+ buffer: raw,
2252
+ };
2253
+ this.onFraming(frame);
2254
+ index += frameLen;
2255
+ }
1972
2256
  }
1973
- else if (data.length > 0) {
1974
- buffer = Buffer.concat([buffer, data]);
2257
+ const newFrameStart = index;
2258
+ const finalRestLen = totalAvailable - newFrameStart;
2259
+ if (finalRestLen === 0) {
2260
+ this._residualLen = 0;
2261
+ this._expectedLen = PREDICT_NEED_MORE;
1975
2262
  }
1976
- while (buffer.length > 0) {
1977
- const result = this.tryExtract(buffer);
1978
- if (result.kind === 'frame') {
1979
- this.processFrame(result.frame);
1980
- buffer = result.rest;
2263
+ else {
2264
+ const keepLen = finalRestLen < MAX_FRAME_LENGTH ? finalRestLen : MAX_FRAME_LENGTH;
2265
+ const discardLen = totalAvailable - keepLen;
2266
+ if (discardLen >= residualLen) {
2267
+ // Kept portion lies entirely within the new `data`
2268
+ data.copy(this._residual, 0, discardLen - residualLen, dataLen);
1981
2269
  }
1982
- else if (result.kind === 'insufficient') {
1983
- break;
2270
+ else if (discardLen > 0) {
2271
+ // Kept portion spans both buffers, or physical left-shift truncation occurred
2272
+ for (let i = 0; i < keepLen; i++) {
2273
+ this._residual[i] = discardLen + i < residualLen ? this._residual[discardLen + i] : data[discardLen + i - residualLen];
2274
+ }
1984
2275
  }
1985
2276
  else {
1986
- this.onFramingError(result.error);
1987
- buffer = EMPTY_BUFFER;
1988
- break;
2277
+ // discardLen === 0 (old data not consumed, truncation limit not hit) — simple append
2278
+ data.copy(this._residual, residualLen, 0, dataLen);
2279
+ }
2280
+ this._residualLen = keepLen;
2281
+ // Unify physical coordinate system translation
2282
+ if (discardLen > 0) {
2283
+ if (this._expectedLen > 0) {
2284
+ const newExpectedLen = this._expectedLen - discardLen;
2285
+ this._expectedLen = newExpectedLen > PREDICT_NEED_MORE ? newExpectedLen : PREDICT_NEED_MORE;
2286
+ }
1989
2287
  }
1990
- }
1991
- if (buffer.length === 0) {
1992
- this._buffer = EMPTY_BUFFER;
1993
- }
1994
- else if (buffer === data) {
1995
- // Copy into a right-sized buffer so we do not retain the potentially
1996
- // large backing buffer of the original incoming data (Node.js pool).
1997
- // `Buffer.copyBytesFrom` is a native fast-path — one C++ memcpy
1998
- // vs allocUnsafe + JS-level copy.
1999
- this._buffer = Buffer.copyBytesFrom(buffer);
2000
- }
2001
- else {
2002
- this._buffer = buffer;
2003
2288
  }
2004
2289
  };
2005
2290
  connection.on('data', onData);
@@ -2013,38 +2298,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2013
2298
  connection.on('close', onClose);
2014
2299
  this._cleanupCbs.push(() => connection.off('close', onClose));
2015
2300
  }
2016
- tryExtract(buffer) {
2017
- if (buffer.length < 8) {
2018
- return { kind: 'insufficient' };
2019
- }
2020
- if (buffer[2] !== 0 || buffer[3] !== 0) {
2021
- return { kind: 'error', error: new Error('Invalid data') };
2022
- }
2023
- const length = (buffer[4] << 8) | buffer[5]; // inline BE read
2024
- const total = 6 + length;
2025
- if (total > MAX_TCP_FRAME || length < 2) {
2026
- return { kind: 'error', error: new Error('Invalid data') };
2027
- }
2028
- if (buffer.length < total) {
2029
- return { kind: 'insufficient' };
2030
- }
2031
- return { kind: 'frame', frame: buffer.subarray(0, total), rest: total === buffer.length ? EMPTY_BUFFER : buffer.subarray(total) };
2032
- }
2033
- processFrame(buffer) {
2034
- const frame = {
2035
- // Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
2036
- // argument coercion + bounds check. Symmetric to the header writes in
2037
- // encode() above. Hits on every received TCP frame.
2038
- transaction: (buffer[0] << 8) | buffer[1],
2039
- unit: buffer[6],
2040
- fc: buffer[7],
2041
- data: buffer.subarray(8),
2042
- buffer,
2043
- };
2044
- this.onFraming(frame);
2045
- }
2046
2301
  flush() {
2047
- this._buffer = EMPTY_BUFFER;
2302
+ this._residualLen = 0;
2303
+ this._expectedLen = PREDICT_NEED_MORE;
2048
2304
  }
2049
2305
  encode(unit, fc, data, transaction) {
2050
2306
  const buffer = Buffer.allocUnsafe(data.length + 8);
@@ -2063,8 +2319,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2063
2319
  // Small-payload fast path: avoid C++ TypedArray.prototype.set boundary
2064
2320
  // crossing when the copy is just a few bytes (common for FC 3/4/6 requests).
2065
2321
  if (data.length <= 16) {
2066
- for (let i = 0; i < data.length; i++)
2322
+ for (let i = 0; i < data.length; i++) {
2067
2323
  buffer[8 + i] = data[i];
2324
+ }
2068
2325
  }
2069
2326
  else {
2070
2327
  buffer.set(data, 8);
@@ -2112,26 +2369,33 @@ class MasterSession {
2112
2369
  }
2113
2370
 
2114
2371
  function validateResponse(frame, unit, fc) {
2115
- if (frame.unit !== unit || frame.fc !== fc)
2372
+ if (frame.unit !== unit || frame.fc !== fc) {
2116
2373
  throw new Error('Invalid response');
2374
+ }
2117
2375
  }
2118
2376
  function validateByteCountResponse(frame, unit, fc, byteCount) {
2119
2377
  validateResponse(frame, unit, fc);
2120
- if (frame.data.length < 1 + byteCount)
2378
+ if (frame.data.length < 1 + byteCount) {
2121
2379
  throw new Error('Insufficient data length');
2122
- if (frame.data.length !== 1 + byteCount)
2380
+ }
2381
+ if (frame.data.length !== 1 + byteCount) {
2123
2382
  throw new Error('Invalid response');
2124
- if (frame.data[0] !== byteCount)
2383
+ }
2384
+ if (frame.data[0] !== byteCount) {
2125
2385
  throw new Error('Invalid response');
2386
+ }
2126
2387
  }
2127
2388
  function validateEchoResponse(frame, unit, fc, expected) {
2128
2389
  validateResponse(frame, unit, fc);
2129
- if (frame.data.length < expected.length)
2390
+ if (frame.data.length < expected.length) {
2130
2391
  throw new Error('Insufficient data length');
2131
- if (frame.data.length !== expected.length)
2392
+ }
2393
+ if (frame.data.length !== expected.length) {
2132
2394
  throw new Error('Invalid response');
2133
- if (!frame.data.equals(expected))
2395
+ }
2396
+ if (!frame.data.equals(expected)) {
2134
2397
  throw new Error('Invalid response');
2398
+ }
2135
2399
  }
2136
2400
  class ModbusMaster extends EventEmitter {
2137
2401
  timeout;
@@ -2159,8 +2423,9 @@ class ModbusMaster extends EventEmitter {
2159
2423
  _pendingExchanges = new Map();
2160
2424
  _timerHeap = new TimerHeap((id) => {
2161
2425
  const pending = this._pendingExchanges.get(id);
2162
- if (!pending)
2163
- return; // lazy deletion: already handled
2426
+ if (!pending) {
2427
+ return;
2428
+ } // lazy deletion: already handled
2164
2429
  pending.settled = true;
2165
2430
  this._pendingExchanges.delete(id);
2166
2431
  if (pending.sessionKey !== null) {
@@ -2202,6 +2467,7 @@ class ModbusMaster extends EventEmitter {
2202
2467
  appLayer.onFraming = NOOP;
2203
2468
  };
2204
2469
  const onFraming = (frame) => {
2470
+ this.emit('framing', frame, connection);
2205
2471
  this._masterSession.handleFrame(frame);
2206
2472
  };
2207
2473
  appLayer.onFraming = onFraming;
@@ -2210,17 +2476,34 @@ class ModbusMaster extends EventEmitter {
2210
2476
  appLayer.onFramingError = NOOP;
2211
2477
  };
2212
2478
  const onFramingError = (error) => {
2479
+ this.emit('framingError', error, connection);
2213
2480
  this._masterSession.handleError(error);
2214
2481
  };
2215
2482
  appLayer.onFramingError = onFramingError;
2216
2483
  this._cleanupFns.add(cleanupFramingError);
2484
+ const cleanupTx = () => connection.off('tx', onTx);
2485
+ const onTx = (buffer) => {
2486
+ this.emit('tx', buffer, connection);
2487
+ };
2488
+ connection.on('tx', onTx);
2489
+ this._cleanupFns.add(cleanupTx);
2490
+ const cleanupRx = () => connection.off('rx', onRx);
2491
+ const onRx = (buffer) => {
2492
+ this.emit('rx', buffer, connection);
2493
+ };
2494
+ connection.on('rx', onRx);
2495
+ this._cleanupFns.add(cleanupRx);
2217
2496
  const cleanupClose = () => connection.off('close', onClose);
2218
2497
  const onClose = () => {
2219
2498
  cleanupFraming();
2220
2499
  cleanupFramingError();
2500
+ cleanupTx();
2501
+ cleanupRx();
2221
2502
  cleanupClose();
2222
2503
  this._cleanupFns.delete(cleanupFraming);
2223
2504
  this._cleanupFns.delete(cleanupFramingError);
2505
+ this._cleanupFns.delete(cleanupTx);
2506
+ this._cleanupFns.delete(cleanupRx);
2224
2507
  this._cleanupFns.delete(cleanupClose);
2225
2508
  };
2226
2509
  connection.on('close', onClose);
@@ -2260,7 +2543,7 @@ class ModbusMaster extends EventEmitter {
2260
2543
  return new RtuApplicationLayer('MASTER', connection, {
2261
2544
  intervalBetweenFrames,
2262
2545
  interCharTimeout,
2263
- poolSize: this._protocol.opts?.poolSize,
2546
+ strictTiming: this._protocol.opts?.strictTiming,
2264
2547
  });
2265
2548
  }
2266
2549
  if (this._protocol.type === 'TCP') {
@@ -2347,7 +2630,9 @@ class ModbusMaster extends EventEmitter {
2347
2630
  }
2348
2631
  // Lazy-deletion timer architecture:
2349
2632
  // 1. Assign an exchangeId and register in _pendingExchanges.
2350
- // 2. Push deadline into the global TimerHeap (no per-request setTimeout).
2633
+ // 2. Push deadline into the global TimerHeap (one native setTimeout under
2634
+ // load; a fast direct-timer path is used when only 1-2 exchanges are
2635
+ // pending).
2351
2636
  // 3. When the response arrives, delete from Map — the heap entry is left
2352
2637
  // behind and silently discarded when it surfaces at the top (lazy deletion).
2353
2638
  const exchangeId = this._nextExchangeId++;
@@ -2358,11 +2643,13 @@ class ModbusMaster extends EventEmitter {
2358
2643
  this._timerHeap.add(exchangeId, timeout);
2359
2644
  connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
2360
2645
  const p = this._pendingExchanges.get(exchangeId);
2361
- if (!p || p.settled)
2646
+ if (!p || p.settled) {
2362
2647
  return;
2648
+ }
2363
2649
  const cb = p.callback;
2364
- if (!cb)
2650
+ if (!cb) {
2365
2651
  return;
2652
+ }
2366
2653
  p.settled = true;
2367
2654
  p.callback = null;
2368
2655
  this._pendingExchanges.delete(exchangeId);
@@ -2391,8 +2678,9 @@ class ModbusMaster extends EventEmitter {
2391
2678
  this._timerHeap.add(exchangeId, timeout);
2392
2679
  connection.write(payload, (writeErr) => {
2393
2680
  const p = this._pendingExchanges.get(exchangeId);
2394
- if (!p || p.settled)
2681
+ if (!p || p.settled) {
2395
2682
  return;
2683
+ }
2396
2684
  if (writeErr) {
2397
2685
  const cb = p.callback;
2398
2686
  if (cb) {
@@ -2417,8 +2705,9 @@ class ModbusMaster extends EventEmitter {
2417
2705
  // Timeout is managed by the global timer heap above.
2418
2706
  this._masterSession.start(key, (err, frame) => {
2419
2707
  const p2 = this._pendingExchanges.get(exchangeId);
2420
- if (!p2 || p2.settled)
2708
+ if (!p2 || p2.settled) {
2421
2709
  return;
2710
+ }
2422
2711
  const cb = p2.callback;
2423
2712
  if (cb) {
2424
2713
  p2.settled = true;
@@ -2430,10 +2719,8 @@ class ModbusMaster extends EventEmitter {
2430
2719
  });
2431
2720
  }
2432
2721
  writeFC1Or2(unit, fc, address, length, timeout) {
2433
- const byteCount = Math.ceil(length / 8);
2722
+ const byteCount = (length + 7) >> 3;
2434
2723
  const bufferTx = Buffer.allocUnsafe(4);
2435
- // Inline big-endian writes — direct typed-array stores skip the argument
2436
- // validation + bounds checks that `writeUInt16BE` runs on each call.
2437
2724
  bufferTx[0] = (address >>> 8) & 0xff;
2438
2725
  bufferTx[1] = address & 0xff;
2439
2726
  bufferTx[2] = (length >>> 8) & 0xff;
@@ -2450,12 +2737,44 @@ class ModbusMaster extends EventEmitter {
2450
2737
  }
2451
2738
  try {
2452
2739
  validateByteCountResponse(frame, unit, fc, byteCount);
2453
- const data = new Array(length);
2454
- for (let i = 0; i < length; i++) {
2455
- data[i] = (frame.data[1 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
2740
+ const data = new Uint8Array(length);
2741
+ let byteIdx = 1;
2742
+ let outIdx = 0;
2743
+ const fullBytes = length >> 3;
2744
+ for (let b = 0; b < fullBytes; b++) {
2745
+ const byte = frame.data[byteIdx++];
2746
+ data[outIdx++] = byte & 0x01;
2747
+ data[outIdx++] = (byte >>> 1) & 0x01;
2748
+ data[outIdx++] = (byte >>> 2) & 0x01;
2749
+ data[outIdx++] = (byte >>> 3) & 0x01;
2750
+ data[outIdx++] = (byte >>> 4) & 0x01;
2751
+ data[outIdx++] = (byte >>> 5) & 0x01;
2752
+ data[outIdx++] = (byte >>> 6) & 0x01;
2753
+ data[outIdx++] = (byte >>> 7) & 0x01;
2754
+ }
2755
+ const rem = length & 7;
2756
+ if (rem) {
2757
+ const byte = frame.data[byteIdx];
2758
+ data[outIdx++] = byte & 0x01;
2759
+ if (rem > 1) {
2760
+ data[outIdx++] = (byte >>> 1) & 0x01;
2761
+ }
2762
+ if (rem > 2) {
2763
+ data[outIdx++] = (byte >>> 2) & 0x01;
2764
+ }
2765
+ if (rem > 3) {
2766
+ data[outIdx++] = (byte >>> 3) & 0x01;
2767
+ }
2768
+ if (rem > 4) {
2769
+ data[outIdx++] = (byte >>> 4) & 0x01;
2770
+ }
2771
+ if (rem > 5) {
2772
+ data[outIdx++] = (byte >>> 5) & 0x01;
2773
+ }
2774
+ if (rem > 6) {
2775
+ data[outIdx++] = (byte >>> 6) & 0x01;
2776
+ }
2456
2777
  }
2457
- // Mutate the frame in place rather than spread-copying — `frame` is freshly
2458
- // allocated per request and not retained anywhere else.
2459
2778
  frame.data = data;
2460
2779
  resolve(frame);
2461
2780
  }
@@ -2500,9 +2819,10 @@ class ModbusMaster extends EventEmitter {
2500
2819
  // on each call. Symmetric to the slave-side BE write inlining
2501
2820
  // in handleFC3/FC4. At length=125 (FC3 max) that's 250 saved
2502
2821
  // bounds-check pairs per response.
2822
+ let off = 0;
2503
2823
  for (let i = 0; i < length; i++) {
2504
- const off = i * 2;
2505
2824
  data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
2825
+ off += 2;
2506
2826
  }
2507
2827
  frame.data = data;
2508
2828
  resolve(frame);
@@ -2525,7 +2845,7 @@ class ModbusMaster extends EventEmitter {
2525
2845
  writeSingleCoil(unit, address, value, timeout = this.timeout) {
2526
2846
  const fc = exports.FunctionCode.WRITE_SINGLE_COIL;
2527
2847
  const bufferTx = Buffer.allocUnsafe(4);
2528
- const coilValue = value ? COIL_ON : COIL_OFF;
2848
+ const coilValue = value === 1 ? COIL_ON : COIL_OFF;
2529
2849
  // Inline big-endian writes — see writeFC1Or2 for the rationale.
2530
2850
  bufferTx[0] = (address >>> 8) & 0xff;
2531
2851
  bufferTx[1] = address & 0xff;
@@ -2585,18 +2905,52 @@ class ModbusMaster extends EventEmitter {
2585
2905
  writeFC15;
2586
2906
  writeMultipleCoils(unit, address, value, timeout = this.timeout) {
2587
2907
  const fc = exports.FunctionCode.WRITE_MULTIPLE_COILS;
2588
- const byteCount = Math.ceil(value.length / 8);
2589
- const bufferTx = Buffer.alloc(5 + byteCount);
2908
+ const len = value.length;
2909
+ const byteCount = (len + 7) >> 3;
2910
+ const bufferTx = Buffer.allocUnsafe(5 + byteCount);
2590
2911
  // Inline big-endian writes — see writeFC1Or2 for the rationale.
2591
2912
  bufferTx[0] = (address >>> 8) & 0xff;
2592
2913
  bufferTx[1] = address & 0xff;
2593
- bufferTx[2] = (value.length >>> 8) & 0xff;
2594
- bufferTx[3] = value.length & 0xff;
2914
+ bufferTx[2] = (len >>> 8) & 0xff;
2915
+ bufferTx[3] = len & 0xff;
2595
2916
  bufferTx[4] = byteCount;
2596
- for (let i = 0; i < value.length; i++) {
2597
- if (value[i]) {
2598
- bufferTx[5 + Math.floor(i / 8)] |= 1 << i % 8;
2917
+ let out = 5;
2918
+ const fullBytes = len >> 3;
2919
+ for (let b = 0; b < fullBytes; b++) {
2920
+ const base = b << 3;
2921
+ bufferTx[out++] =
2922
+ (value[base] & 1) |
2923
+ ((value[base + 1] & 1) << 1) |
2924
+ ((value[base + 2] & 1) << 2) |
2925
+ ((value[base + 3] & 1) << 3) |
2926
+ ((value[base + 4] & 1) << 4) |
2927
+ ((value[base + 5] & 1) << 5) |
2928
+ ((value[base + 6] & 1) << 6) |
2929
+ ((value[base + 7] & 1) << 7);
2930
+ }
2931
+ const rem = len & 7;
2932
+ if (rem) {
2933
+ const base = fullBytes << 3;
2934
+ let acc = value[base] & 1;
2935
+ if (rem > 1) {
2936
+ acc |= (value[base + 1] & 1) << 1;
2937
+ }
2938
+ if (rem > 2) {
2939
+ acc |= (value[base + 2] & 1) << 2;
2940
+ }
2941
+ if (rem > 3) {
2942
+ acc |= (value[base + 3] & 1) << 3;
2599
2943
  }
2944
+ if (rem > 4) {
2945
+ acc |= (value[base + 4] & 1) << 4;
2946
+ }
2947
+ if (rem > 5) {
2948
+ acc |= (value[base + 5] & 1) << 5;
2949
+ }
2950
+ if (rem > 6) {
2951
+ acc |= (value[base + 6] & 1) << 6;
2952
+ }
2953
+ bufferTx[out] = acc;
2600
2954
  }
2601
2955
  return new Promise((resolve, reject) => {
2602
2956
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2630,11 +2984,12 @@ class ModbusMaster extends EventEmitter {
2630
2984
  bufferTx[2] = (value.length >>> 8) & 0xff;
2631
2985
  bufferTx[3] = value.length & 0xff;
2632
2986
  bufferTx[4] = byteCount;
2987
+ let off = 5;
2633
2988
  for (let i = 0; i < value.length; i++) {
2634
2989
  const v = value[i];
2635
- const off = 5 + i * 2;
2636
2990
  bufferTx[off] = (v >>> 8) & 0xff;
2637
2991
  bufferTx[off + 1] = v & 0xff;
2992
+ off += 2;
2638
2993
  }
2639
2994
  return new Promise((resolve, reject) => {
2640
2995
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2672,15 +3027,17 @@ class ModbusMaster extends EventEmitter {
2672
3027
  }
2673
3028
  try {
2674
3029
  validateResponse(frame, unit, fc);
2675
- if (frame.data.length < 2 + serverIdLength)
3030
+ if (frame.data.length < 2 + serverIdLength) {
2676
3031
  throw new Error('Insufficient data length');
2677
- if (frame.data.length !== 1 + frame.data[0])
3032
+ }
3033
+ if (frame.data.length !== 1 + frame.data[0]) {
2678
3034
  throw new Error('Invalid response');
3035
+ }
2679
3036
  const runStatusIndex = 1 + serverIdLength;
2680
3037
  frame.data = {
2681
3038
  serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
2682
3039
  runIndicatorStatus: frame.data[runStatusIndex] === 0xff,
2683
- additionalData: Array.from(frame.data.subarray(runStatusIndex + 1)),
3040
+ additionalData: frame.data.subarray(runStatusIndex + 1),
2684
3041
  };
2685
3042
  resolve(frame);
2686
3043
  }
@@ -2738,11 +3095,12 @@ class ModbusMaster extends EventEmitter {
2738
3095
  bufferTx[6] = (write.value.length >>> 8) & 0xff;
2739
3096
  bufferTx[7] = write.value.length & 0xff;
2740
3097
  bufferTx[8] = byteCount;
3098
+ let off = 9;
2741
3099
  for (let i = 0; i < write.value.length; i++) {
2742
3100
  const v = write.value[i];
2743
- const off = 9 + i * 2;
2744
3101
  bufferTx[off] = (v >>> 8) & 0xff;
2745
3102
  bufferTx[off + 1] = v & 0xff;
3103
+ off += 2;
2746
3104
  }
2747
3105
  return new Promise((resolve, reject) => {
2748
3106
  this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
@@ -2761,9 +3119,10 @@ class ModbusMaster extends EventEmitter {
2761
3119
  // closure + N readUInt16BE bounds-check pairs. See writeFC3Or4
2762
3120
  // response handler for the same optimization.
2763
3121
  const data = new Array(read.length);
3122
+ let off = 0;
2764
3123
  for (let i = 0; i < read.length; i++) {
2765
- const off = i * 2;
2766
3124
  data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
3125
+ off += 2;
2767
3126
  }
2768
3127
  frame.data = data;
2769
3128
  resolve(frame);
@@ -2789,10 +3148,12 @@ class ModbusMaster extends EventEmitter {
2789
3148
  }
2790
3149
  try {
2791
3150
  validateResponse(frame, unit, fc);
2792
- if (frame.data.length < 6)
3151
+ if (frame.data.length < 6) {
2793
3152
  throw new Error('Insufficient data length');
2794
- if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode)
3153
+ }
3154
+ if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode) {
2795
3155
  throw new Error('Invalid response');
3156
+ }
2796
3157
  const objects = [];
2797
3158
  let object = [];
2798
3159
  let totalBytes = 0;
@@ -2820,10 +3181,12 @@ class ModbusMaster extends EventEmitter {
2820
3181
  break;
2821
3182
  }
2822
3183
  }
2823
- if (objects.length !== frame.data[5])
3184
+ if (objects.length !== frame.data[5]) {
2824
3185
  throw new Error('Invalid response');
2825
- if (frame.data.length !== 6 + totalBytes)
3186
+ }
3187
+ if (frame.data.length !== 6 + totalBytes) {
2826
3188
  throw new Error('Invalid response');
3189
+ }
2827
3190
  frame.data = {
2828
3191
  readDeviceIDCode,
2829
3192
  conformityLevel: frame.data[2],
@@ -2870,7 +3233,7 @@ class ModbusMaster extends EventEmitter {
2870
3233
  });
2871
3234
  }
2872
3235
  /**
2873
- * Open the underlying physical layer and begin accepting connections.
3236
+ * Open the underlying physical layer and establish a connection.
2874
3237
  *
2875
3238
  * A `ModbusMaster` instance can only be opened once. Once {@link close}
2876
3239
  * is called — explicitly or because the physical layer disconnected —
@@ -2900,8 +3263,9 @@ class ModbusMaster extends EventEmitter {
2900
3263
  // (broadcasts have no session waiter; non-broadcasts still in the
2901
3264
  // pre-write-window haven't registered in session yet).
2902
3265
  for (const pending of this._pendingExchanges.values()) {
2903
- if (pending.settled)
3266
+ if (pending.settled) {
2904
3267
  continue;
3268
+ }
2905
3269
  pending.settled = true;
2906
3270
  const cb = pending.callback;
2907
3271
  if (cb) {
@@ -2979,6 +3343,7 @@ class ModbusSlave extends EventEmitter {
2979
3343
  appLayer.onFraming = NOOP;
2980
3344
  };
2981
3345
  const onFraming = (frame) => {
3346
+ this.emit('framing', frame, connection);
2982
3347
  if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
2983
3348
  return;
2984
3349
  }
@@ -2994,11 +3359,37 @@ class ModbusSlave extends EventEmitter {
2994
3359
  };
2995
3360
  appLayer.onFraming = onFraming;
2996
3361
  this._cleanupFns.add(cleanupFraming);
3362
+ const cleanupFramingError = () => {
3363
+ appLayer.onFramingError = NOOP;
3364
+ };
3365
+ const onFramingError = (error) => {
3366
+ this.emit('framingError', error, connection);
3367
+ };
3368
+ appLayer.onFramingError = onFramingError;
3369
+ this._cleanupFns.add(cleanupFramingError);
3370
+ const cleanupTx = () => connection.off('tx', onTx);
3371
+ const onTx = (buffer) => {
3372
+ this.emit('tx', buffer, connection);
3373
+ };
3374
+ connection.on('tx', onTx);
3375
+ this._cleanupFns.add(cleanupTx);
3376
+ const cleanupRx = () => connection.off('rx', onRx);
3377
+ const onRx = (buffer) => {
3378
+ this.emit('rx', buffer, connection);
3379
+ };
3380
+ connection.on('rx', onRx);
3381
+ this._cleanupFns.add(cleanupRx);
2997
3382
  const cleanupClose = () => connection.off('close', onClose);
2998
3383
  const onClose = () => {
2999
3384
  cleanupFraming();
3385
+ cleanupFramingError();
3386
+ cleanupTx();
3387
+ cleanupRx();
3000
3388
  cleanupClose();
3001
3389
  this._cleanupFns.delete(cleanupFraming);
3390
+ this._cleanupFns.delete(cleanupFramingError);
3391
+ this._cleanupFns.delete(cleanupTx);
3392
+ this._cleanupFns.delete(cleanupRx);
3002
3393
  this._cleanupFns.delete(cleanupClose);
3003
3394
  this._appLayers.delete(appLayer);
3004
3395
  };
@@ -3027,7 +3418,7 @@ class ModbusSlave extends EventEmitter {
3027
3418
  return new RtuApplicationLayer('SLAVE', connection, {
3028
3419
  intervalBetweenFrames,
3029
3420
  interCharTimeout,
3030
- poolSize: this._protocol.opts?.poolSize,
3421
+ strictTiming: this._protocol.opts?.strictTiming,
3031
3422
  });
3032
3423
  }
3033
3424
  if (this._protocol.type === 'TCP') {
@@ -3059,23 +3450,44 @@ class ModbusSlave extends EventEmitter {
3059
3450
  const byteCount = (length + 7) >> 3;
3060
3451
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3061
3452
  pdu[0] = byteCount;
3062
- // Pack 8 booleans per byte without first zero-filling: accumulate into
3063
- // `acc` and write a full byte once each lane is finished. Saves the
3064
- // `pdu.fill(0, 1, ...)` pass and replaces N `|=` reads-modify-writes
3065
- // with N `read`s + ⌈N/8⌉ `write`s. Measured ~+5% on FC01 / ~+3% on FC02
3066
- // at max payload (2000 / 1968 coils) via benchmark/all-fcs.ts bisect.
3067
- let acc = 0;
3068
3453
  let out = 1;
3069
- for (let i = 0; i < length; i++) {
3070
- if (coils[i])
3071
- acc |= 1 << (i & 7);
3072
- if ((i & 7) === 7) {
3073
- pdu[out++] = acc;
3074
- acc = 0;
3075
- }
3454
+ const fullBytes = length >> 3;
3455
+ for (let i = 0; i < fullBytes; i++) {
3456
+ const base = i << 3;
3457
+ pdu[out++] =
3458
+ (coils[base] & 1) |
3459
+ ((coils[base + 1] & 1) << 1) |
3460
+ ((coils[base + 2] & 1) << 2) |
3461
+ ((coils[base + 3] & 1) << 3) |
3462
+ ((coils[base + 4] & 1) << 4) |
3463
+ ((coils[base + 5] & 1) << 5) |
3464
+ ((coils[base + 6] & 1) << 6) |
3465
+ ((coils[base + 7] & 1) << 7);
3076
3466
  }
3077
- if ((length & 7) !== 0)
3467
+ const rem = length & 7;
3468
+ if (rem) {
3469
+ const base = fullBytes << 3;
3470
+ let acc = coils[base] & 1;
3471
+ if (rem > 1) {
3472
+ acc |= (coils[base + 1] & 1) << 1;
3473
+ }
3474
+ if (rem > 2) {
3475
+ acc |= (coils[base + 2] & 1) << 2;
3476
+ }
3477
+ if (rem > 3) {
3478
+ acc |= (coils[base + 3] & 1) << 3;
3479
+ }
3480
+ if (rem > 4) {
3481
+ acc |= (coils[base + 4] & 1) << 4;
3482
+ }
3483
+ if (rem > 5) {
3484
+ acc |= (coils[base + 5] & 1) << 5;
3485
+ }
3486
+ if (rem > 6) {
3487
+ acc |= (coils[base + 6] & 1) << 6;
3488
+ }
3078
3489
  pdu[out] = acc;
3490
+ }
3079
3491
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3080
3492
  }
3081
3493
  catch (error) {
@@ -3106,19 +3518,44 @@ class ModbusSlave extends EventEmitter {
3106
3518
  const byteCount = (length + 7) >> 3;
3107
3519
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3108
3520
  pdu[0] = byteCount;
3109
- // Accumulator-based bit pack — see handleFC1 for the rationale.
3110
- let acc = 0;
3111
3521
  let out = 1;
3112
- for (let i = 0; i < length; i++) {
3113
- if (discreteInputs[i])
3114
- acc |= 1 << (i & 7);
3115
- if ((i & 7) === 7) {
3116
- pdu[out++] = acc;
3117
- acc = 0;
3118
- }
3522
+ const fullBytes = length >> 3;
3523
+ for (let i = 0; i < fullBytes; i++) {
3524
+ const base = i << 3;
3525
+ pdu[out++] =
3526
+ (discreteInputs[base] & 1) |
3527
+ ((discreteInputs[base + 1] & 1) << 1) |
3528
+ ((discreteInputs[base + 2] & 1) << 2) |
3529
+ ((discreteInputs[base + 3] & 1) << 3) |
3530
+ ((discreteInputs[base + 4] & 1) << 4) |
3531
+ ((discreteInputs[base + 5] & 1) << 5) |
3532
+ ((discreteInputs[base + 6] & 1) << 6) |
3533
+ ((discreteInputs[base + 7] & 1) << 7);
3119
3534
  }
3120
- if ((length & 7) !== 0)
3535
+ const rem = length & 7;
3536
+ if (rem) {
3537
+ const base = fullBytes << 3;
3538
+ let acc = discreteInputs[base] & 1;
3539
+ if (rem > 1) {
3540
+ acc |= (discreteInputs[base + 1] & 1) << 1;
3541
+ }
3542
+ if (rem > 2) {
3543
+ acc |= (discreteInputs[base + 2] & 1) << 2;
3544
+ }
3545
+ if (rem > 3) {
3546
+ acc |= (discreteInputs[base + 3] & 1) << 3;
3547
+ }
3548
+ if (rem > 4) {
3549
+ acc |= (discreteInputs[base + 4] & 1) << 4;
3550
+ }
3551
+ if (rem > 5) {
3552
+ acc |= (discreteInputs[base + 5] & 1) << 5;
3553
+ }
3554
+ if (rem > 6) {
3555
+ acc |= (discreteInputs[base + 6] & 1) << 6;
3556
+ }
3121
3557
  pdu[out] = acc;
3558
+ }
3122
3559
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3123
3560
  }
3124
3561
  catch (error) {
@@ -3151,11 +3588,12 @@ class ModbusSlave extends EventEmitter {
3151
3588
  // Inline big-endian write — `pdu[i] = v` is a direct typed-array store,
3152
3589
  // while `writeUInt16BE` runs argument validation + bounds checks on each
3153
3590
  // call. At length=125 (FC3 max) that's 250 saved checks per request.
3591
+ let off = 1;
3154
3592
  for (let i = 0; i < length; i++) {
3155
3593
  const v = registers[i];
3156
- const off = 1 + i * 2;
3157
3594
  pdu[off] = (v >>> 8) & 0xff;
3158
3595
  pdu[off + 1] = v & 0xff;
3596
+ off += 2;
3159
3597
  }
3160
3598
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3161
3599
  }
@@ -3187,11 +3625,12 @@ class ModbusSlave extends EventEmitter {
3187
3625
  const pdu = Buffer.allocUnsafe(length * 2 + 1);
3188
3626
  pdu[0] = length * 2;
3189
3627
  // Inline big-endian write — see handleFC3 for the rationale.
3628
+ let off = 1;
3190
3629
  for (let i = 0; i < length; i++) {
3191
3630
  const v = registers[i];
3192
- const off = 1 + i * 2;
3193
3631
  pdu[off] = (v >>> 8) & 0xff;
3194
3632
  pdu[off + 1] = v & 0xff;
3633
+ off += 2;
3195
3634
  }
3196
3635
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3197
3636
  }
@@ -3219,7 +3658,7 @@ class ModbusSlave extends EventEmitter {
3219
3658
  return;
3220
3659
  }
3221
3660
  try {
3222
- await model.writeSingleCoil(address, value === COIL_ON);
3661
+ await model.writeSingleCoil(address, value === COIL_ON ? 1 : 0);
3223
3662
  await response(appLayer.encode(frame.unit, frame.fc, frame.data, frame.transaction));
3224
3663
  }
3225
3664
  catch (error) {
@@ -3261,7 +3700,7 @@ class ModbusSlave extends EventEmitter {
3261
3700
  const address = (frame.data[0] << 8) | frame.data[1];
3262
3701
  const length = (frame.data[2] << 8) | frame.data[3];
3263
3702
  const byteCount = frame.data[4];
3264
- if (length < LIMITS.READ_COILS_MIN || length > LIMITS.WRITE_COILS_MAX || byteCount !== Math.ceil(length / 8)) {
3703
+ if (length < LIMITS.READ_COILS_MIN || length > LIMITS.WRITE_COILS_MAX || byteCount !== (length + 7) >> 3) {
3265
3704
  await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.ILLEGAL_DATA_VALUE));
3266
3705
  return;
3267
3706
  }
@@ -3269,13 +3708,43 @@ class ModbusSlave extends EventEmitter {
3269
3708
  await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.ILLEGAL_DATA_ADDRESS));
3270
3709
  return;
3271
3710
  }
3272
- const value = new Array(length);
3273
- for (let i = 0; i < length; i++) {
3274
- // Bit math: kept as `Math.floor(i / 8)` / `i % 8` — V8 pattern-matches
3275
- // both forms with `i` as an integer loop counter to direct shifts, but
3276
- // measured ~+3% throughput vs the bit-shift form on this exact loop
3277
- // (benchmark/all-fcs.ts FC15 slave A/B, 8/8 paired samples).
3278
- value[i] = (frame.data[5 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
3711
+ const value = new Uint8Array(length);
3712
+ let byteIdx = 5;
3713
+ let outIdx = 0;
3714
+ const fullBytes = length >> 3;
3715
+ for (let b = 0; b < fullBytes; b++) {
3716
+ const byte = frame.data[byteIdx++];
3717
+ value[outIdx++] = byte & 0x01;
3718
+ value[outIdx++] = (byte >>> 1) & 0x01;
3719
+ value[outIdx++] = (byte >>> 2) & 0x01;
3720
+ value[outIdx++] = (byte >>> 3) & 0x01;
3721
+ value[outIdx++] = (byte >>> 4) & 0x01;
3722
+ value[outIdx++] = (byte >>> 5) & 0x01;
3723
+ value[outIdx++] = (byte >>> 6) & 0x01;
3724
+ value[outIdx++] = (byte >>> 7) & 0x01;
3725
+ }
3726
+ const rem = length & 7;
3727
+ if (rem) {
3728
+ const byte = frame.data[byteIdx];
3729
+ value[outIdx++] = byte & 0x01;
3730
+ if (rem > 1) {
3731
+ value[outIdx++] = (byte >>> 1) & 0x01;
3732
+ }
3733
+ if (rem > 2) {
3734
+ value[outIdx++] = (byte >>> 2) & 0x01;
3735
+ }
3736
+ if (rem > 3) {
3737
+ value[outIdx++] = (byte >>> 3) & 0x01;
3738
+ }
3739
+ if (rem > 4) {
3740
+ value[outIdx++] = (byte >>> 4) & 0x01;
3741
+ }
3742
+ if (rem > 5) {
3743
+ value[outIdx++] = (byte >>> 5) & 0x01;
3744
+ }
3745
+ if (rem > 6) {
3746
+ value[outIdx++] = (byte >>> 6) & 0x01;
3747
+ }
3279
3748
  }
3280
3749
  try {
3281
3750
  if (model.writeMultipleCoils) {
@@ -3313,8 +3782,10 @@ class ModbusSlave extends EventEmitter {
3313
3782
  return;
3314
3783
  }
3315
3784
  const value = new Array(length);
3785
+ let off = 5;
3316
3786
  for (let i = 0; i < length; i++) {
3317
- value[i] = (frame.data[5 + i * 2] << 8) | frame.data[6 + i * 2];
3787
+ value[i] = (frame.data[off] << 8) | frame.data[off + 1];
3788
+ off += 2;
3318
3789
  }
3319
3790
  try {
3320
3791
  if (model.writeMultipleRegisters) {
@@ -3341,21 +3812,41 @@ class ModbusSlave extends EventEmitter {
3341
3812
  return;
3342
3813
  }
3343
3814
  try {
3344
- const { serverId = [model.unit ?? 1], runIndicatorStatus = true, additionalData = [] } = await model.reportServerId();
3345
- const serverIdBytes = serverId;
3346
- const byteCount = serverIdBytes.length + 1 + additionalData.length;
3815
+ const result = await model.reportServerId();
3816
+ const sid = result.serverId;
3817
+ const extra = result.additionalData;
3818
+ const sidLen = sid?.length ?? 1;
3819
+ const extraLen = extra?.length ?? 0;
3820
+ const byteCount = sidLen + 1 + extraLen;
3347
3821
  if (byteCount > 255) {
3348
3822
  await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.SERVER_DEVICE_FAILURE));
3349
3823
  return;
3350
3824
  }
3351
- const allBytes = [...serverIdBytes, runIndicatorStatus ? 0xff : 0x00, ...additionalData];
3352
- if (allBytes.some((b) => !isUint8(b))) {
3353
- await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.SERVER_DEVICE_FAILURE));
3354
- return;
3355
- }
3356
3825
  const data = Buffer.allocUnsafe(byteCount + 1);
3357
3826
  data[0] = byteCount;
3358
- data.set(allBytes, 1);
3827
+ let off = 1;
3828
+ if (sid) {
3829
+ for (let i = 0; i < sidLen; i++) {
3830
+ const b = sid[i];
3831
+ if ((b & 0xff) !== b) {
3832
+ await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.SERVER_DEVICE_FAILURE));
3833
+ return;
3834
+ }
3835
+ data[off++] = b;
3836
+ }
3837
+ }
3838
+ else {
3839
+ const unitId = model.unit ?? 1;
3840
+ if ((unitId & 0xff) !== unitId) {
3841
+ await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.SERVER_DEVICE_FAILURE));
3842
+ return;
3843
+ }
3844
+ data[off++] = unitId;
3845
+ }
3846
+ data[off++] = (result.runIndicatorStatus ?? true) ? 0xff : 0x00;
3847
+ if (extra) {
3848
+ extra.copy(data, off);
3849
+ }
3359
3850
  await response(appLayer.encode(frame.unit, frame.fc, data, frame.transaction));
3360
3851
  }
3361
3852
  catch (error) {
@@ -3425,8 +3916,10 @@ class ModbusSlave extends EventEmitter {
3425
3916
  return;
3426
3917
  }
3427
3918
  const value = new Array(length.write);
3919
+ let off = 9;
3428
3920
  for (let i = 0; i < length.write; i++) {
3429
- value[i] = (frame.data[9 + i * 2] << 8) | frame.data[10 + i * 2];
3921
+ value[i] = (frame.data[off] << 8) | frame.data[off + 1];
3922
+ off += 2;
3430
3923
  }
3431
3924
  try {
3432
3925
  await this._withIntervalLock(address.write, address.write + length.write, async () => {
@@ -3443,11 +3936,12 @@ class ModbusSlave extends EventEmitter {
3443
3936
  const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
3444
3937
  pdu[0] = length.read * 2;
3445
3938
  // Inline big-endian write — see handleFC3 for the rationale.
3939
+ let off = 1;
3446
3940
  for (let i = 0; i < length.read; i++) {
3447
3941
  const v = registers[i];
3448
- const off = 1 + i * 2;
3449
3942
  pdu[off] = (v >>> 8) & 0xff;
3450
3943
  pdu[off + 1] = v & 0xff;
3944
+ off += 2;
3451
3945
  }
3452
3946
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3453
3947
  }
@@ -3679,8 +4173,9 @@ class ModbusSlave extends EventEmitter {
3679
4173
  for (let i = 0; i < locks.length; i++) {
3680
4174
  const l = locks[i];
3681
4175
  if (l.lo < hi && lo < l.hi) {
3682
- if (overlap === null)
4176
+ if (overlap === null) {
3683
4177
  overlap = [];
4178
+ }
3684
4179
  overlap.push(l.promise);
3685
4180
  }
3686
4181
  }
@@ -3716,8 +4211,9 @@ class ModbusSlave extends EventEmitter {
3716
4211
  if (i !== -1) {
3717
4212
  // O(1) swap-and-pop since lock order doesn't matter for correctness.
3718
4213
  const last = locks.length - 1;
3719
- if (i !== last)
4214
+ if (i !== last) {
3720
4215
  locks[i] = locks[last];
4216
+ }
3721
4217
  locks.pop();
3722
4218
  }
3723
4219
  }