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