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/README.md +177 -131
- package/README.zh-CN.md +177 -130
- package/dist/index.cjs +715 -519
- package/dist/index.d.ts +86 -61
- package/dist/index.mjs +715 -519
- package/dist/utils.cjs +53 -25
- package/dist/utils.d.ts +15 -14
- package/dist/utils.mjs +49 -24
- package/package.json +3 -9
package/dist/index.mjs
CHANGED
|
@@ -232,7 +232,7 @@ function checkRange(value, range) {
|
|
|
232
232
|
return false;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
const
|
|
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
|
-
|
|
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 =
|
|
257
|
+
crc = CRC_TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
|
|
257
258
|
}
|
|
258
259
|
return crc;
|
|
259
260
|
}
|
|
260
|
-
|
|
261
261
|
/**
|
|
262
|
-
*
|
|
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
|
|
269
|
-
|
|
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(
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 +
|
|
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) +
|
|
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) +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
552
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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,
|
|
1580
|
-
this.
|
|
1581
|
-
this.
|
|
1582
|
-
this.
|
|
1583
|
-
|
|
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
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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 (
|
|
1801
|
-
|
|
1802
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
1880
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
1967
|
-
const unitLo =
|
|
1968
|
-
const fcHi =
|
|
1969
|
-
const fcLo =
|
|
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 =
|
|
1978
|
-
const lrcLo =
|
|
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 =
|
|
1991
|
-
const lo =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
|
2072
|
-
if (
|
|
2073
|
-
|
|
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
|
-
|
|
2076
|
-
|
|
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
|
-
|
|
2079
|
-
const
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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 (
|
|
2085
|
-
|
|
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
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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++] =
|
|
2577
|
-
data[outIdx++] = (byte
|
|
2578
|
-
data[outIdx++] = (byte
|
|
2579
|
-
data[outIdx++] = (byte
|
|
2580
|
-
data[outIdx++] = (byte
|
|
2581
|
-
data[outIdx++] = (byte
|
|
2582
|
-
data[outIdx++] = (byte
|
|
2583
|
-
data[outIdx++] = (byte
|
|
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++] =
|
|
2756
|
+
data[outIdx++] = byte & 0x01;
|
|
2589
2757
|
if (rem > 1) {
|
|
2590
|
-
data[outIdx++] = (byte
|
|
2758
|
+
data[outIdx++] = (byte >>> 1) & 0x01;
|
|
2591
2759
|
}
|
|
2592
2760
|
if (rem > 2) {
|
|
2593
|
-
data[outIdx++] = (byte
|
|
2761
|
+
data[outIdx++] = (byte >>> 2) & 0x01;
|
|
2594
2762
|
}
|
|
2595
2763
|
if (rem > 3) {
|
|
2596
|
-
data[outIdx++] = (byte
|
|
2764
|
+
data[outIdx++] = (byte >>> 3) & 0x01;
|
|
2597
2765
|
}
|
|
2598
2766
|
if (rem > 4) {
|
|
2599
|
-
data[outIdx++] = (byte
|
|
2767
|
+
data[outIdx++] = (byte >>> 4) & 0x01;
|
|
2600
2768
|
}
|
|
2601
2769
|
if (rem > 5) {
|
|
2602
|
-
data[outIdx++] = (byte
|
|
2770
|
+
data[outIdx++] = (byte >>> 5) & 0x01;
|
|
2603
2771
|
}
|
|
2604
2772
|
if (rem > 6) {
|
|
2605
|
-
data[outIdx++] = (byte
|
|
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
|
|
2741
|
-
const
|
|
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] = (
|
|
2746
|
-
bufferTx[3] =
|
|
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
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
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 (
|
|
2755
|
-
|
|
2756
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
(coils[base] & 1) |
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
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
|
-
|
|
3252
|
-
|
|
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
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
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 (
|
|
3292
|
-
|
|
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
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
(discreteInputs[base] & 1) |
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
|
|
3341
|
-
|
|
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
|
-
|
|
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 (
|
|
3378
|
-
|
|
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 !==
|
|
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
|
|
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++] =
|
|
3540
|
-
value[outIdx++] = (byte
|
|
3541
|
-
value[outIdx++] = (byte
|
|
3542
|
-
value[outIdx++] = (byte
|
|
3543
|
-
value[outIdx++] = (byte
|
|
3544
|
-
value[outIdx++] = (byte
|
|
3545
|
-
value[outIdx++] = (byte
|
|
3546
|
-
value[outIdx++] = (byte
|
|
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++] =
|
|
3727
|
+
value[outIdx++] = byte & 0x01;
|
|
3552
3728
|
if (rem > 1) {
|
|
3553
|
-
value[outIdx++] = (byte
|
|
3729
|
+
value[outIdx++] = (byte >>> 1) & 0x01;
|
|
3554
3730
|
}
|
|
3555
3731
|
if (rem > 2) {
|
|
3556
|
-
value[outIdx++] = (byte
|
|
3732
|
+
value[outIdx++] = (byte >>> 2) & 0x01;
|
|
3557
3733
|
}
|
|
3558
3734
|
if (rem > 3) {
|
|
3559
|
-
value[outIdx++] = (byte
|
|
3735
|
+
value[outIdx++] = (byte >>> 3) & 0x01;
|
|
3560
3736
|
}
|
|
3561
3737
|
if (rem > 4) {
|
|
3562
|
-
value[outIdx++] = (byte
|
|
3738
|
+
value[outIdx++] = (byte >>> 4) & 0x01;
|
|
3563
3739
|
}
|
|
3564
3740
|
if (rem > 5) {
|
|
3565
|
-
value[outIdx++] = (byte
|
|
3741
|
+
value[outIdx++] = (byte >>> 5) & 0x01;
|
|
3566
3742
|
}
|
|
3567
3743
|
if (rem > 6) {
|
|
3568
|
-
value[outIdx++] = (byte
|
|
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
|
|
3638
|
-
const
|
|
3639
|
-
const
|
|
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
|
-
|
|
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) {
|