njs-modbus 3.1.1 → 3.2.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 +20 -0
- package/README.zh-CN.md +20 -0
- package/dist/index.cjs +878 -384
- package/dist/index.d.ts +87 -25
- package/dist/index.mjs +878 -385
- package/dist/utils.cjs +439 -0
- package/dist/utils.d.ts +161 -0
- package/dist/utils.mjs +425 -0
- package/package.json +15 -2
- package/dist/src/error-code.d.ts +0 -17
- package/dist/src/index.d.ts +0 -7
- package/dist/src/layers/application/abstract-application-layer.d.ts +0 -26
- package/dist/src/layers/application/ascii-application-layer.d.ts +0 -23
- package/dist/src/layers/application/index.d.ts +0 -6
- package/dist/src/layers/application/rtu-application-layer.d.ts +0 -34
- package/dist/src/layers/application/tcp-application-layer.d.ts +0 -16
- package/dist/src/layers/physical/abstract-physical-layer.d.ts +0 -50
- package/dist/src/layers/physical/index.d.ts +0 -12
- package/dist/src/layers/physical/serial-physical-layer.d.ts +0 -70
- package/dist/src/layers/physical/tcp-client-physical-layer.d.ts +0 -20
- package/dist/src/layers/physical/tcp-physical-connection.d.ts +0 -16
- package/dist/src/layers/physical/tcp-server-physical-layer.d.ts +0 -29
- package/dist/src/layers/physical/udp-client-physical-layer.d.ts +0 -34
- package/dist/src/layers/physical/udp-server-physical-layer.d.ts +0 -51
- package/dist/src/layers/physical/utils.d.ts +0 -39
- package/dist/src/layers/physical/vars.d.ts +0 -11
- package/dist/src/master/index.d.ts +0 -3
- package/dist/src/master/master-session.d.ts +0 -18
- package/dist/src/master/master.d.ts +0 -140
- package/dist/src/slave/index.d.ts +0 -2
- package/dist/src/slave/slave.d.ts +0 -119
- package/dist/src/types.d.ts +0 -54
- package/dist/src/utils/bitsToMs.d.ts +0 -13
- package/dist/src/utils/callback.d.ts +0 -8
- package/dist/src/utils/checkRange.d.ts +0 -1
- package/dist/src/utils/crc.d.ts +0 -1
- package/dist/src/utils/index.d.ts +0 -11
- package/dist/src/utils/isUint8.d.ts +0 -8
- package/dist/src/utils/lrc.d.ts +0 -1
- package/dist/src/utils/predictRtuFrameLength.d.ts +0 -17
- package/dist/src/utils/promisify-cb.d.ts +0 -4
- package/dist/src/utils/rtu-timing.d.ts +0 -63
- package/dist/src/utils/whitelist.d.ts +0 -11
- package/dist/src/vars.d.ts +0 -49
package/dist/index.cjs
CHANGED
|
@@ -91,6 +91,10 @@ exports.ConformityLevel = void 0;
|
|
|
91
91
|
})(exports.ConformityLevel || (exports.ConformityLevel = {}));
|
|
92
92
|
/** Shared empty Buffer to avoid repeated allocations. */
|
|
93
93
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
94
|
+
/** Shared no-op function to avoid repeated allocations. */
|
|
95
|
+
const NOOP = () => {
|
|
96
|
+
/* no-op */
|
|
97
|
+
};
|
|
94
98
|
/** Modbus V1.1b3 PDU quantity limits. */
|
|
95
99
|
const LIMITS = {
|
|
96
100
|
READ_COILS_MIN: 0x0001,
|
|
@@ -232,8 +236,12 @@ function isUint8(n) {
|
|
|
232
236
|
return Number.isInteger(n) && n >= 0 && n <= 255;
|
|
233
237
|
}
|
|
234
238
|
|
|
235
|
-
function lrc(data) {
|
|
236
|
-
|
|
239
|
+
function lrc(data, start = 0, end = data.length) {
|
|
240
|
+
let sum = 0;
|
|
241
|
+
for (let i = start; i < end; i++) {
|
|
242
|
+
sum += data[i];
|
|
243
|
+
}
|
|
244
|
+
return (~sum + 1) & 0xff;
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
const REQUEST_FIXED_LENGTHS = {
|
|
@@ -283,11 +291,11 @@ const PREDICT_UNKNOWN = -1;
|
|
|
283
291
|
* the framing layer must defer to a registered `CustomFunctionCode` or treat
|
|
284
292
|
* this as a framing error.
|
|
285
293
|
*/
|
|
286
|
-
function predictRtuFrameLength(buffer, isResponse) {
|
|
287
|
-
if (
|
|
294
|
+
function predictRtuFrameLength(buffer, start, end, isResponse) {
|
|
295
|
+
if (end - start < 2) {
|
|
288
296
|
return PREDICT_NEED_MORE;
|
|
289
297
|
}
|
|
290
|
-
const fc = buffer[1];
|
|
298
|
+
const fc = buffer[start + 1];
|
|
291
299
|
if (isResponse && (fc & EXCEPTION_OFFSET) !== 0) {
|
|
292
300
|
return 5;
|
|
293
301
|
}
|
|
@@ -297,13 +305,13 @@ function predictRtuFrameLength(buffer, isResponse) {
|
|
|
297
305
|
}
|
|
298
306
|
const bc = (isResponse ? RESPONSE_BYTE_COUNT : REQUEST_BYTE_COUNT)[fc];
|
|
299
307
|
if (bc !== undefined) {
|
|
300
|
-
if (
|
|
308
|
+
if (end - start <= bc.offset) {
|
|
301
309
|
return PREDICT_NEED_MORE;
|
|
302
310
|
}
|
|
303
|
-
return bc.extra + buffer[bc.offset];
|
|
311
|
+
return bc.extra + buffer[start + bc.offset];
|
|
304
312
|
}
|
|
305
313
|
if (isResponse && fc === exports.FunctionCode.READ_DEVICE_IDENTIFICATION) {
|
|
306
|
-
return predictFc43_14Response(buffer);
|
|
314
|
+
return predictFc43_14Response(buffer, start, end);
|
|
307
315
|
}
|
|
308
316
|
return PREDICT_UNKNOWN;
|
|
309
317
|
}
|
|
@@ -316,23 +324,23 @@ function predictRtuFrameLength(buffer, isResponse) {
|
|
|
316
324
|
* [objId(1) objLen(1) objData(objLen)] × numObjs
|
|
317
325
|
* CRC(2)
|
|
318
326
|
*/
|
|
319
|
-
function predictFc43_14Response(buffer) {
|
|
320
|
-
if (
|
|
327
|
+
function predictFc43_14Response(buffer, start, end) {
|
|
328
|
+
if (end - start < 8) {
|
|
321
329
|
return PREDICT_NEED_MORE;
|
|
322
330
|
}
|
|
323
|
-
if (buffer[2] !== MEI_READ_DEVICE_ID) {
|
|
331
|
+
if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
|
|
324
332
|
return PREDICT_UNKNOWN;
|
|
325
333
|
}
|
|
326
|
-
const numObjs = buffer[7];
|
|
327
|
-
let
|
|
334
|
+
const numObjs = buffer[start + 7];
|
|
335
|
+
let cursor = start + 8;
|
|
328
336
|
for (let i = 0; i < numObjs; i++) {
|
|
329
|
-
if (
|
|
337
|
+
if (end < cursor + 2) {
|
|
330
338
|
return PREDICT_NEED_MORE;
|
|
331
339
|
}
|
|
332
|
-
const objLen = buffer[
|
|
333
|
-
|
|
340
|
+
const objLen = buffer[cursor + 1];
|
|
341
|
+
cursor += 2 + objLen;
|
|
334
342
|
}
|
|
335
|
-
return
|
|
343
|
+
return cursor - start + 2;
|
|
336
344
|
}
|
|
337
345
|
|
|
338
346
|
/**
|
|
@@ -397,6 +405,107 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
397
405
|
return { intervalBetweenFrames, interCharTimeout };
|
|
398
406
|
}
|
|
399
407
|
|
|
408
|
+
/** @internal
|
|
409
|
+
* Zero-allocation binary min-heap for coalescing per-request timeouts.
|
|
410
|
+
*
|
|
411
|
+
* Uses two parallel numeric arrays (no object allocation per entry).
|
|
412
|
+
* Lazy deletion: callers never remove from the heap; expired entries
|
|
413
|
+
* are silently dropped when they surface at the top.
|
|
414
|
+
*/
|
|
415
|
+
class TimerHeap {
|
|
416
|
+
_deadlines = [];
|
|
417
|
+
_ids = [];
|
|
418
|
+
_timer = null;
|
|
419
|
+
_onFire;
|
|
420
|
+
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
421
|
+
_boundTick;
|
|
422
|
+
constructor(onFire) {
|
|
423
|
+
this._onFire = onFire;
|
|
424
|
+
this._boundTick = this._onTick.bind(this);
|
|
425
|
+
}
|
|
426
|
+
/** Number of pending timers in the heap. */
|
|
427
|
+
get size() {
|
|
428
|
+
return this._deadlines.length;
|
|
429
|
+
}
|
|
430
|
+
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
431
|
+
add(id, ms) {
|
|
432
|
+
const deadline = performance.now() + ms;
|
|
433
|
+
let i = this._deadlines.length;
|
|
434
|
+
this._deadlines.push(deadline);
|
|
435
|
+
this._ids.push(id);
|
|
436
|
+
// sift up
|
|
437
|
+
while (i > 0) {
|
|
438
|
+
const p = (i - 1) >> 1;
|
|
439
|
+
if (this._deadlines[p] <= deadline)
|
|
440
|
+
break;
|
|
441
|
+
this._deadlines[i] = this._deadlines[p];
|
|
442
|
+
this._ids[i] = this._ids[p];
|
|
443
|
+
i = p;
|
|
444
|
+
}
|
|
445
|
+
this._deadlines[i] = deadline;
|
|
446
|
+
this._ids[i] = id;
|
|
447
|
+
// Only reschedule when the new entry became the heap top.
|
|
448
|
+
if (i === 0)
|
|
449
|
+
this._refresh();
|
|
450
|
+
}
|
|
451
|
+
/** Dispose without firing callbacks. */
|
|
452
|
+
clear() {
|
|
453
|
+
if (this._timer) {
|
|
454
|
+
clearTimeout(this._timer);
|
|
455
|
+
this._timer = null;
|
|
456
|
+
}
|
|
457
|
+
this._deadlines.length = 0;
|
|
458
|
+
this._ids.length = 0;
|
|
459
|
+
}
|
|
460
|
+
_refresh() {
|
|
461
|
+
if (this._timer) {
|
|
462
|
+
clearTimeout(this._timer);
|
|
463
|
+
this._timer = null;
|
|
464
|
+
}
|
|
465
|
+
if (this._deadlines.length === 0)
|
|
466
|
+
return;
|
|
467
|
+
const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
|
|
468
|
+
this._timer = setTimeout(this._boundTick, delay);
|
|
469
|
+
}
|
|
470
|
+
_onTick() {
|
|
471
|
+
this._timer = null;
|
|
472
|
+
const now = performance.now();
|
|
473
|
+
while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
|
|
474
|
+
const id = this._pop();
|
|
475
|
+
this._onFire(id);
|
|
476
|
+
}
|
|
477
|
+
this._refresh();
|
|
478
|
+
}
|
|
479
|
+
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
480
|
+
_pop() {
|
|
481
|
+
const topId = this._ids[0];
|
|
482
|
+
const lastId = this._ids.pop();
|
|
483
|
+
const lastDeadline = this._deadlines.pop();
|
|
484
|
+
const n = this._deadlines.length;
|
|
485
|
+
if (n > 0) {
|
|
486
|
+
let i = 0;
|
|
487
|
+
// sift down
|
|
488
|
+
while (true) {
|
|
489
|
+
let min = i;
|
|
490
|
+
const l = i * 2 + 1;
|
|
491
|
+
const r = l + 1;
|
|
492
|
+
if (l < n && this._deadlines[l] < this._deadlines[min])
|
|
493
|
+
min = l;
|
|
494
|
+
if (r < n && this._deadlines[r] < this._deadlines[min])
|
|
495
|
+
min = r;
|
|
496
|
+
if (min === i)
|
|
497
|
+
break;
|
|
498
|
+
this._deadlines[i] = this._deadlines[min];
|
|
499
|
+
this._ids[i] = this._ids[min];
|
|
500
|
+
i = min;
|
|
501
|
+
}
|
|
502
|
+
this._deadlines[i] = lastDeadline;
|
|
503
|
+
this._ids[i] = lastId;
|
|
504
|
+
}
|
|
505
|
+
return topId;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
400
509
|
/**
|
|
401
510
|
* Normalize an IP address by stripping the IPv4-mapped IPv6 prefix.
|
|
402
511
|
* This ensures consistent comparison of addresses like `::ffff:192.168.1.1`.
|
|
@@ -1341,14 +1450,16 @@ function createPhysicalLayer(config) {
|
|
|
1341
1450
|
* established and discarded when the connection closes. Subclasses implement
|
|
1342
1451
|
* ASCII, RTU, or TCP framing rules.
|
|
1343
1452
|
*/
|
|
1344
|
-
class AbstractApplicationLayer
|
|
1453
|
+
class AbstractApplicationLayer {
|
|
1454
|
+
/** Called when a complete frame is decoded. Defaults to no-op. */
|
|
1455
|
+
onFraming = NOOP;
|
|
1456
|
+
/** Called when a framing error is detected. Defaults to no-op. */
|
|
1457
|
+
onFramingError = NOOP;
|
|
1345
1458
|
flush() {
|
|
1346
1459
|
// no-op — override in subclasses
|
|
1347
1460
|
}
|
|
1348
|
-
addCustomFunctionCode(cfc) {
|
|
1349
|
-
}
|
|
1350
|
-
removeCustomFunctionCode(fc) {
|
|
1351
|
-
}
|
|
1461
|
+
addCustomFunctionCode(cfc) { }
|
|
1462
|
+
removeCustomFunctionCode(fc) { }
|
|
1352
1463
|
}
|
|
1353
1464
|
|
|
1354
1465
|
const MAX_FRAME_LENGTH = 256;
|
|
@@ -1362,7 +1473,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1362
1473
|
_threePointFiveT;
|
|
1363
1474
|
_onePointFiveT;
|
|
1364
1475
|
_customFunctionCodes = new Map();
|
|
1365
|
-
|
|
1476
|
+
_cleanupCbs = [];
|
|
1366
1477
|
get connection() {
|
|
1367
1478
|
return this._connection;
|
|
1368
1479
|
}
|
|
@@ -1378,7 +1489,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1378
1489
|
const onData = (data) => {
|
|
1379
1490
|
const state = this._state;
|
|
1380
1491
|
if (state.t15Expired && state.end > state.start) {
|
|
1381
|
-
this.
|
|
1492
|
+
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1382
1493
|
state.start = 0;
|
|
1383
1494
|
state.end = 0;
|
|
1384
1495
|
}
|
|
@@ -1420,7 +1531,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1420
1531
|
// flushBuffer freed nothing — the entire pool is unparseable
|
|
1421
1532
|
// residue (typically a misconfigured poolSize for the wire's
|
|
1422
1533
|
// frame size). Hard reset; we cannot recover automatically.
|
|
1423
|
-
this.
|
|
1534
|
+
this.onFramingError(new Error('Frame buffer exhausted before complete frame received'));
|
|
1424
1535
|
currentState.start = 0;
|
|
1425
1536
|
currentState.end = 0;
|
|
1426
1537
|
currentState.t15Expired = false;
|
|
@@ -1456,16 +1567,16 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1456
1567
|
}
|
|
1457
1568
|
};
|
|
1458
1569
|
connection.on('data', onData);
|
|
1459
|
-
this.
|
|
1570
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1460
1571
|
const onClose = () => {
|
|
1461
|
-
for (const fn of this.
|
|
1572
|
+
for (const fn of this._cleanupCbs) {
|
|
1462
1573
|
fn();
|
|
1463
1574
|
}
|
|
1464
|
-
this.
|
|
1575
|
+
this._cleanupCbs.length = 0;
|
|
1465
1576
|
this.clearStateTimers();
|
|
1466
1577
|
};
|
|
1467
1578
|
connection.on('close', onClose);
|
|
1468
|
-
this.
|
|
1579
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1469
1580
|
}
|
|
1470
1581
|
clearStateTimers() {
|
|
1471
1582
|
const state = this._state;
|
|
@@ -1478,34 +1589,38 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1478
1589
|
state.interCharTimer = undefined;
|
|
1479
1590
|
}
|
|
1480
1591
|
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Shared handler for every "frame is not yet complete" exit in `flushBuffer`.
|
|
1594
|
+
* Returns `true` when the caller should `return` (strict reset), `false` to
|
|
1595
|
+
* `break` the parse loop. Hot path never reaches here — only error/incomplete
|
|
1596
|
+
* edges. Extracted as a method so it is not recreated on every `flushBuffer`
|
|
1597
|
+
* call.
|
|
1598
|
+
*/
|
|
1599
|
+
_handleIncomplete(state, strict) {
|
|
1600
|
+
if (strict) {
|
|
1601
|
+
this.onFramingError(new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
|
|
1602
|
+
state.start = 0;
|
|
1603
|
+
state.end = 0;
|
|
1604
|
+
state.t15Expired = false;
|
|
1605
|
+
return true;
|
|
1606
|
+
}
|
|
1607
|
+
if (state.t15Expired) {
|
|
1608
|
+
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1609
|
+
state.start = 0;
|
|
1610
|
+
state.end = 0;
|
|
1611
|
+
state.t15Expired = false;
|
|
1612
|
+
}
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1481
1615
|
flushBuffer(strict) {
|
|
1482
1616
|
const state = this._state;
|
|
1483
1617
|
const isResponse = this.ROLE === 'MASTER';
|
|
1484
1618
|
const pool = state.pool;
|
|
1485
1619
|
const customFCs = this._customFunctionCodes;
|
|
1486
|
-
// Shared handler for every "frame is not yet complete" exit. Returns true
|
|
1487
|
-
// when the caller should `return` (strict reset), false to `break` the
|
|
1488
|
-
// parse loop. Hot path never reaches here — only error/incomplete edges.
|
|
1489
|
-
const handleIncomplete = () => {
|
|
1490
|
-
if (strict) {
|
|
1491
|
-
this.emit('framing-error', new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
|
|
1492
|
-
state.start = 0;
|
|
1493
|
-
state.end = 0;
|
|
1494
|
-
state.t15Expired = false;
|
|
1495
|
-
return true;
|
|
1496
|
-
}
|
|
1497
|
-
if (state.t15Expired) {
|
|
1498
|
-
this.emit('framing-error', new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1499
|
-
state.start = 0;
|
|
1500
|
-
state.end = 0;
|
|
1501
|
-
state.t15Expired = false;
|
|
1502
|
-
}
|
|
1503
|
-
return false;
|
|
1504
|
-
};
|
|
1505
1620
|
while (state.end - state.start > 0) {
|
|
1506
1621
|
const available = state.end - state.start;
|
|
1507
1622
|
if (available < MIN_FRAME_LENGTH) {
|
|
1508
|
-
if (
|
|
1623
|
+
if (this._handleIncomplete(state, strict))
|
|
1509
1624
|
return;
|
|
1510
1625
|
break;
|
|
1511
1626
|
}
|
|
@@ -1514,7 +1629,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1514
1629
|
let expected;
|
|
1515
1630
|
if (cfc) {
|
|
1516
1631
|
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1517
|
-
const predicted = predictor(pool
|
|
1632
|
+
const predicted = predictor(pool, state.start, state.end);
|
|
1518
1633
|
// Normalize custom predictor's `null` to the std sentinel so both
|
|
1519
1634
|
// paths share the same NEED_MORE tail below.
|
|
1520
1635
|
expected = predicted ?? PREDICT_NEED_MORE;
|
|
@@ -1522,9 +1637,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1522
1637
|
else {
|
|
1523
1638
|
// Standard FC path. predictRtuFrameLength uses sentinel returns to
|
|
1524
1639
|
// avoid per-call object allocation on the decode hot path.
|
|
1525
|
-
expected = predictRtuFrameLength(pool
|
|
1640
|
+
expected = predictRtuFrameLength(pool, state.start, state.end, isResponse);
|
|
1526
1641
|
if (expected === PREDICT_UNKNOWN) {
|
|
1527
|
-
this.
|
|
1642
|
+
this.onFramingError(new Error(`Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`));
|
|
1528
1643
|
state.start = 0;
|
|
1529
1644
|
state.end = 0;
|
|
1530
1645
|
return;
|
|
@@ -1535,12 +1650,12 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1535
1650
|
state.start += 1;
|
|
1536
1651
|
continue;
|
|
1537
1652
|
}
|
|
1538
|
-
if (
|
|
1653
|
+
if (this._handleIncomplete(state, strict))
|
|
1539
1654
|
return;
|
|
1540
1655
|
break;
|
|
1541
1656
|
}
|
|
1542
1657
|
if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
|
|
1543
|
-
this.
|
|
1658
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1544
1659
|
state.start = 0;
|
|
1545
1660
|
state.end = 0;
|
|
1546
1661
|
return;
|
|
@@ -1550,7 +1665,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1550
1665
|
state.start += 1;
|
|
1551
1666
|
continue;
|
|
1552
1667
|
}
|
|
1553
|
-
if (
|
|
1668
|
+
if (this._handleIncomplete(state, strict))
|
|
1554
1669
|
return;
|
|
1555
1670
|
break;
|
|
1556
1671
|
}
|
|
@@ -1561,7 +1676,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1561
1676
|
const actualCrc = crc(pool, crcStart, crcEnd);
|
|
1562
1677
|
if (expectedCrc !== actualCrc) {
|
|
1563
1678
|
if (strict) {
|
|
1564
|
-
this.
|
|
1679
|
+
this.onFramingError(new Error('CRC mismatch'));
|
|
1565
1680
|
state.start = 0;
|
|
1566
1681
|
state.end = 0;
|
|
1567
1682
|
state.t15Expired = false;
|
|
@@ -1582,7 +1697,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1582
1697
|
data: frameBuf.subarray(2, expected - 2),
|
|
1583
1698
|
buffer: frameBuf,
|
|
1584
1699
|
};
|
|
1585
|
-
this.
|
|
1700
|
+
this.onFraming(frame);
|
|
1586
1701
|
}
|
|
1587
1702
|
if (state.start > 0) {
|
|
1588
1703
|
if (state.start < state.end) {
|
|
@@ -1609,11 +1724,21 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1609
1724
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1610
1725
|
encode(unit, fc, data, transaction) {
|
|
1611
1726
|
const buffer = Buffer.allocUnsafe(data.length + 4);
|
|
1612
|
-
|
|
1613
|
-
buffer
|
|
1614
|
-
buffer
|
|
1727
|
+
// Inline header — direct typed-array stores skip Buffer's per-call checks.
|
|
1728
|
+
buffer[0] = unit;
|
|
1729
|
+
buffer[1] = fc;
|
|
1730
|
+
if (data.length <= 16) {
|
|
1731
|
+
for (let i = 0; i < data.length; i++)
|
|
1732
|
+
buffer[2 + i] = data[i];
|
|
1733
|
+
}
|
|
1734
|
+
else {
|
|
1735
|
+
buffer.set(data, 2);
|
|
1736
|
+
}
|
|
1615
1737
|
const crcEnd = buffer.length - 2;
|
|
1616
|
-
|
|
1738
|
+
const c = crc(buffer, 0, crcEnd);
|
|
1739
|
+
// Little-endian inline write of CRC trailer.
|
|
1740
|
+
buffer[crcEnd] = c & 0xff;
|
|
1741
|
+
buffer[crcEnd + 1] = (c >>> 8) & 0xff;
|
|
1617
1742
|
return buffer;
|
|
1618
1743
|
}
|
|
1619
1744
|
}
|
|
@@ -1644,8 +1769,8 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1644
1769
|
ROLE;
|
|
1645
1770
|
lenientHex;
|
|
1646
1771
|
_connection;
|
|
1647
|
-
_state = { status: 'idle', frame:
|
|
1648
|
-
|
|
1772
|
+
_state = { status: 'idle', frame: new Uint8Array(MAX_ASCII_PAYLOAD), frameLen: 0 };
|
|
1773
|
+
_cleanupCbs = [];
|
|
1649
1774
|
get connection() {
|
|
1650
1775
|
return this._connection;
|
|
1651
1776
|
}
|
|
@@ -1669,108 +1794,138 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1669
1794
|
};
|
|
1670
1795
|
const onData = (data) => {
|
|
1671
1796
|
const state = this._state;
|
|
1672
|
-
data.
|
|
1797
|
+
for (let i = 0; i < data.length; i++) {
|
|
1798
|
+
const value = data[i];
|
|
1673
1799
|
switch (state.status) {
|
|
1674
1800
|
case 'idle': {
|
|
1675
1801
|
if (value === CHAR_CODE.COLON) {
|
|
1676
1802
|
state.status = 'reception';
|
|
1677
|
-
state.
|
|
1803
|
+
state.frameLen = 0;
|
|
1678
1804
|
}
|
|
1679
1805
|
break;
|
|
1680
1806
|
}
|
|
1681
1807
|
case 'reception': {
|
|
1682
1808
|
if (value === CHAR_CODE.COLON) {
|
|
1683
|
-
state.
|
|
1809
|
+
state.frameLen = 0;
|
|
1684
1810
|
}
|
|
1685
1811
|
else if (value === CHAR_CODE.CR) {
|
|
1686
1812
|
state.status = 'waiting end';
|
|
1687
1813
|
}
|
|
1688
|
-
else if (state.
|
|
1814
|
+
else if (state.frameLen >= MAX_ASCII_PAYLOAD) {
|
|
1689
1815
|
state.status = 'idle';
|
|
1690
|
-
state.
|
|
1691
|
-
this.
|
|
1816
|
+
state.frameLen = 0;
|
|
1817
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1692
1818
|
}
|
|
1693
1819
|
else if (!isHexChar(value)) {
|
|
1694
1820
|
state.status = 'idle';
|
|
1695
|
-
state.
|
|
1696
|
-
this.
|
|
1821
|
+
state.frameLen = 0;
|
|
1822
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1697
1823
|
}
|
|
1698
1824
|
else {
|
|
1699
|
-
state.frame.
|
|
1825
|
+
state.frame[state.frameLen++] = value;
|
|
1700
1826
|
}
|
|
1701
1827
|
break;
|
|
1702
1828
|
}
|
|
1703
1829
|
case 'waiting end': {
|
|
1704
1830
|
if (value === CHAR_CODE.COLON) {
|
|
1705
1831
|
state.status = 'reception';
|
|
1706
|
-
state.
|
|
1832
|
+
state.frameLen = 0;
|
|
1707
1833
|
}
|
|
1708
1834
|
else {
|
|
1709
1835
|
state.status = 'idle';
|
|
1710
1836
|
if (value === CHAR_CODE.LF) {
|
|
1711
|
-
this.framing(
|
|
1837
|
+
this.framing(state.frame, state.frameLen);
|
|
1712
1838
|
}
|
|
1713
1839
|
}
|
|
1714
1840
|
break;
|
|
1715
1841
|
}
|
|
1716
1842
|
}
|
|
1717
|
-
}
|
|
1843
|
+
}
|
|
1718
1844
|
};
|
|
1719
1845
|
connection.on('data', onData);
|
|
1720
|
-
this.
|
|
1846
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1721
1847
|
const onClose = () => {
|
|
1722
|
-
for (const fn of this.
|
|
1848
|
+
for (const fn of this._cleanupCbs) {
|
|
1723
1849
|
fn();
|
|
1724
1850
|
}
|
|
1725
|
-
this.
|
|
1851
|
+
this._cleanupCbs.length = 0;
|
|
1726
1852
|
};
|
|
1727
1853
|
connection.on('close', onClose);
|
|
1728
|
-
this.
|
|
1854
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1729
1855
|
}
|
|
1730
|
-
framing(hexChars) {
|
|
1731
|
-
if (
|
|
1732
|
-
this.
|
|
1856
|
+
framing(hexChars, hexLen) {
|
|
1857
|
+
if (hexLen < 6) {
|
|
1858
|
+
this.onFramingError(new Error('Insufficient data length'));
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (hexLen % 2 !== 0) {
|
|
1862
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const byteLen = hexLen >> 1;
|
|
1866
|
+
// Decode unit and fc directly from the first 4 hex characters —
|
|
1867
|
+
// avoids allocating a full decoded buffer just to read two bytes.
|
|
1868
|
+
const unitHi = HEX_DECODE[hexChars[0]];
|
|
1869
|
+
const unitLo = HEX_DECODE[hexChars[1]];
|
|
1870
|
+
const fcHi = HEX_DECODE[hexChars[2]];
|
|
1871
|
+
const fcLo = HEX_DECODE[hexChars[3]];
|
|
1872
|
+
if (unitHi === 0xff || unitLo === 0xff || fcHi === 0xff || fcLo === 0xff) {
|
|
1873
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1733
1874
|
return;
|
|
1734
1875
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1876
|
+
const unit = (unitHi << 4) | unitLo;
|
|
1877
|
+
const fc = (fcHi << 4) | fcLo;
|
|
1878
|
+
// Decode LRC from the last 2 hex characters.
|
|
1879
|
+
const lrcHi = HEX_DECODE[hexChars[hexLen - 2]];
|
|
1880
|
+
const lrcLo = HEX_DECODE[hexChars[hexLen - 1]];
|
|
1881
|
+
if (lrcHi === 0xff || lrcLo === 0xff) {
|
|
1882
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1737
1883
|
return;
|
|
1738
1884
|
}
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1885
|
+
const lrcIn = (lrcHi << 4) | lrcLo;
|
|
1886
|
+
// Decode data portion (between unit/fc and lrc) into a right-sized buffer.
|
|
1887
|
+
// dataLen may be 0 for a frame that is only unit + fc + lrc.
|
|
1888
|
+
const dataLen = byteLen - 3;
|
|
1889
|
+
const data = Buffer.allocUnsafe(dataLen);
|
|
1890
|
+
for (let i = 0; i < dataLen; i++) {
|
|
1891
|
+
const hi = HEX_DECODE[hexChars[4 + i * 2]];
|
|
1892
|
+
const lo = HEX_DECODE[hexChars[4 + i * 2 + 1]];
|
|
1745
1893
|
if (hi === 0xff || lo === 0xff) {
|
|
1746
|
-
this.
|
|
1894
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1747
1895
|
return;
|
|
1748
1896
|
}
|
|
1749
|
-
|
|
1897
|
+
data[i] = (hi << 4) | lo;
|
|
1750
1898
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
this.emit('framing-error', new Error('LRC check failed'));
|
|
1899
|
+
// Compute LRC over unit + fc + data.
|
|
1900
|
+
let sum = unit + fc;
|
|
1901
|
+
for (let i = 0; i < dataLen; i++) {
|
|
1902
|
+
sum += data[i];
|
|
1903
|
+
}
|
|
1904
|
+
const lrcComputed = (~sum + 1) & 0xff;
|
|
1905
|
+
if (lrcIn !== lrcComputed) {
|
|
1906
|
+
this.onFramingError(new Error('LRC check failed'));
|
|
1760
1907
|
return;
|
|
1761
1908
|
}
|
|
1762
|
-
|
|
1909
|
+
const frame = {
|
|
1910
|
+
unit,
|
|
1911
|
+
fc,
|
|
1912
|
+
data,
|
|
1913
|
+
buffer: Buffer.copyBytesFrom(hexChars, 0, hexLen),
|
|
1914
|
+
};
|
|
1915
|
+
this.onFraming(frame);
|
|
1763
1916
|
}
|
|
1764
1917
|
flush() {
|
|
1765
|
-
this._state =
|
|
1918
|
+
this._state.status = 'idle';
|
|
1919
|
+
this._state.frameLen = 0;
|
|
1766
1920
|
}
|
|
1767
1921
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1768
1922
|
encode(unit, fc, data, transaction) {
|
|
1769
1923
|
const buffer = Buffer.allocUnsafe(data.length + 3);
|
|
1770
|
-
|
|
1771
|
-
buffer
|
|
1924
|
+
// Inline header + LRC — direct typed-array stores skip Buffer's per-call checks.
|
|
1925
|
+
buffer[0] = unit;
|
|
1926
|
+
buffer[1] = fc;
|
|
1772
1927
|
buffer.set(data, 2);
|
|
1773
|
-
buffer.
|
|
1928
|
+
buffer[buffer.length - 1] = lrc(buffer.subarray(0, -1));
|
|
1774
1929
|
const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
|
|
1775
1930
|
out[0] = CHAR_CODE.COLON;
|
|
1776
1931
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -1791,7 +1946,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1791
1946
|
_connection;
|
|
1792
1947
|
_transactionId = 1;
|
|
1793
1948
|
_buffer = EMPTY_BUFFER;
|
|
1794
|
-
|
|
1949
|
+
_cleanupCbs = [];
|
|
1795
1950
|
get connection() {
|
|
1796
1951
|
return this._connection;
|
|
1797
1952
|
}
|
|
@@ -1800,6 +1955,17 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1800
1955
|
this.ROLE = role;
|
|
1801
1956
|
this._connection = connection;
|
|
1802
1957
|
const onData = (data) => {
|
|
1958
|
+
// Fast path: _buffer is empty and data is a single, complete frame.
|
|
1959
|
+
// This avoids the tryExtract subarray allocations + while loop
|
|
1960
|
+
// for the overwhelmingly common case (one frame per TCP packet).
|
|
1961
|
+
if (this._buffer.length === 0 && data.length >= 8) {
|
|
1962
|
+
const length = (data[4] << 8) | data[5]; // inline BE read
|
|
1963
|
+
const total = 6 + length;
|
|
1964
|
+
if (data[2] === 0 && data[3] === 0 && total <= MAX_TCP_FRAME && length >= 2 && data.length === total) {
|
|
1965
|
+
this.processFrame(data);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1803
1969
|
let buffer = this._buffer;
|
|
1804
1970
|
if (buffer.length === 0) {
|
|
1805
1971
|
buffer = data;
|
|
@@ -1817,7 +1983,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1817
1983
|
break;
|
|
1818
1984
|
}
|
|
1819
1985
|
else {
|
|
1820
|
-
this.
|
|
1986
|
+
this.onFramingError(result.error);
|
|
1821
1987
|
buffer = EMPTY_BUFFER;
|
|
1822
1988
|
break;
|
|
1823
1989
|
}
|
|
@@ -1837,15 +2003,15 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1837
2003
|
}
|
|
1838
2004
|
};
|
|
1839
2005
|
connection.on('data', onData);
|
|
1840
|
-
this.
|
|
2006
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1841
2007
|
const onClose = () => {
|
|
1842
|
-
for (const fn of this.
|
|
2008
|
+
for (const fn of this._cleanupCbs) {
|
|
1843
2009
|
fn();
|
|
1844
2010
|
}
|
|
1845
|
-
this.
|
|
2011
|
+
this._cleanupCbs.length = 0;
|
|
1846
2012
|
};
|
|
1847
2013
|
connection.on('close', onClose);
|
|
1848
|
-
this.
|
|
2014
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1849
2015
|
}
|
|
1850
2016
|
tryExtract(buffer) {
|
|
1851
2017
|
if (buffer.length < 8) {
|
|
@@ -1854,7 +2020,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1854
2020
|
if (buffer[2] !== 0 || buffer[3] !== 0) {
|
|
1855
2021
|
return { kind: 'error', error: new Error('Invalid data') };
|
|
1856
2022
|
}
|
|
1857
|
-
const length = buffer
|
|
2023
|
+
const length = (buffer[4] << 8) | buffer[5]; // inline BE read
|
|
1858
2024
|
const total = 6 + length;
|
|
1859
2025
|
if (total > MAX_TCP_FRAME || length < 2) {
|
|
1860
2026
|
return { kind: 'error', error: new Error('Invalid data') };
|
|
@@ -1862,29 +2028,47 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1862
2028
|
if (buffer.length < total) {
|
|
1863
2029
|
return { kind: 'insufficient' };
|
|
1864
2030
|
}
|
|
1865
|
-
return { kind: 'frame', frame: buffer.subarray(0, total), rest: buffer.subarray(total) };
|
|
2031
|
+
return { kind: 'frame', frame: buffer.subarray(0, total), rest: total === buffer.length ? EMPTY_BUFFER : buffer.subarray(total) };
|
|
1866
2032
|
}
|
|
1867
2033
|
processFrame(buffer) {
|
|
1868
2034
|
const frame = {
|
|
1869
|
-
|
|
2035
|
+
// Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
|
|
2036
|
+
// argument coercion + bounds check. Symmetric to the header writes in
|
|
2037
|
+
// encode() above. Hits on every received TCP frame.
|
|
2038
|
+
transaction: (buffer[0] << 8) | buffer[1],
|
|
1870
2039
|
unit: buffer[6],
|
|
1871
2040
|
fc: buffer[7],
|
|
1872
2041
|
data: buffer.subarray(8),
|
|
1873
2042
|
buffer,
|
|
1874
2043
|
};
|
|
1875
|
-
this.
|
|
2044
|
+
this.onFraming(frame);
|
|
1876
2045
|
}
|
|
1877
2046
|
flush() {
|
|
1878
2047
|
this._buffer = EMPTY_BUFFER;
|
|
1879
2048
|
}
|
|
1880
2049
|
encode(unit, fc, data, transaction) {
|
|
1881
2050
|
const buffer = Buffer.allocUnsafe(data.length + 8);
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
buffer
|
|
1887
|
-
buffer
|
|
2051
|
+
// Inline big-endian header writes — direct typed-array stores skip the
|
|
2052
|
+
// argument validation + bounds checks that `writeUInt16BE/writeUInt8` run.
|
|
2053
|
+
const tid = transaction ?? this._transactionId;
|
|
2054
|
+
const len = data.length + 2;
|
|
2055
|
+
buffer[0] = (tid >>> 8) & 0xff;
|
|
2056
|
+
buffer[1] = tid & 0xff;
|
|
2057
|
+
buffer[2] = 0;
|
|
2058
|
+
buffer[3] = 0;
|
|
2059
|
+
buffer[4] = (len >>> 8) & 0xff;
|
|
2060
|
+
buffer[5] = len & 0xff;
|
|
2061
|
+
buffer[6] = unit;
|
|
2062
|
+
buffer[7] = fc;
|
|
2063
|
+
// Small-payload fast path: avoid C++ TypedArray.prototype.set boundary
|
|
2064
|
+
// crossing when the copy is just a few bytes (common for FC 3/4/6 requests).
|
|
2065
|
+
if (data.length <= 16) {
|
|
2066
|
+
for (let i = 0; i < data.length; i++)
|
|
2067
|
+
buffer[8 + i] = data[i];
|
|
2068
|
+
}
|
|
2069
|
+
else {
|
|
2070
|
+
buffer.set(data, 8);
|
|
2071
|
+
}
|
|
1888
2072
|
if (transaction === undefined) {
|
|
1889
2073
|
this._transactionId = (this._transactionId + 1) % 65536 || 1;
|
|
1890
2074
|
}
|
|
@@ -1963,14 +2147,31 @@ class ModbusMaster extends EventEmitter {
|
|
|
1963
2147
|
_queueDatas = [];
|
|
1964
2148
|
_queueTimeouts = [];
|
|
1965
2149
|
_queueBroadcasts = [];
|
|
1966
|
-
|
|
1967
|
-
_queueRejects = [];
|
|
2150
|
+
_queueCallbacks = [];
|
|
1968
2151
|
_queueHead = 0;
|
|
1969
2152
|
_queueLen = 0;
|
|
1970
2153
|
_draining = false;
|
|
1971
2154
|
_nextTid = 1;
|
|
1972
2155
|
_cleanupFns = new Set();
|
|
1973
2156
|
_closePromise = null;
|
|
2157
|
+
_nextExchangeId = 1;
|
|
2158
|
+
// Global timer heap with lazy deletion — one native setTimeout for all requests.
|
|
2159
|
+
_pendingExchanges = new Map();
|
|
2160
|
+
_timerHeap = new TimerHeap((id) => {
|
|
2161
|
+
const pending = this._pendingExchanges.get(id);
|
|
2162
|
+
if (!pending)
|
|
2163
|
+
return; // lazy deletion: already handled
|
|
2164
|
+
pending.settled = true;
|
|
2165
|
+
this._pendingExchanges.delete(id);
|
|
2166
|
+
if (pending.sessionKey !== null) {
|
|
2167
|
+
this._masterSession.stop(pending.sessionKey);
|
|
2168
|
+
}
|
|
2169
|
+
const cb = pending.callback;
|
|
2170
|
+
if (cb) {
|
|
2171
|
+
pending.callback = null;
|
|
2172
|
+
cb(new Error('Timeout'));
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
1974
2175
|
get state() {
|
|
1975
2176
|
return this._physicalLayer.state;
|
|
1976
2177
|
}
|
|
@@ -1997,17 +2198,21 @@ class ModbusMaster extends EventEmitter {
|
|
|
1997
2198
|
for (const cfc of this._customFunctionCodes.values()) {
|
|
1998
2199
|
appLayer.addCustomFunctionCode(cfc);
|
|
1999
2200
|
}
|
|
2000
|
-
const cleanupFraming = () =>
|
|
2201
|
+
const cleanupFraming = () => {
|
|
2202
|
+
appLayer.onFraming = NOOP;
|
|
2203
|
+
};
|
|
2001
2204
|
const onFraming = (frame) => {
|
|
2002
2205
|
this._masterSession.handleFrame(frame);
|
|
2003
2206
|
};
|
|
2004
|
-
appLayer.
|
|
2207
|
+
appLayer.onFraming = onFraming;
|
|
2005
2208
|
this._cleanupFns.add(cleanupFraming);
|
|
2006
|
-
const cleanupFramingError = () =>
|
|
2209
|
+
const cleanupFramingError = () => {
|
|
2210
|
+
appLayer.onFramingError = NOOP;
|
|
2211
|
+
};
|
|
2007
2212
|
const onFramingError = (error) => {
|
|
2008
2213
|
this._masterSession.handleError(error);
|
|
2009
2214
|
};
|
|
2010
|
-
appLayer.
|
|
2215
|
+
appLayer.onFramingError = onFramingError;
|
|
2011
2216
|
this._cleanupFns.add(cleanupFramingError);
|
|
2012
2217
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2013
2218
|
const onClose = () => {
|
|
@@ -2063,31 +2268,23 @@ class ModbusMaster extends EventEmitter {
|
|
|
2063
2268
|
}
|
|
2064
2269
|
return new AsciiApplicationLayer('MASTER', connection, this._protocol.opts);
|
|
2065
2270
|
}
|
|
2066
|
-
|
|
2271
|
+
_send(unit, fc, data, timeout, broadcast, callback) {
|
|
2067
2272
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2068
|
-
|
|
2273
|
+
callback(new Error('Master is not open'));
|
|
2274
|
+
return;
|
|
2069
2275
|
}
|
|
2070
2276
|
if (this.concurrent) {
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
if (err)
|
|
2074
|
-
reject(err);
|
|
2075
|
-
else
|
|
2076
|
-
resolve(frame);
|
|
2077
|
-
});
|
|
2078
|
-
});
|
|
2277
|
+
this._exchange(unit, fc, data, timeout, broadcast, callback);
|
|
2278
|
+
return;
|
|
2079
2279
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
this._queueLen++;
|
|
2089
|
-
this._drain();
|
|
2090
|
-
});
|
|
2280
|
+
this._queueUnits.push(unit);
|
|
2281
|
+
this._queueFcs.push(fc);
|
|
2282
|
+
this._queueDatas.push(data);
|
|
2283
|
+
this._queueTimeouts.push(timeout);
|
|
2284
|
+
this._queueBroadcasts.push(broadcast);
|
|
2285
|
+
this._queueCallbacks.push(callback);
|
|
2286
|
+
this._queueLen++;
|
|
2287
|
+
this._drain();
|
|
2091
2288
|
}
|
|
2092
2289
|
_drain() {
|
|
2093
2290
|
if (this._draining || this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
@@ -2108,14 +2305,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2108
2305
|
const data = this._queueDatas[h];
|
|
2109
2306
|
const timeout = this._queueTimeouts[h];
|
|
2110
2307
|
const broadcast = this._queueBroadcasts[h];
|
|
2111
|
-
const
|
|
2112
|
-
|
|
2113
|
-
// Drop references so the GC can reclaim data buffers and resolve/reject
|
|
2308
|
+
const callback = this._queueCallbacks[h];
|
|
2309
|
+
// Drop references so the GC can reclaim data buffers and callback
|
|
2114
2310
|
// closures while the rest of the queue is still draining. Primitives
|
|
2115
2311
|
// (unit/fc/timeout/broadcast) need no clearing.
|
|
2116
2312
|
this._queueDatas[h] = undefined;
|
|
2117
|
-
this.
|
|
2118
|
-
this._queueRejects[h] = undefined;
|
|
2313
|
+
this._queueCallbacks[h] = undefined;
|
|
2119
2314
|
this._queueHead = h + 1;
|
|
2120
2315
|
this._queueLen--;
|
|
2121
2316
|
if (this._queueLen === 0) {
|
|
@@ -2126,15 +2321,11 @@ class ModbusMaster extends EventEmitter {
|
|
|
2126
2321
|
this._queueDatas.length = 0;
|
|
2127
2322
|
this._queueTimeouts.length = 0;
|
|
2128
2323
|
this._queueBroadcasts.length = 0;
|
|
2129
|
-
this.
|
|
2130
|
-
this._queueRejects.length = 0;
|
|
2324
|
+
this._queueCallbacks.length = 0;
|
|
2131
2325
|
this._queueHead = 0;
|
|
2132
2326
|
}
|
|
2133
2327
|
this._exchange(unit, fc, data, timeout, broadcast, (err, frame) => {
|
|
2134
|
-
|
|
2135
|
-
reject(err);
|
|
2136
|
-
else
|
|
2137
|
-
resolve(frame);
|
|
2328
|
+
callback(err, frame);
|
|
2138
2329
|
this._processNext();
|
|
2139
2330
|
});
|
|
2140
2331
|
}
|
|
@@ -2154,30 +2345,35 @@ class ModbusMaster extends EventEmitter {
|
|
|
2154
2345
|
if (!this.concurrent) {
|
|
2155
2346
|
appLayer.flush();
|
|
2156
2347
|
}
|
|
2348
|
+
// Lazy-deletion timer architecture:
|
|
2349
|
+
// 1. Assign an exchangeId and register in _pendingExchanges.
|
|
2350
|
+
// 2. Push deadline into the global TimerHeap (no per-request setTimeout).
|
|
2351
|
+
// 3. When the response arrives, delete from Map — the heap entry is left
|
|
2352
|
+
// behind and silently discarded when it surfaces at the top (lazy deletion).
|
|
2353
|
+
const exchangeId = this._nextExchangeId++;
|
|
2354
|
+
const pending = { settled: false, callback, sessionKey: null };
|
|
2355
|
+
this._pendingExchanges.set(exchangeId, pending);
|
|
2157
2356
|
if (broadcast) {
|
|
2158
|
-
// Broadcast: no response expected. Skip the session entirely
|
|
2159
|
-
|
|
2160
|
-
// broadcast's stop() would clear the other request's slot/timer).
|
|
2161
|
-
let settled = false;
|
|
2162
|
-
const timer = setTimeout(() => {
|
|
2163
|
-
if (settled)
|
|
2164
|
-
return;
|
|
2165
|
-
settled = true;
|
|
2166
|
-
callback(new Error('Timeout'));
|
|
2167
|
-
}, timeout);
|
|
2357
|
+
// Broadcast: no response expected. Skip the session entirely.
|
|
2358
|
+
this._timerHeap.add(exchangeId, timeout);
|
|
2168
2359
|
connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
|
|
2169
|
-
|
|
2360
|
+
const p = this._pendingExchanges.get(exchangeId);
|
|
2361
|
+
if (!p || p.settled)
|
|
2362
|
+
return;
|
|
2363
|
+
const cb = p.callback;
|
|
2364
|
+
if (!cb)
|
|
2170
2365
|
return;
|
|
2171
|
-
|
|
2172
|
-
|
|
2366
|
+
p.settled = true;
|
|
2367
|
+
p.callback = null;
|
|
2368
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2173
2369
|
if (writeErr) {
|
|
2174
|
-
|
|
2370
|
+
cb(writeErr);
|
|
2175
2371
|
}
|
|
2176
2372
|
else if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2177
|
-
|
|
2373
|
+
cb(new Error('Master is not open'));
|
|
2178
2374
|
}
|
|
2179
2375
|
else {
|
|
2180
|
-
|
|
2376
|
+
cb(null);
|
|
2181
2377
|
}
|
|
2182
2378
|
});
|
|
2183
2379
|
return;
|
|
@@ -2191,62 +2387,82 @@ class ModbusMaster extends EventEmitter {
|
|
|
2191
2387
|
}
|
|
2192
2388
|
const key = tid ?? FIFO_KEY;
|
|
2193
2389
|
const payload = appLayer.encode(unit, fc, data, tid);
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
// settled guard prevents double-invocation when timeout fires during write.
|
|
2197
|
-
let settled = false;
|
|
2198
|
-
const timer = setTimeout(() => {
|
|
2199
|
-
if (settled)
|
|
2200
|
-
return;
|
|
2201
|
-
settled = true;
|
|
2202
|
-
this._masterSession.stop(key);
|
|
2203
|
-
callback(new Error('Timeout'));
|
|
2204
|
-
}, timeout);
|
|
2390
|
+
pending.sessionKey = key;
|
|
2391
|
+
this._timerHeap.add(exchangeId, timeout);
|
|
2205
2392
|
connection.write(payload, (writeErr) => {
|
|
2393
|
+
const p = this._pendingExchanges.get(exchangeId);
|
|
2394
|
+
if (!p || p.settled)
|
|
2395
|
+
return;
|
|
2206
2396
|
if (writeErr) {
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
settled = true;
|
|
2210
|
-
callback
|
|
2397
|
+
const cb = p.callback;
|
|
2398
|
+
if (cb) {
|
|
2399
|
+
p.settled = true;
|
|
2400
|
+
p.callback = null;
|
|
2401
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2402
|
+
cb(writeErr);
|
|
2211
2403
|
}
|
|
2212
2404
|
return;
|
|
2213
2405
|
}
|
|
2214
2406
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
settled = true;
|
|
2218
|
-
callback
|
|
2407
|
+
const cb = p.callback;
|
|
2408
|
+
if (cb) {
|
|
2409
|
+
p.settled = true;
|
|
2410
|
+
p.callback = null;
|
|
2411
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2412
|
+
cb(new Error('Master is not open'));
|
|
2219
2413
|
}
|
|
2220
2414
|
return;
|
|
2221
2415
|
}
|
|
2222
2416
|
// Write succeeded — register in session for frame matching only.
|
|
2223
|
-
// Timeout is managed by the
|
|
2417
|
+
// Timeout is managed by the global timer heap above.
|
|
2224
2418
|
this._masterSession.start(key, (err, frame) => {
|
|
2225
|
-
|
|
2419
|
+
const p2 = this._pendingExchanges.get(exchangeId);
|
|
2420
|
+
if (!p2 || p2.settled)
|
|
2226
2421
|
return;
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2422
|
+
const cb = p2.callback;
|
|
2423
|
+
if (cb) {
|
|
2424
|
+
p2.settled = true;
|
|
2425
|
+
p2.callback = null;
|
|
2426
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2427
|
+
cb(err, frame);
|
|
2428
|
+
}
|
|
2230
2429
|
});
|
|
2231
2430
|
});
|
|
2232
2431
|
}
|
|
2233
2432
|
writeFC1Or2(unit, fc, address, length, timeout) {
|
|
2234
2433
|
const byteCount = Math.ceil(length / 8);
|
|
2235
2434
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2435
|
+
// Inline big-endian writes — direct typed-array stores skip the argument
|
|
2436
|
+
// validation + bounds checks that `writeUInt16BE` runs on each call.
|
|
2437
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2438
|
+
bufferTx[1] = address & 0xff;
|
|
2439
|
+
bufferTx[2] = (length >>> 8) & 0xff;
|
|
2440
|
+
bufferTx[3] = length & 0xff;
|
|
2441
|
+
return new Promise((resolve, reject) => {
|
|
2442
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2443
|
+
if (err) {
|
|
2444
|
+
reject(err);
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
if (!frame) {
|
|
2448
|
+
resolve(undefined);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
try {
|
|
2452
|
+
validateByteCountResponse(frame, unit, fc, byteCount);
|
|
2453
|
+
const data = new Array(length);
|
|
2454
|
+
for (let i = 0; i < length; i++) {
|
|
2455
|
+
data[i] = (frame.data[1 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
|
|
2456
|
+
}
|
|
2457
|
+
// Mutate the frame in place rather than spread-copying — `frame` is freshly
|
|
2458
|
+
// allocated per request and not retained anywhere else.
|
|
2459
|
+
frame.data = data;
|
|
2460
|
+
resolve(frame);
|
|
2461
|
+
}
|
|
2462
|
+
catch (e) {
|
|
2463
|
+
reject(e);
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2250
2466
|
});
|
|
2251
2467
|
}
|
|
2252
2468
|
writeFC1;
|
|
@@ -2260,19 +2476,41 @@ class ModbusMaster extends EventEmitter {
|
|
|
2260
2476
|
writeFC3Or4(unit, fc, address, length, timeout) {
|
|
2261
2477
|
const byteCount = length * 2;
|
|
2262
2478
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2263
|
-
|
|
2264
|
-
bufferTx
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2479
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2480
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2481
|
+
bufferTx[1] = address & 0xff;
|
|
2482
|
+
bufferTx[2] = (length >>> 8) & 0xff;
|
|
2483
|
+
bufferTx[3] = length & 0xff;
|
|
2484
|
+
return new Promise((resolve, reject) => {
|
|
2485
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2486
|
+
if (err) {
|
|
2487
|
+
reject(err);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
if (!frame) {
|
|
2491
|
+
resolve(undefined);
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
try {
|
|
2495
|
+
validateByteCountResponse(frame, unit, fc, byteCount);
|
|
2496
|
+
const bufferRx = frame.data.subarray(1);
|
|
2497
|
+
const data = new Array(length);
|
|
2498
|
+
// Inline big-endian read — `bufferRx[i]` is a direct typed-array
|
|
2499
|
+
// load, while `readUInt16BE` runs argument coercion + bounds check
|
|
2500
|
+
// on each call. Symmetric to the slave-side BE write inlining
|
|
2501
|
+
// in handleFC3/FC4. At length=125 (FC3 max) that's 250 saved
|
|
2502
|
+
// bounds-check pairs per response.
|
|
2503
|
+
for (let i = 0; i < length; i++) {
|
|
2504
|
+
const off = i * 2;
|
|
2505
|
+
data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
|
|
2506
|
+
}
|
|
2507
|
+
frame.data = data;
|
|
2508
|
+
resolve(frame);
|
|
2509
|
+
}
|
|
2510
|
+
catch (e) {
|
|
2511
|
+
reject(e);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2276
2514
|
});
|
|
2277
2515
|
}
|
|
2278
2516
|
writeFC3;
|
|
@@ -2287,28 +2525,61 @@ class ModbusMaster extends EventEmitter {
|
|
|
2287
2525
|
writeSingleCoil(unit, address, value, timeout = this.timeout) {
|
|
2288
2526
|
const fc = exports.FunctionCode.WRITE_SINGLE_COIL;
|
|
2289
2527
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2528
|
+
const coilValue = value ? COIL_ON : COIL_OFF;
|
|
2529
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2530
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2531
|
+
bufferTx[1] = address & 0xff;
|
|
2532
|
+
bufferTx[2] = (coilValue >>> 8) & 0xff;
|
|
2533
|
+
bufferTx[3] = coilValue & 0xff;
|
|
2534
|
+
return new Promise((resolve, reject) => {
|
|
2535
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2536
|
+
if (err) {
|
|
2537
|
+
reject(err);
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
if (!frame) {
|
|
2541
|
+
resolve(undefined);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
try {
|
|
2545
|
+
validateEchoResponse(frame, unit, fc, bufferTx);
|
|
2546
|
+
frame.data = value;
|
|
2547
|
+
resolve(frame);
|
|
2548
|
+
}
|
|
2549
|
+
catch (e) {
|
|
2550
|
+
reject(e);
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2298
2553
|
});
|
|
2299
2554
|
}
|
|
2300
2555
|
writeFC6;
|
|
2301
2556
|
writeSingleRegister(unit, address, value, timeout = this.timeout) {
|
|
2302
2557
|
const fc = exports.FunctionCode.WRITE_SINGLE_REGISTER;
|
|
2303
2558
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2304
|
-
|
|
2305
|
-
bufferTx
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2559
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2560
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2561
|
+
bufferTx[1] = address & 0xff;
|
|
2562
|
+
bufferTx[2] = (value >>> 8) & 0xff;
|
|
2563
|
+
bufferTx[3] = value & 0xff;
|
|
2564
|
+
return new Promise((resolve, reject) => {
|
|
2565
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2566
|
+
if (err) {
|
|
2567
|
+
reject(err);
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
if (!frame) {
|
|
2571
|
+
resolve(undefined);
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
try {
|
|
2575
|
+
validateEchoResponse(frame, unit, fc, bufferTx);
|
|
2576
|
+
frame.data = value;
|
|
2577
|
+
resolve(frame);
|
|
2578
|
+
}
|
|
2579
|
+
catch (e) {
|
|
2580
|
+
reject(e);
|
|
2581
|
+
}
|
|
2582
|
+
});
|
|
2312
2583
|
});
|
|
2313
2584
|
}
|
|
2314
2585
|
writeFC15;
|
|
@@ -2316,20 +2587,36 @@ class ModbusMaster extends EventEmitter {
|
|
|
2316
2587
|
const fc = exports.FunctionCode.WRITE_MULTIPLE_COILS;
|
|
2317
2588
|
const byteCount = Math.ceil(value.length / 8);
|
|
2318
2589
|
const bufferTx = Buffer.alloc(5 + byteCount);
|
|
2319
|
-
|
|
2320
|
-
bufferTx
|
|
2321
|
-
bufferTx
|
|
2322
|
-
value.
|
|
2323
|
-
|
|
2324
|
-
|
|
2590
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2591
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2592
|
+
bufferTx[1] = address & 0xff;
|
|
2593
|
+
bufferTx[2] = (value.length >>> 8) & 0xff;
|
|
2594
|
+
bufferTx[3] = value.length & 0xff;
|
|
2595
|
+
bufferTx[4] = byteCount;
|
|
2596
|
+
for (let i = 0; i < value.length; i++) {
|
|
2597
|
+
if (value[i]) {
|
|
2598
|
+
bufferTx[5 + Math.floor(i / 8)] |= 1 << i % 8;
|
|
2325
2599
|
}
|
|
2326
|
-
}
|
|
2327
|
-
return
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2600
|
+
}
|
|
2601
|
+
return new Promise((resolve, reject) => {
|
|
2602
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2603
|
+
if (err) {
|
|
2604
|
+
reject(err);
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
if (!frame) {
|
|
2608
|
+
resolve(undefined);
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
try {
|
|
2612
|
+
validateEchoResponse(frame, unit, fc, bufferTx.subarray(0, 4));
|
|
2613
|
+
frame.data = value;
|
|
2614
|
+
resolve(frame);
|
|
2615
|
+
}
|
|
2616
|
+
catch (e) {
|
|
2617
|
+
reject(e);
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2333
2620
|
});
|
|
2334
2621
|
}
|
|
2335
2622
|
writeFC16;
|
|
@@ -2337,53 +2624,102 @@ class ModbusMaster extends EventEmitter {
|
|
|
2337
2624
|
const fc = exports.FunctionCode.WRITE_MULTIPLE_REGISTERS;
|
|
2338
2625
|
const byteCount = value.length * 2;
|
|
2339
2626
|
const bufferTx = Buffer.allocUnsafe(5 + byteCount);
|
|
2340
|
-
|
|
2341
|
-
bufferTx
|
|
2342
|
-
bufferTx
|
|
2343
|
-
value.
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2627
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2628
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2629
|
+
bufferTx[1] = address & 0xff;
|
|
2630
|
+
bufferTx[2] = (value.length >>> 8) & 0xff;
|
|
2631
|
+
bufferTx[3] = value.length & 0xff;
|
|
2632
|
+
bufferTx[4] = byteCount;
|
|
2633
|
+
for (let i = 0; i < value.length; i++) {
|
|
2634
|
+
const v = value[i];
|
|
2635
|
+
const off = 5 + i * 2;
|
|
2636
|
+
bufferTx[off] = (v >>> 8) & 0xff;
|
|
2637
|
+
bufferTx[off + 1] = v & 0xff;
|
|
2638
|
+
}
|
|
2639
|
+
return new Promise((resolve, reject) => {
|
|
2640
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2641
|
+
if (err) {
|
|
2642
|
+
reject(err);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
if (!frame) {
|
|
2646
|
+
resolve(undefined);
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
try {
|
|
2650
|
+
validateEchoResponse(frame, unit, fc, bufferTx.subarray(0, 4));
|
|
2651
|
+
frame.data = value;
|
|
2652
|
+
resolve(frame);
|
|
2653
|
+
}
|
|
2654
|
+
catch (e) {
|
|
2655
|
+
reject(e);
|
|
2656
|
+
}
|
|
2657
|
+
});
|
|
2352
2658
|
});
|
|
2353
2659
|
}
|
|
2354
2660
|
handleFC17;
|
|
2355
2661
|
reportServerId(unit, serverIdLength = 1, timeout = this.timeout) {
|
|
2356
2662
|
const fc = exports.FunctionCode.REPORT_SERVER_ID;
|
|
2357
|
-
return
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2663
|
+
return new Promise((resolve, reject) => {
|
|
2664
|
+
this._send(unit, fc, EMPTY_BUFFER, timeout, unit === 0, (err, frame) => {
|
|
2665
|
+
if (err) {
|
|
2666
|
+
reject(err);
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
if (!frame) {
|
|
2670
|
+
resolve(undefined);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
try {
|
|
2674
|
+
validateResponse(frame, unit, fc);
|
|
2675
|
+
if (frame.data.length < 2 + serverIdLength)
|
|
2676
|
+
throw new Error('Insufficient data length');
|
|
2677
|
+
if (frame.data.length !== 1 + frame.data[0])
|
|
2678
|
+
throw new Error('Invalid response');
|
|
2679
|
+
const runStatusIndex = 1 + serverIdLength;
|
|
2680
|
+
frame.data = {
|
|
2681
|
+
serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
|
|
2682
|
+
runIndicatorStatus: frame.data[runStatusIndex] === 0xff,
|
|
2683
|
+
additionalData: Array.from(frame.data.subarray(runStatusIndex + 1)),
|
|
2684
|
+
};
|
|
2685
|
+
resolve(frame);
|
|
2686
|
+
}
|
|
2687
|
+
catch (e) {
|
|
2688
|
+
reject(e);
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2372
2691
|
});
|
|
2373
2692
|
}
|
|
2374
2693
|
handleFC22;
|
|
2375
2694
|
maskWriteRegister(unit, address, andMask, orMask, timeout = this.timeout) {
|
|
2376
2695
|
const fc = exports.FunctionCode.MASK_WRITE_REGISTER;
|
|
2377
2696
|
const bufferTx = Buffer.allocUnsafe(6);
|
|
2378
|
-
|
|
2379
|
-
bufferTx
|
|
2380
|
-
bufferTx
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2697
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2698
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2699
|
+
bufferTx[1] = address & 0xff;
|
|
2700
|
+
bufferTx[2] = (andMask >>> 8) & 0xff;
|
|
2701
|
+
bufferTx[3] = andMask & 0xff;
|
|
2702
|
+
bufferTx[4] = (orMask >>> 8) & 0xff;
|
|
2703
|
+
bufferTx[5] = orMask & 0xff;
|
|
2704
|
+
return new Promise((resolve, reject) => {
|
|
2705
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2706
|
+
if (err) {
|
|
2707
|
+
reject(err);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
if (!frame) {
|
|
2711
|
+
resolve(undefined);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
try {
|
|
2715
|
+
validateEchoResponse(frame, unit, fc, bufferTx);
|
|
2716
|
+
frame.data = { andMask, orMask };
|
|
2717
|
+
resolve(frame);
|
|
2718
|
+
}
|
|
2719
|
+
catch (e) {
|
|
2720
|
+
reject(e);
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2387
2723
|
});
|
|
2388
2724
|
}
|
|
2389
2725
|
handleFC23;
|
|
@@ -2392,71 +2728,115 @@ class ModbusMaster extends EventEmitter {
|
|
|
2392
2728
|
const byteCount = write.value.length * 2;
|
|
2393
2729
|
const readByteCount = read.length * 2;
|
|
2394
2730
|
const bufferTx = Buffer.allocUnsafe(9 + byteCount);
|
|
2395
|
-
|
|
2396
|
-
bufferTx
|
|
2397
|
-
bufferTx.
|
|
2398
|
-
bufferTx
|
|
2399
|
-
bufferTx.
|
|
2400
|
-
write.
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
const
|
|
2408
|
-
|
|
2409
|
-
|
|
2731
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2732
|
+
bufferTx[0] = (read.address >>> 8) & 0xff;
|
|
2733
|
+
bufferTx[1] = read.address & 0xff;
|
|
2734
|
+
bufferTx[2] = (read.length >>> 8) & 0xff;
|
|
2735
|
+
bufferTx[3] = read.length & 0xff;
|
|
2736
|
+
bufferTx[4] = (write.address >>> 8) & 0xff;
|
|
2737
|
+
bufferTx[5] = write.address & 0xff;
|
|
2738
|
+
bufferTx[6] = (write.value.length >>> 8) & 0xff;
|
|
2739
|
+
bufferTx[7] = write.value.length & 0xff;
|
|
2740
|
+
bufferTx[8] = byteCount;
|
|
2741
|
+
for (let i = 0; i < write.value.length; i++) {
|
|
2742
|
+
const v = write.value[i];
|
|
2743
|
+
const off = 9 + i * 2;
|
|
2744
|
+
bufferTx[off] = (v >>> 8) & 0xff;
|
|
2745
|
+
bufferTx[off + 1] = v & 0xff;
|
|
2746
|
+
}
|
|
2747
|
+
return new Promise((resolve, reject) => {
|
|
2748
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2749
|
+
if (err) {
|
|
2750
|
+
reject(err);
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
if (!frame) {
|
|
2754
|
+
resolve(undefined);
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
try {
|
|
2758
|
+
validateByteCountResponse(frame, unit, fc, readByteCount);
|
|
2759
|
+
const bufferRx = frame.data.subarray(1);
|
|
2760
|
+
// Dense pre-sized array + inline BE read — drops the Array.from
|
|
2761
|
+
// closure + N readUInt16BE bounds-check pairs. See writeFC3Or4
|
|
2762
|
+
// response handler for the same optimization.
|
|
2763
|
+
const data = new Array(read.length);
|
|
2764
|
+
for (let i = 0; i < read.length; i++) {
|
|
2765
|
+
const off = i * 2;
|
|
2766
|
+
data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
|
|
2767
|
+
}
|
|
2768
|
+
frame.data = data;
|
|
2769
|
+
resolve(frame);
|
|
2770
|
+
}
|
|
2771
|
+
catch (e) {
|
|
2772
|
+
reject(e);
|
|
2773
|
+
}
|
|
2774
|
+
});
|
|
2410
2775
|
});
|
|
2411
2776
|
}
|
|
2412
2777
|
handleFC43_14;
|
|
2413
2778
|
readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout = this.timeout) {
|
|
2414
2779
|
const fc = exports.FunctionCode.READ_DEVICE_IDENTIFICATION;
|
|
2415
|
-
return
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2780
|
+
return new Promise((resolve, reject) => {
|
|
2781
|
+
this._send(unit, fc, Buffer.from([MEI_READ_DEVICE_ID, readDeviceIDCode, objectId]), timeout, unit === 0, (err, frame) => {
|
|
2782
|
+
if (err) {
|
|
2783
|
+
reject(err);
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
if (!frame) {
|
|
2787
|
+
resolve(undefined);
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
try {
|
|
2791
|
+
validateResponse(frame, unit, fc);
|
|
2792
|
+
if (frame.data.length < 6)
|
|
2793
|
+
throw new Error('Insufficient data length');
|
|
2794
|
+
if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode)
|
|
2795
|
+
throw new Error('Invalid response');
|
|
2796
|
+
const objects = [];
|
|
2797
|
+
let object = [];
|
|
2798
|
+
let totalBytes = 0;
|
|
2799
|
+
for (const v of frame.data.subarray(6)) {
|
|
2800
|
+
switch (object.length) {
|
|
2801
|
+
case 0:
|
|
2802
|
+
case 1: {
|
|
2803
|
+
object.push(v);
|
|
2804
|
+
break;
|
|
2805
|
+
}
|
|
2806
|
+
case 2: {
|
|
2807
|
+
object.push([v]);
|
|
2808
|
+
break;
|
|
2809
|
+
}
|
|
2810
|
+
case 3: {
|
|
2811
|
+
object[2].push(v);
|
|
2812
|
+
if (object[1] === object[2].length) {
|
|
2813
|
+
objects.push({ id: object[0], value: Buffer.from(object[2]).toString() });
|
|
2814
|
+
totalBytes += 2 + object[1];
|
|
2815
|
+
object = [];
|
|
2816
|
+
}
|
|
2817
|
+
break;
|
|
2818
|
+
}
|
|
2819
|
+
default:
|
|
2820
|
+
break;
|
|
2443
2821
|
}
|
|
2444
|
-
break;
|
|
2445
2822
|
}
|
|
2823
|
+
if (objects.length !== frame.data[5])
|
|
2824
|
+
throw new Error('Invalid response');
|
|
2825
|
+
if (frame.data.length !== 6 + totalBytes)
|
|
2826
|
+
throw new Error('Invalid response');
|
|
2827
|
+
frame.data = {
|
|
2828
|
+
readDeviceIDCode,
|
|
2829
|
+
conformityLevel: frame.data[2],
|
|
2830
|
+
moreFollows: frame.data[3] === 0xff,
|
|
2831
|
+
nextObjectId: frame.data[4],
|
|
2832
|
+
objects,
|
|
2833
|
+
};
|
|
2834
|
+
resolve(frame);
|
|
2446
2835
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
throw new Error('Invalid response');
|
|
2452
|
-
frame.data = {
|
|
2453
|
-
readDeviceIDCode,
|
|
2454
|
-
conformityLevel: frame.data[2],
|
|
2455
|
-
moreFollows: frame.data[3] === 0xff,
|
|
2456
|
-
nextObjectId: frame.data[4],
|
|
2457
|
-
objects,
|
|
2458
|
-
};
|
|
2459
|
-
return frame;
|
|
2836
|
+
catch (e) {
|
|
2837
|
+
reject(e);
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2460
2840
|
});
|
|
2461
2841
|
}
|
|
2462
2842
|
addCustomFunctionCode(cfc) {
|
|
@@ -2469,11 +2849,24 @@ class ModbusMaster extends EventEmitter {
|
|
|
2469
2849
|
}
|
|
2470
2850
|
sendCustomFC(unit, fc, data, timeout = this.timeout) {
|
|
2471
2851
|
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
2472
|
-
return
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2852
|
+
return new Promise((resolve, reject) => {
|
|
2853
|
+
this._send(unit, fc, payload, timeout, unit === 0, (err, frame) => {
|
|
2854
|
+
if (err) {
|
|
2855
|
+
reject(err);
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
if (!frame) {
|
|
2859
|
+
resolve(undefined);
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
try {
|
|
2863
|
+
validateResponse(frame, unit, fc);
|
|
2864
|
+
resolve(frame.data);
|
|
2865
|
+
}
|
|
2866
|
+
catch (e) {
|
|
2867
|
+
reject(e);
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2477
2870
|
});
|
|
2478
2871
|
}
|
|
2479
2872
|
/**
|
|
@@ -2499,9 +2892,24 @@ class ModbusMaster extends EventEmitter {
|
|
|
2499
2892
|
const end = this._queueHead + this._queueLen;
|
|
2500
2893
|
this._queueLen = 0;
|
|
2501
2894
|
for (let i = this._queueHead; i < end; i++) {
|
|
2502
|
-
this.
|
|
2895
|
+
this._queueCallbacks[i](rejectErr);
|
|
2503
2896
|
}
|
|
2504
2897
|
this._masterSession.stopAll(rejectErr);
|
|
2898
|
+
this._timerHeap.clear();
|
|
2899
|
+
// Settle any in-flight exchanges that weren't reached by stopAll
|
|
2900
|
+
// (broadcasts have no session waiter; non-broadcasts still in the
|
|
2901
|
+
// pre-write-window haven't registered in session yet).
|
|
2902
|
+
for (const pending of this._pendingExchanges.values()) {
|
|
2903
|
+
if (pending.settled)
|
|
2904
|
+
continue;
|
|
2905
|
+
pending.settled = true;
|
|
2906
|
+
const cb = pending.callback;
|
|
2907
|
+
if (cb) {
|
|
2908
|
+
pending.callback = null;
|
|
2909
|
+
cb(rejectErr);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
this._pendingExchanges.clear();
|
|
2505
2913
|
let err = null;
|
|
2506
2914
|
try {
|
|
2507
2915
|
await promisifyCb((cb) => this._physicalLayer.close(cb));
|
|
@@ -2538,7 +2946,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
2538
2946
|
_protocol;
|
|
2539
2947
|
_appLayers = new Map();
|
|
2540
2948
|
_customFunctionCodes = new Map();
|
|
2541
|
-
|
|
2949
|
+
// Active interval locks. Typical length: 0 (no contention) — 1-2 (cross-
|
|
2950
|
+
// connection contention). Linear scan for overlap is sub-µs.
|
|
2951
|
+
_intervalLocks = [];
|
|
2542
2952
|
_cleanupFns = new Set();
|
|
2543
2953
|
_closePromise = null;
|
|
2544
2954
|
get state() {
|
|
@@ -2565,7 +2975,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
2565
2975
|
for (const cfc of this._customFunctionCodes.values()) {
|
|
2566
2976
|
appLayer.addCustomFunctionCode(cfc);
|
|
2567
2977
|
}
|
|
2568
|
-
const cleanupFraming = () =>
|
|
2978
|
+
const cleanupFraming = () => {
|
|
2979
|
+
appLayer.onFraming = NOOP;
|
|
2980
|
+
};
|
|
2569
2981
|
const onFraming = (frame) => {
|
|
2570
2982
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2571
2983
|
return;
|
|
@@ -2580,7 +2992,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2580
2992
|
appLayerData.queues.frames.push(frame);
|
|
2581
2993
|
this._drain(appLayer, appLayerData.queues);
|
|
2582
2994
|
};
|
|
2583
|
-
appLayer.
|
|
2995
|
+
appLayer.onFraming = onFraming;
|
|
2584
2996
|
this._cleanupFns.add(cleanupFraming);
|
|
2585
2997
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2586
2998
|
const onClose = () => {
|
|
@@ -2647,12 +3059,23 @@ class ModbusSlave extends EventEmitter {
|
|
|
2647
3059
|
const byteCount = (length + 7) >> 3;
|
|
2648
3060
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
2649
3061
|
pdu[0] = byteCount;
|
|
2650
|
-
|
|
3062
|
+
// Pack 8 booleans per byte without first zero-filling: accumulate into
|
|
3063
|
+
// `acc` and write a full byte once each lane is finished. Saves the
|
|
3064
|
+
// `pdu.fill(0, 1, ...)` pass and replaces N `|=` reads-modify-writes
|
|
3065
|
+
// with N `read`s + ⌈N/8⌉ `write`s. Measured ~+5% on FC01 / ~+3% on FC02
|
|
3066
|
+
// at max payload (2000 / 1968 coils) via benchmark/all-fcs.ts bisect.
|
|
3067
|
+
let acc = 0;
|
|
3068
|
+
let out = 1;
|
|
2651
3069
|
for (let i = 0; i < length; i++) {
|
|
2652
|
-
if (coils[i])
|
|
2653
|
-
|
|
3070
|
+
if (coils[i])
|
|
3071
|
+
acc |= 1 << (i & 7);
|
|
3072
|
+
if ((i & 7) === 7) {
|
|
3073
|
+
pdu[out++] = acc;
|
|
3074
|
+
acc = 0;
|
|
2654
3075
|
}
|
|
2655
3076
|
}
|
|
3077
|
+
if ((length & 7) !== 0)
|
|
3078
|
+
pdu[out] = acc;
|
|
2656
3079
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2657
3080
|
}
|
|
2658
3081
|
catch (error) {
|
|
@@ -2683,12 +3106,19 @@ class ModbusSlave extends EventEmitter {
|
|
|
2683
3106
|
const byteCount = (length + 7) >> 3;
|
|
2684
3107
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
2685
3108
|
pdu[0] = byteCount;
|
|
2686
|
-
|
|
3109
|
+
// Accumulator-based bit pack — see handleFC1 for the rationale.
|
|
3110
|
+
let acc = 0;
|
|
3111
|
+
let out = 1;
|
|
2687
3112
|
for (let i = 0; i < length; i++) {
|
|
2688
|
-
if (discreteInputs[i])
|
|
2689
|
-
|
|
3113
|
+
if (discreteInputs[i])
|
|
3114
|
+
acc |= 1 << (i & 7);
|
|
3115
|
+
if ((i & 7) === 7) {
|
|
3116
|
+
pdu[out++] = acc;
|
|
3117
|
+
acc = 0;
|
|
2690
3118
|
}
|
|
2691
3119
|
}
|
|
3120
|
+
if ((length & 7) !== 0)
|
|
3121
|
+
pdu[out] = acc;
|
|
2692
3122
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2693
3123
|
}
|
|
2694
3124
|
catch (error) {
|
|
@@ -2718,8 +3148,14 @@ class ModbusSlave extends EventEmitter {
|
|
|
2718
3148
|
const registers = await model.readHoldingRegisters(address, length);
|
|
2719
3149
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
2720
3150
|
pdu[0] = length * 2;
|
|
3151
|
+
// Inline big-endian write — `pdu[i] = v` is a direct typed-array store,
|
|
3152
|
+
// while `writeUInt16BE` runs argument validation + bounds checks on each
|
|
3153
|
+
// call. At length=125 (FC3 max) that's 250 saved checks per request.
|
|
2721
3154
|
for (let i = 0; i < length; i++) {
|
|
2722
|
-
|
|
3155
|
+
const v = registers[i];
|
|
3156
|
+
const off = 1 + i * 2;
|
|
3157
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3158
|
+
pdu[off + 1] = v & 0xff;
|
|
2723
3159
|
}
|
|
2724
3160
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2725
3161
|
}
|
|
@@ -2750,8 +3186,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
2750
3186
|
const registers = await model.readInputRegisters(address, length);
|
|
2751
3187
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
2752
3188
|
pdu[0] = length * 2;
|
|
3189
|
+
// Inline big-endian write — see handleFC3 for the rationale.
|
|
2753
3190
|
for (let i = 0; i < length; i++) {
|
|
2754
|
-
|
|
3191
|
+
const v = registers[i];
|
|
3192
|
+
const off = 1 + i * 2;
|
|
3193
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3194
|
+
pdu[off + 1] = v & 0xff;
|
|
2755
3195
|
}
|
|
2756
3196
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2757
3197
|
}
|
|
@@ -2831,6 +3271,10 @@ class ModbusSlave extends EventEmitter {
|
|
|
2831
3271
|
}
|
|
2832
3272
|
const value = new Array(length);
|
|
2833
3273
|
for (let i = 0; i < length; i++) {
|
|
3274
|
+
// Bit math: kept as `Math.floor(i / 8)` / `i % 8` — V8 pattern-matches
|
|
3275
|
+
// both forms with `i` as an integer loop counter to direct shifts, but
|
|
3276
|
+
// measured ~+3% throughput vs the bit-shift form on this exact loop
|
|
3277
|
+
// (benchmark/all-fcs.ts FC15 slave A/B, 8/8 paired samples).
|
|
2834
3278
|
value[i] = (frame.data[5 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
|
|
2835
3279
|
}
|
|
2836
3280
|
try {
|
|
@@ -2935,7 +3379,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2935
3379
|
return;
|
|
2936
3380
|
}
|
|
2937
3381
|
try {
|
|
2938
|
-
await this.
|
|
3382
|
+
await this._withIntervalLock(address, address + 1, async () => {
|
|
2939
3383
|
if (model.maskWriteRegister) {
|
|
2940
3384
|
await model.maskWriteRegister(address, andMask, orMask);
|
|
2941
3385
|
}
|
|
@@ -2985,8 +3429,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2985
3429
|
value[i] = (frame.data[9 + i * 2] << 8) | frame.data[10 + i * 2];
|
|
2986
3430
|
}
|
|
2987
3431
|
try {
|
|
2988
|
-
|
|
2989
|
-
await this._withAddressLock(writeAddresses, async () => {
|
|
3432
|
+
await this._withIntervalLock(address.write, address.write + length.write, async () => {
|
|
2990
3433
|
if (model.writeMultipleRegisters) {
|
|
2991
3434
|
await model.writeMultipleRegisters(address.write, value);
|
|
2992
3435
|
}
|
|
@@ -2999,8 +3442,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
2999
3442
|
const registers = await model.readHoldingRegisters(address.read, length.read);
|
|
3000
3443
|
const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
|
|
3001
3444
|
pdu[0] = length.read * 2;
|
|
3445
|
+
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3002
3446
|
for (let i = 0; i < length.read; i++) {
|
|
3003
|
-
|
|
3447
|
+
const v = registers[i];
|
|
3448
|
+
const off = 1 + i * 2;
|
|
3449
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3450
|
+
pdu[off + 1] = v & 0xff;
|
|
3004
3451
|
}
|
|
3005
3452
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3006
3453
|
}
|
|
@@ -3159,7 +3606,11 @@ class ModbusSlave extends EventEmitter {
|
|
|
3159
3606
|
return Promise.resolve();
|
|
3160
3607
|
}
|
|
3161
3608
|
return new Promise((resolve) => {
|
|
3162
|
-
|
|
3609
|
+
// Pass `resolve` directly as the write cb (vs `() => resolve()`) —
|
|
3610
|
+
// saves one closure allocation per response. `resolve` may receive
|
|
3611
|
+
// the write's err arg, but our Promise<void> contract ignores any
|
|
3612
|
+
// resolved value and the response handler doesn't check success.
|
|
3613
|
+
appLayer.connection.write(data, resolve);
|
|
3163
3614
|
});
|
|
3164
3615
|
};
|
|
3165
3616
|
// Hot path: unicast to a known unit dispatches to a single model.
|
|
@@ -3208,24 +3659,66 @@ class ModbusSlave extends EventEmitter {
|
|
|
3208
3659
|
return true;
|
|
3209
3660
|
}
|
|
3210
3661
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3662
|
+
/**
|
|
3663
|
+
* Serialize a code block over the half-open address interval `[lo, hi)`.
|
|
3664
|
+
* The block runs after all previously-installed locks whose intervals
|
|
3665
|
+
* overlap with this one have completed. Two non-overlapping intervals
|
|
3666
|
+
* execute in parallel.
|
|
3667
|
+
*
|
|
3668
|
+
* Locks are tracked in a flat array (`_intervalLocks`); the typical depth
|
|
3669
|
+
* is 0-2 entries, so the linear overlap scan is sub-µs. Compare with the
|
|
3670
|
+
* old per-address Map design, where FC23 writing 121 registers allocated
|
|
3671
|
+
* ~125 objects per request (one Promise.resolve / address + Set + sort +
|
|
3672
|
+
* Promise.all); this version allocates 1-3.
|
|
3673
|
+
*/
|
|
3674
|
+
async _withIntervalLock(lo, hi, fn) {
|
|
3675
|
+
// Find overlapping active entries. Two half-open intervals [a, b) and
|
|
3676
|
+
// [c, d) overlap iff a < d && c < b.
|
|
3677
|
+
let overlap = null;
|
|
3678
|
+
const locks = this._intervalLocks;
|
|
3679
|
+
for (let i = 0; i < locks.length; i++) {
|
|
3680
|
+
const l = locks[i];
|
|
3681
|
+
if (l.lo < hi && lo < l.hi) {
|
|
3682
|
+
if (overlap === null)
|
|
3683
|
+
overlap = [];
|
|
3684
|
+
overlap.push(l.promise);
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
// Install our entry BEFORE awaiting so any concurrent caller arriving
|
|
3688
|
+
// after this point sees us and waits. Promise field is filled in below.
|
|
3689
|
+
const entry = { lo, hi, promise: undefined };
|
|
3690
|
+
locks.push(entry);
|
|
3691
|
+
let work;
|
|
3692
|
+
if (overlap === null) {
|
|
3693
|
+
work = fn();
|
|
3694
|
+
}
|
|
3695
|
+
else if (overlap.length === 1) {
|
|
3696
|
+
// Skip Promise.all when there's exactly one prior — avoids one
|
|
3697
|
+
// intermediate Promise allocation in the most common contention case.
|
|
3698
|
+
// `.then(fn)` (vs `.then(() => fn())`) saves a closure: fn's signature
|
|
3699
|
+
// is () => Promise<T>, and the resolved value `.then` passes in is
|
|
3700
|
+
// silently discarded by JS's loose-arity rule.
|
|
3701
|
+
work = overlap[0].then(fn);
|
|
3220
3702
|
}
|
|
3703
|
+
else {
|
|
3704
|
+
work = Promise.all(overlap).then(fn);
|
|
3705
|
+
}
|
|
3706
|
+
// We never want the cleanup latch to reject — swallow errors so
|
|
3707
|
+
// downstream `.then` chains don't see an unhandled rejection. Using the
|
|
3708
|
+
// shared `NOOP` singleton avoids allocating a fresh arrow function on
|
|
3709
|
+
// every locked request.
|
|
3710
|
+
entry.promise = work.catch(NOOP);
|
|
3221
3711
|
try {
|
|
3222
3712
|
return await work;
|
|
3223
3713
|
}
|
|
3224
3714
|
finally {
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3715
|
+
const i = locks.indexOf(entry);
|
|
3716
|
+
if (i !== -1) {
|
|
3717
|
+
// O(1) swap-and-pop since lock order doesn't matter for correctness.
|
|
3718
|
+
const last = locks.length - 1;
|
|
3719
|
+
if (i !== last)
|
|
3720
|
+
locks[i] = locks[last];
|
|
3721
|
+
locks.pop();
|
|
3229
3722
|
}
|
|
3230
3723
|
}
|
|
3231
3724
|
}
|
|
@@ -3356,6 +3849,7 @@ exports.MasterSession = MasterSession;
|
|
|
3356
3849
|
exports.ModbusError = ModbusError;
|
|
3357
3850
|
exports.ModbusMaster = ModbusMaster;
|
|
3358
3851
|
exports.ModbusSlave = ModbusSlave;
|
|
3852
|
+
exports.NOOP = NOOP;
|
|
3359
3853
|
exports.RtuApplicationLayer = RtuApplicationLayer;
|
|
3360
3854
|
exports.SerialPhysicalLayer = SerialPhysicalLayer;
|
|
3361
3855
|
exports.TcpApplicationLayer = TcpApplicationLayer;
|