njs-modbus 3.3.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.mjs CHANGED
@@ -232,7 +232,7 @@ function checkRange(value, range) {
232
232
  return false;
233
233
  }
234
234
 
235
- const TABLE = new Uint16Array([
235
+ const CRC_TABLE = new Uint16Array([
236
236
  0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
237
237
  0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
238
238
  0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
@@ -250,23 +250,29 @@ const TABLE = new Uint16Array([
250
250
  0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
251
251
  0x4040,
252
252
  ]);
253
- function crc(data, start, end) {
253
+ /** CRC-16 (Modbus) over a single contiguous buffer. */
254
+ function crcFixed(data, start, end) {
254
255
  let crc = 0xffff;
255
256
  for (let index = start; index < end; index++) {
256
- crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
257
+ crc = CRC_TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
257
258
  }
258
259
  return crc;
259
260
  }
260
-
261
261
  /**
262
- * Returns true when `n` is an integer in the unsigned-byte range [0, 255].
263
- *
264
- * Used for byte-level Modbus payload validation (function-code values, raw
265
- * byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
266
- * Infinity, and out-of-range values uniformly.
262
+ * CRC-16 (Modbus) over two contiguous buffer segments.
263
+ * Computes CRC(head[headOff:headOff+headLen]) followed by CRC(tail[tailOff:tailOff+tailLen]).
267
264
  */
268
- function isUint8(n) {
269
- return (n & 0xff) === n;
265
+ function crcDual(head, headOff, headLen, tail, tailOff, tailLen) {
266
+ let crc = 0xffff;
267
+ const headEnd = headOff + headLen;
268
+ for (let i = headOff; i < headEnd; i++) {
269
+ crc = CRC_TABLE[(crc ^ head[i]) & 0xff] ^ (crc >> 8);
270
+ }
271
+ const tailEnd = tailOff + tailLen;
272
+ for (let i = tailOff; i < tailEnd; i++) {
273
+ crc = CRC_TABLE[(crc ^ tail[i]) & 0xff] ^ (crc >> 8);
274
+ }
275
+ return crc;
270
276
  }
271
277
 
272
278
  function lrc(data, start, end) {
@@ -307,12 +313,12 @@ const RES_TABLE = new Int32Array(256);
307
313
  RES_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
308
314
  RES_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
309
315
  })();
310
- function predictRtuFrameLength(buffer, start, end, isResponse) {
316
+ function predictRtuFrameLength(residual, data, residualLen, start, end, isResponse) {
311
317
  const len = end - start;
312
318
  if (len < 2) {
313
319
  return PREDICT_NEED_MORE;
314
320
  }
315
- const fc = buffer[start + 1];
321
+ const fc = start + 1 < residualLen ? residual[start + 1] : data[start + 1 - residualLen];
316
322
  if (isResponse) {
317
323
  if ((fc & EXCEPTION_OFFSET) !== 0) {
318
324
  return 5;
@@ -328,16 +334,16 @@ function predictRtuFrameLength(buffer, start, end, isResponse) {
328
334
  if (end - start < 8) {
329
335
  return PREDICT_NEED_MORE;
330
336
  }
331
- if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
337
+ if ((start + 2 < residualLen ? residual[start + 2] : data[start + 2 - residualLen]) !== MEI_READ_DEVICE_ID) {
332
338
  return PREDICT_UNKNOWN;
333
339
  }
334
- const numObjs = buffer[start + 7];
340
+ const numObjs = start + 7 < residualLen ? residual[start + 7] : data[start + 7 - residualLen];
335
341
  let cursor = start + 8;
336
342
  for (let i = 0; i < numObjs; i++) {
337
343
  if (end < cursor + 2) {
338
344
  return PREDICT_NEED_MORE;
339
345
  }
340
- cursor += 2 + buffer[cursor + 1];
346
+ cursor += 2 + (cursor + 1 < residualLen ? residual[cursor + 1] : data[cursor + 1 - residualLen]);
341
347
  }
342
348
  return cursor - start + 2;
343
349
  }
@@ -346,7 +352,7 @@ function predictRtuFrameLength(buffer, start, end, isResponse) {
346
352
  if (len <= offset) {
347
353
  return PREDICT_NEED_MORE;
348
354
  }
349
- return (decode & 0xff) + buffer[start + offset];
355
+ return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
350
356
  }
351
357
  }
352
358
  else {
@@ -360,7 +366,7 @@ function predictRtuFrameLength(buffer, start, end, isResponse) {
360
366
  if (len <= offset) {
361
367
  return PREDICT_NEED_MORE;
362
368
  }
363
- return (decode & 0xff) + buffer[start + offset];
369
+ return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
364
370
  }
365
371
  }
366
372
  return PREDICT_UNKNOWN;
@@ -403,7 +409,12 @@ function resolveOne(value, baudRate, fastBaudMs) {
403
409
  if (baudRate === undefined) {
404
410
  return undefined;
405
411
  }
406
- return baudRate > 19200 ? fastBaudMs : Math.ceil(bitsToMs(baudRate, value.value));
412
+ if (baudRate > 19200) {
413
+ return fastBaudMs;
414
+ }
415
+ const ms = bitsToMs(baudRate, value.value);
416
+ const trunc = ms | 0;
417
+ return trunc + (ms > trunc ? 1 : 0);
407
418
  }
408
419
  /**
409
420
  * Resolve Modbus RTU timing parameters from user options into milliseconds.
@@ -420,7 +431,17 @@ function resolveRtuTiming(opts = {}, baudRate) {
420
431
  if (intervalBetweenFrames === undefined) {
421
432
  // Spec default: t3.5 derived from baudRate, or 0 when neither option nor
422
433
  // baudRate were supplied.
423
- intervalBetweenFrames = baudRate !== undefined ? (baudRate > 19200 ? 1.75 : Math.ceil(bitsToMs(baudRate, 38.5))) : 0;
434
+ if (baudRate === undefined) {
435
+ intervalBetweenFrames = 0;
436
+ }
437
+ else if (baudRate > 19200) {
438
+ intervalBetweenFrames = 1.75;
439
+ }
440
+ else {
441
+ const ms = bitsToMs(baudRate, 38.5);
442
+ const trunc = ms | 0;
443
+ intervalBetweenFrames = trunc + (ms > trunc ? 1 : 0);
444
+ }
424
445
  }
425
446
  let interCharTimeout = resolveOne(opts.interCharTimeout, baudRate, 0.75);
426
447
  if (interCharTimeout === undefined) {
@@ -488,7 +509,9 @@ class TimerHeap {
488
509
  this._mode = 'heap';
489
510
  for (const [existingId, { handle, deadline }] of this._directTimers) {
490
511
  clearTimeout(handle);
491
- const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
512
+ const diff = deadline - performance.now();
513
+ const trunc = diff | 0;
514
+ const remaining = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
492
515
  if (remaining === 0) {
493
516
  this._onFire(existingId);
494
517
  }
@@ -548,8 +571,10 @@ class TimerHeap {
548
571
  if (this._deadlines.length === 0) {
549
572
  return;
550
573
  }
551
- const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
552
- const safeDelay = Math.min(delay, 2147483647);
574
+ const diff = this._deadlines[0] - performance.now();
575
+ const trunc = diff | 0;
576
+ const delay = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
577
+ const safeDelay = delay < 2147483647 ? delay : 2147483647;
553
578
  this._timer = setTimeout(this._boundTick, safeDelay);
554
579
  }
555
580
  _onTick() {
@@ -633,6 +658,9 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
633
658
  get physicalLayer() {
634
659
  return this._physicalLayer;
635
660
  }
661
+ get serialport() {
662
+ return this._serialport;
663
+ }
636
664
  constructor(physicalLayer, serialport) {
637
665
  super();
638
666
  this._physicalLayer = physicalLayer;
@@ -641,6 +669,7 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
641
669
  if (this.state !== PhysicalConnectionState.CONNECTED) {
642
670
  return;
643
671
  }
672
+ this.emit('rx', chunk);
644
673
  this.emit('data', chunk);
645
674
  };
646
675
  serialport.on('data', onData);
@@ -669,7 +698,12 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
669
698
  }
670
699
  write(data, cb) {
671
700
  if (this.state === PhysicalConnectionState.CONNECTED) {
672
- this._serialport.write(data, cb);
701
+ this._serialport.write(data, (err) => {
702
+ if (!err) {
703
+ this.emit('tx', data);
704
+ }
705
+ cb?.(err);
706
+ });
673
707
  }
674
708
  else {
675
709
  cb?.(new Error('Connection is not connected'));
@@ -830,6 +864,9 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
830
864
  get physicalLayer() {
831
865
  return this._physicalLayer;
832
866
  }
867
+ get socket() {
868
+ return this._socket;
869
+ }
833
870
  constructor(physicalLayer, socket) {
834
871
  super();
835
872
  this._physicalLayer = physicalLayer;
@@ -838,6 +875,7 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
838
875
  if (this.state !== PhysicalConnectionState.CONNECTED) {
839
876
  return;
840
877
  }
878
+ this.emit('rx', chunk);
841
879
  this.emit('data', chunk);
842
880
  };
843
881
  socket.on('data', onData);
@@ -866,7 +904,12 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
866
904
  }
867
905
  write(data, cb) {
868
906
  if (this.state === PhysicalConnectionState.CONNECTED) {
869
- this._socket.write(data, cb);
907
+ this._socket.write(data, (err) => {
908
+ if (!err) {
909
+ this.emit('tx', data);
910
+ }
911
+ cb?.(err);
912
+ });
870
913
  }
871
914
  else {
872
915
  cb?.(new Error('Connection is not connected'));
@@ -1148,6 +1191,9 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1148
1191
  get physicalLayer() {
1149
1192
  return this._physicalLayer;
1150
1193
  }
1194
+ get socket() {
1195
+ return this._socket;
1196
+ }
1151
1197
  constructor(physicalLayer, socket) {
1152
1198
  super();
1153
1199
  this._physicalLayer = physicalLayer;
@@ -1156,6 +1202,7 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1156
1202
  if (this.state !== PhysicalConnectionState.CONNECTED) {
1157
1203
  return;
1158
1204
  }
1205
+ this.emit('rx', msg);
1159
1206
  this.emit('data', msg);
1160
1207
  };
1161
1208
  socket.on('message', onMessage);
@@ -1184,7 +1231,12 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1184
1231
  }
1185
1232
  write(data, cb) {
1186
1233
  if (this.state === PhysicalConnectionState.CONNECTED) {
1187
- this._socket.send(data, cb);
1234
+ this._socket.send(data, (err) => {
1235
+ if (!err) {
1236
+ this.emit('tx', data);
1237
+ }
1238
+ cb?.(err);
1239
+ });
1188
1240
  }
1189
1241
  else {
1190
1242
  cb?.(new Error('Connection is not connected'));
@@ -1324,6 +1376,12 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1324
1376
  get physicalLayer() {
1325
1377
  return this._physicalLayer;
1326
1378
  }
1379
+ get socket() {
1380
+ return this._socket;
1381
+ }
1382
+ get remote() {
1383
+ return this._remote;
1384
+ }
1327
1385
  constructor(physicalLayer, socket, remote, idleTimeout, messageEventDelegation) {
1328
1386
  super();
1329
1387
  this._physicalLayer = physicalLayer;
@@ -1342,6 +1400,7 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1342
1400
  }, idleTimeout);
1343
1401
  }
1344
1402
  }
1403
+ this.emit('rx', msg);
1345
1404
  this.emit('data', msg);
1346
1405
  };
1347
1406
  messageEventDelegation.add(onMessage);
@@ -1354,7 +1413,12 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1354
1413
  }
1355
1414
  write(data, cb) {
1356
1415
  if (this.state === PhysicalConnectionState.CONNECTED) {
1357
- this._socket.send(data, this._remote.port, this._remote.address, cb);
1416
+ this._socket.send(data, this._remote.port, this._remote.address, (err) => {
1417
+ if (!err) {
1418
+ this.emit('tx', data);
1419
+ }
1420
+ cb?.(err);
1421
+ });
1358
1422
  }
1359
1423
  else {
1360
1424
  cb?.(new Error('Connection is not connected'));
@@ -1557,16 +1621,23 @@ class AbstractApplicationLayer {
1557
1621
  removeCustomFunctionCode(fc) { }
1558
1622
  }
1559
1623
 
1560
- const MAX_FRAME_LENGTH = 256;
1624
+ const MAX_FRAME_LENGTH$1 = 256;
1561
1625
  const MIN_FRAME_LENGTH = 4;
1562
1626
  class RtuApplicationLayer extends AbstractApplicationLayer {
1563
1627
  PROTOCOL = 'RTU';
1564
1628
  ROLE;
1565
1629
  _connection;
1566
- _state;
1567
- _poolSize;
1568
- _threePointFiveT;
1569
- _onePointFiveT;
1630
+ // Stores leftover data between parse rounds
1631
+ _residual = Buffer.alloc(MAX_FRAME_LENGTH$1);
1632
+ _residualLen = 0;
1633
+ _expectedLen = PREDICT_NEED_MORE;
1634
+ _t15Time;
1635
+ _t35Time;
1636
+ _t15Strict;
1637
+ _t15Timer;
1638
+ _t35Timer;
1639
+ // t1.5 cursor: 0 = not triggered; > 0 = virtual index where the gap occurred
1640
+ _t15Marker = 0;
1570
1641
  _customFunctionCodes = new Map();
1571
1642
  _cleanupCbs = [];
1572
1643
  get connection() {
@@ -1576,89 +1647,246 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1576
1647
  super();
1577
1648
  this.ROLE = role;
1578
1649
  this._connection = connection;
1579
- const { intervalBetweenFrames, interCharTimeout, poolSize } = options;
1580
- this._poolSize = poolSize ?? MAX_FRAME_LENGTH * 2;
1581
- this._state = { pool: Buffer.alloc(this._poolSize), start: 0, end: 0 };
1582
- this._threePointFiveT = intervalBetweenFrames ?? 0;
1583
- this._onePointFiveT = interCharTimeout ?? 0;
1650
+ const { intervalBetweenFrames, interCharTimeout, strictTiming } = options;
1651
+ this._t35Time = intervalBetweenFrames ?? 0;
1652
+ this._t15Time = this._t35Time === 0 ? 0 : (interCharTimeout ?? 0);
1653
+ if (this._t35Time < this._t15Time) {
1654
+ throw new Error('t3.5 cannot be less than t1.5');
1655
+ }
1656
+ this._t15Strict = strictTiming ?? false;
1657
+ const isResponse = role === 'MASTER';
1658
+ const timingEnabled = this._t35Time > 0;
1584
1659
  const onData = (data) => {
1585
- const state = this._state;
1586
- if (state.t15Expired && state.end > state.start) {
1587
- this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
1588
- state.start = 0;
1589
- state.end = 0;
1590
- }
1591
- state.t15Expired = false;
1592
1660
  const dataLen = data.length;
1593
- const space = state.pool.length - state.end;
1594
- if (dataLen <= space) {
1595
- // Fast path: incoming chunk fits the current pool tail; one bulk copy,
1596
- // no per-iteration bookkeeping. This is the typical case.
1597
- data.copy(state.pool, state.end);
1598
- state.end += dataLen;
1661
+ const residualLen = this._residualLen;
1662
+ const totalAvailable = residualLen + dataLen;
1663
+ // ========================================================================
1664
+ // 1. Timing reset: any bus activity unconditionally kills all silence timers
1665
+ // ========================================================================
1666
+ if (timingEnabled) {
1667
+ if (this._t15Timer !== undefined) {
1668
+ clearTimeout(this._t15Timer);
1669
+ this._t15Timer = undefined;
1670
+ }
1671
+ if (this._t35Timer !== undefined) {
1672
+ clearTimeout(this._t35Timer);
1673
+ this._t35Timer = undefined;
1674
+ }
1599
1675
  }
1600
- else {
1601
- // Slow path: incoming chunk does NOT fit the pool's remaining tail
1602
- // space (not necessarily larger than the whole pool — typically the
1603
- // pool already holds undrained residue from earlier ticks).
1604
- // Strategy drain & absorb in chunks:
1605
- // 1. Copy as much of `data` as fits the current tail.
1606
- // 2. When the pool fills mid-chunk, run flushBuffer(false) to emit
1607
- // any complete frames and collapse the leftover bytes to offset
1608
- // 0, which reclaims tail space.
1609
- // 3. Repeat until `data` is fully ingested.
1610
- // The mid-chunk flush is NEVER strict — t3.5 has not actually elapsed,
1611
- // the rest of `data` is still in hand. Only the t3.5 timer callback
1612
- // below passes strict=true.
1613
- let dataOffset = 0;
1614
- let currentState = state;
1615
- while (dataOffset < dataLen) {
1616
- const room = currentState.pool.length - currentState.end;
1617
- if (room === 0) {
1618
- // Pool full mid-ingest. Try to extract any complete frames and
1619
- // reclaim tail space so the rest of `data` has somewhere to go.
1620
- this.clearStateTimers();
1621
- this.flushBuffer(false);
1622
- // flushBuffer may emit; user callbacks could in principle replace
1623
- // _state. Re-read to stay correct across that boundary.
1624
- currentState = this._state;
1625
- if (currentState.pool.length - currentState.end === 0) {
1626
- // flushBuffer freed nothing — the entire pool is unparseable
1627
- // residue (typically a misconfigured poolSize for the wire's
1628
- // frame size). Hard reset; we cannot recover automatically.
1629
- this.onFramingError(new Error('Frame buffer exhausted before complete frame received'));
1630
- currentState.start = 0;
1631
- currentState.end = 0;
1632
- currentState.t15Expired = false;
1676
+ // ========================================================================
1677
+ // 2. Fast path: no residual data and the new chunk is exactly one frame
1678
+ // ========================================================================
1679
+ if (residualLen === 0 && dataLen >= MIN_FRAME_LENGTH) {
1680
+ const fc = data[1];
1681
+ let frameLen = PREDICT_NEED_MORE;
1682
+ const cfc = this._customFunctionCodes.size > 0 ? this._customFunctionCodes.get(fc) : undefined;
1683
+ if (cfc) {
1684
+ const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
1685
+ frameLen = predictor((idx) => data[idx], dataLen);
1686
+ }
1687
+ else if (isResponse) {
1688
+ if ((fc & 0x80) !== 0) {
1689
+ frameLen = 5;
1690
+ }
1691
+ else {
1692
+ const val = RES_TABLE[fc];
1693
+ if (val > 0) {
1694
+ frameLen = val;
1695
+ }
1696
+ else if (val < 0 && val !== -999) {
1697
+ const decode = -val;
1698
+ const offset = decode >> 8;
1699
+ if (dataLen > offset) {
1700
+ frameLen = (decode & 0xff) + data[offset];
1701
+ }
1633
1702
  }
1634
- continue;
1635
1703
  }
1636
- // Copy the next chunk into the freshly-available tail space.
1637
- const toCopy = room < dataLen - dataOffset ? room : dataLen - dataOffset;
1638
- data.copy(currentState.pool, currentState.end, dataOffset, dataOffset + toCopy);
1639
- currentState.end += toCopy;
1640
- dataOffset += toCopy;
1704
+ }
1705
+ else {
1706
+ const val = REQ_TABLE[fc];
1707
+ if (val > 0) {
1708
+ frameLen = val;
1709
+ }
1710
+ else if (val < 0) {
1711
+ const decode = -val;
1712
+ const offset = decode >> 8;
1713
+ if (dataLen > offset) {
1714
+ frameLen = (decode & 0xff) + data[offset];
1715
+ }
1716
+ }
1717
+ }
1718
+ if (frameLen === dataLen) {
1719
+ const expectedCrc = data[frameLen - 2] | (data[frameLen - 1] << 8);
1720
+ // Inline CRC for the hot single-buffer path — local table reference helps V8 IC.
1721
+ const table = CRC_TABLE;
1722
+ let crc = 0xffff;
1723
+ const crcEnd = frameLen - 2;
1724
+ for (let i = 0; i < crcEnd; i++) {
1725
+ crc = table[(crc ^ data[i]) & 0xff] ^ (crc >> 8);
1726
+ }
1727
+ if (expectedCrc === crc) {
1728
+ const dropFrame = timingEnabled && this._t15Strict && this._t15Marker > 0;
1729
+ if (!dropFrame) {
1730
+ const frame = {
1731
+ unit: data[0],
1732
+ fc: data[1],
1733
+ data: data.subarray(2, frameLen - 2),
1734
+ buffer: data,
1735
+ };
1736
+ this.onFraming(frame);
1737
+ }
1738
+ this._expectedLen = PREDICT_NEED_MORE;
1739
+ this._t15Marker = 0;
1740
+ return;
1741
+ }
1641
1742
  }
1642
1743
  }
1643
- this.clearStateTimers();
1644
- const liveState = this._state;
1645
- if (liveState.end - liveState.start >= MAX_FRAME_LENGTH) {
1646
- this.flushBuffer(this._threePointFiveT > 0);
1647
- }
1648
- else if (this._threePointFiveT) {
1649
- if (this._onePointFiveT > 0) {
1650
- liveState.interCharTimer = setTimeout(() => {
1651
- liveState.interCharTimer = undefined;
1652
- liveState.t15Expired = true;
1653
- }, this._onePointFiveT);
1744
+ // ========================================================================
1745
+ // 3. Extract frames in a loop until no complete frame remains (skip if data length below prediction)
1746
+ // ========================================================================
1747
+ let index = 0;
1748
+ if (!(this._expectedLen > 0 && totalAvailable < this._expectedLen)) {
1749
+ while (index <= totalAvailable - MIN_FRAME_LENGTH) {
1750
+ const fc = index + 1 < residualLen ? this._residual[index + 1] : data[index + 1 - residualLen];
1751
+ const cfc = this._customFunctionCodes.get(fc);
1752
+ let frameLen = PREDICT_NEED_MORE;
1753
+ if (cfc) {
1754
+ const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
1755
+ frameLen = predictor((idx) => {
1756
+ const pos = index + idx;
1757
+ return pos < residualLen ? this._residual[pos] : data[pos - residualLen];
1758
+ }, totalAvailable - index);
1759
+ }
1760
+ else {
1761
+ frameLen = predictRtuFrameLength(this._residual, data, residualLen, index, totalAvailable, isResponse);
1762
+ }
1763
+ if (frameLen === PREDICT_UNKNOWN) {
1764
+ index++;
1765
+ continue;
1766
+ }
1767
+ if (frameLen === PREDICT_NEED_MORE) {
1768
+ break;
1769
+ }
1770
+ if (frameLen > MAX_FRAME_LENGTH$1 || frameLen < MIN_FRAME_LENGTH) {
1771
+ index++;
1772
+ continue;
1773
+ }
1774
+ if (totalAvailable - index < frameLen) {
1775
+ this._expectedLen = index + frameLen;
1776
+ break;
1777
+ }
1778
+ const expectedCrc = (index + frameLen - 2 < residualLen ? this._residual[index + frameLen - 2] : data[index + frameLen - 2 - residualLen]) |
1779
+ ((index + frameLen - 1 < residualLen ? this._residual[index + frameLen - 1] : data[index + frameLen - 1 - residualLen]) << 8);
1780
+ const crcEnd = index + frameLen - 2;
1781
+ let actualCrc;
1782
+ if (crcEnd <= residualLen) {
1783
+ // Entire CRC range sits in the old residual buffer
1784
+ actualCrc = crcFixed(this._residual, index, crcEnd);
1785
+ }
1786
+ else if (index >= residualLen) {
1787
+ // Entire CRC range sits in the new data chunk
1788
+ actualCrc = crcFixed(data, index - residualLen, crcEnd - residualLen);
1789
+ }
1790
+ else {
1791
+ // CRC range spans both buffers
1792
+ actualCrc = crcDual(this._residual, index, residualLen - index, data, 0, crcEnd - residualLen);
1793
+ }
1794
+ if (expectedCrc !== actualCrc) {
1795
+ index++;
1796
+ continue;
1797
+ }
1798
+ // A complete frame has been received; CRC verification still required
1799
+ this._expectedLen = PREDICT_NEED_MORE;
1800
+ const dropFrame = this._t15Strict && this._t15Marker > index && this._t15Marker < index + frameLen;
1801
+ if (!dropFrame) {
1802
+ // Build contiguous raw buffer — one alloc, zero-copy subarray for data.
1803
+ const raw = Buffer.allocUnsafe(frameLen);
1804
+ if (index + frameLen <= residualLen) {
1805
+ this._residual.copy(raw, 0, index, index + frameLen);
1806
+ }
1807
+ else if (index >= residualLen) {
1808
+ data.copy(raw, 0, index - residualLen, index - residualLen + frameLen);
1809
+ }
1810
+ else {
1811
+ const headLen = residualLen - index;
1812
+ this._residual.copy(raw, 0, index, residualLen);
1813
+ data.copy(raw, headLen, 0, frameLen - headLen);
1814
+ }
1815
+ const frame = {
1816
+ unit: raw[0],
1817
+ fc: raw[1],
1818
+ data: raw.subarray(2, frameLen - 2),
1819
+ buffer: raw,
1820
+ };
1821
+ this.onFraming(frame);
1822
+ }
1823
+ index += frameLen;
1654
1824
  }
1655
- liveState.timer = setTimeout(() => {
1656
- this.clearStateTimers();
1657
- this.flushBuffer(true);
1658
- }, this._threePointFiveT);
1825
+ }
1826
+ // ========================================================================
1827
+ // 4. Compact residual buffer and rebuild silence timers
1828
+ // ========================================================================
1829
+ const newFrameStart = index;
1830
+ const finalRestLen = totalAvailable - newFrameStart;
1831
+ if (finalRestLen === 0) {
1832
+ this._residualLen = 0;
1833
+ this._expectedLen = PREDICT_NEED_MORE;
1834
+ this._t15Marker = 0;
1659
1835
  }
1660
1836
  else {
1661
- this.flushBuffer(false);
1837
+ const keepLen = finalRestLen < MAX_FRAME_LENGTH$1 ? finalRestLen : MAX_FRAME_LENGTH$1;
1838
+ const discardLen = totalAvailable - keepLen;
1839
+ if (discardLen >= residualLen) {
1840
+ // Kept portion lies entirely within the new `data`
1841
+ data.copy(this._residual, 0, discardLen - residualLen, dataLen);
1842
+ }
1843
+ else if (discardLen > 0) {
1844
+ // Kept portion spans both buffers, or physical left-shift truncation occurred
1845
+ for (let i = 0; i < keepLen; i++) {
1846
+ this._residual[i] = discardLen + i < residualLen ? this._residual[discardLen + i] : data[discardLen + i - residualLen];
1847
+ }
1848
+ }
1849
+ else {
1850
+ // discardLen === 0 (old data not consumed, truncation limit not hit) — simple append
1851
+ data.copy(this._residual, residualLen, 0, dataLen);
1852
+ }
1853
+ this._residualLen = keepLen;
1854
+ // Unify physical coordinate system translation
1855
+ if (discardLen > 0) {
1856
+ if (this._expectedLen > 0) {
1857
+ const newExpectedLen = this._expectedLen - discardLen;
1858
+ this._expectedLen = newExpectedLen > PREDICT_NEED_MORE ? newExpectedLen : PREDICT_NEED_MORE;
1859
+ }
1860
+ if (this._t15Marker > 0) {
1861
+ const newT15Marker = this._t15Marker - discardLen;
1862
+ this._t15Marker = newT15Marker > 0 ? newT15Marker : 0;
1863
+ }
1864
+ }
1865
+ if (timingEnabled) {
1866
+ let hasErrorEmitted = false;
1867
+ // Establish t3.5 absolute deadline
1868
+ this._t35Timer = setTimeout(() => {
1869
+ this._t35Timer = undefined;
1870
+ // No complete frame parsed within t3.5: circuit-break, discard all data
1871
+ this._residualLen = 0;
1872
+ this._expectedLen = PREDICT_NEED_MORE;
1873
+ this._t15Marker = 0;
1874
+ if (!hasErrorEmitted) {
1875
+ this.onFramingError(new Error('Incomplete frame at t3.5'));
1876
+ }
1877
+ }, this._t35Time);
1878
+ if (this._t15Time > 0) {
1879
+ // Establish t1.5 inter-character gap monitor
1880
+ this._t15Timer = setTimeout(() => {
1881
+ this._t15Timer = undefined;
1882
+ this._t15Marker = this._residualLen; // Record the residual boundary where the gap occurred
1883
+ if (this._t15Strict) {
1884
+ hasErrorEmitted = true;
1885
+ this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
1886
+ }
1887
+ }, this._t15Time);
1888
+ }
1889
+ }
1662
1890
  }
1663
1891
  };
1664
1892
  connection.on('data', onData);
@@ -1668,150 +1896,33 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1668
1896
  fn();
1669
1897
  }
1670
1898
  this._cleanupCbs.length = 0;
1671
- this.clearStateTimers();
1899
+ if (this._t15Timer !== undefined) {
1900
+ clearTimeout(this._t15Timer);
1901
+ this._t15Timer = undefined;
1902
+ }
1903
+ if (this._t35Timer !== undefined) {
1904
+ clearTimeout(this._t35Timer);
1905
+ this._t35Timer = undefined;
1906
+ }
1672
1907
  };
1673
1908
  connection.on('close', onClose);
1674
1909
  this._cleanupCbs.push(() => connection.off('close', onClose));
1675
1910
  }
1676
- clearStateTimers() {
1677
- const state = this._state;
1678
- if (state.timer) {
1679
- clearTimeout(state.timer);
1680
- state.timer = undefined;
1681
- }
1682
- if (state.interCharTimer) {
1683
- clearTimeout(state.interCharTimer);
1684
- state.interCharTimer = undefined;
1685
- }
1686
- }
1687
- /**
1688
- * Shared handler for every "frame is not yet complete" exit in `flushBuffer`.
1689
- * Returns `true` when the caller should `return` (strict reset), `false` to
1690
- * `break` the parse loop. Hot path never reaches here — only error/incomplete
1691
- * edges. Extracted as a method so it is not recreated on every `flushBuffer`
1692
- * call.
1693
- */
1694
- _handleIncomplete(state, strict) {
1695
- if (strict) {
1696
- this.onFramingError(new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
1697
- state.start = 0;
1698
- state.end = 0;
1699
- state.t15Expired = false;
1700
- return true;
1701
- }
1702
- if (state.t15Expired) {
1703
- this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
1704
- state.start = 0;
1705
- state.end = 0;
1706
- state.t15Expired = false;
1707
- }
1708
- return false;
1709
- }
1710
- flushBuffer(strict) {
1711
- const state = this._state;
1712
- const isResponse = this.ROLE === 'MASTER';
1713
- const pool = state.pool;
1714
- const customFCs = this._customFunctionCodes;
1715
- while (state.end - state.start > 0) {
1716
- const available = state.end - state.start;
1717
- if (available < MIN_FRAME_LENGTH) {
1718
- if (this._handleIncomplete(state, strict)) {
1719
- return;
1720
- }
1721
- break;
1722
- }
1723
- const fc = pool[state.start + 1];
1724
- const cfc = customFCs.get(fc);
1725
- let expected;
1726
- if (cfc) {
1727
- const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
1728
- const predicted = predictor(pool, state.start, state.end);
1729
- // Normalize custom predictor's `null` to the std sentinel so both
1730
- // paths share the same NEED_MORE tail below.
1731
- expected = predicted ?? PREDICT_NEED_MORE;
1732
- }
1733
- else {
1734
- // Standard FC path. predictRtuFrameLength uses sentinel returns to
1735
- // avoid per-call object allocation on the decode hot path.
1736
- expected = predictRtuFrameLength(pool, state.start, state.end, isResponse);
1737
- if (expected === PREDICT_UNKNOWN) {
1738
- this.onFramingError(new Error(`Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`));
1739
- state.start = 0;
1740
- state.end = 0;
1741
- return;
1742
- }
1743
- }
1744
- if (expected === PREDICT_NEED_MORE) {
1745
- if (available >= MAX_FRAME_LENGTH) {
1746
- state.start += 1;
1747
- continue;
1748
- }
1749
- if (this._handleIncomplete(state, strict)) {
1750
- return;
1751
- }
1752
- break;
1753
- }
1754
- if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
1755
- this.onFramingError(new Error('Invalid data'));
1756
- state.start = 0;
1757
- state.end = 0;
1758
- return;
1759
- }
1760
- if (available < expected) {
1761
- if (available >= MAX_FRAME_LENGTH) {
1762
- state.start += 1;
1763
- continue;
1764
- }
1765
- if (this._handleIncomplete(state, strict)) {
1766
- return;
1767
- }
1768
- break;
1769
- }
1770
- // CRC check inline: no helper call, no subarray for the CRC body.
1771
- const crcStart = state.start;
1772
- const crcEnd = crcStart + expected - 2;
1773
- const expectedCrc = pool.readUInt16LE(crcEnd);
1774
- const actualCrc = crc(pool, crcStart, crcEnd);
1775
- if (expectedCrc !== actualCrc) {
1776
- if (strict) {
1777
- this.onFramingError(new Error('CRC mismatch'));
1778
- state.start = 0;
1779
- state.end = 0;
1780
- state.t15Expired = false;
1781
- return;
1782
- }
1783
- state.start += 1;
1784
- continue;
1785
- }
1786
- // Frame located. Copy it out of the pool so the emitted buffer remains
1787
- // valid even if the consumer queues the frame across `onData` ticks.
1788
- // `Buffer.copyBytesFrom` is a native fast-path (Node 18.19+) — measurably
1789
- // faster than `Buffer.from(buffer)` for this size.
1790
- const frameBuf = Buffer.copyBytesFrom(pool, crcStart, expected);
1791
- state.start += expected;
1792
- const frame = {
1793
- unit: frameBuf[0],
1794
- fc: frameBuf[1],
1795
- data: frameBuf.subarray(2, expected - 2),
1796
- buffer: frameBuf,
1797
- };
1798
- this.onFraming(frame);
1911
+ flush() {
1912
+ this._residualLen = 0;
1913
+ this._expectedLen = PREDICT_NEED_MORE;
1914
+ this._t15Marker = 0;
1915
+ if (this._t15Timer !== undefined) {
1916
+ clearTimeout(this._t15Timer);
1917
+ this._t15Timer = undefined;
1799
1918
  }
1800
- if (state.start > 0) {
1801
- if (state.start < state.end) {
1802
- state.pool.copy(state.pool, 0, state.start, state.end);
1803
- }
1804
- state.end -= state.start;
1805
- state.start = 0;
1919
+ if (this._t35Timer !== undefined) {
1920
+ clearTimeout(this._t35Timer);
1921
+ this._t35Timer = undefined;
1806
1922
  }
1807
1923
  }
1808
- flush() {
1809
- this.clearStateTimers();
1810
- this._state.start = 0;
1811
- this._state.end = 0;
1812
- }
1813
1924
  addCustomFunctionCode(cfc) {
1814
- if (!isUint8(cfc.fc)) {
1925
+ if ((cfc.fc & 0xff) !== cfc.fc) {
1815
1926
  throw new Error(`fc must be an integer in 0..255, got ${cfc.fc}`);
1816
1927
  }
1817
1928
  this._customFunctionCodes.set(cfc.fc, cfc);
@@ -1834,7 +1945,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1834
1945
  buffer.set(data, 2);
1835
1946
  }
1836
1947
  const crcEnd = buffer.length - 2;
1837
- const c = crc(buffer, 0, crcEnd);
1948
+ const c = crcFixed(buffer, 0, crcEnd);
1838
1949
  // Little-endian inline write of CRC trailer.
1839
1950
  buffer[crcEnd] = c & 0xff;
1840
1951
  buffer[crcEnd + 1] = (c >>> 8) & 0xff;
@@ -1861,11 +1972,16 @@ for (let i = 0x41; i <= 0x46; i++) {
1861
1972
  for (let i = 0x61; i <= 0x66; i++) {
1862
1973
  HEX_DECODE[i] = i - 0x61 + 10;
1863
1974
  }
1975
+ // Strict variant: lowercase hex digits (a-f) are treated as invalid so the
1976
+ // hot-path validation loop needs only one table lookup instead of two checks.
1977
+ const HEX_DECODE_STRICT = new Uint8Array(HEX_DECODE);
1978
+ for (let i = 0x61; i <= 0x66; i++) {
1979
+ HEX_DECODE_STRICT[i] = 0xff;
1980
+ }
1864
1981
  const HEX_ENCODE = new Uint8Array('0123456789ABCDEF'.split('').map((c) => c.charCodeAt(0)));
1865
1982
  class AsciiApplicationLayer extends AbstractApplicationLayer {
1866
1983
  PROTOCOL = 'ASCII';
1867
1984
  ROLE;
1868
- lenientHex;
1869
1985
  _connection;
1870
1986
  _state = { status: 'idle', frame: new Uint8Array(MAX_ASCII_PAYLOAD), frameLen: 0 };
1871
1987
  _cleanupCbs = [];
@@ -1876,23 +1992,30 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1876
1992
  super();
1877
1993
  this.ROLE = role;
1878
1994
  this._connection = connection;
1879
- this.lenientHex = options.lenientHex ?? false;
1880
- const lenientHex = this.lenientHex;
1881
- const isHexChar = (value) => {
1882
- if (value >= 0x30 && value <= 0x39) {
1883
- return true;
1884
- }
1885
- if (value >= 0x41 && value <= 0x46) {
1886
- return true;
1887
- }
1888
- if (lenientHex && value >= 0x61 && value <= 0x66) {
1889
- return true;
1890
- }
1891
- return false;
1892
- };
1995
+ const lenientHex = options.lenientHex ?? false;
1996
+ const hexTable = lenientHex ? HEX_DECODE : HEX_DECODE_STRICT;
1893
1997
  const onData = (data) => {
1894
1998
  const state = this._state;
1895
- for (let i = 0; i < data.length; i++) {
1999
+ const dataLen = data.length;
2000
+ // Fast path: idle state and the chunk is exactly one complete frame
2001
+ // ASCII frame: length >= 9 and always odd (9 + 2n)
2002
+ if (state.status === 'idle' && dataLen >= 9 && dataLen % 2 !== 0) {
2003
+ if (data[0] === CHAR_CODE.COLON && data[dataLen - 2] === CHAR_CODE.CR && data[dataLen - 1] === CHAR_CODE.LF) {
2004
+ let valid = true;
2005
+ for (let i = 1; i < dataLen - 2; i++) {
2006
+ if (hexTable[data[i]] > 15) {
2007
+ valid = false;
2008
+ break;
2009
+ }
2010
+ }
2011
+ if (valid) {
2012
+ const hexView = data.subarray(1, dataLen - 2);
2013
+ this.framing(hexView, hexView.length, hexTable);
2014
+ return;
2015
+ }
2016
+ }
2017
+ }
2018
+ for (let i = 0; i < dataLen; i++) {
1896
2019
  const value = data[i];
1897
2020
  switch (state.status) {
1898
2021
  case 'idle': {
@@ -1914,7 +2037,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1914
2037
  state.frameLen = 0;
1915
2038
  this.onFramingError(new Error('Invalid data'));
1916
2039
  }
1917
- else if (!isHexChar(value)) {
2040
+ else if (hexTable[value] > 15) {
1918
2041
  state.status = 'idle';
1919
2042
  state.frameLen = 0;
1920
2043
  this.onFramingError(new Error('Invalid hex character'));
@@ -1932,7 +2055,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1932
2055
  else {
1933
2056
  state.status = 'idle';
1934
2057
  if (value === CHAR_CODE.LF) {
1935
- this.framing(state.frame, state.frameLen);
2058
+ this.framing(state.frame, state.frameLen, hexTable);
1936
2059
  }
1937
2060
  }
1938
2061
  break;
@@ -1951,7 +2074,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1951
2074
  connection.on('close', onClose);
1952
2075
  this._cleanupCbs.push(() => connection.off('close', onClose));
1953
2076
  }
1954
- framing(hexChars, hexLen) {
2077
+ framing(hexChars, hexLen, hexTable) {
1955
2078
  if (hexLen < 6) {
1956
2079
  this.onFramingError(new Error('Insufficient data length'));
1957
2080
  return;
@@ -1963,10 +2086,10 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1963
2086
  const byteLen = hexLen >> 1;
1964
2087
  // Decode unit and fc directly from the first 4 hex characters —
1965
2088
  // avoids allocating a full decoded buffer just to read two bytes.
1966
- const unitHi = HEX_DECODE[hexChars[0]];
1967
- const unitLo = HEX_DECODE[hexChars[1]];
1968
- const fcHi = HEX_DECODE[hexChars[2]];
1969
- const fcLo = HEX_DECODE[hexChars[3]];
2089
+ const unitHi = hexTable[hexChars[0]];
2090
+ const unitLo = hexTable[hexChars[1]];
2091
+ const fcHi = hexTable[hexChars[2]];
2092
+ const fcLo = hexTable[hexChars[3]];
1970
2093
  if (unitHi === 0xff || unitLo === 0xff || fcHi === 0xff || fcLo === 0xff) {
1971
2094
  this.onFramingError(new Error('Invalid hex character'));
1972
2095
  return;
@@ -1974,33 +2097,32 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1974
2097
  const unit = (unitHi << 4) | unitLo;
1975
2098
  const fc = (fcHi << 4) | fcLo;
1976
2099
  // Decode LRC from the last 2 hex characters.
1977
- const lrcHi = HEX_DECODE[hexChars[hexLen - 2]];
1978
- const lrcLo = HEX_DECODE[hexChars[hexLen - 1]];
2100
+ const lrcHi = hexTable[hexChars[hexLen - 2]];
2101
+ const lrcLo = hexTable[hexChars[hexLen - 1]];
1979
2102
  if (lrcHi === 0xff || lrcLo === 0xff) {
1980
2103
  this.onFramingError(new Error('Invalid hex character'));
1981
2104
  return;
1982
2105
  }
1983
2106
  const lrcIn = (lrcHi << 4) | lrcLo;
1984
- // Decode data portion (between unit/fc and lrc) into a right-sized buffer.
2107
+ // Decode data portion (between unit/fc and lrc) into a right-sized buffer,
2108
+ // while simultaneously accumulating the LRC sum — one pass instead of two.
1985
2109
  // dataLen may be 0 for a frame that is only unit + fc + lrc.
1986
2110
  const dataLen = byteLen - 3;
1987
2111
  const data = Buffer.allocUnsafe(dataLen);
1988
2112
  let hexOff = 4;
2113
+ let sum = unit + fc;
1989
2114
  for (let i = 0; i < dataLen; i++) {
1990
- const hi = HEX_DECODE[hexChars[hexOff]];
1991
- const lo = HEX_DECODE[hexChars[hexOff + 1]];
2115
+ const hi = hexTable[hexChars[hexOff]];
2116
+ const lo = hexTable[hexChars[hexOff + 1]];
1992
2117
  if (hi === 0xff || lo === 0xff) {
1993
2118
  this.onFramingError(new Error('Invalid hex character'));
1994
2119
  return;
1995
2120
  }
1996
- data[i] = (hi << 4) | lo;
2121
+ const byte = (hi << 4) | lo;
2122
+ data[i] = byte;
2123
+ sum += byte;
1997
2124
  hexOff += 2;
1998
2125
  }
1999
- // Compute LRC over unit + fc + data.
2000
- let sum = unit + fc;
2001
- for (let i = 0; i < dataLen; i++) {
2002
- sum += data[i];
2003
- }
2004
2126
  const lrcComputed = (~sum + 1) & 0xff;
2005
2127
  if (lrcIn !== lrcComputed) {
2006
2128
  this.onFramingError(new Error('LRC check failed'));
@@ -2041,13 +2163,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
2041
2163
  }
2042
2164
  }
2043
2165
 
2044
- const MAX_TCP_FRAME = 260;
2166
+ const MAX_FRAME_LENGTH = 260;
2045
2167
  class TcpApplicationLayer extends AbstractApplicationLayer {
2046
2168
  PROTOCOL = 'TCP';
2047
2169
  ROLE;
2048
2170
  _connection;
2049
2171
  _transactionId = 1;
2050
- _buffer = EMPTY_BUFFER;
2172
+ _residual = Buffer.alloc(MAX_FRAME_LENGTH);
2173
+ _residualLen = 0;
2174
+ _expectedLen = PREDICT_NEED_MORE;
2051
2175
  _cleanupCbs = [];
2052
2176
  get connection() {
2053
2177
  return this._connection;
@@ -2057,51 +2181,108 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2057
2181
  this.ROLE = role;
2058
2182
  this._connection = connection;
2059
2183
  const onData = (data) => {
2060
- // Fast path: _buffer is empty and data is a single, complete frame.
2061
- // This avoids the tryExtract subarray allocations + while loop
2062
- // for the overwhelmingly common case (one frame per TCP packet).
2063
- if (this._buffer.length === 0 && data.length >= 8) {
2064
- const length = (data[4] << 8) | data[5]; // inline BE read
2065
- const total = 6 + length;
2066
- if (data[2] === 0 && data[3] === 0 && total <= MAX_TCP_FRAME && length >= 2 && data.length === total) {
2067
- this.processFrame(data);
2068
- return;
2184
+ const dataLen = data.length;
2185
+ const residualLen = this._residualLen;
2186
+ const totalAvailable = residualLen + dataLen;
2187
+ // Fast path: no residual data and the new chunk is exactly one frame
2188
+ if (residualLen === 0 && dataLen >= 8) {
2189
+ if (data[2] === 0 && data[3] === 0) {
2190
+ const length = (data[4] << 8) | data[5];
2191
+ const frameLen = 6 + length;
2192
+ if (frameLen === dataLen && frameLen <= MAX_FRAME_LENGTH && length >= 2) {
2193
+ const frame = {
2194
+ transaction: (data[0] << 8) | data[1],
2195
+ unit: data[6],
2196
+ fc: data[7],
2197
+ data: data.subarray(8),
2198
+ buffer: data,
2199
+ };
2200
+ this.onFraming(frame);
2201
+ this._expectedLen = PREDICT_NEED_MORE;
2202
+ return;
2203
+ }
2069
2204
  }
2070
2205
  }
2071
- let buffer = this._buffer;
2072
- if (buffer.length === 0) {
2073
- buffer = data;
2206
+ let index = 0;
2207
+ if (!(this._expectedLen > 0 && totalAvailable < this._expectedLen)) {
2208
+ while (index <= totalAvailable - 6) {
2209
+ // Validate MBAP protocol ID (bytes 2-3 must be 0x0000)
2210
+ if ((index + 2 < residualLen ? this._residual[index + 2] : data[index + 2 - residualLen]) !== 0 ||
2211
+ (index + 3 < residualLen ? this._residual[index + 3] : data[index + 3 - residualLen]) !== 0) {
2212
+ this.onFramingError(new Error('Invalid data'));
2213
+ this._residualLen = 0;
2214
+ this._expectedLen = PREDICT_NEED_MORE;
2215
+ return;
2216
+ }
2217
+ const length = ((index + 4 < residualLen ? this._residual[index + 4] : data[index + 4 - residualLen]) << 8) |
2218
+ (index + 5 < residualLen ? this._residual[index + 5] : data[index + 5 - residualLen]);
2219
+ const frameLen = 6 + length;
2220
+ if (frameLen > MAX_FRAME_LENGTH || length < 2) {
2221
+ this.onFramingError(new Error('Invalid data'));
2222
+ this._residualLen = 0;
2223
+ this._expectedLen = PREDICT_NEED_MORE;
2224
+ return;
2225
+ }
2226
+ if (totalAvailable - index < frameLen) {
2227
+ this._expectedLen = index + frameLen;
2228
+ break;
2229
+ }
2230
+ // A complete frame has been received
2231
+ this._expectedLen = PREDICT_NEED_MORE;
2232
+ const raw = Buffer.allocUnsafe(frameLen);
2233
+ if (index + frameLen <= residualLen) {
2234
+ this._residual.copy(raw, 0, index, index + frameLen);
2235
+ }
2236
+ else if (index >= residualLen) {
2237
+ data.copy(raw, 0, index - residualLen, index - residualLen + frameLen);
2238
+ }
2239
+ else {
2240
+ const headLen = residualLen - index;
2241
+ this._residual.copy(raw, 0, index, residualLen);
2242
+ data.copy(raw, headLen, 0, frameLen - headLen);
2243
+ }
2244
+ const frame = {
2245
+ transaction: (raw[0] << 8) | raw[1],
2246
+ unit: raw[6],
2247
+ fc: raw[7],
2248
+ data: raw.subarray(8),
2249
+ buffer: raw,
2250
+ };
2251
+ this.onFraming(frame);
2252
+ index += frameLen;
2253
+ }
2074
2254
  }
2075
- else if (data.length > 0) {
2076
- buffer = Buffer.concat([buffer, data]);
2255
+ const newFrameStart = index;
2256
+ const finalRestLen = totalAvailable - newFrameStart;
2257
+ if (finalRestLen === 0) {
2258
+ this._residualLen = 0;
2259
+ this._expectedLen = PREDICT_NEED_MORE;
2077
2260
  }
2078
- while (buffer.length > 0) {
2079
- const result = this.tryExtract(buffer);
2080
- if (result.kind === 'frame') {
2081
- this.processFrame(result.frame);
2082
- buffer = result.rest;
2261
+ else {
2262
+ const keepLen = finalRestLen < MAX_FRAME_LENGTH ? finalRestLen : MAX_FRAME_LENGTH;
2263
+ const discardLen = totalAvailable - keepLen;
2264
+ if (discardLen >= residualLen) {
2265
+ // Kept portion lies entirely within the new `data`
2266
+ data.copy(this._residual, 0, discardLen - residualLen, dataLen);
2083
2267
  }
2084
- else if (result.kind === 'insufficient') {
2085
- break;
2268
+ else if (discardLen > 0) {
2269
+ // Kept portion spans both buffers, or physical left-shift truncation occurred
2270
+ for (let i = 0; i < keepLen; i++) {
2271
+ this._residual[i] = discardLen + i < residualLen ? this._residual[discardLen + i] : data[discardLen + i - residualLen];
2272
+ }
2086
2273
  }
2087
2274
  else {
2088
- this.onFramingError(result.error);
2089
- buffer = EMPTY_BUFFER;
2090
- break;
2275
+ // discardLen === 0 (old data not consumed, truncation limit not hit) — simple append
2276
+ data.copy(this._residual, residualLen, 0, dataLen);
2277
+ }
2278
+ this._residualLen = keepLen;
2279
+ // Unify physical coordinate system translation
2280
+ if (discardLen > 0) {
2281
+ if (this._expectedLen > 0) {
2282
+ const newExpectedLen = this._expectedLen - discardLen;
2283
+ this._expectedLen = newExpectedLen > PREDICT_NEED_MORE ? newExpectedLen : PREDICT_NEED_MORE;
2284
+ }
2091
2285
  }
2092
- }
2093
- if (buffer.length === 0) {
2094
- this._buffer = EMPTY_BUFFER;
2095
- }
2096
- else if (buffer === data) {
2097
- // Copy into a right-sized buffer so we do not retain the potentially
2098
- // large backing buffer of the original incoming data (Node.js pool).
2099
- // `Buffer.copyBytesFrom` is a native fast-path — one C++ memcpy
2100
- // vs allocUnsafe + JS-level copy.
2101
- this._buffer = Buffer.copyBytesFrom(buffer);
2102
- }
2103
- else {
2104
- this._buffer = buffer;
2105
2286
  }
2106
2287
  };
2107
2288
  connection.on('data', onData);
@@ -2115,38 +2296,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2115
2296
  connection.on('close', onClose);
2116
2297
  this._cleanupCbs.push(() => connection.off('close', onClose));
2117
2298
  }
2118
- tryExtract(buffer) {
2119
- if (buffer.length < 8) {
2120
- return { kind: 'insufficient' };
2121
- }
2122
- if (buffer[2] !== 0 || buffer[3] !== 0) {
2123
- return { kind: 'error', error: new Error('Invalid data') };
2124
- }
2125
- const length = (buffer[4] << 8) | buffer[5]; // inline BE read
2126
- const total = 6 + length;
2127
- if (total > MAX_TCP_FRAME || length < 2) {
2128
- return { kind: 'error', error: new Error('Invalid data') };
2129
- }
2130
- if (buffer.length < total) {
2131
- return { kind: 'insufficient' };
2132
- }
2133
- return { kind: 'frame', frame: buffer.subarray(0, total), rest: total === buffer.length ? EMPTY_BUFFER : buffer.subarray(total) };
2134
- }
2135
- processFrame(buffer) {
2136
- const frame = {
2137
- // Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
2138
- // argument coercion + bounds check. Symmetric to the header writes in
2139
- // encode() below. Hits on every received TCP frame.
2140
- transaction: (buffer[0] << 8) | buffer[1],
2141
- unit: buffer[6],
2142
- fc: buffer[7],
2143
- data: buffer.subarray(8),
2144
- buffer,
2145
- };
2146
- this.onFraming(frame);
2147
- }
2148
2299
  flush() {
2149
- this._buffer = EMPTY_BUFFER;
2300
+ this._residualLen = 0;
2301
+ this._expectedLen = PREDICT_NEED_MORE;
2150
2302
  }
2151
2303
  encode(unit, fc, data, transaction) {
2152
2304
  const buffer = Buffer.allocUnsafe(data.length + 8);
@@ -2313,6 +2465,7 @@ class ModbusMaster extends EventEmitter {
2313
2465
  appLayer.onFraming = NOOP;
2314
2466
  };
2315
2467
  const onFraming = (frame) => {
2468
+ this.emit('framing', frame, connection);
2316
2469
  this._masterSession.handleFrame(frame);
2317
2470
  };
2318
2471
  appLayer.onFraming = onFraming;
@@ -2321,17 +2474,34 @@ class ModbusMaster extends EventEmitter {
2321
2474
  appLayer.onFramingError = NOOP;
2322
2475
  };
2323
2476
  const onFramingError = (error) => {
2477
+ this.emit('framingError', error, connection);
2324
2478
  this._masterSession.handleError(error);
2325
2479
  };
2326
2480
  appLayer.onFramingError = onFramingError;
2327
2481
  this._cleanupFns.add(cleanupFramingError);
2482
+ const cleanupTx = () => connection.off('tx', onTx);
2483
+ const onTx = (buffer) => {
2484
+ this.emit('tx', buffer, connection);
2485
+ };
2486
+ connection.on('tx', onTx);
2487
+ this._cleanupFns.add(cleanupTx);
2488
+ const cleanupRx = () => connection.off('rx', onRx);
2489
+ const onRx = (buffer) => {
2490
+ this.emit('rx', buffer, connection);
2491
+ };
2492
+ connection.on('rx', onRx);
2493
+ this._cleanupFns.add(cleanupRx);
2328
2494
  const cleanupClose = () => connection.off('close', onClose);
2329
2495
  const onClose = () => {
2330
2496
  cleanupFraming();
2331
2497
  cleanupFramingError();
2498
+ cleanupTx();
2499
+ cleanupRx();
2332
2500
  cleanupClose();
2333
2501
  this._cleanupFns.delete(cleanupFraming);
2334
2502
  this._cleanupFns.delete(cleanupFramingError);
2503
+ this._cleanupFns.delete(cleanupTx);
2504
+ this._cleanupFns.delete(cleanupRx);
2335
2505
  this._cleanupFns.delete(cleanupClose);
2336
2506
  };
2337
2507
  connection.on('close', onClose);
@@ -2371,7 +2541,7 @@ class ModbusMaster extends EventEmitter {
2371
2541
  return new RtuApplicationLayer('MASTER', connection, {
2372
2542
  intervalBetweenFrames,
2373
2543
  interCharTimeout,
2374
- poolSize: this._protocol.opts?.poolSize,
2544
+ strictTiming: this._protocol.opts?.strictTiming,
2375
2545
  });
2376
2546
  }
2377
2547
  if (this._protocol.type === 'TCP') {
@@ -2547,10 +2717,8 @@ class ModbusMaster extends EventEmitter {
2547
2717
  });
2548
2718
  }
2549
2719
  writeFC1Or2(unit, fc, address, length, timeout) {
2550
- const byteCount = Math.ceil(length / 8);
2720
+ const byteCount = (length + 7) >> 3;
2551
2721
  const bufferTx = Buffer.allocUnsafe(4);
2552
- // Inline big-endian writes — direct typed-array stores skip the argument
2553
- // validation + bounds checks that `writeUInt16BE` runs on each call.
2554
2722
  bufferTx[0] = (address >>> 8) & 0xff;
2555
2723
  bufferTx[1] = address & 0xff;
2556
2724
  bufferTx[2] = (length >>> 8) & 0xff;
@@ -2567,46 +2735,44 @@ class ModbusMaster extends EventEmitter {
2567
2735
  }
2568
2736
  try {
2569
2737
  validateByteCountResponse(frame, unit, fc, byteCount);
2570
- const data = new Array(length);
2738
+ const data = new Uint8Array(length);
2571
2739
  let byteIdx = 1;
2572
2740
  let outIdx = 0;
2573
2741
  const fullBytes = length >> 3;
2574
2742
  for (let b = 0; b < fullBytes; b++) {
2575
2743
  const byte = frame.data[byteIdx++];
2576
- data[outIdx++] = (byte & 0x01) > 0;
2577
- data[outIdx++] = (byte & 0x02) > 0;
2578
- data[outIdx++] = (byte & 0x04) > 0;
2579
- data[outIdx++] = (byte & 0x08) > 0;
2580
- data[outIdx++] = (byte & 0x10) > 0;
2581
- data[outIdx++] = (byte & 0x20) > 0;
2582
- data[outIdx++] = (byte & 0x40) > 0;
2583
- data[outIdx++] = (byte & 0x80) > 0;
2744
+ data[outIdx++] = byte & 0x01;
2745
+ data[outIdx++] = (byte >>> 1) & 0x01;
2746
+ data[outIdx++] = (byte >>> 2) & 0x01;
2747
+ data[outIdx++] = (byte >>> 3) & 0x01;
2748
+ data[outIdx++] = (byte >>> 4) & 0x01;
2749
+ data[outIdx++] = (byte >>> 5) & 0x01;
2750
+ data[outIdx++] = (byte >>> 6) & 0x01;
2751
+ data[outIdx++] = (byte >>> 7) & 0x01;
2584
2752
  }
2585
2753
  const rem = length & 7;
2586
2754
  if (rem) {
2587
2755
  const byte = frame.data[byteIdx];
2588
- data[outIdx++] = (byte & 0x01) > 0;
2756
+ data[outIdx++] = byte & 0x01;
2589
2757
  if (rem > 1) {
2590
- data[outIdx++] = (byte & 0x02) > 0;
2758
+ data[outIdx++] = (byte >>> 1) & 0x01;
2591
2759
  }
2592
2760
  if (rem > 2) {
2593
- data[outIdx++] = (byte & 0x04) > 0;
2761
+ data[outIdx++] = (byte >>> 2) & 0x01;
2594
2762
  }
2595
2763
  if (rem > 3) {
2596
- data[outIdx++] = (byte & 0x08) > 0;
2764
+ data[outIdx++] = (byte >>> 3) & 0x01;
2597
2765
  }
2598
2766
  if (rem > 4) {
2599
- data[outIdx++] = (byte & 0x10) > 0;
2767
+ data[outIdx++] = (byte >>> 4) & 0x01;
2600
2768
  }
2601
2769
  if (rem > 5) {
2602
- data[outIdx++] = (byte & 0x20) > 0;
2770
+ data[outIdx++] = (byte >>> 5) & 0x01;
2603
2771
  }
2604
2772
  if (rem > 6) {
2605
- data[outIdx++] = (byte & 0x40) > 0;
2773
+ data[outIdx++] = (byte >>> 6) & 0x01;
2606
2774
  }
2607
2775
  }
2608
- // Mutate the frame in place rather than spread-copying — `frame` is freshly
2609
- // allocated per request and not retained anywhere else.
2610
2776
  frame.data = data;
2611
2777
  resolve(frame);
2612
2778
  }
@@ -2677,7 +2843,7 @@ class ModbusMaster extends EventEmitter {
2677
2843
  writeSingleCoil(unit, address, value, timeout = this.timeout) {
2678
2844
  const fc = FunctionCode.WRITE_SINGLE_COIL;
2679
2845
  const bufferTx = Buffer.allocUnsafe(4);
2680
- const coilValue = value ? COIL_ON : COIL_OFF;
2846
+ const coilValue = value === 1 ? COIL_ON : COIL_OFF;
2681
2847
  // Inline big-endian writes — see writeFC1Or2 for the rationale.
2682
2848
  bufferTx[0] = (address >>> 8) & 0xff;
2683
2849
  bufferTx[1] = address & 0xff;
@@ -2737,26 +2903,51 @@ class ModbusMaster extends EventEmitter {
2737
2903
  writeFC15;
2738
2904
  writeMultipleCoils(unit, address, value, timeout = this.timeout) {
2739
2905
  const fc = FunctionCode.WRITE_MULTIPLE_COILS;
2740
- const byteCount = Math.ceil(value.length / 8);
2741
- const bufferTx = Buffer.alloc(5 + byteCount);
2906
+ const len = value.length;
2907
+ const byteCount = (len + 7) >> 3;
2908
+ const bufferTx = Buffer.allocUnsafe(5 + byteCount);
2742
2909
  // Inline big-endian writes — see writeFC1Or2 for the rationale.
2743
2910
  bufferTx[0] = (address >>> 8) & 0xff;
2744
2911
  bufferTx[1] = address & 0xff;
2745
- bufferTx[2] = (value.length >>> 8) & 0xff;
2746
- bufferTx[3] = value.length & 0xff;
2912
+ bufferTx[2] = (len >>> 8) & 0xff;
2913
+ bufferTx[3] = len & 0xff;
2747
2914
  bufferTx[4] = byteCount;
2748
- let acc = 0;
2749
2915
  let out = 5;
2750
- for (let i = 0; i < value.length; i++) {
2751
- if (value[i]) {
2752
- acc |= 1 << (i & 7);
2916
+ const fullBytes = len >> 3;
2917
+ for (let b = 0; b < fullBytes; b++) {
2918
+ const base = b << 3;
2919
+ bufferTx[out++] =
2920
+ (value[base] & 1) |
2921
+ ((value[base + 1] & 1) << 1) |
2922
+ ((value[base + 2] & 1) << 2) |
2923
+ ((value[base + 3] & 1) << 3) |
2924
+ ((value[base + 4] & 1) << 4) |
2925
+ ((value[base + 5] & 1) << 5) |
2926
+ ((value[base + 6] & 1) << 6) |
2927
+ ((value[base + 7] & 1) << 7);
2928
+ }
2929
+ const rem = len & 7;
2930
+ if (rem) {
2931
+ const base = fullBytes << 3;
2932
+ let acc = value[base] & 1;
2933
+ if (rem > 1) {
2934
+ acc |= (value[base + 1] & 1) << 1;
2935
+ }
2936
+ if (rem > 2) {
2937
+ acc |= (value[base + 2] & 1) << 2;
2753
2938
  }
2754
- if ((i & 7) === 7) {
2755
- bufferTx[out++] = acc;
2756
- acc = 0;
2939
+ if (rem > 3) {
2940
+ acc |= (value[base + 3] & 1) << 3;
2941
+ }
2942
+ if (rem > 4) {
2943
+ acc |= (value[base + 4] & 1) << 4;
2944
+ }
2945
+ if (rem > 5) {
2946
+ acc |= (value[base + 5] & 1) << 5;
2947
+ }
2948
+ if (rem > 6) {
2949
+ acc |= (value[base + 6] & 1) << 6;
2757
2950
  }
2758
- }
2759
- if ((value.length & 7) !== 0) {
2760
2951
  bufferTx[out] = acc;
2761
2952
  }
2762
2953
  return new Promise((resolve, reject) => {
@@ -2844,7 +3035,7 @@ class ModbusMaster extends EventEmitter {
2844
3035
  frame.data = {
2845
3036
  serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
2846
3037
  runIndicatorStatus: frame.data[runStatusIndex] === 0xff,
2847
- additionalData: Array.from(frame.data.subarray(runStatusIndex + 1)),
3038
+ additionalData: frame.data.subarray(runStatusIndex + 1),
2848
3039
  };
2849
3040
  resolve(frame);
2850
3041
  }
@@ -3150,6 +3341,7 @@ class ModbusSlave extends EventEmitter {
3150
3341
  appLayer.onFraming = NOOP;
3151
3342
  };
3152
3343
  const onFraming = (frame) => {
3344
+ this.emit('framing', frame, connection);
3153
3345
  if (this._physicalLayer.state !== PhysicalState.OPEN) {
3154
3346
  return;
3155
3347
  }
@@ -3165,11 +3357,37 @@ class ModbusSlave extends EventEmitter {
3165
3357
  };
3166
3358
  appLayer.onFraming = onFraming;
3167
3359
  this._cleanupFns.add(cleanupFraming);
3360
+ const cleanupFramingError = () => {
3361
+ appLayer.onFramingError = NOOP;
3362
+ };
3363
+ const onFramingError = (error) => {
3364
+ this.emit('framingError', error, connection);
3365
+ };
3366
+ appLayer.onFramingError = onFramingError;
3367
+ this._cleanupFns.add(cleanupFramingError);
3368
+ const cleanupTx = () => connection.off('tx', onTx);
3369
+ const onTx = (buffer) => {
3370
+ this.emit('tx', buffer, connection);
3371
+ };
3372
+ connection.on('tx', onTx);
3373
+ this._cleanupFns.add(cleanupTx);
3374
+ const cleanupRx = () => connection.off('rx', onRx);
3375
+ const onRx = (buffer) => {
3376
+ this.emit('rx', buffer, connection);
3377
+ };
3378
+ connection.on('rx', onRx);
3379
+ this._cleanupFns.add(cleanupRx);
3168
3380
  const cleanupClose = () => connection.off('close', onClose);
3169
3381
  const onClose = () => {
3170
3382
  cleanupFraming();
3383
+ cleanupFramingError();
3384
+ cleanupTx();
3385
+ cleanupRx();
3171
3386
  cleanupClose();
3172
3387
  this._cleanupFns.delete(cleanupFraming);
3388
+ this._cleanupFns.delete(cleanupFramingError);
3389
+ this._cleanupFns.delete(cleanupTx);
3390
+ this._cleanupFns.delete(cleanupRx);
3173
3391
  this._cleanupFns.delete(cleanupClose);
3174
3392
  this._appLayers.delete(appLayer);
3175
3393
  };
@@ -3198,7 +3416,7 @@ class ModbusSlave extends EventEmitter {
3198
3416
  return new RtuApplicationLayer('SLAVE', connection, {
3199
3417
  intervalBetweenFrames,
3200
3418
  interCharTimeout,
3201
- poolSize: this._protocol.opts?.poolSize,
3419
+ strictTiming: this._protocol.opts?.strictTiming,
3202
3420
  });
3203
3421
  }
3204
3422
  if (this._protocol.type === 'TCP') {
@@ -3230,67 +3448,43 @@ class ModbusSlave extends EventEmitter {
3230
3448
  const byteCount = (length + 7) >> 3;
3231
3449
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3232
3450
  pdu[0] = byteCount;
3233
- if (coils instanceof Uint8Array) {
3234
- // Branchless fast path `coils[i]` is already 0/1, no boolean
3235
- // coercion or conditional jumps. At max payload (2000 coils) this
3236
- // avoids 2000 branch-predictor slots and boolean-to-number casts.
3237
- let out = 1;
3238
- const fullBytes = length >> 3;
3239
- for (let i = 0; i < fullBytes; i++) {
3240
- const base = i << 3;
3241
- pdu[out++] =
3242
- (coils[base] & 1) |
3243
- ((coils[base + 1] & 1) << 1) |
3244
- ((coils[base + 2] & 1) << 2) |
3245
- ((coils[base + 3] & 1) << 3) |
3246
- ((coils[base + 4] & 1) << 4) |
3247
- ((coils[base + 5] & 1) << 5) |
3248
- ((coils[base + 6] & 1) << 6) |
3249
- ((coils[base + 7] & 1) << 7);
3451
+ let out = 1;
3452
+ const fullBytes = length >> 3;
3453
+ for (let i = 0; i < fullBytes; i++) {
3454
+ const base = i << 3;
3455
+ pdu[out++] =
3456
+ (coils[base] & 1) |
3457
+ ((coils[base + 1] & 1) << 1) |
3458
+ ((coils[base + 2] & 1) << 2) |
3459
+ ((coils[base + 3] & 1) << 3) |
3460
+ ((coils[base + 4] & 1) << 4) |
3461
+ ((coils[base + 5] & 1) << 5) |
3462
+ ((coils[base + 6] & 1) << 6) |
3463
+ ((coils[base + 7] & 1) << 7);
3464
+ }
3465
+ const rem = length & 7;
3466
+ if (rem) {
3467
+ const base = fullBytes << 3;
3468
+ let acc = coils[base] & 1;
3469
+ if (rem > 1) {
3470
+ acc |= (coils[base + 1] & 1) << 1;
3250
3471
  }
3251
- const rem = length & 7;
3252
- if (rem) {
3253
- const base = fullBytes << 3;
3254
- let acc = coils[base] & 1;
3255
- if (rem > 1) {
3256
- acc |= (coils[base + 1] & 1) << 1;
3257
- }
3258
- if (rem > 2) {
3259
- acc |= (coils[base + 2] & 1) << 2;
3260
- }
3261
- if (rem > 3) {
3262
- acc |= (coils[base + 3] & 1) << 3;
3263
- }
3264
- if (rem > 4) {
3265
- acc |= (coils[base + 4] & 1) << 4;
3266
- }
3267
- if (rem > 5) {
3268
- acc |= (coils[base + 5] & 1) << 5;
3269
- }
3270
- if (rem > 6) {
3271
- acc |= (coils[base + 6] & 1) << 6;
3272
- }
3273
- pdu[out] = acc;
3472
+ if (rem > 2) {
3473
+ acc |= (coils[base + 2] & 1) << 2;
3274
3474
  }
3275
- }
3276
- else {
3277
- // Fallback for boolean[] — accumulate into `acc` and write a full byte
3278
- // once each lane is finished. Saves N `|=` read-modify-writes on the
3279
- // output buffer.
3280
- let acc = 0;
3281
- let out = 1;
3282
- for (let i = 0; i < length; i++) {
3283
- if (coils[i]) {
3284
- acc |= 1 << (i & 7);
3285
- }
3286
- if ((i & 7) === 7) {
3287
- pdu[out++] = acc;
3288
- acc = 0;
3289
- }
3475
+ if (rem > 3) {
3476
+ acc |= (coils[base + 3] & 1) << 3;
3477
+ }
3478
+ if (rem > 4) {
3479
+ acc |= (coils[base + 4] & 1) << 4;
3290
3480
  }
3291
- if ((length & 7) !== 0) {
3292
- pdu[out] = acc;
3481
+ if (rem > 5) {
3482
+ acc |= (coils[base + 5] & 1) << 5;
3293
3483
  }
3484
+ if (rem > 6) {
3485
+ acc |= (coils[base + 6] & 1) << 6;
3486
+ }
3487
+ pdu[out] = acc;
3294
3488
  }
3295
3489
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3296
3490
  }
@@ -3322,61 +3516,43 @@ class ModbusSlave extends EventEmitter {
3322
3516
  const byteCount = (length + 7) >> 3;
3323
3517
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3324
3518
  pdu[0] = byteCount;
3325
- if (discreteInputs instanceof Uint8Array) {
3326
- let out = 1;
3327
- const fullBytes = length >> 3;
3328
- for (let i = 0; i < fullBytes; i++) {
3329
- const base = i << 3;
3330
- pdu[out++] =
3331
- (discreteInputs[base] & 1) |
3332
- ((discreteInputs[base + 1] & 1) << 1) |
3333
- ((discreteInputs[base + 2] & 1) << 2) |
3334
- ((discreteInputs[base + 3] & 1) << 3) |
3335
- ((discreteInputs[base + 4] & 1) << 4) |
3336
- ((discreteInputs[base + 5] & 1) << 5) |
3337
- ((discreteInputs[base + 6] & 1) << 6) |
3338
- ((discreteInputs[base + 7] & 1) << 7);
3519
+ let out = 1;
3520
+ const fullBytes = length >> 3;
3521
+ for (let i = 0; i < fullBytes; i++) {
3522
+ const base = i << 3;
3523
+ pdu[out++] =
3524
+ (discreteInputs[base] & 1) |
3525
+ ((discreteInputs[base + 1] & 1) << 1) |
3526
+ ((discreteInputs[base + 2] & 1) << 2) |
3527
+ ((discreteInputs[base + 3] & 1) << 3) |
3528
+ ((discreteInputs[base + 4] & 1) << 4) |
3529
+ ((discreteInputs[base + 5] & 1) << 5) |
3530
+ ((discreteInputs[base + 6] & 1) << 6) |
3531
+ ((discreteInputs[base + 7] & 1) << 7);
3532
+ }
3533
+ const rem = length & 7;
3534
+ if (rem) {
3535
+ const base = fullBytes << 3;
3536
+ let acc = discreteInputs[base] & 1;
3537
+ if (rem > 1) {
3538
+ acc |= (discreteInputs[base + 1] & 1) << 1;
3339
3539
  }
3340
- const rem = length & 7;
3341
- if (rem) {
3342
- const base = fullBytes << 3;
3343
- let acc = discreteInputs[base] & 1;
3344
- if (rem > 1) {
3345
- acc |= (discreteInputs[base + 1] & 1) << 1;
3346
- }
3347
- if (rem > 2) {
3348
- acc |= (discreteInputs[base + 2] & 1) << 2;
3349
- }
3350
- if (rem > 3) {
3351
- acc |= (discreteInputs[base + 3] & 1) << 3;
3352
- }
3353
- if (rem > 4) {
3354
- acc |= (discreteInputs[base + 4] & 1) << 4;
3355
- }
3356
- if (rem > 5) {
3357
- acc |= (discreteInputs[base + 5] & 1) << 5;
3358
- }
3359
- if (rem > 6) {
3360
- acc |= (discreteInputs[base + 6] & 1) << 6;
3361
- }
3362
- pdu[out] = acc;
3540
+ if (rem > 2) {
3541
+ acc |= (discreteInputs[base + 2] & 1) << 2;
3363
3542
  }
3364
- }
3365
- else {
3366
- let acc = 0;
3367
- let out = 1;
3368
- for (let i = 0; i < length; i++) {
3369
- if (discreteInputs[i]) {
3370
- acc |= 1 << (i & 7);
3371
- }
3372
- if ((i & 7) === 7) {
3373
- pdu[out++] = acc;
3374
- acc = 0;
3375
- }
3543
+ if (rem > 3) {
3544
+ acc |= (discreteInputs[base + 3] & 1) << 3;
3376
3545
  }
3377
- if ((length & 7) !== 0) {
3378
- pdu[out] = acc;
3546
+ if (rem > 4) {
3547
+ acc |= (discreteInputs[base + 4] & 1) << 4;
3379
3548
  }
3549
+ if (rem > 5) {
3550
+ acc |= (discreteInputs[base + 5] & 1) << 5;
3551
+ }
3552
+ if (rem > 6) {
3553
+ acc |= (discreteInputs[base + 6] & 1) << 6;
3554
+ }
3555
+ pdu[out] = acc;
3380
3556
  }
3381
3557
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3382
3558
  }
@@ -3480,7 +3656,7 @@ class ModbusSlave extends EventEmitter {
3480
3656
  return;
3481
3657
  }
3482
3658
  try {
3483
- await model.writeSingleCoil(address, value === COIL_ON);
3659
+ await model.writeSingleCoil(address, value === COIL_ON ? 1 : 0);
3484
3660
  await response(appLayer.encode(frame.unit, frame.fc, frame.data, frame.transaction));
3485
3661
  }
3486
3662
  catch (error) {
@@ -3522,7 +3698,7 @@ class ModbusSlave extends EventEmitter {
3522
3698
  const address = (frame.data[0] << 8) | frame.data[1];
3523
3699
  const length = (frame.data[2] << 8) | frame.data[3];
3524
3700
  const byteCount = frame.data[4];
3525
- if (length < LIMITS.READ_COILS_MIN || length > LIMITS.WRITE_COILS_MAX || byteCount !== Math.ceil(length / 8)) {
3701
+ if (length < LIMITS.READ_COILS_MIN || length > LIMITS.WRITE_COILS_MAX || byteCount !== (length + 7) >> 3) {
3526
3702
  await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
3527
3703
  return;
3528
3704
  }
@@ -3530,42 +3706,42 @@ class ModbusSlave extends EventEmitter {
3530
3706
  await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
3531
3707
  return;
3532
3708
  }
3533
- const value = new Array(length);
3709
+ const value = new Uint8Array(length);
3534
3710
  let byteIdx = 5;
3535
3711
  let outIdx = 0;
3536
3712
  const fullBytes = length >> 3;
3537
3713
  for (let b = 0; b < fullBytes; b++) {
3538
3714
  const byte = frame.data[byteIdx++];
3539
- value[outIdx++] = (byte & 0x01) > 0;
3540
- value[outIdx++] = (byte & 0x02) > 0;
3541
- value[outIdx++] = (byte & 0x04) > 0;
3542
- value[outIdx++] = (byte & 0x08) > 0;
3543
- value[outIdx++] = (byte & 0x10) > 0;
3544
- value[outIdx++] = (byte & 0x20) > 0;
3545
- value[outIdx++] = (byte & 0x40) > 0;
3546
- value[outIdx++] = (byte & 0x80) > 0;
3715
+ value[outIdx++] = byte & 0x01;
3716
+ value[outIdx++] = (byte >>> 1) & 0x01;
3717
+ value[outIdx++] = (byte >>> 2) & 0x01;
3718
+ value[outIdx++] = (byte >>> 3) & 0x01;
3719
+ value[outIdx++] = (byte >>> 4) & 0x01;
3720
+ value[outIdx++] = (byte >>> 5) & 0x01;
3721
+ value[outIdx++] = (byte >>> 6) & 0x01;
3722
+ value[outIdx++] = (byte >>> 7) & 0x01;
3547
3723
  }
3548
3724
  const rem = length & 7;
3549
3725
  if (rem) {
3550
3726
  const byte = frame.data[byteIdx];
3551
- value[outIdx++] = (byte & 0x01) > 0;
3727
+ value[outIdx++] = byte & 0x01;
3552
3728
  if (rem > 1) {
3553
- value[outIdx++] = (byte & 0x02) > 0;
3729
+ value[outIdx++] = (byte >>> 1) & 0x01;
3554
3730
  }
3555
3731
  if (rem > 2) {
3556
- value[outIdx++] = (byte & 0x04) > 0;
3732
+ value[outIdx++] = (byte >>> 2) & 0x01;
3557
3733
  }
3558
3734
  if (rem > 3) {
3559
- value[outIdx++] = (byte & 0x08) > 0;
3735
+ value[outIdx++] = (byte >>> 3) & 0x01;
3560
3736
  }
3561
3737
  if (rem > 4) {
3562
- value[outIdx++] = (byte & 0x10) > 0;
3738
+ value[outIdx++] = (byte >>> 4) & 0x01;
3563
3739
  }
3564
3740
  if (rem > 5) {
3565
- value[outIdx++] = (byte & 0x20) > 0;
3741
+ value[outIdx++] = (byte >>> 5) & 0x01;
3566
3742
  }
3567
3743
  if (rem > 6) {
3568
- value[outIdx++] = (byte & 0x40) > 0;
3744
+ value[outIdx++] = (byte >>> 6) & 0x01;
3569
3745
  }
3570
3746
  }
3571
3747
  try {
@@ -3634,21 +3810,41 @@ class ModbusSlave extends EventEmitter {
3634
3810
  return;
3635
3811
  }
3636
3812
  try {
3637
- const { serverId = [model.unit ?? 1], runIndicatorStatus = true, additionalData = [] } = await model.reportServerId();
3638
- const serverIdBytes = serverId;
3639
- const byteCount = serverIdBytes.length + 1 + additionalData.length;
3813
+ const result = await model.reportServerId();
3814
+ const sid = result.serverId;
3815
+ const extra = result.additionalData;
3816
+ const sidLen = sid?.length ?? 1;
3817
+ const extraLen = extra?.length ?? 0;
3818
+ const byteCount = sidLen + 1 + extraLen;
3640
3819
  if (byteCount > 255) {
3641
3820
  await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
3642
3821
  return;
3643
3822
  }
3644
- const allBytes = [...serverIdBytes, runIndicatorStatus ? 0xff : 0x00, ...additionalData];
3645
- if (allBytes.some((b) => !isUint8(b))) {
3646
- await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
3647
- return;
3648
- }
3649
3823
  const data = Buffer.allocUnsafe(byteCount + 1);
3650
3824
  data[0] = byteCount;
3651
- data.set(allBytes, 1);
3825
+ let off = 1;
3826
+ if (sid) {
3827
+ for (let i = 0; i < sidLen; i++) {
3828
+ const b = sid[i];
3829
+ if ((b & 0xff) !== b) {
3830
+ await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
3831
+ return;
3832
+ }
3833
+ data[off++] = b;
3834
+ }
3835
+ }
3836
+ else {
3837
+ const unitId = model.unit ?? 1;
3838
+ if ((unitId & 0xff) !== unitId) {
3839
+ await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
3840
+ return;
3841
+ }
3842
+ data[off++] = unitId;
3843
+ }
3844
+ data[off++] = (result.runIndicatorStatus ?? true) ? 0xff : 0x00;
3845
+ if (extra) {
3846
+ extra.copy(data, off);
3847
+ }
3652
3848
  await response(appLayer.encode(frame.unit, frame.fc, data, frame.transaction));
3653
3849
  }
3654
3850
  catch (error) {