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.cjs
CHANGED
|
@@ -234,7 +234,7 @@ function checkRange(value, range) {
|
|
|
234
234
|
return false;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
const
|
|
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
|
-
|
|
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 =
|
|
259
|
+
crc = CRC_TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
|
|
259
260
|
}
|
|
260
261
|
return crc;
|
|
261
262
|
}
|
|
262
|
-
|
|
263
263
|
/**
|
|
264
|
-
*
|
|
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
|
|
271
|
-
|
|
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(
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 +
|
|
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) +
|
|
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) +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
554
|
-
const
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
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,
|
|
1582
|
-
this.
|
|
1583
|
-
this.
|
|
1584
|
-
this.
|
|
1585
|
-
|
|
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
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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 (
|
|
1803
|
-
|
|
1804
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
1882
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
1969
|
-
const unitLo =
|
|
1970
|
-
const fcHi =
|
|
1971
|
-
const fcLo =
|
|
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 =
|
|
1980
|
-
const lrcLo =
|
|
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 =
|
|
1993
|
-
const lo =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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
|
|
2074
|
-
if (
|
|
2075
|
-
|
|
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
|
-
|
|
2078
|
-
|
|
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
|
-
|
|
2081
|
-
const
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
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 (
|
|
2087
|
-
|
|
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
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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++] =
|
|
2579
|
-
data[outIdx++] = (byte
|
|
2580
|
-
data[outIdx++] = (byte
|
|
2581
|
-
data[outIdx++] = (byte
|
|
2582
|
-
data[outIdx++] = (byte
|
|
2583
|
-
data[outIdx++] = (byte
|
|
2584
|
-
data[outIdx++] = (byte
|
|
2585
|
-
data[outIdx++] = (byte
|
|
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++] =
|
|
2758
|
+
data[outIdx++] = byte & 0x01;
|
|
2591
2759
|
if (rem > 1) {
|
|
2592
|
-
data[outIdx++] = (byte
|
|
2760
|
+
data[outIdx++] = (byte >>> 1) & 0x01;
|
|
2593
2761
|
}
|
|
2594
2762
|
if (rem > 2) {
|
|
2595
|
-
data[outIdx++] = (byte
|
|
2763
|
+
data[outIdx++] = (byte >>> 2) & 0x01;
|
|
2596
2764
|
}
|
|
2597
2765
|
if (rem > 3) {
|
|
2598
|
-
data[outIdx++] = (byte
|
|
2766
|
+
data[outIdx++] = (byte >>> 3) & 0x01;
|
|
2599
2767
|
}
|
|
2600
2768
|
if (rem > 4) {
|
|
2601
|
-
data[outIdx++] = (byte
|
|
2769
|
+
data[outIdx++] = (byte >>> 4) & 0x01;
|
|
2602
2770
|
}
|
|
2603
2771
|
if (rem > 5) {
|
|
2604
|
-
data[outIdx++] = (byte
|
|
2772
|
+
data[outIdx++] = (byte >>> 5) & 0x01;
|
|
2605
2773
|
}
|
|
2606
2774
|
if (rem > 6) {
|
|
2607
|
-
data[outIdx++] = (byte
|
|
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
|
|
2743
|
-
const
|
|
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] = (
|
|
2748
|
-
bufferTx[3] =
|
|
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
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
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 (
|
|
2757
|
-
|
|
2758
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
(coils[base] & 1) |
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
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
|
-
|
|
3254
|
-
|
|
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
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
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 (
|
|
3294
|
-
|
|
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
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
(discreteInputs[base] & 1) |
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
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
|
-
|
|
3343
|
-
|
|
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
|
-
|
|
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 (
|
|
3380
|
-
|
|
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 !==
|
|
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
|
|
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++] =
|
|
3542
|
-
value[outIdx++] = (byte
|
|
3543
|
-
value[outIdx++] = (byte
|
|
3544
|
-
value[outIdx++] = (byte
|
|
3545
|
-
value[outIdx++] = (byte
|
|
3546
|
-
value[outIdx++] = (byte
|
|
3547
|
-
value[outIdx++] = (byte
|
|
3548
|
-
value[outIdx++] = (byte
|
|
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++] =
|
|
3729
|
+
value[outIdx++] = byte & 0x01;
|
|
3554
3730
|
if (rem > 1) {
|
|
3555
|
-
value[outIdx++] = (byte
|
|
3731
|
+
value[outIdx++] = (byte >>> 1) & 0x01;
|
|
3556
3732
|
}
|
|
3557
3733
|
if (rem > 2) {
|
|
3558
|
-
value[outIdx++] = (byte
|
|
3734
|
+
value[outIdx++] = (byte >>> 2) & 0x01;
|
|
3559
3735
|
}
|
|
3560
3736
|
if (rem > 3) {
|
|
3561
|
-
value[outIdx++] = (byte
|
|
3737
|
+
value[outIdx++] = (byte >>> 3) & 0x01;
|
|
3562
3738
|
}
|
|
3563
3739
|
if (rem > 4) {
|
|
3564
|
-
value[outIdx++] = (byte
|
|
3740
|
+
value[outIdx++] = (byte >>> 4) & 0x01;
|
|
3565
3741
|
}
|
|
3566
3742
|
if (rem > 5) {
|
|
3567
|
-
value[outIdx++] = (byte
|
|
3743
|
+
value[outIdx++] = (byte >>> 5) & 0x01;
|
|
3568
3744
|
}
|
|
3569
3745
|
if (rem > 6) {
|
|
3570
|
-
value[outIdx++] = (byte
|
|
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
|
|
3640
|
-
const
|
|
3641
|
-
const
|
|
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
|
-
|
|
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) {
|