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.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`.
|
|
@@ -1192,12 +1301,6 @@ class UdpServerPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
1192
1301
|
super();
|
|
1193
1302
|
this._opts = options ?? {};
|
|
1194
1303
|
}
|
|
1195
|
-
/**
|
|
1196
|
-
* Bind the UDP socket and start accepting datagrams.
|
|
1197
|
-
*
|
|
1198
|
-
* @param options Bind options (port, address, etc.). Defaults to port 502.
|
|
1199
|
-
* @param cb Callback invoked when binding completes or fails.
|
|
1200
|
-
*/
|
|
1201
1304
|
open(options, cb) {
|
|
1202
1305
|
// Node-style callback overload: if the first arg is a function, treat it as cb.
|
|
1203
1306
|
if (typeof options === 'function') {
|
|
@@ -1345,14 +1448,16 @@ function createPhysicalLayer(config) {
|
|
|
1345
1448
|
* established and discarded when the connection closes. Subclasses implement
|
|
1346
1449
|
* ASCII, RTU, or TCP framing rules.
|
|
1347
1450
|
*/
|
|
1348
|
-
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;
|
|
1349
1456
|
flush() {
|
|
1350
1457
|
// no-op — override in subclasses
|
|
1351
1458
|
}
|
|
1352
|
-
addCustomFunctionCode(cfc) {
|
|
1353
|
-
}
|
|
1354
|
-
removeCustomFunctionCode(fc) {
|
|
1355
|
-
}
|
|
1459
|
+
addCustomFunctionCode(cfc) { }
|
|
1460
|
+
removeCustomFunctionCode(fc) { }
|
|
1356
1461
|
}
|
|
1357
1462
|
|
|
1358
1463
|
const MAX_FRAME_LENGTH = 256;
|
|
@@ -1366,7 +1471,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1366
1471
|
_threePointFiveT;
|
|
1367
1472
|
_onePointFiveT;
|
|
1368
1473
|
_customFunctionCodes = new Map();
|
|
1369
|
-
|
|
1474
|
+
_cleanupCbs = [];
|
|
1370
1475
|
get connection() {
|
|
1371
1476
|
return this._connection;
|
|
1372
1477
|
}
|
|
@@ -1382,7 +1487,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1382
1487
|
const onData = (data) => {
|
|
1383
1488
|
const state = this._state;
|
|
1384
1489
|
if (state.t15Expired && state.end > state.start) {
|
|
1385
|
-
this.
|
|
1490
|
+
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1386
1491
|
state.start = 0;
|
|
1387
1492
|
state.end = 0;
|
|
1388
1493
|
}
|
|
@@ -1424,7 +1529,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1424
1529
|
// flushBuffer freed nothing — the entire pool is unparseable
|
|
1425
1530
|
// residue (typically a misconfigured poolSize for the wire's
|
|
1426
1531
|
// frame size). Hard reset; we cannot recover automatically.
|
|
1427
|
-
this.
|
|
1532
|
+
this.onFramingError(new Error('Frame buffer exhausted before complete frame received'));
|
|
1428
1533
|
currentState.start = 0;
|
|
1429
1534
|
currentState.end = 0;
|
|
1430
1535
|
currentState.t15Expired = false;
|
|
@@ -1460,16 +1565,16 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1460
1565
|
}
|
|
1461
1566
|
};
|
|
1462
1567
|
connection.on('data', onData);
|
|
1463
|
-
this.
|
|
1568
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1464
1569
|
const onClose = () => {
|
|
1465
|
-
for (const fn of this.
|
|
1570
|
+
for (const fn of this._cleanupCbs) {
|
|
1466
1571
|
fn();
|
|
1467
1572
|
}
|
|
1468
|
-
this.
|
|
1573
|
+
this._cleanupCbs.length = 0;
|
|
1469
1574
|
this.clearStateTimers();
|
|
1470
1575
|
};
|
|
1471
1576
|
connection.on('close', onClose);
|
|
1472
|
-
this.
|
|
1577
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1473
1578
|
}
|
|
1474
1579
|
clearStateTimers() {
|
|
1475
1580
|
const state = this._state;
|
|
@@ -1482,34 +1587,38 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1482
1587
|
state.interCharTimer = undefined;
|
|
1483
1588
|
}
|
|
1484
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
|
+
}
|
|
1485
1613
|
flushBuffer(strict) {
|
|
1486
1614
|
const state = this._state;
|
|
1487
1615
|
const isResponse = this.ROLE === 'MASTER';
|
|
1488
1616
|
const pool = state.pool;
|
|
1489
1617
|
const customFCs = this._customFunctionCodes;
|
|
1490
|
-
// Shared handler for every "frame is not yet complete" exit. Returns true
|
|
1491
|
-
// when the caller should `return` (strict reset), false to `break` the
|
|
1492
|
-
// parse loop. Hot path never reaches here — only error/incomplete edges.
|
|
1493
|
-
const handleIncomplete = () => {
|
|
1494
|
-
if (strict) {
|
|
1495
|
-
this.emit('framing-error', new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
|
|
1496
|
-
state.start = 0;
|
|
1497
|
-
state.end = 0;
|
|
1498
|
-
state.t15Expired = false;
|
|
1499
|
-
return true;
|
|
1500
|
-
}
|
|
1501
|
-
if (state.t15Expired) {
|
|
1502
|
-
this.emit('framing-error', new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1503
|
-
state.start = 0;
|
|
1504
|
-
state.end = 0;
|
|
1505
|
-
state.t15Expired = false;
|
|
1506
|
-
}
|
|
1507
|
-
return false;
|
|
1508
|
-
};
|
|
1509
1618
|
while (state.end - state.start > 0) {
|
|
1510
1619
|
const available = state.end - state.start;
|
|
1511
1620
|
if (available < MIN_FRAME_LENGTH) {
|
|
1512
|
-
if (
|
|
1621
|
+
if (this._handleIncomplete(state, strict))
|
|
1513
1622
|
return;
|
|
1514
1623
|
break;
|
|
1515
1624
|
}
|
|
@@ -1518,7 +1627,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1518
1627
|
let expected;
|
|
1519
1628
|
if (cfc) {
|
|
1520
1629
|
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1521
|
-
const predicted = predictor(pool
|
|
1630
|
+
const predicted = predictor(pool, state.start, state.end);
|
|
1522
1631
|
// Normalize custom predictor's `null` to the std sentinel so both
|
|
1523
1632
|
// paths share the same NEED_MORE tail below.
|
|
1524
1633
|
expected = predicted ?? PREDICT_NEED_MORE;
|
|
@@ -1526,9 +1635,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1526
1635
|
else {
|
|
1527
1636
|
// Standard FC path. predictRtuFrameLength uses sentinel returns to
|
|
1528
1637
|
// avoid per-call object allocation on the decode hot path.
|
|
1529
|
-
expected = predictRtuFrameLength(pool
|
|
1638
|
+
expected = predictRtuFrameLength(pool, state.start, state.end, isResponse);
|
|
1530
1639
|
if (expected === PREDICT_UNKNOWN) {
|
|
1531
|
-
this.
|
|
1640
|
+
this.onFramingError(new Error(`Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`));
|
|
1532
1641
|
state.start = 0;
|
|
1533
1642
|
state.end = 0;
|
|
1534
1643
|
return;
|
|
@@ -1539,12 +1648,12 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1539
1648
|
state.start += 1;
|
|
1540
1649
|
continue;
|
|
1541
1650
|
}
|
|
1542
|
-
if (
|
|
1651
|
+
if (this._handleIncomplete(state, strict))
|
|
1543
1652
|
return;
|
|
1544
1653
|
break;
|
|
1545
1654
|
}
|
|
1546
1655
|
if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
|
|
1547
|
-
this.
|
|
1656
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1548
1657
|
state.start = 0;
|
|
1549
1658
|
state.end = 0;
|
|
1550
1659
|
return;
|
|
@@ -1554,7 +1663,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1554
1663
|
state.start += 1;
|
|
1555
1664
|
continue;
|
|
1556
1665
|
}
|
|
1557
|
-
if (
|
|
1666
|
+
if (this._handleIncomplete(state, strict))
|
|
1558
1667
|
return;
|
|
1559
1668
|
break;
|
|
1560
1669
|
}
|
|
@@ -1565,7 +1674,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1565
1674
|
const actualCrc = crc(pool, crcStart, crcEnd);
|
|
1566
1675
|
if (expectedCrc !== actualCrc) {
|
|
1567
1676
|
if (strict) {
|
|
1568
|
-
this.
|
|
1677
|
+
this.onFramingError(new Error('CRC mismatch'));
|
|
1569
1678
|
state.start = 0;
|
|
1570
1679
|
state.end = 0;
|
|
1571
1680
|
state.t15Expired = false;
|
|
@@ -1586,7 +1695,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1586
1695
|
data: frameBuf.subarray(2, expected - 2),
|
|
1587
1696
|
buffer: frameBuf,
|
|
1588
1697
|
};
|
|
1589
|
-
this.
|
|
1698
|
+
this.onFraming(frame);
|
|
1590
1699
|
}
|
|
1591
1700
|
if (state.start > 0) {
|
|
1592
1701
|
if (state.start < state.end) {
|
|
@@ -1613,11 +1722,21 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1613
1722
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1614
1723
|
encode(unit, fc, data, transaction) {
|
|
1615
1724
|
const buffer = Buffer.allocUnsafe(data.length + 4);
|
|
1616
|
-
|
|
1617
|
-
buffer
|
|
1618
|
-
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
|
+
}
|
|
1619
1735
|
const crcEnd = buffer.length - 2;
|
|
1620
|
-
|
|
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;
|
|
1621
1740
|
return buffer;
|
|
1622
1741
|
}
|
|
1623
1742
|
}
|
|
@@ -1648,8 +1767,8 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1648
1767
|
ROLE;
|
|
1649
1768
|
lenientHex;
|
|
1650
1769
|
_connection;
|
|
1651
|
-
_state = { status: 'idle', frame:
|
|
1652
|
-
|
|
1770
|
+
_state = { status: 'idle', frame: new Uint8Array(MAX_ASCII_PAYLOAD), frameLen: 0 };
|
|
1771
|
+
_cleanupCbs = [];
|
|
1653
1772
|
get connection() {
|
|
1654
1773
|
return this._connection;
|
|
1655
1774
|
}
|
|
@@ -1673,108 +1792,138 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1673
1792
|
};
|
|
1674
1793
|
const onData = (data) => {
|
|
1675
1794
|
const state = this._state;
|
|
1676
|
-
data.
|
|
1795
|
+
for (let i = 0; i < data.length; i++) {
|
|
1796
|
+
const value = data[i];
|
|
1677
1797
|
switch (state.status) {
|
|
1678
1798
|
case 'idle': {
|
|
1679
1799
|
if (value === CHAR_CODE.COLON) {
|
|
1680
1800
|
state.status = 'reception';
|
|
1681
|
-
state.
|
|
1801
|
+
state.frameLen = 0;
|
|
1682
1802
|
}
|
|
1683
1803
|
break;
|
|
1684
1804
|
}
|
|
1685
1805
|
case 'reception': {
|
|
1686
1806
|
if (value === CHAR_CODE.COLON) {
|
|
1687
|
-
state.
|
|
1807
|
+
state.frameLen = 0;
|
|
1688
1808
|
}
|
|
1689
1809
|
else if (value === CHAR_CODE.CR) {
|
|
1690
1810
|
state.status = 'waiting end';
|
|
1691
1811
|
}
|
|
1692
|
-
else if (state.
|
|
1812
|
+
else if (state.frameLen >= MAX_ASCII_PAYLOAD) {
|
|
1693
1813
|
state.status = 'idle';
|
|
1694
|
-
state.
|
|
1695
|
-
this.
|
|
1814
|
+
state.frameLen = 0;
|
|
1815
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1696
1816
|
}
|
|
1697
1817
|
else if (!isHexChar(value)) {
|
|
1698
1818
|
state.status = 'idle';
|
|
1699
|
-
state.
|
|
1700
|
-
this.
|
|
1819
|
+
state.frameLen = 0;
|
|
1820
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1701
1821
|
}
|
|
1702
1822
|
else {
|
|
1703
|
-
state.frame.
|
|
1823
|
+
state.frame[state.frameLen++] = value;
|
|
1704
1824
|
}
|
|
1705
1825
|
break;
|
|
1706
1826
|
}
|
|
1707
1827
|
case 'waiting end': {
|
|
1708
1828
|
if (value === CHAR_CODE.COLON) {
|
|
1709
1829
|
state.status = 'reception';
|
|
1710
|
-
state.
|
|
1830
|
+
state.frameLen = 0;
|
|
1711
1831
|
}
|
|
1712
1832
|
else {
|
|
1713
1833
|
state.status = 'idle';
|
|
1714
1834
|
if (value === CHAR_CODE.LF) {
|
|
1715
|
-
this.framing(
|
|
1835
|
+
this.framing(state.frame, state.frameLen);
|
|
1716
1836
|
}
|
|
1717
1837
|
}
|
|
1718
1838
|
break;
|
|
1719
1839
|
}
|
|
1720
1840
|
}
|
|
1721
|
-
}
|
|
1841
|
+
}
|
|
1722
1842
|
};
|
|
1723
1843
|
connection.on('data', onData);
|
|
1724
|
-
this.
|
|
1844
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1725
1845
|
const onClose = () => {
|
|
1726
|
-
for (const fn of this.
|
|
1846
|
+
for (const fn of this._cleanupCbs) {
|
|
1727
1847
|
fn();
|
|
1728
1848
|
}
|
|
1729
|
-
this.
|
|
1849
|
+
this._cleanupCbs.length = 0;
|
|
1730
1850
|
};
|
|
1731
1851
|
connection.on('close', onClose);
|
|
1732
|
-
this.
|
|
1852
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1733
1853
|
}
|
|
1734
|
-
framing(hexChars) {
|
|
1735
|
-
if (
|
|
1736
|
-
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'));
|
|
1737
1872
|
return;
|
|
1738
1873
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
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'));
|
|
1741
1881
|
return;
|
|
1742
1882
|
}
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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]];
|
|
1749
1891
|
if (hi === 0xff || lo === 0xff) {
|
|
1750
|
-
this.
|
|
1892
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1751
1893
|
return;
|
|
1752
1894
|
}
|
|
1753
|
-
|
|
1895
|
+
data[i] = (hi << 4) | lo;
|
|
1754
1896
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
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'));
|
|
1764
1905
|
return;
|
|
1765
1906
|
}
|
|
1766
|
-
|
|
1907
|
+
const frame = {
|
|
1908
|
+
unit,
|
|
1909
|
+
fc,
|
|
1910
|
+
data,
|
|
1911
|
+
buffer: Buffer.copyBytesFrom(hexChars, 0, hexLen),
|
|
1912
|
+
};
|
|
1913
|
+
this.onFraming(frame);
|
|
1767
1914
|
}
|
|
1768
1915
|
flush() {
|
|
1769
|
-
this._state =
|
|
1916
|
+
this._state.status = 'idle';
|
|
1917
|
+
this._state.frameLen = 0;
|
|
1770
1918
|
}
|
|
1771
1919
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1772
1920
|
encode(unit, fc, data, transaction) {
|
|
1773
1921
|
const buffer = Buffer.allocUnsafe(data.length + 3);
|
|
1774
|
-
|
|
1775
|
-
buffer
|
|
1922
|
+
// Inline header + LRC — direct typed-array stores skip Buffer's per-call checks.
|
|
1923
|
+
buffer[0] = unit;
|
|
1924
|
+
buffer[1] = fc;
|
|
1776
1925
|
buffer.set(data, 2);
|
|
1777
|
-
buffer.
|
|
1926
|
+
buffer[buffer.length - 1] = lrc(buffer.subarray(0, -1));
|
|
1778
1927
|
const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
|
|
1779
1928
|
out[0] = CHAR_CODE.COLON;
|
|
1780
1929
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -1795,7 +1944,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1795
1944
|
_connection;
|
|
1796
1945
|
_transactionId = 1;
|
|
1797
1946
|
_buffer = EMPTY_BUFFER;
|
|
1798
|
-
|
|
1947
|
+
_cleanupCbs = [];
|
|
1799
1948
|
get connection() {
|
|
1800
1949
|
return this._connection;
|
|
1801
1950
|
}
|
|
@@ -1804,6 +1953,17 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1804
1953
|
this.ROLE = role;
|
|
1805
1954
|
this._connection = connection;
|
|
1806
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
|
+
}
|
|
1807
1967
|
let buffer = this._buffer;
|
|
1808
1968
|
if (buffer.length === 0) {
|
|
1809
1969
|
buffer = data;
|
|
@@ -1821,7 +1981,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1821
1981
|
break;
|
|
1822
1982
|
}
|
|
1823
1983
|
else {
|
|
1824
|
-
this.
|
|
1984
|
+
this.onFramingError(result.error);
|
|
1825
1985
|
buffer = EMPTY_BUFFER;
|
|
1826
1986
|
break;
|
|
1827
1987
|
}
|
|
@@ -1841,15 +2001,15 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1841
2001
|
}
|
|
1842
2002
|
};
|
|
1843
2003
|
connection.on('data', onData);
|
|
1844
|
-
this.
|
|
2004
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1845
2005
|
const onClose = () => {
|
|
1846
|
-
for (const fn of this.
|
|
2006
|
+
for (const fn of this._cleanupCbs) {
|
|
1847
2007
|
fn();
|
|
1848
2008
|
}
|
|
1849
|
-
this.
|
|
2009
|
+
this._cleanupCbs.length = 0;
|
|
1850
2010
|
};
|
|
1851
2011
|
connection.on('close', onClose);
|
|
1852
|
-
this.
|
|
2012
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1853
2013
|
}
|
|
1854
2014
|
tryExtract(buffer) {
|
|
1855
2015
|
if (buffer.length < 8) {
|
|
@@ -1858,7 +2018,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1858
2018
|
if (buffer[2] !== 0 || buffer[3] !== 0) {
|
|
1859
2019
|
return { kind: 'error', error: new Error('Invalid data') };
|
|
1860
2020
|
}
|
|
1861
|
-
const length = buffer
|
|
2021
|
+
const length = (buffer[4] << 8) | buffer[5]; // inline BE read
|
|
1862
2022
|
const total = 6 + length;
|
|
1863
2023
|
if (total > MAX_TCP_FRAME || length < 2) {
|
|
1864
2024
|
return { kind: 'error', error: new Error('Invalid data') };
|
|
@@ -1866,29 +2026,47 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1866
2026
|
if (buffer.length < total) {
|
|
1867
2027
|
return { kind: 'insufficient' };
|
|
1868
2028
|
}
|
|
1869
|
-
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) };
|
|
1870
2030
|
}
|
|
1871
2031
|
processFrame(buffer) {
|
|
1872
2032
|
const frame = {
|
|
1873
|
-
|
|
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],
|
|
1874
2037
|
unit: buffer[6],
|
|
1875
2038
|
fc: buffer[7],
|
|
1876
2039
|
data: buffer.subarray(8),
|
|
1877
2040
|
buffer,
|
|
1878
2041
|
};
|
|
1879
|
-
this.
|
|
2042
|
+
this.onFraming(frame);
|
|
1880
2043
|
}
|
|
1881
2044
|
flush() {
|
|
1882
2045
|
this._buffer = EMPTY_BUFFER;
|
|
1883
2046
|
}
|
|
1884
2047
|
encode(unit, fc, data, transaction) {
|
|
1885
2048
|
const buffer = Buffer.allocUnsafe(data.length + 8);
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
buffer
|
|
1891
|
-
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
|
+
}
|
|
1892
2070
|
if (transaction === undefined) {
|
|
1893
2071
|
this._transactionId = (this._transactionId + 1) % 65536 || 1;
|
|
1894
2072
|
}
|
|
@@ -1967,14 +2145,31 @@ class ModbusMaster extends EventEmitter {
|
|
|
1967
2145
|
_queueDatas = [];
|
|
1968
2146
|
_queueTimeouts = [];
|
|
1969
2147
|
_queueBroadcasts = [];
|
|
1970
|
-
|
|
1971
|
-
_queueRejects = [];
|
|
2148
|
+
_queueCallbacks = [];
|
|
1972
2149
|
_queueHead = 0;
|
|
1973
2150
|
_queueLen = 0;
|
|
1974
2151
|
_draining = false;
|
|
1975
2152
|
_nextTid = 1;
|
|
1976
2153
|
_cleanupFns = new Set();
|
|
1977
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
|
+
});
|
|
1978
2173
|
get state() {
|
|
1979
2174
|
return this._physicalLayer.state;
|
|
1980
2175
|
}
|
|
@@ -2001,17 +2196,21 @@ class ModbusMaster extends EventEmitter {
|
|
|
2001
2196
|
for (const cfc of this._customFunctionCodes.values()) {
|
|
2002
2197
|
appLayer.addCustomFunctionCode(cfc);
|
|
2003
2198
|
}
|
|
2004
|
-
const cleanupFraming = () =>
|
|
2199
|
+
const cleanupFraming = () => {
|
|
2200
|
+
appLayer.onFraming = NOOP;
|
|
2201
|
+
};
|
|
2005
2202
|
const onFraming = (frame) => {
|
|
2006
2203
|
this._masterSession.handleFrame(frame);
|
|
2007
2204
|
};
|
|
2008
|
-
appLayer.
|
|
2205
|
+
appLayer.onFraming = onFraming;
|
|
2009
2206
|
this._cleanupFns.add(cleanupFraming);
|
|
2010
|
-
const cleanupFramingError = () =>
|
|
2207
|
+
const cleanupFramingError = () => {
|
|
2208
|
+
appLayer.onFramingError = NOOP;
|
|
2209
|
+
};
|
|
2011
2210
|
const onFramingError = (error) => {
|
|
2012
2211
|
this._masterSession.handleError(error);
|
|
2013
2212
|
};
|
|
2014
|
-
appLayer.
|
|
2213
|
+
appLayer.onFramingError = onFramingError;
|
|
2015
2214
|
this._cleanupFns.add(cleanupFramingError);
|
|
2016
2215
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2017
2216
|
const onClose = () => {
|
|
@@ -2067,31 +2266,23 @@ class ModbusMaster extends EventEmitter {
|
|
|
2067
2266
|
}
|
|
2068
2267
|
return new AsciiApplicationLayer('MASTER', connection, this._protocol.opts);
|
|
2069
2268
|
}
|
|
2070
|
-
|
|
2269
|
+
_send(unit, fc, data, timeout, broadcast, callback) {
|
|
2071
2270
|
if (this._physicalLayer.state !== PhysicalState.OPEN) {
|
|
2072
|
-
|
|
2271
|
+
callback(new Error('Master is not open'));
|
|
2272
|
+
return;
|
|
2073
2273
|
}
|
|
2074
2274
|
if (this.concurrent) {
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
if (err)
|
|
2078
|
-
reject(err);
|
|
2079
|
-
else
|
|
2080
|
-
resolve(frame);
|
|
2081
|
-
});
|
|
2082
|
-
});
|
|
2275
|
+
this._exchange(unit, fc, data, timeout, broadcast, callback);
|
|
2276
|
+
return;
|
|
2083
2277
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
this._queueLen++;
|
|
2093
|
-
this._drain();
|
|
2094
|
-
});
|
|
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();
|
|
2095
2286
|
}
|
|
2096
2287
|
_drain() {
|
|
2097
2288
|
if (this._draining || this._physicalLayer.state !== PhysicalState.OPEN) {
|
|
@@ -2112,14 +2303,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2112
2303
|
const data = this._queueDatas[h];
|
|
2113
2304
|
const timeout = this._queueTimeouts[h];
|
|
2114
2305
|
const broadcast = this._queueBroadcasts[h];
|
|
2115
|
-
const
|
|
2116
|
-
|
|
2117
|
-
// 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
|
|
2118
2308
|
// closures while the rest of the queue is still draining. Primitives
|
|
2119
2309
|
// (unit/fc/timeout/broadcast) need no clearing.
|
|
2120
2310
|
this._queueDatas[h] = undefined;
|
|
2121
|
-
this.
|
|
2122
|
-
this._queueRejects[h] = undefined;
|
|
2311
|
+
this._queueCallbacks[h] = undefined;
|
|
2123
2312
|
this._queueHead = h + 1;
|
|
2124
2313
|
this._queueLen--;
|
|
2125
2314
|
if (this._queueLen === 0) {
|
|
@@ -2130,15 +2319,11 @@ class ModbusMaster extends EventEmitter {
|
|
|
2130
2319
|
this._queueDatas.length = 0;
|
|
2131
2320
|
this._queueTimeouts.length = 0;
|
|
2132
2321
|
this._queueBroadcasts.length = 0;
|
|
2133
|
-
this.
|
|
2134
|
-
this._queueRejects.length = 0;
|
|
2322
|
+
this._queueCallbacks.length = 0;
|
|
2135
2323
|
this._queueHead = 0;
|
|
2136
2324
|
}
|
|
2137
2325
|
this._exchange(unit, fc, data, timeout, broadcast, (err, frame) => {
|
|
2138
|
-
|
|
2139
|
-
reject(err);
|
|
2140
|
-
else
|
|
2141
|
-
resolve(frame);
|
|
2326
|
+
callback(err, frame);
|
|
2142
2327
|
this._processNext();
|
|
2143
2328
|
});
|
|
2144
2329
|
}
|
|
@@ -2158,30 +2343,35 @@ class ModbusMaster extends EventEmitter {
|
|
|
2158
2343
|
if (!this.concurrent) {
|
|
2159
2344
|
appLayer.flush();
|
|
2160
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);
|
|
2161
2354
|
if (broadcast) {
|
|
2162
|
-
// Broadcast: no response expected. Skip the session entirely
|
|
2163
|
-
|
|
2164
|
-
// broadcast's stop() would clear the other request's slot/timer).
|
|
2165
|
-
let settled = false;
|
|
2166
|
-
const timer = setTimeout(() => {
|
|
2167
|
-
if (settled)
|
|
2168
|
-
return;
|
|
2169
|
-
settled = true;
|
|
2170
|
-
callback(new Error('Timeout'));
|
|
2171
|
-
}, timeout);
|
|
2355
|
+
// Broadcast: no response expected. Skip the session entirely.
|
|
2356
|
+
this._timerHeap.add(exchangeId, timeout);
|
|
2172
2357
|
connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
|
|
2173
|
-
|
|
2358
|
+
const p = this._pendingExchanges.get(exchangeId);
|
|
2359
|
+
if (!p || p.settled)
|
|
2360
|
+
return;
|
|
2361
|
+
const cb = p.callback;
|
|
2362
|
+
if (!cb)
|
|
2174
2363
|
return;
|
|
2175
|
-
|
|
2176
|
-
|
|
2364
|
+
p.settled = true;
|
|
2365
|
+
p.callback = null;
|
|
2366
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2177
2367
|
if (writeErr) {
|
|
2178
|
-
|
|
2368
|
+
cb(writeErr);
|
|
2179
2369
|
}
|
|
2180
2370
|
else if (this._physicalLayer.state !== PhysicalState.OPEN) {
|
|
2181
|
-
|
|
2371
|
+
cb(new Error('Master is not open'));
|
|
2182
2372
|
}
|
|
2183
2373
|
else {
|
|
2184
|
-
|
|
2374
|
+
cb(null);
|
|
2185
2375
|
}
|
|
2186
2376
|
});
|
|
2187
2377
|
return;
|
|
@@ -2195,62 +2385,82 @@ class ModbusMaster extends EventEmitter {
|
|
|
2195
2385
|
}
|
|
2196
2386
|
const key = tid ?? FIFO_KEY;
|
|
2197
2387
|
const payload = appLayer.encode(unit, fc, data, tid);
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
// settled guard prevents double-invocation when timeout fires during write.
|
|
2201
|
-
let settled = false;
|
|
2202
|
-
const timer = setTimeout(() => {
|
|
2203
|
-
if (settled)
|
|
2204
|
-
return;
|
|
2205
|
-
settled = true;
|
|
2206
|
-
this._masterSession.stop(key);
|
|
2207
|
-
callback(new Error('Timeout'));
|
|
2208
|
-
}, timeout);
|
|
2388
|
+
pending.sessionKey = key;
|
|
2389
|
+
this._timerHeap.add(exchangeId, timeout);
|
|
2209
2390
|
connection.write(payload, (writeErr) => {
|
|
2391
|
+
const p = this._pendingExchanges.get(exchangeId);
|
|
2392
|
+
if (!p || p.settled)
|
|
2393
|
+
return;
|
|
2210
2394
|
if (writeErr) {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
settled = true;
|
|
2214
|
-
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);
|
|
2215
2401
|
}
|
|
2216
2402
|
return;
|
|
2217
2403
|
}
|
|
2218
2404
|
if (this._physicalLayer.state !== PhysicalState.OPEN) {
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
settled = true;
|
|
2222
|
-
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'));
|
|
2223
2411
|
}
|
|
2224
2412
|
return;
|
|
2225
2413
|
}
|
|
2226
2414
|
// Write succeeded — register in session for frame matching only.
|
|
2227
|
-
// Timeout is managed by the
|
|
2415
|
+
// Timeout is managed by the global timer heap above.
|
|
2228
2416
|
this._masterSession.start(key, (err, frame) => {
|
|
2229
|
-
|
|
2417
|
+
const p2 = this._pendingExchanges.get(exchangeId);
|
|
2418
|
+
if (!p2 || p2.settled)
|
|
2230
2419
|
return;
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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
|
+
}
|
|
2234
2427
|
});
|
|
2235
2428
|
});
|
|
2236
2429
|
}
|
|
2237
2430
|
writeFC1Or2(unit, fc, address, length, timeout) {
|
|
2238
2431
|
const byteCount = Math.ceil(length / 8);
|
|
2239
2432
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
+
});
|
|
2254
2464
|
});
|
|
2255
2465
|
}
|
|
2256
2466
|
writeFC1;
|
|
@@ -2264,19 +2474,41 @@ class ModbusMaster extends EventEmitter {
|
|
|
2264
2474
|
writeFC3Or4(unit, fc, address, length, timeout) {
|
|
2265
2475
|
const byteCount = length * 2;
|
|
2266
2476
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2267
|
-
|
|
2268
|
-
bufferTx
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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
|
+
});
|
|
2280
2512
|
});
|
|
2281
2513
|
}
|
|
2282
2514
|
writeFC3;
|
|
@@ -2291,28 +2523,61 @@ class ModbusMaster extends EventEmitter {
|
|
|
2291
2523
|
writeSingleCoil(unit, address, value, timeout = this.timeout) {
|
|
2292
2524
|
const fc = FunctionCode.WRITE_SINGLE_COIL;
|
|
2293
2525
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
+
});
|
|
2302
2551
|
});
|
|
2303
2552
|
}
|
|
2304
2553
|
writeFC6;
|
|
2305
2554
|
writeSingleRegister(unit, address, value, timeout = this.timeout) {
|
|
2306
2555
|
const fc = FunctionCode.WRITE_SINGLE_REGISTER;
|
|
2307
2556
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2308
|
-
|
|
2309
|
-
bufferTx
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
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
|
+
});
|
|
2316
2581
|
});
|
|
2317
2582
|
}
|
|
2318
2583
|
writeFC15;
|
|
@@ -2320,20 +2585,36 @@ class ModbusMaster extends EventEmitter {
|
|
|
2320
2585
|
const fc = FunctionCode.WRITE_MULTIPLE_COILS;
|
|
2321
2586
|
const byteCount = Math.ceil(value.length / 8);
|
|
2322
2587
|
const bufferTx = Buffer.alloc(5 + byteCount);
|
|
2323
|
-
|
|
2324
|
-
bufferTx
|
|
2325
|
-
bufferTx
|
|
2326
|
-
value.
|
|
2327
|
-
|
|
2328
|
-
|
|
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;
|
|
2329
2597
|
}
|
|
2330
|
-
}
|
|
2331
|
-
return
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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
|
+
});
|
|
2337
2618
|
});
|
|
2338
2619
|
}
|
|
2339
2620
|
writeFC16;
|
|
@@ -2341,53 +2622,102 @@ class ModbusMaster extends EventEmitter {
|
|
|
2341
2622
|
const fc = FunctionCode.WRITE_MULTIPLE_REGISTERS;
|
|
2342
2623
|
const byteCount = value.length * 2;
|
|
2343
2624
|
const bufferTx = Buffer.allocUnsafe(5 + byteCount);
|
|
2344
|
-
|
|
2345
|
-
bufferTx
|
|
2346
|
-
bufferTx
|
|
2347
|
-
value.
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
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
|
+
});
|
|
2356
2656
|
});
|
|
2357
2657
|
}
|
|
2358
2658
|
handleFC17;
|
|
2359
2659
|
reportServerId(unit, serverIdLength = 1, timeout = this.timeout) {
|
|
2360
2660
|
const fc = FunctionCode.REPORT_SERVER_ID;
|
|
2361
|
-
return
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
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
|
+
});
|
|
2376
2689
|
});
|
|
2377
2690
|
}
|
|
2378
2691
|
handleFC22;
|
|
2379
2692
|
maskWriteRegister(unit, address, andMask, orMask, timeout = this.timeout) {
|
|
2380
2693
|
const fc = FunctionCode.MASK_WRITE_REGISTER;
|
|
2381
2694
|
const bufferTx = Buffer.allocUnsafe(6);
|
|
2382
|
-
|
|
2383
|
-
bufferTx
|
|
2384
|
-
bufferTx
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
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
|
+
});
|
|
2391
2721
|
});
|
|
2392
2722
|
}
|
|
2393
2723
|
handleFC23;
|
|
@@ -2396,71 +2726,115 @@ class ModbusMaster extends EventEmitter {
|
|
|
2396
2726
|
const byteCount = write.value.length * 2;
|
|
2397
2727
|
const readByteCount = read.length * 2;
|
|
2398
2728
|
const bufferTx = Buffer.allocUnsafe(9 + byteCount);
|
|
2399
|
-
|
|
2400
|
-
bufferTx
|
|
2401
|
-
bufferTx.
|
|
2402
|
-
bufferTx
|
|
2403
|
-
bufferTx.
|
|
2404
|
-
write.
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2413
|
-
|
|
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
|
+
});
|
|
2414
2773
|
});
|
|
2415
2774
|
}
|
|
2416
2775
|
handleFC43_14;
|
|
2417
2776
|
readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout = this.timeout) {
|
|
2418
2777
|
const fc = FunctionCode.READ_DEVICE_IDENTIFICATION;
|
|
2419
|
-
return
|
|
2420
|
-
|
|
2421
|
-
|
|
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
|
-
|
|
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;
|
|
2447
2819
|
}
|
|
2448
|
-
break;
|
|
2449
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);
|
|
2450
2833
|
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
throw new Error('Invalid response');
|
|
2456
|
-
frame.data = {
|
|
2457
|
-
readDeviceIDCode,
|
|
2458
|
-
conformityLevel: frame.data[2],
|
|
2459
|
-
moreFollows: frame.data[3] === 0xff,
|
|
2460
|
-
nextObjectId: frame.data[4],
|
|
2461
|
-
objects,
|
|
2462
|
-
};
|
|
2463
|
-
return frame;
|
|
2834
|
+
catch (e) {
|
|
2835
|
+
reject(e);
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2464
2838
|
});
|
|
2465
2839
|
}
|
|
2466
2840
|
addCustomFunctionCode(cfc) {
|
|
@@ -2473,11 +2847,24 @@ class ModbusMaster extends EventEmitter {
|
|
|
2473
2847
|
}
|
|
2474
2848
|
sendCustomFC(unit, fc, data, timeout = this.timeout) {
|
|
2475
2849
|
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
2476
|
-
return
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
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
|
+
});
|
|
2481
2868
|
});
|
|
2482
2869
|
}
|
|
2483
2870
|
/**
|
|
@@ -2503,9 +2890,24 @@ class ModbusMaster extends EventEmitter {
|
|
|
2503
2890
|
const end = this._queueHead + this._queueLen;
|
|
2504
2891
|
this._queueLen = 0;
|
|
2505
2892
|
for (let i = this._queueHead; i < end; i++) {
|
|
2506
|
-
this.
|
|
2893
|
+
this._queueCallbacks[i](rejectErr);
|
|
2507
2894
|
}
|
|
2508
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();
|
|
2509
2911
|
let err = null;
|
|
2510
2912
|
try {
|
|
2511
2913
|
await promisifyCb((cb) => this._physicalLayer.close(cb));
|
|
@@ -2542,7 +2944,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
2542
2944
|
_protocol;
|
|
2543
2945
|
_appLayers = new Map();
|
|
2544
2946
|
_customFunctionCodes = new Map();
|
|
2545
|
-
|
|
2947
|
+
// Active interval locks. Typical length: 0 (no contention) — 1-2 (cross-
|
|
2948
|
+
// connection contention). Linear scan for overlap is sub-µs.
|
|
2949
|
+
_intervalLocks = [];
|
|
2546
2950
|
_cleanupFns = new Set();
|
|
2547
2951
|
_closePromise = null;
|
|
2548
2952
|
get state() {
|
|
@@ -2569,7 +2973,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
2569
2973
|
for (const cfc of this._customFunctionCodes.values()) {
|
|
2570
2974
|
appLayer.addCustomFunctionCode(cfc);
|
|
2571
2975
|
}
|
|
2572
|
-
const cleanupFraming = () =>
|
|
2976
|
+
const cleanupFraming = () => {
|
|
2977
|
+
appLayer.onFraming = NOOP;
|
|
2978
|
+
};
|
|
2573
2979
|
const onFraming = (frame) => {
|
|
2574
2980
|
if (this._physicalLayer.state !== PhysicalState.OPEN) {
|
|
2575
2981
|
return;
|
|
@@ -2584,7 +2990,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2584
2990
|
appLayerData.queues.frames.push(frame);
|
|
2585
2991
|
this._drain(appLayer, appLayerData.queues);
|
|
2586
2992
|
};
|
|
2587
|
-
appLayer.
|
|
2993
|
+
appLayer.onFraming = onFraming;
|
|
2588
2994
|
this._cleanupFns.add(cleanupFraming);
|
|
2589
2995
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2590
2996
|
const onClose = () => {
|
|
@@ -2651,12 +3057,23 @@ class ModbusSlave extends EventEmitter {
|
|
|
2651
3057
|
const byteCount = (length + 7) >> 3;
|
|
2652
3058
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
2653
3059
|
pdu[0] = byteCount;
|
|
2654
|
-
|
|
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;
|
|
2655
3067
|
for (let i = 0; i < length; i++) {
|
|
2656
|
-
if (coils[i])
|
|
2657
|
-
|
|
3068
|
+
if (coils[i])
|
|
3069
|
+
acc |= 1 << (i & 7);
|
|
3070
|
+
if ((i & 7) === 7) {
|
|
3071
|
+
pdu[out++] = acc;
|
|
3072
|
+
acc = 0;
|
|
2658
3073
|
}
|
|
2659
3074
|
}
|
|
3075
|
+
if ((length & 7) !== 0)
|
|
3076
|
+
pdu[out] = acc;
|
|
2660
3077
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2661
3078
|
}
|
|
2662
3079
|
catch (error) {
|
|
@@ -2687,12 +3104,19 @@ class ModbusSlave extends EventEmitter {
|
|
|
2687
3104
|
const byteCount = (length + 7) >> 3;
|
|
2688
3105
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
2689
3106
|
pdu[0] = byteCount;
|
|
2690
|
-
|
|
3107
|
+
// Accumulator-based bit pack — see handleFC1 for the rationale.
|
|
3108
|
+
let acc = 0;
|
|
3109
|
+
let out = 1;
|
|
2691
3110
|
for (let i = 0; i < length; i++) {
|
|
2692
|
-
if (discreteInputs[i])
|
|
2693
|
-
|
|
3111
|
+
if (discreteInputs[i])
|
|
3112
|
+
acc |= 1 << (i & 7);
|
|
3113
|
+
if ((i & 7) === 7) {
|
|
3114
|
+
pdu[out++] = acc;
|
|
3115
|
+
acc = 0;
|
|
2694
3116
|
}
|
|
2695
3117
|
}
|
|
3118
|
+
if ((length & 7) !== 0)
|
|
3119
|
+
pdu[out] = acc;
|
|
2696
3120
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2697
3121
|
}
|
|
2698
3122
|
catch (error) {
|
|
@@ -2722,8 +3146,14 @@ class ModbusSlave extends EventEmitter {
|
|
|
2722
3146
|
const registers = await model.readHoldingRegisters(address, length);
|
|
2723
3147
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
2724
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.
|
|
2725
3152
|
for (let i = 0; i < length; i++) {
|
|
2726
|
-
|
|
3153
|
+
const v = registers[i];
|
|
3154
|
+
const off = 1 + i * 2;
|
|
3155
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3156
|
+
pdu[off + 1] = v & 0xff;
|
|
2727
3157
|
}
|
|
2728
3158
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2729
3159
|
}
|
|
@@ -2754,8 +3184,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
2754
3184
|
const registers = await model.readInputRegisters(address, length);
|
|
2755
3185
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
2756
3186
|
pdu[0] = length * 2;
|
|
3187
|
+
// Inline big-endian write — see handleFC3 for the rationale.
|
|
2757
3188
|
for (let i = 0; i < length; i++) {
|
|
2758
|
-
|
|
3189
|
+
const v = registers[i];
|
|
3190
|
+
const off = 1 + i * 2;
|
|
3191
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3192
|
+
pdu[off + 1] = v & 0xff;
|
|
2759
3193
|
}
|
|
2760
3194
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2761
3195
|
}
|
|
@@ -2835,6 +3269,10 @@ class ModbusSlave extends EventEmitter {
|
|
|
2835
3269
|
}
|
|
2836
3270
|
const value = new Array(length);
|
|
2837
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).
|
|
2838
3276
|
value[i] = (frame.data[5 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
|
|
2839
3277
|
}
|
|
2840
3278
|
try {
|
|
@@ -2939,7 +3377,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2939
3377
|
return;
|
|
2940
3378
|
}
|
|
2941
3379
|
try {
|
|
2942
|
-
await this.
|
|
3380
|
+
await this._withIntervalLock(address, address + 1, async () => {
|
|
2943
3381
|
if (model.maskWriteRegister) {
|
|
2944
3382
|
await model.maskWriteRegister(address, andMask, orMask);
|
|
2945
3383
|
}
|
|
@@ -2989,8 +3427,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2989
3427
|
value[i] = (frame.data[9 + i * 2] << 8) | frame.data[10 + i * 2];
|
|
2990
3428
|
}
|
|
2991
3429
|
try {
|
|
2992
|
-
|
|
2993
|
-
await this._withAddressLock(writeAddresses, async () => {
|
|
3430
|
+
await this._withIntervalLock(address.write, address.write + length.write, async () => {
|
|
2994
3431
|
if (model.writeMultipleRegisters) {
|
|
2995
3432
|
await model.writeMultipleRegisters(address.write, value);
|
|
2996
3433
|
}
|
|
@@ -3003,8 +3440,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
3003
3440
|
const registers = await model.readHoldingRegisters(address.read, length.read);
|
|
3004
3441
|
const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
|
|
3005
3442
|
pdu[0] = length.read * 2;
|
|
3443
|
+
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3006
3444
|
for (let i = 0; i < length.read; i++) {
|
|
3007
|
-
|
|
3445
|
+
const v = registers[i];
|
|
3446
|
+
const off = 1 + i * 2;
|
|
3447
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3448
|
+
pdu[off + 1] = v & 0xff;
|
|
3008
3449
|
}
|
|
3009
3450
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3010
3451
|
}
|
|
@@ -3163,7 +3604,11 @@ class ModbusSlave extends EventEmitter {
|
|
|
3163
3604
|
return Promise.resolve();
|
|
3164
3605
|
}
|
|
3165
3606
|
return new Promise((resolve) => {
|
|
3166
|
-
|
|
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);
|
|
3167
3612
|
});
|
|
3168
3613
|
};
|
|
3169
3614
|
// Hot path: unicast to a known unit dispatches to a single model.
|
|
@@ -3212,24 +3657,66 @@ class ModbusSlave extends EventEmitter {
|
|
|
3212
3657
|
return true;
|
|
3213
3658
|
}
|
|
3214
3659
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
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);
|
|
3224
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);
|
|
3225
3709
|
try {
|
|
3226
3710
|
return await work;
|
|
3227
3711
|
}
|
|
3228
3712
|
finally {
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
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();
|
|
3233
3720
|
}
|
|
3234
3721
|
}
|
|
3235
3722
|
}
|
|
@@ -3346,4 +3833,4 @@ class ModbusSlave extends EventEmitter {
|
|
|
3346
3833
|
}
|
|
3347
3834
|
}
|
|
3348
3835
|
|
|
3349
|
-
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 };
|