njs-modbus 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -110
- package/README.zh-CN.md +185 -109
- package/dist/index.cjs +1084 -588
- package/dist/index.d.ts +92 -61
- package/dist/index.mjs +1084 -588
- package/dist/utils.cjs +293 -168
- package/dist/utils.d.ts +43 -40
- package/dist/utils.mjs +289 -167
- package/package.json +4 -3
package/dist/utils.mjs
CHANGED
|
@@ -22,40 +22,75 @@ function bitsToMs(baudRate, bits) {
|
|
|
22
22
|
* open / close / destroy callbacks.
|
|
23
23
|
*/
|
|
24
24
|
function drainCbs(cbs, err) {
|
|
25
|
-
if (!cbs)
|
|
25
|
+
if (!cbs) {
|
|
26
26
|
return;
|
|
27
|
+
}
|
|
27
28
|
for (const cb of cbs) {
|
|
28
29
|
cb?.(err);
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function inRange(n, [min, max]) {
|
|
33
|
-
return n >= min && n <= max;
|
|
34
|
-
}
|
|
35
|
-
function isRangeArray(range) {
|
|
36
|
-
return Array.isArray(range[0]);
|
|
37
|
-
}
|
|
38
33
|
function checkRange(value, range) {
|
|
39
34
|
if (!range || range.length === 0) {
|
|
40
35
|
return true;
|
|
41
36
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
const isMultiRange = Array.isArray(range[0]);
|
|
38
|
+
const isValueArray = Array.isArray(value);
|
|
39
|
+
if (!isValueArray && !isMultiRange) {
|
|
40
|
+
const r = range;
|
|
41
|
+
const min = r[0], max = r[1];
|
|
42
|
+
const v = value;
|
|
43
|
+
return min <= max ? v >= min && v <= max : v >= max && v <= min;
|
|
44
|
+
}
|
|
45
|
+
if (!isValueArray && isMultiRange) {
|
|
46
|
+
const ranges = range;
|
|
47
|
+
const v = value;
|
|
48
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
49
|
+
const min = ranges[i][0], max = ranges[i][1];
|
|
50
|
+
const lo = min <= max ? min : max;
|
|
51
|
+
const hi = min <= max ? max : min;
|
|
52
|
+
if (v >= lo && v <= hi) {
|
|
48
53
|
return true;
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
return false;
|
|
52
57
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
const values = value;
|
|
59
|
+
if (values.length === 0) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (!isMultiRange) {
|
|
63
|
+
const r = range;
|
|
64
|
+
const min = r[0], max = r[1];
|
|
65
|
+
const lo = min <= max ? min : max;
|
|
66
|
+
const hi = min <= max ? max : min;
|
|
67
|
+
for (let i = 0; i < values.length; i++) {
|
|
68
|
+
if (values[i] < lo || values[i] > hi) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const ranges = range;
|
|
75
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
76
|
+
const min = ranges[i][0], max = ranges[i][1];
|
|
77
|
+
const lo = min <= max ? min : max;
|
|
78
|
+
const hi = min <= max ? max : min;
|
|
79
|
+
let allInRange = true;
|
|
80
|
+
for (let j = 0; j < values.length; j++) {
|
|
81
|
+
if (values[j] < lo || values[j] > hi) {
|
|
82
|
+
allInRange = false;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (allInRange) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
56
91
|
}
|
|
57
92
|
|
|
58
|
-
const
|
|
93
|
+
const CRC_TABLE = new Uint16Array([
|
|
59
94
|
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
|
|
60
95
|
0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
|
|
61
96
|
0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
|
|
@@ -72,32 +107,38 @@ const TABLE = [
|
|
|
72
107
|
0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
|
|
73
108
|
0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
|
|
74
109
|
0x4040,
|
|
75
|
-
];
|
|
76
|
-
|
|
110
|
+
]);
|
|
111
|
+
/** CRC-16 (Modbus) over a single contiguous buffer. */
|
|
112
|
+
function crcFixed(data, start, end) {
|
|
77
113
|
let crc = 0xffff;
|
|
78
114
|
for (let index = start; index < end; index++) {
|
|
79
|
-
crc =
|
|
115
|
+
crc = CRC_TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8);
|
|
80
116
|
}
|
|
81
117
|
return crc;
|
|
82
118
|
}
|
|
83
|
-
|
|
84
119
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* Used for byte-level Modbus payload validation (function-code values, raw
|
|
88
|
-
* byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
|
|
89
|
-
* Infinity, and out-of-range values uniformly.
|
|
120
|
+
* CRC-16 (Modbus) over two contiguous buffer segments.
|
|
121
|
+
* Computes CRC(head[headOff:headOff+headLen]) followed by CRC(tail[tailOff:tailOff+tailLen]).
|
|
90
122
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
123
|
+
function crcDual(head, headOff, headLen, tail, tailOff, tailLen) {
|
|
124
|
+
let crc = 0xffff;
|
|
125
|
+
const headEnd = headOff + headLen;
|
|
126
|
+
for (let i = headOff; i < headEnd; i++) {
|
|
127
|
+
crc = CRC_TABLE[(crc ^ head[i]) & 0xff] ^ (crc >> 8);
|
|
128
|
+
}
|
|
129
|
+
const tailEnd = tailOff + tailLen;
|
|
130
|
+
for (let i = tailOff; i < tailEnd; i++) {
|
|
131
|
+
crc = CRC_TABLE[(crc ^ tail[i]) & 0xff] ^ (crc >> 8);
|
|
132
|
+
}
|
|
133
|
+
return crc;
|
|
93
134
|
}
|
|
94
135
|
|
|
95
|
-
function lrc(data, start
|
|
136
|
+
function lrc(data, start, end) {
|
|
96
137
|
let sum = 0;
|
|
97
138
|
for (let i = start; i < end; i++) {
|
|
98
139
|
sum += data[i];
|
|
99
140
|
}
|
|
100
|
-
return
|
|
141
|
+
return -sum & 0xff;
|
|
101
142
|
}
|
|
102
143
|
|
|
103
144
|
/**
|
|
@@ -140,103 +181,93 @@ var ConformityLevel;
|
|
|
140
181
|
/** Shared empty Buffer to avoid repeated allocations. */
|
|
141
182
|
Buffer.alloc(0);
|
|
142
183
|
|
|
143
|
-
const REQUEST_FIXED_LENGTHS = {
|
|
144
|
-
[FunctionCode.READ_COILS]: 8,
|
|
145
|
-
[FunctionCode.READ_DISCRETE_INPUTS]: 8,
|
|
146
|
-
[FunctionCode.READ_HOLDING_REGISTERS]: 8,
|
|
147
|
-
[FunctionCode.READ_INPUT_REGISTERS]: 8,
|
|
148
|
-
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
149
|
-
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
150
|
-
[FunctionCode.REPORT_SERVER_ID]: 4,
|
|
151
|
-
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
152
|
-
[FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
|
|
153
|
-
};
|
|
154
|
-
const REQUEST_BYTE_COUNT = {
|
|
155
|
-
[FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
|
|
156
|
-
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
|
|
157
|
-
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
|
|
158
|
-
};
|
|
159
|
-
const RESPONSE_FIXED_LENGTHS = {
|
|
160
|
-
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
161
|
-
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
162
|
-
[FunctionCode.WRITE_MULTIPLE_COILS]: 8,
|
|
163
|
-
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
|
|
164
|
-
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
165
|
-
};
|
|
166
|
-
const RESPONSE_BYTE_COUNT = {
|
|
167
|
-
[FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
|
|
168
|
-
[FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
|
|
169
|
-
[FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
|
|
170
|
-
[FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
|
|
171
|
-
[FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
|
|
172
|
-
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
|
|
173
|
-
};
|
|
174
|
-
/** Sentinel: caller needs to feed more bytes before length can be determined. */
|
|
175
184
|
const PREDICT_NEED_MORE = 0;
|
|
176
|
-
/** Sentinel: function code is not in the standard tables. */
|
|
177
185
|
const PREDICT_UNKNOWN = -1;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
const REQ_TABLE = new Int32Array(256);
|
|
187
|
+
const RES_TABLE = new Int32Array(256);
|
|
188
|
+
(function initTables() {
|
|
189
|
+
REQ_TABLE[FunctionCode.READ_COILS] = 8;
|
|
190
|
+
REQ_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = 8;
|
|
191
|
+
REQ_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = 8;
|
|
192
|
+
REQ_TABLE[FunctionCode.READ_INPUT_REGISTERS] = 8;
|
|
193
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
194
|
+
REQ_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
195
|
+
REQ_TABLE[FunctionCode.REPORT_SERVER_ID] = 4;
|
|
196
|
+
REQ_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
197
|
+
REQ_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = 7;
|
|
198
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = -1545;
|
|
199
|
+
REQ_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = -1545;
|
|
200
|
+
REQ_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -2573;
|
|
201
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_COIL] = 8;
|
|
202
|
+
RES_TABLE[FunctionCode.WRITE_SINGLE_REGISTER] = 8;
|
|
203
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_COILS] = 8;
|
|
204
|
+
RES_TABLE[FunctionCode.WRITE_MULTIPLE_REGISTERS] = 8;
|
|
205
|
+
RES_TABLE[FunctionCode.MASK_WRITE_REGISTER] = 10;
|
|
206
|
+
RES_TABLE[FunctionCode.READ_COILS] = -517;
|
|
207
|
+
RES_TABLE[FunctionCode.READ_DISCRETE_INPUTS] = -517;
|
|
208
|
+
RES_TABLE[FunctionCode.READ_HOLDING_REGISTERS] = -517;
|
|
209
|
+
RES_TABLE[FunctionCode.READ_INPUT_REGISTERS] = -517;
|
|
210
|
+
RES_TABLE[FunctionCode.REPORT_SERVER_ID] = -517;
|
|
211
|
+
RES_TABLE[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS] = -517;
|
|
212
|
+
RES_TABLE[FunctionCode.READ_DEVICE_IDENTIFICATION] = -999;
|
|
213
|
+
})();
|
|
214
|
+
function predictRtuFrameLength(residual, data, residualLen, start, end, isResponse) {
|
|
215
|
+
const len = end - start;
|
|
216
|
+
if (len < 2) {
|
|
192
217
|
return PREDICT_NEED_MORE;
|
|
193
218
|
}
|
|
194
|
-
const fc =
|
|
195
|
-
if (isResponse
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
219
|
+
const fc = start + 1 < residualLen ? residual[start + 1] : data[start + 1 - residualLen];
|
|
220
|
+
if (isResponse) {
|
|
221
|
+
if ((fc & EXCEPTION_OFFSET) !== 0) {
|
|
222
|
+
return 5;
|
|
223
|
+
}
|
|
224
|
+
const val = RES_TABLE[fc];
|
|
225
|
+
if (val > 0) {
|
|
226
|
+
return val;
|
|
227
|
+
}
|
|
228
|
+
if (val < 0) {
|
|
229
|
+
if (val === -999) {
|
|
230
|
+
// FC 43 / MEI 14 response — inline to avoid function-call overhead on
|
|
231
|
+
// the framing hot path (even though this FC is uncommon).
|
|
232
|
+
if (end - start < 8) {
|
|
233
|
+
return PREDICT_NEED_MORE;
|
|
234
|
+
}
|
|
235
|
+
if ((start + 2 < residualLen ? residual[start + 2] : data[start + 2 - residualLen]) !== MEI_READ_DEVICE_ID) {
|
|
236
|
+
return PREDICT_UNKNOWN;
|
|
237
|
+
}
|
|
238
|
+
const numObjs = start + 7 < residualLen ? residual[start + 7] : data[start + 7 - residualLen];
|
|
239
|
+
let cursor = start + 8;
|
|
240
|
+
for (let i = 0; i < numObjs; i++) {
|
|
241
|
+
if (end < cursor + 2) {
|
|
242
|
+
return PREDICT_NEED_MORE;
|
|
243
|
+
}
|
|
244
|
+
cursor += 2 + (cursor + 1 < residualLen ? residual[cursor + 1] : data[cursor + 1 - residualLen]);
|
|
245
|
+
}
|
|
246
|
+
return cursor - start + 2;
|
|
247
|
+
}
|
|
248
|
+
const decode = -val;
|
|
249
|
+
const offset = decode >> 8;
|
|
250
|
+
if (len <= offset) {
|
|
251
|
+
return PREDICT_NEED_MORE;
|
|
252
|
+
}
|
|
253
|
+
return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
|
|
206
254
|
}
|
|
207
|
-
return bc.extra + buffer[start + bc.offset];
|
|
208
|
-
}
|
|
209
|
-
if (isResponse && fc === FunctionCode.READ_DEVICE_IDENTIFICATION) {
|
|
210
|
-
return predictFc43_14Response(buffer, start, end);
|
|
211
|
-
}
|
|
212
|
-
return PREDICT_UNKNOWN;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
|
|
216
|
-
* response structure per Modbus V1.1b3 §6.21.
|
|
217
|
-
*
|
|
218
|
-
* Layout (after unit and fc):
|
|
219
|
-
* mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
|
|
220
|
-
* [objId(1) objLen(1) objData(objLen)] × numObjs
|
|
221
|
-
* CRC(2)
|
|
222
|
-
*/
|
|
223
|
-
function predictFc43_14Response(buffer, start, end) {
|
|
224
|
-
if (end - start < 8) {
|
|
225
|
-
return PREDICT_NEED_MORE;
|
|
226
|
-
}
|
|
227
|
-
if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
|
|
228
|
-
return PREDICT_UNKNOWN;
|
|
229
255
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
256
|
+
else {
|
|
257
|
+
const val = REQ_TABLE[fc];
|
|
258
|
+
if (val > 0) {
|
|
259
|
+
return val;
|
|
260
|
+
}
|
|
261
|
+
if (val < 0) {
|
|
262
|
+
const decode = -val;
|
|
263
|
+
const offset = decode >> 8;
|
|
264
|
+
if (len <= offset) {
|
|
265
|
+
return PREDICT_NEED_MORE;
|
|
266
|
+
}
|
|
267
|
+
return (decode & 0xff) + (start + offset < residualLen ? residual[start + offset] : data[start + offset - residualLen]);
|
|
235
268
|
}
|
|
236
|
-
const objLen = buffer[cursor + 1];
|
|
237
|
-
cursor += 2 + objLen;
|
|
238
269
|
}
|
|
239
|
-
return
|
|
270
|
+
return PREDICT_UNKNOWN;
|
|
240
271
|
}
|
|
241
272
|
|
|
242
273
|
/**
|
|
@@ -245,10 +276,12 @@ function predictFc43_14Response(buffer, start, end) {
|
|
|
245
276
|
function promisifyCb(fn) {
|
|
246
277
|
return new Promise((resolve, reject) => {
|
|
247
278
|
fn((err) => {
|
|
248
|
-
if (err)
|
|
279
|
+
if (err) {
|
|
249
280
|
reject(err);
|
|
250
|
-
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
251
283
|
resolve();
|
|
284
|
+
}
|
|
252
285
|
});
|
|
253
286
|
});
|
|
254
287
|
}
|
|
@@ -274,7 +307,12 @@ function resolveOne(value, baudRate, fastBaudMs) {
|
|
|
274
307
|
if (baudRate === undefined) {
|
|
275
308
|
return undefined;
|
|
276
309
|
}
|
|
277
|
-
|
|
310
|
+
if (baudRate > 19200) {
|
|
311
|
+
return fastBaudMs;
|
|
312
|
+
}
|
|
313
|
+
const ms = bitsToMs(baudRate, value.value);
|
|
314
|
+
const trunc = ms | 0;
|
|
315
|
+
return trunc + (ms > trunc ? 1 : 0);
|
|
278
316
|
}
|
|
279
317
|
/**
|
|
280
318
|
* Resolve Modbus RTU timing parameters from user options into milliseconds.
|
|
@@ -291,7 +329,17 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
291
329
|
if (intervalBetweenFrames === undefined) {
|
|
292
330
|
// Spec default: t3.5 derived from baudRate, or 0 when neither option nor
|
|
293
331
|
// baudRate were supplied.
|
|
294
|
-
|
|
332
|
+
if (baudRate === undefined) {
|
|
333
|
+
intervalBetweenFrames = 0;
|
|
334
|
+
}
|
|
335
|
+
else if (baudRate > 19200) {
|
|
336
|
+
intervalBetweenFrames = 1.75;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const ms = bitsToMs(baudRate, 38.5);
|
|
340
|
+
const trunc = ms | 0;
|
|
341
|
+
intervalBetweenFrames = trunc + (ms > trunc ? 1 : 0);
|
|
342
|
+
}
|
|
295
343
|
}
|
|
296
344
|
let interCharTimeout = resolveOne(opts.interCharTimeout, baudRate, 0.75);
|
|
297
345
|
if (interCharTimeout === undefined) {
|
|
@@ -301,102 +349,176 @@ function resolveRtuTiming(opts = {}, baudRate) {
|
|
|
301
349
|
return { intervalBetweenFrames, interCharTimeout };
|
|
302
350
|
}
|
|
303
351
|
|
|
304
|
-
/**
|
|
305
|
-
*
|
|
352
|
+
/**
|
|
353
|
+
* Hybrid timer manager: uses native `setTimeout` for low concurrency
|
|
354
|
+
* and switches to a binary min-heap when concurrency exceeds the threshold.
|
|
355
|
+
*
|
|
356
|
+
* Benchmarks (add + clear throughput, Node 24, x64):
|
|
357
|
+
* 1 concurrent: setTimeout ~1.7× faster than heap
|
|
358
|
+
* 2 concurrent: setTimeout ~1.6× faster than heap
|
|
359
|
+
* 5 concurrent: setTimeout ~1.5-1.9× faster than heap
|
|
360
|
+
* 10 concurrent: roughly equal
|
|
361
|
+
* 20 concurrent: heap ~1.3× faster than setTimeout[]
|
|
362
|
+
* 50 concurrent: heap ~1.4-1.7× faster than setTimeout[]
|
|
306
363
|
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
364
|
+
* The crossover point is around 10 concurrent timers, so the default
|
|
365
|
+
* `concurrentThreshold = 2` keeps the common 1-2 request case on the
|
|
366
|
+
* fast direct path while delegating to the heap for larger batches.
|
|
310
367
|
*/
|
|
311
368
|
class TimerHeap {
|
|
312
369
|
_deadlines = [];
|
|
313
370
|
_ids = [];
|
|
371
|
+
_seqs = [];
|
|
372
|
+
_counter = 0;
|
|
314
373
|
_timer = null;
|
|
315
374
|
_onFire;
|
|
316
|
-
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
317
375
|
_boundTick;
|
|
318
|
-
|
|
376
|
+
_threshold;
|
|
377
|
+
_mode = 'direct';
|
|
378
|
+
_directTimers = new Map();
|
|
379
|
+
/**
|
|
380
|
+
* @param onFire Callback invoked with the timer id when it expires.
|
|
381
|
+
* @param concurrentThreshold Maximum number of timers kept as individual
|
|
382
|
+
* native `setTimeout` handles. Once exceeded, all timers migrate to
|
|
383
|
+
* the internal heap and share a single native timer. Default is 2.
|
|
384
|
+
*/
|
|
385
|
+
constructor(onFire, concurrentThreshold = 2) {
|
|
319
386
|
this._onFire = onFire;
|
|
320
387
|
this._boundTick = this._onTick.bind(this);
|
|
388
|
+
this._threshold = concurrentThreshold;
|
|
321
389
|
}
|
|
322
|
-
/** Number of pending timers in the heap. */
|
|
323
390
|
get size() {
|
|
324
|
-
return this._deadlines.length;
|
|
391
|
+
return this._mode === 'direct' ? this._directTimers.size : this._deadlines.length;
|
|
325
392
|
}
|
|
326
|
-
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
327
393
|
add(id, ms) {
|
|
394
|
+
if (this._mode === 'direct' && this._directTimers.size + 1 <= this._threshold) {
|
|
395
|
+
const deadline = performance.now() + ms;
|
|
396
|
+
const handle = setTimeout(() => {
|
|
397
|
+
if (this._mode !== 'direct') {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this._directTimers.delete(id);
|
|
401
|
+
this._onFire(id);
|
|
402
|
+
}, ms);
|
|
403
|
+
this._directTimers.set(id, { handle, deadline });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (this._mode === 'direct') {
|
|
407
|
+
this._mode = 'heap';
|
|
408
|
+
for (const [existingId, { handle, deadline }] of this._directTimers) {
|
|
409
|
+
clearTimeout(handle);
|
|
410
|
+
const diff = deadline - performance.now();
|
|
411
|
+
const trunc = diff | 0;
|
|
412
|
+
const remaining = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
|
|
413
|
+
if (remaining === 0) {
|
|
414
|
+
this._onFire(existingId);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
this._heapAdd(existingId, remaining);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
this._directTimers.clear();
|
|
421
|
+
}
|
|
422
|
+
this._heapAdd(id, ms);
|
|
423
|
+
}
|
|
424
|
+
clear() {
|
|
425
|
+
for (const { handle } of this._directTimers.values()) {
|
|
426
|
+
clearTimeout(handle);
|
|
427
|
+
}
|
|
428
|
+
this._directTimers.clear();
|
|
429
|
+
this._mode = 'direct';
|
|
430
|
+
if (this._timer) {
|
|
431
|
+
clearTimeout(this._timer);
|
|
432
|
+
this._timer = null;
|
|
433
|
+
}
|
|
434
|
+
this._deadlines.length = 0;
|
|
435
|
+
this._ids.length = 0;
|
|
436
|
+
this._seqs.length = 0;
|
|
437
|
+
this._counter = 0;
|
|
438
|
+
}
|
|
439
|
+
_heapAdd(id, ms) {
|
|
328
440
|
const deadline = performance.now() + ms;
|
|
441
|
+
const seq = this._counter++;
|
|
329
442
|
let i = this._deadlines.length;
|
|
330
443
|
this._deadlines.push(deadline);
|
|
331
444
|
this._ids.push(id);
|
|
332
|
-
|
|
445
|
+
this._seqs.push(seq);
|
|
333
446
|
while (i > 0) {
|
|
334
447
|
const p = (i - 1) >> 1;
|
|
335
|
-
|
|
448
|
+
const parentComesFirst = this._deadlines[p] < deadline || (this._deadlines[p] === deadline && this._seqs[p] < seq);
|
|
449
|
+
if (parentComesFirst) {
|
|
336
450
|
break;
|
|
451
|
+
}
|
|
337
452
|
this._deadlines[i] = this._deadlines[p];
|
|
338
453
|
this._ids[i] = this._ids[p];
|
|
454
|
+
this._seqs[i] = this._seqs[p];
|
|
339
455
|
i = p;
|
|
340
456
|
}
|
|
341
457
|
this._deadlines[i] = deadline;
|
|
342
458
|
this._ids[i] = id;
|
|
343
|
-
|
|
344
|
-
if (i === 0)
|
|
459
|
+
this._seqs[i] = seq;
|
|
460
|
+
if (i === 0) {
|
|
345
461
|
this._refresh();
|
|
346
|
-
}
|
|
347
|
-
/** Dispose without firing callbacks. */
|
|
348
|
-
clear() {
|
|
349
|
-
if (this._timer) {
|
|
350
|
-
clearTimeout(this._timer);
|
|
351
|
-
this._timer = null;
|
|
352
462
|
}
|
|
353
|
-
this._deadlines.length = 0;
|
|
354
|
-
this._ids.length = 0;
|
|
355
463
|
}
|
|
356
464
|
_refresh() {
|
|
357
465
|
if (this._timer) {
|
|
358
466
|
clearTimeout(this._timer);
|
|
359
467
|
this._timer = null;
|
|
360
468
|
}
|
|
361
|
-
if (this._deadlines.length === 0)
|
|
469
|
+
if (this._deadlines.length === 0) {
|
|
362
470
|
return;
|
|
363
|
-
|
|
364
|
-
|
|
471
|
+
}
|
|
472
|
+
const diff = this._deadlines[0] - performance.now();
|
|
473
|
+
const trunc = diff | 0;
|
|
474
|
+
const delay = diff > 0 ? trunc + (diff > trunc ? 1 : 0) : 0;
|
|
475
|
+
const safeDelay = delay < 2147483647 ? delay : 2147483647;
|
|
476
|
+
this._timer = setTimeout(this._boundTick, safeDelay);
|
|
365
477
|
}
|
|
366
478
|
_onTick() {
|
|
367
479
|
this._timer = null;
|
|
368
480
|
const now = performance.now();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
481
|
+
try {
|
|
482
|
+
while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
|
|
483
|
+
const id = this._pop();
|
|
484
|
+
this._onFire(id);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
this._refresh();
|
|
372
489
|
}
|
|
373
|
-
this._refresh();
|
|
374
490
|
}
|
|
375
|
-
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
376
491
|
_pop() {
|
|
377
492
|
const topId = this._ids[0];
|
|
378
493
|
const lastId = this._ids.pop();
|
|
379
494
|
const lastDeadline = this._deadlines.pop();
|
|
495
|
+
const lastSeq = this._seqs.pop();
|
|
380
496
|
const n = this._deadlines.length;
|
|
381
497
|
if (n > 0) {
|
|
382
498
|
let i = 0;
|
|
383
|
-
|
|
384
|
-
while (
|
|
385
|
-
let
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
499
|
+
const half = n >> 1;
|
|
500
|
+
while (i < half) {
|
|
501
|
+
let minChild = (i << 1) + 1;
|
|
502
|
+
const rightChild = minChild + 1;
|
|
503
|
+
if (rightChild < n) {
|
|
504
|
+
const rightComesFirst = this._deadlines[rightChild] < this._deadlines[minChild] ||
|
|
505
|
+
(this._deadlines[rightChild] === this._deadlines[minChild] && this._seqs[rightChild] < this._seqs[minChild]);
|
|
506
|
+
if (rightComesFirst) {
|
|
507
|
+
minChild = rightChild;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const lastComesFirst = lastDeadline < this._deadlines[minChild] || (lastDeadline === this._deadlines[minChild] && lastSeq < this._seqs[minChild]);
|
|
511
|
+
if (lastComesFirst) {
|
|
393
512
|
break;
|
|
394
|
-
|
|
395
|
-
this.
|
|
396
|
-
i =
|
|
513
|
+
}
|
|
514
|
+
this._deadlines[i] = this._deadlines[minChild];
|
|
515
|
+
this._ids[i] = this._ids[minChild];
|
|
516
|
+
this._seqs[i] = this._seqs[minChild];
|
|
517
|
+
i = minChild;
|
|
397
518
|
}
|
|
398
519
|
this._deadlines[i] = lastDeadline;
|
|
399
520
|
this._ids[i] = lastId;
|
|
521
|
+
this._seqs[i] = lastSeq;
|
|
400
522
|
}
|
|
401
523
|
return topId;
|
|
402
524
|
}
|
|
@@ -422,4 +544,4 @@ function isWhitelisted(address, whitelist) {
|
|
|
422
544
|
return whitelist.includes(normalized);
|
|
423
545
|
}
|
|
424
546
|
|
|
425
|
-
export { PREDICT_NEED_MORE, PREDICT_UNKNOWN, TimerHeap, bitsToMs, checkRange,
|
|
547
|
+
export { CRC_TABLE, PREDICT_NEED_MORE, PREDICT_UNKNOWN, REQ_TABLE, RES_TABLE, TimerHeap, bitsToMs, checkRange, crcDual, crcFixed, drainCbs, isWhitelisted, lrc, predictRtuFrameLength, promisifyCb, resolveRtuTiming };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "njs-modbus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "A pure JavaScript implementation of Modbus for Node.js.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"modbus",
|
|
@@ -45,9 +45,10 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "node -e \"fs.rmSync('dist', {recursive: true, force: true})\" && rollup -c && node -e \"fs.rmSync('dist/src', {recursive: true, force: true})\"",
|
|
47
47
|
"test": "tsx --test test/**/*.test.ts",
|
|
48
|
-
"benchmark": "tsx benchmark/encode-decode.ts && echo && tsx benchmark/tcp-throughput.ts && echo && tsx benchmark/concurrent.ts",
|
|
49
|
-
"benchmark:all-fcs": "tsx benchmark/all-fcs.ts",
|
|
50
48
|
"benchmark:report": "tsx benchmark/generate-report.ts",
|
|
49
|
+
"benchmark:report:test": "tsx benchmark/test-report.ts",
|
|
50
|
+
"benchmark:report:quick": "tsx benchmark/generate-report.ts -- --duration 10s --runs 2",
|
|
51
|
+
"benchmark:report:full": "tsx benchmark/generate-report.ts -- --duration 120s --runs 5 --max-payload",
|
|
51
52
|
"util:sort-package-json": "sort-package-json"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|