njs-modbus 3.2.0 → 3.4.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 +185 -110
- package/README.zh-CN.md +185 -109
- package/dist/index.cjs +1084 -588
- package/dist/index.d.ts +92 -61
- package/dist/index.mjs +1084 -588
- package/dist/utils.cjs +293 -168
- package/dist/utils.d.ts +43 -40
- package/dist/utils.mjs +289 -167
- package/package.json +4 -3
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
|
|
235
|
+
const CRC_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,131 +249,127 @@ 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
|
-
|
|
252
|
+
]);
|
|
253
|
+
/** CRC-16 (Modbus) over a single contiguous buffer. */
|
|
254
|
+
function crcFixed(data, start, end) {
|
|
219
255
|
let crc = 0xffff;
|
|
220
256
|
for (let index = start; index < end; index++) {
|
|
221
|
-
crc =
|
|
257
|
+
crc = CRC_TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
|
|
222
258
|
}
|
|
223
259
|
return crc;
|
|
224
260
|
}
|
|
225
|
-
|
|
226
261
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* Used for byte-level Modbus payload validation (function-code values, raw
|
|
230
|
-
* byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
|
|
231
|
-
* Infinity, and out-of-range values uniformly.
|
|
262
|
+
* CRC-16 (Modbus) over two contiguous buffer segments.
|
|
263
|
+
* Computes CRC(head[headOff:headOff+headLen]) followed by CRC(tail[tailOff:tailOff+tailLen]).
|
|
232
264
|
*/
|
|
233
|
-
function
|
|
234
|
-
|
|
265
|
+
function crcDual(head, headOff, headLen, tail, tailOff, tailLen) {
|
|
266
|
+
let crc = 0xffff;
|
|
267
|
+
const headEnd = headOff + headLen;
|
|
268
|
+
for (let i = headOff; i < headEnd; i++) {
|
|
269
|
+
crc = CRC_TABLE[(crc ^ head[i]) & 0xff] ^ (crc >> 8);
|
|
270
|
+
}
|
|
271
|
+
const tailEnd = tailOff + tailLen;
|
|
272
|
+
for (let i = tailOff; i < tailEnd; i++) {
|
|
273
|
+
crc = CRC_TABLE[(crc ^ tail[i]) & 0xff] ^ (crc >> 8);
|
|
274
|
+
}
|
|
275
|
+
return crc;
|
|
235
276
|
}
|
|
236
277
|
|
|
237
|
-
function lrc(data, start
|
|
278
|
+
function lrc(data, start, end) {
|
|
238
279
|
let sum = 0;
|
|
239
280
|
for (let i = start; i < end; i++) {
|
|
240
281
|
sum += data[i];
|
|
241
282
|
}
|
|
242
|
-
return
|
|
283
|
+
return -sum & 0xff;
|
|
243
284
|
}
|
|
244
285
|
|
|
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
286
|
const PREDICT_NEED_MORE = 0;
|
|
278
|
-
/** Sentinel: function code is not in the standard tables. */
|
|
279
287
|
const PREDICT_UNKNOWN = -1;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
const REQ_TABLE = new Int32Array(256);
|
|
289
|
+
const RES_TABLE = new Int32Array(256);
|
|
290
|
+
(function initTables() {
|
|
291
|
+
REQ_TABLE[FunctionCode.READ_COILS] = 8;
|
|
292
|
+
REQ_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = 8;
|
|
293
|
+
REQ_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = 8;
|
|
294
|
+
REQ_TABLE[FunctionCode.READ_INPUT_REGISTERS] = 8;
|
|
295
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
296
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
297
|
+
REQ_TABLE[FunctionCode.REPORT_SERVER_ID] = 4;
|
|
298
|
+
REQ_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
299
|
+
REQ_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
|
|
300
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
|
|
301
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
|
|
302
|
+
REQ_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
|
|
303
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
304
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
305
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = 8;
|
|
306
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
|
|
307
|
+
RES_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
308
|
+
RES_TABLE[FunctionCode.READ_COILS] = -517;
|
|
309
|
+
RES_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = -517;
|
|
310
|
+
RES_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = -517;
|
|
311
|
+
RES_TABLE[FunctionCode.READ_INPUT_REGISTERS] = -517;
|
|
312
|
+
RES_TABLE[FunctionCode.REPORT_SERVER_ID] = -517;
|
|
313
|
+
RES_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
|
|
314
|
+
RES_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
|
|
315
|
+
})();
|
|
316
|
+
function predictRtuFrameLength(residual, data, residualLen, start, end, isResponse) {
|
|
317
|
+
const len = end - start;
|
|
318
|
+
if (len < 2) {
|
|
294
319
|
return PREDICT_NEED_MORE;
|
|
295
320
|
}
|
|
296
|
-
const fc =
|
|
297
|
-
if (isResponse
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
321
|
+
const fc = start + 1 < residualLen ? residual[start + 1] : data[start + 1 - residualLen];
|
|
322
|
+
if (isResponse) {
|
|
323
|
+
if ((fc & EXCEPTION_OFFSET) !== 0) {
|
|
324
|
+
return 5;
|
|
325
|
+
}
|
|
326
|
+
const val = RES_TABLE[fc];
|
|
327
|
+
if (val > 0) {
|
|
328
|
+
return val;
|
|
329
|
+
}
|
|
330
|
+
if (val < 0) {
|
|
331
|
+
if (val === -999) {
|
|
332
|
+
// FC 43 / MEI 14 response — inline to avoid function-call overhead on
|
|
333
|
+
// the framing hot path (even though this FC is uncommon).
|
|
334
|
+
if (end - start < 8) {
|
|
335
|
+
return PREDICT_NEED_MORE;
|
|
336
|
+
}
|
|
337
|
+
if ((start + 2 < residualLen ? residual[start + 2] : data[start + 2 - residualLen]) !== MEI_READ_DEVICE_ID) {
|
|
338
|
+
return PREDICT_UNKNOWN;
|
|
339
|
+
}
|
|
340
|
+
const numObjs = start + 7 < residualLen ? residual[start + 7] : data[start + 7 - residualLen];
|
|
341
|
+
let cursor = start + 8;
|
|
342
|
+
for (let i = 0; i < numObjs; i++) {
|
|
343
|
+
if (end < cursor + 2) {
|
|
344
|
+
return PREDICT_NEED_MORE;
|
|
345
|
+
}
|
|
346
|
+
cursor += 2 + (cursor + 1 < residualLen ? residual[cursor + 1] : data[cursor + 1 - residualLen]);
|
|
347
|
+
}
|
|
348
|
+
return cursor - start + 2;
|
|
349
|
+
}
|
|
350
|
+
const decode = -val;
|
|
351
|
+
const offset = decode >> 8;
|
|
352
|
+
if (len <= offset) {
|
|
353
|
+
return PREDICT_NEED_MORE;
|
|
354
|
+
}
|
|
355
|
+
return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
|
|
308
356
|
}
|
|
309
|
-
return bc.extra + buffer[start + bc.offset];
|
|
310
|
-
}
|
|
311
|
-
if (isResponse && fc === FunctionCode.READ_DEVICE_IDENTIFICATION) {
|
|
312
|
-
return predictFc43_14Response(buffer, start, end);
|
|
313
357
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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;
|
|
358
|
+
else {
|
|
359
|
+
const val = REQ_TABLE[fc];
|
|
360
|
+
if (val > 0) {
|
|
361
|
+
return val;
|
|
362
|
+
}
|
|
363
|
+
if (val < 0) {
|
|
364
|
+
const decode = -val;
|
|
365
|
+
const offset = decode >> 8;
|
|
366
|
+
if (len <= offset) {
|
|
367
|
+
return PREDICT_NEED_MORE;
|
|
368
|
+
}
|
|
369
|
+
return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
|
|
337
370
|
}
|
|
338
|
-
const objLen = buffer[cursor + 1];
|
|
339
|
-
cursor += 2 + objLen;
|
|
340
371
|
}
|
|
341
|
-
return
|
|
372
|
+
return PREDICT_UNKNOWN;
|
|
342
373
|
}
|
|
343
374
|
|
|
344
375
|
/**
|
|
@@ -347,10 +378,12 @@ function predictFc43_14Response(buffer, start, end) {
|
|
|
347
378
|
function promisifyCb(fn) {
|
|
348
379
|
return new Promise((resolve, reject) => {
|
|
349
380
|
fn((err) => {
|
|
350
|
-
if (err)
|
|
381
|
+
if (err) {
|
|
351
382
|
reject(err);
|
|
352
|
-
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
353
385
|
resolve();
|
|
386
|
+
}
|
|
354
387
|
});
|
|
355
388
|
});
|
|
356
389
|
}
|
|
@@ -376,7 +409,12 @@ function resolveOne(value, baudRate, fastBaudMs) {
|
|
|
376
409
|
if (baudRate === undefined) {
|
|
377
410
|
return undefined;
|
|
378
411
|
}
|
|
379
|
-
|
|
412
|
+
if (baudRate > 19200) {
|
|
413
|
+
return fastBaudMs;
|
|
414
|
+
}
|
|
415
|
+
const ms = bitsToMs(baudRate, value.value);
|
|
416
|
+
const trunc = ms | 0;
|
|
417
|
+
return trunc + (ms > trunc ? 1 : 0);
|
|
380
418
|
}
|
|
381
419
|
/**
|
|
382
420
|
* Resolve Modbus RTU timing parameters from user options into milliseconds.
|
|
@@ -393,7 +431,17 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
393
431
|
if (intervalBetweenFrames === undefined) {
|
|
394
432
|
// Spec default: t3.5 derived from baudRate, or 0 when neither option nor
|
|
395
433
|
// baudRate were supplied.
|
|
396
|
-
|
|
434
|
+
if (baudRate === undefined) {
|
|
435
|
+
intervalBetweenFrames = 0;
|
|
436
|
+
}
|
|
437
|
+
else if (baudRate > 19200) {
|
|
438
|
+
intervalBetweenFrames = 1.75;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const ms = bitsToMs(baudRate, 38.5);
|
|
442
|
+
const trunc = ms | 0;
|
|
443
|
+
intervalBetweenFrames = trunc + (ms > trunc ? 1 : 0);
|
|
444
|
+
}
|
|
397
445
|
}
|
|
398
446
|
let interCharTimeout = resolveOne(opts.interCharTimeout, baudRate, 0.75);
|
|
399
447
|
if (interCharTimeout === undefined) {
|
|
@@ -403,102 +451,176 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
403
451
|
return { intervalBetweenFrames, interCharTimeout };
|
|
404
452
|
}
|
|
405
453
|
|
|
406
|
-
/**
|
|
407
|
-
*
|
|
454
|
+
/**
|
|
455
|
+
* Hybrid timer manager: uses native `setTimeout` for low concurrency
|
|
456
|
+
* and switches to a binary min-heap when concurrency exceeds the threshold.
|
|
408
457
|
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
458
|
+
* Benchmarks (add + clear throughput, Node 24, x64):
|
|
459
|
+
* 1 concurrent: setTimeout ~1.7× faster than heap
|
|
460
|
+
* 2 concurrent: setTimeout ~1.6× faster than heap
|
|
461
|
+
* 5 concurrent: setTimeout ~1.5-1.9× faster than heap
|
|
462
|
+
* 10 concurrent: roughly equal
|
|
463
|
+
* 20 concurrent: heap ~1.3× faster than setTimeout[]
|
|
464
|
+
* 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
|
|
465
|
+
*
|
|
466
|
+
* The crossover point is around 10 concurrent timers, so the default
|
|
467
|
+
* `concurrentThreshold = 2` keeps the common 1-2 request case on the
|
|
468
|
+
* fast direct path while delegating to the heap for larger batches.
|
|
412
469
|
*/
|
|
413
470
|
class TimerHeap {
|
|
414
471
|
_deadlines = [];
|
|
415
472
|
_ids = [];
|
|
473
|
+
_seqs = [];
|
|
474
|
+
_counter = 0;
|
|
416
475
|
_timer = null;
|
|
417
476
|
_onFire;
|
|
418
|
-
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
419
477
|
_boundTick;
|
|
420
|
-
|
|
478
|
+
_threshold;
|
|
479
|
+
_mode = 'direct';
|
|
480
|
+
_directTimers = new Map();
|
|
481
|
+
/**
|
|
482
|
+
* @param onFire Callback invoked with the timer id when it expires.
|
|
483
|
+
* @param concurrentThreshold Maximum number of timers kept as individual
|
|
484
|
+
* native `setTimeout` handles. Once exceeded, all timers migrate to
|
|
485
|
+
* the internal heap and share a single native timer. Default is 2.
|
|
486
|
+
*/
|
|
487
|
+
constructor(onFire, concurrentThreshold = 2) {
|
|
421
488
|
this._onFire = onFire;
|
|
422
489
|
this._boundTick = this._onTick.bind(this);
|
|
490
|
+
this._threshold = concurrentThreshold;
|
|
423
491
|
}
|
|
424
|
-
/** Number of pending timers in the heap. */
|
|
425
492
|
get size() {
|
|
426
|
-
return this._deadlines.length;
|
|
493
|
+
return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
|
|
427
494
|
}
|
|
428
|
-
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
429
495
|
add(id, ms) {
|
|
496
|
+
if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
|
|
497
|
+
const deadline = performance.now() + ms;
|
|
498
|
+
const handle = setTimeout(() => {
|
|
499
|
+
if (this._mode !== 'direct') {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this._directTimers.delete(id);
|
|
503
|
+
this._onFire(id);
|
|
504
|
+
}, ms);
|
|
505
|
+
this._directTimers.set(id, { handle, deadline });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (this._mode === 'direct') {
|
|
509
|
+
this._mode = 'heap';
|
|
510
|
+
for (const [existingId, { handle, deadline }] of this._directTimers) {
|
|
511
|
+
clearTimeout(handle);
|
|
512
|
+
const diff = deadline - performance.now();
|
|
513
|
+
const trunc = diff | 0;
|
|
514
|
+
const remaining = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
|
|
515
|
+
if (remaining === 0) {
|
|
516
|
+
this._onFire(existingId);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
this._heapAdd(existingId, remaining);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
this._directTimers.clear();
|
|
523
|
+
}
|
|
524
|
+
this._heapAdd(id, ms);
|
|
525
|
+
}
|
|
526
|
+
clear() {
|
|
527
|
+
for (const { handle } of this._directTimers.values()) {
|
|
528
|
+
clearTimeout(handle);
|
|
529
|
+
}
|
|
530
|
+
this._directTimers.clear();
|
|
531
|
+
this._mode = 'direct';
|
|
532
|
+
if (this._timer) {
|
|
533
|
+
clearTimeout(this._timer);
|
|
534
|
+
this._timer = null;
|
|
535
|
+
}
|
|
536
|
+
this._deadlines.length = 0;
|
|
537
|
+
this._ids.length = 0;
|
|
538
|
+
this._seqs.length = 0;
|
|
539
|
+
this._counter = 0;
|
|
540
|
+
}
|
|
541
|
+
_heapAdd(id, ms) {
|
|
430
542
|
const deadline = performance.now() + ms;
|
|
543
|
+
const seq = this._counter++;
|
|
431
544
|
let i = this._deadlines.length;
|
|
432
545
|
this._deadlines.push(deadline);
|
|
433
546
|
this._ids.push(id);
|
|
434
|
-
|
|
547
|
+
this._seqs.push(seq);
|
|
435
548
|
while (i > 0) {
|
|
436
549
|
const p = (i - 1) >> 1;
|
|
437
|
-
|
|
550
|
+
const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
|
|
551
|
+
if (parentComesFirst) {
|
|
438
552
|
break;
|
|
553
|
+
}
|
|
439
554
|
this._deadlines[i] = this._deadlines[p];
|
|
440
555
|
this._ids[i] = this._ids[p];
|
|
556
|
+
this._seqs[i] = this._seqs[p];
|
|
441
557
|
i = p;
|
|
442
558
|
}
|
|
443
559
|
this._deadlines[i] = deadline;
|
|
444
560
|
this._ids[i] = id;
|
|
445
|
-
|
|
446
|
-
if (i === 0)
|
|
561
|
+
this._seqs[i] = seq;
|
|
562
|
+
if (i === 0) {
|
|
447
563
|
this._refresh();
|
|
448
|
-
}
|
|
449
|
-
/** Dispose without firing callbacks. */
|
|
450
|
-
clear() {
|
|
451
|
-
if (this._timer) {
|
|
452
|
-
clearTimeout(this._timer);
|
|
453
|
-
this._timer = null;
|
|
454
564
|
}
|
|
455
|
-
this._deadlines.length = 0;
|
|
456
|
-
this._ids.length = 0;
|
|
457
565
|
}
|
|
458
566
|
_refresh() {
|
|
459
567
|
if (this._timer) {
|
|
460
568
|
clearTimeout(this._timer);
|
|
461
569
|
this._timer = null;
|
|
462
570
|
}
|
|
463
|
-
if (this._deadlines.length === 0)
|
|
571
|
+
if (this._deadlines.length === 0) {
|
|
464
572
|
return;
|
|
465
|
-
|
|
466
|
-
|
|
573
|
+
}
|
|
574
|
+
const diff = this._deadlines[0] - performance.now();
|
|
575
|
+
const trunc = diff | 0;
|
|
576
|
+
const delay = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
|
|
577
|
+
const safeDelay = delay < 2147483647 ? delay : 2147483647;
|
|
578
|
+
this._timer = setTimeout(this._boundTick, safeDelay);
|
|
467
579
|
}
|
|
468
580
|
_onTick() {
|
|
469
581
|
this._timer = null;
|
|
470
582
|
const now = performance.now();
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
583
|
+
try {
|
|
584
|
+
while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
|
|
585
|
+
const id = this._pop();
|
|
586
|
+
this._onFire(id);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
this._refresh();
|
|
474
591
|
}
|
|
475
|
-
this._refresh();
|
|
476
592
|
}
|
|
477
|
-
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
478
593
|
_pop() {
|
|
479
594
|
const topId = this._ids[0];
|
|
480
595
|
const lastId = this._ids.pop();
|
|
481
596
|
const lastDeadline = this._deadlines.pop();
|
|
597
|
+
const lastSeq = this._seqs.pop();
|
|
482
598
|
const n = this._deadlines.length;
|
|
483
599
|
if (n > 0) {
|
|
484
600
|
let i = 0;
|
|
485
|
-
|
|
486
|
-
while (
|
|
487
|
-
let
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
601
|
+
const half = n >> 1;
|
|
602
|
+
while (i < half) {
|
|
603
|
+
let minChild = (i << 1) + 1;
|
|
604
|
+
const rightChild = minChild + 1;
|
|
605
|
+
if (rightChild < n) {
|
|
606
|
+
const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
|
|
607
|
+
(this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
|
|
608
|
+
if (rightComesFirst) {
|
|
609
|
+
minChild = rightChild;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
|
|
613
|
+
if (lastComesFirst) {
|
|
495
614
|
break;
|
|
496
|
-
|
|
497
|
-
this.
|
|
498
|
-
i =
|
|
615
|
+
}
|
|
616
|
+
this._deadlines[i] = this._deadlines[minChild];
|
|
617
|
+
this._ids[i] = this._ids[minChild];
|
|
618
|
+
this._seqs[i] = this._seqs[minChild];
|
|
619
|
+
i = minChild;
|
|
499
620
|
}
|
|
500
621
|
this._deadlines[i] = lastDeadline;
|
|
501
622
|
this._ids[i] = lastId;
|
|
623
|
+
this._seqs[i] = lastSeq;
|
|
502
624
|
}
|
|
503
625
|
return topId;
|
|
504
626
|
}
|
|
@@ -536,6 +658,9 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
536
658
|
get physicalLayer() {
|
|
537
659
|
return this._physicalLayer;
|
|
538
660
|
}
|
|
661
|
+
get serialport() {
|
|
662
|
+
return this._serialport;
|
|
663
|
+
}
|
|
539
664
|
constructor(physicalLayer, serialport) {
|
|
540
665
|
super();
|
|
541
666
|
this._physicalLayer = physicalLayer;
|
|
@@ -544,6 +669,7 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
544
669
|
if (this.state !== PhysicalConnectionState.CONNECTED) {
|
|
545
670
|
return;
|
|
546
671
|
}
|
|
672
|
+
this.emit('rx', chunk);
|
|
547
673
|
this.emit('data', chunk);
|
|
548
674
|
};
|
|
549
675
|
serialport.on('data', onData);
|
|
@@ -572,7 +698,12 @@ class SerialPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
572
698
|
}
|
|
573
699
|
write(data, cb) {
|
|
574
700
|
if (this.state === PhysicalConnectionState.CONNECTED) {
|
|
575
|
-
this._serialport.write(data,
|
|
701
|
+
this._serialport.write(data, (err) => {
|
|
702
|
+
if (!err) {
|
|
703
|
+
this.emit('tx', data);
|
|
704
|
+
}
|
|
705
|
+
cb?.(err);
|
|
706
|
+
});
|
|
576
707
|
}
|
|
577
708
|
else {
|
|
578
709
|
cb?.(new Error('Connection is not connected'));
|
|
@@ -733,6 +864,9 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
733
864
|
get physicalLayer() {
|
|
734
865
|
return this._physicalLayer;
|
|
735
866
|
}
|
|
867
|
+
get socket() {
|
|
868
|
+
return this._socket;
|
|
869
|
+
}
|
|
736
870
|
constructor(physicalLayer, socket) {
|
|
737
871
|
super();
|
|
738
872
|
this._physicalLayer = physicalLayer;
|
|
@@ -741,6 +875,7 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
741
875
|
if (this.state !== PhysicalConnectionState.CONNECTED) {
|
|
742
876
|
return;
|
|
743
877
|
}
|
|
878
|
+
this.emit('rx', chunk);
|
|
744
879
|
this.emit('data', chunk);
|
|
745
880
|
};
|
|
746
881
|
socket.on('data', onData);
|
|
@@ -769,7 +904,12 @@ class TcpPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
769
904
|
}
|
|
770
905
|
write(data, cb) {
|
|
771
906
|
if (this.state === PhysicalConnectionState.CONNECTED) {
|
|
772
|
-
this._socket.write(data,
|
|
907
|
+
this._socket.write(data, (err) => {
|
|
908
|
+
if (!err) {
|
|
909
|
+
this.emit('tx', data);
|
|
910
|
+
}
|
|
911
|
+
cb?.(err);
|
|
912
|
+
});
|
|
773
913
|
}
|
|
774
914
|
else {
|
|
775
915
|
cb?.(new Error('Connection is not connected'));
|
|
@@ -1051,6 +1191,9 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
1051
1191
|
get physicalLayer() {
|
|
1052
1192
|
return this._physicalLayer;
|
|
1053
1193
|
}
|
|
1194
|
+
get socket() {
|
|
1195
|
+
return this._socket;
|
|
1196
|
+
}
|
|
1054
1197
|
constructor(physicalLayer, socket) {
|
|
1055
1198
|
super();
|
|
1056
1199
|
this._physicalLayer = physicalLayer;
|
|
@@ -1059,6 +1202,7 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
1059
1202
|
if (this.state !== PhysicalConnectionState.CONNECTED) {
|
|
1060
1203
|
return;
|
|
1061
1204
|
}
|
|
1205
|
+
this.emit('rx', msg);
|
|
1062
1206
|
this.emit('data', msg);
|
|
1063
1207
|
};
|
|
1064
1208
|
socket.on('message', onMessage);
|
|
@@ -1087,7 +1231,12 @@ class UdpClientPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
1087
1231
|
}
|
|
1088
1232
|
write(data, cb) {
|
|
1089
1233
|
if (this.state === PhysicalConnectionState.CONNECTED) {
|
|
1090
|
-
this._socket.send(data,
|
|
1234
|
+
this._socket.send(data, (err) => {
|
|
1235
|
+
if (!err) {
|
|
1236
|
+
this.emit('tx', data);
|
|
1237
|
+
}
|
|
1238
|
+
cb?.(err);
|
|
1239
|
+
});
|
|
1091
1240
|
}
|
|
1092
1241
|
else {
|
|
1093
1242
|
cb?.(new Error('Connection is not connected'));
|
|
@@ -1227,6 +1376,12 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
1227
1376
|
get physicalLayer() {
|
|
1228
1377
|
return this._physicalLayer;
|
|
1229
1378
|
}
|
|
1379
|
+
get socket() {
|
|
1380
|
+
return this._socket;
|
|
1381
|
+
}
|
|
1382
|
+
get remote() {
|
|
1383
|
+
return this._remote;
|
|
1384
|
+
}
|
|
1230
1385
|
constructor(physicalLayer, socket, remote, idleTimeout, messageEventDelegation) {
|
|
1231
1386
|
super();
|
|
1232
1387
|
this._physicalLayer = physicalLayer;
|
|
@@ -1245,6 +1400,7 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
1245
1400
|
}, idleTimeout);
|
|
1246
1401
|
}
|
|
1247
1402
|
}
|
|
1403
|
+
this.emit('rx', msg);
|
|
1248
1404
|
this.emit('data', msg);
|
|
1249
1405
|
};
|
|
1250
1406
|
messageEventDelegation.add(onMessage);
|
|
@@ -1257,7 +1413,12 @@ class UdpServerPhysicalConnection extends AbstractPhysicalConnection {
|
|
|
1257
1413
|
}
|
|
1258
1414
|
write(data, cb) {
|
|
1259
1415
|
if (this.state === PhysicalConnectionState.CONNECTED) {
|
|
1260
|
-
this._socket.send(data, this._remote.port, this._remote.address,
|
|
1416
|
+
this._socket.send(data, this._remote.port, this._remote.address, (err) => {
|
|
1417
|
+
if (!err) {
|
|
1418
|
+
this.emit('tx', data);
|
|
1419
|
+
}
|
|
1420
|
+
cb?.(err);
|
|
1421
|
+
});
|
|
1261
1422
|
}
|
|
1262
1423
|
else {
|
|
1263
1424
|
cb?.(new Error('Connection is not connected'));
|
|
@@ -1460,16 +1621,23 @@ class AbstractApplicationLayer {
|
|
|
1460
1621
|
removeCustomFunctionCode(fc) { }
|
|
1461
1622
|
}
|
|
1462
1623
|
|
|
1463
|
-
const MAX_FRAME_LENGTH = 256;
|
|
1624
|
+
const MAX_FRAME_LENGTH$1 = 256;
|
|
1464
1625
|
const MIN_FRAME_LENGTH = 4;
|
|
1465
1626
|
class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
1466
1627
|
PROTOCOL = 'RTU';
|
|
1467
1628
|
ROLE;
|
|
1468
1629
|
_connection;
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1630
|
+
// Stores leftover data between parse rounds
|
|
1631
|
+
_residual = Buffer.alloc(MAX_FRAME_LENGTH$1);
|
|
1632
|
+
_residualLen = 0;
|
|
1633
|
+
_expectedLen = PREDICT_NEED_MORE;
|
|
1634
|
+
_t15Time;
|
|
1635
|
+
_t35Time;
|
|
1636
|
+
_t15Strict;
|
|
1637
|
+
_t15Timer;
|
|
1638
|
+
_t35Timer;
|
|
1639
|
+
// t1.5 cursor: 0 = not triggered; > 0 = virtual index where the gap occurred
|
|
1640
|
+
_t15Marker = 0;
|
|
1473
1641
|
_customFunctionCodes = new Map();
|
|
1474
1642
|
_cleanupCbs = [];
|
|
1475
1643
|
get connection() {
|
|
@@ -1479,89 +1647,246 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1479
1647
|
super();
|
|
1480
1648
|
this.ROLE = role;
|
|
1481
1649
|
this._connection = connection;
|
|
1482
|
-
const { intervalBetweenFrames, interCharTimeout,
|
|
1483
|
-
this.
|
|
1484
|
-
this.
|
|
1485
|
-
this.
|
|
1486
|
-
|
|
1650
|
+
const { intervalBetweenFrames, interCharTimeout, strictTiming } = options;
|
|
1651
|
+
this._t35Time = intervalBetweenFrames ?? 0;
|
|
1652
|
+
this._t15Time = this._t35Time === 0 ? 0 : (interCharTimeout ?? 0);
|
|
1653
|
+
if (this._t35Time < this._t15Time) {
|
|
1654
|
+
throw new Error('t3.5 cannot be less than t1.5');
|
|
1655
|
+
}
|
|
1656
|
+
this._t15Strict = strictTiming ?? false;
|
|
1657
|
+
const isResponse = role === 'MASTER';
|
|
1658
|
+
const timingEnabled = this._t35Time > 0;
|
|
1487
1659
|
const onData = (data) => {
|
|
1488
|
-
const state = this._state;
|
|
1489
|
-
if (state.t15Expired && state.end > state.start) {
|
|
1490
|
-
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1491
|
-
state.start = 0;
|
|
1492
|
-
state.end = 0;
|
|
1493
|
-
}
|
|
1494
|
-
state.t15Expired = false;
|
|
1495
1660
|
const dataLen = data.length;
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1661
|
+
const residualLen = this._residualLen;
|
|
1662
|
+
const totalAvailable = residualLen + dataLen;
|
|
1663
|
+
// ========================================================================
|
|
1664
|
+
// 1. Timing reset: any bus activity unconditionally kills all silence timers
|
|
1665
|
+
// ========================================================================
|
|
1666
|
+
if (timingEnabled) {
|
|
1667
|
+
if (this._t15Timer !== undefined) {
|
|
1668
|
+
clearTimeout(this._t15Timer);
|
|
1669
|
+
this._t15Timer = undefined;
|
|
1670
|
+
}
|
|
1671
|
+
if (this._t35Timer !== undefined) {
|
|
1672
|
+
clearTimeout(this._t35Timer);
|
|
1673
|
+
this._t35Timer = undefined;
|
|
1674
|
+
}
|
|
1502
1675
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
// flushBuffer freed nothing — the entire pool is unparseable
|
|
1530
|
-
// residue (typically a misconfigured poolSize for the wire's
|
|
1531
|
-
// frame size). Hard reset; we cannot recover automatically.
|
|
1532
|
-
this.onFramingError(new Error('Frame buffer exhausted before complete frame received'));
|
|
1533
|
-
currentState.start = 0;
|
|
1534
|
-
currentState.end = 0;
|
|
1535
|
-
currentState.t15Expired = false;
|
|
1676
|
+
// ========================================================================
|
|
1677
|
+
// 2. Fast path: no residual data and the new chunk is exactly one frame
|
|
1678
|
+
// ========================================================================
|
|
1679
|
+
if (residualLen === 0 && dataLen >= MIN_FRAME_LENGTH) {
|
|
1680
|
+
const fc = data[1];
|
|
1681
|
+
let frameLen = PREDICT_NEED_MORE;
|
|
1682
|
+
const cfc = this._customFunctionCodes.size > 0 ? this._customFunctionCodes.get(fc) : undefined;
|
|
1683
|
+
if (cfc) {
|
|
1684
|
+
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1685
|
+
frameLen = predictor((idx) => data[idx], dataLen);
|
|
1686
|
+
}
|
|
1687
|
+
else if (isResponse) {
|
|
1688
|
+
if ((fc & 0x80) !== 0) {
|
|
1689
|
+
frameLen = 5;
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
const val = RES_TABLE[fc];
|
|
1693
|
+
if (val > 0) {
|
|
1694
|
+
frameLen = val;
|
|
1695
|
+
}
|
|
1696
|
+
else if (val < 0 && val !== -999) {
|
|
1697
|
+
const decode = -val;
|
|
1698
|
+
const offset = decode >> 8;
|
|
1699
|
+
if (dataLen > offset) {
|
|
1700
|
+
frameLen = (decode & 0xff) + data[offset];
|
|
1701
|
+
}
|
|
1536
1702
|
}
|
|
1537
|
-
continue;
|
|
1538
1703
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1704
|
+
}
|
|
1705
|
+
else {
|
|
1706
|
+
const val = REQ_TABLE[fc];
|
|
1707
|
+
if (val > 0) {
|
|
1708
|
+
frameLen = val;
|
|
1709
|
+
}
|
|
1710
|
+
else if (val < 0) {
|
|
1711
|
+
const decode = -val;
|
|
1712
|
+
const offset = decode >> 8;
|
|
1713
|
+
if (dataLen > offset) {
|
|
1714
|
+
frameLen = (decode & 0xff) + data[offset];
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (frameLen === dataLen) {
|
|
1719
|
+
const expectedCrc = data[frameLen - 2] | (data[frameLen - 1] << 8);
|
|
1720
|
+
// Inline CRC for the hot single-buffer path — local table reference helps V8 IC.
|
|
1721
|
+
const table = CRC_TABLE;
|
|
1722
|
+
let crc = 0xffff;
|
|
1723
|
+
const crcEnd = frameLen - 2;
|
|
1724
|
+
for (let i = 0; i < crcEnd; i++) {
|
|
1725
|
+
crc = table[(crc ^ data[i]) & 0xff] ^ (crc >> 8);
|
|
1726
|
+
}
|
|
1727
|
+
if (expectedCrc === crc) {
|
|
1728
|
+
const dropFrame = timingEnabled && this._t15Strict && this._t15Marker > 0;
|
|
1729
|
+
if (!dropFrame) {
|
|
1730
|
+
const frame = {
|
|
1731
|
+
unit: data[0],
|
|
1732
|
+
fc: data[1],
|
|
1733
|
+
data: data.subarray(2, frameLen - 2),
|
|
1734
|
+
buffer: data,
|
|
1735
|
+
};
|
|
1736
|
+
this.onFraming(frame);
|
|
1737
|
+
}
|
|
1738
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
1739
|
+
this._t15Marker = 0;
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1544
1742
|
}
|
|
1545
1743
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1744
|
+
// ========================================================================
|
|
1745
|
+
// 3. Extract frames in a loop until no complete frame remains (skip if data length below prediction)
|
|
1746
|
+
// ========================================================================
|
|
1747
|
+
let index = 0;
|
|
1748
|
+
if (!(this._expectedLen > 0 && totalAvailable < this._expectedLen)) {
|
|
1749
|
+
while (index <= totalAvailable - MIN_FRAME_LENGTH) {
|
|
1750
|
+
const fc = index + 1 < residualLen ? this._residual[index + 1] : data[index + 1 - residualLen];
|
|
1751
|
+
const cfc = this._customFunctionCodes.get(fc);
|
|
1752
|
+
let frameLen = PREDICT_NEED_MORE;
|
|
1753
|
+
if (cfc) {
|
|
1754
|
+
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1755
|
+
frameLen = predictor((idx) => {
|
|
1756
|
+
const pos = index + idx;
|
|
1757
|
+
return pos < residualLen ? this._residual[pos] : data[pos - residualLen];
|
|
1758
|
+
}, totalAvailable - index);
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
frameLen = predictRtuFrameLength(this._residual, data, residualLen, index, totalAvailable, isResponse);
|
|
1762
|
+
}
|
|
1763
|
+
if (frameLen === PREDICT_UNKNOWN) {
|
|
1764
|
+
index++;
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (frameLen === PREDICT_NEED_MORE) {
|
|
1768
|
+
break;
|
|
1769
|
+
}
|
|
1770
|
+
if (frameLen > MAX_FRAME_LENGTH$1 || frameLen < MIN_FRAME_LENGTH) {
|
|
1771
|
+
index++;
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (totalAvailable - index < frameLen) {
|
|
1775
|
+
this._expectedLen = index + frameLen;
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
const expectedCrc = (index + frameLen - 2 < residualLen ? this._residual[index + frameLen - 2] : data[index + frameLen - 2 - residualLen]) |
|
|
1779
|
+
((index + frameLen - 1 < residualLen ? this._residual[index + frameLen - 1] : data[index + frameLen - 1 - residualLen]) << 8);
|
|
1780
|
+
const crcEnd = index + frameLen - 2;
|
|
1781
|
+
let actualCrc;
|
|
1782
|
+
if (crcEnd <= residualLen) {
|
|
1783
|
+
// Entire CRC range sits in the old residual buffer
|
|
1784
|
+
actualCrc = crcFixed(this._residual, index, crcEnd);
|
|
1785
|
+
}
|
|
1786
|
+
else if (index >= residualLen) {
|
|
1787
|
+
// Entire CRC range sits in the new data chunk
|
|
1788
|
+
actualCrc = crcFixed(data, index - residualLen, crcEnd - residualLen);
|
|
1789
|
+
}
|
|
1790
|
+
else {
|
|
1791
|
+
// CRC range spans both buffers
|
|
1792
|
+
actualCrc = crcDual(this._residual, index, residualLen - index, data, 0, crcEnd - residualLen);
|
|
1793
|
+
}
|
|
1794
|
+
if (expectedCrc !== actualCrc) {
|
|
1795
|
+
index++;
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
// A complete frame has been received; CRC verification still required
|
|
1799
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
1800
|
+
const dropFrame = this._t15Strict && this._t15Marker > index && this._t15Marker < index + frameLen;
|
|
1801
|
+
if (!dropFrame) {
|
|
1802
|
+
// Build contiguous raw buffer — one alloc, zero-copy subarray for data.
|
|
1803
|
+
const raw = Buffer.allocUnsafe(frameLen);
|
|
1804
|
+
if (index + frameLen <= residualLen) {
|
|
1805
|
+
this._residual.copy(raw, 0, index, index + frameLen);
|
|
1806
|
+
}
|
|
1807
|
+
else if (index >= residualLen) {
|
|
1808
|
+
data.copy(raw, 0, index - residualLen, index - residualLen + frameLen);
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
const headLen = residualLen - index;
|
|
1812
|
+
this._residual.copy(raw, 0, index, residualLen);
|
|
1813
|
+
data.copy(raw, headLen, 0, frameLen - headLen);
|
|
1814
|
+
}
|
|
1815
|
+
const frame = {
|
|
1816
|
+
unit: raw[0],
|
|
1817
|
+
fc: raw[1],
|
|
1818
|
+
data: raw.subarray(2, frameLen - 2),
|
|
1819
|
+
buffer: raw,
|
|
1820
|
+
};
|
|
1821
|
+
this.onFraming(frame);
|
|
1822
|
+
}
|
|
1823
|
+
index += frameLen;
|
|
1557
1824
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1825
|
+
}
|
|
1826
|
+
// ========================================================================
|
|
1827
|
+
// 4. Compact residual buffer and rebuild silence timers
|
|
1828
|
+
// ========================================================================
|
|
1829
|
+
const newFrameStart = index;
|
|
1830
|
+
const finalRestLen = totalAvailable - newFrameStart;
|
|
1831
|
+
if (finalRestLen === 0) {
|
|
1832
|
+
this._residualLen = 0;
|
|
1833
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
1834
|
+
this._t15Marker = 0;
|
|
1562
1835
|
}
|
|
1563
1836
|
else {
|
|
1564
|
-
|
|
1837
|
+
const keepLen = finalRestLen < MAX_FRAME_LENGTH$1 ? finalRestLen : MAX_FRAME_LENGTH$1;
|
|
1838
|
+
const discardLen = totalAvailable - keepLen;
|
|
1839
|
+
if (discardLen >= residualLen) {
|
|
1840
|
+
// Kept portion lies entirely within the new `data`
|
|
1841
|
+
data.copy(this._residual, 0, discardLen - residualLen, dataLen);
|
|
1842
|
+
}
|
|
1843
|
+
else if (discardLen > 0) {
|
|
1844
|
+
// Kept portion spans both buffers, or physical left-shift truncation occurred
|
|
1845
|
+
for (let i = 0; i < keepLen; i++) {
|
|
1846
|
+
this._residual[i] = discardLen + i < residualLen ? this._residual[discardLen + i] : data[discardLen + i - residualLen];
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
else {
|
|
1850
|
+
// discardLen === 0 (old data not consumed, truncation limit not hit) — simple append
|
|
1851
|
+
data.copy(this._residual, residualLen, 0, dataLen);
|
|
1852
|
+
}
|
|
1853
|
+
this._residualLen = keepLen;
|
|
1854
|
+
// Unify physical coordinate system translation
|
|
1855
|
+
if (discardLen > 0) {
|
|
1856
|
+
if (this._expectedLen > 0) {
|
|
1857
|
+
const newExpectedLen = this._expectedLen - discardLen;
|
|
1858
|
+
this._expectedLen = newExpectedLen > PREDICT_NEED_MORE ? newExpectedLen : PREDICT_NEED_MORE;
|
|
1859
|
+
}
|
|
1860
|
+
if (this._t15Marker > 0) {
|
|
1861
|
+
const newT15Marker = this._t15Marker - discardLen;
|
|
1862
|
+
this._t15Marker = newT15Marker > 0 ? newT15Marker : 0;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (timingEnabled) {
|
|
1866
|
+
let hasErrorEmitted = false;
|
|
1867
|
+
// Establish t3.5 absolute deadline
|
|
1868
|
+
this._t35Timer = setTimeout(() => {
|
|
1869
|
+
this._t35Timer = undefined;
|
|
1870
|
+
// No complete frame parsed within t3.5: circuit-break, discard all data
|
|
1871
|
+
this._residualLen = 0;
|
|
1872
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
1873
|
+
this._t15Marker = 0;
|
|
1874
|
+
if (!hasErrorEmitted) {
|
|
1875
|
+
this.onFramingError(new Error('Incomplete frame at t3.5'));
|
|
1876
|
+
}
|
|
1877
|
+
}, this._t35Time);
|
|
1878
|
+
if (this._t15Time > 0) {
|
|
1879
|
+
// Establish t1.5 inter-character gap monitor
|
|
1880
|
+
this._t15Timer = setTimeout(() => {
|
|
1881
|
+
this._t15Timer = undefined;
|
|
1882
|
+
this._t15Marker = this._residualLen; // Record the residual boundary where the gap occurred
|
|
1883
|
+
if (this._t15Strict) {
|
|
1884
|
+
hasErrorEmitted = true;
|
|
1885
|
+
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1886
|
+
}
|
|
1887
|
+
}, this._t15Time);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1565
1890
|
}
|
|
1566
1891
|
};
|
|
1567
1892
|
connection.on('data', onData);
|
|
@@ -1571,147 +1896,33 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1571
1896
|
fn();
|
|
1572
1897
|
}
|
|
1573
1898
|
this._cleanupCbs.length = 0;
|
|
1574
|
-
this.
|
|
1899
|
+
if (this._t15Timer !== undefined) {
|
|
1900
|
+
clearTimeout(this._t15Timer);
|
|
1901
|
+
this._t15Timer = undefined;
|
|
1902
|
+
}
|
|
1903
|
+
if (this._t35Timer !== undefined) {
|
|
1904
|
+
clearTimeout(this._t35Timer);
|
|
1905
|
+
this._t35Timer = undefined;
|
|
1906
|
+
}
|
|
1575
1907
|
};
|
|
1576
1908
|
connection.on('close', onClose);
|
|
1577
1909
|
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1578
1910
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
clearTimeout(state.interCharTimer);
|
|
1587
|
-
state.interCharTimer = undefined;
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
/**
|
|
1591
|
-
* Shared handler for every "frame is not yet complete" exit in `flushBuffer`.
|
|
1592
|
-
* Returns `true` when the caller should `return` (strict reset), `false` to
|
|
1593
|
-
* `break` the parse loop. Hot path never reaches here — only error/incomplete
|
|
1594
|
-
* edges. Extracted as a method so it is not recreated on every `flushBuffer`
|
|
1595
|
-
* call.
|
|
1596
|
-
*/
|
|
1597
|
-
_handleIncomplete(state, strict) {
|
|
1598
|
-
if (strict) {
|
|
1599
|
-
this.onFramingError(new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
|
|
1600
|
-
state.start = 0;
|
|
1601
|
-
state.end = 0;
|
|
1602
|
-
state.t15Expired = false;
|
|
1603
|
-
return true;
|
|
1604
|
-
}
|
|
1605
|
-
if (state.t15Expired) {
|
|
1606
|
-
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1607
|
-
state.start = 0;
|
|
1608
|
-
state.end = 0;
|
|
1609
|
-
state.t15Expired = false;
|
|
1610
|
-
}
|
|
1611
|
-
return false;
|
|
1612
|
-
}
|
|
1613
|
-
flushBuffer(strict) {
|
|
1614
|
-
const state = this._state;
|
|
1615
|
-
const isResponse = this.ROLE === 'MASTER';
|
|
1616
|
-
const pool = state.pool;
|
|
1617
|
-
const customFCs = this._customFunctionCodes;
|
|
1618
|
-
while (state.end - state.start > 0) {
|
|
1619
|
-
const available = state.end - state.start;
|
|
1620
|
-
if (available < MIN_FRAME_LENGTH) {
|
|
1621
|
-
if (this._handleIncomplete(state, strict))
|
|
1622
|
-
return;
|
|
1623
|
-
break;
|
|
1624
|
-
}
|
|
1625
|
-
const fc = pool[state.start + 1];
|
|
1626
|
-
const cfc = customFCs.get(fc);
|
|
1627
|
-
let expected;
|
|
1628
|
-
if (cfc) {
|
|
1629
|
-
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1630
|
-
const predicted = predictor(pool, state.start, state.end);
|
|
1631
|
-
// Normalize custom predictor's `null` to the std sentinel so both
|
|
1632
|
-
// paths share the same NEED_MORE tail below.
|
|
1633
|
-
expected = predicted ?? PREDICT_NEED_MORE;
|
|
1634
|
-
}
|
|
1635
|
-
else {
|
|
1636
|
-
// Standard FC path. predictRtuFrameLength uses sentinel returns to
|
|
1637
|
-
// avoid per-call object allocation on the decode hot path.
|
|
1638
|
-
expected = predictRtuFrameLength(pool, state.start, state.end, isResponse);
|
|
1639
|
-
if (expected === PREDICT_UNKNOWN) {
|
|
1640
|
-
this.onFramingError(new Error(`Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`));
|
|
1641
|
-
state.start = 0;
|
|
1642
|
-
state.end = 0;
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
if (expected === PREDICT_NEED_MORE) {
|
|
1647
|
-
if (available >= MAX_FRAME_LENGTH) {
|
|
1648
|
-
state.start += 1;
|
|
1649
|
-
continue;
|
|
1650
|
-
}
|
|
1651
|
-
if (this._handleIncomplete(state, strict))
|
|
1652
|
-
return;
|
|
1653
|
-
break;
|
|
1654
|
-
}
|
|
1655
|
-
if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
|
|
1656
|
-
this.onFramingError(new Error('Invalid data'));
|
|
1657
|
-
state.start = 0;
|
|
1658
|
-
state.end = 0;
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
if (available < expected) {
|
|
1662
|
-
if (available >= MAX_FRAME_LENGTH) {
|
|
1663
|
-
state.start += 1;
|
|
1664
|
-
continue;
|
|
1665
|
-
}
|
|
1666
|
-
if (this._handleIncomplete(state, strict))
|
|
1667
|
-
return;
|
|
1668
|
-
break;
|
|
1669
|
-
}
|
|
1670
|
-
// CRC check inline: no helper call, no subarray for the CRC body.
|
|
1671
|
-
const crcStart = state.start;
|
|
1672
|
-
const crcEnd = crcStart + expected - 2;
|
|
1673
|
-
const expectedCrc = pool.readUInt16LE(crcEnd);
|
|
1674
|
-
const actualCrc = crc(pool, crcStart, crcEnd);
|
|
1675
|
-
if (expectedCrc !== actualCrc) {
|
|
1676
|
-
if (strict) {
|
|
1677
|
-
this.onFramingError(new Error('CRC mismatch'));
|
|
1678
|
-
state.start = 0;
|
|
1679
|
-
state.end = 0;
|
|
1680
|
-
state.t15Expired = false;
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
state.start += 1;
|
|
1684
|
-
continue;
|
|
1685
|
-
}
|
|
1686
|
-
// Frame located. Copy it out of the pool so the emitted buffer remains
|
|
1687
|
-
// valid even if the consumer queues the frame across `onData` ticks.
|
|
1688
|
-
// `Buffer.copyBytesFrom` is a native fast-path (Node 18.19+) — measurably
|
|
1689
|
-
// faster than `Buffer.from(buffer)` for this size.
|
|
1690
|
-
const frameBuf = Buffer.copyBytesFrom(pool, crcStart, expected);
|
|
1691
|
-
state.start += expected;
|
|
1692
|
-
const frame = {
|
|
1693
|
-
unit: frameBuf[0],
|
|
1694
|
-
fc: frameBuf[1],
|
|
1695
|
-
data: frameBuf.subarray(2, expected - 2),
|
|
1696
|
-
buffer: frameBuf,
|
|
1697
|
-
};
|
|
1698
|
-
this.onFraming(frame);
|
|
1911
|
+
flush() {
|
|
1912
|
+
this._residualLen = 0;
|
|
1913
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
1914
|
+
this._t15Marker = 0;
|
|
1915
|
+
if (this._t15Timer !== undefined) {
|
|
1916
|
+
clearTimeout(this._t15Timer);
|
|
1917
|
+
this._t15Timer = undefined;
|
|
1699
1918
|
}
|
|
1700
|
-
if (
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
}
|
|
1704
|
-
state.end -= state.start;
|
|
1705
|
-
state.start = 0;
|
|
1919
|
+
if (this._t35Timer !== undefined) {
|
|
1920
|
+
clearTimeout(this._t35Timer);
|
|
1921
|
+
this._t35Timer = undefined;
|
|
1706
1922
|
}
|
|
1707
1923
|
}
|
|
1708
|
-
flush() {
|
|
1709
|
-
this.clearStateTimers();
|
|
1710
|
-
this._state.start = 0;
|
|
1711
|
-
this._state.end = 0;
|
|
1712
|
-
}
|
|
1713
1924
|
addCustomFunctionCode(cfc) {
|
|
1714
|
-
if (
|
|
1925
|
+
if ((cfc.fc & 0xff) !== cfc.fc) {
|
|
1715
1926
|
throw new Error(`fc must be an integer in 0..255, got ${cfc.fc}`);
|
|
1716
1927
|
}
|
|
1717
1928
|
this._customFunctionCodes.set(cfc.fc, cfc);
|
|
@@ -1726,14 +1937,15 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1726
1937
|
buffer[0] = unit;
|
|
1727
1938
|
buffer[1] = fc;
|
|
1728
1939
|
if (data.length <= 16) {
|
|
1729
|
-
for (let i = 0; i < data.length; i++)
|
|
1940
|
+
for (let i = 0; i < data.length; i++) {
|
|
1730
1941
|
buffer[2 + i] = data[i];
|
|
1942
|
+
}
|
|
1731
1943
|
}
|
|
1732
1944
|
else {
|
|
1733
1945
|
buffer.set(data, 2);
|
|
1734
1946
|
}
|
|
1735
1947
|
const crcEnd = buffer.length - 2;
|
|
1736
|
-
const c =
|
|
1948
|
+
const c = crcFixed(buffer, 0, crcEnd);
|
|
1737
1949
|
// Little-endian inline write of CRC trailer.
|
|
1738
1950
|
buffer[crcEnd] = c & 0xff;
|
|
1739
1951
|
buffer[crcEnd + 1] = (c >>> 8) & 0xff;
|
|
@@ -1746,9 +1958,8 @@ const CHAR_CODE = {
|
|
|
1746
1958
|
CR: '\r'.charCodeAt(0),
|
|
1747
1959
|
LF: '\n'.charCodeAt(0),
|
|
1748
1960
|
};
|
|
1749
|
-
// Modbus ASCII frame
|
|
1750
|
-
//
|
|
1751
|
-
// cannot grow `state.frame` without bound.
|
|
1961
|
+
// Modbus ASCII frame body is capped well below the theoretical maximum so a
|
|
1962
|
+
// peer that never sends `\r` cannot grow `state.frame` without bound.
|
|
1752
1963
|
const MAX_ASCII_PAYLOAD = 512;
|
|
1753
1964
|
const HEX_DECODE = new Uint8Array(256);
|
|
1754
1965
|
HEX_DECODE.fill(0xff);
|
|
@@ -1761,11 +1972,16 @@ for (let i = 0x41; i <= 0x46; i++) {
|
|
|
1761
1972
|
for (let i = 0x61; i <= 0x66; i++) {
|
|
1762
1973
|
HEX_DECODE[i] = i - 0x61 + 10;
|
|
1763
1974
|
}
|
|
1975
|
+
// Strict variant: lowercase hex digits (a-f) are treated as invalid so the
|
|
1976
|
+
// hot-path validation loop needs only one table lookup instead of two checks.
|
|
1977
|
+
const HEX_DECODE_STRICT = new Uint8Array(HEX_DECODE);
|
|
1978
|
+
for (let i = 0x61; i <= 0x66; i++) {
|
|
1979
|
+
HEX_DECODE_STRICT[i] = 0xff;
|
|
1980
|
+
}
|
|
1764
1981
|
const HEX_ENCODE = new Uint8Array('0123456789ABCDEF'.split('').map((c) => c.charCodeAt(0)));
|
|
1765
1982
|
class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
1766
1983
|
PROTOCOL = 'ASCII';
|
|
1767
1984
|
ROLE;
|
|
1768
|
-
lenientHex;
|
|
1769
1985
|
_connection;
|
|
1770
1986
|
_state = { status: 'idle', frame: new Uint8Array(MAX_ASCII_PAYLOAD), frameLen: 0 };
|
|
1771
1987
|
_cleanupCbs = [];
|
|
@@ -1776,23 +1992,30 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1776
1992
|
super();
|
|
1777
1993
|
this.ROLE = role;
|
|
1778
1994
|
this._connection = connection;
|
|
1779
|
-
|
|
1780
|
-
const
|
|
1781
|
-
const isHexChar = (value) => {
|
|
1782
|
-
if (value >= 0x30 && value <= 0x39) {
|
|
1783
|
-
return true;
|
|
1784
|
-
}
|
|
1785
|
-
if (value >= 0x41 && value <= 0x46) {
|
|
1786
|
-
return true;
|
|
1787
|
-
}
|
|
1788
|
-
if (lenientHex && value >= 0x61 && value <= 0x66) {
|
|
1789
|
-
return true;
|
|
1790
|
-
}
|
|
1791
|
-
return false;
|
|
1792
|
-
};
|
|
1995
|
+
const lenientHex = options.lenientHex ?? false;
|
|
1996
|
+
const hexTable = lenientHex ? HEX_DECODE : HEX_DECODE_STRICT;
|
|
1793
1997
|
const onData = (data) => {
|
|
1794
1998
|
const state = this._state;
|
|
1795
|
-
|
|
1999
|
+
const dataLen = data.length;
|
|
2000
|
+
// Fast path: idle state and the chunk is exactly one complete frame
|
|
2001
|
+
// ASCII frame: length >= 9 and always odd (9 + 2n)
|
|
2002
|
+
if (state.status === 'idle' && dataLen >= 9 && dataLen % 2 !== 0) {
|
|
2003
|
+
if (data[0] === CHAR_CODE.COLON && data[dataLen - 2] === CHAR_CODE.CR && data[dataLen - 1] === CHAR_CODE.LF) {
|
|
2004
|
+
let valid = true;
|
|
2005
|
+
for (let i = 1; i < dataLen - 2; i++) {
|
|
2006
|
+
if (hexTable[data[i]] > 15) {
|
|
2007
|
+
valid = false;
|
|
2008
|
+
break;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (valid) {
|
|
2012
|
+
const hexView = data.subarray(1, dataLen - 2);
|
|
2013
|
+
this.framing(hexView, hexView.length, hexTable);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
for (let i = 0; i < dataLen; i++) {
|
|
1796
2019
|
const value = data[i];
|
|
1797
2020
|
switch (state.status) {
|
|
1798
2021
|
case 'idle': {
|
|
@@ -1814,7 +2037,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1814
2037
|
state.frameLen = 0;
|
|
1815
2038
|
this.onFramingError(new Error('Invalid data'));
|
|
1816
2039
|
}
|
|
1817
|
-
else if (
|
|
2040
|
+
else if (hexTable[value] > 15) {
|
|
1818
2041
|
state.status = 'idle';
|
|
1819
2042
|
state.frameLen = 0;
|
|
1820
2043
|
this.onFramingError(new Error('Invalid hex character'));
|
|
@@ -1832,7 +2055,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1832
2055
|
else {
|
|
1833
2056
|
state.status = 'idle';
|
|
1834
2057
|
if (value === CHAR_CODE.LF) {
|
|
1835
|
-
this.framing(state.frame, state.frameLen);
|
|
2058
|
+
this.framing(state.frame, state.frameLen, hexTable);
|
|
1836
2059
|
}
|
|
1837
2060
|
}
|
|
1838
2061
|
break;
|
|
@@ -1851,7 +2074,7 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1851
2074
|
connection.on('close', onClose);
|
|
1852
2075
|
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1853
2076
|
}
|
|
1854
|
-
framing(hexChars, hexLen) {
|
|
2077
|
+
framing(hexChars, hexLen, hexTable) {
|
|
1855
2078
|
if (hexLen < 6) {
|
|
1856
2079
|
this.onFramingError(new Error('Insufficient data length'));
|
|
1857
2080
|
return;
|
|
@@ -1863,10 +2086,10 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1863
2086
|
const byteLen = hexLen >> 1;
|
|
1864
2087
|
// Decode unit and fc directly from the first 4 hex characters —
|
|
1865
2088
|
// avoids allocating a full decoded buffer just to read two bytes.
|
|
1866
|
-
const unitHi =
|
|
1867
|
-
const unitLo =
|
|
1868
|
-
const fcHi =
|
|
1869
|
-
const fcLo =
|
|
2089
|
+
const unitHi = hexTable[hexChars[0]];
|
|
2090
|
+
const unitLo = hexTable[hexChars[1]];
|
|
2091
|
+
const fcHi = hexTable[hexChars[2]];
|
|
2092
|
+
const fcLo = hexTable[hexChars[3]];
|
|
1870
2093
|
if (unitHi === 0xff || unitLo === 0xff || fcHi === 0xff || fcLo === 0xff) {
|
|
1871
2094
|
this.onFramingError(new Error('Invalid hex character'));
|
|
1872
2095
|
return;
|
|
@@ -1874,30 +2097,31 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1874
2097
|
const unit = (unitHi << 4) | unitLo;
|
|
1875
2098
|
const fc = (fcHi << 4) | fcLo;
|
|
1876
2099
|
// Decode LRC from the last 2 hex characters.
|
|
1877
|
-
const lrcHi =
|
|
1878
|
-
const lrcLo =
|
|
2100
|
+
const lrcHi = hexTable[hexChars[hexLen - 2]];
|
|
2101
|
+
const lrcLo = hexTable[hexChars[hexLen - 1]];
|
|
1879
2102
|
if (lrcHi === 0xff || lrcLo === 0xff) {
|
|
1880
2103
|
this.onFramingError(new Error('Invalid hex character'));
|
|
1881
2104
|
return;
|
|
1882
2105
|
}
|
|
1883
2106
|
const lrcIn = (lrcHi << 4) | lrcLo;
|
|
1884
|
-
// Decode data portion (between unit/fc and lrc) into a right-sized buffer
|
|
2107
|
+
// Decode data portion (between unit/fc and lrc) into a right-sized buffer,
|
|
2108
|
+
// while simultaneously accumulating the LRC sum — one pass instead of two.
|
|
1885
2109
|
// dataLen may be 0 for a frame that is only unit + fc + lrc.
|
|
1886
2110
|
const dataLen = byteLen - 3;
|
|
1887
2111
|
const data = Buffer.allocUnsafe(dataLen);
|
|
2112
|
+
let hexOff = 4;
|
|
2113
|
+
let sum = unit + fc;
|
|
1888
2114
|
for (let i = 0; i < dataLen; i++) {
|
|
1889
|
-
const hi =
|
|
1890
|
-
const lo =
|
|
2115
|
+
const hi = hexTable[hexChars[hexOff]];
|
|
2116
|
+
const lo = hexTable[hexChars[hexOff + 1]];
|
|
1891
2117
|
if (hi === 0xff || lo === 0xff) {
|
|
1892
2118
|
this.onFramingError(new Error('Invalid hex character'));
|
|
1893
2119
|
return;
|
|
1894
2120
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
for (let i = 0; i < dataLen; i++) {
|
|
1900
|
-
sum += data[i];
|
|
2121
|
+
const byte = (hi << 4) | lo;
|
|
2122
|
+
data[i] = byte;
|
|
2123
|
+
sum += byte;
|
|
2124
|
+
hexOff += 2;
|
|
1901
2125
|
}
|
|
1902
2126
|
const lrcComputed = (~sum + 1) & 0xff;
|
|
1903
2127
|
if (lrcIn !== lrcComputed) {
|
|
@@ -1923,13 +2147,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1923
2147
|
buffer[0] = unit;
|
|
1924
2148
|
buffer[1] = fc;
|
|
1925
2149
|
buffer.set(data, 2);
|
|
1926
|
-
buffer[buffer.length - 1] = lrc(buffer
|
|
2150
|
+
buffer[buffer.length - 1] = lrc(buffer, 0, buffer.length - 1);
|
|
1927
2151
|
const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
|
|
1928
2152
|
out[0] = CHAR_CODE.COLON;
|
|
2153
|
+
let outOff = 1;
|
|
1929
2154
|
for (let i = 0; i < buffer.length; i++) {
|
|
1930
2155
|
const byte = buffer[i];
|
|
1931
|
-
out[
|
|
1932
|
-
out[
|
|
2156
|
+
out[outOff] = HEX_ENCODE[byte >> 4];
|
|
2157
|
+
out[outOff + 1] = HEX_ENCODE[byte & 0x0f];
|
|
2158
|
+
outOff += 2;
|
|
1933
2159
|
}
|
|
1934
2160
|
out[out.length - 2] = CHAR_CODE.CR;
|
|
1935
2161
|
out[out.length - 1] = CHAR_CODE.LF;
|
|
@@ -1937,13 +2163,15 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1937
2163
|
}
|
|
1938
2164
|
}
|
|
1939
2165
|
|
|
1940
|
-
const
|
|
2166
|
+
const MAX_FRAME_LENGTH = 260;
|
|
1941
2167
|
class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
1942
2168
|
PROTOCOL = 'TCP';
|
|
1943
2169
|
ROLE;
|
|
1944
2170
|
_connection;
|
|
1945
2171
|
_transactionId = 1;
|
|
1946
|
-
|
|
2172
|
+
_residual = Buffer.alloc(MAX_FRAME_LENGTH);
|
|
2173
|
+
_residualLen = 0;
|
|
2174
|
+
_expectedLen = PREDICT_NEED_MORE;
|
|
1947
2175
|
_cleanupCbs = [];
|
|
1948
2176
|
get connection() {
|
|
1949
2177
|
return this._connection;
|
|
@@ -1953,51 +2181,108 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1953
2181
|
this.ROLE = role;
|
|
1954
2182
|
this._connection = connection;
|
|
1955
2183
|
const onData = (data) => {
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2184
|
+
const dataLen = data.length;
|
|
2185
|
+
const residualLen = this._residualLen;
|
|
2186
|
+
const totalAvailable = residualLen + dataLen;
|
|
2187
|
+
// Fast path: no residual data and the new chunk is exactly one frame
|
|
2188
|
+
if (residualLen === 0 && dataLen >= 8) {
|
|
2189
|
+
if (data[2] === 0 && data[3] === 0) {
|
|
2190
|
+
const length = (data[4] << 8) | data[5];
|
|
2191
|
+
const frameLen = 6 + length;
|
|
2192
|
+
if (frameLen === dataLen && frameLen <= MAX_FRAME_LENGTH && length >= 2) {
|
|
2193
|
+
const frame = {
|
|
2194
|
+
transaction: (data[0] << 8) | data[1],
|
|
2195
|
+
unit: data[6],
|
|
2196
|
+
fc: data[7],
|
|
2197
|
+
data: data.subarray(8),
|
|
2198
|
+
buffer: data,
|
|
2199
|
+
};
|
|
2200
|
+
this.onFraming(frame);
|
|
2201
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
1965
2204
|
}
|
|
1966
2205
|
}
|
|
1967
|
-
let
|
|
1968
|
-
if (
|
|
1969
|
-
|
|
2206
|
+
let index = 0;
|
|
2207
|
+
if (!(this._expectedLen > 0 && totalAvailable < this._expectedLen)) {
|
|
2208
|
+
while (index <= totalAvailable - 6) {
|
|
2209
|
+
// Validate MBAP protocol ID (bytes 2-3 must be 0x0000)
|
|
2210
|
+
if ((index + 2 < residualLen ? this._residual[index + 2] : data[index + 2 - residualLen]) !== 0 ||
|
|
2211
|
+
(index + 3 < residualLen ? this._residual[index + 3] : data[index + 3 - residualLen]) !== 0) {
|
|
2212
|
+
this.onFramingError(new Error('Invalid data'));
|
|
2213
|
+
this._residualLen = 0;
|
|
2214
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
const length = ((index + 4 < residualLen ? this._residual[index + 4] : data[index + 4 - residualLen]) << 8) |
|
|
2218
|
+
(index + 5 < residualLen ? this._residual[index + 5] : data[index + 5 - residualLen]);
|
|
2219
|
+
const frameLen = 6 + length;
|
|
2220
|
+
if (frameLen > MAX_FRAME_LENGTH || length < 2) {
|
|
2221
|
+
this.onFramingError(new Error('Invalid data'));
|
|
2222
|
+
this._residualLen = 0;
|
|
2223
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (totalAvailable - index < frameLen) {
|
|
2227
|
+
this._expectedLen = index + frameLen;
|
|
2228
|
+
break;
|
|
2229
|
+
}
|
|
2230
|
+
// A complete frame has been received
|
|
2231
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
2232
|
+
const raw = Buffer.allocUnsafe(frameLen);
|
|
2233
|
+
if (index + frameLen <= residualLen) {
|
|
2234
|
+
this._residual.copy(raw, 0, index, index + frameLen);
|
|
2235
|
+
}
|
|
2236
|
+
else if (index >= residualLen) {
|
|
2237
|
+
data.copy(raw, 0, index - residualLen, index - residualLen + frameLen);
|
|
2238
|
+
}
|
|
2239
|
+
else {
|
|
2240
|
+
const headLen = residualLen - index;
|
|
2241
|
+
this._residual.copy(raw, 0, index, residualLen);
|
|
2242
|
+
data.copy(raw, headLen, 0, frameLen - headLen);
|
|
2243
|
+
}
|
|
2244
|
+
const frame = {
|
|
2245
|
+
transaction: (raw[0] << 8) | raw[1],
|
|
2246
|
+
unit: raw[6],
|
|
2247
|
+
fc: raw[7],
|
|
2248
|
+
data: raw.subarray(8),
|
|
2249
|
+
buffer: raw,
|
|
2250
|
+
};
|
|
2251
|
+
this.onFraming(frame);
|
|
2252
|
+
index += frameLen;
|
|
2253
|
+
}
|
|
1970
2254
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
2255
|
+
const newFrameStart = index;
|
|
2256
|
+
const finalRestLen = totalAvailable - newFrameStart;
|
|
2257
|
+
if (finalRestLen === 0) {
|
|
2258
|
+
this._residualLen = 0;
|
|
2259
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
1973
2260
|
}
|
|
1974
|
-
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2261
|
+
else {
|
|
2262
|
+
const keepLen = finalRestLen < MAX_FRAME_LENGTH ? finalRestLen : MAX_FRAME_LENGTH;
|
|
2263
|
+
const discardLen = totalAvailable - keepLen;
|
|
2264
|
+
if (discardLen >= residualLen) {
|
|
2265
|
+
// Kept portion lies entirely within the new `data`
|
|
2266
|
+
data.copy(this._residual, 0, discardLen - residualLen, dataLen);
|
|
1979
2267
|
}
|
|
1980
|
-
else if (
|
|
1981
|
-
|
|
2268
|
+
else if (discardLen > 0) {
|
|
2269
|
+
// Kept portion spans both buffers, or physical left-shift truncation occurred
|
|
2270
|
+
for (let i = 0; i < keepLen; i++) {
|
|
2271
|
+
this._residual[i] = discardLen + i < residualLen ? this._residual[discardLen + i] : data[discardLen + i - residualLen];
|
|
2272
|
+
}
|
|
1982
2273
|
}
|
|
1983
2274
|
else {
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2275
|
+
// discardLen === 0 (old data not consumed, truncation limit not hit) — simple append
|
|
2276
|
+
data.copy(this._residual, residualLen, 0, dataLen);
|
|
2277
|
+
}
|
|
2278
|
+
this._residualLen = keepLen;
|
|
2279
|
+
// Unify physical coordinate system translation
|
|
2280
|
+
if (discardLen > 0) {
|
|
2281
|
+
if (this._expectedLen > 0) {
|
|
2282
|
+
const newExpectedLen = this._expectedLen - discardLen;
|
|
2283
|
+
this._expectedLen = newExpectedLen > PREDICT_NEED_MORE ? newExpectedLen : PREDICT_NEED_MORE;
|
|
2284
|
+
}
|
|
1987
2285
|
}
|
|
1988
|
-
}
|
|
1989
|
-
if (buffer.length === 0) {
|
|
1990
|
-
this._buffer = EMPTY_BUFFER;
|
|
1991
|
-
}
|
|
1992
|
-
else if (buffer === data) {
|
|
1993
|
-
// Copy into a right-sized buffer so we do not retain the potentially
|
|
1994
|
-
// large backing buffer of the original incoming data (Node.js pool).
|
|
1995
|
-
// `Buffer.copyBytesFrom` is a native fast-path — one C++ memcpy
|
|
1996
|
-
// vs allocUnsafe + JS-level copy.
|
|
1997
|
-
this._buffer = Buffer.copyBytesFrom(buffer);
|
|
1998
|
-
}
|
|
1999
|
-
else {
|
|
2000
|
-
this._buffer = buffer;
|
|
2001
2286
|
}
|
|
2002
2287
|
};
|
|
2003
2288
|
connection.on('data', onData);
|
|
@@ -2011,38 +2296,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
2011
2296
|
connection.on('close', onClose);
|
|
2012
2297
|
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
2013
2298
|
}
|
|
2014
|
-
tryExtract(buffer) {
|
|
2015
|
-
if (buffer.length < 8) {
|
|
2016
|
-
return { kind: 'insufficient' };
|
|
2017
|
-
}
|
|
2018
|
-
if (buffer[2] !== 0 || buffer[3] !== 0) {
|
|
2019
|
-
return { kind: 'error', error: new Error('Invalid data') };
|
|
2020
|
-
}
|
|
2021
|
-
const length = (buffer[4] << 8) | buffer[5]; // inline BE read
|
|
2022
|
-
const total = 6 + length;
|
|
2023
|
-
if (total > MAX_TCP_FRAME || length < 2) {
|
|
2024
|
-
return { kind: 'error', error: new Error('Invalid data') };
|
|
2025
|
-
}
|
|
2026
|
-
if (buffer.length < total) {
|
|
2027
|
-
return { kind: 'insufficient' };
|
|
2028
|
-
}
|
|
2029
|
-
return { kind: 'frame', frame: buffer.subarray(0, total), rest: total === buffer.length ? EMPTY_BUFFER : buffer.subarray(total) };
|
|
2030
|
-
}
|
|
2031
|
-
processFrame(buffer) {
|
|
2032
|
-
const frame = {
|
|
2033
|
-
// Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
|
|
2034
|
-
// argument coercion + bounds check. Symmetric to the header writes in
|
|
2035
|
-
// encode() above. Hits on every received TCP frame.
|
|
2036
|
-
transaction: (buffer[0] << 8) | buffer[1],
|
|
2037
|
-
unit: buffer[6],
|
|
2038
|
-
fc: buffer[7],
|
|
2039
|
-
data: buffer.subarray(8),
|
|
2040
|
-
buffer,
|
|
2041
|
-
};
|
|
2042
|
-
this.onFraming(frame);
|
|
2043
|
-
}
|
|
2044
2299
|
flush() {
|
|
2045
|
-
this.
|
|
2300
|
+
this._residualLen = 0;
|
|
2301
|
+
this._expectedLen = PREDICT_NEED_MORE;
|
|
2046
2302
|
}
|
|
2047
2303
|
encode(unit, fc, data, transaction) {
|
|
2048
2304
|
const buffer = Buffer.allocUnsafe(data.length + 8);
|
|
@@ -2061,8 +2317,9 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
2061
2317
|
// Small-payload fast path: avoid C++ TypedArray.prototype.set boundary
|
|
2062
2318
|
// crossing when the copy is just a few bytes (common for FC 3/4/6 requests).
|
|
2063
2319
|
if (data.length <= 16) {
|
|
2064
|
-
for (let i = 0; i < data.length; i++)
|
|
2320
|
+
for (let i = 0; i < data.length; i++) {
|
|
2065
2321
|
buffer[8 + i] = data[i];
|
|
2322
|
+
}
|
|
2066
2323
|
}
|
|
2067
2324
|
else {
|
|
2068
2325
|
buffer.set(data, 8);
|
|
@@ -2110,26 +2367,33 @@ class MasterSession {
|
|
|
2110
2367
|
}
|
|
2111
2368
|
|
|
2112
2369
|
function validateResponse(frame, unit, fc) {
|
|
2113
|
-
if (frame.unit !== unit || frame.fc !== fc)
|
|
2370
|
+
if (frame.unit !== unit || frame.fc !== fc) {
|
|
2114
2371
|
throw new Error('Invalid response');
|
|
2372
|
+
}
|
|
2115
2373
|
}
|
|
2116
2374
|
function validateByteCountResponse(frame, unit, fc, byteCount) {
|
|
2117
2375
|
validateResponse(frame, unit, fc);
|
|
2118
|
-
if (frame.data.length < 1 + byteCount)
|
|
2376
|
+
if (frame.data.length < 1 + byteCount) {
|
|
2119
2377
|
throw new Error('Insufficient data length');
|
|
2120
|
-
|
|
2378
|
+
}
|
|
2379
|
+
if (frame.data.length !== 1 + byteCount) {
|
|
2121
2380
|
throw new Error('Invalid response');
|
|
2122
|
-
|
|
2381
|
+
}
|
|
2382
|
+
if (frame.data[0] !== byteCount) {
|
|
2123
2383
|
throw new Error('Invalid response');
|
|
2384
|
+
}
|
|
2124
2385
|
}
|
|
2125
2386
|
function validateEchoResponse(frame, unit, fc, expected) {
|
|
2126
2387
|
validateResponse(frame, unit, fc);
|
|
2127
|
-
if (frame.data.length < expected.length)
|
|
2388
|
+
if (frame.data.length < expected.length) {
|
|
2128
2389
|
throw new Error('Insufficient data length');
|
|
2129
|
-
|
|
2390
|
+
}
|
|
2391
|
+
if (frame.data.length !== expected.length) {
|
|
2130
2392
|
throw new Error('Invalid response');
|
|
2131
|
-
|
|
2393
|
+
}
|
|
2394
|
+
if (!frame.data.equals(expected)) {
|
|
2132
2395
|
throw new Error('Invalid response');
|
|
2396
|
+
}
|
|
2133
2397
|
}
|
|
2134
2398
|
class ModbusMaster extends EventEmitter {
|
|
2135
2399
|
timeout;
|
|
@@ -2157,8 +2421,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2157
2421
|
_pendingExchanges = new Map();
|
|
2158
2422
|
_timerHeap = new TimerHeap((id) => {
|
|
2159
2423
|
const pending = this._pendingExchanges.get(id);
|
|
2160
|
-
if (!pending)
|
|
2161
|
-
return;
|
|
2424
|
+
if (!pending) {
|
|
2425
|
+
return;
|
|
2426
|
+
} // lazy deletion: already handled
|
|
2162
2427
|
pending.settled = true;
|
|
2163
2428
|
this._pendingExchanges.delete(id);
|
|
2164
2429
|
if (pending.sessionKey !== null) {
|
|
@@ -2200,6 +2465,7 @@ class ModbusMaster extends EventEmitter {
|
|
|
2200
2465
|
appLayer.onFraming = NOOP;
|
|
2201
2466
|
};
|
|
2202
2467
|
const onFraming = (frame) => {
|
|
2468
|
+
this.emit('framing', frame, connection);
|
|
2203
2469
|
this._masterSession.handleFrame(frame);
|
|
2204
2470
|
};
|
|
2205
2471
|
appLayer.onFraming = onFraming;
|
|
@@ -2208,17 +2474,34 @@ class ModbusMaster extends EventEmitter {
|
|
|
2208
2474
|
appLayer.onFramingError = NOOP;
|
|
2209
2475
|
};
|
|
2210
2476
|
const onFramingError = (error) => {
|
|
2477
|
+
this.emit('framingError', error, connection);
|
|
2211
2478
|
this._masterSession.handleError(error);
|
|
2212
2479
|
};
|
|
2213
2480
|
appLayer.onFramingError = onFramingError;
|
|
2214
2481
|
this._cleanupFns.add(cleanupFramingError);
|
|
2482
|
+
const cleanupTx = () => connection.off('tx', onTx);
|
|
2483
|
+
const onTx = (buffer) => {
|
|
2484
|
+
this.emit('tx', buffer, connection);
|
|
2485
|
+
};
|
|
2486
|
+
connection.on('tx', onTx);
|
|
2487
|
+
this._cleanupFns.add(cleanupTx);
|
|
2488
|
+
const cleanupRx = () => connection.off('rx', onRx);
|
|
2489
|
+
const onRx = (buffer) => {
|
|
2490
|
+
this.emit('rx', buffer, connection);
|
|
2491
|
+
};
|
|
2492
|
+
connection.on('rx', onRx);
|
|
2493
|
+
this._cleanupFns.add(cleanupRx);
|
|
2215
2494
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2216
2495
|
const onClose = () => {
|
|
2217
2496
|
cleanupFraming();
|
|
2218
2497
|
cleanupFramingError();
|
|
2498
|
+
cleanupTx();
|
|
2499
|
+
cleanupRx();
|
|
2219
2500
|
cleanupClose();
|
|
2220
2501
|
this._cleanupFns.delete(cleanupFraming);
|
|
2221
2502
|
this._cleanupFns.delete(cleanupFramingError);
|
|
2503
|
+
this._cleanupFns.delete(cleanupTx);
|
|
2504
|
+
this._cleanupFns.delete(cleanupRx);
|
|
2222
2505
|
this._cleanupFns.delete(cleanupClose);
|
|
2223
2506
|
};
|
|
2224
2507
|
connection.on('close', onClose);
|
|
@@ -2258,7 +2541,7 @@ class ModbusMaster extends EventEmitter {
|
|
|
2258
2541
|
return new RtuApplicationLayer('MASTER', connection, {
|
|
2259
2542
|
intervalBetweenFrames,
|
|
2260
2543
|
interCharTimeout,
|
|
2261
|
-
|
|
2544
|
+
strictTiming: this._protocol.opts?.strictTiming,
|
|
2262
2545
|
});
|
|
2263
2546
|
}
|
|
2264
2547
|
if (this._protocol.type === 'TCP') {
|
|
@@ -2345,7 +2628,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2345
2628
|
}
|
|
2346
2629
|
// Lazy-deletion timer architecture:
|
|
2347
2630
|
// 1. Assign an exchangeId and register in _pendingExchanges.
|
|
2348
|
-
// 2. Push deadline into the global TimerHeap (
|
|
2631
|
+
// 2. Push deadline into the global TimerHeap (one native setTimeout under
|
|
2632
|
+
// load; a fast direct-timer path is used when only 1-2 exchanges are
|
|
2633
|
+
// pending).
|
|
2349
2634
|
// 3. When the response arrives, delete from Map — the heap entry is left
|
|
2350
2635
|
// behind and silently discarded when it surfaces at the top (lazy deletion).
|
|
2351
2636
|
const exchangeId = this._nextExchangeId++;
|
|
@@ -2356,11 +2641,13 @@ class ModbusMaster extends EventEmitter {
|
|
|
2356
2641
|
this._timerHeap.add(exchangeId, timeout);
|
|
2357
2642
|
connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
|
|
2358
2643
|
const p = this._pendingExchanges.get(exchangeId);
|
|
2359
|
-
if (!p || p.settled)
|
|
2644
|
+
if (!p || p.settled) {
|
|
2360
2645
|
return;
|
|
2646
|
+
}
|
|
2361
2647
|
const cb = p.callback;
|
|
2362
|
-
if (!cb)
|
|
2648
|
+
if (!cb) {
|
|
2363
2649
|
return;
|
|
2650
|
+
}
|
|
2364
2651
|
p.settled = true;
|
|
2365
2652
|
p.callback = null;
|
|
2366
2653
|
this._pendingExchanges.delete(exchangeId);
|
|
@@ -2389,8 +2676,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2389
2676
|
this._timerHeap.add(exchangeId, timeout);
|
|
2390
2677
|
connection.write(payload, (writeErr) => {
|
|
2391
2678
|
const p = this._pendingExchanges.get(exchangeId);
|
|
2392
|
-
if (!p || p.settled)
|
|
2679
|
+
if (!p || p.settled) {
|
|
2393
2680
|
return;
|
|
2681
|
+
}
|
|
2394
2682
|
if (writeErr) {
|
|
2395
2683
|
const cb = p.callback;
|
|
2396
2684
|
if (cb) {
|
|
@@ -2415,8 +2703,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2415
2703
|
// Timeout is managed by the global timer heap above.
|
|
2416
2704
|
this._masterSession.start(key, (err, frame) => {
|
|
2417
2705
|
const p2 = this._pendingExchanges.get(exchangeId);
|
|
2418
|
-
if (!p2 || p2.settled)
|
|
2706
|
+
if (!p2 || p2.settled) {
|
|
2419
2707
|
return;
|
|
2708
|
+
}
|
|
2420
2709
|
const cb = p2.callback;
|
|
2421
2710
|
if (cb) {
|
|
2422
2711
|
p2.settled = true;
|
|
@@ -2428,10 +2717,8 @@ class ModbusMaster extends EventEmitter {
|
|
|
2428
2717
|
});
|
|
2429
2718
|
}
|
|
2430
2719
|
writeFC1Or2(unit, fc, address, length, timeout) {
|
|
2431
|
-
const byteCount =
|
|
2720
|
+
const byteCount = (length + 7) >> 3;
|
|
2432
2721
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2433
|
-
// Inline big-endian writes — direct typed-array stores skip the argument
|
|
2434
|
-
// validation + bounds checks that `writeUInt16BE` runs on each call.
|
|
2435
2722
|
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2436
2723
|
bufferTx[1] = address & 0xff;
|
|
2437
2724
|
bufferTx[2] = (length >>> 8) & 0xff;
|
|
@@ -2448,12 +2735,44 @@ class ModbusMaster extends EventEmitter {
|
|
|
2448
2735
|
}
|
|
2449
2736
|
try {
|
|
2450
2737
|
validateByteCountResponse(frame, unit, fc, byteCount);
|
|
2451
|
-
const data = new
|
|
2452
|
-
|
|
2453
|
-
|
|
2738
|
+
const data = new Uint8Array(length);
|
|
2739
|
+
let byteIdx = 1;
|
|
2740
|
+
let outIdx = 0;
|
|
2741
|
+
const fullBytes = length >> 3;
|
|
2742
|
+
for (let b = 0; b < fullBytes; b++) {
|
|
2743
|
+
const byte = frame.data[byteIdx++];
|
|
2744
|
+
data[outIdx++] = byte & 0x01;
|
|
2745
|
+
data[outIdx++] = (byte >>> 1) & 0x01;
|
|
2746
|
+
data[outIdx++] = (byte >>> 2) & 0x01;
|
|
2747
|
+
data[outIdx++] = (byte >>> 3) & 0x01;
|
|
2748
|
+
data[outIdx++] = (byte >>> 4) & 0x01;
|
|
2749
|
+
data[outIdx++] = (byte >>> 5) & 0x01;
|
|
2750
|
+
data[outIdx++] = (byte >>> 6) & 0x01;
|
|
2751
|
+
data[outIdx++] = (byte >>> 7) & 0x01;
|
|
2752
|
+
}
|
|
2753
|
+
const rem = length & 7;
|
|
2754
|
+
if (rem) {
|
|
2755
|
+
const byte = frame.data[byteIdx];
|
|
2756
|
+
data[outIdx++] = byte & 0x01;
|
|
2757
|
+
if (rem > 1) {
|
|
2758
|
+
data[outIdx++] = (byte >>> 1) & 0x01;
|
|
2759
|
+
}
|
|
2760
|
+
if (rem > 2) {
|
|
2761
|
+
data[outIdx++] = (byte >>> 2) & 0x01;
|
|
2762
|
+
}
|
|
2763
|
+
if (rem > 3) {
|
|
2764
|
+
data[outIdx++] = (byte >>> 3) & 0x01;
|
|
2765
|
+
}
|
|
2766
|
+
if (rem > 4) {
|
|
2767
|
+
data[outIdx++] = (byte >>> 4) & 0x01;
|
|
2768
|
+
}
|
|
2769
|
+
if (rem > 5) {
|
|
2770
|
+
data[outIdx++] = (byte >>> 5) & 0x01;
|
|
2771
|
+
}
|
|
2772
|
+
if (rem > 6) {
|
|
2773
|
+
data[outIdx++] = (byte >>> 6) & 0x01;
|
|
2774
|
+
}
|
|
2454
2775
|
}
|
|
2455
|
-
// Mutate the frame in place rather than spread-copying — `frame` is freshly
|
|
2456
|
-
// allocated per request and not retained anywhere else.
|
|
2457
2776
|
frame.data = data;
|
|
2458
2777
|
resolve(frame);
|
|
2459
2778
|
}
|
|
@@ -2498,9 +2817,10 @@ class ModbusMaster extends EventEmitter {
|
|
|
2498
2817
|
// on each call. Symmetric to the slave-side BE write inlining
|
|
2499
2818
|
// in handleFC3/FC4. At length=125 (FC3 max) that's 250 saved
|
|
2500
2819
|
// bounds-check pairs per response.
|
|
2820
|
+
let off = 0;
|
|
2501
2821
|
for (let i = 0; i < length; i++) {
|
|
2502
|
-
const off = i * 2;
|
|
2503
2822
|
data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
|
|
2823
|
+
off += 2;
|
|
2504
2824
|
}
|
|
2505
2825
|
frame.data = data;
|
|
2506
2826
|
resolve(frame);
|
|
@@ -2523,7 +2843,7 @@ class ModbusMaster extends EventEmitter {
|
|
|
2523
2843
|
writeSingleCoil(unit, address, value, timeout = this.timeout) {
|
|
2524
2844
|
const fc = FunctionCode.WRITE_SINGLE_COIL;
|
|
2525
2845
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2526
|
-
const coilValue = value ? COIL_ON : COIL_OFF;
|
|
2846
|
+
const coilValue = value === 1 ? COIL_ON : COIL_OFF;
|
|
2527
2847
|
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2528
2848
|
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2529
2849
|
bufferTx[1] = address & 0xff;
|
|
@@ -2583,18 +2903,52 @@ class ModbusMaster extends EventEmitter {
|
|
|
2583
2903
|
writeFC15;
|
|
2584
2904
|
writeMultipleCoils(unit, address, value, timeout = this.timeout) {
|
|
2585
2905
|
const fc = FunctionCode.WRITE_MULTIPLE_COILS;
|
|
2586
|
-
const
|
|
2587
|
-
const
|
|
2906
|
+
const len = value.length;
|
|
2907
|
+
const byteCount = (len + 7) >> 3;
|
|
2908
|
+
const bufferTx = Buffer.allocUnsafe(5 + byteCount);
|
|
2588
2909
|
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2589
2910
|
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2590
2911
|
bufferTx[1] = address & 0xff;
|
|
2591
|
-
bufferTx[2] = (
|
|
2592
|
-
bufferTx[3] =
|
|
2912
|
+
bufferTx[2] = (len >>> 8) & 0xff;
|
|
2913
|
+
bufferTx[3] = len & 0xff;
|
|
2593
2914
|
bufferTx[4] = byteCount;
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2915
|
+
let out = 5;
|
|
2916
|
+
const fullBytes = len >> 3;
|
|
2917
|
+
for (let b = 0; b < fullBytes; b++) {
|
|
2918
|
+
const base = b << 3;
|
|
2919
|
+
bufferTx[out++] =
|
|
2920
|
+
(value[base] & 1) |
|
|
2921
|
+
((value[base + 1] & 1) << 1) |
|
|
2922
|
+
((value[base + 2] & 1) << 2) |
|
|
2923
|
+
((value[base + 3] & 1) << 3) |
|
|
2924
|
+
((value[base + 4] & 1) << 4) |
|
|
2925
|
+
((value[base + 5] & 1) << 5) |
|
|
2926
|
+
((value[base + 6] & 1) << 6) |
|
|
2927
|
+
((value[base + 7] & 1) << 7);
|
|
2928
|
+
}
|
|
2929
|
+
const rem = len & 7;
|
|
2930
|
+
if (rem) {
|
|
2931
|
+
const base = fullBytes << 3;
|
|
2932
|
+
let acc = value[base] & 1;
|
|
2933
|
+
if (rem > 1) {
|
|
2934
|
+
acc |= (value[base + 1] & 1) << 1;
|
|
2935
|
+
}
|
|
2936
|
+
if (rem > 2) {
|
|
2937
|
+
acc |= (value[base + 2] & 1) << 2;
|
|
2938
|
+
}
|
|
2939
|
+
if (rem > 3) {
|
|
2940
|
+
acc |= (value[base + 3] & 1) << 3;
|
|
2597
2941
|
}
|
|
2942
|
+
if (rem > 4) {
|
|
2943
|
+
acc |= (value[base + 4] & 1) << 4;
|
|
2944
|
+
}
|
|
2945
|
+
if (rem > 5) {
|
|
2946
|
+
acc |= (value[base + 5] & 1) << 5;
|
|
2947
|
+
}
|
|
2948
|
+
if (rem > 6) {
|
|
2949
|
+
acc |= (value[base + 6] & 1) << 6;
|
|
2950
|
+
}
|
|
2951
|
+
bufferTx[out] = acc;
|
|
2598
2952
|
}
|
|
2599
2953
|
return new Promise((resolve, reject) => {
|
|
2600
2954
|
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
@@ -2628,11 +2982,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2628
2982
|
bufferTx[2] = (value.length >>> 8) & 0xff;
|
|
2629
2983
|
bufferTx[3] = value.length & 0xff;
|
|
2630
2984
|
bufferTx[4] = byteCount;
|
|
2985
|
+
let off = 5;
|
|
2631
2986
|
for (let i = 0; i < value.length; i++) {
|
|
2632
2987
|
const v = value[i];
|
|
2633
|
-
const off = 5 + i * 2;
|
|
2634
2988
|
bufferTx[off] = (v >>> 8) & 0xff;
|
|
2635
2989
|
bufferTx[off + 1] = v & 0xff;
|
|
2990
|
+
off += 2;
|
|
2636
2991
|
}
|
|
2637
2992
|
return new Promise((resolve, reject) => {
|
|
2638
2993
|
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
@@ -2670,15 +3025,17 @@ class ModbusMaster extends EventEmitter {
|
|
|
2670
3025
|
}
|
|
2671
3026
|
try {
|
|
2672
3027
|
validateResponse(frame, unit, fc);
|
|
2673
|
-
if (frame.data.length < 2 + serverIdLength)
|
|
3028
|
+
if (frame.data.length < 2 + serverIdLength) {
|
|
2674
3029
|
throw new Error('Insufficient data length');
|
|
2675
|
-
|
|
3030
|
+
}
|
|
3031
|
+
if (frame.data.length !== 1 + frame.data[0]) {
|
|
2676
3032
|
throw new Error('Invalid response');
|
|
3033
|
+
}
|
|
2677
3034
|
const runStatusIndex = 1 + serverIdLength;
|
|
2678
3035
|
frame.data = {
|
|
2679
3036
|
serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
|
|
2680
3037
|
runIndicatorStatus: frame.data[runStatusIndex] === 0xff,
|
|
2681
|
-
additionalData:
|
|
3038
|
+
additionalData: frame.data.subarray(runStatusIndex + 1),
|
|
2682
3039
|
};
|
|
2683
3040
|
resolve(frame);
|
|
2684
3041
|
}
|
|
@@ -2736,11 +3093,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2736
3093
|
bufferTx[6] = (write.value.length >>> 8) & 0xff;
|
|
2737
3094
|
bufferTx[7] = write.value.length & 0xff;
|
|
2738
3095
|
bufferTx[8] = byteCount;
|
|
3096
|
+
let off = 9;
|
|
2739
3097
|
for (let i = 0; i < write.value.length; i++) {
|
|
2740
3098
|
const v = write.value[i];
|
|
2741
|
-
const off = 9 + i * 2;
|
|
2742
3099
|
bufferTx[off] = (v >>> 8) & 0xff;
|
|
2743
3100
|
bufferTx[off + 1] = v & 0xff;
|
|
3101
|
+
off += 2;
|
|
2744
3102
|
}
|
|
2745
3103
|
return new Promise((resolve, reject) => {
|
|
2746
3104
|
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
@@ -2759,9 +3117,10 @@ class ModbusMaster extends EventEmitter {
|
|
|
2759
3117
|
// closure + N readUInt16BE bounds-check pairs. See writeFC3Or4
|
|
2760
3118
|
// response handler for the same optimization.
|
|
2761
3119
|
const data = new Array(read.length);
|
|
3120
|
+
let off = 0;
|
|
2762
3121
|
for (let i = 0; i < read.length; i++) {
|
|
2763
|
-
const off = i * 2;
|
|
2764
3122
|
data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
|
|
3123
|
+
off += 2;
|
|
2765
3124
|
}
|
|
2766
3125
|
frame.data = data;
|
|
2767
3126
|
resolve(frame);
|
|
@@ -2787,10 +3146,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2787
3146
|
}
|
|
2788
3147
|
try {
|
|
2789
3148
|
validateResponse(frame, unit, fc);
|
|
2790
|
-
if (frame.data.length < 6)
|
|
3149
|
+
if (frame.data.length < 6) {
|
|
2791
3150
|
throw new Error('Insufficient data length');
|
|
2792
|
-
|
|
3151
|
+
}
|
|
3152
|
+
if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode) {
|
|
2793
3153
|
throw new Error('Invalid response');
|
|
3154
|
+
}
|
|
2794
3155
|
const objects = [];
|
|
2795
3156
|
let object = [];
|
|
2796
3157
|
let totalBytes = 0;
|
|
@@ -2818,10 +3179,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2818
3179
|
break;
|
|
2819
3180
|
}
|
|
2820
3181
|
}
|
|
2821
|
-
if (objects.length !== frame.data[5])
|
|
3182
|
+
if (objects.length !== frame.data[5]) {
|
|
2822
3183
|
throw new Error('Invalid response');
|
|
2823
|
-
|
|
3184
|
+
}
|
|
3185
|
+
if (frame.data.length !== 6 + totalBytes) {
|
|
2824
3186
|
throw new Error('Invalid response');
|
|
3187
|
+
}
|
|
2825
3188
|
frame.data = {
|
|
2826
3189
|
readDeviceIDCode,
|
|
2827
3190
|
conformityLevel: frame.data[2],
|
|
@@ -2868,7 +3231,7 @@ class ModbusMaster extends EventEmitter {
|
|
|
2868
3231
|
});
|
|
2869
3232
|
}
|
|
2870
3233
|
/**
|
|
2871
|
-
* Open the underlying physical layer and
|
|
3234
|
+
* Open the underlying physical layer and establish a connection.
|
|
2872
3235
|
*
|
|
2873
3236
|
* A `ModbusMaster` instance can only be opened once. Once {@link close}
|
|
2874
3237
|
* is called — explicitly or because the physical layer disconnected —
|
|
@@ -2898,8 +3261,9 @@ class ModbusMaster extends EventEmitter {
|
|
|
2898
3261
|
// (broadcasts have no session waiter; non-broadcasts still in the
|
|
2899
3262
|
// pre-write-window haven't registered in session yet).
|
|
2900
3263
|
for (const pending of this._pendingExchanges.values()) {
|
|
2901
|
-
if (pending.settled)
|
|
3264
|
+
if (pending.settled) {
|
|
2902
3265
|
continue;
|
|
3266
|
+
}
|
|
2903
3267
|
pending.settled = true;
|
|
2904
3268
|
const cb = pending.callback;
|
|
2905
3269
|
if (cb) {
|
|
@@ -2977,6 +3341,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2977
3341
|
appLayer.onFraming = NOOP;
|
|
2978
3342
|
};
|
|
2979
3343
|
const onFraming = (frame) => {
|
|
3344
|
+
this.emit('framing', frame, connection);
|
|
2980
3345
|
if (this._physicalLayer.state !== PhysicalState.OPEN) {
|
|
2981
3346
|
return;
|
|
2982
3347
|
}
|
|
@@ -2992,11 +3357,37 @@ class ModbusSlave extends EventEmitter {
|
|
|
2992
3357
|
};
|
|
2993
3358
|
appLayer.onFraming = onFraming;
|
|
2994
3359
|
this._cleanupFns.add(cleanupFraming);
|
|
3360
|
+
const cleanupFramingError = () => {
|
|
3361
|
+
appLayer.onFramingError = NOOP;
|
|
3362
|
+
};
|
|
3363
|
+
const onFramingError = (error) => {
|
|
3364
|
+
this.emit('framingError', error, connection);
|
|
3365
|
+
};
|
|
3366
|
+
appLayer.onFramingError = onFramingError;
|
|
3367
|
+
this._cleanupFns.add(cleanupFramingError);
|
|
3368
|
+
const cleanupTx = () => connection.off('tx', onTx);
|
|
3369
|
+
const onTx = (buffer) => {
|
|
3370
|
+
this.emit('tx', buffer, connection);
|
|
3371
|
+
};
|
|
3372
|
+
connection.on('tx', onTx);
|
|
3373
|
+
this._cleanupFns.add(cleanupTx);
|
|
3374
|
+
const cleanupRx = () => connection.off('rx', onRx);
|
|
3375
|
+
const onRx = (buffer) => {
|
|
3376
|
+
this.emit('rx', buffer, connection);
|
|
3377
|
+
};
|
|
3378
|
+
connection.on('rx', onRx);
|
|
3379
|
+
this._cleanupFns.add(cleanupRx);
|
|
2995
3380
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2996
3381
|
const onClose = () => {
|
|
2997
3382
|
cleanupFraming();
|
|
3383
|
+
cleanupFramingError();
|
|
3384
|
+
cleanupTx();
|
|
3385
|
+
cleanupRx();
|
|
2998
3386
|
cleanupClose();
|
|
2999
3387
|
this._cleanupFns.delete(cleanupFraming);
|
|
3388
|
+
this._cleanupFns.delete(cleanupFramingError);
|
|
3389
|
+
this._cleanupFns.delete(cleanupTx);
|
|
3390
|
+
this._cleanupFns.delete(cleanupRx);
|
|
3000
3391
|
this._cleanupFns.delete(cleanupClose);
|
|
3001
3392
|
this._appLayers.delete(appLayer);
|
|
3002
3393
|
};
|
|
@@ -3025,7 +3416,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
3025
3416
|
return new RtuApplicationLayer('SLAVE', connection, {
|
|
3026
3417
|
intervalBetweenFrames,
|
|
3027
3418
|
interCharTimeout,
|
|
3028
|
-
|
|
3419
|
+
strictTiming: this._protocol.opts?.strictTiming,
|
|
3029
3420
|
});
|
|
3030
3421
|
}
|
|
3031
3422
|
if (this._protocol.type === 'TCP') {
|
|
@@ -3057,23 +3448,44 @@ class ModbusSlave extends EventEmitter {
|
|
|
3057
3448
|
const byteCount = (length + 7) >> 3;
|
|
3058
3449
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
3059
3450
|
pdu[0] = byteCount;
|
|
3060
|
-
// Pack 8 booleans per byte without first zero-filling: accumulate into
|
|
3061
|
-
// `acc` and write a full byte once each lane is finished. Saves the
|
|
3062
|
-
// `pdu.fill(0, 1, ...)` pass and replaces N `|=` reads-modify-writes
|
|
3063
|
-
// with N `read`s + ⌈N/8⌉ `write`s. Measured ~+5% on FC01 / ~+3% on FC02
|
|
3064
|
-
// at max payload (2000 / 1968 coils) via benchmark/all-fcs.ts bisect.
|
|
3065
|
-
let acc = 0;
|
|
3066
3451
|
let out = 1;
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3452
|
+
const fullBytes = length >> 3;
|
|
3453
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
3454
|
+
const base = i << 3;
|
|
3455
|
+
pdu[out++] =
|
|
3456
|
+
(coils[base] & 1) |
|
|
3457
|
+
((coils[base + 1] & 1) << 1) |
|
|
3458
|
+
((coils[base + 2] & 1) << 2) |
|
|
3459
|
+
((coils[base + 3] & 1) << 3) |
|
|
3460
|
+
((coils[base + 4] & 1) << 4) |
|
|
3461
|
+
((coils[base + 5] & 1) << 5) |
|
|
3462
|
+
((coils[base + 6] & 1) << 6) |
|
|
3463
|
+
((coils[base + 7] & 1) << 7);
|
|
3074
3464
|
}
|
|
3075
|
-
|
|
3465
|
+
const rem = length & 7;
|
|
3466
|
+
if (rem) {
|
|
3467
|
+
const base = fullBytes << 3;
|
|
3468
|
+
let acc = coils[base] & 1;
|
|
3469
|
+
if (rem > 1) {
|
|
3470
|
+
acc |= (coils[base + 1] & 1) << 1;
|
|
3471
|
+
}
|
|
3472
|
+
if (rem > 2) {
|
|
3473
|
+
acc |= (coils[base + 2] & 1) << 2;
|
|
3474
|
+
}
|
|
3475
|
+
if (rem > 3) {
|
|
3476
|
+
acc |= (coils[base + 3] & 1) << 3;
|
|
3477
|
+
}
|
|
3478
|
+
if (rem > 4) {
|
|
3479
|
+
acc |= (coils[base + 4] & 1) << 4;
|
|
3480
|
+
}
|
|
3481
|
+
if (rem > 5) {
|
|
3482
|
+
acc |= (coils[base + 5] & 1) << 5;
|
|
3483
|
+
}
|
|
3484
|
+
if (rem > 6) {
|
|
3485
|
+
acc |= (coils[base + 6] & 1) << 6;
|
|
3486
|
+
}
|
|
3076
3487
|
pdu[out] = acc;
|
|
3488
|
+
}
|
|
3077
3489
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3078
3490
|
}
|
|
3079
3491
|
catch (error) {
|
|
@@ -3104,19 +3516,44 @@ class ModbusSlave extends EventEmitter {
|
|
|
3104
3516
|
const byteCount = (length + 7) >> 3;
|
|
3105
3517
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
3106
3518
|
pdu[0] = byteCount;
|
|
3107
|
-
// Accumulator-based bit pack — see handleFC1 for the rationale.
|
|
3108
|
-
let acc = 0;
|
|
3109
3519
|
let out = 1;
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3520
|
+
const fullBytes = length >> 3;
|
|
3521
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
3522
|
+
const base = i << 3;
|
|
3523
|
+
pdu[out++] =
|
|
3524
|
+
(discreteInputs[base] & 1) |
|
|
3525
|
+
((discreteInputs[base + 1] & 1) << 1) |
|
|
3526
|
+
((discreteInputs[base + 2] & 1) << 2) |
|
|
3527
|
+
((discreteInputs[base + 3] & 1) << 3) |
|
|
3528
|
+
((discreteInputs[base + 4] & 1) << 4) |
|
|
3529
|
+
((discreteInputs[base + 5] & 1) << 5) |
|
|
3530
|
+
((discreteInputs[base + 6] & 1) << 6) |
|
|
3531
|
+
((discreteInputs[base + 7] & 1) << 7);
|
|
3117
3532
|
}
|
|
3118
|
-
|
|
3533
|
+
const rem = length & 7;
|
|
3534
|
+
if (rem) {
|
|
3535
|
+
const base = fullBytes << 3;
|
|
3536
|
+
let acc = discreteInputs[base] & 1;
|
|
3537
|
+
if (rem > 1) {
|
|
3538
|
+
acc |= (discreteInputs[base + 1] & 1) << 1;
|
|
3539
|
+
}
|
|
3540
|
+
if (rem > 2) {
|
|
3541
|
+
acc |= (discreteInputs[base + 2] & 1) << 2;
|
|
3542
|
+
}
|
|
3543
|
+
if (rem > 3) {
|
|
3544
|
+
acc |= (discreteInputs[base + 3] & 1) << 3;
|
|
3545
|
+
}
|
|
3546
|
+
if (rem > 4) {
|
|
3547
|
+
acc |= (discreteInputs[base + 4] & 1) << 4;
|
|
3548
|
+
}
|
|
3549
|
+
if (rem > 5) {
|
|
3550
|
+
acc |= (discreteInputs[base + 5] & 1) << 5;
|
|
3551
|
+
}
|
|
3552
|
+
if (rem > 6) {
|
|
3553
|
+
acc |= (discreteInputs[base + 6] & 1) << 6;
|
|
3554
|
+
}
|
|
3119
3555
|
pdu[out] = acc;
|
|
3556
|
+
}
|
|
3120
3557
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3121
3558
|
}
|
|
3122
3559
|
catch (error) {
|
|
@@ -3149,11 +3586,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
3149
3586
|
// Inline big-endian write — `pdu[i] = v` is a direct typed-array store,
|
|
3150
3587
|
// while `writeUInt16BE` runs argument validation + bounds checks on each
|
|
3151
3588
|
// call. At length=125 (FC3 max) that's 250 saved checks per request.
|
|
3589
|
+
let off = 1;
|
|
3152
3590
|
for (let i = 0; i < length; i++) {
|
|
3153
3591
|
const v = registers[i];
|
|
3154
|
-
const off = 1 + i * 2;
|
|
3155
3592
|
pdu[off] = (v >>> 8) & 0xff;
|
|
3156
3593
|
pdu[off + 1] = v & 0xff;
|
|
3594
|
+
off += 2;
|
|
3157
3595
|
}
|
|
3158
3596
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3159
3597
|
}
|
|
@@ -3185,11 +3623,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
3185
3623
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
3186
3624
|
pdu[0] = length * 2;
|
|
3187
3625
|
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3626
|
+
let off = 1;
|
|
3188
3627
|
for (let i = 0; i < length; i++) {
|
|
3189
3628
|
const v = registers[i];
|
|
3190
|
-
const off = 1 + i * 2;
|
|
3191
3629
|
pdu[off] = (v >>> 8) & 0xff;
|
|
3192
3630
|
pdu[off + 1] = v & 0xff;
|
|
3631
|
+
off += 2;
|
|
3193
3632
|
}
|
|
3194
3633
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3195
3634
|
}
|
|
@@ -3217,7 +3656,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
3217
3656
|
return;
|
|
3218
3657
|
}
|
|
3219
3658
|
try {
|
|
3220
|
-
await model.writeSingleCoil(address, value === COIL_ON);
|
|
3659
|
+
await model.writeSingleCoil(address, value === COIL_ON ? 1 : 0);
|
|
3221
3660
|
await response(appLayer.encode(frame.unit, frame.fc, frame.data, frame.transaction));
|
|
3222
3661
|
}
|
|
3223
3662
|
catch (error) {
|
|
@@ -3259,7 +3698,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
3259
3698
|
const address = (frame.data[0] << 8) | frame.data[1];
|
|
3260
3699
|
const length = (frame.data[2] << 8) | frame.data[3];
|
|
3261
3700
|
const byteCount = frame.data[4];
|
|
3262
|
-
if (length < LIMITS.READ_COILS_MIN || length > LIMITS.WRITE_COILS_MAX || byteCount !==
|
|
3701
|
+
if (length < LIMITS.READ_COILS_MIN || length > LIMITS.WRITE_COILS_MAX || byteCount !== (length + 7) >> 3) {
|
|
3263
3702
|
await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
3264
3703
|
return;
|
|
3265
3704
|
}
|
|
@@ -3267,13 +3706,43 @@ class ModbusSlave extends EventEmitter {
|
|
|
3267
3706
|
await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
3268
3707
|
return;
|
|
3269
3708
|
}
|
|
3270
|
-
const value = new
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
value[
|
|
3709
|
+
const value = new Uint8Array(length);
|
|
3710
|
+
let byteIdx = 5;
|
|
3711
|
+
let outIdx = 0;
|
|
3712
|
+
const fullBytes = length >> 3;
|
|
3713
|
+
for (let b = 0; b < fullBytes; b++) {
|
|
3714
|
+
const byte = frame.data[byteIdx++];
|
|
3715
|
+
value[outIdx++] = byte & 0x01;
|
|
3716
|
+
value[outIdx++] = (byte >>> 1) & 0x01;
|
|
3717
|
+
value[outIdx++] = (byte >>> 2) & 0x01;
|
|
3718
|
+
value[outIdx++] = (byte >>> 3) & 0x01;
|
|
3719
|
+
value[outIdx++] = (byte >>> 4) & 0x01;
|
|
3720
|
+
value[outIdx++] = (byte >>> 5) & 0x01;
|
|
3721
|
+
value[outIdx++] = (byte >>> 6) & 0x01;
|
|
3722
|
+
value[outIdx++] = (byte >>> 7) & 0x01;
|
|
3723
|
+
}
|
|
3724
|
+
const rem = length & 7;
|
|
3725
|
+
if (rem) {
|
|
3726
|
+
const byte = frame.data[byteIdx];
|
|
3727
|
+
value[outIdx++] = byte & 0x01;
|
|
3728
|
+
if (rem > 1) {
|
|
3729
|
+
value[outIdx++] = (byte >>> 1) & 0x01;
|
|
3730
|
+
}
|
|
3731
|
+
if (rem > 2) {
|
|
3732
|
+
value[outIdx++] = (byte >>> 2) & 0x01;
|
|
3733
|
+
}
|
|
3734
|
+
if (rem > 3) {
|
|
3735
|
+
value[outIdx++] = (byte >>> 3) & 0x01;
|
|
3736
|
+
}
|
|
3737
|
+
if (rem > 4) {
|
|
3738
|
+
value[outIdx++] = (byte >>> 4) & 0x01;
|
|
3739
|
+
}
|
|
3740
|
+
if (rem > 5) {
|
|
3741
|
+
value[outIdx++] = (byte >>> 5) & 0x01;
|
|
3742
|
+
}
|
|
3743
|
+
if (rem > 6) {
|
|
3744
|
+
value[outIdx++] = (byte >>> 6) & 0x01;
|
|
3745
|
+
}
|
|
3277
3746
|
}
|
|
3278
3747
|
try {
|
|
3279
3748
|
if (model.writeMultipleCoils) {
|
|
@@ -3311,8 +3780,10 @@ class ModbusSlave extends EventEmitter {
|
|
|
3311
3780
|
return;
|
|
3312
3781
|
}
|
|
3313
3782
|
const value = new Array(length);
|
|
3783
|
+
let off = 5;
|
|
3314
3784
|
for (let i = 0; i < length; i++) {
|
|
3315
|
-
value[i] = (frame.data[
|
|
3785
|
+
value[i] = (frame.data[off] << 8) | frame.data[off + 1];
|
|
3786
|
+
off += 2;
|
|
3316
3787
|
}
|
|
3317
3788
|
try {
|
|
3318
3789
|
if (model.writeMultipleRegisters) {
|
|
@@ -3339,21 +3810,41 @@ class ModbusSlave extends EventEmitter {
|
|
|
3339
3810
|
return;
|
|
3340
3811
|
}
|
|
3341
3812
|
try {
|
|
3342
|
-
const
|
|
3343
|
-
const
|
|
3344
|
-
const
|
|
3813
|
+
const result = await model.reportServerId();
|
|
3814
|
+
const sid = result.serverId;
|
|
3815
|
+
const extra = result.additionalData;
|
|
3816
|
+
const sidLen = sid?.length ?? 1;
|
|
3817
|
+
const extraLen = extra?.length ?? 0;
|
|
3818
|
+
const byteCount = sidLen + 1 + extraLen;
|
|
3345
3819
|
if (byteCount > 255) {
|
|
3346
3820
|
await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
3347
3821
|
return;
|
|
3348
3822
|
}
|
|
3349
|
-
const allBytes = [...serverIdBytes, runIndicatorStatus ? 0xff : 0x00, ...additionalData];
|
|
3350
|
-
if (allBytes.some((b) => !isUint8(b))) {
|
|
3351
|
-
await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
3352
|
-
return;
|
|
3353
|
-
}
|
|
3354
3823
|
const data = Buffer.allocUnsafe(byteCount + 1);
|
|
3355
3824
|
data[0] = byteCount;
|
|
3356
|
-
|
|
3825
|
+
let off = 1;
|
|
3826
|
+
if (sid) {
|
|
3827
|
+
for (let i = 0; i < sidLen; i++) {
|
|
3828
|
+
const b = sid[i];
|
|
3829
|
+
if ((b & 0xff) !== b) {
|
|
3830
|
+
await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
data[off++] = b;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
else {
|
|
3837
|
+
const unitId = model.unit ?? 1;
|
|
3838
|
+
if ((unitId & 0xff) !== unitId) {
|
|
3839
|
+
await this.responseError(appLayer, frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
data[off++] = unitId;
|
|
3843
|
+
}
|
|
3844
|
+
data[off++] = (result.runIndicatorStatus ?? true) ? 0xff : 0x00;
|
|
3845
|
+
if (extra) {
|
|
3846
|
+
extra.copy(data, off);
|
|
3847
|
+
}
|
|
3357
3848
|
await response(appLayer.encode(frame.unit, frame.fc, data, frame.transaction));
|
|
3358
3849
|
}
|
|
3359
3850
|
catch (error) {
|
|
@@ -3423,8 +3914,10 @@ class ModbusSlave extends EventEmitter {
|
|
|
3423
3914
|
return;
|
|
3424
3915
|
}
|
|
3425
3916
|
const value = new Array(length.write);
|
|
3917
|
+
let off = 9;
|
|
3426
3918
|
for (let i = 0; i < length.write; i++) {
|
|
3427
|
-
value[i] = (frame.data[
|
|
3919
|
+
value[i] = (frame.data[off] << 8) | frame.data[off + 1];
|
|
3920
|
+
off += 2;
|
|
3428
3921
|
}
|
|
3429
3922
|
try {
|
|
3430
3923
|
await this._withIntervalLock(address.write, address.write + length.write, async () => {
|
|
@@ -3441,11 +3934,12 @@ class ModbusSlave extends EventEmitter {
|
|
|
3441
3934
|
const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
|
|
3442
3935
|
pdu[0] = length.read * 2;
|
|
3443
3936
|
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3937
|
+
let off = 1;
|
|
3444
3938
|
for (let i = 0; i < length.read; i++) {
|
|
3445
3939
|
const v = registers[i];
|
|
3446
|
-
const off = 1 + i * 2;
|
|
3447
3940
|
pdu[off] = (v >>> 8) & 0xff;
|
|
3448
3941
|
pdu[off + 1] = v & 0xff;
|
|
3942
|
+
off += 2;
|
|
3449
3943
|
}
|
|
3450
3944
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3451
3945
|
}
|
|
@@ -3677,8 +4171,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
3677
4171
|
for (let i = 0; i < locks.length; i++) {
|
|
3678
4172
|
const l = locks[i];
|
|
3679
4173
|
if (l.lo < hi && lo < l.hi) {
|
|
3680
|
-
if (overlap === null)
|
|
4174
|
+
if (overlap === null) {
|
|
3681
4175
|
overlap = [];
|
|
4176
|
+
}
|
|
3682
4177
|
overlap.push(l.promise);
|
|
3683
4178
|
}
|
|
3684
4179
|
}
|
|
@@ -3714,8 +4209,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
3714
4209
|
if (i !== -1) {
|
|
3715
4210
|
// O(1) swap-and-pop since lock order doesn't matter for correctness.
|
|
3716
4211
|
const last = locks.length - 1;
|
|
3717
|
-
if (i !== last)
|
|
4212
|
+
if (i !== last) {
|
|
3718
4213
|
locks[i] = locks[last];
|
|
4214
|
+
}
|
|
3719
4215
|
locks.pop();
|
|
3720
4216
|
}
|
|
3721
4217
|
}
|