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.cjs CHANGED
@@ -234,7 +234,7 @@ function checkRange(value, range) {
234
234
  return false;
235
235
  }
236
236
 
237
- const TABLE = new Uint16Array([
237
+ const CRC_TABLE = new Uint16Array([
238
238
  0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
239
239
  0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
240
240
  0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
@@ -252,23 +252,29 @@ const TABLE = new Uint16Array([
252
252
  0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
253
253
  0x4040,
254
254
  ]);
255
- function crc(data, start, end) {
255
+ /** CRC-16 (Modbus) over a single contiguous buffer. */
256
+ function crcFixed(data, start, end) {
256
257
  let crc = 0xffff;
257
258
  for (let index = start; index < end; index++) {
258
- crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
259
+ crc = CRC_TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
259
260
  }
260
261
  return crc;
261
262
  }
262
-
263
263
  /**
264
- * Returns true when `n` is an integer in the unsigned-byte range [0, 255].
265
- *
266
- * Used for byte-level Modbus payload validation (function-code values, raw
267
- * byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
268
- * 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]).
269
266
  */
270
- function isUint8(n) {
271
- return (n & 0xff) === n;
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;
272
278
  }
273
279
 
274
280
  function lrc(data, start, end) {
@@ -309,12 +315,12 @@ const RES_TABLE = new Int32Array(256);
309
315
  RES_TABLE[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
310
316
  RES_TABLE[exports.FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
311
317
  })();
312
- function predictRtuFrameLength(buffer, start, end, isResponse) {
318
+ function predictRtuFrameLength(residual, data, residualLen, start, end, isResponse) {
313
319
  const len = end - start;
314
320
  if (len < 2) {
315
321
  return PREDICT_NEED_MORE;
316
322
  }
317
- const fc = buffer[start + 1];
323
+ const fc = start + 1 < residualLen ? residual[start + 1] : data[start + 1 - residualLen];
318
324
  if (isResponse) {
319
325
  if ((fc & EXCEPTION_OFFSET) !== 0) {
320
326
  return 5;
@@ -330,16 +336,16 @@ function predictRtuFrameLength(buffer, start, end, isResponse) {
330
336
  if (end - start < 8) {
331
337
  return PREDICT_NEED_MORE;
332
338
  }
333
- if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
339
+ if ((start + 2 < residualLen ? residual[start + 2] : data[start + 2 - residualLen]) !== MEI_READ_DEVICE_ID) {
334
340
  return PREDICT_UNKNOWN;
335
341
  }
336
- const numObjs = buffer[start + 7];
342
+ const numObjs = start + 7 < residualLen ? residual[start + 7] : data[start + 7 - residualLen];
337
343
  let cursor = start + 8;
338
344
  for (let i = 0; i < numObjs; i++) {
339
345
  if (end < cursor + 2) {
340
346
  return PREDICT_NEED_MORE;
341
347
  }
342
- cursor += 2 + buffer[cursor + 1];
348
+ cursor += 2 + (cursor + 1 < residualLen ? residual[cursor + 1] : data[cursor + 1 - residualLen]);
343
349
  }
344
350
  return cursor - start + 2;
345
351
  }
@@ -348,7 +354,7 @@ function predictRtuFrameLength(buffer, start, end, isResponse) {
348
354
  if (len <= offset) {
349
355
  return PREDICT_NEED_MORE;
350
356
  }
351
- return (decode & 0xff) + buffer[start + offset];
357
+ return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
352
358
  }
353
359
  }
354
360
  else {
@@ -362,7 +368,7 @@ function predictRtuFrameLength(buffer, start, end, isResponse) {
362
368
  if (len <= offset) {
363
369
  return PREDICT_NEED_MORE;
364
370
  }
365
- return (decode & 0xff) + buffer[start + offset];
371
+ return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
366
372
  }
367
373
  }
368
374
  return PREDICT_UNKNOWN;
@@ -405,7 +411,12 @@ function resolveOne(value, baudRate, fastBaudMs) {
405
411
  if (baudRate === undefined) {
406
412
  return undefined;
407
413
  }
408
- 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);
409
420
  }
410
421
  /**
411
422
  * Resolve Modbus RTU timing parameters from user options into milliseconds.
@@ -422,7 +433,17 @@ function resolveRtuTiming(opts = {}, baudRate) {
422
433
  if (intervalBetweenFrames === undefined) {
423
434
  // Spec default: t3.5 derived from baudRate, or 0 when neither option nor
424
435
  // baudRate were supplied.
425
- 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
+ }
426
447
  }
427
448
  let interCharTimeout = resolveOne(opts.interCharTimeout, baudRate, 0.75);
428
449
  if (interCharTimeout === undefined) {
@@ -490,7 +511,9 @@ class TimerHeap {
490
511
  this._mode = 'heap';
491
512
  for (const [existingId, { handle, deadline }] of this._directTimers) {
492
513
  clearTimeout(handle);
493
- const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
514
+ const diff = deadline - performance.now();
515
+ const trunc = diff | 0;
516
+ const remaining = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
494
517
  if (remaining === 0) {
495
518
  this._onFire(existingId);
496
519
  }
@@ -550,8 +573,10 @@ class TimerHeap {
550
573
  if (this._deadlines.length === 0) {
551
574
  return;
552
575
  }
553
- const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
554
- const safeDelay = Math.min(delay, 2147483647);
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;
555
580
  this._timer = setTimeout(this._boundTick, safeDelay);
556
581
  }
557
582
  _onTick() {
@@ -635,6 +660,9 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
635
660
  get physicalLayer() {
636
661
  return this._physicalLayer;
637
662
  }
663
+ get serialport() {
664
+ return this._serialport;
665
+ }
638
666
  constructor(physicalLayer, serialport) {
639
667
  super();
640
668
  this._physicalLayer = physicalLayer;
@@ -643,6 +671,7 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
643
671
  if (this.state !== exports.PhysicalConnectionState.CONNECTED) {
644
672
  return;
645
673
  }
674
+ this.emit('rx', chunk);
646
675
  this.emit('data', chunk);
647
676
  };
648
677
  serialport.on('data', onData);
@@ -671,7 +700,12 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
671
700
  }
672
701
  write(data, cb) {
673
702
  if (this.state === exports.PhysicalConnectionState.CONNECTED) {
674
- this._serialport.write(data, cb);
703
+ this._serialport.write(data, (err) => {
704
+ if (!err) {
705
+ this.emit('tx', data);
706
+ }
707
+ cb?.(err);
708
+ });
675
709
  }
676
710
  else {
677
711
  cb?.(new Error('Connection is not connected'));
@@ -832,6 +866,9 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
832
866
  get physicalLayer() {
833
867
  return this._physicalLayer;
834
868
  }
869
+ get socket() {
870
+ return this._socket;
871
+ }
835
872
  constructor(physicalLayer, socket) {
836
873
  super();
837
874
  this._physicalLayer = physicalLayer;
@@ -840,6 +877,7 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
840
877
  if (this.state !== exports.PhysicalConnectionState.CONNECTED) {
841
878
  return;
842
879
  }
880
+ this.emit('rx', chunk);
843
881
  this.emit('data', chunk);
844
882
  };
845
883
  socket.on('data', onData);
@@ -868,7 +906,12 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
868
906
  }
869
907
  write(data, cb) {
870
908
  if (this.state === exports.PhysicalConnectionState.CONNECTED) {
871
- this._socket.write(data, cb);
909
+ this._socket.write(data, (err) => {
910
+ if (!err) {
911
+ this.emit('tx', data);
912
+ }
913
+ cb?.(err);
914
+ });
872
915
  }
873
916
  else {
874
917
  cb?.(new Error('Connection is not connected'));
@@ -1150,6 +1193,9 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1150
1193
  get physicalLayer() {
1151
1194
  return this._physicalLayer;
1152
1195
  }
1196
+ get socket() {
1197
+ return this._socket;
1198
+ }
1153
1199
  constructor(physicalLayer, socket) {
1154
1200
  super();
1155
1201
  this._physicalLayer = physicalLayer;
@@ -1158,6 +1204,7 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1158
1204
  if (this.state !== exports.PhysicalConnectionState.CONNECTED) {
1159
1205
  return;
1160
1206
  }
1207
+ this.emit('rx', msg);
1161
1208
  this.emit('data', msg);
1162
1209
  };
1163
1210
  socket.on('message', onMessage);
@@ -1186,7 +1233,12 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
1186
1233
  }
1187
1234
  write(data, cb) {
1188
1235
  if (this.state === exports.PhysicalConnectionState.CONNECTED) {
1189
- this._socket.send(data, cb);
1236
+ this._socket.send(data, (err) => {
1237
+ if (!err) {
1238
+ this.emit('tx', data);
1239
+ }
1240
+ cb?.(err);
1241
+ });
1190
1242
  }
1191
1243
  else {
1192
1244
  cb?.(new Error('Connection is not connected'));
@@ -1326,6 +1378,12 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1326
1378
  get physicalLayer() {
1327
1379
  return this._physicalLayer;
1328
1380
  }
1381
+ get socket() {
1382
+ return this._socket;
1383
+ }
1384
+ get remote() {
1385
+ return this._remote;
1386
+ }
1329
1387
  constructor(physicalLayer, socket, remote, idleTimeout, messageEventDelegation) {
1330
1388
  super();
1331
1389
  this._physicalLayer = physicalLayer;
@@ -1344,6 +1402,7 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1344
1402
  }, idleTimeout);
1345
1403
  }
1346
1404
  }
1405
+ this.emit('rx', msg);
1347
1406
  this.emit('data', msg);
1348
1407
  };
1349
1408
  messageEventDelegation.add(onMessage);
@@ -1356,7 +1415,12 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
1356
1415
  }
1357
1416
  write(data, cb) {
1358
1417
  if (this.state === exports.PhysicalConnectionState.CONNECTED) {
1359
- 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
+ });
1360
1424
  }
1361
1425
  else {
1362
1426
  cb?.(new Error('Connection is not connected'));
@@ -1559,16 +1623,23 @@ class AbstractApplicationLayer {
1559
1623
  removeCustomFunctionCode(fc) { }
1560
1624
  }
1561
1625
 
1562
- const MAX_FRAME_LENGTH = 256;
1626
+ const MAX_FRAME_LENGTH$1 = 256;
1563
1627
  const MIN_FRAME_LENGTH = 4;
1564
1628
  class RtuApplicationLayer extends AbstractApplicationLayer {
1565
1629
  PROTOCOL = 'RTU';
1566
1630
  ROLE;
1567
1631
  _connection;
1568
- _state;
1569
- _poolSize;
1570
- _threePointFiveT;
1571
- _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;
1572
1643
  _customFunctionCodes = new Map();
1573
1644
  _cleanupCbs = [];
1574
1645
  get connection() {
@@ -1578,89 +1649,246 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1578
1649
  super();
1579
1650
  this.ROLE = role;
1580
1651
  this._connection = connection;
1581
- const { intervalBetweenFrames, interCharTimeout, poolSize } = options;
1582
- this._poolSize = poolSize ?? MAX_FRAME_LENGTH * 2;
1583
- this._state = { pool: Buffer.alloc(this._poolSize), start: 0, end: 0 };
1584
- this._threePointFiveT = intervalBetweenFrames ?? 0;
1585
- 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;
1586
1661
  const onData = (data) => {
1587
- const state = this._state;
1588
- if (state.t15Expired && state.end > state.start) {
1589
- this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
1590
- state.start = 0;
1591
- state.end = 0;
1592
- }
1593
- state.t15Expired = false;
1594
1662
  const dataLen = data.length;
1595
- const space = state.pool.length - state.end;
1596
- if (dataLen <= space) {
1597
- // Fast path: incoming chunk fits the current pool tail; one bulk copy,
1598
- // no per-iteration bookkeeping. This is the typical case.
1599
- data.copy(state.pool, state.end);
1600
- 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
+ }
1601
1677
  }
1602
- else {
1603
- // Slow path: incoming chunk does NOT fit the pool's remaining tail
1604
- // space (not necessarily larger than the whole pool — typically the
1605
- // pool already holds undrained residue from earlier ticks).
1606
- // Strategy drain & absorb in chunks:
1607
- // 1. Copy as much of `data` as fits the current tail.
1608
- // 2. When the pool fills mid-chunk, run flushBuffer(false) to emit
1609
- // any complete frames and collapse the leftover bytes to offset
1610
- // 0, which reclaims tail space.
1611
- // 3. Repeat until `data` is fully ingested.
1612
- // The mid-chunk flush is NEVER strict — t3.5 has not actually elapsed,
1613
- // the rest of `data` is still in hand. Only the t3.5 timer callback
1614
- // below passes strict=true.
1615
- let dataOffset = 0;
1616
- let currentState = state;
1617
- while (dataOffset < dataLen) {
1618
- const room = currentState.pool.length - currentState.end;
1619
- if (room === 0) {
1620
- // Pool full mid-ingest. Try to extract any complete frames and
1621
- // reclaim tail space so the rest of `data` has somewhere to go.
1622
- this.clearStateTimers();
1623
- this.flushBuffer(false);
1624
- // flushBuffer may emit; user callbacks could in principle replace
1625
- // _state. Re-read to stay correct across that boundary.
1626
- currentState = this._state;
1627
- if (currentState.pool.length - currentState.end === 0) {
1628
- // flushBuffer freed nothing — the entire pool is unparseable
1629
- // residue (typically a misconfigured poolSize for the wire's
1630
- // frame size). Hard reset; we cannot recover automatically.
1631
- this.onFramingError(new Error('Frame buffer exhausted before complete frame received'));
1632
- currentState.start = 0;
1633
- currentState.end = 0;
1634
- 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
+ }
1635
1704
  }
1636
- continue;
1637
1705
  }
1638
- // Copy the next chunk into the freshly-available tail space.
1639
- const toCopy = room < dataLen - dataOffset ? room : dataLen - dataOffset;
1640
- data.copy(currentState.pool, currentState.end, dataOffset, dataOffset + toCopy);
1641
- currentState.end += toCopy;
1642
- 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
+ }
1643
1744
  }
1644
1745
  }
1645
- this.clearStateTimers();
1646
- const liveState = this._state;
1647
- if (liveState.end - liveState.start >= MAX_FRAME_LENGTH) {
1648
- this.flushBuffer(this._threePointFiveT > 0);
1649
- }
1650
- else if (this._threePointFiveT) {
1651
- if (this._onePointFiveT > 0) {
1652
- liveState.interCharTimer = setTimeout(() => {
1653
- liveState.interCharTimer = undefined;
1654
- liveState.t15Expired = true;
1655
- }, 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;
1656
1826
  }
1657
- liveState.timer = setTimeout(() => {
1658
- this.clearStateTimers();
1659
- this.flushBuffer(true);
1660
- }, 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;
1661
1837
  }
1662
1838
  else {
1663
- 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
+ }
1664
1892
  }
1665
1893
  };
1666
1894
  connection.on('data', onData);
@@ -1670,150 +1898,33 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1670
1898
  fn();
1671
1899
  }
1672
1900
  this._cleanupCbs.length = 0;
1673
- 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
+ }
1674
1909
  };
1675
1910
  connection.on('close', onClose);
1676
1911
  this._cleanupCbs.push(() => connection.off('close', onClose));
1677
1912
  }
1678
- clearStateTimers() {
1679
- const state = this._state;
1680
- if (state.timer) {
1681
- clearTimeout(state.timer);
1682
- state.timer = undefined;
1683
- }
1684
- if (state.interCharTimer) {
1685
- clearTimeout(state.interCharTimer);
1686
- state.interCharTimer = undefined;
1687
- }
1688
- }
1689
- /**
1690
- * Shared handler for every "frame is not yet complete" exit in `flushBuffer`.
1691
- * Returns `true` when the caller should `return` (strict reset), `false` to
1692
- * `break` the parse loop. Hot path never reaches here — only error/incomplete
1693
- * edges. Extracted as a method so it is not recreated on every `flushBuffer`
1694
- * call.
1695
- */
1696
- _handleIncomplete(state, strict) {
1697
- if (strict) {
1698
- this.onFramingError(new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
1699
- state.start = 0;
1700
- state.end = 0;
1701
- state.t15Expired = false;
1702
- return true;
1703
- }
1704
- if (state.t15Expired) {
1705
- this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
1706
- state.start = 0;
1707
- state.end = 0;
1708
- state.t15Expired = false;
1709
- }
1710
- return false;
1711
- }
1712
- flushBuffer(strict) {
1713
- const state = this._state;
1714
- const isResponse = this.ROLE === 'MASTER';
1715
- const pool = state.pool;
1716
- const customFCs = this._customFunctionCodes;
1717
- while (state.end - state.start > 0) {
1718
- const available = state.end - state.start;
1719
- if (available < MIN_FRAME_LENGTH) {
1720
- if (this._handleIncomplete(state, strict)) {
1721
- return;
1722
- }
1723
- break;
1724
- }
1725
- const fc = pool[state.start + 1];
1726
- const cfc = customFCs.get(fc);
1727
- let expected;
1728
- if (cfc) {
1729
- const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
1730
- const predicted = predictor(pool, state.start, state.end);
1731
- // Normalize custom predictor's `null` to the std sentinel so both
1732
- // paths share the same NEED_MORE tail below.
1733
- expected = predicted ?? PREDICT_NEED_MORE;
1734
- }
1735
- else {
1736
- // Standard FC path. predictRtuFrameLength uses sentinel returns to
1737
- // avoid per-call object allocation on the decode hot path.
1738
- expected = predictRtuFrameLength(pool, state.start, state.end, isResponse);
1739
- if (expected === PREDICT_UNKNOWN) {
1740
- this.onFramingError(new Error(`Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`));
1741
- state.start = 0;
1742
- state.end = 0;
1743
- return;
1744
- }
1745
- }
1746
- if (expected === PREDICT_NEED_MORE) {
1747
- if (available >= MAX_FRAME_LENGTH) {
1748
- state.start += 1;
1749
- continue;
1750
- }
1751
- if (this._handleIncomplete(state, strict)) {
1752
- return;
1753
- }
1754
- break;
1755
- }
1756
- if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
1757
- this.onFramingError(new Error('Invalid data'));
1758
- state.start = 0;
1759
- state.end = 0;
1760
- return;
1761
- }
1762
- if (available < expected) {
1763
- if (available >= MAX_FRAME_LENGTH) {
1764
- state.start += 1;
1765
- continue;
1766
- }
1767
- if (this._handleIncomplete(state, strict)) {
1768
- return;
1769
- }
1770
- break;
1771
- }
1772
- // CRC check inline: no helper call, no subarray for the CRC body.
1773
- const crcStart = state.start;
1774
- const crcEnd = crcStart + expected - 2;
1775
- const expectedCrc = pool.readUInt16LE(crcEnd);
1776
- const actualCrc = crc(pool, crcStart, crcEnd);
1777
- if (expectedCrc !== actualCrc) {
1778
- if (strict) {
1779
- this.onFramingError(new Error('CRC mismatch'));
1780
- state.start = 0;
1781
- state.end = 0;
1782
- state.t15Expired = false;
1783
- return;
1784
- }
1785
- state.start += 1;
1786
- continue;
1787
- }
1788
- // Frame located. Copy it out of the pool so the emitted buffer remains
1789
- // valid even if the consumer queues the frame across `onData` ticks.
1790
- // `Buffer.copyBytesFrom` is a native fast-path (Node 18.19+) — measurably
1791
- // faster than `Buffer.from(buffer)` for this size.
1792
- const frameBuf = Buffer.copyBytesFrom(pool, crcStart, expected);
1793
- state.start += expected;
1794
- const frame = {
1795
- unit: frameBuf[0],
1796
- fc: frameBuf[1],
1797
- data: frameBuf.subarray(2, expected - 2),
1798
- buffer: frameBuf,
1799
- };
1800
- 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;
1801
1920
  }
1802
- if (state.start > 0) {
1803
- if (state.start < state.end) {
1804
- state.pool.copy(state.pool, 0, state.start, state.end);
1805
- }
1806
- state.end -= state.start;
1807
- state.start = 0;
1921
+ if (this._t35Timer !== undefined) {
1922
+ clearTimeout(this._t35Timer);
1923
+ this._t35Timer = undefined;
1808
1924
  }
1809
1925
  }
1810
- flush() {
1811
- this.clearStateTimers();
1812
- this._state.start = 0;
1813
- this._state.end = 0;
1814
- }
1815
1926
  addCustomFunctionCode(cfc) {
1816
- if (!isUint8(cfc.fc)) {
1927
+ if ((cfc.fc & 0xff) !== cfc.fc) {
1817
1928
  throw new Error(`fc must be an integer in 0..255, got ${cfc.fc}`);
1818
1929
  }
1819
1930
  this._customFunctionCodes.set(cfc.fc, cfc);
@@ -1836,7 +1947,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
1836
1947
  buffer.set(data, 2);
1837
1948
  }
1838
1949
  const crcEnd = buffer.length - 2;
1839
- const c = crc(buffer, 0, crcEnd);
1950
+ const c = crcFixed(buffer, 0, crcEnd);
1840
1951
  // Little-endian inline write of CRC trailer.
1841
1952
  buffer[crcEnd] = c & 0xff;
1842
1953
  buffer[crcEnd + 1] = (c >>> 8) & 0xff;
@@ -1863,11 +1974,16 @@ for (let i = 0x41; i <= 0x46; i++) {
1863
1974
  for (let i = 0x61; i <= 0x66; i++) {
1864
1975
  HEX_DECODE[i] = i - 0x61 + 10;
1865
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
+ }
1866
1983
  const HEX_ENCODE = new Uint8Array('0123456789ABCDEF'.split('').map((c) => c.charCodeAt(0)));
1867
1984
  class AsciiApplicationLayer extends AbstractApplicationLayer {
1868
1985
  PROTOCOL = 'ASCII';
1869
1986
  ROLE;
1870
- lenientHex;
1871
1987
  _connection;
1872
1988
  _state = { status: 'idle', frame: new Uint8Array(MAX_ASCII_PAYLOAD), frameLen: 0 };
1873
1989
  _cleanupCbs = [];
@@ -1878,23 +1994,30 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1878
1994
  super();
1879
1995
  this.ROLE = role;
1880
1996
  this._connection = connection;
1881
- this.lenientHex = options.lenientHex ?? false;
1882
- const lenientHex = this.lenientHex;
1883
- const isHexChar = (value) => {
1884
- if (value >= 0x30 && value <= 0x39) {
1885
- return true;
1886
- }
1887
- if (value >= 0x41 && value <= 0x46) {
1888
- return true;
1889
- }
1890
- if (lenientHex && value >= 0x61 && value <= 0x66) {
1891
- return true;
1892
- }
1893
- return false;
1894
- };
1997
+ const lenientHex = options.lenientHex ?? false;
1998
+ const hexTable = lenientHex ? HEX_DECODE : HEX_DECODE_STRICT;
1895
1999
  const onData = (data) => {
1896
2000
  const state = this._state;
1897
- 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++) {
1898
2021
  const value = data[i];
1899
2022
  switch (state.status) {
1900
2023
  case 'idle': {
@@ -1916,7 +2039,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1916
2039
  state.frameLen = 0;
1917
2040
  this.onFramingError(new Error('Invalid data'));
1918
2041
  }
1919
- else if (!isHexChar(value)) {
2042
+ else if (hexTable[value] > 15) {
1920
2043
  state.status = 'idle';
1921
2044
  state.frameLen = 0;
1922
2045
  this.onFramingError(new Error('Invalid hex character'));
@@ -1934,7 +2057,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1934
2057
  else {
1935
2058
  state.status = 'idle';
1936
2059
  if (value === CHAR_CODE.LF) {
1937
- this.framing(state.frame, state.frameLen);
2060
+ this.framing(state.frame, state.frameLen, hexTable);
1938
2061
  }
1939
2062
  }
1940
2063
  break;
@@ -1953,7 +2076,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1953
2076
  connection.on('close', onClose);
1954
2077
  this._cleanupCbs.push(() => connection.off('close', onClose));
1955
2078
  }
1956
- framing(hexChars, hexLen) {
2079
+ framing(hexChars, hexLen, hexTable) {
1957
2080
  if (hexLen < 6) {
1958
2081
  this.onFramingError(new Error('Insufficient data length'));
1959
2082
  return;
@@ -1965,10 +2088,10 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1965
2088
  const byteLen = hexLen >> 1;
1966
2089
  // Decode unit and fc directly from the first 4 hex characters —
1967
2090
  // avoids allocating a full decoded buffer just to read two bytes.
1968
- const unitHi = HEX_DECODE[hexChars[0]];
1969
- const unitLo = HEX_DECODE[hexChars[1]];
1970
- const fcHi = HEX_DECODE[hexChars[2]];
1971
- 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]];
1972
2095
  if (unitHi === 0xff || unitLo === 0xff || fcHi === 0xff || fcLo === 0xff) {
1973
2096
  this.onFramingError(new Error('Invalid hex character'));
1974
2097
  return;
@@ -1976,33 +2099,32 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
1976
2099
  const unit = (unitHi << 4) | unitLo;
1977
2100
  const fc = (fcHi << 4) | fcLo;
1978
2101
  // Decode LRC from the last 2 hex characters.
1979
- const lrcHi = HEX_DECODE[hexChars[hexLen - 2]];
1980
- const lrcLo = HEX_DECODE[hexChars[hexLen - 1]];
2102
+ const lrcHi = hexTable[hexChars[hexLen - 2]];
2103
+ const lrcLo = hexTable[hexChars[hexLen - 1]];
1981
2104
  if (lrcHi === 0xff || lrcLo === 0xff) {
1982
2105
  this.onFramingError(new Error('Invalid hex character'));
1983
2106
  return;
1984
2107
  }
1985
2108
  const lrcIn = (lrcHi << 4) | lrcLo;
1986
- // 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.
1987
2111
  // dataLen may be 0 for a frame that is only unit + fc + lrc.
1988
2112
  const dataLen = byteLen - 3;
1989
2113
  const data = Buffer.allocUnsafe(dataLen);
1990
2114
  let hexOff = 4;
2115
+ let sum = unit + fc;
1991
2116
  for (let i = 0; i < dataLen; i++) {
1992
- const hi = HEX_DECODE[hexChars[hexOff]];
1993
- const lo = HEX_DECODE[hexChars[hexOff + 1]];
2117
+ const hi = hexTable[hexChars[hexOff]];
2118
+ const lo = hexTable[hexChars[hexOff + 1]];
1994
2119
  if (hi === 0xff || lo === 0xff) {
1995
2120
  this.onFramingError(new Error('Invalid hex character'));
1996
2121
  return;
1997
2122
  }
1998
- data[i] = (hi << 4) | lo;
2123
+ const byte = (hi << 4) | lo;
2124
+ data[i] = byte;
2125
+ sum += byte;
1999
2126
  hexOff += 2;
2000
2127
  }
2001
- // Compute LRC over unit + fc + data.
2002
- let sum = unit + fc;
2003
- for (let i = 0; i < dataLen; i++) {
2004
- sum += data[i];
2005
- }
2006
2128
  const lrcComputed = (~sum + 1) & 0xff;
2007
2129
  if (lrcIn !== lrcComputed) {
2008
2130
  this.onFramingError(new Error('LRC check failed'));
@@ -2043,13 +2165,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
2043
2165
  }
2044
2166
  }
2045
2167
 
2046
- const MAX_TCP_FRAME = 260;
2168
+ const MAX_FRAME_LENGTH = 260;
2047
2169
  class TcpApplicationLayer extends AbstractApplicationLayer {
2048
2170
  PROTOCOL = 'TCP';
2049
2171
  ROLE;
2050
2172
  _connection;
2051
2173
  _transactionId = 1;
2052
- _buffer = EMPTY_BUFFER;
2174
+ _residual = Buffer.alloc(MAX_FRAME_LENGTH);
2175
+ _residualLen = 0;
2176
+ _expectedLen = PREDICT_NEED_MORE;
2053
2177
  _cleanupCbs = [];
2054
2178
  get connection() {
2055
2179
  return this._connection;
@@ -2059,51 +2183,108 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2059
2183
  this.ROLE = role;
2060
2184
  this._connection = connection;
2061
2185
  const onData = (data) => {
2062
- // Fast path: _buffer is empty and data is a single, complete frame.
2063
- // This avoids the tryExtract subarray allocations + while loop
2064
- // for the overwhelmingly common case (one frame per TCP packet).
2065
- if (this._buffer.length === 0 && data.length >= 8) {
2066
- const length = (data[4] << 8) | data[5]; // inline BE read
2067
- const total = 6 + length;
2068
- if (data[2] === 0 && data[3] === 0 && total <= MAX_TCP_FRAME && length >= 2 && data.length === total) {
2069
- this.processFrame(data);
2070
- 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
+ }
2071
2206
  }
2072
2207
  }
2073
- let buffer = this._buffer;
2074
- if (buffer.length === 0) {
2075
- 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
+ }
2076
2256
  }
2077
- else if (data.length > 0) {
2078
- 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;
2079
2262
  }
2080
- while (buffer.length > 0) {
2081
- const result = this.tryExtract(buffer);
2082
- if (result.kind === 'frame') {
2083
- this.processFrame(result.frame);
2084
- 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);
2085
2269
  }
2086
- else if (result.kind === 'insufficient') {
2087
- 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
+ }
2088
2275
  }
2089
2276
  else {
2090
- this.onFramingError(result.error);
2091
- buffer = EMPTY_BUFFER;
2092
- 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
+ }
2093
2287
  }
2094
- }
2095
- if (buffer.length === 0) {
2096
- this._buffer = EMPTY_BUFFER;
2097
- }
2098
- else if (buffer === data) {
2099
- // Copy into a right-sized buffer so we do not retain the potentially
2100
- // large backing buffer of the original incoming data (Node.js pool).
2101
- // `Buffer.copyBytesFrom` is a native fast-path — one C++ memcpy
2102
- // vs allocUnsafe + JS-level copy.
2103
- this._buffer = Buffer.copyBytesFrom(buffer);
2104
- }
2105
- else {
2106
- this._buffer = buffer;
2107
2288
  }
2108
2289
  };
2109
2290
  connection.on('data', onData);
@@ -2117,38 +2298,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
2117
2298
  connection.on('close', onClose);
2118
2299
  this._cleanupCbs.push(() => connection.off('close', onClose));
2119
2300
  }
2120
- tryExtract(buffer) {
2121
- if (buffer.length < 8) {
2122
- return { kind: 'insufficient' };
2123
- }
2124
- if (buffer[2] !== 0 || buffer[3] !== 0) {
2125
- return { kind: 'error', error: new Error('Invalid data') };
2126
- }
2127
- const length = (buffer[4] << 8) | buffer[5]; // inline BE read
2128
- const total = 6 + length;
2129
- if (total > MAX_TCP_FRAME || length < 2) {
2130
- return { kind: 'error', error: new Error('Invalid data') };
2131
- }
2132
- if (buffer.length < total) {
2133
- return { kind: 'insufficient' };
2134
- }
2135
- return { kind: 'frame', frame: buffer.subarray(0, total), rest: total === buffer.length ? EMPTY_BUFFER : buffer.subarray(total) };
2136
- }
2137
- processFrame(buffer) {
2138
- const frame = {
2139
- // Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
2140
- // argument coercion + bounds check. Symmetric to the header writes in
2141
- // encode() below. Hits on every received TCP frame.
2142
- transaction: (buffer[0] << 8) | buffer[1],
2143
- unit: buffer[6],
2144
- fc: buffer[7],
2145
- data: buffer.subarray(8),
2146
- buffer,
2147
- };
2148
- this.onFraming(frame);
2149
- }
2150
2301
  flush() {
2151
- this._buffer = EMPTY_BUFFER;
2302
+ this._residualLen = 0;
2303
+ this._expectedLen = PREDICT_NEED_MORE;
2152
2304
  }
2153
2305
  encode(unit, fc, data, transaction) {
2154
2306
  const buffer = Buffer.allocUnsafe(data.length + 8);
@@ -2315,6 +2467,7 @@ class ModbusMaster extends EventEmitter {
2315
2467
  appLayer.onFraming = NOOP;
2316
2468
  };
2317
2469
  const onFraming = (frame) => {
2470
+ this.emit('framing', frame, connection);
2318
2471
  this._masterSession.handleFrame(frame);
2319
2472
  };
2320
2473
  appLayer.onFraming = onFraming;
@@ -2323,17 +2476,34 @@ class ModbusMaster extends EventEmitter {
2323
2476
  appLayer.onFramingError = NOOP;
2324
2477
  };
2325
2478
  const onFramingError = (error) => {
2479
+ this.emit('framingError', error, connection);
2326
2480
  this._masterSession.handleError(error);
2327
2481
  };
2328
2482
  appLayer.onFramingError = onFramingError;
2329
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);
2330
2496
  const cleanupClose = () => connection.off('close', onClose);
2331
2497
  const onClose = () => {
2332
2498
  cleanupFraming();
2333
2499
  cleanupFramingError();
2500
+ cleanupTx();
2501
+ cleanupRx();
2334
2502
  cleanupClose();
2335
2503
  this._cleanupFns.delete(cleanupFraming);
2336
2504
  this._cleanupFns.delete(cleanupFramingError);
2505
+ this._cleanupFns.delete(cleanupTx);
2506
+ this._cleanupFns.delete(cleanupRx);
2337
2507
  this._cleanupFns.delete(cleanupClose);
2338
2508
  };
2339
2509
  connection.on('close', onClose);
@@ -2373,7 +2543,7 @@ class ModbusMaster extends EventEmitter {
2373
2543
  return new RtuApplicationLayer('MASTER', connection, {
2374
2544
  intervalBetweenFrames,
2375
2545
  interCharTimeout,
2376
- poolSize: this._protocol.opts?.poolSize,
2546
+ strictTiming: this._protocol.opts?.strictTiming,
2377
2547
  });
2378
2548
  }
2379
2549
  if (this._protocol.type === 'TCP') {
@@ -2549,10 +2719,8 @@ class ModbusMaster extends EventEmitter {
2549
2719
  });
2550
2720
  }
2551
2721
  writeFC1Or2(unit, fc, address, length, timeout) {
2552
- const byteCount = Math.ceil(length / 8);
2722
+ const byteCount = (length + 7) >> 3;
2553
2723
  const bufferTx = Buffer.allocUnsafe(4);
2554
- // Inline big-endian writes — direct typed-array stores skip the argument
2555
- // validation + bounds checks that `writeUInt16BE` runs on each call.
2556
2724
  bufferTx[0] = (address >>> 8) & 0xff;
2557
2725
  bufferTx[1] = address & 0xff;
2558
2726
  bufferTx[2] = (length >>> 8) & 0xff;
@@ -2569,46 +2737,44 @@ class ModbusMaster extends EventEmitter {
2569
2737
  }
2570
2738
  try {
2571
2739
  validateByteCountResponse(frame, unit, fc, byteCount);
2572
- const data = new Array(length);
2740
+ const data = new Uint8Array(length);
2573
2741
  let byteIdx = 1;
2574
2742
  let outIdx = 0;
2575
2743
  const fullBytes = length >> 3;
2576
2744
  for (let b = 0; b < fullBytes; b++) {
2577
2745
  const byte = frame.data[byteIdx++];
2578
- data[outIdx++] = (byte & 0x01) > 0;
2579
- data[outIdx++] = (byte & 0x02) > 0;
2580
- data[outIdx++] = (byte & 0x04) > 0;
2581
- data[outIdx++] = (byte & 0x08) > 0;
2582
- data[outIdx++] = (byte & 0x10) > 0;
2583
- data[outIdx++] = (byte & 0x20) > 0;
2584
- data[outIdx++] = (byte & 0x40) > 0;
2585
- data[outIdx++] = (byte & 0x80) > 0;
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;
2586
2754
  }
2587
2755
  const rem = length & 7;
2588
2756
  if (rem) {
2589
2757
  const byte = frame.data[byteIdx];
2590
- data[outIdx++] = (byte & 0x01) > 0;
2758
+ data[outIdx++] = byte & 0x01;
2591
2759
  if (rem > 1) {
2592
- data[outIdx++] = (byte & 0x02) > 0;
2760
+ data[outIdx++] = (byte >>> 1) & 0x01;
2593
2761
  }
2594
2762
  if (rem > 2) {
2595
- data[outIdx++] = (byte & 0x04) > 0;
2763
+ data[outIdx++] = (byte >>> 2) & 0x01;
2596
2764
  }
2597
2765
  if (rem > 3) {
2598
- data[outIdx++] = (byte & 0x08) > 0;
2766
+ data[outIdx++] = (byte >>> 3) & 0x01;
2599
2767
  }
2600
2768
  if (rem > 4) {
2601
- data[outIdx++] = (byte & 0x10) > 0;
2769
+ data[outIdx++] = (byte >>> 4) & 0x01;
2602
2770
  }
2603
2771
  if (rem > 5) {
2604
- data[outIdx++] = (byte & 0x20) > 0;
2772
+ data[outIdx++] = (byte >>> 5) & 0x01;
2605
2773
  }
2606
2774
  if (rem > 6) {
2607
- data[outIdx++] = (byte & 0x40) > 0;
2775
+ data[outIdx++] = (byte >>> 6) & 0x01;
2608
2776
  }
2609
2777
  }
2610
- // Mutate the frame in place rather than spread-copying — `frame` is freshly
2611
- // allocated per request and not retained anywhere else.
2612
2778
  frame.data = data;
2613
2779
  resolve(frame);
2614
2780
  }
@@ -2679,7 +2845,7 @@ class ModbusMaster extends EventEmitter {
2679
2845
  writeSingleCoil(unit, address, value, timeout = this.timeout) {
2680
2846
  const fc = exports.FunctionCode.WRITE_SINGLE_COIL;
2681
2847
  const bufferTx = Buffer.allocUnsafe(4);
2682
- const coilValue = value ? COIL_ON : COIL_OFF;
2848
+ const coilValue = value === 1 ? COIL_ON : COIL_OFF;
2683
2849
  // Inline big-endian writes — see writeFC1Or2 for the rationale.
2684
2850
  bufferTx[0] = (address >>> 8) & 0xff;
2685
2851
  bufferTx[1] = address & 0xff;
@@ -2739,26 +2905,51 @@ class ModbusMaster extends EventEmitter {
2739
2905
  writeFC15;
2740
2906
  writeMultipleCoils(unit, address, value, timeout = this.timeout) {
2741
2907
  const fc = exports.FunctionCode.WRITE_MULTIPLE_COILS;
2742
- const byteCount = Math.ceil(value.length / 8);
2743
- const bufferTx = Buffer.alloc(5 + byteCount);
2908
+ const len = value.length;
2909
+ const byteCount = (len + 7) >> 3;
2910
+ const bufferTx = Buffer.allocUnsafe(5 + byteCount);
2744
2911
  // Inline big-endian writes — see writeFC1Or2 for the rationale.
2745
2912
  bufferTx[0] = (address >>> 8) & 0xff;
2746
2913
  bufferTx[1] = address & 0xff;
2747
- bufferTx[2] = (value.length >>> 8) & 0xff;
2748
- bufferTx[3] = value.length & 0xff;
2914
+ bufferTx[2] = (len >>> 8) & 0xff;
2915
+ bufferTx[3] = len & 0xff;
2749
2916
  bufferTx[4] = byteCount;
2750
- let acc = 0;
2751
2917
  let out = 5;
2752
- for (let i = 0; i < value.length; i++) {
2753
- if (value[i]) {
2754
- acc |= 1 << (i & 7);
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;
2755
2940
  }
2756
- if ((i & 7) === 7) {
2757
- bufferTx[out++] = acc;
2758
- acc = 0;
2941
+ if (rem > 3) {
2942
+ acc |= (value[base + 3] & 1) << 3;
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;
2759
2952
  }
2760
- }
2761
- if ((value.length & 7) !== 0) {
2762
2953
  bufferTx[out] = acc;
2763
2954
  }
2764
2955
  return new Promise((resolve, reject) => {
@@ -2846,7 +3037,7 @@ class ModbusMaster extends EventEmitter {
2846
3037
  frame.data = {
2847
3038
  serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
2848
3039
  runIndicatorStatus: frame.data[runStatusIndex] === 0xff,
2849
- additionalData: Array.from(frame.data.subarray(runStatusIndex + 1)),
3040
+ additionalData: frame.data.subarray(runStatusIndex + 1),
2850
3041
  };
2851
3042
  resolve(frame);
2852
3043
  }
@@ -3152,6 +3343,7 @@ class ModbusSlave extends EventEmitter {
3152
3343
  appLayer.onFraming = NOOP;
3153
3344
  };
3154
3345
  const onFraming = (frame) => {
3346
+ this.emit('framing', frame, connection);
3155
3347
  if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
3156
3348
  return;
3157
3349
  }
@@ -3167,11 +3359,37 @@ class ModbusSlave extends EventEmitter {
3167
3359
  };
3168
3360
  appLayer.onFraming = onFraming;
3169
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);
3170
3382
  const cleanupClose = () => connection.off('close', onClose);
3171
3383
  const onClose = () => {
3172
3384
  cleanupFraming();
3385
+ cleanupFramingError();
3386
+ cleanupTx();
3387
+ cleanupRx();
3173
3388
  cleanupClose();
3174
3389
  this._cleanupFns.delete(cleanupFraming);
3390
+ this._cleanupFns.delete(cleanupFramingError);
3391
+ this._cleanupFns.delete(cleanupTx);
3392
+ this._cleanupFns.delete(cleanupRx);
3175
3393
  this._cleanupFns.delete(cleanupClose);
3176
3394
  this._appLayers.delete(appLayer);
3177
3395
  };
@@ -3200,7 +3418,7 @@ class ModbusSlave extends EventEmitter {
3200
3418
  return new RtuApplicationLayer('SLAVE', connection, {
3201
3419
  intervalBetweenFrames,
3202
3420
  interCharTimeout,
3203
- poolSize: this._protocol.opts?.poolSize,
3421
+ strictTiming: this._protocol.opts?.strictTiming,
3204
3422
  });
3205
3423
  }
3206
3424
  if (this._protocol.type === 'TCP') {
@@ -3232,67 +3450,43 @@ class ModbusSlave extends EventEmitter {
3232
3450
  const byteCount = (length + 7) >> 3;
3233
3451
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3234
3452
  pdu[0] = byteCount;
3235
- if (coils instanceof Uint8Array) {
3236
- // Branchless fast path `coils[i]` is already 0/1, no boolean
3237
- // coercion or conditional jumps. At max payload (2000 coils) this
3238
- // avoids 2000 branch-predictor slots and boolean-to-number casts.
3239
- let out = 1;
3240
- const fullBytes = length >> 3;
3241
- for (let i = 0; i < fullBytes; i++) {
3242
- const base = i << 3;
3243
- pdu[out++] =
3244
- (coils[base] & 1) |
3245
- ((coils[base + 1] & 1) << 1) |
3246
- ((coils[base + 2] & 1) << 2) |
3247
- ((coils[base + 3] & 1) << 3) |
3248
- ((coils[base + 4] & 1) << 4) |
3249
- ((coils[base + 5] & 1) << 5) |
3250
- ((coils[base + 6] & 1) << 6) |
3251
- ((coils[base + 7] & 1) << 7);
3453
+ let out = 1;
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);
3466
+ }
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;
3252
3473
  }
3253
- const rem = length & 7;
3254
- if (rem) {
3255
- const base = fullBytes << 3;
3256
- let acc = coils[base] & 1;
3257
- if (rem > 1) {
3258
- acc |= (coils[base + 1] & 1) << 1;
3259
- }
3260
- if (rem > 2) {
3261
- acc |= (coils[base + 2] & 1) << 2;
3262
- }
3263
- if (rem > 3) {
3264
- acc |= (coils[base + 3] & 1) << 3;
3265
- }
3266
- if (rem > 4) {
3267
- acc |= (coils[base + 4] & 1) << 4;
3268
- }
3269
- if (rem > 5) {
3270
- acc |= (coils[base + 5] & 1) << 5;
3271
- }
3272
- if (rem > 6) {
3273
- acc |= (coils[base + 6] & 1) << 6;
3274
- }
3275
- pdu[out] = acc;
3474
+ if (rem > 2) {
3475
+ acc |= (coils[base + 2] & 1) << 2;
3276
3476
  }
3277
- }
3278
- else {
3279
- // Fallback for boolean[] — accumulate into `acc` and write a full byte
3280
- // once each lane is finished. Saves N `|=` read-modify-writes on the
3281
- // output buffer.
3282
- let acc = 0;
3283
- let out = 1;
3284
- for (let i = 0; i < length; i++) {
3285
- if (coils[i]) {
3286
- acc |= 1 << (i & 7);
3287
- }
3288
- if ((i & 7) === 7) {
3289
- pdu[out++] = acc;
3290
- acc = 0;
3291
- }
3477
+ if (rem > 3) {
3478
+ acc |= (coils[base + 3] & 1) << 3;
3479
+ }
3480
+ if (rem > 4) {
3481
+ acc |= (coils[base + 4] & 1) << 4;
3292
3482
  }
3293
- if ((length & 7) !== 0) {
3294
- pdu[out] = acc;
3483
+ if (rem > 5) {
3484
+ acc |= (coils[base + 5] & 1) << 5;
3295
3485
  }
3486
+ if (rem > 6) {
3487
+ acc |= (coils[base + 6] & 1) << 6;
3488
+ }
3489
+ pdu[out] = acc;
3296
3490
  }
3297
3491
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3298
3492
  }
@@ -3324,61 +3518,43 @@ class ModbusSlave extends EventEmitter {
3324
3518
  const byteCount = (length + 7) >> 3;
3325
3519
  const pdu = Buffer.allocUnsafe(byteCount + 1);
3326
3520
  pdu[0] = byteCount;
3327
- if (discreteInputs instanceof Uint8Array) {
3328
- let out = 1;
3329
- const fullBytes = length >> 3;
3330
- for (let i = 0; i < fullBytes; i++) {
3331
- const base = i << 3;
3332
- pdu[out++] =
3333
- (discreteInputs[base] & 1) |
3334
- ((discreteInputs[base + 1] & 1) << 1) |
3335
- ((discreteInputs[base + 2] & 1) << 2) |
3336
- ((discreteInputs[base + 3] & 1) << 3) |
3337
- ((discreteInputs[base + 4] & 1) << 4) |
3338
- ((discreteInputs[base + 5] & 1) << 5) |
3339
- ((discreteInputs[base + 6] & 1) << 6) |
3340
- ((discreteInputs[base + 7] & 1) << 7);
3521
+ let out = 1;
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);
3534
+ }
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;
3341
3541
  }
3342
- const rem = length & 7;
3343
- if (rem) {
3344
- const base = fullBytes << 3;
3345
- let acc = discreteInputs[base] & 1;
3346
- if (rem > 1) {
3347
- acc |= (discreteInputs[base + 1] & 1) << 1;
3348
- }
3349
- if (rem > 2) {
3350
- acc |= (discreteInputs[base + 2] & 1) << 2;
3351
- }
3352
- if (rem > 3) {
3353
- acc |= (discreteInputs[base + 3] & 1) << 3;
3354
- }
3355
- if (rem > 4) {
3356
- acc |= (discreteInputs[base + 4] & 1) << 4;
3357
- }
3358
- if (rem > 5) {
3359
- acc |= (discreteInputs[base + 5] & 1) << 5;
3360
- }
3361
- if (rem > 6) {
3362
- acc |= (discreteInputs[base + 6] & 1) << 6;
3363
- }
3364
- pdu[out] = acc;
3542
+ if (rem > 2) {
3543
+ acc |= (discreteInputs[base + 2] & 1) << 2;
3365
3544
  }
3366
- }
3367
- else {
3368
- let acc = 0;
3369
- let out = 1;
3370
- for (let i = 0; i < length; i++) {
3371
- if (discreteInputs[i]) {
3372
- acc |= 1 << (i & 7);
3373
- }
3374
- if ((i & 7) === 7) {
3375
- pdu[out++] = acc;
3376
- acc = 0;
3377
- }
3545
+ if (rem > 3) {
3546
+ acc |= (discreteInputs[base + 3] & 1) << 3;
3378
3547
  }
3379
- if ((length & 7) !== 0) {
3380
- pdu[out] = acc;
3548
+ if (rem > 4) {
3549
+ acc |= (discreteInputs[base + 4] & 1) << 4;
3381
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
+ }
3557
+ pdu[out] = acc;
3382
3558
  }
3383
3559
  await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
3384
3560
  }
@@ -3482,7 +3658,7 @@ class ModbusSlave extends EventEmitter {
3482
3658
  return;
3483
3659
  }
3484
3660
  try {
3485
- await model.writeSingleCoil(address, value === COIL_ON);
3661
+ await model.writeSingleCoil(address, value === COIL_ON ? 1 : 0);
3486
3662
  await response(appLayer.encode(frame.unit, frame.fc, frame.data, frame.transaction));
3487
3663
  }
3488
3664
  catch (error) {
@@ -3524,7 +3700,7 @@ class ModbusSlave extends EventEmitter {
3524
3700
  const address = (frame.data[0] << 8) | frame.data[1];
3525
3701
  const length = (frame.data[2] << 8) | frame.data[3];
3526
3702
  const byteCount = frame.data[4];
3527
- 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) {
3528
3704
  await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.ILLEGAL_DATA_VALUE));
3529
3705
  return;
3530
3706
  }
@@ -3532,42 +3708,42 @@ class ModbusSlave extends EventEmitter {
3532
3708
  await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.ILLEGAL_DATA_ADDRESS));
3533
3709
  return;
3534
3710
  }
3535
- const value = new Array(length);
3711
+ const value = new Uint8Array(length);
3536
3712
  let byteIdx = 5;
3537
3713
  let outIdx = 0;
3538
3714
  const fullBytes = length >> 3;
3539
3715
  for (let b = 0; b < fullBytes; b++) {
3540
3716
  const byte = frame.data[byteIdx++];
3541
- value[outIdx++] = (byte & 0x01) > 0;
3542
- value[outIdx++] = (byte & 0x02) > 0;
3543
- value[outIdx++] = (byte & 0x04) > 0;
3544
- value[outIdx++] = (byte & 0x08) > 0;
3545
- value[outIdx++] = (byte & 0x10) > 0;
3546
- value[outIdx++] = (byte & 0x20) > 0;
3547
- value[outIdx++] = (byte & 0x40) > 0;
3548
- value[outIdx++] = (byte & 0x80) > 0;
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;
3549
3725
  }
3550
3726
  const rem = length & 7;
3551
3727
  if (rem) {
3552
3728
  const byte = frame.data[byteIdx];
3553
- value[outIdx++] = (byte & 0x01) > 0;
3729
+ value[outIdx++] = byte & 0x01;
3554
3730
  if (rem > 1) {
3555
- value[outIdx++] = (byte & 0x02) > 0;
3731
+ value[outIdx++] = (byte >>> 1) & 0x01;
3556
3732
  }
3557
3733
  if (rem > 2) {
3558
- value[outIdx++] = (byte & 0x04) > 0;
3734
+ value[outIdx++] = (byte >>> 2) & 0x01;
3559
3735
  }
3560
3736
  if (rem > 3) {
3561
- value[outIdx++] = (byte & 0x08) > 0;
3737
+ value[outIdx++] = (byte >>> 3) & 0x01;
3562
3738
  }
3563
3739
  if (rem > 4) {
3564
- value[outIdx++] = (byte & 0x10) > 0;
3740
+ value[outIdx++] = (byte >>> 4) & 0x01;
3565
3741
  }
3566
3742
  if (rem > 5) {
3567
- value[outIdx++] = (byte & 0x20) > 0;
3743
+ value[outIdx++] = (byte >>> 5) & 0x01;
3568
3744
  }
3569
3745
  if (rem > 6) {
3570
- value[outIdx++] = (byte & 0x40) > 0;
3746
+ value[outIdx++] = (byte >>> 6) & 0x01;
3571
3747
  }
3572
3748
  }
3573
3749
  try {
@@ -3636,21 +3812,41 @@ class ModbusSlave extends EventEmitter {
3636
3812
  return;
3637
3813
  }
3638
3814
  try {
3639
- const { serverId = [model.unit ?? 1], runIndicatorStatus = true, additionalData = [] } = await model.reportServerId();
3640
- const serverIdBytes = serverId;
3641
- 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;
3642
3821
  if (byteCount > 255) {
3643
3822
  await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.SERVER_DEVICE_FAILURE));
3644
3823
  return;
3645
3824
  }
3646
- const allBytes = [...serverIdBytes, runIndicatorStatus ? 0xff : 0x00, ...additionalData];
3647
- if (allBytes.some((b) => !isUint8(b))) {
3648
- await this.responseError(appLayer, frame, response, getErrorByCode(exports.ErrorCode.SERVER_DEVICE_FAILURE));
3649
- return;
3650
- }
3651
3825
  const data = Buffer.allocUnsafe(byteCount + 1);
3652
3826
  data[0] = byteCount;
3653
- 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
+ }
3654
3850
  await response(appLayer.encode(frame.unit, frame.fc, data, frame.transaction));
3655
3851
  }
3656
3852
  catch (error) {