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