njs-modbus 3.1.0 → 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 -390
- package/dist/index.d.ts +96 -30
- package/dist/index.mjs +878 -391
- package/dist/utils.cjs +439 -0
- package/dist/utils.d.ts +161 -0
- package/dist/utils.mjs +425 -0
- package/package.json +16 -3
- 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 -19
- package/dist/src/layers/physical/tcp-physical-connection.d.ts +0 -16
- package/dist/src/layers/physical/tcp-server-physical-layer.d.ts +0 -28
- package/dist/src/layers/physical/udp-client-physical-layer.d.ts +0 -33
- package/dist/src/layers/physical/udp-server-physical-layer.d.ts +0 -50
- 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`.
|
|
@@ -1194,12 +1303,6 @@ class UdpServerPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
1194
1303
|
super();
|
|
1195
1304
|
this._opts = options ?? {};
|
|
1196
1305
|
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Bind the UDP socket and start accepting datagrams.
|
|
1199
|
-
*
|
|
1200
|
-
* @param options Bind options (port, address, etc.). Defaults to port 502.
|
|
1201
|
-
* @param cb Callback invoked when binding completes or fails.
|
|
1202
|
-
*/
|
|
1203
1306
|
open(options, cb) {
|
|
1204
1307
|
// Node-style callback overload: if the first arg is a function, treat it as cb.
|
|
1205
1308
|
if (typeof options === 'function') {
|
|
@@ -1347,14 +1450,16 @@ function createPhysicalLayer(config) {
|
|
|
1347
1450
|
* established and discarded when the connection closes. Subclasses implement
|
|
1348
1451
|
* ASCII, RTU, or TCP framing rules.
|
|
1349
1452
|
*/
|
|
1350
|
-
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;
|
|
1351
1458
|
flush() {
|
|
1352
1459
|
// no-op — override in subclasses
|
|
1353
1460
|
}
|
|
1354
|
-
addCustomFunctionCode(cfc) {
|
|
1355
|
-
}
|
|
1356
|
-
removeCustomFunctionCode(fc) {
|
|
1357
|
-
}
|
|
1461
|
+
addCustomFunctionCode(cfc) { }
|
|
1462
|
+
removeCustomFunctionCode(fc) { }
|
|
1358
1463
|
}
|
|
1359
1464
|
|
|
1360
1465
|
const MAX_FRAME_LENGTH = 256;
|
|
@@ -1368,7 +1473,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1368
1473
|
_threePointFiveT;
|
|
1369
1474
|
_onePointFiveT;
|
|
1370
1475
|
_customFunctionCodes = new Map();
|
|
1371
|
-
|
|
1476
|
+
_cleanupCbs = [];
|
|
1372
1477
|
get connection() {
|
|
1373
1478
|
return this._connection;
|
|
1374
1479
|
}
|
|
@@ -1384,7 +1489,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1384
1489
|
const onData = (data) => {
|
|
1385
1490
|
const state = this._state;
|
|
1386
1491
|
if (state.t15Expired && state.end > state.start) {
|
|
1387
|
-
this.
|
|
1492
|
+
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1388
1493
|
state.start = 0;
|
|
1389
1494
|
state.end = 0;
|
|
1390
1495
|
}
|
|
@@ -1426,7 +1531,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1426
1531
|
// flushBuffer freed nothing — the entire pool is unparseable
|
|
1427
1532
|
// residue (typically a misconfigured poolSize for the wire's
|
|
1428
1533
|
// frame size). Hard reset; we cannot recover automatically.
|
|
1429
|
-
this.
|
|
1534
|
+
this.onFramingError(new Error('Frame buffer exhausted before complete frame received'));
|
|
1430
1535
|
currentState.start = 0;
|
|
1431
1536
|
currentState.end = 0;
|
|
1432
1537
|
currentState.t15Expired = false;
|
|
@@ -1462,16 +1567,16 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1462
1567
|
}
|
|
1463
1568
|
};
|
|
1464
1569
|
connection.on('data', onData);
|
|
1465
|
-
this.
|
|
1570
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1466
1571
|
const onClose = () => {
|
|
1467
|
-
for (const fn of this.
|
|
1572
|
+
for (const fn of this._cleanupCbs) {
|
|
1468
1573
|
fn();
|
|
1469
1574
|
}
|
|
1470
|
-
this.
|
|
1575
|
+
this._cleanupCbs.length = 0;
|
|
1471
1576
|
this.clearStateTimers();
|
|
1472
1577
|
};
|
|
1473
1578
|
connection.on('close', onClose);
|
|
1474
|
-
this.
|
|
1579
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1475
1580
|
}
|
|
1476
1581
|
clearStateTimers() {
|
|
1477
1582
|
const state = this._state;
|
|
@@ -1484,34 +1589,38 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1484
1589
|
state.interCharTimer = undefined;
|
|
1485
1590
|
}
|
|
1486
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
|
+
}
|
|
1487
1615
|
flushBuffer(strict) {
|
|
1488
1616
|
const state = this._state;
|
|
1489
1617
|
const isResponse = this.ROLE === 'MASTER';
|
|
1490
1618
|
const pool = state.pool;
|
|
1491
1619
|
const customFCs = this._customFunctionCodes;
|
|
1492
|
-
// Shared handler for every "frame is not yet complete" exit. Returns true
|
|
1493
|
-
// when the caller should `return` (strict reset), false to `break` the
|
|
1494
|
-
// parse loop. Hot path never reaches here — only error/incomplete edges.
|
|
1495
|
-
const handleIncomplete = () => {
|
|
1496
|
-
if (strict) {
|
|
1497
|
-
this.emit('framing-error', new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
|
|
1498
|
-
state.start = 0;
|
|
1499
|
-
state.end = 0;
|
|
1500
|
-
state.t15Expired = false;
|
|
1501
|
-
return true;
|
|
1502
|
-
}
|
|
1503
|
-
if (state.t15Expired) {
|
|
1504
|
-
this.emit('framing-error', new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1505
|
-
state.start = 0;
|
|
1506
|
-
state.end = 0;
|
|
1507
|
-
state.t15Expired = false;
|
|
1508
|
-
}
|
|
1509
|
-
return false;
|
|
1510
|
-
};
|
|
1511
1620
|
while (state.end - state.start > 0) {
|
|
1512
1621
|
const available = state.end - state.start;
|
|
1513
1622
|
if (available < MIN_FRAME_LENGTH) {
|
|
1514
|
-
if (
|
|
1623
|
+
if (this._handleIncomplete(state, strict))
|
|
1515
1624
|
return;
|
|
1516
1625
|
break;
|
|
1517
1626
|
}
|
|
@@ -1520,7 +1629,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1520
1629
|
let expected;
|
|
1521
1630
|
if (cfc) {
|
|
1522
1631
|
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1523
|
-
const predicted = predictor(pool
|
|
1632
|
+
const predicted = predictor(pool, state.start, state.end);
|
|
1524
1633
|
// Normalize custom predictor's `null` to the std sentinel so both
|
|
1525
1634
|
// paths share the same NEED_MORE tail below.
|
|
1526
1635
|
expected = predicted ?? PREDICT_NEED_MORE;
|
|
@@ -1528,9 +1637,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1528
1637
|
else {
|
|
1529
1638
|
// Standard FC path. predictRtuFrameLength uses sentinel returns to
|
|
1530
1639
|
// avoid per-call object allocation on the decode hot path.
|
|
1531
|
-
expected = predictRtuFrameLength(pool
|
|
1640
|
+
expected = predictRtuFrameLength(pool, state.start, state.end, isResponse);
|
|
1532
1641
|
if (expected === PREDICT_UNKNOWN) {
|
|
1533
|
-
this.
|
|
1642
|
+
this.onFramingError(new Error(`Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`));
|
|
1534
1643
|
state.start = 0;
|
|
1535
1644
|
state.end = 0;
|
|
1536
1645
|
return;
|
|
@@ -1541,12 +1650,12 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1541
1650
|
state.start += 1;
|
|
1542
1651
|
continue;
|
|
1543
1652
|
}
|
|
1544
|
-
if (
|
|
1653
|
+
if (this._handleIncomplete(state, strict))
|
|
1545
1654
|
return;
|
|
1546
1655
|
break;
|
|
1547
1656
|
}
|
|
1548
1657
|
if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
|
|
1549
|
-
this.
|
|
1658
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1550
1659
|
state.start = 0;
|
|
1551
1660
|
state.end = 0;
|
|
1552
1661
|
return;
|
|
@@ -1556,7 +1665,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1556
1665
|
state.start += 1;
|
|
1557
1666
|
continue;
|
|
1558
1667
|
}
|
|
1559
|
-
if (
|
|
1668
|
+
if (this._handleIncomplete(state, strict))
|
|
1560
1669
|
return;
|
|
1561
1670
|
break;
|
|
1562
1671
|
}
|
|
@@ -1567,7 +1676,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1567
1676
|
const actualCrc = crc(pool, crcStart, crcEnd);
|
|
1568
1677
|
if (expectedCrc !== actualCrc) {
|
|
1569
1678
|
if (strict) {
|
|
1570
|
-
this.
|
|
1679
|
+
this.onFramingError(new Error('CRC mismatch'));
|
|
1571
1680
|
state.start = 0;
|
|
1572
1681
|
state.end = 0;
|
|
1573
1682
|
state.t15Expired = false;
|
|
@@ -1588,7 +1697,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1588
1697
|
data: frameBuf.subarray(2, expected - 2),
|
|
1589
1698
|
buffer: frameBuf,
|
|
1590
1699
|
};
|
|
1591
|
-
this.
|
|
1700
|
+
this.onFraming(frame);
|
|
1592
1701
|
}
|
|
1593
1702
|
if (state.start > 0) {
|
|
1594
1703
|
if (state.start < state.end) {
|
|
@@ -1615,11 +1724,21 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1615
1724
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1616
1725
|
encode(unit, fc, data, transaction) {
|
|
1617
1726
|
const buffer = Buffer.allocUnsafe(data.length + 4);
|
|
1618
|
-
|
|
1619
|
-
buffer
|
|
1620
|
-
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
|
+
}
|
|
1621
1737
|
const crcEnd = buffer.length - 2;
|
|
1622
|
-
|
|
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;
|
|
1623
1742
|
return buffer;
|
|
1624
1743
|
}
|
|
1625
1744
|
}
|
|
@@ -1650,8 +1769,8 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1650
1769
|
ROLE;
|
|
1651
1770
|
lenientHex;
|
|
1652
1771
|
_connection;
|
|
1653
|
-
_state = { status: 'idle', frame:
|
|
1654
|
-
|
|
1772
|
+
_state = { status: 'idle', frame: new Uint8Array(MAX_ASCII_PAYLOAD), frameLen: 0 };
|
|
1773
|
+
_cleanupCbs = [];
|
|
1655
1774
|
get connection() {
|
|
1656
1775
|
return this._connection;
|
|
1657
1776
|
}
|
|
@@ -1675,108 +1794,138 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1675
1794
|
};
|
|
1676
1795
|
const onData = (data) => {
|
|
1677
1796
|
const state = this._state;
|
|
1678
|
-
data.
|
|
1797
|
+
for (let i = 0; i < data.length; i++) {
|
|
1798
|
+
const value = data[i];
|
|
1679
1799
|
switch (state.status) {
|
|
1680
1800
|
case 'idle': {
|
|
1681
1801
|
if (value === CHAR_CODE.COLON) {
|
|
1682
1802
|
state.status = 'reception';
|
|
1683
|
-
state.
|
|
1803
|
+
state.frameLen = 0;
|
|
1684
1804
|
}
|
|
1685
1805
|
break;
|
|
1686
1806
|
}
|
|
1687
1807
|
case 'reception': {
|
|
1688
1808
|
if (value === CHAR_CODE.COLON) {
|
|
1689
|
-
state.
|
|
1809
|
+
state.frameLen = 0;
|
|
1690
1810
|
}
|
|
1691
1811
|
else if (value === CHAR_CODE.CR) {
|
|
1692
1812
|
state.status = 'waiting end';
|
|
1693
1813
|
}
|
|
1694
|
-
else if (state.
|
|
1814
|
+
else if (state.frameLen >= MAX_ASCII_PAYLOAD) {
|
|
1695
1815
|
state.status = 'idle';
|
|
1696
|
-
state.
|
|
1697
|
-
this.
|
|
1816
|
+
state.frameLen = 0;
|
|
1817
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1698
1818
|
}
|
|
1699
1819
|
else if (!isHexChar(value)) {
|
|
1700
1820
|
state.status = 'idle';
|
|
1701
|
-
state.
|
|
1702
|
-
this.
|
|
1821
|
+
state.frameLen = 0;
|
|
1822
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1703
1823
|
}
|
|
1704
1824
|
else {
|
|
1705
|
-
state.frame.
|
|
1825
|
+
state.frame[state.frameLen++] = value;
|
|
1706
1826
|
}
|
|
1707
1827
|
break;
|
|
1708
1828
|
}
|
|
1709
1829
|
case 'waiting end': {
|
|
1710
1830
|
if (value === CHAR_CODE.COLON) {
|
|
1711
1831
|
state.status = 'reception';
|
|
1712
|
-
state.
|
|
1832
|
+
state.frameLen = 0;
|
|
1713
1833
|
}
|
|
1714
1834
|
else {
|
|
1715
1835
|
state.status = 'idle';
|
|
1716
1836
|
if (value === CHAR_CODE.LF) {
|
|
1717
|
-
this.framing(
|
|
1837
|
+
this.framing(state.frame, state.frameLen);
|
|
1718
1838
|
}
|
|
1719
1839
|
}
|
|
1720
1840
|
break;
|
|
1721
1841
|
}
|
|
1722
1842
|
}
|
|
1723
|
-
}
|
|
1843
|
+
}
|
|
1724
1844
|
};
|
|
1725
1845
|
connection.on('data', onData);
|
|
1726
|
-
this.
|
|
1846
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1727
1847
|
const onClose = () => {
|
|
1728
|
-
for (const fn of this.
|
|
1848
|
+
for (const fn of this._cleanupCbs) {
|
|
1729
1849
|
fn();
|
|
1730
1850
|
}
|
|
1731
|
-
this.
|
|
1851
|
+
this._cleanupCbs.length = 0;
|
|
1732
1852
|
};
|
|
1733
1853
|
connection.on('close', onClose);
|
|
1734
|
-
this.
|
|
1854
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1735
1855
|
}
|
|
1736
|
-
framing(hexChars) {
|
|
1737
|
-
if (
|
|
1738
|
-
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'));
|
|
1739
1874
|
return;
|
|
1740
1875
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
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'));
|
|
1743
1883
|
return;
|
|
1744
1884
|
}
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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]];
|
|
1751
1893
|
if (hi === 0xff || lo === 0xff) {
|
|
1752
|
-
this.
|
|
1894
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1753
1895
|
return;
|
|
1754
1896
|
}
|
|
1755
|
-
|
|
1897
|
+
data[i] = (hi << 4) | lo;
|
|
1756
1898
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
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'));
|
|
1766
1907
|
return;
|
|
1767
1908
|
}
|
|
1768
|
-
|
|
1909
|
+
const frame = {
|
|
1910
|
+
unit,
|
|
1911
|
+
fc,
|
|
1912
|
+
data,
|
|
1913
|
+
buffer: Buffer.copyBytesFrom(hexChars, 0, hexLen),
|
|
1914
|
+
};
|
|
1915
|
+
this.onFraming(frame);
|
|
1769
1916
|
}
|
|
1770
1917
|
flush() {
|
|
1771
|
-
this._state =
|
|
1918
|
+
this._state.status = 'idle';
|
|
1919
|
+
this._state.frameLen = 0;
|
|
1772
1920
|
}
|
|
1773
1921
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1774
1922
|
encode(unit, fc, data, transaction) {
|
|
1775
1923
|
const buffer = Buffer.allocUnsafe(data.length + 3);
|
|
1776
|
-
|
|
1777
|
-
buffer
|
|
1924
|
+
// Inline header + LRC — direct typed-array stores skip Buffer's per-call checks.
|
|
1925
|
+
buffer[0] = unit;
|
|
1926
|
+
buffer[1] = fc;
|
|
1778
1927
|
buffer.set(data, 2);
|
|
1779
|
-
buffer.
|
|
1928
|
+
buffer[buffer.length - 1] = lrc(buffer.subarray(0, -1));
|
|
1780
1929
|
const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
|
|
1781
1930
|
out[0] = CHAR_CODE.COLON;
|
|
1782
1931
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -1797,7 +1946,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1797
1946
|
_connection;
|
|
1798
1947
|
_transactionId = 1;
|
|
1799
1948
|
_buffer = EMPTY_BUFFER;
|
|
1800
|
-
|
|
1949
|
+
_cleanupCbs = [];
|
|
1801
1950
|
get connection() {
|
|
1802
1951
|
return this._connection;
|
|
1803
1952
|
}
|
|
@@ -1806,6 +1955,17 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1806
1955
|
this.ROLE = role;
|
|
1807
1956
|
this._connection = connection;
|
|
1808
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
|
+
}
|
|
1809
1969
|
let buffer = this._buffer;
|
|
1810
1970
|
if (buffer.length === 0) {
|
|
1811
1971
|
buffer = data;
|
|
@@ -1823,7 +1983,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1823
1983
|
break;
|
|
1824
1984
|
}
|
|
1825
1985
|
else {
|
|
1826
|
-
this.
|
|
1986
|
+
this.onFramingError(result.error);
|
|
1827
1987
|
buffer = EMPTY_BUFFER;
|
|
1828
1988
|
break;
|
|
1829
1989
|
}
|
|
@@ -1843,15 +2003,15 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1843
2003
|
}
|
|
1844
2004
|
};
|
|
1845
2005
|
connection.on('data', onData);
|
|
1846
|
-
this.
|
|
2006
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1847
2007
|
const onClose = () => {
|
|
1848
|
-
for (const fn of this.
|
|
2008
|
+
for (const fn of this._cleanupCbs) {
|
|
1849
2009
|
fn();
|
|
1850
2010
|
}
|
|
1851
|
-
this.
|
|
2011
|
+
this._cleanupCbs.length = 0;
|
|
1852
2012
|
};
|
|
1853
2013
|
connection.on('close', onClose);
|
|
1854
|
-
this.
|
|
2014
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1855
2015
|
}
|
|
1856
2016
|
tryExtract(buffer) {
|
|
1857
2017
|
if (buffer.length < 8) {
|
|
@@ -1860,7 +2020,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1860
2020
|
if (buffer[2] !== 0 || buffer[3] !== 0) {
|
|
1861
2021
|
return { kind: 'error', error: new Error('Invalid data') };
|
|
1862
2022
|
}
|
|
1863
|
-
const length = buffer
|
|
2023
|
+
const length = (buffer[4] << 8) | buffer[5]; // inline BE read
|
|
1864
2024
|
const total = 6 + length;
|
|
1865
2025
|
if (total > MAX_TCP_FRAME || length < 2) {
|
|
1866
2026
|
return { kind: 'error', error: new Error('Invalid data') };
|
|
@@ -1868,29 +2028,47 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1868
2028
|
if (buffer.length < total) {
|
|
1869
2029
|
return { kind: 'insufficient' };
|
|
1870
2030
|
}
|
|
1871
|
-
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) };
|
|
1872
2032
|
}
|
|
1873
2033
|
processFrame(buffer) {
|
|
1874
2034
|
const frame = {
|
|
1875
|
-
|
|
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],
|
|
1876
2039
|
unit: buffer[6],
|
|
1877
2040
|
fc: buffer[7],
|
|
1878
2041
|
data: buffer.subarray(8),
|
|
1879
2042
|
buffer,
|
|
1880
2043
|
};
|
|
1881
|
-
this.
|
|
2044
|
+
this.onFraming(frame);
|
|
1882
2045
|
}
|
|
1883
2046
|
flush() {
|
|
1884
2047
|
this._buffer = EMPTY_BUFFER;
|
|
1885
2048
|
}
|
|
1886
2049
|
encode(unit, fc, data, transaction) {
|
|
1887
2050
|
const buffer = Buffer.allocUnsafe(data.length + 8);
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
buffer
|
|
1893
|
-
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
|
+
}
|
|
1894
2072
|
if (transaction === undefined) {
|
|
1895
2073
|
this._transactionId = (this._transactionId + 1) % 65536 || 1;
|
|
1896
2074
|
}
|
|
@@ -1969,14 +2147,31 @@ class ModbusMaster extends EventEmitter {
|
|
|
1969
2147
|
_queueDatas = [];
|
|
1970
2148
|
_queueTimeouts = [];
|
|
1971
2149
|
_queueBroadcasts = [];
|
|
1972
|
-
|
|
1973
|
-
_queueRejects = [];
|
|
2150
|
+
_queueCallbacks = [];
|
|
1974
2151
|
_queueHead = 0;
|
|
1975
2152
|
_queueLen = 0;
|
|
1976
2153
|
_draining = false;
|
|
1977
2154
|
_nextTid = 1;
|
|
1978
2155
|
_cleanupFns = new Set();
|
|
1979
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
|
+
});
|
|
1980
2175
|
get state() {
|
|
1981
2176
|
return this._physicalLayer.state;
|
|
1982
2177
|
}
|
|
@@ -2003,17 +2198,21 @@ class ModbusMaster extends EventEmitter {
|
|
|
2003
2198
|
for (const cfc of this._customFunctionCodes.values()) {
|
|
2004
2199
|
appLayer.addCustomFunctionCode(cfc);
|
|
2005
2200
|
}
|
|
2006
|
-
const cleanupFraming = () =>
|
|
2201
|
+
const cleanupFraming = () => {
|
|
2202
|
+
appLayer.onFraming = NOOP;
|
|
2203
|
+
};
|
|
2007
2204
|
const onFraming = (frame) => {
|
|
2008
2205
|
this._masterSession.handleFrame(frame);
|
|
2009
2206
|
};
|
|
2010
|
-
appLayer.
|
|
2207
|
+
appLayer.onFraming = onFraming;
|
|
2011
2208
|
this._cleanupFns.add(cleanupFraming);
|
|
2012
|
-
const cleanupFramingError = () =>
|
|
2209
|
+
const cleanupFramingError = () => {
|
|
2210
|
+
appLayer.onFramingError = NOOP;
|
|
2211
|
+
};
|
|
2013
2212
|
const onFramingError = (error) => {
|
|
2014
2213
|
this._masterSession.handleError(error);
|
|
2015
2214
|
};
|
|
2016
|
-
appLayer.
|
|
2215
|
+
appLayer.onFramingError = onFramingError;
|
|
2017
2216
|
this._cleanupFns.add(cleanupFramingError);
|
|
2018
2217
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2019
2218
|
const onClose = () => {
|
|
@@ -2069,31 +2268,23 @@ class ModbusMaster extends EventEmitter {
|
|
|
2069
2268
|
}
|
|
2070
2269
|
return new AsciiApplicationLayer('MASTER', connection, this._protocol.opts);
|
|
2071
2270
|
}
|
|
2072
|
-
|
|
2271
|
+
_send(unit, fc, data, timeout, broadcast, callback) {
|
|
2073
2272
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2074
|
-
|
|
2273
|
+
callback(new Error('Master is not open'));
|
|
2274
|
+
return;
|
|
2075
2275
|
}
|
|
2076
2276
|
if (this.concurrent) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
if (err)
|
|
2080
|
-
reject(err);
|
|
2081
|
-
else
|
|
2082
|
-
resolve(frame);
|
|
2083
|
-
});
|
|
2084
|
-
});
|
|
2277
|
+
this._exchange(unit, fc, data, timeout, broadcast, callback);
|
|
2278
|
+
return;
|
|
2085
2279
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
this._queueLen++;
|
|
2095
|
-
this._drain();
|
|
2096
|
-
});
|
|
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();
|
|
2097
2288
|
}
|
|
2098
2289
|
_drain() {
|
|
2099
2290
|
if (this._draining || this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
@@ -2114,14 +2305,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2114
2305
|
const data = this._queueDatas[h];
|
|
2115
2306
|
const timeout = this._queueTimeouts[h];
|
|
2116
2307
|
const broadcast = this._queueBroadcasts[h];
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
// 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
|
|
2120
2310
|
// closures while the rest of the queue is still draining. Primitives
|
|
2121
2311
|
// (unit/fc/timeout/broadcast) need no clearing.
|
|
2122
2312
|
this._queueDatas[h] = undefined;
|
|
2123
|
-
this.
|
|
2124
|
-
this._queueRejects[h] = undefined;
|
|
2313
|
+
this._queueCallbacks[h] = undefined;
|
|
2125
2314
|
this._queueHead = h + 1;
|
|
2126
2315
|
this._queueLen--;
|
|
2127
2316
|
if (this._queueLen === 0) {
|
|
@@ -2132,15 +2321,11 @@ class ModbusMaster extends EventEmitter {
|
|
|
2132
2321
|
this._queueDatas.length = 0;
|
|
2133
2322
|
this._queueTimeouts.length = 0;
|
|
2134
2323
|
this._queueBroadcasts.length = 0;
|
|
2135
|
-
this.
|
|
2136
|
-
this._queueRejects.length = 0;
|
|
2324
|
+
this._queueCallbacks.length = 0;
|
|
2137
2325
|
this._queueHead = 0;
|
|
2138
2326
|
}
|
|
2139
2327
|
this._exchange(unit, fc, data, timeout, broadcast, (err, frame) => {
|
|
2140
|
-
|
|
2141
|
-
reject(err);
|
|
2142
|
-
else
|
|
2143
|
-
resolve(frame);
|
|
2328
|
+
callback(err, frame);
|
|
2144
2329
|
this._processNext();
|
|
2145
2330
|
});
|
|
2146
2331
|
}
|
|
@@ -2160,30 +2345,35 @@ class ModbusMaster extends EventEmitter {
|
|
|
2160
2345
|
if (!this.concurrent) {
|
|
2161
2346
|
appLayer.flush();
|
|
2162
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);
|
|
2163
2356
|
if (broadcast) {
|
|
2164
|
-
// Broadcast: no response expected. Skip the session entirely
|
|
2165
|
-
|
|
2166
|
-
// broadcast's stop() would clear the other request's slot/timer).
|
|
2167
|
-
let settled = false;
|
|
2168
|
-
const timer = setTimeout(() => {
|
|
2169
|
-
if (settled)
|
|
2170
|
-
return;
|
|
2171
|
-
settled = true;
|
|
2172
|
-
callback(new Error('Timeout'));
|
|
2173
|
-
}, timeout);
|
|
2357
|
+
// Broadcast: no response expected. Skip the session entirely.
|
|
2358
|
+
this._timerHeap.add(exchangeId, timeout);
|
|
2174
2359
|
connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
|
|
2175
|
-
|
|
2360
|
+
const p = this._pendingExchanges.get(exchangeId);
|
|
2361
|
+
if (!p || p.settled)
|
|
2362
|
+
return;
|
|
2363
|
+
const cb = p.callback;
|
|
2364
|
+
if (!cb)
|
|
2176
2365
|
return;
|
|
2177
|
-
|
|
2178
|
-
|
|
2366
|
+
p.settled = true;
|
|
2367
|
+
p.callback = null;
|
|
2368
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2179
2369
|
if (writeErr) {
|
|
2180
|
-
|
|
2370
|
+
cb(writeErr);
|
|
2181
2371
|
}
|
|
2182
2372
|
else if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2183
|
-
|
|
2373
|
+
cb(new Error('Master is not open'));
|
|
2184
2374
|
}
|
|
2185
2375
|
else {
|
|
2186
|
-
|
|
2376
|
+
cb(null);
|
|
2187
2377
|
}
|
|
2188
2378
|
});
|
|
2189
2379
|
return;
|
|
@@ -2197,62 +2387,82 @@ class ModbusMaster extends EventEmitter {
|
|
|
2197
2387
|
}
|
|
2198
2388
|
const key = tid ?? FIFO_KEY;
|
|
2199
2389
|
const payload = appLayer.encode(unit, fc, data, tid);
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
// settled guard prevents double-invocation when timeout fires during write.
|
|
2203
|
-
let settled = false;
|
|
2204
|
-
const timer = setTimeout(() => {
|
|
2205
|
-
if (settled)
|
|
2206
|
-
return;
|
|
2207
|
-
settled = true;
|
|
2208
|
-
this._masterSession.stop(key);
|
|
2209
|
-
callback(new Error('Timeout'));
|
|
2210
|
-
}, timeout);
|
|
2390
|
+
pending.sessionKey = key;
|
|
2391
|
+
this._timerHeap.add(exchangeId, timeout);
|
|
2211
2392
|
connection.write(payload, (writeErr) => {
|
|
2393
|
+
const p = this._pendingExchanges.get(exchangeId);
|
|
2394
|
+
if (!p || p.settled)
|
|
2395
|
+
return;
|
|
2212
2396
|
if (writeErr) {
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
settled = true;
|
|
2216
|
-
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);
|
|
2217
2403
|
}
|
|
2218
2404
|
return;
|
|
2219
2405
|
}
|
|
2220
2406
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
settled = true;
|
|
2224
|
-
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'));
|
|
2225
2413
|
}
|
|
2226
2414
|
return;
|
|
2227
2415
|
}
|
|
2228
2416
|
// Write succeeded — register in session for frame matching only.
|
|
2229
|
-
// Timeout is managed by the
|
|
2417
|
+
// Timeout is managed by the global timer heap above.
|
|
2230
2418
|
this._masterSession.start(key, (err, frame) => {
|
|
2231
|
-
|
|
2419
|
+
const p2 = this._pendingExchanges.get(exchangeId);
|
|
2420
|
+
if (!p2 || p2.settled)
|
|
2232
2421
|
return;
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
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
|
+
}
|
|
2236
2429
|
});
|
|
2237
2430
|
});
|
|
2238
2431
|
}
|
|
2239
2432
|
writeFC1Or2(unit, fc, address, length, timeout) {
|
|
2240
2433
|
const byteCount = Math.ceil(length / 8);
|
|
2241
2434
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
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
|
+
});
|
|
2256
2466
|
});
|
|
2257
2467
|
}
|
|
2258
2468
|
writeFC1;
|
|
@@ -2266,19 +2476,41 @@ class ModbusMaster extends EventEmitter {
|
|
|
2266
2476
|
writeFC3Or4(unit, fc, address, length, timeout) {
|
|
2267
2477
|
const byteCount = length * 2;
|
|
2268
2478
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2269
|
-
|
|
2270
|
-
bufferTx
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
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
|
+
});
|
|
2282
2514
|
});
|
|
2283
2515
|
}
|
|
2284
2516
|
writeFC3;
|
|
@@ -2293,28 +2525,61 @@ class ModbusMaster extends EventEmitter {
|
|
|
2293
2525
|
writeSingleCoil(unit, address, value, timeout = this.timeout) {
|
|
2294
2526
|
const fc = exports.FunctionCode.WRITE_SINGLE_COIL;
|
|
2295
2527
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
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
|
+
});
|
|
2304
2553
|
});
|
|
2305
2554
|
}
|
|
2306
2555
|
writeFC6;
|
|
2307
2556
|
writeSingleRegister(unit, address, value, timeout = this.timeout) {
|
|
2308
2557
|
const fc = exports.FunctionCode.WRITE_SINGLE_REGISTER;
|
|
2309
2558
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2310
|
-
|
|
2311
|
-
bufferTx
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
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
|
+
});
|
|
2318
2583
|
});
|
|
2319
2584
|
}
|
|
2320
2585
|
writeFC15;
|
|
@@ -2322,20 +2587,36 @@ class ModbusMaster extends EventEmitter {
|
|
|
2322
2587
|
const fc = exports.FunctionCode.WRITE_MULTIPLE_COILS;
|
|
2323
2588
|
const byteCount = Math.ceil(value.length / 8);
|
|
2324
2589
|
const bufferTx = Buffer.alloc(5 + byteCount);
|
|
2325
|
-
|
|
2326
|
-
bufferTx
|
|
2327
|
-
bufferTx
|
|
2328
|
-
value.
|
|
2329
|
-
|
|
2330
|
-
|
|
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;
|
|
2331
2599
|
}
|
|
2332
|
-
}
|
|
2333
|
-
return
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
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
|
+
});
|
|
2339
2620
|
});
|
|
2340
2621
|
}
|
|
2341
2622
|
writeFC16;
|
|
@@ -2343,53 +2624,102 @@ class ModbusMaster extends EventEmitter {
|
|
|
2343
2624
|
const fc = exports.FunctionCode.WRITE_MULTIPLE_REGISTERS;
|
|
2344
2625
|
const byteCount = value.length * 2;
|
|
2345
2626
|
const bufferTx = Buffer.allocUnsafe(5 + byteCount);
|
|
2346
|
-
|
|
2347
|
-
bufferTx
|
|
2348
|
-
bufferTx
|
|
2349
|
-
value.
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
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
|
+
});
|
|
2358
2658
|
});
|
|
2359
2659
|
}
|
|
2360
2660
|
handleFC17;
|
|
2361
2661
|
reportServerId(unit, serverIdLength = 1, timeout = this.timeout) {
|
|
2362
2662
|
const fc = exports.FunctionCode.REPORT_SERVER_ID;
|
|
2363
|
-
return
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
+
});
|
|
2378
2691
|
});
|
|
2379
2692
|
}
|
|
2380
2693
|
handleFC22;
|
|
2381
2694
|
maskWriteRegister(unit, address, andMask, orMask, timeout = this.timeout) {
|
|
2382
2695
|
const fc = exports.FunctionCode.MASK_WRITE_REGISTER;
|
|
2383
2696
|
const bufferTx = Buffer.allocUnsafe(6);
|
|
2384
|
-
|
|
2385
|
-
bufferTx
|
|
2386
|
-
bufferTx
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
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
|
+
});
|
|
2393
2723
|
});
|
|
2394
2724
|
}
|
|
2395
2725
|
handleFC23;
|
|
@@ -2398,71 +2728,115 @@ class ModbusMaster extends EventEmitter {
|
|
|
2398
2728
|
const byteCount = write.value.length * 2;
|
|
2399
2729
|
const readByteCount = read.length * 2;
|
|
2400
2730
|
const bufferTx = Buffer.allocUnsafe(9 + byteCount);
|
|
2401
|
-
|
|
2402
|
-
bufferTx
|
|
2403
|
-
bufferTx.
|
|
2404
|
-
bufferTx
|
|
2405
|
-
bufferTx.
|
|
2406
|
-
write.
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
const
|
|
2414
|
-
|
|
2415
|
-
|
|
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
|
+
});
|
|
2416
2775
|
});
|
|
2417
2776
|
}
|
|
2418
2777
|
handleFC43_14;
|
|
2419
2778
|
readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout = this.timeout) {
|
|
2420
2779
|
const fc = exports.FunctionCode.READ_DEVICE_IDENTIFICATION;
|
|
2421
|
-
return
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
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;
|
|
2449
2821
|
}
|
|
2450
|
-
break;
|
|
2451
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);
|
|
2452
2835
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
throw new Error('Invalid response');
|
|
2458
|
-
frame.data = {
|
|
2459
|
-
readDeviceIDCode,
|
|
2460
|
-
conformityLevel: frame.data[2],
|
|
2461
|
-
moreFollows: frame.data[3] === 0xff,
|
|
2462
|
-
nextObjectId: frame.data[4],
|
|
2463
|
-
objects,
|
|
2464
|
-
};
|
|
2465
|
-
return frame;
|
|
2836
|
+
catch (e) {
|
|
2837
|
+
reject(e);
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2466
2840
|
});
|
|
2467
2841
|
}
|
|
2468
2842
|
addCustomFunctionCode(cfc) {
|
|
@@ -2475,11 +2849,24 @@ class ModbusMaster extends EventEmitter {
|
|
|
2475
2849
|
}
|
|
2476
2850
|
sendCustomFC(unit, fc, data, timeout = this.timeout) {
|
|
2477
2851
|
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
2478
|
-
return
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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
|
+
});
|
|
2483
2870
|
});
|
|
2484
2871
|
}
|
|
2485
2872
|
/**
|
|
@@ -2505,9 +2892,24 @@ class ModbusMaster extends EventEmitter {
|
|
|
2505
2892
|
const end = this._queueHead + this._queueLen;
|
|
2506
2893
|
this._queueLen = 0;
|
|
2507
2894
|
for (let i = this._queueHead; i < end; i++) {
|
|
2508
|
-
this.
|
|
2895
|
+
this._queueCallbacks[i](rejectErr);
|
|
2509
2896
|
}
|
|
2510
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();
|
|
2511
2913
|
let err = null;
|
|
2512
2914
|
try {
|
|
2513
2915
|
await promisifyCb((cb) => this._physicalLayer.close(cb));
|
|
@@ -2544,7 +2946,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
2544
2946
|
_protocol;
|
|
2545
2947
|
_appLayers = new Map();
|
|
2546
2948
|
_customFunctionCodes = new Map();
|
|
2547
|
-
|
|
2949
|
+
// Active interval locks. Typical length: 0 (no contention) — 1-2 (cross-
|
|
2950
|
+
// connection contention). Linear scan for overlap is sub-µs.
|
|
2951
|
+
_intervalLocks = [];
|
|
2548
2952
|
_cleanupFns = new Set();
|
|
2549
2953
|
_closePromise = null;
|
|
2550
2954
|
get state() {
|
|
@@ -2571,7 +2975,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
2571
2975
|
for (const cfc of this._customFunctionCodes.values()) {
|
|
2572
2976
|
appLayer.addCustomFunctionCode(cfc);
|
|
2573
2977
|
}
|
|
2574
|
-
const cleanupFraming = () =>
|
|
2978
|
+
const cleanupFraming = () => {
|
|
2979
|
+
appLayer.onFraming = NOOP;
|
|
2980
|
+
};
|
|
2575
2981
|
const onFraming = (frame) => {
|
|
2576
2982
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2577
2983
|
return;
|
|
@@ -2586,7 +2992,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2586
2992
|
appLayerData.queues.frames.push(frame);
|
|
2587
2993
|
this._drain(appLayer, appLayerData.queues);
|
|
2588
2994
|
};
|
|
2589
|
-
appLayer.
|
|
2995
|
+
appLayer.onFraming = onFraming;
|
|
2590
2996
|
this._cleanupFns.add(cleanupFraming);
|
|
2591
2997
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2592
2998
|
const onClose = () => {
|
|
@@ -2653,12 +3059,23 @@ class ModbusSlave extends EventEmitter {
|
|
|
2653
3059
|
const byteCount = (length + 7) >> 3;
|
|
2654
3060
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
2655
3061
|
pdu[0] = byteCount;
|
|
2656
|
-
|
|
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;
|
|
2657
3069
|
for (let i = 0; i < length; i++) {
|
|
2658
|
-
if (coils[i])
|
|
2659
|
-
|
|
3070
|
+
if (coils[i])
|
|
3071
|
+
acc |= 1 << (i & 7);
|
|
3072
|
+
if ((i & 7) === 7) {
|
|
3073
|
+
pdu[out++] = acc;
|
|
3074
|
+
acc = 0;
|
|
2660
3075
|
}
|
|
2661
3076
|
}
|
|
3077
|
+
if ((length & 7) !== 0)
|
|
3078
|
+
pdu[out] = acc;
|
|
2662
3079
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2663
3080
|
}
|
|
2664
3081
|
catch (error) {
|
|
@@ -2689,12 +3106,19 @@ class ModbusSlave extends EventEmitter {
|
|
|
2689
3106
|
const byteCount = (length + 7) >> 3;
|
|
2690
3107
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
2691
3108
|
pdu[0] = byteCount;
|
|
2692
|
-
|
|
3109
|
+
// Accumulator-based bit pack — see handleFC1 for the rationale.
|
|
3110
|
+
let acc = 0;
|
|
3111
|
+
let out = 1;
|
|
2693
3112
|
for (let i = 0; i < length; i++) {
|
|
2694
|
-
if (discreteInputs[i])
|
|
2695
|
-
|
|
3113
|
+
if (discreteInputs[i])
|
|
3114
|
+
acc |= 1 << (i & 7);
|
|
3115
|
+
if ((i & 7) === 7) {
|
|
3116
|
+
pdu[out++] = acc;
|
|
3117
|
+
acc = 0;
|
|
2696
3118
|
}
|
|
2697
3119
|
}
|
|
3120
|
+
if ((length & 7) !== 0)
|
|
3121
|
+
pdu[out] = acc;
|
|
2698
3122
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2699
3123
|
}
|
|
2700
3124
|
catch (error) {
|
|
@@ -2724,8 +3148,14 @@ class ModbusSlave extends EventEmitter {
|
|
|
2724
3148
|
const registers = await model.readHoldingRegisters(address, length);
|
|
2725
3149
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
2726
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.
|
|
2727
3154
|
for (let i = 0; i < length; i++) {
|
|
2728
|
-
|
|
3155
|
+
const v = registers[i];
|
|
3156
|
+
const off = 1 + i * 2;
|
|
3157
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3158
|
+
pdu[off + 1] = v & 0xff;
|
|
2729
3159
|
}
|
|
2730
3160
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2731
3161
|
}
|
|
@@ -2756,8 +3186,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
2756
3186
|
const registers = await model.readInputRegisters(address, length);
|
|
2757
3187
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
2758
3188
|
pdu[0] = length * 2;
|
|
3189
|
+
// Inline big-endian write — see handleFC3 for the rationale.
|
|
2759
3190
|
for (let i = 0; i < length; i++) {
|
|
2760
|
-
|
|
3191
|
+
const v = registers[i];
|
|
3192
|
+
const off = 1 + i * 2;
|
|
3193
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3194
|
+
pdu[off + 1] = v & 0xff;
|
|
2761
3195
|
}
|
|
2762
3196
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2763
3197
|
}
|
|
@@ -2837,6 +3271,10 @@ class ModbusSlave extends EventEmitter {
|
|
|
2837
3271
|
}
|
|
2838
3272
|
const value = new Array(length);
|
|
2839
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).
|
|
2840
3278
|
value[i] = (frame.data[5 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
|
|
2841
3279
|
}
|
|
2842
3280
|
try {
|
|
@@ -2941,7 +3379,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2941
3379
|
return;
|
|
2942
3380
|
}
|
|
2943
3381
|
try {
|
|
2944
|
-
await this.
|
|
3382
|
+
await this._withIntervalLock(address, address + 1, async () => {
|
|
2945
3383
|
if (model.maskWriteRegister) {
|
|
2946
3384
|
await model.maskWriteRegister(address, andMask, orMask);
|
|
2947
3385
|
}
|
|
@@ -2991,8 +3429,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2991
3429
|
value[i] = (frame.data[9 + i * 2] << 8) | frame.data[10 + i * 2];
|
|
2992
3430
|
}
|
|
2993
3431
|
try {
|
|
2994
|
-
|
|
2995
|
-
await this._withAddressLock(writeAddresses, async () => {
|
|
3432
|
+
await this._withIntervalLock(address.write, address.write + length.write, async () => {
|
|
2996
3433
|
if (model.writeMultipleRegisters) {
|
|
2997
3434
|
await model.writeMultipleRegisters(address.write, value);
|
|
2998
3435
|
}
|
|
@@ -3005,8 +3442,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
3005
3442
|
const registers = await model.readHoldingRegisters(address.read, length.read);
|
|
3006
3443
|
const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
|
|
3007
3444
|
pdu[0] = length.read * 2;
|
|
3445
|
+
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3008
3446
|
for (let i = 0; i < length.read; i++) {
|
|
3009
|
-
|
|
3447
|
+
const v = registers[i];
|
|
3448
|
+
const off = 1 + i * 2;
|
|
3449
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3450
|
+
pdu[off + 1] = v & 0xff;
|
|
3010
3451
|
}
|
|
3011
3452
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3012
3453
|
}
|
|
@@ -3165,7 +3606,11 @@ class ModbusSlave extends EventEmitter {
|
|
|
3165
3606
|
return Promise.resolve();
|
|
3166
3607
|
}
|
|
3167
3608
|
return new Promise((resolve) => {
|
|
3168
|
-
|
|
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);
|
|
3169
3614
|
});
|
|
3170
3615
|
};
|
|
3171
3616
|
// Hot path: unicast to a known unit dispatches to a single model.
|
|
@@ -3214,24 +3659,66 @@ class ModbusSlave extends EventEmitter {
|
|
|
3214
3659
|
return true;
|
|
3215
3660
|
}
|
|
3216
3661
|
}
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
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);
|
|
3226
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);
|
|
3227
3711
|
try {
|
|
3228
3712
|
return await work;
|
|
3229
3713
|
}
|
|
3230
3714
|
finally {
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
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();
|
|
3235
3722
|
}
|
|
3236
3723
|
}
|
|
3237
3724
|
}
|
|
@@ -3362,6 +3849,7 @@ exports.MasterSession = MasterSession;
|
|
|
3362
3849
|
exports.ModbusError = ModbusError;
|
|
3363
3850
|
exports.ModbusMaster = ModbusMaster;
|
|
3364
3851
|
exports.ModbusSlave = ModbusSlave;
|
|
3852
|
+
exports.NOOP = NOOP;
|
|
3365
3853
|
exports.RtuApplicationLayer = RtuApplicationLayer;
|
|
3366
3854
|
exports.SerialPhysicalLayer = SerialPhysicalLayer;
|
|
3367
3855
|
exports.TcpApplicationLayer = TcpApplicationLayer;
|