njs-modbus 3.2.0 → 3.3.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 +64 -35
- package/README.zh-CN.md +64 -35
- package/dist/index.cjs +536 -236
- package/dist/index.d.ts +12 -6
- package/dist/index.mjs +536 -236
- package/dist/utils.cjs +251 -154
- package/dist/utils.d.ts +29 -27
- package/dist/utils.mjs +251 -154
- package/package.json +8 -1
package/dist/index.mjs
CHANGED
|
@@ -164,40 +164,75 @@ function bitsToMs(baudRate, bits) {
|
|
|
164
164
|
* open / close / destroy callbacks.
|
|
165
165
|
*/
|
|
166
166
|
function drainCbs(cbs, err) {
|
|
167
|
-
if (!cbs)
|
|
167
|
+
if (!cbs) {
|
|
168
168
|
return;
|
|
169
|
+
}
|
|
169
170
|
for (const cb of cbs) {
|
|
170
171
|
cb?.(err);
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
function inRange(n, [min, max]) {
|
|
175
|
-
return n >= min && n <= max;
|
|
176
|
-
}
|
|
177
|
-
function isRangeArray(range) {
|
|
178
|
-
return Array.isArray(range[0]);
|
|
179
|
-
}
|
|
180
175
|
function checkRange(value, range) {
|
|
181
176
|
if (!range || range.length === 0) {
|
|
182
177
|
return true;
|
|
183
178
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
179
|
+
const isMultiRange = Array.isArray(range[0]);
|
|
180
|
+
const isValueArray = Array.isArray(value);
|
|
181
|
+
if (!isValueArray && !isMultiRange) {
|
|
182
|
+
const r = range;
|
|
183
|
+
const min = r[0], max = r[1];
|
|
184
|
+
const v = value;
|
|
185
|
+
return min <= max ? v >= min && v <= max : v >= max && v <= min;
|
|
186
|
+
}
|
|
187
|
+
if (!isValueArray && isMultiRange) {
|
|
188
|
+
const ranges = range;
|
|
189
|
+
const v = value;
|
|
190
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
191
|
+
const min = ranges[i][0], max = ranges[i][1];
|
|
192
|
+
const lo = min <= max ? min : max;
|
|
193
|
+
const hi = min <= max ? max : min;
|
|
194
|
+
if (v >= lo && v <= hi) {
|
|
190
195
|
return true;
|
|
191
196
|
}
|
|
192
197
|
}
|
|
193
198
|
return false;
|
|
194
199
|
}
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
const values = value;
|
|
201
|
+
if (values.length === 0) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (!isMultiRange) {
|
|
205
|
+
const r = range;
|
|
206
|
+
const min = r[0], max = r[1];
|
|
207
|
+
const lo = min <= max ? min : max;
|
|
208
|
+
const hi = min <= max ? max : min;
|
|
209
|
+
for (let i = 0; i < values.length; i++) {
|
|
210
|
+
if (values[i] < lo || values[i] > hi) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
const ranges = range;
|
|
217
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
218
|
+
const min = ranges[i][0], max = ranges[i][1];
|
|
219
|
+
const lo = min <= max ? min : max;
|
|
220
|
+
const hi = min <= max ? max : min;
|
|
221
|
+
let allInRange = true;
|
|
222
|
+
for (let j = 0; j < values.length; j++) {
|
|
223
|
+
if (values[j] < lo || values[j] > hi) {
|
|
224
|
+
allInRange = false;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (allInRange) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
198
233
|
}
|
|
199
234
|
|
|
200
|
-
const TABLE = [
|
|
235
|
+
const TABLE = new Uint16Array([
|
|
201
236
|
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
|
|
202
237
|
0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
|
|
203
238
|
0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
|
|
@@ -214,11 +249,11 @@ const TABLE = [
|
|
|
214
249
|
0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
|
|
215
250
|
0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
|
|
216
251
|
0x4040,
|
|
217
|
-
];
|
|
218
|
-
function crc(data, start
|
|
252
|
+
]);
|
|
253
|
+
function crc(data, start, end) {
|
|
219
254
|
let crc = 0xffff;
|
|
220
255
|
for (let index = start; index < end; index++) {
|
|
221
|
-
crc =
|
|
256
|
+
crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
|
|
222
257
|
}
|
|
223
258
|
return crc;
|
|
224
259
|
}
|
|
@@ -231,114 +266,104 @@ function crc(data, start = 0, end = data.length) {
|
|
|
231
266
|
* Infinity, and out-of-range values uniformly.
|
|
232
267
|
*/
|
|
233
268
|
function isUint8(n) {
|
|
234
|
-
return
|
|
269
|
+
return (n & 0xff) === n;
|
|
235
270
|
}
|
|
236
271
|
|
|
237
|
-
function lrc(data, start
|
|
272
|
+
function lrc(data, start, end) {
|
|
238
273
|
let sum = 0;
|
|
239
274
|
for (let i = start; i < end; i++) {
|
|
240
275
|
sum += data[i];
|
|
241
276
|
}
|
|
242
|
-
return
|
|
277
|
+
return -sum & 0xff;
|
|
243
278
|
}
|
|
244
279
|
|
|
245
|
-
const REQUEST_FIXED_LENGTHS = {
|
|
246
|
-
[FunctionCode.READ_COILS]: 8,
|
|
247
|
-
[FunctionCode.READ_DISCRETE_INPUTS]: 8,
|
|
248
|
-
[FunctionCode.READ_HOLDING_REGISTERS]: 8,
|
|
249
|
-
[FunctionCode.READ_INPUT_REGISTERS]: 8,
|
|
250
|
-
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
251
|
-
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
252
|
-
[FunctionCode.REPORT_SERVER_ID]: 4,
|
|
253
|
-
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
254
|
-
[FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
|
|
255
|
-
};
|
|
256
|
-
const REQUEST_BYTE_COUNT = {
|
|
257
|
-
[FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
|
|
258
|
-
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
|
|
259
|
-
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
|
|
260
|
-
};
|
|
261
|
-
const RESPONSE_FIXED_LENGTHS = {
|
|
262
|
-
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
263
|
-
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
264
|
-
[FunctionCode.WRITE_MULTIPLE_COILS]: 8,
|
|
265
|
-
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
|
|
266
|
-
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
267
|
-
};
|
|
268
|
-
const RESPONSE_BYTE_COUNT = {
|
|
269
|
-
[FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
|
|
270
|
-
[FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
|
|
271
|
-
[FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
|
|
272
|
-
[FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
|
|
273
|
-
[FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
|
|
274
|
-
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
|
|
275
|
-
};
|
|
276
|
-
/** Sentinel: caller needs to feed more bytes before length can be determined. */
|
|
277
280
|
const PREDICT_NEED_MORE = 0;
|
|
278
|
-
/** Sentinel: function code is not in the standard tables. */
|
|
279
281
|
const PREDICT_UNKNOWN = -1;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
282
|
+
const REQ_TABLE = new Int32Array(256);
|
|
283
|
+
const RES_TABLE = new Int32Array(256);
|
|
284
|
+
(function initTables() {
|
|
285
|
+
REQ_TABLE[FunctionCode.READ_COILS] = 8;
|
|
286
|
+
REQ_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = 8;
|
|
287
|
+
REQ_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = 8;
|
|
288
|
+
REQ_TABLE[FunctionCode.READ_INPUT_REGISTERS] = 8;
|
|
289
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
290
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
291
|
+
REQ_TABLE[FunctionCode.REPORT_SERVER_ID] = 4;
|
|
292
|
+
REQ_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
293
|
+
REQ_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
|
|
294
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
|
|
295
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
|
|
296
|
+
REQ_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
|
|
297
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
298
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
299
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = 8;
|
|
300
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
|
|
301
|
+
RES_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
302
|
+
RES_TABLE[FunctionCode.READ_COILS] = -517;
|
|
303
|
+
RES_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = -517;
|
|
304
|
+
RES_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = -517;
|
|
305
|
+
RES_TABLE[FunctionCode.READ_INPUT_REGISTERS] = -517;
|
|
306
|
+
RES_TABLE[FunctionCode.REPORT_SERVER_ID] = -517;
|
|
307
|
+
RES_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
|
|
308
|
+
RES_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
|
|
309
|
+
})();
|
|
292
310
|
function predictRtuFrameLength(buffer, start, end, isResponse) {
|
|
293
|
-
|
|
311
|
+
const len = end - start;
|
|
312
|
+
if (len < 2) {
|
|
294
313
|
return PREDICT_NEED_MORE;
|
|
295
314
|
}
|
|
296
315
|
const fc = buffer[start + 1];
|
|
297
|
-
if (isResponse
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
316
|
+
if (isResponse) {
|
|
317
|
+
if ((fc & EXCEPTION_OFFSET) !== 0) {
|
|
318
|
+
return 5;
|
|
319
|
+
}
|
|
320
|
+
const val = RES_TABLE[fc];
|
|
321
|
+
if (val > 0) {
|
|
322
|
+
return val;
|
|
323
|
+
}
|
|
324
|
+
if (val < 0) {
|
|
325
|
+
if (val === -999) {
|
|
326
|
+
// FC 43 / MEI 14 response — inline to avoid function-call overhead on
|
|
327
|
+
// the framing hot path (even though this FC is uncommon).
|
|
328
|
+
if (end - start < 8) {
|
|
329
|
+
return PREDICT_NEED_MORE;
|
|
330
|
+
}
|
|
331
|
+
if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
|
|
332
|
+
return PREDICT_UNKNOWN;
|
|
333
|
+
}
|
|
334
|
+
const numObjs = buffer[start + 7];
|
|
335
|
+
let cursor = start + 8;
|
|
336
|
+
for (let i = 0; i < numObjs; i++) {
|
|
337
|
+
if (end < cursor + 2) {
|
|
338
|
+
return PREDICT_NEED_MORE;
|
|
339
|
+
}
|
|
340
|
+
cursor += 2 + buffer[cursor + 1];
|
|
341
|
+
}
|
|
342
|
+
return cursor - start + 2;
|
|
343
|
+
}
|
|
344
|
+
const decode = -val;
|
|
345
|
+
const offset = decode >> 8;
|
|
346
|
+
if (len <= offset) {
|
|
347
|
+
return PREDICT_NEED_MORE;
|
|
348
|
+
}
|
|
349
|
+
return (decode & 0xff) + buffer[start + offset];
|
|
308
350
|
}
|
|
309
|
-
return bc.extra + buffer[start + bc.offset];
|
|
310
351
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
* CRC(2)
|
|
324
|
-
*/
|
|
325
|
-
function predictFc43_14Response(buffer, start, end) {
|
|
326
|
-
if (end - start < 8) {
|
|
327
|
-
return PREDICT_NEED_MORE;
|
|
328
|
-
}
|
|
329
|
-
if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
|
|
330
|
-
return PREDICT_UNKNOWN;
|
|
331
|
-
}
|
|
332
|
-
const numObjs = buffer[start + 7];
|
|
333
|
-
let cursor = start + 8;
|
|
334
|
-
for (let i = 0; i < numObjs; i++) {
|
|
335
|
-
if (end < cursor + 2) {
|
|
336
|
-
return PREDICT_NEED_MORE;
|
|
352
|
+
else {
|
|
353
|
+
const val = REQ_TABLE[fc];
|
|
354
|
+
if (val > 0) {
|
|
355
|
+
return val;
|
|
356
|
+
}
|
|
357
|
+
if (val < 0) {
|
|
358
|
+
const decode = -val;
|
|
359
|
+
const offset = decode >> 8;
|
|
360
|
+
if (len <= offset) {
|
|
361
|
+
return PREDICT_NEED_MORE;
|
|
362
|
+
}
|
|
363
|
+
return (decode & 0xff) + buffer[start + offset];
|
|
337
364
|
}
|
|
338
|
-
const objLen = buffer[cursor + 1];
|
|
339
|
-
cursor += 2 + objLen;
|
|
340
365
|
}
|
|
341
|
-
return
|
|
366
|
+
return PREDICT_UNKNOWN;
|
|
342
367
|
}
|
|
343
368
|
|
|
344
369
|
/**
|
|
@@ -347,10 +372,12 @@ function predictFc43_14Response(buffer, start, end) {
|
|
|
347
372
|
function promisifyCb(fn) {
|
|
348
373
|
return new Promise((resolve, reject) => {
|
|
349
374
|
fn((err) => {
|
|
350
|
-
if (err)
|
|
375
|
+
if (err) {
|
|
351
376
|
reject(err);
|
|
352
|
-
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
353
379
|
resolve();
|
|
380
|
+
}
|
|
354
381
|
});
|
|
355
382
|
});
|
|
356
383
|
}
|
|
@@ -403,102 +430,172 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
403
430
|
return { intervalBetweenFrames, interCharTimeout };
|
|
404
431
|
}
|
|
405
432
|
|
|
406
|
-
/**
|
|
407
|
-
*
|
|
433
|
+
/**
|
|
434
|
+
* Hybrid timer manager: uses native `setTimeout` for low concurrency
|
|
435
|
+
* and switches to a binary min-heap when concurrency exceeds the threshold.
|
|
436
|
+
*
|
|
437
|
+
* Benchmarks (add + clear throughput, Node 24, x64):
|
|
438
|
+
* 1 concurrent: setTimeout ~1.7× faster than heap
|
|
439
|
+
* 2 concurrent: setTimeout ~1.6× faster than heap
|
|
440
|
+
* 5 concurrent: setTimeout ~1.5-1.9× faster than heap
|
|
441
|
+
* 10 concurrent: roughly equal
|
|
442
|
+
* 20 concurrent: heap ~1.3× faster than setTimeout[]
|
|
443
|
+
* 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
|
|
408
444
|
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
445
|
+
* The crossover point is around 10 concurrent timers, so the default
|
|
446
|
+
* `concurrentThreshold = 2` keeps the common 1-2 request case on the
|
|
447
|
+
* fast direct path while delegating to the heap for larger batches.
|
|
412
448
|
*/
|
|
413
449
|
class TimerHeap {
|
|
414
450
|
_deadlines = [];
|
|
415
451
|
_ids = [];
|
|
452
|
+
_seqs = [];
|
|
453
|
+
_counter = 0;
|
|
416
454
|
_timer = null;
|
|
417
455
|
_onFire;
|
|
418
|
-
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
419
456
|
_boundTick;
|
|
420
|
-
|
|
457
|
+
_threshold;
|
|
458
|
+
_mode = 'direct';
|
|
459
|
+
_directTimers = new Map();
|
|
460
|
+
/**
|
|
461
|
+
* @param onFire Callback invoked with the timer id when it expires.
|
|
462
|
+
* @param concurrentThreshold Maximum number of timers kept as individual
|
|
463
|
+
* native `setTimeout` handles. Once exceeded, all timers migrate to
|
|
464
|
+
* the internal heap and share a single native timer. Default is 2.
|
|
465
|
+
*/
|
|
466
|
+
constructor(onFire, concurrentThreshold = 2) {
|
|
421
467
|
this._onFire = onFire;
|
|
422
468
|
this._boundTick = this._onTick.bind(this);
|
|
469
|
+
this._threshold = concurrentThreshold;
|
|
423
470
|
}
|
|
424
|
-
/** Number of pending timers in the heap. */
|
|
425
471
|
get size() {
|
|
426
|
-
return this._deadlines.length;
|
|
472
|
+
return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
|
|
427
473
|
}
|
|
428
|
-
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
429
474
|
add(id, ms) {
|
|
475
|
+
if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
|
|
476
|
+
const deadline = performance.now() + ms;
|
|
477
|
+
const handle = setTimeout(() => {
|
|
478
|
+
if (this._mode !== 'direct') {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
this._directTimers.delete(id);
|
|
482
|
+
this._onFire(id);
|
|
483
|
+
}, ms);
|
|
484
|
+
this._directTimers.set(id, { handle, deadline });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (this._mode === 'direct') {
|
|
488
|
+
this._mode = 'heap';
|
|
489
|
+
for (const [existingId, { handle, deadline }] of this._directTimers) {
|
|
490
|
+
clearTimeout(handle);
|
|
491
|
+
const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
|
|
492
|
+
if (remaining === 0) {
|
|
493
|
+
this._onFire(existingId);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
this._heapAdd(existingId, remaining);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
this._directTimers.clear();
|
|
500
|
+
}
|
|
501
|
+
this._heapAdd(id, ms);
|
|
502
|
+
}
|
|
503
|
+
clear() {
|
|
504
|
+
for (const { handle } of this._directTimers.values()) {
|
|
505
|
+
clearTimeout(handle);
|
|
506
|
+
}
|
|
507
|
+
this._directTimers.clear();
|
|
508
|
+
this._mode = 'direct';
|
|
509
|
+
if (this._timer) {
|
|
510
|
+
clearTimeout(this._timer);
|
|
511
|
+
this._timer = null;
|
|
512
|
+
}
|
|
513
|
+
this._deadlines.length = 0;
|
|
514
|
+
this._ids.length = 0;
|
|
515
|
+
this._seqs.length = 0;
|
|
516
|
+
this._counter = 0;
|
|
517
|
+
}
|
|
518
|
+
_heapAdd(id, ms) {
|
|
430
519
|
const deadline = performance.now() + ms;
|
|
520
|
+
const seq = this._counter++;
|
|
431
521
|
let i = this._deadlines.length;
|
|
432
522
|
this._deadlines.push(deadline);
|
|
433
523
|
this._ids.push(id);
|
|
434
|
-
|
|
524
|
+
this._seqs.push(seq);
|
|
435
525
|
while (i > 0) {
|
|
436
526
|
const p = (i - 1) >> 1;
|
|
437
|
-
|
|
527
|
+
const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
|
|
528
|
+
if (parentComesFirst) {
|
|
438
529
|
break;
|
|
530
|
+
}
|
|
439
531
|
this._deadlines[i] = this._deadlines[p];
|
|
440
532
|
this._ids[i] = this._ids[p];
|
|
533
|
+
this._seqs[i] = this._seqs[p];
|
|
441
534
|
i = p;
|
|
442
535
|
}
|
|
443
536
|
this._deadlines[i] = deadline;
|
|
444
537
|
this._ids[i] = id;
|
|
445
|
-
|
|
446
|
-
if (i === 0)
|
|
538
|
+
this._seqs[i] = seq;
|
|
539
|
+
if (i === 0) {
|
|
447
540
|
this._refresh();
|
|
448
|
-
}
|
|
449
|
-
/** Dispose without firing callbacks. */
|
|
450
|
-
clear() {
|
|
451
|
-
if (this._timer) {
|
|
452
|
-
clearTimeout(this._timer);
|
|
453
|
-
this._timer = null;
|
|
454
541
|
}
|
|
455
|
-
this._deadlines.length = 0;
|
|
456
|
-
this._ids.length = 0;
|
|
457
542
|
}
|
|
458
543
|
_refresh() {
|
|
459
544
|
if (this._timer) {
|
|
460
545
|
clearTimeout(this._timer);
|
|
461
546
|
this._timer = null;
|
|
462
547
|
}
|
|
463
|
-
if (this._deadlines.length === 0)
|
|
548
|
+
if (this._deadlines.length === 0) {
|
|
464
549
|
return;
|
|
550
|
+
}
|
|
465
551
|
const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
|
|
466
|
-
|
|
552
|
+
const safeDelay = Math.min(delay, 2147483647);
|
|
553
|
+
this._timer = setTimeout(this._boundTick, safeDelay);
|
|
467
554
|
}
|
|
468
555
|
_onTick() {
|
|
469
556
|
this._timer = null;
|
|
470
557
|
const now = performance.now();
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
558
|
+
try {
|
|
559
|
+
while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
|
|
560
|
+
const id = this._pop();
|
|
561
|
+
this._onFire(id);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
finally {
|
|
565
|
+
this._refresh();
|
|
474
566
|
}
|
|
475
|
-
this._refresh();
|
|
476
567
|
}
|
|
477
|
-
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
478
568
|
_pop() {
|
|
479
569
|
const topId = this._ids[0];
|
|
480
570
|
const lastId = this._ids.pop();
|
|
481
571
|
const lastDeadline = this._deadlines.pop();
|
|
572
|
+
const lastSeq = this._seqs.pop();
|
|
482
573
|
const n = this._deadlines.length;
|
|
483
574
|
if (n > 0) {
|
|
484
575
|
let i = 0;
|
|
485
|
-
|
|
486
|
-
while (
|
|
487
|
-
let
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
576
|
+
const half = n >> 1;
|
|
577
|
+
while (i < half) {
|
|
578
|
+
let minChild = (i << 1) + 1;
|
|
579
|
+
const rightChild = minChild + 1;
|
|
580
|
+
if (rightChild < n) {
|
|
581
|
+
const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
|
|
582
|
+
(this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
|
|
583
|
+
if (rightComesFirst) {
|
|
584
|
+
minChild = rightChild;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
|
|
588
|
+
if (lastComesFirst) {
|
|
495
589
|
break;
|
|
496
|
-
|
|
497
|
-
this.
|
|
498
|
-
i =
|
|
590
|
+
}
|
|
591
|
+
this._deadlines[i] = this._deadlines[minChild];
|
|
592
|
+
this._ids[i] = this._ids[minChild];
|
|
593
|
+
this._seqs[i] = this._seqs[minChild];
|
|
594
|
+
i = minChild;
|
|
499
595
|
}
|
|
500
596
|
this._deadlines[i] = lastDeadline;
|
|
501
597
|
this._ids[i] = lastId;
|
|
598
|
+
this._seqs[i] = lastSeq;
|
|
502
599
|
}
|
|
503
600
|
return topId;
|
|
504
601
|
}
|
|
@@ -1618,8 +1715,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1618
1715
|
while (state.end - state.start > 0) {
|
|
1619
1716
|
const available = state.end - state.start;
|
|
1620
1717
|
if (available < MIN_FRAME_LENGTH) {
|
|
1621
|
-
if (this._handleIncomplete(state, strict))
|
|
1718
|
+
if (this._handleIncomplete(state, strict)) {
|
|
1622
1719
|
return;
|
|
1720
|
+
}
|
|
1623
1721
|
break;
|
|
1624
1722
|
}
|
|
1625
1723
|
const fc = pool[state.start + 1];
|
|
@@ -1648,8 +1746,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1648
1746
|
state.start += 1;
|
|
1649
1747
|
continue;
|
|
1650
1748
|
}
|
|
1651
|
-
if (this._handleIncomplete(state, strict))
|
|
1749
|
+
if (this._handleIncomplete(state, strict)) {
|
|
1652
1750
|
return;
|
|
1751
|
+
}
|
|
1653
1752
|
break;
|
|
1654
1753
|
}
|
|
1655
1754
|
if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
|
|
@@ -1663,8 +1762,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1663
1762
|
state.start += 1;
|
|
1664
1763
|
continue;
|
|
1665
1764
|
}
|
|
1666
|
-
if (this._handleIncomplete(state, strict))
|
|
1765
|
+
if (this._handleIncomplete(state, strict)) {
|
|
1667
1766
|
return;
|
|
1767
|
+
}
|
|
1668
1768
|
break;
|
|
1669
1769
|
}
|
|
1670
1770
|
// CRC check inline: no helper call, no subarray for the CRC body.
|
|
@@ -1726,8 +1826,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1726
1826
|
buffer[0] = unit;
|
|
1727
1827
|
buffer[1] = fc;
|
|
1728
1828
|
if (data.length <= 16) {
|
|
1729
|
-
for (let i = 0; i < data.length; i++)
|
|
1829
|
+
for (let i = 0; i < data.length; i++) {
|
|
1730
1830
|
buffer[2 + i] = data[i];
|
|
1831
|
+
}
|
|
1731
1832
|
}
|
|
1732
1833
|
else {
|
|
1733
1834
|
buffer.set(data, 2);
|
|
@@ -1746,9 +1847,8 @@ const CHAR_CODE = {
|
|
|
1746
1847
|
CR: '\r'.charCodeAt(0),
|
|
1747
1848
|
LF: '\n'.charCodeAt(0),
|
|
1748
1849
|
};
|
|
1749
|
-
// Modbus ASCII frame
|
|
1750
|
-
//
|
|
1751
|
-
// cannot grow `state.frame` without bound.
|
|
1850
|
+
// Modbus ASCII frame body is capped well below the theoretical maximum so a
|
|
1851
|
+
// peer that never sends `\r` cannot grow `state.frame` without bound.
|
|
1752
1852
|
const MAX_ASCII_PAYLOAD = 512;
|
|
1753
1853
|
const HEX_DECODE = new Uint8Array(256);
|
|
1754
1854
|
HEX_DECODE.fill(0xff);
|
|
@@ -1885,14 +1985,16 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1885
1985
|
// dataLen may be 0 for a frame that is only unit + fc + lrc.
|
|
1886
1986
|
const dataLen = byteLen - 3;
|
|
1887
1987
|
const data = Buffer.allocUnsafe(dataLen);
|
|
1988
|
+
let hexOff = 4;
|
|
1888
1989
|
for (let i = 0; i < dataLen; i++) {
|
|
1889
|
-
const hi = HEX_DECODE[hexChars[
|
|
1890
|
-
const lo = HEX_DECODE[hexChars[
|
|
1990
|
+
const hi = HEX_DECODE[hexChars[hexOff]];
|
|
1991
|
+
const lo = HEX_DECODE[hexChars[hexOff + 1]];
|
|
1891
1992
|
if (hi === 0xff || lo === 0xff) {
|
|
1892
1993
|
this.onFramingError(new Error('Invalid hex character'));
|
|
1893
1994
|
return;
|
|
1894
1995
|
}
|
|
1895
1996
|
data[i] = (hi << 4) | lo;
|
|
1997
|
+
hexOff += 2;
|
|
1896
1998
|
}
|
|
1897
1999
|
// Compute LRC over unit + fc + data.
|
|
1898
2000
|
let sum = unit + fc;
|
|
@@ -1923,13 +2025,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1923
2025
|
buffer[0] = unit;
|
|
1924
2026
|
buffer[1] = fc;
|
|
1925
2027
|
buffer.set(data, 2);
|
|
1926
|
-
buffer[buffer.length - 1] = lrc(buffer
|
|
2028
|
+
buffer[buffer.length - 1] = lrc(buffer, 0, buffer.length - 1);
|
|
1927
2029
|
const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
|
|
1928
2030
|
out[0] = CHAR_CODE.COLON;
|
|
2031
|
+
let outOff = 1;
|
|
1929
2032
|
for (let i = 0; i < buffer.length; i++) {
|
|
1930
2033
|
const byte = buffer[i];
|
|
1931
|
-
out[
|
|
1932
|
-
out[
|
|
2034
|
+
out[outOff] = HEX_ENCODE[byte >> 4];
|
|
2035
|
+
out[outOff + 1] = HEX_ENCODE[byte & 0x0f];
|
|
2036
|
+
outOff += 2;
|
|
1933
2037
|
}
|
|
1934
2038
|
out[out.length - 2] = CHAR_CODE.CR;
|
|
1935
2039
|
out[out.length - 1] = CHAR_CODE.LF;
|
|
@@ -2032,7 +2136,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
2032
2136
|
const frame = {
|
|
2033
2137
|
// Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
|
|
2034
2138
|
// argument coercion + bounds check. Symmetric to the header writes in
|
|
2035
|
-
// encode()
|
|
2139
|
+
// encode() below. Hits on every received TCP frame.
|
|
2036
2140
|
transaction: (buffer[0] << 8) | buffer[1],
|
|
2037
2141
|
unit: buffer[6],
|
|
2038
2142
|
fc: buffer[7],
|
|
@@ -2061,8 +2165,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
2061
2165
|
// Small-payload fast path: avoid C++ TypedArray.prototype.set boundary
|
|
2062
2166
|
// crossing when the copy is just a few bytes (common for FC 3/4/6 requests).
|
|
2063
2167
|
if (data.length <= 16) {
|
|
2064
|
-
for (let i = 0; i < data.length; i++)
|
|
2168
|
+
for (let i = 0; i < data.length; i++) {
|
|
2065
2169
|
buffer[8 + i] = data[i];
|
|
2170
|
+
}
|
|
2066
2171
|
}
|
|
2067
2172
|
else {
|
|
2068
2173
|
buffer.set(data, 8);
|
|
@@ -2110,26 +2215,33 @@ class MasterSession {
|
|
|
2110
2215
|
}
|
|
2111
2216
|
|
|
2112
2217
|
function validateResponse(frame, unit, fc) {
|
|
2113
|
-
if (frame.unit !== unit || frame.fc !== fc)
|
|
2218
|
+
if (frame.unit !== unit || frame.fc !== fc) {
|
|
2114
2219
|
throw new Error('Invalid response');
|
|
2220
|
+
}
|
|
2115
2221
|
}
|
|
2116
2222
|
function validateByteCountResponse(frame, unit, fc, byteCount) {
|
|
2117
2223
|
validateResponse(frame, unit, fc);
|
|
2118
|
-
if (frame.data.length < 1 + byteCount)
|
|
2224
|
+
if (frame.data.length < 1 + byteCount) {
|
|
2119
2225
|
throw new Error('Insufficient data length');
|
|
2120
|
-
|
|
2226
|
+
}
|
|
2227
|
+
if (frame.data.length !== 1 + byteCount) {
|
|
2121
2228
|
throw new Error('Invalid response');
|
|
2122
|
-
|
|
2229
|
+
}
|
|
2230
|
+
if (frame.data[0] !== byteCount) {
|
|
2123
2231
|
throw new Error('Invalid response');
|
|
2232
|
+
}
|
|
2124
2233
|
}
|
|
2125
2234
|
function validateEchoResponse(frame, unit, fc, expected) {
|
|
2126
2235
|
validateResponse(frame, unit, fc);
|
|
2127
|
-
if (frame.data.length < expected.length)
|
|
2236
|
+
if (frame.data.length < expected.length) {
|
|
2128
2237
|
throw new Error('Insufficient data length');
|
|
2129
|
-
|
|
2238
|
+
}
|
|
2239
|
+
if (frame.data.length !== expected.length) {
|
|
2130
2240
|
throw new Error('Invalid response');
|
|
2131
|
-
|
|
2241
|
+
}
|
|
2242
|
+
if (!frame.data.equals(expected)) {
|
|
2132
2243
|
throw new Error('Invalid response');
|
|
2244
|
+
}
|
|
2133
2245
|
}
|
|
2134
2246
|
class ModbusMaster extends EventEmitter {
|
|
2135
2247
|
timeout;
|
|
@@ -2157,8 +2269,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2157
2269
|
_pendingExchanges = new Map();
|
|
2158
2270
|
_timerHeap = new TimerHeap((id) => {
|
|
2159
2271
|
const pending = this._pendingExchanges.get(id);
|
|
2160
|
-
if (!pending)
|
|
2161
|
-
return;
|
|
2272
|
+
if (!pending) {
|
|
2273
|
+
return;
|
|
2274
|
+
} // lazy deletion: already handled
|
|
2162
2275
|
pending.settled = true;
|
|
2163
2276
|
this._pendingExchanges.delete(id);
|
|
2164
2277
|
if (pending.sessionKey !== null) {
|
|
@@ -2345,7 +2458,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2345
2458
|
}
|
|
2346
2459
|
// Lazy-deletion timer architecture:
|
|
2347
2460
|
// 1. Assign an exchangeId and register in _pendingExchanges.
|
|
2348
|
-
// 2. Push deadline into the global TimerHeap (
|
|
2461
|
+
// 2. Push deadline into the global TimerHeap (one native setTimeout under
|
|
2462
|
+
// load; a fast direct-timer path is used when only 1-2 exchanges are
|
|
2463
|
+
// pending).
|
|
2349
2464
|
// 3. When the response arrives, delete from Map — the heap entry is left
|
|
2350
2465
|
// behind and silently discarded when it surfaces at the top (lazy deletion).
|
|
2351
2466
|
const exchangeId = this._nextExchangeId++;
|
|
@@ -2356,11 +2471,13 @@ class ModbusMaster extends EventEmitter {
|
|
|
2356
2471
|
this._timerHeap.add(exchangeId, timeout);
|
|
2357
2472
|
connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
|
|
2358
2473
|
const p = this._pendingExchanges.get(exchangeId);
|
|
2359
|
-
if (!p || p.settled)
|
|
2474
|
+
if (!p || p.settled) {
|
|
2360
2475
|
return;
|
|
2476
|
+
}
|
|
2361
2477
|
const cb = p.callback;
|
|
2362
|
-
if (!cb)
|
|
2478
|
+
if (!cb) {
|
|
2363
2479
|
return;
|
|
2480
|
+
}
|
|
2364
2481
|
p.settled = true;
|
|
2365
2482
|
p.callback = null;
|
|
2366
2483
|
this._pendingExchanges.delete(exchangeId);
|
|
@@ -2389,8 +2506,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2389
2506
|
this._timerHeap.add(exchangeId, timeout);
|
|
2390
2507
|
connection.write(payload, (writeErr) => {
|
|
2391
2508
|
const p = this._pendingExchanges.get(exchangeId);
|
|
2392
|
-
if (!p || p.settled)
|
|
2509
|
+
if (!p || p.settled) {
|
|
2393
2510
|
return;
|
|
2511
|
+
}
|
|
2394
2512
|
if (writeErr) {
|
|
2395
2513
|
const cb = p.callback;
|
|
2396
2514
|
if (cb) {
|
|
@@ -2415,8 +2533,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2415
2533
|
// Timeout is managed by the global timer heap above.
|
|
2416
2534
|
this._masterSession.start(key, (err, frame) => {
|
|
2417
2535
|
const p2 = this._pendingExchanges.get(exchangeId);
|
|
2418
|
-
if (!p2 || p2.settled)
|
|
2536
|
+
if (!p2 || p2.settled) {
|
|
2419
2537
|
return;
|
|
2538
|
+
}
|
|
2420
2539
|
const cb = p2.callback;
|
|
2421
2540
|
if (cb) {
|
|
2422
2541
|
p2.settled = true;
|
|
@@ -2449,8 +2568,42 @@ class ModbusMaster extends EventEmitter {
|
|
|
2449
2568
|
try {
|
|
2450
2569
|
validateByteCountResponse(frame, unit, fc, byteCount);
|
|
2451
2570
|
const data = new Array(length);
|
|
2452
|
-
|
|
2453
|
-
|
|
2571
|
+
let byteIdx = 1;
|
|
2572
|
+
let outIdx = 0;
|
|
2573
|
+
const fullBytes = length >> 3;
|
|
2574
|
+
for (let b = 0; b < fullBytes; b++) {
|
|
2575
|
+
const byte = frame.data[byteIdx++];
|
|
2576
|
+
data[outIdx++] = (byte & 0x01) > 0;
|
|
2577
|
+
data[outIdx++] = (byte & 0x02) > 0;
|
|
2578
|
+
data[outIdx++] = (byte & 0x04) > 0;
|
|
2579
|
+
data[outIdx++] = (byte & 0x08) > 0;
|
|
2580
|
+
data[outIdx++] = (byte & 0x10) > 0;
|
|
2581
|
+
data[outIdx++] = (byte & 0x20) > 0;
|
|
2582
|
+
data[outIdx++] = (byte & 0x40) > 0;
|
|
2583
|
+
data[outIdx++] = (byte & 0x80) > 0;
|
|
2584
|
+
}
|
|
2585
|
+
const rem = length & 7;
|
|
2586
|
+
if (rem) {
|
|
2587
|
+
const byte = frame.data[byteIdx];
|
|
2588
|
+
data[outIdx++] = (byte & 0x01) > 0;
|
|
2589
|
+
if (rem > 1) {
|
|
2590
|
+
data[outIdx++] = (byte & 0x02) > 0;
|
|
2591
|
+
}
|
|
2592
|
+
if (rem > 2) {
|
|
2593
|
+
data[outIdx++] = (byte & 0x04) > 0;
|
|
2594
|
+
}
|
|
2595
|
+
if (rem > 3) {
|
|
2596
|
+
data[outIdx++] = (byte & 0x08) > 0;
|
|
2597
|
+
}
|
|
2598
|
+
if (rem > 4) {
|
|
2599
|
+
data[outIdx++] = (byte & 0x10) > 0;
|
|
2600
|
+
}
|
|
2601
|
+
if (rem > 5) {
|
|
2602
|
+
data[outIdx++] = (byte & 0x20) > 0;
|
|
2603
|
+
}
|
|
2604
|
+
if (rem > 6) {
|
|
2605
|
+
data[outIdx++] = (byte & 0x40) > 0;
|
|
2606
|
+
}
|
|
2454
2607
|
}
|
|
2455
2608
|
// Mutate the frame in place rather than spread-copying — `frame` is freshly
|
|
2456
2609
|
// allocated per request and not retained anywhere else.
|
|
@@ -2498,9 +2651,10 @@ class ModbusMaster extends EventEmitter {
|
|
|
2498
2651
|
// on each call. Symmetric to the slave-side BE write inlining
|
|
2499
2652
|
// in handleFC3/FC4. At length=125 (FC3 max) that's 250 saved
|
|
2500
2653
|
// bounds-check pairs per response.
|
|
2654
|
+
let off = 0;
|
|
2501
2655
|
for (let i = 0; i < length; i++) {
|
|
2502
|
-
const off = i * 2;
|
|
2503
2656
|
data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
|
|
2657
|
+
off += 2;
|
|
2504
2658
|
}
|
|
2505
2659
|
frame.data = data;
|
|
2506
2660
|
resolve(frame);
|
|
@@ -2591,10 +2745,19 @@ class ModbusMaster extends EventEmitter {
|
|
|
2591
2745
|
bufferTx[2] = (value.length >>> 8) & 0xff;
|
|
2592
2746
|
bufferTx[3] = value.length & 0xff;
|
|
2593
2747
|
bufferTx[4] = byteCount;
|
|
2748
|
+
let acc = 0;
|
|
2749
|
+
let out = 5;
|
|
2594
2750
|
for (let i = 0; i < value.length; i++) {
|
|
2595
2751
|
if (value[i]) {
|
|
2596
|
-
|
|
2752
|
+
acc |= 1 << (i & 7);
|
|
2597
2753
|
}
|
|
2754
|
+
if ((i & 7) === 7) {
|
|
2755
|
+
bufferTx[out++] = acc;
|
|
2756
|
+
acc = 0;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
if ((value.length & 7) !== 0) {
|
|
2760
|
+
bufferTx[out] = acc;
|
|
2598
2761
|
}
|
|
2599
2762
|
return new Promise((resolve, reject) => {
|
|
2600
2763
|
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
@@ -2628,11 +2791,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2628
2791
|
bufferTx[2] = (value.length >>> 8) & 0xff;
|
|
2629
2792
|
bufferTx[3] = value.length & 0xff;
|
|
2630
2793
|
bufferTx[4] = byteCount;
|
|
2794
|
+
let off = 5;
|
|
2631
2795
|
for (let i = 0; i < value.length; i++) {
|
|
2632
2796
|
const v = value[i];
|
|
2633
|
-
const off = 5 + i * 2;
|
|
2634
2797
|
bufferTx[off] = (v >>> 8) & 0xff;
|
|
2635
2798
|
bufferTx[off + 1] = v & 0xff;
|
|
2799
|
+
off += 2;
|
|
2636
2800
|
}
|
|
2637
2801
|
return new Promise((resolve, reject) => {
|
|
2638
2802
|
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
@@ -2670,10 +2834,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2670
2834
|
}
|
|
2671
2835
|
try {
|
|
2672
2836
|
validateResponse(frame, unit, fc);
|
|
2673
|
-
if (frame.data.length < 2 + serverIdLength)
|
|
2837
|
+
if (frame.data.length < 2 + serverIdLength) {
|
|
2674
2838
|
throw new Error('Insufficient data length');
|
|
2675
|
-
|
|
2839
|
+
}
|
|
2840
|
+
if (frame.data.length !== 1 + frame.data[0]) {
|
|
2676
2841
|
throw new Error('Invalid response');
|
|
2842
|
+
}
|
|
2677
2843
|
const runStatusIndex = 1 + serverIdLength;
|
|
2678
2844
|
frame.data = {
|
|
2679
2845
|
serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
|
|
@@ -2736,11 +2902,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2736
2902
|
bufferTx[6] = (write.value.length >>> 8) & 0xff;
|
|
2737
2903
|
bufferTx[7] = write.value.length & 0xff;
|
|
2738
2904
|
bufferTx[8] = byteCount;
|
|
2905
|
+
let off = 9;
|
|
2739
2906
|
for (let i = 0; i < write.value.length; i++) {
|
|
2740
2907
|
const v = write.value[i];
|
|
2741
|
-
const off = 9 + i * 2;
|
|
2742
2908
|
bufferTx[off] = (v >>> 8) & 0xff;
|
|
2743
2909
|
bufferTx[off + 1] = v & 0xff;
|
|
2910
|
+
off += 2;
|
|
2744
2911
|
}
|
|
2745
2912
|
return new Promise((resolve, reject) => {
|
|
2746
2913
|
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
@@ -2759,9 +2926,10 @@ class ModbusMaster extends EventEmitter {
|
|
|
2759
2926
|
// closure + N readUInt16BE bounds-check pairs. See writeFC3Or4
|
|
2760
2927
|
// response handler for the same optimization.
|
|
2761
2928
|
const data = new Array(read.length);
|
|
2929
|
+
let off = 0;
|
|
2762
2930
|
for (let i = 0; i < read.length; i++) {
|
|
2763
|
-
const off = i * 2;
|
|
2764
2931
|
data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
|
|
2932
|
+
off += 2;
|
|
2765
2933
|
}
|
|
2766
2934
|
frame.data = data;
|
|
2767
2935
|
resolve(frame);
|
|
@@ -2787,10 +2955,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2787
2955
|
}
|
|
2788
2956
|
try {
|
|
2789
2957
|
validateResponse(frame, unit, fc);
|
|
2790
|
-
if (frame.data.length < 6)
|
|
2958
|
+
if (frame.data.length < 6) {
|
|
2791
2959
|
throw new Error('Insufficient data length');
|
|
2792
|
-
|
|
2960
|
+
}
|
|
2961
|
+
if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode) {
|
|
2793
2962
|
throw new Error('Invalid response');
|
|
2963
|
+
}
|
|
2794
2964
|
const objects = [];
|
|
2795
2965
|
let object = [];
|
|
2796
2966
|
let totalBytes = 0;
|
|
@@ -2818,10 +2988,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2818
2988
|
break;
|
|
2819
2989
|
}
|
|
2820
2990
|
}
|
|
2821
|
-
if (objects.length !== frame.data[5])
|
|
2991
|
+
if (objects.length !== frame.data[5]) {
|
|
2822
2992
|
throw new Error('Invalid response');
|
|
2823
|
-
|
|
2993
|
+
}
|
|
2994
|
+
if (frame.data.length !== 6 + totalBytes) {
|
|
2824
2995
|
throw new Error('Invalid response');
|
|
2996
|
+
}
|
|
2825
2997
|
frame.data = {
|
|
2826
2998
|
readDeviceIDCode,
|
|
2827
2999
|
conformityLevel: frame.data[2],
|
|
@@ -2868,7 +3040,7 @@ class ModbusMaster extends EventEmitter {
|
|
|
2868
3040
|
});
|
|
2869
3041
|
}
|
|
2870
3042
|
/**
|
|
2871
|
-
* Open the underlying physical layer and
|
|
3043
|
+
* Open the underlying physical layer and establish a connection.
|
|
2872
3044
|
*
|
|
2873
3045
|
* A `ModbusMaster` instance can only be opened once. Once {@link close}
|
|
2874
3046
|
* is called — explicitly or because the physical layer disconnected —
|
|
@@ -2898,8 +3070,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2898
3070
|
// (broadcasts have no session waiter; non-broadcasts still in the
|
|
2899
3071
|
// pre-write-window haven't registered in session yet).
|
|
2900
3072
|
for (const pending of this._pendingExchanges.values()) {
|
|
2901
|
-
if (pending.settled)
|
|
3073
|
+
if (pending.settled) {
|
|
2902
3074
|
continue;
|
|
3075
|
+
}
|
|
2903
3076
|
pending.settled = true;
|
|
2904
3077
|
const cb = pending.callback;
|
|
2905
3078
|
if (cb) {
|
|
@@ -3057,23 +3230,68 @@ class ModbusSlave extends EventEmitter {
|
|
|
3057
3230
|
const byteCount = (length + 7) >> 3;
|
|
3058
3231
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
3059
3232
|
pdu[0] = byteCount;
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3233
|
+
if (coils instanceof Uint8Array) {
|
|
3234
|
+
// Branchless fast path — `coils[i]` is already 0/1, no boolean
|
|
3235
|
+
// coercion or conditional jumps. At max payload (2000 coils) this
|
|
3236
|
+
// avoids 2000 branch-predictor slots and boolean-to-number casts.
|
|
3237
|
+
let out = 1;
|
|
3238
|
+
const fullBytes = length >> 3;
|
|
3239
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
3240
|
+
const base = i << 3;
|
|
3241
|
+
pdu[out++] =
|
|
3242
|
+
(coils[base] & 1) |
|
|
3243
|
+
((coils[base + 1] & 1) << 1) |
|
|
3244
|
+
((coils[base + 2] & 1) << 2) |
|
|
3245
|
+
((coils[base + 3] & 1) << 3) |
|
|
3246
|
+
((coils[base + 4] & 1) << 4) |
|
|
3247
|
+
((coils[base + 5] & 1) << 5) |
|
|
3248
|
+
((coils[base + 6] & 1) << 6) |
|
|
3249
|
+
((coils[base + 7] & 1) << 7);
|
|
3250
|
+
}
|
|
3251
|
+
const rem = length & 7;
|
|
3252
|
+
if (rem) {
|
|
3253
|
+
const base = fullBytes << 3;
|
|
3254
|
+
let acc = coils[base] & 1;
|
|
3255
|
+
if (rem > 1) {
|
|
3256
|
+
acc |= (coils[base + 1] & 1) << 1;
|
|
3257
|
+
}
|
|
3258
|
+
if (rem > 2) {
|
|
3259
|
+
acc |= (coils[base + 2] & 1) << 2;
|
|
3260
|
+
}
|
|
3261
|
+
if (rem > 3) {
|
|
3262
|
+
acc |= (coils[base + 3] & 1) << 3;
|
|
3263
|
+
}
|
|
3264
|
+
if (rem > 4) {
|
|
3265
|
+
acc |= (coils[base + 4] & 1) << 4;
|
|
3266
|
+
}
|
|
3267
|
+
if (rem > 5) {
|
|
3268
|
+
acc |= (coils[base + 5] & 1) << 5;
|
|
3269
|
+
}
|
|
3270
|
+
if (rem > 6) {
|
|
3271
|
+
acc |= (coils[base + 6] & 1) << 6;
|
|
3272
|
+
}
|
|
3273
|
+
pdu[out] = acc;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
else {
|
|
3277
|
+
// Fallback for boolean[] — accumulate into `acc` and write a full byte
|
|
3278
|
+
// once each lane is finished. Saves N `|=` read-modify-writes on the
|
|
3279
|
+
// output buffer.
|
|
3280
|
+
let acc = 0;
|
|
3281
|
+
let out = 1;
|
|
3282
|
+
for (let i = 0; i < length; i++) {
|
|
3283
|
+
if (coils[i]) {
|
|
3284
|
+
acc |= 1 << (i & 7);
|
|
3285
|
+
}
|
|
3286
|
+
if ((i & 7) === 7) {
|
|
3287
|
+
pdu[out++] = acc;
|
|
3288
|
+
acc = 0;
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
if ((length & 7) !== 0) {
|
|
3292
|
+
pdu[out] = acc;
|
|
3073
3293
|
}
|
|
3074
3294
|
}
|
|
3075
|
-
if ((length & 7) !== 0)
|
|
3076
|
-
pdu[out] = acc;
|
|
3077
3295
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3078
3296
|
}
|
|
3079
3297
|
catch (error) {
|
|
@@ -3104,19 +3322,62 @@ class ModbusSlave extends EventEmitter {
|
|
|
3104
3322
|
const byteCount = (length + 7) >> 3;
|
|
3105
3323
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
3106
3324
|
pdu[0] = byteCount;
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3325
|
+
if (discreteInputs instanceof Uint8Array) {
|
|
3326
|
+
let out = 1;
|
|
3327
|
+
const fullBytes = length >> 3;
|
|
3328
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
3329
|
+
const base = i << 3;
|
|
3330
|
+
pdu[out++] =
|
|
3331
|
+
(discreteInputs[base] & 1) |
|
|
3332
|
+
((discreteInputs[base + 1] & 1) << 1) |
|
|
3333
|
+
((discreteInputs[base + 2] & 1) << 2) |
|
|
3334
|
+
((discreteInputs[base + 3] & 1) << 3) |
|
|
3335
|
+
((discreteInputs[base + 4] & 1) << 4) |
|
|
3336
|
+
((discreteInputs[base + 5] & 1) << 5) |
|
|
3337
|
+
((discreteInputs[base + 6] & 1) << 6) |
|
|
3338
|
+
((discreteInputs[base + 7] & 1) << 7);
|
|
3339
|
+
}
|
|
3340
|
+
const rem = length & 7;
|
|
3341
|
+
if (rem) {
|
|
3342
|
+
const base = fullBytes << 3;
|
|
3343
|
+
let acc = discreteInputs[base] & 1;
|
|
3344
|
+
if (rem > 1) {
|
|
3345
|
+
acc |= (discreteInputs[base + 1] & 1) << 1;
|
|
3346
|
+
}
|
|
3347
|
+
if (rem > 2) {
|
|
3348
|
+
acc |= (discreteInputs[base + 2] & 1) << 2;
|
|
3349
|
+
}
|
|
3350
|
+
if (rem > 3) {
|
|
3351
|
+
acc |= (discreteInputs[base + 3] & 1) << 3;
|
|
3352
|
+
}
|
|
3353
|
+
if (rem > 4) {
|
|
3354
|
+
acc |= (discreteInputs[base + 4] & 1) << 4;
|
|
3355
|
+
}
|
|
3356
|
+
if (rem > 5) {
|
|
3357
|
+
acc |= (discreteInputs[base + 5] & 1) << 5;
|
|
3358
|
+
}
|
|
3359
|
+
if (rem > 6) {
|
|
3360
|
+
acc |= (discreteInputs[base + 6] & 1) << 6;
|
|
3361
|
+
}
|
|
3362
|
+
pdu[out] = acc;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
else {
|
|
3366
|
+
let acc = 0;
|
|
3367
|
+
let out = 1;
|
|
3368
|
+
for (let i = 0; i < length; i++) {
|
|
3369
|
+
if (discreteInputs[i]) {
|
|
3370
|
+
acc |= 1 << (i & 7);
|
|
3371
|
+
}
|
|
3372
|
+
if ((i & 7) === 7) {
|
|
3373
|
+
pdu[out++] = acc;
|
|
3374
|
+
acc = 0;
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
if ((length & 7) !== 0) {
|
|
3378
|
+
pdu[out] = acc;
|
|
3116
3379
|
}
|
|
3117
3380
|
}
|
|
3118
|
-
if ((length & 7) !== 0)
|
|
3119
|
-
pdu[out] = acc;
|
|
3120
3381
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3121
3382
|
}
|
|
3122
3383
|
catch (error) {
|
|
@@ -3149,11 +3410,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
3149
3410
|
// Inline big-endian write — `pdu[i] = v` is a direct typed-array store,
|
|
3150
3411
|
// while `writeUInt16BE` runs argument validation + bounds checks on each
|
|
3151
3412
|
// call. At length=125 (FC3 max) that's 250 saved checks per request.
|
|
3413
|
+
let off = 1;
|
|
3152
3414
|
for (let i = 0; i < length; i++) {
|
|
3153
3415
|
const v = registers[i];
|
|
3154
|
-
const off = 1 + i * 2;
|
|
3155
3416
|
pdu[off] = (v >>> 8) & 0xff;
|
|
3156
3417
|
pdu[off + 1] = v & 0xff;
|
|
3418
|
+
off += 2;
|
|
3157
3419
|
}
|
|
3158
3420
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3159
3421
|
}
|
|
@@ -3185,11 +3447,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
3185
3447
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
3186
3448
|
pdu[0] = length * 2;
|
|
3187
3449
|
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3450
|
+
let off = 1;
|
|
3188
3451
|
for (let i = 0; i < length; i++) {
|
|
3189
3452
|
const v = registers[i];
|
|
3190
|
-
const off = 1 + i * 2;
|
|
3191
3453
|
pdu[off] = (v >>> 8) & 0xff;
|
|
3192
3454
|
pdu[off + 1] = v & 0xff;
|
|
3455
|
+
off += 2;
|
|
3193
3456
|
}
|
|
3194
3457
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3195
3458
|
}
|
|
@@ -3268,12 +3531,42 @@ class ModbusSlave extends EventEmitter {
|
|
|
3268
3531
|
return;
|
|
3269
3532
|
}
|
|
3270
3533
|
const value = new Array(length);
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
value[
|
|
3534
|
+
let byteIdx = 5;
|
|
3535
|
+
let outIdx = 0;
|
|
3536
|
+
const fullBytes = length >> 3;
|
|
3537
|
+
for (let b = 0; b < fullBytes; b++) {
|
|
3538
|
+
const byte = frame.data[byteIdx++];
|
|
3539
|
+
value[outIdx++] = (byte & 0x01) > 0;
|
|
3540
|
+
value[outIdx++] = (byte & 0x02) > 0;
|
|
3541
|
+
value[outIdx++] = (byte & 0x04) > 0;
|
|
3542
|
+
value[outIdx++] = (byte & 0x08) > 0;
|
|
3543
|
+
value[outIdx++] = (byte & 0x10) > 0;
|
|
3544
|
+
value[outIdx++] = (byte & 0x20) > 0;
|
|
3545
|
+
value[outIdx++] = (byte & 0x40) > 0;
|
|
3546
|
+
value[outIdx++] = (byte & 0x80) > 0;
|
|
3547
|
+
}
|
|
3548
|
+
const rem = length & 7;
|
|
3549
|
+
if (rem) {
|
|
3550
|
+
const byte = frame.data[byteIdx];
|
|
3551
|
+
value[outIdx++] = (byte & 0x01) > 0;
|
|
3552
|
+
if (rem > 1) {
|
|
3553
|
+
value[outIdx++] = (byte & 0x02) > 0;
|
|
3554
|
+
}
|
|
3555
|
+
if (rem > 2) {
|
|
3556
|
+
value[outIdx++] = (byte & 0x04) > 0;
|
|
3557
|
+
}
|
|
3558
|
+
if (rem > 3) {
|
|
3559
|
+
value[outIdx++] = (byte & 0x08) > 0;
|
|
3560
|
+
}
|
|
3561
|
+
if (rem > 4) {
|
|
3562
|
+
value[outIdx++] = (byte & 0x10) > 0;
|
|
3563
|
+
}
|
|
3564
|
+
if (rem > 5) {
|
|
3565
|
+
value[outIdx++] = (byte & 0x20) > 0;
|
|
3566
|
+
}
|
|
3567
|
+
if (rem > 6) {
|
|
3568
|
+
value[outIdx++] = (byte & 0x40) > 0;
|
|
3569
|
+
}
|
|
3277
3570
|
}
|
|
3278
3571
|
try {
|
|
3279
3572
|
if (model.writeMultipleCoils) {
|
|
@@ -3311,8 +3604,10 @@ class ModbusSlave extends EventEmitter {
|
|
|
3311
3604
|
return;
|
|
3312
3605
|
}
|
|
3313
3606
|
const value = new Array(length);
|
|
3607
|
+
let off = 5;
|
|
3314
3608
|
for (let i = 0; i < length; i++) {
|
|
3315
|
-
value[i] = (frame.data[
|
|
3609
|
+
value[i] = (frame.data[off] << 8) | frame.data[off + 1];
|
|
3610
|
+
off += 2;
|
|
3316
3611
|
}
|
|
3317
3612
|
try {
|
|
3318
3613
|
if (model.writeMultipleRegisters) {
|
|
@@ -3423,8 +3718,10 @@ class ModbusSlave extends EventEmitter {
|
|
|
3423
3718
|
return;
|
|
3424
3719
|
}
|
|
3425
3720
|
const value = new Array(length.write);
|
|
3721
|
+
let off = 9;
|
|
3426
3722
|
for (let i = 0; i < length.write; i++) {
|
|
3427
|
-
value[i] = (frame.data[
|
|
3723
|
+
value[i] = (frame.data[off] << 8) | frame.data[off + 1];
|
|
3724
|
+
off += 2;
|
|
3428
3725
|
}
|
|
3429
3726
|
try {
|
|
3430
3727
|
await this._withIntervalLock(address.write, address.write + length.write, async () => {
|
|
@@ -3441,11 +3738,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
3441
3738
|
const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
|
|
3442
3739
|
pdu[0] = length.read * 2;
|
|
3443
3740
|
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3741
|
+
let off = 1;
|
|
3444
3742
|
for (let i = 0; i < length.read; i++) {
|
|
3445
3743
|
const v = registers[i];
|
|
3446
|
-
const off = 1 + i * 2;
|
|
3447
3744
|
pdu[off] = (v >>> 8) & 0xff;
|
|
3448
3745
|
pdu[off + 1] = v & 0xff;
|
|
3746
|
+
off += 2;
|
|
3449
3747
|
}
|
|
3450
3748
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3451
3749
|
}
|
|
@@ -3677,8 +3975,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
3677
3975
|
for (let i = 0; i < locks.length; i++) {
|
|
3678
3976
|
const l = locks[i];
|
|
3679
3977
|
if (l.lo < hi && lo < l.hi) {
|
|
3680
|
-
if (overlap === null)
|
|
3978
|
+
if (overlap === null) {
|
|
3681
3979
|
overlap = [];
|
|
3980
|
+
}
|
|
3682
3981
|
overlap.push(l.promise);
|
|
3683
3982
|
}
|
|
3684
3983
|
}
|
|
@@ -3714,8 +4013,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
3714
4013
|
if (i !== -1) {
|
|
3715
4014
|
// O(1) swap-and-pop since lock order doesn't matter for correctness.
|
|
3716
4015
|
const last = locks.length - 1;
|
|
3717
|
-
if (i !== last)
|
|
4016
|
+
if (i !== last) {
|
|
3718
4017
|
locks[i] = locks[last];
|
|
4018
|
+
}
|
|
3719
4019
|
locks.pop();
|
|
3720
4020
|
}
|
|
3721
4021
|
}
|