njs-modbus 3.1.1 → 3.2.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 +20 -0
- package/README.zh-CN.md +20 -0
- package/dist/index.cjs +878 -384
- package/dist/index.d.ts +87 -25
- package/dist/index.mjs +878 -385
- package/dist/utils.cjs +439 -0
- package/dist/utils.d.ts +161 -0
- package/dist/utils.mjs +425 -0
- package/package.json +15 -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/utils.cjs
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert a number of bits to milliseconds at a given baud rate.
|
|
5
|
+
*
|
|
6
|
+
* Used to derive Modbus RTU timing intervals from bit counts — e.g. 38.5 bits
|
|
7
|
+
* = 3.5 character times at 11 bits/char (t3.5 inter-frame silence), or 16.5
|
|
8
|
+
* bits = 1.5 character times (t1.5 inter-character timeout), per Modbus V1.02
|
|
9
|
+
* §2.5.1.1.
|
|
10
|
+
*
|
|
11
|
+
* @param baudRate Serial port baud rate.
|
|
12
|
+
* @param bits Number of bits to convert.
|
|
13
|
+
* @returns Duration in milliseconds.
|
|
14
|
+
*/
|
|
15
|
+
function bitsToMs(baudRate, bits) {
|
|
16
|
+
return (bits * 1000) / baudRate;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Drain a pending-callback array: invoke each callback with the given error (or null).
|
|
21
|
+
*
|
|
22
|
+
* Handles `null`/`undefined` entries (from optional `cb?` parameters) gracefully.
|
|
23
|
+
* Used by physical layers and connections to resolve queued
|
|
24
|
+
* open / close / destroy callbacks.
|
|
25
|
+
*/
|
|
26
|
+
function drainCbs(cbs, err) {
|
|
27
|
+
if (!cbs)
|
|
28
|
+
return;
|
|
29
|
+
for (const cb of cbs) {
|
|
30
|
+
cb?.(err);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function inRange(n, [min, max]) {
|
|
35
|
+
return n >= min && n <= max;
|
|
36
|
+
}
|
|
37
|
+
function isRangeArray(range) {
|
|
38
|
+
return Array.isArray(range[0]);
|
|
39
|
+
}
|
|
40
|
+
function checkRange(value, range) {
|
|
41
|
+
if (!range || range.length === 0) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const values = Array.isArray(value) ? value : [value];
|
|
45
|
+
if (isRangeArray(range)) {
|
|
46
|
+
for (const r of range) {
|
|
47
|
+
const [min, max] = r;
|
|
48
|
+
const [lo, hi] = min <= max ? [min, max] : [max, min];
|
|
49
|
+
if (values.every((n) => inRange(n, [lo, hi]))) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const [min, max] = range;
|
|
56
|
+
const [lo, hi] = min <= max ? [min, max] : [max, min];
|
|
57
|
+
return values.every((n) => inRange(n, [lo, hi]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const TABLE = [
|
|
61
|
+
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
|
|
62
|
+
0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
|
|
63
|
+
0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
|
|
64
|
+
0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141,
|
|
65
|
+
0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01,
|
|
66
|
+
0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0,
|
|
67
|
+
0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681,
|
|
68
|
+
0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
|
|
69
|
+
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
|
|
70
|
+
0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0,
|
|
71
|
+
0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381,
|
|
72
|
+
0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741,
|
|
73
|
+
0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901,
|
|
74
|
+
0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
|
|
75
|
+
0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
|
|
76
|
+
0x4040,
|
|
77
|
+
];
|
|
78
|
+
function crc(data, start = 0, end = data.length) {
|
|
79
|
+
let crc = 0xffff;
|
|
80
|
+
for (let index = start; index < end; index++) {
|
|
81
|
+
crc = (TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8)) & 0xffff;
|
|
82
|
+
}
|
|
83
|
+
return crc;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns true when `n` is an integer in the unsigned-byte range [0, 255].
|
|
88
|
+
*
|
|
89
|
+
* Used for byte-level Modbus payload validation (function-code values, raw
|
|
90
|
+
* byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
|
|
91
|
+
* Infinity, and out-of-range values uniformly.
|
|
92
|
+
*/
|
|
93
|
+
function isUint8(n) {
|
|
94
|
+
return Number.isInteger(n) && n >= 0 && n <= 255;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function lrc(data, start = 0, end = data.length) {
|
|
98
|
+
let sum = 0;
|
|
99
|
+
for (let i = start; i < end; i++) {
|
|
100
|
+
sum += data[i];
|
|
101
|
+
}
|
|
102
|
+
return (~sum + 1) & 0xff;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Standard Modbus function codes (V1.1b3 §6).
|
|
107
|
+
*/
|
|
108
|
+
var FunctionCode;
|
|
109
|
+
(function (FunctionCode) {
|
|
110
|
+
FunctionCode[FunctionCode["READ_COILS"] = 1] = "READ_COILS";
|
|
111
|
+
FunctionCode[FunctionCode["READ_DISCRETE_INPUTS"] = 2] = "READ_DISCRETE_INPUTS";
|
|
112
|
+
FunctionCode[FunctionCode["READ_HOLDING_REGISTERS"] = 3] = "READ_HOLDING_REGISTERS";
|
|
113
|
+
FunctionCode[FunctionCode["READ_INPUT_REGISTERS"] = 4] = "READ_INPUT_REGISTERS";
|
|
114
|
+
FunctionCode[FunctionCode["WRITE_SINGLE_COIL"] = 5] = "WRITE_SINGLE_COIL";
|
|
115
|
+
FunctionCode[FunctionCode["WRITE_SINGLE_REGISTER"] = 6] = "WRITE_SINGLE_REGISTER";
|
|
116
|
+
FunctionCode[FunctionCode["WRITE_MULTIPLE_COILS"] = 15] = "WRITE_MULTIPLE_COILS";
|
|
117
|
+
FunctionCode[FunctionCode["WRITE_MULTIPLE_REGISTERS"] = 16] = "WRITE_MULTIPLE_REGISTERS";
|
|
118
|
+
FunctionCode[FunctionCode["REPORT_SERVER_ID"] = 17] = "REPORT_SERVER_ID";
|
|
119
|
+
FunctionCode[FunctionCode["MASK_WRITE_REGISTER"] = 22] = "MASK_WRITE_REGISTER";
|
|
120
|
+
FunctionCode[FunctionCode["READ_WRITE_MULTIPLE_REGISTERS"] = 23] = "READ_WRITE_MULTIPLE_REGISTERS";
|
|
121
|
+
FunctionCode[FunctionCode["READ_DEVICE_IDENTIFICATION"] = 43] = "READ_DEVICE_IDENTIFICATION";
|
|
122
|
+
})(FunctionCode || (FunctionCode = {}));
|
|
123
|
+
/** Exception response FC = request FC | EXCEPTION_OFFSET (V1.1b3 §7). */
|
|
124
|
+
const EXCEPTION_OFFSET = 0x80;
|
|
125
|
+
/** FC 0x2B MEI sub-function selecting Read Device Identification (V1.1b3 §6.21). */
|
|
126
|
+
const MEI_READ_DEVICE_ID = 0x0e;
|
|
127
|
+
/** Read Device ID code values inside an FC 0x2B / MEI 0x0E request. */
|
|
128
|
+
var ReadDeviceIDCode;
|
|
129
|
+
(function (ReadDeviceIDCode) {
|
|
130
|
+
ReadDeviceIDCode[ReadDeviceIDCode["BASIC_STREAM"] = 1] = "BASIC_STREAM";
|
|
131
|
+
ReadDeviceIDCode[ReadDeviceIDCode["REGULAR_STREAM"] = 2] = "REGULAR_STREAM";
|
|
132
|
+
ReadDeviceIDCode[ReadDeviceIDCode["EXTENDED_STREAM"] = 3] = "EXTENDED_STREAM";
|
|
133
|
+
ReadDeviceIDCode[ReadDeviceIDCode["SPECIFIC_ACCESS"] = 4] = "SPECIFIC_ACCESS";
|
|
134
|
+
})(ReadDeviceIDCode || (ReadDeviceIDCode = {}));
|
|
135
|
+
/** Conformity level reported in an FC 0x2B / MEI 0x0E response. */
|
|
136
|
+
var ConformityLevel;
|
|
137
|
+
(function (ConformityLevel) {
|
|
138
|
+
ConformityLevel[ConformityLevel["BASIC"] = 129] = "BASIC";
|
|
139
|
+
ConformityLevel[ConformityLevel["REGULAR"] = 130] = "REGULAR";
|
|
140
|
+
ConformityLevel[ConformityLevel["EXTENDED"] = 131] = "EXTENDED";
|
|
141
|
+
})(ConformityLevel || (ConformityLevel = {}));
|
|
142
|
+
/** Shared empty Buffer to avoid repeated allocations. */
|
|
143
|
+
Buffer.alloc(0);
|
|
144
|
+
|
|
145
|
+
const REQUEST_FIXED_LENGTHS = {
|
|
146
|
+
[FunctionCode.READ_COILS]: 8,
|
|
147
|
+
[FunctionCode.READ_DISCRETE_INPUTS]: 8,
|
|
148
|
+
[FunctionCode.READ_HOLDING_REGISTERS]: 8,
|
|
149
|
+
[FunctionCode.READ_INPUT_REGISTERS]: 8,
|
|
150
|
+
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
151
|
+
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
152
|
+
[FunctionCode.REPORT_SERVER_ID]: 4,
|
|
153
|
+
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
154
|
+
[FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
|
|
155
|
+
};
|
|
156
|
+
const REQUEST_BYTE_COUNT = {
|
|
157
|
+
[FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
|
|
158
|
+
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
|
|
159
|
+
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
|
|
160
|
+
};
|
|
161
|
+
const RESPONSE_FIXED_LENGTHS = {
|
|
162
|
+
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
163
|
+
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
164
|
+
[FunctionCode.WRITE_MULTIPLE_COILS]: 8,
|
|
165
|
+
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
|
|
166
|
+
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
167
|
+
};
|
|
168
|
+
const RESPONSE_BYTE_COUNT = {
|
|
169
|
+
[FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
|
|
170
|
+
[FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
|
|
171
|
+
[FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
|
|
172
|
+
[FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
|
|
173
|
+
[FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
|
|
174
|
+
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
|
|
175
|
+
};
|
|
176
|
+
/** Sentinel: caller needs to feed more bytes before length can be determined. */
|
|
177
|
+
const PREDICT_NEED_MORE = 0;
|
|
178
|
+
/** Sentinel: function code is not in the standard tables. */
|
|
179
|
+
const PREDICT_UNKNOWN = -1;
|
|
180
|
+
/**
|
|
181
|
+
* Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
|
|
182
|
+
*
|
|
183
|
+
* Returns a sentinel-encoded number to avoid per-call object allocation on the
|
|
184
|
+
* RTU decode hot path:
|
|
185
|
+
* - Positive integer (>= 4): total frame length, function code is known.
|
|
186
|
+
* - {@link PREDICT_NEED_MORE} (0): function code is known but more bytes are
|
|
187
|
+
* required (typically waiting on the byteCount byte).
|
|
188
|
+
* - {@link PREDICT_UNKNOWN} (-1): function code is not in the standard tables —
|
|
189
|
+
* the framing layer must defer to a registered `CustomFunctionCode` or treat
|
|
190
|
+
* this as a framing error.
|
|
191
|
+
*/
|
|
192
|
+
function predictRtuFrameLength(buffer, start, end, isResponse) {
|
|
193
|
+
if (end - start < 2) {
|
|
194
|
+
return PREDICT_NEED_MORE;
|
|
195
|
+
}
|
|
196
|
+
const fc = buffer[start + 1];
|
|
197
|
+
if (isResponse && (fc & EXCEPTION_OFFSET) !== 0) {
|
|
198
|
+
return 5;
|
|
199
|
+
}
|
|
200
|
+
const fixed = (isResponse ? RESPONSE_FIXED_LENGTHS : REQUEST_FIXED_LENGTHS)[fc];
|
|
201
|
+
if (fixed !== undefined) {
|
|
202
|
+
return fixed;
|
|
203
|
+
}
|
|
204
|
+
const bc = (isResponse ? RESPONSE_BYTE_COUNT : REQUEST_BYTE_COUNT)[fc];
|
|
205
|
+
if (bc !== undefined) {
|
|
206
|
+
if (end - start <= bc.offset) {
|
|
207
|
+
return PREDICT_NEED_MORE;
|
|
208
|
+
}
|
|
209
|
+
return bc.extra + buffer[start + bc.offset];
|
|
210
|
+
}
|
|
211
|
+
if (isResponse && fc === FunctionCode.READ_DEVICE_IDENTIFICATION) {
|
|
212
|
+
return predictFc43_14Response(buffer, start, end);
|
|
213
|
+
}
|
|
214
|
+
return PREDICT_UNKNOWN;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
|
|
218
|
+
* response structure per Modbus V1.1b3 §6.21.
|
|
219
|
+
*
|
|
220
|
+
* Layout (after unit and fc):
|
|
221
|
+
* mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
|
|
222
|
+
* [objId(1) objLen(1) objData(objLen)] × numObjs
|
|
223
|
+
* CRC(2)
|
|
224
|
+
*/
|
|
225
|
+
function predictFc43_14Response(buffer, start, end) {
|
|
226
|
+
if (end - start < 8) {
|
|
227
|
+
return PREDICT_NEED_MORE;
|
|
228
|
+
}
|
|
229
|
+
if (buffer[start + 2] !== MEI_READ_DEVICE_ID) {
|
|
230
|
+
return PREDICT_UNKNOWN;
|
|
231
|
+
}
|
|
232
|
+
const numObjs = buffer[start + 7];
|
|
233
|
+
let cursor = start + 8;
|
|
234
|
+
for (let i = 0; i < numObjs; i++) {
|
|
235
|
+
if (end < cursor + 2) {
|
|
236
|
+
return PREDICT_NEED_MORE;
|
|
237
|
+
}
|
|
238
|
+
const objLen = buffer[cursor + 1];
|
|
239
|
+
cursor += 2 + objLen;
|
|
240
|
+
}
|
|
241
|
+
return cursor - start + 2;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Convert a callback-style `(cb: (err?) => void) => void` call into a Promise.
|
|
246
|
+
*/
|
|
247
|
+
function promisifyCb(fn) {
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
fn((err) => {
|
|
250
|
+
if (err)
|
|
251
|
+
reject(err);
|
|
252
|
+
else
|
|
253
|
+
resolve();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolve a single RTU timing parameter to milliseconds.
|
|
260
|
+
*
|
|
261
|
+
* Returns `undefined` when the caller did not supply the parameter at all
|
|
262
|
+
* (so the caller knows to fall back to a spec default). An explicit `0` or
|
|
263
|
+
* `{ value: 0 }` returns `0` — the parameter is set, just disabled.
|
|
264
|
+
*/
|
|
265
|
+
function resolveOne(value, baudRate, fastBaudMs) {
|
|
266
|
+
if (value === undefined) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
if (typeof value === 'number') {
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
if (value.unit === 'ms') {
|
|
273
|
+
return value.value;
|
|
274
|
+
}
|
|
275
|
+
// unit === 'bit' — needs baudRate to convert
|
|
276
|
+
if (baudRate === undefined) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return baudRate > 19200 ? fastBaudMs : Math.ceil(bitsToMs(baudRate, value.value));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Resolve Modbus RTU timing parameters from user options into milliseconds.
|
|
283
|
+
*
|
|
284
|
+
* - `intervalBetweenFrames` (t3.5): if omitted and `baudRate` is present,
|
|
285
|
+
* defaults to 38.5 bits per Modbus V1.02 §2.5.1.1. Pass `0` to disable.
|
|
286
|
+
* - `interCharTimeout` (t1.5): opt-in; only resolved when explicitly provided.
|
|
287
|
+
*
|
|
288
|
+
* Per the spec, at baud rates > 19200 fixed values are used
|
|
289
|
+
* (1.75 ms for t3.5, 0.75 ms for t1.5) regardless of the bit value.
|
|
290
|
+
*/
|
|
291
|
+
function resolveRtuTiming(opts = {}, baudRate) {
|
|
292
|
+
let intervalBetweenFrames = resolveOne(opts.intervalBetweenFrames, baudRate, 1.75);
|
|
293
|
+
if (intervalBetweenFrames === undefined) {
|
|
294
|
+
// Spec default: t3.5 derived from baudRate, or 0 when neither option nor
|
|
295
|
+
// baudRate were supplied.
|
|
296
|
+
intervalBetweenFrames = baudRate !== undefined ? (baudRate > 19200 ? 1.75 : Math.ceil(bitsToMs(baudRate, 38.5))) : 0;
|
|
297
|
+
}
|
|
298
|
+
let interCharTimeout = resolveOne(opts.interCharTimeout, baudRate, 0.75);
|
|
299
|
+
if (interCharTimeout === undefined) {
|
|
300
|
+
// t1.5 is opt-in — no spec-default fallback.
|
|
301
|
+
interCharTimeout = 0;
|
|
302
|
+
}
|
|
303
|
+
return { intervalBetweenFrames, interCharTimeout };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** @internal
|
|
307
|
+
* Zero-allocation binary min-heap for coalescing per-request timeouts.
|
|
308
|
+
*
|
|
309
|
+
* Uses two parallel numeric arrays (no object allocation per entry).
|
|
310
|
+
* Lazy deletion: callers never remove from the heap; expired entries
|
|
311
|
+
* are silently dropped when they surface at the top.
|
|
312
|
+
*/
|
|
313
|
+
class TimerHeap {
|
|
314
|
+
_deadlines = [];
|
|
315
|
+
_ids = [];
|
|
316
|
+
_timer = null;
|
|
317
|
+
_onFire;
|
|
318
|
+
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
319
|
+
_boundTick;
|
|
320
|
+
constructor(onFire) {
|
|
321
|
+
this._onFire = onFire;
|
|
322
|
+
this._boundTick = this._onTick.bind(this);
|
|
323
|
+
}
|
|
324
|
+
/** Number of pending timers in the heap. */
|
|
325
|
+
get size() {
|
|
326
|
+
return this._deadlines.length;
|
|
327
|
+
}
|
|
328
|
+
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
329
|
+
add(id, ms) {
|
|
330
|
+
const deadline = performance.now() + ms;
|
|
331
|
+
let i = this._deadlines.length;
|
|
332
|
+
this._deadlines.push(deadline);
|
|
333
|
+
this._ids.push(id);
|
|
334
|
+
// sift up
|
|
335
|
+
while (i > 0) {
|
|
336
|
+
const p = (i - 1) >> 1;
|
|
337
|
+
if (this._deadlines[p] <= deadline)
|
|
338
|
+
break;
|
|
339
|
+
this._deadlines[i] = this._deadlines[p];
|
|
340
|
+
this._ids[i] = this._ids[p];
|
|
341
|
+
i = p;
|
|
342
|
+
}
|
|
343
|
+
this._deadlines[i] = deadline;
|
|
344
|
+
this._ids[i] = id;
|
|
345
|
+
// Only reschedule when the new entry became the heap top.
|
|
346
|
+
if (i === 0)
|
|
347
|
+
this._refresh();
|
|
348
|
+
}
|
|
349
|
+
/** Dispose without firing callbacks. */
|
|
350
|
+
clear() {
|
|
351
|
+
if (this._timer) {
|
|
352
|
+
clearTimeout(this._timer);
|
|
353
|
+
this._timer = null;
|
|
354
|
+
}
|
|
355
|
+
this._deadlines.length = 0;
|
|
356
|
+
this._ids.length = 0;
|
|
357
|
+
}
|
|
358
|
+
_refresh() {
|
|
359
|
+
if (this._timer) {
|
|
360
|
+
clearTimeout(this._timer);
|
|
361
|
+
this._timer = null;
|
|
362
|
+
}
|
|
363
|
+
if (this._deadlines.length === 0)
|
|
364
|
+
return;
|
|
365
|
+
const delay = Math.max(0, Math.ceil(this._deadlines[0] - performance.now()));
|
|
366
|
+
this._timer = setTimeout(this._boundTick, delay);
|
|
367
|
+
}
|
|
368
|
+
_onTick() {
|
|
369
|
+
this._timer = null;
|
|
370
|
+
const now = performance.now();
|
|
371
|
+
while (this._deadlines.length > 0 && this._deadlines[0] <= now) {
|
|
372
|
+
const id = this._pop();
|
|
373
|
+
this._onFire(id);
|
|
374
|
+
}
|
|
375
|
+
this._refresh();
|
|
376
|
+
}
|
|
377
|
+
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
378
|
+
_pop() {
|
|
379
|
+
const topId = this._ids[0];
|
|
380
|
+
const lastId = this._ids.pop();
|
|
381
|
+
const lastDeadline = this._deadlines.pop();
|
|
382
|
+
const n = this._deadlines.length;
|
|
383
|
+
if (n > 0) {
|
|
384
|
+
let i = 0;
|
|
385
|
+
// sift down
|
|
386
|
+
while (true) {
|
|
387
|
+
let min = i;
|
|
388
|
+
const l = i * 2 + 1;
|
|
389
|
+
const r = l + 1;
|
|
390
|
+
if (l < n && this._deadlines[l] < this._deadlines[min])
|
|
391
|
+
min = l;
|
|
392
|
+
if (r < n && this._deadlines[r] < this._deadlines[min])
|
|
393
|
+
min = r;
|
|
394
|
+
if (min === i)
|
|
395
|
+
break;
|
|
396
|
+
this._deadlines[i] = this._deadlines[min];
|
|
397
|
+
this._ids[i] = this._ids[min];
|
|
398
|
+
i = min;
|
|
399
|
+
}
|
|
400
|
+
this._deadlines[i] = lastDeadline;
|
|
401
|
+
this._ids[i] = lastId;
|
|
402
|
+
}
|
|
403
|
+
return topId;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Normalize an IP address by stripping the IPv4-mapped IPv6 prefix.
|
|
409
|
+
* This ensures consistent comparison of addresses like `::ffff:192.168.1.1`.
|
|
410
|
+
*/
|
|
411
|
+
function normalizeAddress(address = '') {
|
|
412
|
+
return address.replace(/^::ffff:/, '');
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Check whether a remote address is allowed by the given whitelist.
|
|
416
|
+
* IPv4-mapped IPv6 addresses are normalized before comparison.
|
|
417
|
+
* Returns `true` when whitelist is absent or empty.
|
|
418
|
+
*/
|
|
419
|
+
function isWhitelisted(address, whitelist) {
|
|
420
|
+
if (!whitelist || whitelist.length === 0) {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
const normalized = normalizeAddress(address);
|
|
424
|
+
return whitelist.includes(normalized);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
exports.PREDICT_NEED_MORE = PREDICT_NEED_MORE;
|
|
428
|
+
exports.PREDICT_UNKNOWN = PREDICT_UNKNOWN;
|
|
429
|
+
exports.TimerHeap = TimerHeap;
|
|
430
|
+
exports.bitsToMs = bitsToMs;
|
|
431
|
+
exports.checkRange = checkRange;
|
|
432
|
+
exports.crc = crc;
|
|
433
|
+
exports.drainCbs = drainCbs;
|
|
434
|
+
exports.isUint8 = isUint8;
|
|
435
|
+
exports.isWhitelisted = isWhitelisted;
|
|
436
|
+
exports.lrc = lrc;
|
|
437
|
+
exports.predictRtuFrameLength = predictRtuFrameLength;
|
|
438
|
+
exports.promisifyCb = promisifyCb;
|
|
439
|
+
exports.resolveRtuTiming = resolveRtuTiming;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTU timing parameter — accepts either:
|
|
3
|
+
* - a bare `number` in milliseconds (`0` to disable the timer entirely)
|
|
4
|
+
* - `{ unit: 'ms', value: N }` — explicit milliseconds (equivalent to bare `N`)
|
|
5
|
+
* - `{ unit: 'bit', value: N }` — bit-time approximation, derived from `baudRate`
|
|
6
|
+
*
|
|
7
|
+
* The bare-number form is the recommended default; the object form exists for
|
|
8
|
+
* specs that quote bit-time. Pass `0` (or `{ unit: 'ms', value: 0 }`) to disable
|
|
9
|
+
* the timer; either form short-circuits the baudRate-derived fallback.
|
|
10
|
+
*/
|
|
11
|
+
type RtuTimingValue = number | {
|
|
12
|
+
unit: 'bit' | 'ms';
|
|
13
|
+
value: number;
|
|
14
|
+
};
|
|
15
|
+
/** User-facing RTU protocol options (supports both bit and ms units). */
|
|
16
|
+
interface RtuProtocolOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Inter-frame silence (Modbus RTU t3.5).
|
|
19
|
+
*
|
|
20
|
+
* - `20` or `{ unit: 'ms', value: 20 }` — 20 ms
|
|
21
|
+
* - `{ unit: 'bit', value: 38.5 }` — spec bit-time approximation (default when `baudRate` is provided)
|
|
22
|
+
* - `0` — disable t3.5 timing (immediate parse on every chunk; useful for
|
|
23
|
+
* lossless transports such as RTU-over-TCP or PTY-based tests where the
|
|
24
|
+
* wire's silence semantics do not apply)
|
|
25
|
+
*
|
|
26
|
+
* Per Modbus V1.02 §2.5.1.1, at baud rates > 19200 a fixed 1.75 ms is used
|
|
27
|
+
* regardless of the bit value.
|
|
28
|
+
*/
|
|
29
|
+
intervalBetweenFrames?: RtuTimingValue;
|
|
30
|
+
/**
|
|
31
|
+
* Inter-character timeout (Modbus RTU t1.5). Opt-in; **disabled** by default.
|
|
32
|
+
*
|
|
33
|
+
* - `1` or `{ unit: 'ms', value: 1 }` — 1 ms
|
|
34
|
+
* - `{ unit: 'bit', value: 21 }` — bit-time approximation (~1.5 char times)
|
|
35
|
+
* - `0` — disable explicitly
|
|
36
|
+
*
|
|
37
|
+
* Per Modbus V1.02 §2.5.1.1, at baud rates > 19200 a fixed 0.75 ms is used
|
|
38
|
+
* regardless of the bit value.
|
|
39
|
+
*/
|
|
40
|
+
interCharTimeout?: RtuTimingValue;
|
|
41
|
+
/**
|
|
42
|
+
* Buffer pool size per connection (bytes). Defaults to `MAX_FRAME_LENGTH * 2`
|
|
43
|
+
* (512 bytes). Increase this if you expect frames larger than 256 bytes or
|
|
44
|
+
* heavy pipelining on a single connection.
|
|
45
|
+
*/
|
|
46
|
+
poolSize?: number;
|
|
47
|
+
}
|
|
48
|
+
/** Resolved RTU timing values in milliseconds. */
|
|
49
|
+
interface ResolvedRtuTiming {
|
|
50
|
+
intervalBetweenFrames: number;
|
|
51
|
+
interCharTimeout: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve Modbus RTU timing parameters from user options into milliseconds.
|
|
55
|
+
*
|
|
56
|
+
* - `intervalBetweenFrames` (t3.5): if omitted and `baudRate` is present,
|
|
57
|
+
* defaults to 38.5 bits per Modbus V1.02 §2.5.1.1. Pass `0` to disable.
|
|
58
|
+
* - `interCharTimeout` (t1.5): opt-in; only resolved when explicitly provided.
|
|
59
|
+
*
|
|
60
|
+
* Per the spec, at baud rates > 19200 fixed values are used
|
|
61
|
+
* (1.75 ms for t3.5, 0.75 ms for t1.5) regardless of the bit value.
|
|
62
|
+
*/
|
|
63
|
+
declare function resolveRtuTiming(opts?: RtuProtocolOptions, baudRate?: number): ResolvedRtuTiming;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert a number of bits to milliseconds at a given baud rate.
|
|
67
|
+
*
|
|
68
|
+
* Used to derive Modbus RTU timing intervals from bit counts — e.g. 38.5 bits
|
|
69
|
+
* = 3.5 character times at 11 bits/char (t3.5 inter-frame silence), or 16.5
|
|
70
|
+
* bits = 1.5 character times (t1.5 inter-character timeout), per Modbus V1.02
|
|
71
|
+
* §2.5.1.1.
|
|
72
|
+
*
|
|
73
|
+
* @param baudRate Serial port baud rate.
|
|
74
|
+
* @param bits Number of bits to convert.
|
|
75
|
+
* @returns Duration in milliseconds.
|
|
76
|
+
*/
|
|
77
|
+
declare function bitsToMs(baudRate: number, bits: number): number;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Drain a pending-callback array: invoke each callback with the given error (or null).
|
|
81
|
+
*
|
|
82
|
+
* Handles `null`/`undefined` entries (from optional `cb?` parameters) gracefully.
|
|
83
|
+
* Used by physical layers and connections to resolve queued
|
|
84
|
+
* open / close / destroy callbacks.
|
|
85
|
+
*/
|
|
86
|
+
declare function drainCbs(cbs: (((err?: Error | null) => void) | undefined)[] | null, err?: Error | null): void;
|
|
87
|
+
|
|
88
|
+
declare function checkRange(value: number | number[], range?: [number, number] | [number, number][]): boolean;
|
|
89
|
+
|
|
90
|
+
declare function crc(data: Uint8Array, start?: number, end?: number): number;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns true when `n` is an integer in the unsigned-byte range [0, 255].
|
|
94
|
+
*
|
|
95
|
+
* Used for byte-level Modbus payload validation (function-code values, raw
|
|
96
|
+
* byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
|
|
97
|
+
* Infinity, and out-of-range values uniformly.
|
|
98
|
+
*/
|
|
99
|
+
declare function isUint8(n: number): boolean;
|
|
100
|
+
|
|
101
|
+
declare function lrc(data: Uint8Array, start?: number, end?: number): number;
|
|
102
|
+
|
|
103
|
+
/** Sentinel: caller needs to feed more bytes before length can be determined. */
|
|
104
|
+
declare const PREDICT_NEED_MORE = 0;
|
|
105
|
+
/** Sentinel: function code is not in the standard tables. */
|
|
106
|
+
declare const PREDICT_UNKNOWN = -1;
|
|
107
|
+
/**
|
|
108
|
+
* Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
|
|
109
|
+
*
|
|
110
|
+
* Returns a sentinel-encoded number to avoid per-call object allocation on the
|
|
111
|
+
* RTU decode hot path:
|
|
112
|
+
* - Positive integer (>= 4): total frame length, function code is known.
|
|
113
|
+
* - {@link PREDICT_NEED_MORE} (0): function code is known but more bytes are
|
|
114
|
+
* required (typically waiting on the byteCount byte).
|
|
115
|
+
* - {@link PREDICT_UNKNOWN} (-1): function code is not in the standard tables —
|
|
116
|
+
* the framing layer must defer to a registered `CustomFunctionCode` or treat
|
|
117
|
+
* this as a framing error.
|
|
118
|
+
*/
|
|
119
|
+
declare function predictRtuFrameLength(buffer: Buffer, start: number, end: number, isResponse: boolean): number;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Convert a callback-style `(cb: (err?) => void) => void` call into a Promise.
|
|
123
|
+
*/
|
|
124
|
+
declare function promisifyCb(fn: (cb: (err?: Error | null) => void) => void): Promise<void>;
|
|
125
|
+
|
|
126
|
+
/** @internal
|
|
127
|
+
* Zero-allocation binary min-heap for coalescing per-request timeouts.
|
|
128
|
+
*
|
|
129
|
+
* Uses two parallel numeric arrays (no object allocation per entry).
|
|
130
|
+
* Lazy deletion: callers never remove from the heap; expired entries
|
|
131
|
+
* are silently dropped when they surface at the top.
|
|
132
|
+
*/
|
|
133
|
+
declare class TimerHeap {
|
|
134
|
+
private _deadlines;
|
|
135
|
+
private _ids;
|
|
136
|
+
private _timer;
|
|
137
|
+
private _onFire;
|
|
138
|
+
/** Pre-bound tick handler to avoid creating a new arrow function on every setTimeout. */
|
|
139
|
+
private _boundTick;
|
|
140
|
+
constructor(onFire: (id: number) => void);
|
|
141
|
+
/** Number of pending timers in the heap. */
|
|
142
|
+
get size(): number;
|
|
143
|
+
/** Schedule an entry. If it becomes the new top, the global timer is re-scheduled (pre-emption). */
|
|
144
|
+
add(id: number, ms: number): void;
|
|
145
|
+
/** Dispose without firing callbacks. */
|
|
146
|
+
clear(): void;
|
|
147
|
+
private _refresh;
|
|
148
|
+
private _onTick;
|
|
149
|
+
/** Pop and return the id with the earliest deadline. Assumes heap is non-empty. */
|
|
150
|
+
private _pop;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check whether a remote address is allowed by the given whitelist.
|
|
155
|
+
* IPv4-mapped IPv6 addresses are normalized before comparison.
|
|
156
|
+
* Returns `true` when whitelist is absent or empty.
|
|
157
|
+
*/
|
|
158
|
+
declare function isWhitelisted(address: string | undefined, whitelist: string[] | undefined): boolean;
|
|
159
|
+
|
|
160
|
+
export { PREDICT_NEED_MORE, PREDICT_UNKNOWN, TimerHeap, bitsToMs, checkRange, crc, drainCbs, isUint8, isWhitelisted, lrc, predictRtuFrameLength, promisifyCb, resolveRtuTiming };
|
|
161
|
+
export type { ResolvedRtuTiming, RtuProtocolOptions };
|