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