njs-modbus 3.1.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -35
- package/README.zh-CN.md +84 -35
- package/dist/index.cjs +1291 -497
- package/dist/index.d.ts +99 -31
- package/dist/index.mjs +1291 -498
- package/dist/utils.cjs +536 -0
- package/dist/utils.d.ts +163 -0
- package/dist/utils.mjs +522 -0
- package/package.json +22 -2
- package/dist/src/error-code.d.ts +0 -17
- package/dist/src/index.d.ts +0 -7
- package/dist/src/layers/application/abstract-application-layer.d.ts +0 -26
- package/dist/src/layers/application/ascii-application-layer.d.ts +0 -23
- package/dist/src/layers/application/index.d.ts +0 -6
- package/dist/src/layers/application/rtu-application-layer.d.ts +0 -34
- package/dist/src/layers/application/tcp-application-layer.d.ts +0 -16
- package/dist/src/layers/physical/abstract-physical-layer.d.ts +0 -50
- package/dist/src/layers/physical/index.d.ts +0 -12
- package/dist/src/layers/physical/serial-physical-layer.d.ts +0 -70
- package/dist/src/layers/physical/tcp-client-physical-layer.d.ts +0 -20
- package/dist/src/layers/physical/tcp-physical-connection.d.ts +0 -16
- package/dist/src/layers/physical/tcp-server-physical-layer.d.ts +0 -29
- package/dist/src/layers/physical/udp-client-physical-layer.d.ts +0 -34
- package/dist/src/layers/physical/udp-server-physical-layer.d.ts +0 -51
- package/dist/src/layers/physical/utils.d.ts +0 -39
- package/dist/src/layers/physical/vars.d.ts +0 -11
- package/dist/src/master/index.d.ts +0 -3
- package/dist/src/master/master-session.d.ts +0 -18
- package/dist/src/master/master.d.ts +0 -140
- package/dist/src/slave/index.d.ts +0 -2
- package/dist/src/slave/slave.d.ts +0 -119
- package/dist/src/types.d.ts +0 -54
- package/dist/src/utils/bitsToMs.d.ts +0 -13
- package/dist/src/utils/callback.d.ts +0 -8
- package/dist/src/utils/checkRange.d.ts +0 -1
- package/dist/src/utils/crc.d.ts +0 -1
- package/dist/src/utils/index.d.ts +0 -11
- package/dist/src/utils/isUint8.d.ts +0 -8
- package/dist/src/utils/lrc.d.ts +0 -1
- package/dist/src/utils/predictRtuFrameLength.d.ts +0 -17
- package/dist/src/utils/promisify-cb.d.ts +0 -4
- package/dist/src/utils/rtu-timing.d.ts +0 -63
- package/dist/src/utils/whitelist.d.ts +0 -11
- package/dist/src/vars.d.ts +0 -49
package/dist/index.cjs
CHANGED
|
@@ -91,6 +91,10 @@ exports.ConformityLevel = void 0;
|
|
|
91
91
|
})(exports.ConformityLevel || (exports.ConformityLevel = {}));
|
|
92
92
|
/** Shared empty Buffer to avoid repeated allocations. */
|
|
93
93
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
94
|
+
/** Shared no-op function to avoid repeated allocations. */
|
|
95
|
+
const NOOP = () => {
|
|
96
|
+
/* no-op */
|
|
97
|
+
};
|
|
94
98
|
/** Modbus V1.1b3 PDU quantity limits. */
|
|
95
99
|
const LIMITS = {
|
|
96
100
|
READ_COILS_MIN: 0x0001,
|
|
@@ -162,40 +166,75 @@ function bitsToMs(baudRate, bits) {
|
|
|
162
166
|
* open / close / destroy callbacks.
|
|
163
167
|
*/
|
|
164
168
|
function drainCbs(cbs, err) {
|
|
165
|
-
if (!cbs)
|
|
169
|
+
if (!cbs) {
|
|
166
170
|
return;
|
|
171
|
+
}
|
|
167
172
|
for (const cb of cbs) {
|
|
168
173
|
cb?.(err);
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
function inRange(n, [min, max]) {
|
|
173
|
-
return n >= min && n <= max;
|
|
174
|
-
}
|
|
175
|
-
function isRangeArray(range) {
|
|
176
|
-
return Array.isArray(range[0]);
|
|
177
|
-
}
|
|
178
177
|
function checkRange(value, range) {
|
|
179
178
|
if (!range || range.length === 0) {
|
|
180
179
|
return true;
|
|
181
180
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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) {
|
|
188
197
|
return true;
|
|
189
198
|
}
|
|
190
199
|
}
|
|
191
200
|
return false;
|
|
192
201
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
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;
|
|
196
235
|
}
|
|
197
236
|
|
|
198
|
-
const TABLE = [
|
|
237
|
+
const TABLE = new Uint16Array([
|
|
199
238
|
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
|
|
200
239
|
0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
|
|
201
240
|
0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
|
|
@@ -212,11 +251,11 @@ const TABLE = [
|
|
|
212
251
|
0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
|
|
213
252
|
0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
|
|
214
253
|
0x4040,
|
|
215
|
-
];
|
|
216
|
-
function crc(data, start
|
|
254
|
+
]);
|
|
255
|
+
function crc(data, start, end) {
|
|
217
256
|
let crc = 0xffff;
|
|
218
257
|
for (let index = start; index < end; index++) {
|
|
219
|
-
crc =
|
|
258
|
+
crc = TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
|
|
220
259
|
}
|
|
221
260
|
return crc;
|
|
222
261
|
}
|
|
@@ -229,110 +268,104 @@ function crc(data, start = 0, end = data.length) {
|
|
|
229
268
|
* Infinity, and out-of-range values uniformly.
|
|
230
269
|
*/
|
|
231
270
|
function isUint8(n) {
|
|
232
|
-
return
|
|
271
|
+
return (n & 0xff) === n;
|
|
233
272
|
}
|
|
234
273
|
|
|
235
|
-
function lrc(data) {
|
|
236
|
-
|
|
274
|
+
function lrc(data, start, end) {
|
|
275
|
+
let sum = 0;
|
|
276
|
+
for (let i = start; i < end; i++) {
|
|
277
|
+
sum += data[i];
|
|
278
|
+
}
|
|
279
|
+
return -sum & 0xff;
|
|
237
280
|
}
|
|
238
281
|
|
|
239
|
-
const REQUEST_FIXED_LENGTHS = {
|
|
240
|
-
[exports.FunctionCode.READ_COILS]: 8,
|
|
241
|
-
[exports.FunctionCode.READ_DISCRETE_INPUTS]: 8,
|
|
242
|
-
[exports.FunctionCode.READ_HOLDING_REGISTERS]: 8,
|
|
243
|
-
[exports.FunctionCode.READ_INPUT_REGISTERS]: 8,
|
|
244
|
-
[exports.FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
245
|
-
[exports.FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
246
|
-
[exports.FunctionCode.REPORT_SERVER_ID]: 4,
|
|
247
|
-
[exports.FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
248
|
-
[exports.FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
|
|
249
|
-
};
|
|
250
|
-
const REQUEST_BYTE_COUNT = {
|
|
251
|
-
[exports.FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
|
|
252
|
-
[exports.FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
|
|
253
|
-
[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
|
|
254
|
-
};
|
|
255
|
-
const RESPONSE_FIXED_LENGTHS = {
|
|
256
|
-
[exports.FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
257
|
-
[exports.FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
258
|
-
[exports.FunctionCode.WRITE_MULTIPLE_COILS]: 8,
|
|
259
|
-
[exports.FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
|
|
260
|
-
[exports.FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
261
|
-
};
|
|
262
|
-
const RESPONSE_BYTE_COUNT = {
|
|
263
|
-
[exports.FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
|
|
264
|
-
[exports.FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
|
|
265
|
-
[exports.FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
|
|
266
|
-
[exports.FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
|
|
267
|
-
[exports.FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
|
|
268
|
-
[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
|
|
269
|
-
};
|
|
270
|
-
/** Sentinel: caller needs to feed more bytes before length can be determined. */
|
|
271
282
|
const PREDICT_NEED_MORE = 0;
|
|
272
|
-
/** Sentinel: function code is not in the standard tables. */
|
|
273
283
|
const PREDICT_UNKNOWN = -1;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
const REQ_TABLE = new Int32Array(256);
|
|
285
|
+
const RES_TABLE = new Int32Array(256);
|
|
286
|
+
(function initTables() {
|
|
287
|
+
REQ_TABLE[exports.FunctionCode.READ_COILS] = 8;
|
|
288
|
+
REQ_TABLE[exports.FunctionCode.READ_DISCRETE_INPUTS] = 8;
|
|
289
|
+
REQ_TABLE[exports.FunctionCode.READ_HOLDING_REGISTERS] = 8;
|
|
290
|
+
REQ_TABLE[exports.FunctionCode.READ_INPUT_REGISTERS] = 8;
|
|
291
|
+
REQ_TABLE[exports.FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
292
|
+
REQ_TABLE[exports.FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
293
|
+
REQ_TABLE[exports.FunctionCode.REPORT_SERVER_ID] = 4;
|
|
294
|
+
REQ_TABLE[exports.FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
295
|
+
REQ_TABLE[exports.FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
|
|
296
|
+
REQ_TABLE[exports.FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
|
|
297
|
+
REQ_TABLE[exports.FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
|
|
298
|
+
REQ_TABLE[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
|
|
299
|
+
RES_TABLE[exports.FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
300
|
+
RES_TABLE[exports.FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
301
|
+
RES_TABLE[exports.FunctionCode.WRITE_MULTIPLE_COILS] = 8;
|
|
302
|
+
RES_TABLE[exports.FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
|
|
303
|
+
RES_TABLE[exports.FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
304
|
+
RES_TABLE[exports.FunctionCode.READ_COILS] = -517;
|
|
305
|
+
RES_TABLE[exports.FunctionCode.READ_DISCRETE_INPUTS] = -517;
|
|
306
|
+
RES_TABLE[exports.FunctionCode.READ_HOLDING_REGISTERS] = -517;
|
|
307
|
+
RES_TABLE[exports.FunctionCode.READ_INPUT_REGISTERS] = -517;
|
|
308
|
+
RES_TABLE[exports.FunctionCode.REPORT_SERVER_ID] = -517;
|
|
309
|
+
RES_TABLE[exports.FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
|
|
310
|
+
RES_TABLE[exports.FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
|
|
311
|
+
})();
|
|
312
|
+
function predictRtuFrameLength(buffer, start, end, isResponse) {
|
|
313
|
+
const len = end - start;
|
|
314
|
+
if (len < 2) {
|
|
288
315
|
return PREDICT_NEED_MORE;
|
|
289
316
|
}
|
|
290
|
-
const fc = buffer[1];
|
|
291
|
-
if (isResponse
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
317
|
+
const fc = buffer[start + 1];
|
|
318
|
+
if (isResponse) {
|
|
319
|
+
if ((fc & EXCEPTION_OFFSET) !== 0) {
|
|
320
|
+
return 5;
|
|
321
|
+
}
|
|
322
|
+
const val = RES_TABLE[fc];
|
|
323
|
+
if (val > 0) {
|
|
324
|
+
return val;
|
|
325
|
+
}
|
|
326
|
+
if (val < 0) {
|
|
327
|
+
if (val === -999) {
|
|
328
|
+
// FC 43 / MEI 14 response — inline to avoid function-call overhead on
|
|
329
|
+
// the framing hot path (even though this FC is uncommon).
|
|
330
|
+
if (end - start < 8) {
|
|
331
|
+
return PREDICT_NEED_MORE;
|
|
332
|
+
}
|
|
333
|
+
if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
|
|
334
|
+
return PREDICT_UNKNOWN;
|
|
335
|
+
}
|
|
336
|
+
const numObjs = buffer[start + 7];
|
|
337
|
+
let cursor = start + 8;
|
|
338
|
+
for (let i = 0; i < numObjs; i++) {
|
|
339
|
+
if (end < cursor + 2) {
|
|
340
|
+
return PREDICT_NEED_MORE;
|
|
341
|
+
}
|
|
342
|
+
cursor += 2 + buffer[cursor + 1];
|
|
343
|
+
}
|
|
344
|
+
return cursor - start + 2;
|
|
345
|
+
}
|
|
346
|
+
const decode = -val;
|
|
347
|
+
const offset = decode >> 8;
|
|
348
|
+
if (len <= offset) {
|
|
349
|
+
return PREDICT_NEED_MORE;
|
|
350
|
+
}
|
|
351
|
+
return (decode & 0xff) + buffer[start + offset];
|
|
302
352
|
}
|
|
303
|
-
return bc.extra + buffer[bc.offset];
|
|
304
|
-
}
|
|
305
|
-
if (isResponse && fc === exports.FunctionCode.READ_DEVICE_IDENTIFICATION) {
|
|
306
|
-
return predictFc43_14Response(buffer);
|
|
307
|
-
}
|
|
308
|
-
return PREDICT_UNKNOWN;
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
|
|
312
|
-
* response structure per Modbus V1.1b3 §6.21.
|
|
313
|
-
*
|
|
314
|
-
* Layout (after unit and fc):
|
|
315
|
-
* mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
|
|
316
|
-
* [objId(1) objLen(1) objData(objLen)] × numObjs
|
|
317
|
-
* CRC(2)
|
|
318
|
-
*/
|
|
319
|
-
function predictFc43_14Response(buffer) {
|
|
320
|
-
if (buffer.length < 8) {
|
|
321
|
-
return PREDICT_NEED_MORE;
|
|
322
|
-
}
|
|
323
|
-
if (buffer[2] !== MEI_READ_DEVICE_ID) {
|
|
324
|
-
return PREDICT_UNKNOWN;
|
|
325
353
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
354
|
+
else {
|
|
355
|
+
const val = REQ_TABLE[fc];
|
|
356
|
+
if (val > 0) {
|
|
357
|
+
return val;
|
|
358
|
+
}
|
|
359
|
+
if (val < 0) {
|
|
360
|
+
const decode = -val;
|
|
361
|
+
const offset = decode >> 8;
|
|
362
|
+
if (len <= offset) {
|
|
363
|
+
return PREDICT_NEED_MORE;
|
|
364
|
+
}
|
|
365
|
+
return (decode & 0xff) + buffer[start + offset];
|
|
331
366
|
}
|
|
332
|
-
const objLen = buffer[offset + 1];
|
|
333
|
-
offset += 2 + objLen;
|
|
334
367
|
}
|
|
335
|
-
return
|
|
368
|
+
return PREDICT_UNKNOWN;
|
|
336
369
|
}
|
|
337
370
|
|
|
338
371
|
/**
|
|
@@ -341,10 +374,12 @@ function predictFc43_14Response(buffer) {
|
|
|
341
374
|
function promisifyCb(fn) {
|
|
342
375
|
return new Promise((resolve, reject) => {
|
|
343
376
|
fn((err) => {
|
|
344
|
-
if (err)
|
|
377
|
+
if (err) {
|
|
345
378
|
reject(err);
|
|
346
|
-
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
347
381
|
resolve();
|
|
382
|
+
}
|
|
348
383
|
});
|
|
349
384
|
});
|
|
350
385
|
}
|
|
@@ -397,6 +432,177 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
397
432
|
return { intervalBetweenFrames, interCharTimeout };
|
|
398
433
|
}
|
|
399
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Hybrid timer manager: uses native `setTimeout` for low concurrency
|
|
437
|
+
* and switches to a binary min-heap when concurrency exceeds the threshold.
|
|
438
|
+
*
|
|
439
|
+
* Benchmarks (add + clear throughput, Node 24, x64):
|
|
440
|
+
* 1 concurrent: setTimeout ~1.7× faster than heap
|
|
441
|
+
* 2 concurrent: setTimeout ~1.6× faster than heap
|
|
442
|
+
* 5 concurrent: setTimeout ~1.5-1.9× faster than heap
|
|
443
|
+
* 10 concurrent: roughly equal
|
|
444
|
+
* 20 concurrent: heap ~1.3× faster than setTimeout[]
|
|
445
|
+
* 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
|
|
446
|
+
*
|
|
447
|
+
* The crossover point is around 10 concurrent timers, so the default
|
|
448
|
+
* `concurrentThreshold = 2` keeps the common 1-2 request case on the
|
|
449
|
+
* fast direct path while delegating to the heap for larger batches.
|
|
450
|
+
*/
|
|
451
|
+
class TimerHeap {
|
|
452
|
+
_deadlines = [];
|
|
453
|
+
_ids = [];
|
|
454
|
+
_seqs = [];
|
|
455
|
+
_counter = 0;
|
|
456
|
+
_timer = null;
|
|
457
|
+
_onFire;
|
|
458
|
+
_boundTick;
|
|
459
|
+
_threshold;
|
|
460
|
+
_mode = 'direct';
|
|
461
|
+
_directTimers = new Map();
|
|
462
|
+
/**
|
|
463
|
+
* @param onFire Callback invoked with the timer id when it expires.
|
|
464
|
+
* @param concurrentThreshold Maximum number of timers kept as individual
|
|
465
|
+
* native `setTimeout` handles. Once exceeded, all timers migrate to
|
|
466
|
+
* the internal heap and share a single native timer. Default is 2.
|
|
467
|
+
*/
|
|
468
|
+
constructor(onFire, concurrentThreshold = 2) {
|
|
469
|
+
this._onFire = onFire;
|
|
470
|
+
this._boundTick = this._onTick.bind(this);
|
|
471
|
+
this._threshold = concurrentThreshold;
|
|
472
|
+
}
|
|
473
|
+
get size() {
|
|
474
|
+
return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
|
|
475
|
+
}
|
|
476
|
+
add(id, ms) {
|
|
477
|
+
if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
|
|
478
|
+
const deadline = performance.now() + ms;
|
|
479
|
+
const handle = setTimeout(() => {
|
|
480
|
+
if (this._mode !== 'direct') {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
this._directTimers.delete(id);
|
|
484
|
+
this._onFire(id);
|
|
485
|
+
}, ms);
|
|
486
|
+
this._directTimers.set(id, { handle, deadline });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (this._mode === 'direct') {
|
|
490
|
+
this._mode = 'heap';
|
|
491
|
+
for (const [existingId, { handle, deadline }] of this._directTimers) {
|
|
492
|
+
clearTimeout(handle);
|
|
493
|
+
const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
|
|
494
|
+
if (remaining === 0) {
|
|
495
|
+
this._onFire(existingId);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
this._heapAdd(existingId, remaining);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
this._directTimers.clear();
|
|
502
|
+
}
|
|
503
|
+
this._heapAdd(id, ms);
|
|
504
|
+
}
|
|
505
|
+
clear() {
|
|
506
|
+
for (const { handle } of this._directTimers.values()) {
|
|
507
|
+
clearTimeout(handle);
|
|
508
|
+
}
|
|
509
|
+
this._directTimers.clear();
|
|
510
|
+
this._mode = 'direct';
|
|
511
|
+
if (this._timer) {
|
|
512
|
+
clearTimeout(this._timer);
|
|
513
|
+
this._timer = null;
|
|
514
|
+
}
|
|
515
|
+
this._deadlines.length = 0;
|
|
516
|
+
this._ids.length = 0;
|
|
517
|
+
this._seqs.length = 0;
|
|
518
|
+
this._counter = 0;
|
|
519
|
+
}
|
|
520
|
+
_heapAdd(id, ms) {
|
|
521
|
+
const deadline = performance.now() + ms;
|
|
522
|
+
const seq = this._counter++;
|
|
523
|
+
let i = this._deadlines.length;
|
|
524
|
+
this._deadlines.push(deadline);
|
|
525
|
+
this._ids.push(id);
|
|
526
|
+
this._seqs.push(seq);
|
|
527
|
+
while (i > 0) {
|
|
528
|
+
const p = (i - 1) >> 1;
|
|
529
|
+
const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
|
|
530
|
+
if (parentComesFirst) {
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
this._deadlines[i] = this._deadlines[p];
|
|
534
|
+
this._ids[i] = this._ids[p];
|
|
535
|
+
this._seqs[i] = this._seqs[p];
|
|
536
|
+
i = p;
|
|
537
|
+
}
|
|
538
|
+
this._deadlines[i] = deadline;
|
|
539
|
+
this._ids[i] = id;
|
|
540
|
+
this._seqs[i] = seq;
|
|
541
|
+
if (i === 0) {
|
|
542
|
+
this._refresh();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
_refresh() {
|
|
546
|
+
if (this._timer) {
|
|
547
|
+
clearTimeout(this._timer);
|
|
548
|
+
this._timer = null;
|
|
549
|
+
}
|
|
550
|
+
if (this._deadlines.length === 0) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
|
|
554
|
+
const safeDelay = Math.min(delay, 2147483647);
|
|
555
|
+
this._timer = setTimeout(this._boundTick, safeDelay);
|
|
556
|
+
}
|
|
557
|
+
_onTick() {
|
|
558
|
+
this._timer = null;
|
|
559
|
+
const now = performance.now();
|
|
560
|
+
try {
|
|
561
|
+
while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
|
|
562
|
+
const id = this._pop();
|
|
563
|
+
this._onFire(id);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
this._refresh();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
_pop() {
|
|
571
|
+
const topId = this._ids[0];
|
|
572
|
+
const lastId = this._ids.pop();
|
|
573
|
+
const lastDeadline = this._deadlines.pop();
|
|
574
|
+
const lastSeq = this._seqs.pop();
|
|
575
|
+
const n = this._deadlines.length;
|
|
576
|
+
if (n > 0) {
|
|
577
|
+
let i = 0;
|
|
578
|
+
const half = n >> 1;
|
|
579
|
+
while (i < half) {
|
|
580
|
+
let minChild = (i << 1) + 1;
|
|
581
|
+
const rightChild = minChild + 1;
|
|
582
|
+
if (rightChild < n) {
|
|
583
|
+
const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
|
|
584
|
+
(this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
|
|
585
|
+
if (rightComesFirst) {
|
|
586
|
+
minChild = rightChild;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
|
|
590
|
+
if (lastComesFirst) {
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
this._deadlines[i] = this._deadlines[minChild];
|
|
594
|
+
this._ids[i] = this._ids[minChild];
|
|
595
|
+
this._seqs[i] = this._seqs[minChild];
|
|
596
|
+
i = minChild;
|
|
597
|
+
}
|
|
598
|
+
this._deadlines[i] = lastDeadline;
|
|
599
|
+
this._ids[i] = lastId;
|
|
600
|
+
this._seqs[i] = lastSeq;
|
|
601
|
+
}
|
|
602
|
+
return topId;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
400
606
|
/**
|
|
401
607
|
* Normalize an IP address by stripping the IPv4-mapped IPv6 prefix.
|
|
402
608
|
* This ensures consistent comparison of addresses like `::ffff:192.168.1.1`.
|
|
@@ -1341,14 +1547,16 @@ function createPhysicalLayer(config) {
|
|
|
1341
1547
|
* established and discarded when the connection closes. Subclasses implement
|
|
1342
1548
|
* ASCII, RTU, or TCP framing rules.
|
|
1343
1549
|
*/
|
|
1344
|
-
class AbstractApplicationLayer
|
|
1550
|
+
class AbstractApplicationLayer {
|
|
1551
|
+
/** Called when a complete frame is decoded. Defaults to no-op. */
|
|
1552
|
+
onFraming = NOOP;
|
|
1553
|
+
/** Called when a framing error is detected. Defaults to no-op. */
|
|
1554
|
+
onFramingError = NOOP;
|
|
1345
1555
|
flush() {
|
|
1346
1556
|
// no-op — override in subclasses
|
|
1347
1557
|
}
|
|
1348
|
-
addCustomFunctionCode(cfc) {
|
|
1349
|
-
}
|
|
1350
|
-
removeCustomFunctionCode(fc) {
|
|
1351
|
-
}
|
|
1558
|
+
addCustomFunctionCode(cfc) { }
|
|
1559
|
+
removeCustomFunctionCode(fc) { }
|
|
1352
1560
|
}
|
|
1353
1561
|
|
|
1354
1562
|
const MAX_FRAME_LENGTH = 256;
|
|
@@ -1362,7 +1570,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1362
1570
|
_threePointFiveT;
|
|
1363
1571
|
_onePointFiveT;
|
|
1364
1572
|
_customFunctionCodes = new Map();
|
|
1365
|
-
|
|
1573
|
+
_cleanupCbs = [];
|
|
1366
1574
|
get connection() {
|
|
1367
1575
|
return this._connection;
|
|
1368
1576
|
}
|
|
@@ -1378,7 +1586,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1378
1586
|
const onData = (data) => {
|
|
1379
1587
|
const state = this._state;
|
|
1380
1588
|
if (state.t15Expired && state.end > state.start) {
|
|
1381
|
-
this.
|
|
1589
|
+
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1382
1590
|
state.start = 0;
|
|
1383
1591
|
state.end = 0;
|
|
1384
1592
|
}
|
|
@@ -1420,7 +1628,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1420
1628
|
// flushBuffer freed nothing — the entire pool is unparseable
|
|
1421
1629
|
// residue (typically a misconfigured poolSize for the wire's
|
|
1422
1630
|
// frame size). Hard reset; we cannot recover automatically.
|
|
1423
|
-
this.
|
|
1631
|
+
this.onFramingError(new Error('Frame buffer exhausted before complete frame received'));
|
|
1424
1632
|
currentState.start = 0;
|
|
1425
1633
|
currentState.end = 0;
|
|
1426
1634
|
currentState.t15Expired = false;
|
|
@@ -1456,16 +1664,16 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1456
1664
|
}
|
|
1457
1665
|
};
|
|
1458
1666
|
connection.on('data', onData);
|
|
1459
|
-
this.
|
|
1667
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1460
1668
|
const onClose = () => {
|
|
1461
|
-
for (const fn of this.
|
|
1669
|
+
for (const fn of this._cleanupCbs) {
|
|
1462
1670
|
fn();
|
|
1463
1671
|
}
|
|
1464
|
-
this.
|
|
1672
|
+
this._cleanupCbs.length = 0;
|
|
1465
1673
|
this.clearStateTimers();
|
|
1466
1674
|
};
|
|
1467
1675
|
connection.on('close', onClose);
|
|
1468
|
-
this.
|
|
1676
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1469
1677
|
}
|
|
1470
1678
|
clearStateTimers() {
|
|
1471
1679
|
const state = this._state;
|
|
@@ -1478,35 +1686,40 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1478
1686
|
state.interCharTimer = undefined;
|
|
1479
1687
|
}
|
|
1480
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Shared handler for every "frame is not yet complete" exit in `flushBuffer`.
|
|
1691
|
+
* Returns `true` when the caller should `return` (strict reset), `false` to
|
|
1692
|
+
* `break` the parse loop. Hot path never reaches here — only error/incomplete
|
|
1693
|
+
* edges. Extracted as a method so it is not recreated on every `flushBuffer`
|
|
1694
|
+
* call.
|
|
1695
|
+
*/
|
|
1696
|
+
_handleIncomplete(state, strict) {
|
|
1697
|
+
if (strict) {
|
|
1698
|
+
this.onFramingError(new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
|
|
1699
|
+
state.start = 0;
|
|
1700
|
+
state.end = 0;
|
|
1701
|
+
state.t15Expired = false;
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
if (state.t15Expired) {
|
|
1705
|
+
this.onFramingError(new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1706
|
+
state.start = 0;
|
|
1707
|
+
state.end = 0;
|
|
1708
|
+
state.t15Expired = false;
|
|
1709
|
+
}
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1481
1712
|
flushBuffer(strict) {
|
|
1482
1713
|
const state = this._state;
|
|
1483
1714
|
const isResponse = this.ROLE === 'MASTER';
|
|
1484
1715
|
const pool = state.pool;
|
|
1485
1716
|
const customFCs = this._customFunctionCodes;
|
|
1486
|
-
// Shared handler for every "frame is not yet complete" exit. Returns true
|
|
1487
|
-
// when the caller should `return` (strict reset), false to `break` the
|
|
1488
|
-
// parse loop. Hot path never reaches here — only error/incomplete edges.
|
|
1489
|
-
const handleIncomplete = () => {
|
|
1490
|
-
if (strict) {
|
|
1491
|
-
this.emit('framing-error', new Error(state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
|
|
1492
|
-
state.start = 0;
|
|
1493
|
-
state.end = 0;
|
|
1494
|
-
state.t15Expired = false;
|
|
1495
|
-
return true;
|
|
1496
|
-
}
|
|
1497
|
-
if (state.t15Expired) {
|
|
1498
|
-
this.emit('framing-error', new Error('Inter-character timeout (t1.5) exceeded'));
|
|
1499
|
-
state.start = 0;
|
|
1500
|
-
state.end = 0;
|
|
1501
|
-
state.t15Expired = false;
|
|
1502
|
-
}
|
|
1503
|
-
return false;
|
|
1504
|
-
};
|
|
1505
1717
|
while (state.end - state.start > 0) {
|
|
1506
1718
|
const available = state.end - state.start;
|
|
1507
1719
|
if (available < MIN_FRAME_LENGTH) {
|
|
1508
|
-
if (
|
|
1720
|
+
if (this._handleIncomplete(state, strict)) {
|
|
1509
1721
|
return;
|
|
1722
|
+
}
|
|
1510
1723
|
break;
|
|
1511
1724
|
}
|
|
1512
1725
|
const fc = pool[state.start + 1];
|
|
@@ -1514,7 +1727,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1514
1727
|
let expected;
|
|
1515
1728
|
if (cfc) {
|
|
1516
1729
|
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1517
|
-
const predicted = predictor(pool
|
|
1730
|
+
const predicted = predictor(pool, state.start, state.end);
|
|
1518
1731
|
// Normalize custom predictor's `null` to the std sentinel so both
|
|
1519
1732
|
// paths share the same NEED_MORE tail below.
|
|
1520
1733
|
expected = predicted ?? PREDICT_NEED_MORE;
|
|
@@ -1522,9 +1735,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1522
1735
|
else {
|
|
1523
1736
|
// Standard FC path. predictRtuFrameLength uses sentinel returns to
|
|
1524
1737
|
// avoid per-call object allocation on the decode hot path.
|
|
1525
|
-
expected = predictRtuFrameLength(pool
|
|
1738
|
+
expected = predictRtuFrameLength(pool, state.start, state.end, isResponse);
|
|
1526
1739
|
if (expected === PREDICT_UNKNOWN) {
|
|
1527
|
-
this.
|
|
1740
|
+
this.onFramingError(new Error(`Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`));
|
|
1528
1741
|
state.start = 0;
|
|
1529
1742
|
state.end = 0;
|
|
1530
1743
|
return;
|
|
@@ -1535,12 +1748,13 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1535
1748
|
state.start += 1;
|
|
1536
1749
|
continue;
|
|
1537
1750
|
}
|
|
1538
|
-
if (
|
|
1751
|
+
if (this._handleIncomplete(state, strict)) {
|
|
1539
1752
|
return;
|
|
1753
|
+
}
|
|
1540
1754
|
break;
|
|
1541
1755
|
}
|
|
1542
1756
|
if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
|
|
1543
|
-
this.
|
|
1757
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1544
1758
|
state.start = 0;
|
|
1545
1759
|
state.end = 0;
|
|
1546
1760
|
return;
|
|
@@ -1550,8 +1764,9 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1550
1764
|
state.start += 1;
|
|
1551
1765
|
continue;
|
|
1552
1766
|
}
|
|
1553
|
-
if (
|
|
1767
|
+
if (this._handleIncomplete(state, strict)) {
|
|
1554
1768
|
return;
|
|
1769
|
+
}
|
|
1555
1770
|
break;
|
|
1556
1771
|
}
|
|
1557
1772
|
// CRC check inline: no helper call, no subarray for the CRC body.
|
|
@@ -1561,7 +1776,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1561
1776
|
const actualCrc = crc(pool, crcStart, crcEnd);
|
|
1562
1777
|
if (expectedCrc !== actualCrc) {
|
|
1563
1778
|
if (strict) {
|
|
1564
|
-
this.
|
|
1779
|
+
this.onFramingError(new Error('CRC mismatch'));
|
|
1565
1780
|
state.start = 0;
|
|
1566
1781
|
state.end = 0;
|
|
1567
1782
|
state.t15Expired = false;
|
|
@@ -1582,7 +1797,7 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1582
1797
|
data: frameBuf.subarray(2, expected - 2),
|
|
1583
1798
|
buffer: frameBuf,
|
|
1584
1799
|
};
|
|
1585
|
-
this.
|
|
1800
|
+
this.onFraming(frame);
|
|
1586
1801
|
}
|
|
1587
1802
|
if (state.start > 0) {
|
|
1588
1803
|
if (state.start < state.end) {
|
|
@@ -1609,11 +1824,22 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
1609
1824
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1610
1825
|
encode(unit, fc, data, transaction) {
|
|
1611
1826
|
const buffer = Buffer.allocUnsafe(data.length + 4);
|
|
1612
|
-
|
|
1613
|
-
buffer
|
|
1614
|
-
buffer
|
|
1827
|
+
// Inline header — direct typed-array stores skip Buffer's per-call checks.
|
|
1828
|
+
buffer[0] = unit;
|
|
1829
|
+
buffer[1] = fc;
|
|
1830
|
+
if (data.length <= 16) {
|
|
1831
|
+
for (let i = 0; i < data.length; i++) {
|
|
1832
|
+
buffer[2 + i] = data[i];
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
else {
|
|
1836
|
+
buffer.set(data, 2);
|
|
1837
|
+
}
|
|
1615
1838
|
const crcEnd = buffer.length - 2;
|
|
1616
|
-
|
|
1839
|
+
const c = crc(buffer, 0, crcEnd);
|
|
1840
|
+
// Little-endian inline write of CRC trailer.
|
|
1841
|
+
buffer[crcEnd] = c & 0xff;
|
|
1842
|
+
buffer[crcEnd + 1] = (c >>> 8) & 0xff;
|
|
1617
1843
|
return buffer;
|
|
1618
1844
|
}
|
|
1619
1845
|
}
|
|
@@ -1623,9 +1849,8 @@ const CHAR_CODE = {
|
|
|
1623
1849
|
CR: '\r'.charCodeAt(0),
|
|
1624
1850
|
LF: '\n'.charCodeAt(0),
|
|
1625
1851
|
};
|
|
1626
|
-
// Modbus ASCII frame
|
|
1627
|
-
//
|
|
1628
|
-
// cannot grow `state.frame` without bound.
|
|
1852
|
+
// Modbus ASCII frame body is capped well below the theoretical maximum so a
|
|
1853
|
+
// peer that never sends `\r` cannot grow `state.frame` without bound.
|
|
1629
1854
|
const MAX_ASCII_PAYLOAD = 512;
|
|
1630
1855
|
const HEX_DECODE = new Uint8Array(256);
|
|
1631
1856
|
HEX_DECODE.fill(0xff);
|
|
@@ -1644,8 +1869,8 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1644
1869
|
ROLE;
|
|
1645
1870
|
lenientHex;
|
|
1646
1871
|
_connection;
|
|
1647
|
-
_state = { status: 'idle', frame:
|
|
1648
|
-
|
|
1872
|
+
_state = { status: 'idle', frame: new Uint8Array(MAX_ASCII_PAYLOAD), frameLen: 0 };
|
|
1873
|
+
_cleanupCbs = [];
|
|
1649
1874
|
get connection() {
|
|
1650
1875
|
return this._connection;
|
|
1651
1876
|
}
|
|
@@ -1669,114 +1894,148 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
1669
1894
|
};
|
|
1670
1895
|
const onData = (data) => {
|
|
1671
1896
|
const state = this._state;
|
|
1672
|
-
data.
|
|
1897
|
+
for (let i = 0; i < data.length; i++) {
|
|
1898
|
+
const value = data[i];
|
|
1673
1899
|
switch (state.status) {
|
|
1674
1900
|
case 'idle': {
|
|
1675
1901
|
if (value === CHAR_CODE.COLON) {
|
|
1676
1902
|
state.status = 'reception';
|
|
1677
|
-
state.
|
|
1903
|
+
state.frameLen = 0;
|
|
1678
1904
|
}
|
|
1679
1905
|
break;
|
|
1680
1906
|
}
|
|
1681
1907
|
case 'reception': {
|
|
1682
1908
|
if (value === CHAR_CODE.COLON) {
|
|
1683
|
-
state.
|
|
1909
|
+
state.frameLen = 0;
|
|
1684
1910
|
}
|
|
1685
1911
|
else if (value === CHAR_CODE.CR) {
|
|
1686
1912
|
state.status = 'waiting end';
|
|
1687
1913
|
}
|
|
1688
|
-
else if (state.
|
|
1914
|
+
else if (state.frameLen >= MAX_ASCII_PAYLOAD) {
|
|
1689
1915
|
state.status = 'idle';
|
|
1690
|
-
state.
|
|
1691
|
-
this.
|
|
1916
|
+
state.frameLen = 0;
|
|
1917
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1692
1918
|
}
|
|
1693
1919
|
else if (!isHexChar(value)) {
|
|
1694
1920
|
state.status = 'idle';
|
|
1695
|
-
state.
|
|
1696
|
-
this.
|
|
1921
|
+
state.frameLen = 0;
|
|
1922
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1697
1923
|
}
|
|
1698
1924
|
else {
|
|
1699
|
-
state.frame.
|
|
1925
|
+
state.frame[state.frameLen++] = value;
|
|
1700
1926
|
}
|
|
1701
1927
|
break;
|
|
1702
1928
|
}
|
|
1703
1929
|
case 'waiting end': {
|
|
1704
1930
|
if (value === CHAR_CODE.COLON) {
|
|
1705
1931
|
state.status = 'reception';
|
|
1706
|
-
state.
|
|
1932
|
+
state.frameLen = 0;
|
|
1707
1933
|
}
|
|
1708
1934
|
else {
|
|
1709
1935
|
state.status = 'idle';
|
|
1710
1936
|
if (value === CHAR_CODE.LF) {
|
|
1711
|
-
this.framing(
|
|
1937
|
+
this.framing(state.frame, state.frameLen);
|
|
1712
1938
|
}
|
|
1713
1939
|
}
|
|
1714
1940
|
break;
|
|
1715
1941
|
}
|
|
1716
1942
|
}
|
|
1717
|
-
}
|
|
1943
|
+
}
|
|
1718
1944
|
};
|
|
1719
1945
|
connection.on('data', onData);
|
|
1720
|
-
this.
|
|
1946
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1721
1947
|
const onClose = () => {
|
|
1722
|
-
for (const fn of this.
|
|
1948
|
+
for (const fn of this._cleanupCbs) {
|
|
1723
1949
|
fn();
|
|
1724
1950
|
}
|
|
1725
|
-
this.
|
|
1951
|
+
this._cleanupCbs.length = 0;
|
|
1726
1952
|
};
|
|
1727
1953
|
connection.on('close', onClose);
|
|
1728
|
-
this.
|
|
1954
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1729
1955
|
}
|
|
1730
|
-
framing(hexChars) {
|
|
1731
|
-
if (
|
|
1732
|
-
this.
|
|
1956
|
+
framing(hexChars, hexLen) {
|
|
1957
|
+
if (hexLen < 6) {
|
|
1958
|
+
this.onFramingError(new Error('Insufficient data length'));
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
if (hexLen % 2 !== 0) {
|
|
1962
|
+
this.onFramingError(new Error('Invalid data'));
|
|
1733
1963
|
return;
|
|
1734
1964
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1965
|
+
const byteLen = hexLen >> 1;
|
|
1966
|
+
// Decode unit and fc directly from the first 4 hex characters —
|
|
1967
|
+
// avoids allocating a full decoded buffer just to read two bytes.
|
|
1968
|
+
const unitHi = HEX_DECODE[hexChars[0]];
|
|
1969
|
+
const unitLo = HEX_DECODE[hexChars[1]];
|
|
1970
|
+
const fcHi = HEX_DECODE[hexChars[2]];
|
|
1971
|
+
const fcLo = HEX_DECODE[hexChars[3]];
|
|
1972
|
+
if (unitHi === 0xff || unitLo === 0xff || fcHi === 0xff || fcLo === 0xff) {
|
|
1973
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1737
1974
|
return;
|
|
1738
1975
|
}
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1976
|
+
const unit = (unitHi << 4) | unitLo;
|
|
1977
|
+
const fc = (fcHi << 4) | fcLo;
|
|
1978
|
+
// Decode LRC from the last 2 hex characters.
|
|
1979
|
+
const lrcHi = HEX_DECODE[hexChars[hexLen - 2]];
|
|
1980
|
+
const lrcLo = HEX_DECODE[hexChars[hexLen - 1]];
|
|
1981
|
+
if (lrcHi === 0xff || lrcLo === 0xff) {
|
|
1982
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
const lrcIn = (lrcHi << 4) | lrcLo;
|
|
1986
|
+
// Decode data portion (between unit/fc and lrc) into a right-sized buffer.
|
|
1987
|
+
// dataLen may be 0 for a frame that is only unit + fc + lrc.
|
|
1988
|
+
const dataLen = byteLen - 3;
|
|
1989
|
+
const data = Buffer.allocUnsafe(dataLen);
|
|
1990
|
+
let hexOff = 4;
|
|
1991
|
+
for (let i = 0; i < dataLen; i++) {
|
|
1992
|
+
const hi = HEX_DECODE[hexChars[hexOff]];
|
|
1993
|
+
const lo = HEX_DECODE[hexChars[hexOff + 1]];
|
|
1745
1994
|
if (hi === 0xff || lo === 0xff) {
|
|
1746
|
-
this.
|
|
1995
|
+
this.onFramingError(new Error('Invalid hex character'));
|
|
1747
1996
|
return;
|
|
1748
1997
|
}
|
|
1749
|
-
|
|
1998
|
+
data[i] = (hi << 4) | lo;
|
|
1999
|
+
hexOff += 2;
|
|
1750
2000
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
this.emit('framing-error', new Error('LRC check failed'));
|
|
2001
|
+
// Compute LRC over unit + fc + data.
|
|
2002
|
+
let sum = unit + fc;
|
|
2003
|
+
for (let i = 0; i < dataLen; i++) {
|
|
2004
|
+
sum += data[i];
|
|
2005
|
+
}
|
|
2006
|
+
const lrcComputed = (~sum + 1) & 0xff;
|
|
2007
|
+
if (lrcIn !== lrcComputed) {
|
|
2008
|
+
this.onFramingError(new Error('LRC check failed'));
|
|
1760
2009
|
return;
|
|
1761
2010
|
}
|
|
1762
|
-
|
|
2011
|
+
const frame = {
|
|
2012
|
+
unit,
|
|
2013
|
+
fc,
|
|
2014
|
+
data,
|
|
2015
|
+
buffer: Buffer.copyBytesFrom(hexChars, 0, hexLen),
|
|
2016
|
+
};
|
|
2017
|
+
this.onFraming(frame);
|
|
1763
2018
|
}
|
|
1764
2019
|
flush() {
|
|
1765
|
-
this._state =
|
|
2020
|
+
this._state.status = 'idle';
|
|
2021
|
+
this._state.frameLen = 0;
|
|
1766
2022
|
}
|
|
1767
2023
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1768
2024
|
encode(unit, fc, data, transaction) {
|
|
1769
2025
|
const buffer = Buffer.allocUnsafe(data.length + 3);
|
|
1770
|
-
|
|
1771
|
-
buffer
|
|
2026
|
+
// Inline header + LRC — direct typed-array stores skip Buffer's per-call checks.
|
|
2027
|
+
buffer[0] = unit;
|
|
2028
|
+
buffer[1] = fc;
|
|
1772
2029
|
buffer.set(data, 2);
|
|
1773
|
-
buffer.
|
|
2030
|
+
buffer[buffer.length - 1] = lrc(buffer, 0, buffer.length - 1);
|
|
1774
2031
|
const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
|
|
1775
2032
|
out[0] = CHAR_CODE.COLON;
|
|
2033
|
+
let outOff = 1;
|
|
1776
2034
|
for (let i = 0; i < buffer.length; i++) {
|
|
1777
2035
|
const byte = buffer[i];
|
|
1778
|
-
out[
|
|
1779
|
-
out[
|
|
2036
|
+
out[outOff] = HEX_ENCODE[byte >> 4];
|
|
2037
|
+
out[outOff + 1] = HEX_ENCODE[byte & 0x0f];
|
|
2038
|
+
outOff += 2;
|
|
1780
2039
|
}
|
|
1781
2040
|
out[out.length - 2] = CHAR_CODE.CR;
|
|
1782
2041
|
out[out.length - 1] = CHAR_CODE.LF;
|
|
@@ -1791,7 +2050,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1791
2050
|
_connection;
|
|
1792
2051
|
_transactionId = 1;
|
|
1793
2052
|
_buffer = EMPTY_BUFFER;
|
|
1794
|
-
|
|
2053
|
+
_cleanupCbs = [];
|
|
1795
2054
|
get connection() {
|
|
1796
2055
|
return this._connection;
|
|
1797
2056
|
}
|
|
@@ -1800,6 +2059,17 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1800
2059
|
this.ROLE = role;
|
|
1801
2060
|
this._connection = connection;
|
|
1802
2061
|
const onData = (data) => {
|
|
2062
|
+
// Fast path: _buffer is empty and data is a single, complete frame.
|
|
2063
|
+
// This avoids the tryExtract subarray allocations + while loop
|
|
2064
|
+
// for the overwhelmingly common case (one frame per TCP packet).
|
|
2065
|
+
if (this._buffer.length === 0 && data.length >= 8) {
|
|
2066
|
+
const length = (data[4] << 8) | data[5]; // inline BE read
|
|
2067
|
+
const total = 6 + length;
|
|
2068
|
+
if (data[2] === 0 && data[3] === 0 && total <= MAX_TCP_FRAME && length >= 2 && data.length === total) {
|
|
2069
|
+
this.processFrame(data);
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
1803
2073
|
let buffer = this._buffer;
|
|
1804
2074
|
if (buffer.length === 0) {
|
|
1805
2075
|
buffer = data;
|
|
@@ -1817,7 +2087,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1817
2087
|
break;
|
|
1818
2088
|
}
|
|
1819
2089
|
else {
|
|
1820
|
-
this.
|
|
2090
|
+
this.onFramingError(result.error);
|
|
1821
2091
|
buffer = EMPTY_BUFFER;
|
|
1822
2092
|
break;
|
|
1823
2093
|
}
|
|
@@ -1837,15 +2107,15 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1837
2107
|
}
|
|
1838
2108
|
};
|
|
1839
2109
|
connection.on('data', onData);
|
|
1840
|
-
this.
|
|
2110
|
+
this._cleanupCbs.push(() => connection.off('data', onData));
|
|
1841
2111
|
const onClose = () => {
|
|
1842
|
-
for (const fn of this.
|
|
2112
|
+
for (const fn of this._cleanupCbs) {
|
|
1843
2113
|
fn();
|
|
1844
2114
|
}
|
|
1845
|
-
this.
|
|
2115
|
+
this._cleanupCbs.length = 0;
|
|
1846
2116
|
};
|
|
1847
2117
|
connection.on('close', onClose);
|
|
1848
|
-
this.
|
|
2118
|
+
this._cleanupCbs.push(() => connection.off('close', onClose));
|
|
1849
2119
|
}
|
|
1850
2120
|
tryExtract(buffer) {
|
|
1851
2121
|
if (buffer.length < 8) {
|
|
@@ -1854,7 +2124,7 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1854
2124
|
if (buffer[2] !== 0 || buffer[3] !== 0) {
|
|
1855
2125
|
return { kind: 'error', error: new Error('Invalid data') };
|
|
1856
2126
|
}
|
|
1857
|
-
const length = buffer
|
|
2127
|
+
const length = (buffer[4] << 8) | buffer[5]; // inline BE read
|
|
1858
2128
|
const total = 6 + length;
|
|
1859
2129
|
if (total > MAX_TCP_FRAME || length < 2) {
|
|
1860
2130
|
return { kind: 'error', error: new Error('Invalid data') };
|
|
@@ -1862,29 +2132,48 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
1862
2132
|
if (buffer.length < total) {
|
|
1863
2133
|
return { kind: 'insufficient' };
|
|
1864
2134
|
}
|
|
1865
|
-
return { kind: 'frame', frame: buffer.subarray(0, total), rest: buffer.subarray(total) };
|
|
2135
|
+
return { kind: 'frame', frame: buffer.subarray(0, total), rest: total === buffer.length ? EMPTY_BUFFER : buffer.subarray(total) };
|
|
1866
2136
|
}
|
|
1867
2137
|
processFrame(buffer) {
|
|
1868
2138
|
const frame = {
|
|
1869
|
-
|
|
2139
|
+
// Inline 16-bit BE read — direct typed-array loads skip readUInt16BE's
|
|
2140
|
+
// argument coercion + bounds check. Symmetric to the header writes in
|
|
2141
|
+
// encode() below. Hits on every received TCP frame.
|
|
2142
|
+
transaction: (buffer[0] << 8) | buffer[1],
|
|
1870
2143
|
unit: buffer[6],
|
|
1871
2144
|
fc: buffer[7],
|
|
1872
2145
|
data: buffer.subarray(8),
|
|
1873
2146
|
buffer,
|
|
1874
2147
|
};
|
|
1875
|
-
this.
|
|
2148
|
+
this.onFraming(frame);
|
|
1876
2149
|
}
|
|
1877
2150
|
flush() {
|
|
1878
2151
|
this._buffer = EMPTY_BUFFER;
|
|
1879
2152
|
}
|
|
1880
2153
|
encode(unit, fc, data, transaction) {
|
|
1881
2154
|
const buffer = Buffer.allocUnsafe(data.length + 8);
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
buffer
|
|
1887
|
-
buffer
|
|
2155
|
+
// Inline big-endian header writes — direct typed-array stores skip the
|
|
2156
|
+
// argument validation + bounds checks that `writeUInt16BE/writeUInt8` run.
|
|
2157
|
+
const tid = transaction ?? this._transactionId;
|
|
2158
|
+
const len = data.length + 2;
|
|
2159
|
+
buffer[0] = (tid >>> 8) & 0xff;
|
|
2160
|
+
buffer[1] = tid & 0xff;
|
|
2161
|
+
buffer[2] = 0;
|
|
2162
|
+
buffer[3] = 0;
|
|
2163
|
+
buffer[4] = (len >>> 8) & 0xff;
|
|
2164
|
+
buffer[5] = len & 0xff;
|
|
2165
|
+
buffer[6] = unit;
|
|
2166
|
+
buffer[7] = fc;
|
|
2167
|
+
// Small-payload fast path: avoid C++ TypedArray.prototype.set boundary
|
|
2168
|
+
// crossing when the copy is just a few bytes (common for FC 3/4/6 requests).
|
|
2169
|
+
if (data.length <= 16) {
|
|
2170
|
+
for (let i = 0; i < data.length; i++) {
|
|
2171
|
+
buffer[8 + i] = data[i];
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
else {
|
|
2175
|
+
buffer.set(data, 8);
|
|
2176
|
+
}
|
|
1888
2177
|
if (transaction === undefined) {
|
|
1889
2178
|
this._transactionId = (this._transactionId + 1) % 65536 || 1;
|
|
1890
2179
|
}
|
|
@@ -1928,26 +2217,33 @@ class MasterSession {
|
|
|
1928
2217
|
}
|
|
1929
2218
|
|
|
1930
2219
|
function validateResponse(frame, unit, fc) {
|
|
1931
|
-
if (frame.unit !== unit || frame.fc !== fc)
|
|
2220
|
+
if (frame.unit !== unit || frame.fc !== fc) {
|
|
1932
2221
|
throw new Error('Invalid response');
|
|
2222
|
+
}
|
|
1933
2223
|
}
|
|
1934
2224
|
function validateByteCountResponse(frame, unit, fc, byteCount) {
|
|
1935
2225
|
validateResponse(frame, unit, fc);
|
|
1936
|
-
if (frame.data.length < 1 + byteCount)
|
|
2226
|
+
if (frame.data.length < 1 + byteCount) {
|
|
1937
2227
|
throw new Error('Insufficient data length');
|
|
1938
|
-
|
|
2228
|
+
}
|
|
2229
|
+
if (frame.data.length !== 1 + byteCount) {
|
|
1939
2230
|
throw new Error('Invalid response');
|
|
1940
|
-
|
|
2231
|
+
}
|
|
2232
|
+
if (frame.data[0] !== byteCount) {
|
|
1941
2233
|
throw new Error('Invalid response');
|
|
2234
|
+
}
|
|
1942
2235
|
}
|
|
1943
2236
|
function validateEchoResponse(frame, unit, fc, expected) {
|
|
1944
2237
|
validateResponse(frame, unit, fc);
|
|
1945
|
-
if (frame.data.length < expected.length)
|
|
2238
|
+
if (frame.data.length < expected.length) {
|
|
1946
2239
|
throw new Error('Insufficient data length');
|
|
1947
|
-
|
|
2240
|
+
}
|
|
2241
|
+
if (frame.data.length !== expected.length) {
|
|
1948
2242
|
throw new Error('Invalid response');
|
|
1949
|
-
|
|
2243
|
+
}
|
|
2244
|
+
if (!frame.data.equals(expected)) {
|
|
1950
2245
|
throw new Error('Invalid response');
|
|
2246
|
+
}
|
|
1951
2247
|
}
|
|
1952
2248
|
class ModbusMaster extends EventEmitter {
|
|
1953
2249
|
timeout;
|
|
@@ -1963,14 +2259,32 @@ class ModbusMaster extends EventEmitter {
|
|
|
1963
2259
|
_queueDatas = [];
|
|
1964
2260
|
_queueTimeouts = [];
|
|
1965
2261
|
_queueBroadcasts = [];
|
|
1966
|
-
|
|
1967
|
-
_queueRejects = [];
|
|
2262
|
+
_queueCallbacks = [];
|
|
1968
2263
|
_queueHead = 0;
|
|
1969
2264
|
_queueLen = 0;
|
|
1970
2265
|
_draining = false;
|
|
1971
2266
|
_nextTid = 1;
|
|
1972
2267
|
_cleanupFns = new Set();
|
|
1973
2268
|
_closePromise = null;
|
|
2269
|
+
_nextExchangeId = 1;
|
|
2270
|
+
// Global timer heap with lazy deletion — one native setTimeout for all requests.
|
|
2271
|
+
_pendingExchanges = new Map();
|
|
2272
|
+
_timerHeap = new TimerHeap((id) => {
|
|
2273
|
+
const pending = this._pendingExchanges.get(id);
|
|
2274
|
+
if (!pending) {
|
|
2275
|
+
return;
|
|
2276
|
+
} // lazy deletion: already handled
|
|
2277
|
+
pending.settled = true;
|
|
2278
|
+
this._pendingExchanges.delete(id);
|
|
2279
|
+
if (pending.sessionKey !== null) {
|
|
2280
|
+
this._masterSession.stop(pending.sessionKey);
|
|
2281
|
+
}
|
|
2282
|
+
const cb = pending.callback;
|
|
2283
|
+
if (cb) {
|
|
2284
|
+
pending.callback = null;
|
|
2285
|
+
cb(new Error('Timeout'));
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
1974
2288
|
get state() {
|
|
1975
2289
|
return this._physicalLayer.state;
|
|
1976
2290
|
}
|
|
@@ -1997,17 +2311,21 @@ class ModbusMaster extends EventEmitter {
|
|
|
1997
2311
|
for (const cfc of this._customFunctionCodes.values()) {
|
|
1998
2312
|
appLayer.addCustomFunctionCode(cfc);
|
|
1999
2313
|
}
|
|
2000
|
-
const cleanupFraming = () =>
|
|
2314
|
+
const cleanupFraming = () => {
|
|
2315
|
+
appLayer.onFraming = NOOP;
|
|
2316
|
+
};
|
|
2001
2317
|
const onFraming = (frame) => {
|
|
2002
2318
|
this._masterSession.handleFrame(frame);
|
|
2003
2319
|
};
|
|
2004
|
-
appLayer.
|
|
2320
|
+
appLayer.onFraming = onFraming;
|
|
2005
2321
|
this._cleanupFns.add(cleanupFraming);
|
|
2006
|
-
const cleanupFramingError = () =>
|
|
2322
|
+
const cleanupFramingError = () => {
|
|
2323
|
+
appLayer.onFramingError = NOOP;
|
|
2324
|
+
};
|
|
2007
2325
|
const onFramingError = (error) => {
|
|
2008
2326
|
this._masterSession.handleError(error);
|
|
2009
2327
|
};
|
|
2010
|
-
appLayer.
|
|
2328
|
+
appLayer.onFramingError = onFramingError;
|
|
2011
2329
|
this._cleanupFns.add(cleanupFramingError);
|
|
2012
2330
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2013
2331
|
const onClose = () => {
|
|
@@ -2063,31 +2381,23 @@ class ModbusMaster extends EventEmitter {
|
|
|
2063
2381
|
}
|
|
2064
2382
|
return new AsciiApplicationLayer('MASTER', connection, this._protocol.opts);
|
|
2065
2383
|
}
|
|
2066
|
-
|
|
2384
|
+
_send(unit, fc, data, timeout, broadcast, callback) {
|
|
2067
2385
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2068
|
-
|
|
2386
|
+
callback(new Error('Master is not open'));
|
|
2387
|
+
return;
|
|
2069
2388
|
}
|
|
2070
2389
|
if (this.concurrent) {
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
if (err)
|
|
2074
|
-
reject(err);
|
|
2075
|
-
else
|
|
2076
|
-
resolve(frame);
|
|
2077
|
-
});
|
|
2078
|
-
});
|
|
2390
|
+
this._exchange(unit, fc, data, timeout, broadcast, callback);
|
|
2391
|
+
return;
|
|
2079
2392
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
this._queueLen++;
|
|
2089
|
-
this._drain();
|
|
2090
|
-
});
|
|
2393
|
+
this._queueUnits.push(unit);
|
|
2394
|
+
this._queueFcs.push(fc);
|
|
2395
|
+
this._queueDatas.push(data);
|
|
2396
|
+
this._queueTimeouts.push(timeout);
|
|
2397
|
+
this._queueBroadcasts.push(broadcast);
|
|
2398
|
+
this._queueCallbacks.push(callback);
|
|
2399
|
+
this._queueLen++;
|
|
2400
|
+
this._drain();
|
|
2091
2401
|
}
|
|
2092
2402
|
_drain() {
|
|
2093
2403
|
if (this._draining || this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
@@ -2108,14 +2418,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
2108
2418
|
const data = this._queueDatas[h];
|
|
2109
2419
|
const timeout = this._queueTimeouts[h];
|
|
2110
2420
|
const broadcast = this._queueBroadcasts[h];
|
|
2111
|
-
const
|
|
2112
|
-
|
|
2113
|
-
// Drop references so the GC can reclaim data buffers and resolve/reject
|
|
2421
|
+
const callback = this._queueCallbacks[h];
|
|
2422
|
+
// Drop references so the GC can reclaim data buffers and callback
|
|
2114
2423
|
// closures while the rest of the queue is still draining. Primitives
|
|
2115
2424
|
// (unit/fc/timeout/broadcast) need no clearing.
|
|
2116
2425
|
this._queueDatas[h] = undefined;
|
|
2117
|
-
this.
|
|
2118
|
-
this._queueRejects[h] = undefined;
|
|
2426
|
+
this._queueCallbacks[h] = undefined;
|
|
2119
2427
|
this._queueHead = h + 1;
|
|
2120
2428
|
this._queueLen--;
|
|
2121
2429
|
if (this._queueLen === 0) {
|
|
@@ -2126,15 +2434,11 @@ class ModbusMaster extends EventEmitter {
|
|
|
2126
2434
|
this._queueDatas.length = 0;
|
|
2127
2435
|
this._queueTimeouts.length = 0;
|
|
2128
2436
|
this._queueBroadcasts.length = 0;
|
|
2129
|
-
this.
|
|
2130
|
-
this._queueRejects.length = 0;
|
|
2437
|
+
this._queueCallbacks.length = 0;
|
|
2131
2438
|
this._queueHead = 0;
|
|
2132
2439
|
}
|
|
2133
2440
|
this._exchange(unit, fc, data, timeout, broadcast, (err, frame) => {
|
|
2134
|
-
|
|
2135
|
-
reject(err);
|
|
2136
|
-
else
|
|
2137
|
-
resolve(frame);
|
|
2441
|
+
callback(err, frame);
|
|
2138
2442
|
this._processNext();
|
|
2139
2443
|
});
|
|
2140
2444
|
}
|
|
@@ -2154,30 +2458,39 @@ class ModbusMaster extends EventEmitter {
|
|
|
2154
2458
|
if (!this.concurrent) {
|
|
2155
2459
|
appLayer.flush();
|
|
2156
2460
|
}
|
|
2461
|
+
// Lazy-deletion timer architecture:
|
|
2462
|
+
// 1. Assign an exchangeId and register in _pendingExchanges.
|
|
2463
|
+
// 2. Push deadline into the global TimerHeap (one native setTimeout under
|
|
2464
|
+
// load; a fast direct-timer path is used when only 1-2 exchanges are
|
|
2465
|
+
// pending).
|
|
2466
|
+
// 3. When the response arrives, delete from Map — the heap entry is left
|
|
2467
|
+
// behind and silently discarded when it surfaces at the top (lazy deletion).
|
|
2468
|
+
const exchangeId = this._nextExchangeId++;
|
|
2469
|
+
const pending = { settled: false, callback, sessionKey: null };
|
|
2470
|
+
this._pendingExchanges.set(exchangeId, pending);
|
|
2157
2471
|
if (broadcast) {
|
|
2158
|
-
// Broadcast: no response expected. Skip the session entirely
|
|
2159
|
-
|
|
2160
|
-
// broadcast's stop() would clear the other request's slot/timer).
|
|
2161
|
-
let settled = false;
|
|
2162
|
-
const timer = setTimeout(() => {
|
|
2163
|
-
if (settled)
|
|
2164
|
-
return;
|
|
2165
|
-
settled = true;
|
|
2166
|
-
callback(new Error('Timeout'));
|
|
2167
|
-
}, timeout);
|
|
2472
|
+
// Broadcast: no response expected. Skip the session entirely.
|
|
2473
|
+
this._timerHeap.add(exchangeId, timeout);
|
|
2168
2474
|
connection.write(appLayer.encode(unit, fc, data), (writeErr) => {
|
|
2169
|
-
|
|
2475
|
+
const p = this._pendingExchanges.get(exchangeId);
|
|
2476
|
+
if (!p || p.settled) {
|
|
2170
2477
|
return;
|
|
2171
|
-
|
|
2172
|
-
|
|
2478
|
+
}
|
|
2479
|
+
const cb = p.callback;
|
|
2480
|
+
if (!cb) {
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
p.settled = true;
|
|
2484
|
+
p.callback = null;
|
|
2485
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2173
2486
|
if (writeErr) {
|
|
2174
|
-
|
|
2487
|
+
cb(writeErr);
|
|
2175
2488
|
}
|
|
2176
2489
|
else if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2177
|
-
|
|
2490
|
+
cb(new Error('Master is not open'));
|
|
2178
2491
|
}
|
|
2179
2492
|
else {
|
|
2180
|
-
|
|
2493
|
+
cb(null);
|
|
2181
2494
|
}
|
|
2182
2495
|
});
|
|
2183
2496
|
return;
|
|
@@ -2191,62 +2504,118 @@ class ModbusMaster extends EventEmitter {
|
|
|
2191
2504
|
}
|
|
2192
2505
|
const key = tid ?? FIFO_KEY;
|
|
2193
2506
|
const payload = appLayer.encode(unit, fc, data, tid);
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
// settled guard prevents double-invocation when timeout fires during write.
|
|
2197
|
-
let settled = false;
|
|
2198
|
-
const timer = setTimeout(() => {
|
|
2199
|
-
if (settled)
|
|
2200
|
-
return;
|
|
2201
|
-
settled = true;
|
|
2202
|
-
this._masterSession.stop(key);
|
|
2203
|
-
callback(new Error('Timeout'));
|
|
2204
|
-
}, timeout);
|
|
2507
|
+
pending.sessionKey = key;
|
|
2508
|
+
this._timerHeap.add(exchangeId, timeout);
|
|
2205
2509
|
connection.write(payload, (writeErr) => {
|
|
2510
|
+
const p = this._pendingExchanges.get(exchangeId);
|
|
2511
|
+
if (!p || p.settled) {
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2206
2514
|
if (writeErr) {
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
settled = true;
|
|
2210
|
-
callback
|
|
2515
|
+
const cb = p.callback;
|
|
2516
|
+
if (cb) {
|
|
2517
|
+
p.settled = true;
|
|
2518
|
+
p.callback = null;
|
|
2519
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2520
|
+
cb(writeErr);
|
|
2211
2521
|
}
|
|
2212
2522
|
return;
|
|
2213
2523
|
}
|
|
2214
2524
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
settled = true;
|
|
2218
|
-
callback
|
|
2525
|
+
const cb = p.callback;
|
|
2526
|
+
if (cb) {
|
|
2527
|
+
p.settled = true;
|
|
2528
|
+
p.callback = null;
|
|
2529
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2530
|
+
cb(new Error('Master is not open'));
|
|
2219
2531
|
}
|
|
2220
2532
|
return;
|
|
2221
2533
|
}
|
|
2222
2534
|
// Write succeeded — register in session for frame matching only.
|
|
2223
|
-
// Timeout is managed by the
|
|
2535
|
+
// Timeout is managed by the global timer heap above.
|
|
2224
2536
|
this._masterSession.start(key, (err, frame) => {
|
|
2225
|
-
|
|
2537
|
+
const p2 = this._pendingExchanges.get(exchangeId);
|
|
2538
|
+
if (!p2 || p2.settled) {
|
|
2226
2539
|
return;
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2540
|
+
}
|
|
2541
|
+
const cb = p2.callback;
|
|
2542
|
+
if (cb) {
|
|
2543
|
+
p2.settled = true;
|
|
2544
|
+
p2.callback = null;
|
|
2545
|
+
this._pendingExchanges.delete(exchangeId);
|
|
2546
|
+
cb(err, frame);
|
|
2547
|
+
}
|
|
2230
2548
|
});
|
|
2231
2549
|
});
|
|
2232
2550
|
}
|
|
2233
2551
|
writeFC1Or2(unit, fc, address, length, timeout) {
|
|
2234
2552
|
const byteCount = Math.ceil(length / 8);
|
|
2235
2553
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2554
|
+
// Inline big-endian writes — direct typed-array stores skip the argument
|
|
2555
|
+
// validation + bounds checks that `writeUInt16BE` runs on each call.
|
|
2556
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2557
|
+
bufferTx[1] = address & 0xff;
|
|
2558
|
+
bufferTx[2] = (length >>> 8) & 0xff;
|
|
2559
|
+
bufferTx[3] = length & 0xff;
|
|
2560
|
+
return new Promise((resolve, reject) => {
|
|
2561
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2562
|
+
if (err) {
|
|
2563
|
+
reject(err);
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
if (!frame) {
|
|
2567
|
+
resolve(undefined);
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
try {
|
|
2571
|
+
validateByteCountResponse(frame, unit, fc, byteCount);
|
|
2572
|
+
const data = new Array(length);
|
|
2573
|
+
let byteIdx = 1;
|
|
2574
|
+
let outIdx = 0;
|
|
2575
|
+
const fullBytes = length >> 3;
|
|
2576
|
+
for (let b = 0; b < fullBytes; b++) {
|
|
2577
|
+
const byte = frame.data[byteIdx++];
|
|
2578
|
+
data[outIdx++] = (byte & 0x01) > 0;
|
|
2579
|
+
data[outIdx++] = (byte & 0x02) > 0;
|
|
2580
|
+
data[outIdx++] = (byte & 0x04) > 0;
|
|
2581
|
+
data[outIdx++] = (byte & 0x08) > 0;
|
|
2582
|
+
data[outIdx++] = (byte & 0x10) > 0;
|
|
2583
|
+
data[outIdx++] = (byte & 0x20) > 0;
|
|
2584
|
+
data[outIdx++] = (byte & 0x40) > 0;
|
|
2585
|
+
data[outIdx++] = (byte & 0x80) > 0;
|
|
2586
|
+
}
|
|
2587
|
+
const rem = length & 7;
|
|
2588
|
+
if (rem) {
|
|
2589
|
+
const byte = frame.data[byteIdx];
|
|
2590
|
+
data[outIdx++] = (byte & 0x01) > 0;
|
|
2591
|
+
if (rem > 1) {
|
|
2592
|
+
data[outIdx++] = (byte & 0x02) > 0;
|
|
2593
|
+
}
|
|
2594
|
+
if (rem > 2) {
|
|
2595
|
+
data[outIdx++] = (byte & 0x04) > 0;
|
|
2596
|
+
}
|
|
2597
|
+
if (rem > 3) {
|
|
2598
|
+
data[outIdx++] = (byte & 0x08) > 0;
|
|
2599
|
+
}
|
|
2600
|
+
if (rem > 4) {
|
|
2601
|
+
data[outIdx++] = (byte & 0x10) > 0;
|
|
2602
|
+
}
|
|
2603
|
+
if (rem > 5) {
|
|
2604
|
+
data[outIdx++] = (byte & 0x20) > 0;
|
|
2605
|
+
}
|
|
2606
|
+
if (rem > 6) {
|
|
2607
|
+
data[outIdx++] = (byte & 0x40) > 0;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
// Mutate the frame in place rather than spread-copying — `frame` is freshly
|
|
2611
|
+
// allocated per request and not retained anywhere else.
|
|
2612
|
+
frame.data = data;
|
|
2613
|
+
resolve(frame);
|
|
2614
|
+
}
|
|
2615
|
+
catch (e) {
|
|
2616
|
+
reject(e);
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2250
2619
|
});
|
|
2251
2620
|
}
|
|
2252
2621
|
writeFC1;
|
|
@@ -2260,19 +2629,42 @@ class ModbusMaster extends EventEmitter {
|
|
|
2260
2629
|
writeFC3Or4(unit, fc, address, length, timeout) {
|
|
2261
2630
|
const byteCount = length * 2;
|
|
2262
2631
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2263
|
-
|
|
2264
|
-
bufferTx
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2632
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2633
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2634
|
+
bufferTx[1] = address & 0xff;
|
|
2635
|
+
bufferTx[2] = (length >>> 8) & 0xff;
|
|
2636
|
+
bufferTx[3] = length & 0xff;
|
|
2637
|
+
return new Promise((resolve, reject) => {
|
|
2638
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2639
|
+
if (err) {
|
|
2640
|
+
reject(err);
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
if (!frame) {
|
|
2644
|
+
resolve(undefined);
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
try {
|
|
2648
|
+
validateByteCountResponse(frame, unit, fc, byteCount);
|
|
2649
|
+
const bufferRx = frame.data.subarray(1);
|
|
2650
|
+
const data = new Array(length);
|
|
2651
|
+
// Inline big-endian read — `bufferRx[i]` is a direct typed-array
|
|
2652
|
+
// load, while `readUInt16BE` runs argument coercion + bounds check
|
|
2653
|
+
// on each call. Symmetric to the slave-side BE write inlining
|
|
2654
|
+
// in handleFC3/FC4. At length=125 (FC3 max) that's 250 saved
|
|
2655
|
+
// bounds-check pairs per response.
|
|
2656
|
+
let off = 0;
|
|
2657
|
+
for (let i = 0; i < length; i++) {
|
|
2658
|
+
data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
|
|
2659
|
+
off += 2;
|
|
2660
|
+
}
|
|
2661
|
+
frame.data = data;
|
|
2662
|
+
resolve(frame);
|
|
2663
|
+
}
|
|
2664
|
+
catch (e) {
|
|
2665
|
+
reject(e);
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2276
2668
|
});
|
|
2277
2669
|
}
|
|
2278
2670
|
writeFC3;
|
|
@@ -2287,28 +2679,61 @@ class ModbusMaster extends EventEmitter {
|
|
|
2287
2679
|
writeSingleCoil(unit, address, value, timeout = this.timeout) {
|
|
2288
2680
|
const fc = exports.FunctionCode.WRITE_SINGLE_COIL;
|
|
2289
2681
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2682
|
+
const coilValue = value ? COIL_ON : COIL_OFF;
|
|
2683
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2684
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2685
|
+
bufferTx[1] = address & 0xff;
|
|
2686
|
+
bufferTx[2] = (coilValue >>> 8) & 0xff;
|
|
2687
|
+
bufferTx[3] = coilValue & 0xff;
|
|
2688
|
+
return new Promise((resolve, reject) => {
|
|
2689
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2690
|
+
if (err) {
|
|
2691
|
+
reject(err);
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
if (!frame) {
|
|
2695
|
+
resolve(undefined);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
try {
|
|
2699
|
+
validateEchoResponse(frame, unit, fc, bufferTx);
|
|
2700
|
+
frame.data = value;
|
|
2701
|
+
resolve(frame);
|
|
2702
|
+
}
|
|
2703
|
+
catch (e) {
|
|
2704
|
+
reject(e);
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2298
2707
|
});
|
|
2299
2708
|
}
|
|
2300
2709
|
writeFC6;
|
|
2301
2710
|
writeSingleRegister(unit, address, value, timeout = this.timeout) {
|
|
2302
2711
|
const fc = exports.FunctionCode.WRITE_SINGLE_REGISTER;
|
|
2303
2712
|
const bufferTx = Buffer.allocUnsafe(4);
|
|
2304
|
-
|
|
2305
|
-
bufferTx
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2713
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2714
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2715
|
+
bufferTx[1] = address & 0xff;
|
|
2716
|
+
bufferTx[2] = (value >>> 8) & 0xff;
|
|
2717
|
+
bufferTx[3] = value & 0xff;
|
|
2718
|
+
return new Promise((resolve, reject) => {
|
|
2719
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2720
|
+
if (err) {
|
|
2721
|
+
reject(err);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
if (!frame) {
|
|
2725
|
+
resolve(undefined);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
try {
|
|
2729
|
+
validateEchoResponse(frame, unit, fc, bufferTx);
|
|
2730
|
+
frame.data = value;
|
|
2731
|
+
resolve(frame);
|
|
2732
|
+
}
|
|
2733
|
+
catch (e) {
|
|
2734
|
+
reject(e);
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2312
2737
|
});
|
|
2313
2738
|
}
|
|
2314
2739
|
writeFC15;
|
|
@@ -2316,20 +2741,45 @@ class ModbusMaster extends EventEmitter {
|
|
|
2316
2741
|
const fc = exports.FunctionCode.WRITE_MULTIPLE_COILS;
|
|
2317
2742
|
const byteCount = Math.ceil(value.length / 8);
|
|
2318
2743
|
const bufferTx = Buffer.alloc(5 + byteCount);
|
|
2319
|
-
|
|
2320
|
-
bufferTx
|
|
2321
|
-
bufferTx
|
|
2322
|
-
value.
|
|
2323
|
-
|
|
2324
|
-
|
|
2744
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2745
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2746
|
+
bufferTx[1] = address & 0xff;
|
|
2747
|
+
bufferTx[2] = (value.length >>> 8) & 0xff;
|
|
2748
|
+
bufferTx[3] = value.length & 0xff;
|
|
2749
|
+
bufferTx[4] = byteCount;
|
|
2750
|
+
let acc = 0;
|
|
2751
|
+
let out = 5;
|
|
2752
|
+
for (let i = 0; i < value.length; i++) {
|
|
2753
|
+
if (value[i]) {
|
|
2754
|
+
acc |= 1 << (i & 7);
|
|
2325
2755
|
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2756
|
+
if ((i & 7) === 7) {
|
|
2757
|
+
bufferTx[out++] = acc;
|
|
2758
|
+
acc = 0;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
if ((value.length & 7) !== 0) {
|
|
2762
|
+
bufferTx[out] = acc;
|
|
2763
|
+
}
|
|
2764
|
+
return new Promise((resolve, reject) => {
|
|
2765
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2766
|
+
if (err) {
|
|
2767
|
+
reject(err);
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
if (!frame) {
|
|
2771
|
+
resolve(undefined);
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
try {
|
|
2775
|
+
validateEchoResponse(frame, unit, fc, bufferTx.subarray(0, 4));
|
|
2776
|
+
frame.data = value;
|
|
2777
|
+
resolve(frame);
|
|
2778
|
+
}
|
|
2779
|
+
catch (e) {
|
|
2780
|
+
reject(e);
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2333
2783
|
});
|
|
2334
2784
|
}
|
|
2335
2785
|
writeFC16;
|
|
@@ -2337,53 +2787,105 @@ class ModbusMaster extends EventEmitter {
|
|
|
2337
2787
|
const fc = exports.FunctionCode.WRITE_MULTIPLE_REGISTERS;
|
|
2338
2788
|
const byteCount = value.length * 2;
|
|
2339
2789
|
const bufferTx = Buffer.allocUnsafe(5 + byteCount);
|
|
2340
|
-
|
|
2341
|
-
bufferTx
|
|
2342
|
-
bufferTx
|
|
2343
|
-
value.
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2790
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2791
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2792
|
+
bufferTx[1] = address & 0xff;
|
|
2793
|
+
bufferTx[2] = (value.length >>> 8) & 0xff;
|
|
2794
|
+
bufferTx[3] = value.length & 0xff;
|
|
2795
|
+
bufferTx[4] = byteCount;
|
|
2796
|
+
let off = 5;
|
|
2797
|
+
for (let i = 0; i < value.length; i++) {
|
|
2798
|
+
const v = value[i];
|
|
2799
|
+
bufferTx[off] = (v >>> 8) & 0xff;
|
|
2800
|
+
bufferTx[off + 1] = v & 0xff;
|
|
2801
|
+
off += 2;
|
|
2802
|
+
}
|
|
2803
|
+
return new Promise((resolve, reject) => {
|
|
2804
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2805
|
+
if (err) {
|
|
2806
|
+
reject(err);
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
if (!frame) {
|
|
2810
|
+
resolve(undefined);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
try {
|
|
2814
|
+
validateEchoResponse(frame, unit, fc, bufferTx.subarray(0, 4));
|
|
2815
|
+
frame.data = value;
|
|
2816
|
+
resolve(frame);
|
|
2817
|
+
}
|
|
2818
|
+
catch (e) {
|
|
2819
|
+
reject(e);
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2352
2822
|
});
|
|
2353
2823
|
}
|
|
2354
2824
|
handleFC17;
|
|
2355
2825
|
reportServerId(unit, serverIdLength = 1, timeout = this.timeout) {
|
|
2356
2826
|
const fc = exports.FunctionCode.REPORT_SERVER_ID;
|
|
2357
|
-
return
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2827
|
+
return new Promise((resolve, reject) => {
|
|
2828
|
+
this._send(unit, fc, EMPTY_BUFFER, timeout, unit === 0, (err, frame) => {
|
|
2829
|
+
if (err) {
|
|
2830
|
+
reject(err);
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
if (!frame) {
|
|
2834
|
+
resolve(undefined);
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
try {
|
|
2838
|
+
validateResponse(frame, unit, fc);
|
|
2839
|
+
if (frame.data.length < 2 + serverIdLength) {
|
|
2840
|
+
throw new Error('Insufficient data length');
|
|
2841
|
+
}
|
|
2842
|
+
if (frame.data.length !== 1 + frame.data[0]) {
|
|
2843
|
+
throw new Error('Invalid response');
|
|
2844
|
+
}
|
|
2845
|
+
const runStatusIndex = 1 + serverIdLength;
|
|
2846
|
+
frame.data = {
|
|
2847
|
+
serverId: Array.from(frame.data.subarray(1, runStatusIndex)),
|
|
2848
|
+
runIndicatorStatus: frame.data[runStatusIndex] === 0xff,
|
|
2849
|
+
additionalData: Array.from(frame.data.subarray(runStatusIndex + 1)),
|
|
2850
|
+
};
|
|
2851
|
+
resolve(frame);
|
|
2852
|
+
}
|
|
2853
|
+
catch (e) {
|
|
2854
|
+
reject(e);
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2372
2857
|
});
|
|
2373
2858
|
}
|
|
2374
2859
|
handleFC22;
|
|
2375
2860
|
maskWriteRegister(unit, address, andMask, orMask, timeout = this.timeout) {
|
|
2376
2861
|
const fc = exports.FunctionCode.MASK_WRITE_REGISTER;
|
|
2377
2862
|
const bufferTx = Buffer.allocUnsafe(6);
|
|
2378
|
-
|
|
2379
|
-
bufferTx
|
|
2380
|
-
bufferTx
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2863
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2864
|
+
bufferTx[0] = (address >>> 8) & 0xff;
|
|
2865
|
+
bufferTx[1] = address & 0xff;
|
|
2866
|
+
bufferTx[2] = (andMask >>> 8) & 0xff;
|
|
2867
|
+
bufferTx[3] = andMask & 0xff;
|
|
2868
|
+
bufferTx[4] = (orMask >>> 8) & 0xff;
|
|
2869
|
+
bufferTx[5] = orMask & 0xff;
|
|
2870
|
+
return new Promise((resolve, reject) => {
|
|
2871
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2872
|
+
if (err) {
|
|
2873
|
+
reject(err);
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
if (!frame) {
|
|
2877
|
+
resolve(undefined);
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
try {
|
|
2881
|
+
validateEchoResponse(frame, unit, fc, bufferTx);
|
|
2882
|
+
frame.data = { andMask, orMask };
|
|
2883
|
+
resolve(frame);
|
|
2884
|
+
}
|
|
2885
|
+
catch (e) {
|
|
2886
|
+
reject(e);
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2387
2889
|
});
|
|
2388
2890
|
}
|
|
2389
2891
|
handleFC23;
|
|
@@ -2392,71 +2894,121 @@ class ModbusMaster extends EventEmitter {
|
|
|
2392
2894
|
const byteCount = write.value.length * 2;
|
|
2393
2895
|
const readByteCount = read.length * 2;
|
|
2394
2896
|
const bufferTx = Buffer.allocUnsafe(9 + byteCount);
|
|
2395
|
-
|
|
2396
|
-
bufferTx
|
|
2397
|
-
bufferTx.
|
|
2398
|
-
bufferTx
|
|
2399
|
-
bufferTx.
|
|
2400
|
-
write.
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
const
|
|
2408
|
-
|
|
2409
|
-
|
|
2897
|
+
// Inline big-endian writes — see writeFC1Or2 for the rationale.
|
|
2898
|
+
bufferTx[0] = (read.address >>> 8) & 0xff;
|
|
2899
|
+
bufferTx[1] = read.address & 0xff;
|
|
2900
|
+
bufferTx[2] = (read.length >>> 8) & 0xff;
|
|
2901
|
+
bufferTx[3] = read.length & 0xff;
|
|
2902
|
+
bufferTx[4] = (write.address >>> 8) & 0xff;
|
|
2903
|
+
bufferTx[5] = write.address & 0xff;
|
|
2904
|
+
bufferTx[6] = (write.value.length >>> 8) & 0xff;
|
|
2905
|
+
bufferTx[7] = write.value.length & 0xff;
|
|
2906
|
+
bufferTx[8] = byteCount;
|
|
2907
|
+
let off = 9;
|
|
2908
|
+
for (let i = 0; i < write.value.length; i++) {
|
|
2909
|
+
const v = write.value[i];
|
|
2910
|
+
bufferTx[off] = (v >>> 8) & 0xff;
|
|
2911
|
+
bufferTx[off + 1] = v & 0xff;
|
|
2912
|
+
off += 2;
|
|
2913
|
+
}
|
|
2914
|
+
return new Promise((resolve, reject) => {
|
|
2915
|
+
this._send(unit, fc, bufferTx, timeout, unit === 0, (err, frame) => {
|
|
2916
|
+
if (err) {
|
|
2917
|
+
reject(err);
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
if (!frame) {
|
|
2921
|
+
resolve(undefined);
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
try {
|
|
2925
|
+
validateByteCountResponse(frame, unit, fc, readByteCount);
|
|
2926
|
+
const bufferRx = frame.data.subarray(1);
|
|
2927
|
+
// Dense pre-sized array + inline BE read — drops the Array.from
|
|
2928
|
+
// closure + N readUInt16BE bounds-check pairs. See writeFC3Or4
|
|
2929
|
+
// response handler for the same optimization.
|
|
2930
|
+
const data = new Array(read.length);
|
|
2931
|
+
let off = 0;
|
|
2932
|
+
for (let i = 0; i < read.length; i++) {
|
|
2933
|
+
data[i] = (bufferRx[off] << 8) | bufferRx[off + 1];
|
|
2934
|
+
off += 2;
|
|
2935
|
+
}
|
|
2936
|
+
frame.data = data;
|
|
2937
|
+
resolve(frame);
|
|
2938
|
+
}
|
|
2939
|
+
catch (e) {
|
|
2940
|
+
reject(e);
|
|
2941
|
+
}
|
|
2942
|
+
});
|
|
2410
2943
|
});
|
|
2411
2944
|
}
|
|
2412
2945
|
handleFC43_14;
|
|
2413
2946
|
readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout = this.timeout) {
|
|
2414
2947
|
const fc = exports.FunctionCode.READ_DEVICE_IDENTIFICATION;
|
|
2415
|
-
return
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
case 1: {
|
|
2430
|
-
object.push(v);
|
|
2431
|
-
break;
|
|
2948
|
+
return new Promise((resolve, reject) => {
|
|
2949
|
+
this._send(unit, fc, Buffer.from([MEI_READ_DEVICE_ID, readDeviceIDCode, objectId]), timeout, unit === 0, (err, frame) => {
|
|
2950
|
+
if (err) {
|
|
2951
|
+
reject(err);
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
if (!frame) {
|
|
2955
|
+
resolve(undefined);
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
try {
|
|
2959
|
+
validateResponse(frame, unit, fc);
|
|
2960
|
+
if (frame.data.length < 6) {
|
|
2961
|
+
throw new Error('Insufficient data length');
|
|
2432
2962
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
break;
|
|
2963
|
+
if (frame.data[0] !== MEI_READ_DEVICE_ID || frame.data[1] !== readDeviceIDCode) {
|
|
2964
|
+
throw new Error('Invalid response');
|
|
2436
2965
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2966
|
+
const objects = [];
|
|
2967
|
+
let object = [];
|
|
2968
|
+
let totalBytes = 0;
|
|
2969
|
+
for (const v of frame.data.subarray(6)) {
|
|
2970
|
+
switch (object.length) {
|
|
2971
|
+
case 0:
|
|
2972
|
+
case 1: {
|
|
2973
|
+
object.push(v);
|
|
2974
|
+
break;
|
|
2975
|
+
}
|
|
2976
|
+
case 2: {
|
|
2977
|
+
object.push([v]);
|
|
2978
|
+
break;
|
|
2979
|
+
}
|
|
2980
|
+
case 3: {
|
|
2981
|
+
object[2].push(v);
|
|
2982
|
+
if (object[1] === object[2].length) {
|
|
2983
|
+
objects.push({ id: object[0], value: Buffer.from(object[2]).toString() });
|
|
2984
|
+
totalBytes += 2 + object[1];
|
|
2985
|
+
object = [];
|
|
2986
|
+
}
|
|
2987
|
+
break;
|
|
2988
|
+
}
|
|
2989
|
+
default:
|
|
2990
|
+
break;
|
|
2443
2991
|
}
|
|
2444
|
-
break;
|
|
2445
2992
|
}
|
|
2993
|
+
if (objects.length !== frame.data[5]) {
|
|
2994
|
+
throw new Error('Invalid response');
|
|
2995
|
+
}
|
|
2996
|
+
if (frame.data.length !== 6 + totalBytes) {
|
|
2997
|
+
throw new Error('Invalid response');
|
|
2998
|
+
}
|
|
2999
|
+
frame.data = {
|
|
3000
|
+
readDeviceIDCode,
|
|
3001
|
+
conformityLevel: frame.data[2],
|
|
3002
|
+
moreFollows: frame.data[3] === 0xff,
|
|
3003
|
+
nextObjectId: frame.data[4],
|
|
3004
|
+
objects,
|
|
3005
|
+
};
|
|
3006
|
+
resolve(frame);
|
|
2446
3007
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
throw new Error('Invalid response');
|
|
2452
|
-
frame.data = {
|
|
2453
|
-
readDeviceIDCode,
|
|
2454
|
-
conformityLevel: frame.data[2],
|
|
2455
|
-
moreFollows: frame.data[3] === 0xff,
|
|
2456
|
-
nextObjectId: frame.data[4],
|
|
2457
|
-
objects,
|
|
2458
|
-
};
|
|
2459
|
-
return frame;
|
|
3008
|
+
catch (e) {
|
|
3009
|
+
reject(e);
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
2460
3012
|
});
|
|
2461
3013
|
}
|
|
2462
3014
|
addCustomFunctionCode(cfc) {
|
|
@@ -2469,15 +3021,28 @@ class ModbusMaster extends EventEmitter {
|
|
|
2469
3021
|
}
|
|
2470
3022
|
sendCustomFC(unit, fc, data, timeout = this.timeout) {
|
|
2471
3023
|
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
2472
|
-
return
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
3024
|
+
return new Promise((resolve, reject) => {
|
|
3025
|
+
this._send(unit, fc, payload, timeout, unit === 0, (err, frame) => {
|
|
3026
|
+
if (err) {
|
|
3027
|
+
reject(err);
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
if (!frame) {
|
|
3031
|
+
resolve(undefined);
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
try {
|
|
3035
|
+
validateResponse(frame, unit, fc);
|
|
3036
|
+
resolve(frame.data);
|
|
3037
|
+
}
|
|
3038
|
+
catch (e) {
|
|
3039
|
+
reject(e);
|
|
3040
|
+
}
|
|
3041
|
+
});
|
|
2477
3042
|
});
|
|
2478
3043
|
}
|
|
2479
3044
|
/**
|
|
2480
|
-
* Open the underlying physical layer and
|
|
3045
|
+
* Open the underlying physical layer and establish a connection.
|
|
2481
3046
|
*
|
|
2482
3047
|
* A `ModbusMaster` instance can only be opened once. Once {@link close}
|
|
2483
3048
|
* is called — explicitly or because the physical layer disconnected —
|
|
@@ -2499,9 +3064,25 @@ class ModbusMaster extends EventEmitter {
|
|
|
2499
3064
|
const end = this._queueHead + this._queueLen;
|
|
2500
3065
|
this._queueLen = 0;
|
|
2501
3066
|
for (let i = this._queueHead; i < end; i++) {
|
|
2502
|
-
this.
|
|
3067
|
+
this._queueCallbacks[i](rejectErr);
|
|
2503
3068
|
}
|
|
2504
3069
|
this._masterSession.stopAll(rejectErr);
|
|
3070
|
+
this._timerHeap.clear();
|
|
3071
|
+
// Settle any in-flight exchanges that weren't reached by stopAll
|
|
3072
|
+
// (broadcasts have no session waiter; non-broadcasts still in the
|
|
3073
|
+
// pre-write-window haven't registered in session yet).
|
|
3074
|
+
for (const pending of this._pendingExchanges.values()) {
|
|
3075
|
+
if (pending.settled) {
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
pending.settled = true;
|
|
3079
|
+
const cb = pending.callback;
|
|
3080
|
+
if (cb) {
|
|
3081
|
+
pending.callback = null;
|
|
3082
|
+
cb(rejectErr);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
this._pendingExchanges.clear();
|
|
2505
3086
|
let err = null;
|
|
2506
3087
|
try {
|
|
2507
3088
|
await promisifyCb((cb) => this._physicalLayer.close(cb));
|
|
@@ -2538,7 +3119,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
2538
3119
|
_protocol;
|
|
2539
3120
|
_appLayers = new Map();
|
|
2540
3121
|
_customFunctionCodes = new Map();
|
|
2541
|
-
|
|
3122
|
+
// Active interval locks. Typical length: 0 (no contention) — 1-2 (cross-
|
|
3123
|
+
// connection contention). Linear scan for overlap is sub-µs.
|
|
3124
|
+
_intervalLocks = [];
|
|
2542
3125
|
_cleanupFns = new Set();
|
|
2543
3126
|
_closePromise = null;
|
|
2544
3127
|
get state() {
|
|
@@ -2565,7 +3148,9 @@ class ModbusSlave extends EventEmitter {
|
|
|
2565
3148
|
for (const cfc of this._customFunctionCodes.values()) {
|
|
2566
3149
|
appLayer.addCustomFunctionCode(cfc);
|
|
2567
3150
|
}
|
|
2568
|
-
const cleanupFraming = () =>
|
|
3151
|
+
const cleanupFraming = () => {
|
|
3152
|
+
appLayer.onFraming = NOOP;
|
|
3153
|
+
};
|
|
2569
3154
|
const onFraming = (frame) => {
|
|
2570
3155
|
if (this._physicalLayer.state !== exports.PhysicalState.OPEN) {
|
|
2571
3156
|
return;
|
|
@@ -2580,7 +3165,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2580
3165
|
appLayerData.queues.frames.push(frame);
|
|
2581
3166
|
this._drain(appLayer, appLayerData.queues);
|
|
2582
3167
|
};
|
|
2583
|
-
appLayer.
|
|
3168
|
+
appLayer.onFraming = onFraming;
|
|
2584
3169
|
this._cleanupFns.add(cleanupFraming);
|
|
2585
3170
|
const cleanupClose = () => connection.off('close', onClose);
|
|
2586
3171
|
const onClose = () => {
|
|
@@ -2647,10 +3232,66 @@ class ModbusSlave extends EventEmitter {
|
|
|
2647
3232
|
const byteCount = (length + 7) >> 3;
|
|
2648
3233
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
2649
3234
|
pdu[0] = byteCount;
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
3235
|
+
if (coils instanceof Uint8Array) {
|
|
3236
|
+
// Branchless fast path — `coils[i]` is already 0/1, no boolean
|
|
3237
|
+
// coercion or conditional jumps. At max payload (2000 coils) this
|
|
3238
|
+
// avoids 2000 branch-predictor slots and boolean-to-number casts.
|
|
3239
|
+
let out = 1;
|
|
3240
|
+
const fullBytes = length >> 3;
|
|
3241
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
3242
|
+
const base = i << 3;
|
|
3243
|
+
pdu[out++] =
|
|
3244
|
+
(coils[base] & 1) |
|
|
3245
|
+
((coils[base + 1] & 1) << 1) |
|
|
3246
|
+
((coils[base + 2] & 1) << 2) |
|
|
3247
|
+
((coils[base + 3] & 1) << 3) |
|
|
3248
|
+
((coils[base + 4] & 1) << 4) |
|
|
3249
|
+
((coils[base + 5] & 1) << 5) |
|
|
3250
|
+
((coils[base + 6] & 1) << 6) |
|
|
3251
|
+
((coils[base + 7] & 1) << 7);
|
|
3252
|
+
}
|
|
3253
|
+
const rem = length & 7;
|
|
3254
|
+
if (rem) {
|
|
3255
|
+
const base = fullBytes << 3;
|
|
3256
|
+
let acc = coils[base] & 1;
|
|
3257
|
+
if (rem > 1) {
|
|
3258
|
+
acc |= (coils[base + 1] & 1) << 1;
|
|
3259
|
+
}
|
|
3260
|
+
if (rem > 2) {
|
|
3261
|
+
acc |= (coils[base + 2] & 1) << 2;
|
|
3262
|
+
}
|
|
3263
|
+
if (rem > 3) {
|
|
3264
|
+
acc |= (coils[base + 3] & 1) << 3;
|
|
3265
|
+
}
|
|
3266
|
+
if (rem > 4) {
|
|
3267
|
+
acc |= (coils[base + 4] & 1) << 4;
|
|
3268
|
+
}
|
|
3269
|
+
if (rem > 5) {
|
|
3270
|
+
acc |= (coils[base + 5] & 1) << 5;
|
|
3271
|
+
}
|
|
3272
|
+
if (rem > 6) {
|
|
3273
|
+
acc |= (coils[base + 6] & 1) << 6;
|
|
3274
|
+
}
|
|
3275
|
+
pdu[out] = acc;
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
else {
|
|
3279
|
+
// Fallback for boolean[] — accumulate into `acc` and write a full byte
|
|
3280
|
+
// once each lane is finished. Saves N `|=` read-modify-writes on the
|
|
3281
|
+
// output buffer.
|
|
3282
|
+
let acc = 0;
|
|
3283
|
+
let out = 1;
|
|
3284
|
+
for (let i = 0; i < length; i++) {
|
|
3285
|
+
if (coils[i]) {
|
|
3286
|
+
acc |= 1 << (i & 7);
|
|
3287
|
+
}
|
|
3288
|
+
if ((i & 7) === 7) {
|
|
3289
|
+
pdu[out++] = acc;
|
|
3290
|
+
acc = 0;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
if ((length & 7) !== 0) {
|
|
3294
|
+
pdu[out] = acc;
|
|
2654
3295
|
}
|
|
2655
3296
|
}
|
|
2656
3297
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
@@ -2683,10 +3324,60 @@ class ModbusSlave extends EventEmitter {
|
|
|
2683
3324
|
const byteCount = (length + 7) >> 3;
|
|
2684
3325
|
const pdu = Buffer.allocUnsafe(byteCount + 1);
|
|
2685
3326
|
pdu[0] = byteCount;
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
3327
|
+
if (discreteInputs instanceof Uint8Array) {
|
|
3328
|
+
let out = 1;
|
|
3329
|
+
const fullBytes = length >> 3;
|
|
3330
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
3331
|
+
const base = i << 3;
|
|
3332
|
+
pdu[out++] =
|
|
3333
|
+
(discreteInputs[base] & 1) |
|
|
3334
|
+
((discreteInputs[base + 1] & 1) << 1) |
|
|
3335
|
+
((discreteInputs[base + 2] & 1) << 2) |
|
|
3336
|
+
((discreteInputs[base + 3] & 1) << 3) |
|
|
3337
|
+
((discreteInputs[base + 4] & 1) << 4) |
|
|
3338
|
+
((discreteInputs[base + 5] & 1) << 5) |
|
|
3339
|
+
((discreteInputs[base + 6] & 1) << 6) |
|
|
3340
|
+
((discreteInputs[base + 7] & 1) << 7);
|
|
3341
|
+
}
|
|
3342
|
+
const rem = length & 7;
|
|
3343
|
+
if (rem) {
|
|
3344
|
+
const base = fullBytes << 3;
|
|
3345
|
+
let acc = discreteInputs[base] & 1;
|
|
3346
|
+
if (rem > 1) {
|
|
3347
|
+
acc |= (discreteInputs[base + 1] & 1) << 1;
|
|
3348
|
+
}
|
|
3349
|
+
if (rem > 2) {
|
|
3350
|
+
acc |= (discreteInputs[base + 2] & 1) << 2;
|
|
3351
|
+
}
|
|
3352
|
+
if (rem > 3) {
|
|
3353
|
+
acc |= (discreteInputs[base + 3] & 1) << 3;
|
|
3354
|
+
}
|
|
3355
|
+
if (rem > 4) {
|
|
3356
|
+
acc |= (discreteInputs[base + 4] & 1) << 4;
|
|
3357
|
+
}
|
|
3358
|
+
if (rem > 5) {
|
|
3359
|
+
acc |= (discreteInputs[base + 5] & 1) << 5;
|
|
3360
|
+
}
|
|
3361
|
+
if (rem > 6) {
|
|
3362
|
+
acc |= (discreteInputs[base + 6] & 1) << 6;
|
|
3363
|
+
}
|
|
3364
|
+
pdu[out] = acc;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
else {
|
|
3368
|
+
let acc = 0;
|
|
3369
|
+
let out = 1;
|
|
3370
|
+
for (let i = 0; i < length; i++) {
|
|
3371
|
+
if (discreteInputs[i]) {
|
|
3372
|
+
acc |= 1 << (i & 7);
|
|
3373
|
+
}
|
|
3374
|
+
if ((i & 7) === 7) {
|
|
3375
|
+
pdu[out++] = acc;
|
|
3376
|
+
acc = 0;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
if ((length & 7) !== 0) {
|
|
3380
|
+
pdu[out] = acc;
|
|
2690
3381
|
}
|
|
2691
3382
|
}
|
|
2692
3383
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
@@ -2718,8 +3409,15 @@ class ModbusSlave extends EventEmitter {
|
|
|
2718
3409
|
const registers = await model.readHoldingRegisters(address, length);
|
|
2719
3410
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
2720
3411
|
pdu[0] = length * 2;
|
|
3412
|
+
// Inline big-endian write — `pdu[i] = v` is a direct typed-array store,
|
|
3413
|
+
// while `writeUInt16BE` runs argument validation + bounds checks on each
|
|
3414
|
+
// call. At length=125 (FC3 max) that's 250 saved checks per request.
|
|
3415
|
+
let off = 1;
|
|
2721
3416
|
for (let i = 0; i < length; i++) {
|
|
2722
|
-
|
|
3417
|
+
const v = registers[i];
|
|
3418
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3419
|
+
pdu[off + 1] = v & 0xff;
|
|
3420
|
+
off += 2;
|
|
2723
3421
|
}
|
|
2724
3422
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2725
3423
|
}
|
|
@@ -2750,8 +3448,13 @@ class ModbusSlave extends EventEmitter {
|
|
|
2750
3448
|
const registers = await model.readInputRegisters(address, length);
|
|
2751
3449
|
const pdu = Buffer.allocUnsafe(length * 2 + 1);
|
|
2752
3450
|
pdu[0] = length * 2;
|
|
3451
|
+
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3452
|
+
let off = 1;
|
|
2753
3453
|
for (let i = 0; i < length; i++) {
|
|
2754
|
-
|
|
3454
|
+
const v = registers[i];
|
|
3455
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3456
|
+
pdu[off + 1] = v & 0xff;
|
|
3457
|
+
off += 2;
|
|
2755
3458
|
}
|
|
2756
3459
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
2757
3460
|
}
|
|
@@ -2830,8 +3533,42 @@ class ModbusSlave extends EventEmitter {
|
|
|
2830
3533
|
return;
|
|
2831
3534
|
}
|
|
2832
3535
|
const value = new Array(length);
|
|
2833
|
-
|
|
2834
|
-
|
|
3536
|
+
let byteIdx = 5;
|
|
3537
|
+
let outIdx = 0;
|
|
3538
|
+
const fullBytes = length >> 3;
|
|
3539
|
+
for (let b = 0; b < fullBytes; b++) {
|
|
3540
|
+
const byte = frame.data[byteIdx++];
|
|
3541
|
+
value[outIdx++] = (byte & 0x01) > 0;
|
|
3542
|
+
value[outIdx++] = (byte & 0x02) > 0;
|
|
3543
|
+
value[outIdx++] = (byte & 0x04) > 0;
|
|
3544
|
+
value[outIdx++] = (byte & 0x08) > 0;
|
|
3545
|
+
value[outIdx++] = (byte & 0x10) > 0;
|
|
3546
|
+
value[outIdx++] = (byte & 0x20) > 0;
|
|
3547
|
+
value[outIdx++] = (byte & 0x40) > 0;
|
|
3548
|
+
value[outIdx++] = (byte & 0x80) > 0;
|
|
3549
|
+
}
|
|
3550
|
+
const rem = length & 7;
|
|
3551
|
+
if (rem) {
|
|
3552
|
+
const byte = frame.data[byteIdx];
|
|
3553
|
+
value[outIdx++] = (byte & 0x01) > 0;
|
|
3554
|
+
if (rem > 1) {
|
|
3555
|
+
value[outIdx++] = (byte & 0x02) > 0;
|
|
3556
|
+
}
|
|
3557
|
+
if (rem > 2) {
|
|
3558
|
+
value[outIdx++] = (byte & 0x04) > 0;
|
|
3559
|
+
}
|
|
3560
|
+
if (rem > 3) {
|
|
3561
|
+
value[outIdx++] = (byte & 0x08) > 0;
|
|
3562
|
+
}
|
|
3563
|
+
if (rem > 4) {
|
|
3564
|
+
value[outIdx++] = (byte & 0x10) > 0;
|
|
3565
|
+
}
|
|
3566
|
+
if (rem > 5) {
|
|
3567
|
+
value[outIdx++] = (byte & 0x20) > 0;
|
|
3568
|
+
}
|
|
3569
|
+
if (rem > 6) {
|
|
3570
|
+
value[outIdx++] = (byte & 0x40) > 0;
|
|
3571
|
+
}
|
|
2835
3572
|
}
|
|
2836
3573
|
try {
|
|
2837
3574
|
if (model.writeMultipleCoils) {
|
|
@@ -2869,8 +3606,10 @@ class ModbusSlave extends EventEmitter {
|
|
|
2869
3606
|
return;
|
|
2870
3607
|
}
|
|
2871
3608
|
const value = new Array(length);
|
|
3609
|
+
let off = 5;
|
|
2872
3610
|
for (let i = 0; i < length; i++) {
|
|
2873
|
-
value[i] = (frame.data[
|
|
3611
|
+
value[i] = (frame.data[off] << 8) | frame.data[off + 1];
|
|
3612
|
+
off += 2;
|
|
2874
3613
|
}
|
|
2875
3614
|
try {
|
|
2876
3615
|
if (model.writeMultipleRegisters) {
|
|
@@ -2935,7 +3674,7 @@ class ModbusSlave extends EventEmitter {
|
|
|
2935
3674
|
return;
|
|
2936
3675
|
}
|
|
2937
3676
|
try {
|
|
2938
|
-
await this.
|
|
3677
|
+
await this._withIntervalLock(address, address + 1, async () => {
|
|
2939
3678
|
if (model.maskWriteRegister) {
|
|
2940
3679
|
await model.maskWriteRegister(address, andMask, orMask);
|
|
2941
3680
|
}
|
|
@@ -2981,12 +3720,13 @@ class ModbusSlave extends EventEmitter {
|
|
|
2981
3720
|
return;
|
|
2982
3721
|
}
|
|
2983
3722
|
const value = new Array(length.write);
|
|
3723
|
+
let off = 9;
|
|
2984
3724
|
for (let i = 0; i < length.write; i++) {
|
|
2985
|
-
value[i] = (frame.data[
|
|
3725
|
+
value[i] = (frame.data[off] << 8) | frame.data[off + 1];
|
|
3726
|
+
off += 2;
|
|
2986
3727
|
}
|
|
2987
3728
|
try {
|
|
2988
|
-
|
|
2989
|
-
await this._withAddressLock(writeAddresses, async () => {
|
|
3729
|
+
await this._withIntervalLock(address.write, address.write + length.write, async () => {
|
|
2990
3730
|
if (model.writeMultipleRegisters) {
|
|
2991
3731
|
await model.writeMultipleRegisters(address.write, value);
|
|
2992
3732
|
}
|
|
@@ -2999,8 +3739,13 @@ class ModbusSlave extends EventEmitter {
|
|
|
2999
3739
|
const registers = await model.readHoldingRegisters(address.read, length.read);
|
|
3000
3740
|
const pdu = Buffer.allocUnsafe(length.read * 2 + 1);
|
|
3001
3741
|
pdu[0] = length.read * 2;
|
|
3742
|
+
// Inline big-endian write — see handleFC3 for the rationale.
|
|
3743
|
+
let off = 1;
|
|
3002
3744
|
for (let i = 0; i < length.read; i++) {
|
|
3003
|
-
|
|
3745
|
+
const v = registers[i];
|
|
3746
|
+
pdu[off] = (v >>> 8) & 0xff;
|
|
3747
|
+
pdu[off + 1] = v & 0xff;
|
|
3748
|
+
off += 2;
|
|
3004
3749
|
}
|
|
3005
3750
|
await response(appLayer.encode(frame.unit, frame.fc, pdu, frame.transaction));
|
|
3006
3751
|
}
|
|
@@ -3159,7 +3904,11 @@ class ModbusSlave extends EventEmitter {
|
|
|
3159
3904
|
return Promise.resolve();
|
|
3160
3905
|
}
|
|
3161
3906
|
return new Promise((resolve) => {
|
|
3162
|
-
|
|
3907
|
+
// Pass `resolve` directly as the write cb (vs `() => resolve()`) —
|
|
3908
|
+
// saves one closure allocation per response. `resolve` may receive
|
|
3909
|
+
// the write's err arg, but our Promise<void> contract ignores any
|
|
3910
|
+
// resolved value and the response handler doesn't check success.
|
|
3911
|
+
appLayer.connection.write(data, resolve);
|
|
3163
3912
|
});
|
|
3164
3913
|
};
|
|
3165
3914
|
// Hot path: unicast to a known unit dispatches to a single model.
|
|
@@ -3208,24 +3957,68 @@ class ModbusSlave extends EventEmitter {
|
|
|
3208
3957
|
return true;
|
|
3209
3958
|
}
|
|
3210
3959
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3960
|
+
/**
|
|
3961
|
+
* Serialize a code block over the half-open address interval `[lo, hi)`.
|
|
3962
|
+
* The block runs after all previously-installed locks whose intervals
|
|
3963
|
+
* overlap with this one have completed. Two non-overlapping intervals
|
|
3964
|
+
* execute in parallel.
|
|
3965
|
+
*
|
|
3966
|
+
* Locks are tracked in a flat array (`_intervalLocks`); the typical depth
|
|
3967
|
+
* is 0-2 entries, so the linear overlap scan is sub-µs. Compare with the
|
|
3968
|
+
* old per-address Map design, where FC23 writing 121 registers allocated
|
|
3969
|
+
* ~125 objects per request (one Promise.resolve / address + Set + sort +
|
|
3970
|
+
* Promise.all); this version allocates 1-3.
|
|
3971
|
+
*/
|
|
3972
|
+
async _withIntervalLock(lo, hi, fn) {
|
|
3973
|
+
// Find overlapping active entries. Two half-open intervals [a, b) and
|
|
3974
|
+
// [c, d) overlap iff a < d && c < b.
|
|
3975
|
+
let overlap = null;
|
|
3976
|
+
const locks = this._intervalLocks;
|
|
3977
|
+
for (let i = 0; i < locks.length; i++) {
|
|
3978
|
+
const l = locks[i];
|
|
3979
|
+
if (l.lo < hi && lo < l.hi) {
|
|
3980
|
+
if (overlap === null) {
|
|
3981
|
+
overlap = [];
|
|
3982
|
+
}
|
|
3983
|
+
overlap.push(l.promise);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
// Install our entry BEFORE awaiting so any concurrent caller arriving
|
|
3987
|
+
// after this point sees us and waits. Promise field is filled in below.
|
|
3988
|
+
const entry = { lo, hi, promise: undefined };
|
|
3989
|
+
locks.push(entry);
|
|
3990
|
+
let work;
|
|
3991
|
+
if (overlap === null) {
|
|
3992
|
+
work = fn();
|
|
3993
|
+
}
|
|
3994
|
+
else if (overlap.length === 1) {
|
|
3995
|
+
// Skip Promise.all when there's exactly one prior — avoids one
|
|
3996
|
+
// intermediate Promise allocation in the most common contention case.
|
|
3997
|
+
// `.then(fn)` (vs `.then(() => fn())`) saves a closure: fn's signature
|
|
3998
|
+
// is () => Promise<T>, and the resolved value `.then` passes in is
|
|
3999
|
+
// silently discarded by JS's loose-arity rule.
|
|
4000
|
+
work = overlap[0].then(fn);
|
|
4001
|
+
}
|
|
4002
|
+
else {
|
|
4003
|
+
work = Promise.all(overlap).then(fn);
|
|
3220
4004
|
}
|
|
4005
|
+
// We never want the cleanup latch to reject — swallow errors so
|
|
4006
|
+
// downstream `.then` chains don't see an unhandled rejection. Using the
|
|
4007
|
+
// shared `NOOP` singleton avoids allocating a fresh arrow function on
|
|
4008
|
+
// every locked request.
|
|
4009
|
+
entry.promise = work.catch(NOOP);
|
|
3221
4010
|
try {
|
|
3222
4011
|
return await work;
|
|
3223
4012
|
}
|
|
3224
4013
|
finally {
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
4014
|
+
const i = locks.indexOf(entry);
|
|
4015
|
+
if (i !== -1) {
|
|
4016
|
+
// O(1) swap-and-pop since lock order doesn't matter for correctness.
|
|
4017
|
+
const last = locks.length - 1;
|
|
4018
|
+
if (i !== last) {
|
|
4019
|
+
locks[i] = locks[last];
|
|
3228
4020
|
}
|
|
4021
|
+
locks.pop();
|
|
3229
4022
|
}
|
|
3230
4023
|
}
|
|
3231
4024
|
}
|
|
@@ -3356,6 +4149,7 @@ exports.MasterSession = MasterSession;
|
|
|
3356
4149
|
exports.ModbusError = ModbusError;
|
|
3357
4150
|
exports.ModbusMaster = ModbusMaster;
|
|
3358
4151
|
exports.ModbusSlave = ModbusSlave;
|
|
4152
|
+
exports.NOOP = NOOP;
|
|
3359
4153
|
exports.RtuApplicationLayer = RtuApplicationLayer;
|
|
3360
4154
|
exports.SerialPhysicalLayer = SerialPhysicalLayer;
|
|
3361
4155
|
exports.TcpApplicationLayer = TcpApplicationLayer;
|