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