njs-modbus 1.5.0 → 2.0.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 +16 -0
- package/dist/index.cjs +1938 -1148
- package/dist/index.d.ts +308 -89
- package/dist/index.mjs +1931 -1149
- package/dist/src/error-code.d.ts +27 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/layers/application/abstract-application-layer.d.ts +11 -8
- package/dist/src/layers/application/ascii-application-layer.d.ts +14 -10
- package/dist/src/layers/application/index.d.ts +2 -0
- package/dist/src/layers/application/rtu-application-layer.d.ts +52 -18
- package/dist/src/layers/application/tcp-application-layer.d.ts +4 -8
- package/dist/src/layers/physical/abstract-physical-layer.d.ts +5 -1
- package/dist/src/layers/physical/index.d.ts +1 -0
- package/dist/src/layers/physical/serial-physical-layer.d.ts +2 -0
- package/dist/src/layers/physical/tcp-client-physical-layer.d.ts +3 -0
- package/dist/src/layers/physical/tcp-server-physical-layer.d.ts +3 -1
- package/dist/src/layers/physical/udp-physical-layer.d.ts +25 -10
- package/dist/src/master/index.d.ts +2 -0
- package/dist/src/master/master-session.d.ts +18 -0
- package/dist/src/master/master.d.ts +27 -5
- package/dist/src/slave/index.d.ts +1 -1
- package/dist/src/slave/slave.d.ts +18 -5
- package/dist/src/types.d.ts +33 -2
- package/dist/src/utils/bitsToMs.d.ts +13 -0
- package/dist/src/utils/crc.d.ts +1 -1
- package/dist/src/utils/genConnectionId.d.ts +2 -0
- package/dist/src/utils/index.d.ts +5 -1
- package/dist/src/utils/isUint8.d.ts +8 -0
- package/dist/src/utils/predictRtuFrameLength.d.ts +20 -0
- package/dist/src/vars.d.ts +47 -0
- package/dist/test/adu-buffer.test.d.ts +1 -0
- package/dist/test/ascii-hex-sentry.test.d.ts +1 -0
- package/dist/test/ascii-hex-validation.test.d.ts +1 -0
- package/dist/test/ascii-tcp-fragmentation.test.d.ts +1 -0
- package/dist/test/check-range.test.d.ts +1 -0
- package/dist/test/fallback-atomic.test.d.ts +1 -0
- package/dist/test/fallback-serial.test.d.ts +1 -0
- package/dist/test/fc17-serverid-validation.test.d.ts +1 -0
- package/dist/test/fc43-conformity.test.d.ts +1 -0
- package/dist/test/fc43-utf8-objects.test.d.ts +1 -0
- package/dist/test/gen-connection-id.test.d.ts +1 -0
- package/dist/test/helpers/raw-tcp.d.ts +38 -0
- package/dist/test/master-concurrent.test.d.ts +1 -0
- package/dist/test/modbus-error.test.d.ts +1 -0
- package/dist/test/physical-lifecycle.test.d.ts +1 -0
- package/dist/test/predict-rtu.test.d.ts +1 -0
- package/dist/test/rtu-custom-fc.test.d.ts +1 -0
- package/dist/test/rtu-pool-overflow.test.d.ts +1 -0
- package/dist/test/rtu-t15-timing.test.d.ts +1 -0
- package/dist/test/rtu-t35-default.test.d.ts +1 -0
- package/dist/test/rtu-t35-strict.test.d.ts +1 -0
- package/dist/test/rtu-tcp-fragmentation.test.d.ts +1 -0
- package/dist/test/serial-e2e.test.d.ts +1 -0
- package/dist/test/slave-multi-connection.test.d.ts +1 -0
- package/dist/test/tcp-fragmentation.test.d.ts +1 -0
- package/dist/test/udp-multi-client.test.d.ts +1 -0
- package/package.json +4 -2
- package/dist/src/utils/getThreePointFiveT.d.ts +0 -7
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
2
|
import { SerialPort } from 'serialport';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { Socket, createServer } from 'node:net';
|
|
4
5
|
import { createSocket } from 'node:dgram';
|
|
5
6
|
|
|
@@ -15,20 +16,293 @@ var ErrorCode;
|
|
|
15
16
|
ErrorCode[ErrorCode["GATEWAY_PATH_UNAVAILABLE"] = 10] = "GATEWAY_PATH_UNAVAILABLE";
|
|
16
17
|
ErrorCode[ErrorCode["GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND"] = 11] = "GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND";
|
|
17
18
|
})(ErrorCode || (ErrorCode = {}));
|
|
18
|
-
|
|
19
|
+
/** Internal error codes for programmatic error handling. */
|
|
20
|
+
const ModbusErrorCode = {
|
|
21
|
+
TIMEOUT: 'ETIMEOUT',
|
|
22
|
+
INVALID_RESPONSE: 'EINVALID_RESPONSE',
|
|
23
|
+
INSUFFICIENT_DATA: 'EINSUFFICIENT_DATA',
|
|
24
|
+
MASTER_CLOSED: 'EMASTER_CLOSED',
|
|
25
|
+
MASTER_DESTROYED: 'EMASTER_DESTROYED',
|
|
26
|
+
CONCURRENT_NOT_TCP: 'ECONCURRENT_NOT_TCP',
|
|
27
|
+
PORT_DESTROYED: 'EPORT_DESTROYED',
|
|
28
|
+
PORT_ALREADY_OPEN: 'EPORT_ALREADY_OPEN',
|
|
29
|
+
PORT_NOT_OPEN: 'EPORT_NOT_OPEN',
|
|
30
|
+
NOT_SUPPORTED: 'ENOT_SUPPORTED',
|
|
31
|
+
INVALID_DATA: 'EINVALID_DATA',
|
|
32
|
+
INVALID_HEX: 'EINVALID_HEX',
|
|
33
|
+
CRC_MISMATCH: 'ECRC_MISMATCH',
|
|
34
|
+
LRC_MISMATCH: 'ELRC_MISMATCH',
|
|
35
|
+
INCOMPLETE_FRAME: 'EINCOMPLETE_FRAME',
|
|
36
|
+
T1_5_EXCEEDED: 'ET1_5_EXCEEDED',
|
|
37
|
+
UNKNOWN_FC: 'EUNKNOWN_FC',
|
|
38
|
+
INVALID_ROLE: 'EINVALID_ROLE',
|
|
39
|
+
RANGE: 'ERANGE',
|
|
40
|
+
};
|
|
41
|
+
const MODBUS_PREFIX = 'MODBUS_ERROR_CODE_';
|
|
42
|
+
class ModbusError extends Error {
|
|
43
|
+
constructor(code, message) {
|
|
44
|
+
super(message !== null && message !== void 0 ? message : code);
|
|
45
|
+
Object.defineProperty(this, "code", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
value: code
|
|
50
|
+
});
|
|
51
|
+
this.name = 'ModbusError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
19
54
|
function getErrorByCode(code) {
|
|
20
|
-
return new
|
|
55
|
+
return new ModbusError(String(code), `${MODBUS_PREFIX}${code}`);
|
|
21
56
|
}
|
|
22
57
|
function getCodeByError(err) {
|
|
23
|
-
if (err
|
|
24
|
-
|
|
58
|
+
if (err instanceof ModbusError) {
|
|
59
|
+
const num = Number(err.code);
|
|
60
|
+
if (!Number.isNaN(num) && num in ErrorCode) {
|
|
61
|
+
return num;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (err.message.startsWith(MODBUS_PREFIX)) {
|
|
65
|
+
return Number(err.message.slice(MODBUS_PREFIX.length));
|
|
25
66
|
}
|
|
26
67
|
return ErrorCode.SERVER_DEVICE_FAILURE;
|
|
27
68
|
}
|
|
28
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Standard Modbus function codes (V1.1b3 §6).
|
|
72
|
+
*/
|
|
73
|
+
var FunctionCode;
|
|
74
|
+
(function (FunctionCode) {
|
|
75
|
+
FunctionCode[FunctionCode["READ_COILS"] = 1] = "READ_COILS";
|
|
76
|
+
FunctionCode[FunctionCode["READ_DISCRETE_INPUTS"] = 2] = "READ_DISCRETE_INPUTS";
|
|
77
|
+
FunctionCode[FunctionCode["READ_HOLDING_REGISTERS"] = 3] = "READ_HOLDING_REGISTERS";
|
|
78
|
+
FunctionCode[FunctionCode["READ_INPUT_REGISTERS"] = 4] = "READ_INPUT_REGISTERS";
|
|
79
|
+
FunctionCode[FunctionCode["WRITE_SINGLE_COIL"] = 5] = "WRITE_SINGLE_COIL";
|
|
80
|
+
FunctionCode[FunctionCode["WRITE_SINGLE_REGISTER"] = 6] = "WRITE_SINGLE_REGISTER";
|
|
81
|
+
FunctionCode[FunctionCode["WRITE_MULTIPLE_COILS"] = 15] = "WRITE_MULTIPLE_COILS";
|
|
82
|
+
FunctionCode[FunctionCode["WRITE_MULTIPLE_REGISTERS"] = 16] = "WRITE_MULTIPLE_REGISTERS";
|
|
83
|
+
FunctionCode[FunctionCode["REPORT_SERVER_ID"] = 17] = "REPORT_SERVER_ID";
|
|
84
|
+
FunctionCode[FunctionCode["MASK_WRITE_REGISTER"] = 22] = "MASK_WRITE_REGISTER";
|
|
85
|
+
FunctionCode[FunctionCode["READ_WRITE_MULTIPLE_REGISTERS"] = 23] = "READ_WRITE_MULTIPLE_REGISTERS";
|
|
86
|
+
FunctionCode[FunctionCode["READ_DEVICE_IDENTIFICATION"] = 43] = "READ_DEVICE_IDENTIFICATION";
|
|
87
|
+
})(FunctionCode || (FunctionCode = {}));
|
|
88
|
+
/** Exception response FC = request FC | EXCEPTION_OFFSET (V1.1b3 §7). */
|
|
89
|
+
const EXCEPTION_OFFSET = 0x80;
|
|
90
|
+
/** Coil value encoding for FC 5 / FC 15 (V1.1b3 §6.5/§6.11). */
|
|
91
|
+
const COIL_ON = 0xff00;
|
|
92
|
+
const COIL_OFF = 0x0000;
|
|
93
|
+
/** FC 0x2B MEI sub-function selecting Read Device Identification (V1.1b3 §6.21). */
|
|
94
|
+
const MEI_READ_DEVICE_ID = 0x0e;
|
|
95
|
+
/** Read Device ID code values inside an FC 0x2B / MEI 0x0E request. */
|
|
96
|
+
var ReadDeviceIDCode;
|
|
97
|
+
(function (ReadDeviceIDCode) {
|
|
98
|
+
ReadDeviceIDCode[ReadDeviceIDCode["BASIC_STREAM"] = 1] = "BASIC_STREAM";
|
|
99
|
+
ReadDeviceIDCode[ReadDeviceIDCode["REGULAR_STREAM"] = 2] = "REGULAR_STREAM";
|
|
100
|
+
ReadDeviceIDCode[ReadDeviceIDCode["EXTENDED_STREAM"] = 3] = "EXTENDED_STREAM";
|
|
101
|
+
ReadDeviceIDCode[ReadDeviceIDCode["SPECIFIC_ACCESS"] = 4] = "SPECIFIC_ACCESS";
|
|
102
|
+
})(ReadDeviceIDCode || (ReadDeviceIDCode = {}));
|
|
103
|
+
/** Conformity level reported in an FC 0x2B / MEI 0x0E response. */
|
|
104
|
+
var ConformityLevel;
|
|
105
|
+
(function (ConformityLevel) {
|
|
106
|
+
ConformityLevel[ConformityLevel["BASIC"] = 129] = "BASIC";
|
|
107
|
+
ConformityLevel[ConformityLevel["REGULAR"] = 130] = "REGULAR";
|
|
108
|
+
ConformityLevel[ConformityLevel["EXTENDED"] = 131] = "EXTENDED";
|
|
109
|
+
})(ConformityLevel || (ConformityLevel = {}));
|
|
110
|
+
/** Modbus V1.1b3 PDU quantity limits. */
|
|
111
|
+
const LIMITS = {
|
|
112
|
+
READ_COILS_MIN: 0x0001,
|
|
113
|
+
READ_COILS_MAX: 0x07d0,
|
|
114
|
+
READ_REGISTERS_MIN: 0x0001,
|
|
115
|
+
READ_REGISTERS_MAX: 0x007d,
|
|
116
|
+
WRITE_COILS_MAX: 0x07b0,
|
|
117
|
+
WRITE_REGISTERS_MAX: 0x007b,
|
|
118
|
+
RW_REGISTERS_WRITE_MAX: 0x0079,
|
|
119
|
+
};
|
|
120
|
+
|
|
29
121
|
class AbstractPhysicalLayer extends EventEmitter {
|
|
30
122
|
}
|
|
31
123
|
|
|
124
|
+
function inRange(n, [min, max]) {
|
|
125
|
+
return n >= min && n <= max;
|
|
126
|
+
}
|
|
127
|
+
function checkRange(value, range) {
|
|
128
|
+
if (!range) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const values = Array.isArray(value) ? value : [value];
|
|
132
|
+
if (typeof range[0] === 'number' && typeof range[1] === 'number') {
|
|
133
|
+
const [min, max] = range;
|
|
134
|
+
const [lo, hi] = min <= max ? [min, max] : [max, min];
|
|
135
|
+
return values.every((n) => inRange(n, [lo, hi]));
|
|
136
|
+
}
|
|
137
|
+
else if (range.length > 0) {
|
|
138
|
+
for (const r of range) {
|
|
139
|
+
const [min, max] = r;
|
|
140
|
+
const [lo, hi] = min <= max ? [min, max] : [max, min];
|
|
141
|
+
if (values.every((n) => inRange(n, [lo, hi]))) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const TABLE = [
|
|
151
|
+
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01,
|
|
152
|
+
0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0,
|
|
153
|
+
0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
|
|
154
|
+
0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141,
|
|
155
|
+
0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01,
|
|
156
|
+
0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0,
|
|
157
|
+
0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681,
|
|
158
|
+
0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
|
|
159
|
+
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
|
|
160
|
+
0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0,
|
|
161
|
+
0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381,
|
|
162
|
+
0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741,
|
|
163
|
+
0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901,
|
|
164
|
+
0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
|
|
165
|
+
0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
|
|
166
|
+
0x4040,
|
|
167
|
+
];
|
|
168
|
+
function crc(data, seed = 0xffff) {
|
|
169
|
+
let crc = seed;
|
|
170
|
+
for (let index = 0; index < data.length; index++) {
|
|
171
|
+
crc = (TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8)) & 0xffff;
|
|
172
|
+
}
|
|
173
|
+
return crc;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Cross-process unique id: `${prefix}-${uuid-v4}`. */
|
|
177
|
+
function genConnectionId(prefix) {
|
|
178
|
+
return `${prefix}-${randomUUID()}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Convert a number of bits to milliseconds at a given baud rate.
|
|
183
|
+
*
|
|
184
|
+
* Used to derive Modbus RTU timing intervals from bit counts — e.g. 38.5 bits
|
|
185
|
+
* = 3.5 character times at 11 bits/char (t3.5 inter-frame silence), or 16.5
|
|
186
|
+
* bits = 1.5 character times (t1.5 inter-character timeout), per Modbus V1.02
|
|
187
|
+
* §2.5.1.1.
|
|
188
|
+
*
|
|
189
|
+
* @param baudRate Serial port baud rate.
|
|
190
|
+
* @param bits Number of bits to convert.
|
|
191
|
+
* @returns Duration in milliseconds.
|
|
192
|
+
*/
|
|
193
|
+
function bitsToMs(baudRate, bits) {
|
|
194
|
+
return (bits * 1000) / baudRate;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns true when `n` is an integer in the unsigned-byte range [0, 255].
|
|
199
|
+
*
|
|
200
|
+
* Used for byte-level Modbus payload validation (function-code values, raw
|
|
201
|
+
* byte arrays in FC17/FC43 responses) — rejects negative, fractional, NaN,
|
|
202
|
+
* Infinity, and out-of-range values uniformly.
|
|
203
|
+
*/
|
|
204
|
+
function isUint8(n) {
|
|
205
|
+
return Number.isInteger(n) && n >= 0 && n <= 255;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function lrc(data) {
|
|
209
|
+
return (~data.reduce((sum, n) => sum + n, 0) + 1) & 0xff;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const REQUEST_FIXED_LENGTHS = {
|
|
213
|
+
[FunctionCode.READ_COILS]: 8,
|
|
214
|
+
[FunctionCode.READ_DISCRETE_INPUTS]: 8,
|
|
215
|
+
[FunctionCode.READ_HOLDING_REGISTERS]: 8,
|
|
216
|
+
[FunctionCode.READ_INPUT_REGISTERS]: 8,
|
|
217
|
+
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
218
|
+
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
219
|
+
[FunctionCode.REPORT_SERVER_ID]: 4,
|
|
220
|
+
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
221
|
+
[FunctionCode.READ_DEVICE_IDENTIFICATION]: 7,
|
|
222
|
+
};
|
|
223
|
+
const REQUEST_BYTE_COUNT = {
|
|
224
|
+
[FunctionCode.WRITE_MULTIPLE_COILS]: { offset: 6, extra: 9 },
|
|
225
|
+
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: { offset: 6, extra: 9 },
|
|
226
|
+
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 10, extra: 13 },
|
|
227
|
+
};
|
|
228
|
+
const RESPONSE_FIXED_LENGTHS = {
|
|
229
|
+
[FunctionCode.WRITE_SINGLE_COIL]: 8,
|
|
230
|
+
[FunctionCode.WRITE_SINGLE_REGISTER]: 8,
|
|
231
|
+
[FunctionCode.WRITE_MULTIPLE_COILS]: 8,
|
|
232
|
+
[FunctionCode.WRITE_MULTIPLE_REGISTERS]: 8,
|
|
233
|
+
[FunctionCode.MASK_WRITE_REGISTER]: 10,
|
|
234
|
+
};
|
|
235
|
+
const RESPONSE_BYTE_COUNT = {
|
|
236
|
+
[FunctionCode.READ_COILS]: { offset: 2, extra: 5 },
|
|
237
|
+
[FunctionCode.READ_DISCRETE_INPUTS]: { offset: 2, extra: 5 },
|
|
238
|
+
[FunctionCode.READ_HOLDING_REGISTERS]: { offset: 2, extra: 5 },
|
|
239
|
+
[FunctionCode.READ_INPUT_REGISTERS]: { offset: 2, extra: 5 },
|
|
240
|
+
[FunctionCode.REPORT_SERVER_ID]: { offset: 2, extra: 5 },
|
|
241
|
+
[FunctionCode.READ_WRITE_MULTIPLE_REGISTERS]: { offset: 2, extra: 5 },
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Predict the total RTU frame length (PDU + 2-byte CRC) given the leading bytes.
|
|
245
|
+
*
|
|
246
|
+
* Returns a discriminated result so callers can distinguish:
|
|
247
|
+
* - `{ kind: 'length' }`: function code is known and total frame length is determined.
|
|
248
|
+
* - `{ kind: 'need-more' }`: function code is known, but more bytes are required
|
|
249
|
+
* (typically waiting on the byteCount byte).
|
|
250
|
+
* - `{ kind: 'unknown' }`: function code is not in the standard tables — the
|
|
251
|
+
* framing layer must defer to a registered `CustomFunctionCode` or treat this
|
|
252
|
+
* as a framing error.
|
|
253
|
+
*/
|
|
254
|
+
function predictRtuFrameLength(buffer, isResponse) {
|
|
255
|
+
if (buffer.length < 2) {
|
|
256
|
+
return { kind: 'need-more' };
|
|
257
|
+
}
|
|
258
|
+
const fc = buffer[1];
|
|
259
|
+
if (isResponse && (fc & EXCEPTION_OFFSET) !== 0) {
|
|
260
|
+
return { kind: 'length', length: 5 };
|
|
261
|
+
}
|
|
262
|
+
const fixed = (isResponse ? RESPONSE_FIXED_LENGTHS : REQUEST_FIXED_LENGTHS)[fc];
|
|
263
|
+
if (fixed !== undefined) {
|
|
264
|
+
return { kind: 'length', length: fixed };
|
|
265
|
+
}
|
|
266
|
+
const bc = (isResponse ? RESPONSE_BYTE_COUNT : REQUEST_BYTE_COUNT)[fc];
|
|
267
|
+
if (bc !== undefined) {
|
|
268
|
+
if (buffer.length <= bc.offset) {
|
|
269
|
+
return { kind: 'need-more' };
|
|
270
|
+
}
|
|
271
|
+
return { kind: 'length', length: bc.extra + buffer[bc.offset] };
|
|
272
|
+
}
|
|
273
|
+
if (isResponse && fc === FunctionCode.READ_DEVICE_IDENTIFICATION) {
|
|
274
|
+
return predictFc43_14Response(buffer);
|
|
275
|
+
}
|
|
276
|
+
return { kind: 'unknown' };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Walk the variable-length FC 0x2B / MEI 0x0E (Read Device Identification)
|
|
280
|
+
* response structure per Modbus V1.1b3 §6.21.
|
|
281
|
+
*
|
|
282
|
+
* Layout (after unit and fc):
|
|
283
|
+
* mei(1) rdic(1) conformity(1) more(1) nextObjId(1) numObjs(1)
|
|
284
|
+
* [objId(1) objLen(1) objData(objLen)] × numObjs
|
|
285
|
+
* CRC(2)
|
|
286
|
+
*/
|
|
287
|
+
function predictFc43_14Response(buffer) {
|
|
288
|
+
if (buffer.length < 8) {
|
|
289
|
+
return { kind: 'need-more' };
|
|
290
|
+
}
|
|
291
|
+
if (buffer[2] !== MEI_READ_DEVICE_ID) {
|
|
292
|
+
return { kind: 'unknown' };
|
|
293
|
+
}
|
|
294
|
+
const numObjs = buffer[7];
|
|
295
|
+
let offset = 8;
|
|
296
|
+
for (let i = 0; i < numObjs; i++) {
|
|
297
|
+
if (buffer.length < offset + 2) {
|
|
298
|
+
return { kind: 'need-more' };
|
|
299
|
+
}
|
|
300
|
+
const objLen = buffer[offset + 1];
|
|
301
|
+
offset += 2 + objLen;
|
|
302
|
+
}
|
|
303
|
+
return { kind: 'length', length: offset + 2 };
|
|
304
|
+
}
|
|
305
|
+
|
|
32
306
|
class SerialPhysicalLayer extends AbstractPhysicalLayer {
|
|
33
307
|
get isOpen() {
|
|
34
308
|
return this._serialport.isOpen;
|
|
@@ -53,6 +327,18 @@ class SerialPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
53
327
|
writable: true,
|
|
54
328
|
value: void 0
|
|
55
329
|
});
|
|
330
|
+
Object.defineProperty(this, "_connection", {
|
|
331
|
+
enumerable: true,
|
|
332
|
+
configurable: true,
|
|
333
|
+
writable: true,
|
|
334
|
+
value: null
|
|
335
|
+
});
|
|
336
|
+
Object.defineProperty(this, "_isOpening", {
|
|
337
|
+
enumerable: true,
|
|
338
|
+
configurable: true,
|
|
339
|
+
writable: true,
|
|
340
|
+
value: false
|
|
341
|
+
});
|
|
56
342
|
Object.defineProperty(this, "_destroyed", {
|
|
57
343
|
enumerable: true,
|
|
58
344
|
configurable: true,
|
|
@@ -69,27 +355,41 @@ class SerialPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
69
355
|
this._baudRate = options.baudRate;
|
|
70
356
|
}
|
|
71
357
|
open() {
|
|
72
|
-
if (this.
|
|
73
|
-
return Promise.reject(new
|
|
358
|
+
if (this._destroyed) {
|
|
359
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_DESTROYED, 'Port is destroyed'));
|
|
360
|
+
}
|
|
361
|
+
if (this.isOpen || this._isOpening) {
|
|
362
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_ALREADY_OPEN, 'Port is already open'));
|
|
74
363
|
}
|
|
364
|
+
this._isOpening = true;
|
|
365
|
+
const connection = { id: genConnectionId('serial') };
|
|
366
|
+
this._connection = connection;
|
|
75
367
|
return new Promise((resolve, reject) => {
|
|
76
368
|
this._serialport.open((error) => {
|
|
77
369
|
if (error) {
|
|
370
|
+
this._isOpening = false;
|
|
371
|
+
if (this._connection === connection) {
|
|
372
|
+
this._connection = null;
|
|
373
|
+
}
|
|
78
374
|
reject(error);
|
|
375
|
+
return;
|
|
79
376
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
377
|
+
this._isOpening = false;
|
|
378
|
+
this._serialport.on('data', (data) => {
|
|
379
|
+
this.emit('data', data, (d) => this.write(d), connection);
|
|
380
|
+
});
|
|
381
|
+
this._serialport.on('error', (err) => {
|
|
382
|
+
this.emit('error', err);
|
|
383
|
+
});
|
|
384
|
+
this._serialport.once('close', () => {
|
|
385
|
+
if (this._connection === connection) {
|
|
88
386
|
this._serialport.removeAllListeners();
|
|
387
|
+
this._connection = null;
|
|
388
|
+
this.emit('connection-close', connection);
|
|
89
389
|
this.emit('close');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
resolve();
|
|
93
393
|
});
|
|
94
394
|
});
|
|
95
395
|
}
|
|
@@ -107,22 +407,24 @@ class SerialPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
107
407
|
});
|
|
108
408
|
}
|
|
109
409
|
else {
|
|
110
|
-
reject(new
|
|
410
|
+
reject(new ModbusError(ModbusErrorCode.PORT_NOT_OPEN, 'Port is not open'));
|
|
111
411
|
}
|
|
112
412
|
});
|
|
113
413
|
}
|
|
114
414
|
close() {
|
|
415
|
+
if (!this._serialport.isOpen) {
|
|
416
|
+
return Promise.resolve();
|
|
417
|
+
}
|
|
115
418
|
return new Promise((resolve) => {
|
|
116
|
-
this._serialport.
|
|
117
|
-
this._serialport.close(
|
|
118
|
-
resolve();
|
|
119
|
-
});
|
|
419
|
+
this._serialport.once('close', () => resolve());
|
|
420
|
+
this._serialport.close();
|
|
120
421
|
});
|
|
121
422
|
}
|
|
122
423
|
destroy() {
|
|
123
424
|
this._destroyed = true;
|
|
124
|
-
this.
|
|
125
|
-
|
|
425
|
+
return this.close().then(() => {
|
|
426
|
+
this.removeAllListeners();
|
|
427
|
+
});
|
|
126
428
|
}
|
|
127
429
|
}
|
|
128
430
|
|
|
@@ -145,7 +447,13 @@ class TcpClientPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
145
447
|
enumerable: true,
|
|
146
448
|
configurable: true,
|
|
147
449
|
writable: true,
|
|
148
|
-
value:
|
|
450
|
+
value: null
|
|
451
|
+
});
|
|
452
|
+
Object.defineProperty(this, "_connection", {
|
|
453
|
+
enumerable: true,
|
|
454
|
+
configurable: true,
|
|
455
|
+
writable: true,
|
|
456
|
+
value: null
|
|
149
457
|
});
|
|
150
458
|
Object.defineProperty(this, "_isOpen", {
|
|
151
459
|
enumerable: true,
|
|
@@ -153,46 +461,79 @@ class TcpClientPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
153
461
|
writable: true,
|
|
154
462
|
value: false
|
|
155
463
|
});
|
|
464
|
+
Object.defineProperty(this, "_isOpening", {
|
|
465
|
+
enumerable: true,
|
|
466
|
+
configurable: true,
|
|
467
|
+
writable: true,
|
|
468
|
+
value: false
|
|
469
|
+
});
|
|
156
470
|
Object.defineProperty(this, "_destroyed", {
|
|
157
471
|
enumerable: true,
|
|
158
472
|
configurable: true,
|
|
159
473
|
writable: true,
|
|
160
474
|
value: false
|
|
161
475
|
});
|
|
162
|
-
this
|
|
476
|
+
Object.defineProperty(this, "_socketOptions", {
|
|
477
|
+
enumerable: true,
|
|
478
|
+
configurable: true,
|
|
479
|
+
writable: true,
|
|
480
|
+
value: void 0
|
|
481
|
+
});
|
|
482
|
+
this._socketOptions = options;
|
|
163
483
|
}
|
|
164
484
|
open(options) {
|
|
165
|
-
if (this.
|
|
166
|
-
return Promise.reject(new
|
|
485
|
+
if (this._destroyed) {
|
|
486
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_DESTROYED, 'Port is destroyed'));
|
|
487
|
+
}
|
|
488
|
+
if (this._isOpen || this._isOpening) {
|
|
489
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_ALREADY_OPEN, 'Port is already open'));
|
|
167
490
|
}
|
|
491
|
+
this._isOpening = true;
|
|
492
|
+
const socket = new Socket(this._socketOptions);
|
|
493
|
+
const connection = { id: genConnectionId('tcp-client') };
|
|
494
|
+
this._socket = socket;
|
|
495
|
+
this._connection = connection;
|
|
168
496
|
return new Promise((resolve, reject) => {
|
|
169
|
-
let
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this._isOpen = true;
|
|
173
|
-
this._socket.on('data', (data) => {
|
|
174
|
-
this.emit('data', data, (data) => this.write(data));
|
|
175
|
-
});
|
|
176
|
-
this._socket.on('close', () => {
|
|
177
|
-
this._isOpen = false;
|
|
178
|
-
this._socket.removeAllListeners();
|
|
179
|
-
this.emit('close');
|
|
180
|
-
});
|
|
181
|
-
resolve();
|
|
497
|
+
let connected = false;
|
|
498
|
+
socket.on('data', (data) => {
|
|
499
|
+
this.emit('data', data, (d) => this.write(d), connection);
|
|
182
500
|
});
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
this.emit('error',
|
|
501
|
+
socket.on('error', (err) => {
|
|
502
|
+
if (connected) {
|
|
503
|
+
this.emit('error', err);
|
|
186
504
|
}
|
|
187
505
|
else {
|
|
188
|
-
|
|
506
|
+
if (this._connection === connection) {
|
|
507
|
+
this._isOpening = false;
|
|
508
|
+
this._socket = null;
|
|
509
|
+
this._connection = null;
|
|
510
|
+
}
|
|
511
|
+
socket.removeAllListeners();
|
|
512
|
+
reject(err);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
socket.once('close', () => {
|
|
516
|
+
if (this._connection === connection) {
|
|
517
|
+
this._isOpen = false;
|
|
518
|
+
this._isOpening = false;
|
|
519
|
+
this._socket = null;
|
|
520
|
+
this._connection = null;
|
|
521
|
+
socket.removeAllListeners();
|
|
522
|
+
this.emit('connection-close', connection);
|
|
523
|
+
this.emit('close');
|
|
189
524
|
}
|
|
190
525
|
});
|
|
526
|
+
socket.connect(options !== null && options !== void 0 ? options : { port: 502 }, () => {
|
|
527
|
+
connected = true;
|
|
528
|
+
this._isOpening = false;
|
|
529
|
+
this._isOpen = true;
|
|
530
|
+
resolve();
|
|
531
|
+
});
|
|
191
532
|
});
|
|
192
533
|
}
|
|
193
534
|
write(data) {
|
|
194
535
|
return new Promise((resolve, reject) => {
|
|
195
|
-
if (this.
|
|
536
|
+
if (this._isOpen && this._socket) {
|
|
196
537
|
this._socket.write(data, (error) => {
|
|
197
538
|
if (error) {
|
|
198
539
|
reject(error);
|
|
@@ -204,22 +545,25 @@ class TcpClientPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
204
545
|
});
|
|
205
546
|
}
|
|
206
547
|
else {
|
|
207
|
-
reject(new
|
|
548
|
+
reject(new ModbusError(ModbusErrorCode.PORT_NOT_OPEN, 'Port is not open'));
|
|
208
549
|
}
|
|
209
550
|
});
|
|
210
551
|
}
|
|
211
552
|
close() {
|
|
553
|
+
if (!this._socket) {
|
|
554
|
+
return Promise.resolve();
|
|
555
|
+
}
|
|
556
|
+
const socket = this._socket;
|
|
212
557
|
return new Promise((resolve) => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this._socket.destroy();
|
|
216
|
-
resolve();
|
|
558
|
+
socket.once('close', () => resolve());
|
|
559
|
+
socket.destroy();
|
|
217
560
|
});
|
|
218
561
|
}
|
|
219
562
|
destroy() {
|
|
220
563
|
this._destroyed = true;
|
|
221
|
-
this.
|
|
222
|
-
|
|
564
|
+
return this.close().then(() => {
|
|
565
|
+
this.removeAllListeners();
|
|
566
|
+
});
|
|
223
567
|
}
|
|
224
568
|
}
|
|
225
569
|
|
|
@@ -242,7 +586,7 @@ class TcpServerPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
242
586
|
enumerable: true,
|
|
243
587
|
configurable: true,
|
|
244
588
|
writable: true,
|
|
245
|
-
value:
|
|
589
|
+
value: null
|
|
246
590
|
});
|
|
247
591
|
Object.defineProperty(this, "_isOpen", {
|
|
248
592
|
enumerable: true,
|
|
@@ -250,92 +594,129 @@ class TcpServerPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
250
594
|
writable: true,
|
|
251
595
|
value: false
|
|
252
596
|
});
|
|
597
|
+
Object.defineProperty(this, "_isOpening", {
|
|
598
|
+
enumerable: true,
|
|
599
|
+
configurable: true,
|
|
600
|
+
writable: true,
|
|
601
|
+
value: false
|
|
602
|
+
});
|
|
253
603
|
Object.defineProperty(this, "_destroyed", {
|
|
254
604
|
enumerable: true,
|
|
255
605
|
configurable: true,
|
|
256
606
|
writable: true,
|
|
257
607
|
value: false
|
|
258
608
|
});
|
|
259
|
-
Object.defineProperty(this, "
|
|
609
|
+
Object.defineProperty(this, "_connections", {
|
|
610
|
+
enumerable: true,
|
|
611
|
+
configurable: true,
|
|
612
|
+
writable: true,
|
|
613
|
+
value: new Map()
|
|
614
|
+
});
|
|
615
|
+
Object.defineProperty(this, "_serverOptions", {
|
|
260
616
|
enumerable: true,
|
|
261
617
|
configurable: true,
|
|
262
618
|
writable: true,
|
|
263
|
-
value:
|
|
619
|
+
value: void 0
|
|
264
620
|
});
|
|
265
|
-
this.
|
|
266
|
-
|
|
621
|
+
this._serverOptions = options;
|
|
622
|
+
}
|
|
623
|
+
open(options) {
|
|
624
|
+
if (this._destroyed) {
|
|
625
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_DESTROYED, 'Port is destroyed'));
|
|
626
|
+
}
|
|
627
|
+
if (this._isOpen || this._isOpening) {
|
|
628
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_ALREADY_OPEN, 'Port is already open'));
|
|
629
|
+
}
|
|
630
|
+
this._isOpening = true;
|
|
631
|
+
this._connections.clear();
|
|
632
|
+
const server = createServer(this._serverOptions, (socket) => {
|
|
633
|
+
const conn = { id: genConnectionId('tcp-server') };
|
|
634
|
+
this._connections.set(socket, conn);
|
|
635
|
+
const response = (data) => new Promise((resolve, reject) => {
|
|
636
|
+
socket.write(data, (error) => {
|
|
637
|
+
if (error) {
|
|
638
|
+
reject(error);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
resolve();
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
});
|
|
267
645
|
socket.on('data', (data) => {
|
|
268
|
-
this.emit('data', data,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
resolve();
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
}));
|
|
646
|
+
this.emit('data', data, response, conn);
|
|
647
|
+
});
|
|
648
|
+
socket.on('error', (err) => {
|
|
649
|
+
this.emit('error', err);
|
|
278
650
|
});
|
|
279
651
|
socket.once('close', () => {
|
|
652
|
+
if (this._connections.delete(socket)) {
|
|
653
|
+
this.emit('connection-close', conn);
|
|
654
|
+
}
|
|
280
655
|
socket.removeAllListeners();
|
|
281
|
-
this._sockets.delete(socket);
|
|
282
656
|
});
|
|
283
657
|
});
|
|
284
|
-
|
|
285
|
-
open(options) {
|
|
286
|
-
if (this.destroyed) {
|
|
287
|
-
return Promise.reject(new Error('Port is destroyed'));
|
|
288
|
-
}
|
|
658
|
+
this._server = server;
|
|
289
659
|
return new Promise((resolve, reject) => {
|
|
290
660
|
var _a;
|
|
291
|
-
let
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
661
|
+
let listening = false;
|
|
662
|
+
server.once('error', (err) => {
|
|
663
|
+
if (listening) {
|
|
664
|
+
this.emit('error', err);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
if (this._server === server) {
|
|
668
|
+
this._isOpening = false;
|
|
669
|
+
this._server = null;
|
|
670
|
+
}
|
|
671
|
+
server.removeAllListeners();
|
|
672
|
+
reject(err);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
server.once('close', () => {
|
|
676
|
+
if (this._server === server) {
|
|
297
677
|
this._isOpen = false;
|
|
298
|
-
this.
|
|
299
|
-
|
|
300
|
-
|
|
678
|
+
this._isOpening = false;
|
|
679
|
+
this._server = null;
|
|
680
|
+
const conns = Array.from(this._connections.values());
|
|
681
|
+
this._connections.clear();
|
|
682
|
+
for (const conn of conns) {
|
|
683
|
+
this.emit('connection-close', conn);
|
|
301
684
|
}
|
|
302
685
|
this.emit('close');
|
|
303
|
-
});
|
|
304
|
-
resolve();
|
|
305
|
-
});
|
|
306
|
-
this._server.on('error', (error) => {
|
|
307
|
-
if (called) {
|
|
308
|
-
this.emit('error', error);
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
reject(error);
|
|
312
686
|
}
|
|
313
687
|
});
|
|
688
|
+
server.listen(Object.assign(Object.assign({}, options), { port: (_a = options === null || options === void 0 ? void 0 : options.port) !== null && _a !== void 0 ? _a : 502 }), () => {
|
|
689
|
+
listening = true;
|
|
690
|
+
this._isOpening = false;
|
|
691
|
+
this._isOpen = true;
|
|
692
|
+
resolve();
|
|
693
|
+
});
|
|
314
694
|
});
|
|
315
695
|
}
|
|
316
696
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
317
697
|
write(data) {
|
|
318
698
|
return new Promise((resolve, reject) => {
|
|
319
|
-
reject(new
|
|
699
|
+
reject(new ModbusError(ModbusErrorCode.NOT_SUPPORTED, 'Not supported'));
|
|
320
700
|
});
|
|
321
701
|
}
|
|
322
702
|
close() {
|
|
703
|
+
if (!this._server) {
|
|
704
|
+
return Promise.resolve();
|
|
705
|
+
}
|
|
706
|
+
const server = this._server;
|
|
323
707
|
return new Promise((resolve) => {
|
|
324
|
-
|
|
325
|
-
this.
|
|
326
|
-
for (const socket of this._sockets) {
|
|
327
|
-
socket.removeAllListeners();
|
|
708
|
+
server.once('close', () => resolve());
|
|
709
|
+
for (const [socket] of this._connections) {
|
|
328
710
|
socket.destroy();
|
|
329
711
|
}
|
|
330
|
-
|
|
331
|
-
resolve();
|
|
332
|
-
});
|
|
712
|
+
server.close();
|
|
333
713
|
});
|
|
334
714
|
}
|
|
335
715
|
destroy() {
|
|
336
716
|
this._destroyed = true;
|
|
337
|
-
this.
|
|
338
|
-
|
|
717
|
+
return this.close().then(() => {
|
|
718
|
+
this.removeAllListeners();
|
|
719
|
+
});
|
|
339
720
|
}
|
|
340
721
|
}
|
|
341
722
|
|
|
@@ -346,14 +727,8 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
346
727
|
get destroyed() {
|
|
347
728
|
return this._destroyed;
|
|
348
729
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
* @param options
|
|
352
|
-
* @param remote If omitted, as server.
|
|
353
|
-
* Otherwise as client.
|
|
354
|
-
*/
|
|
355
|
-
constructor(options, remote) {
|
|
356
|
-
var _a, _b;
|
|
730
|
+
constructor(options) {
|
|
731
|
+
var _a, _b, _c, _d;
|
|
357
732
|
super();
|
|
358
733
|
Object.defineProperty(this, "TYPE", {
|
|
359
734
|
enumerable: true,
|
|
@@ -365,7 +740,13 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
365
740
|
enumerable: true,
|
|
366
741
|
configurable: true,
|
|
367
742
|
writable: true,
|
|
368
|
-
value:
|
|
743
|
+
value: null
|
|
744
|
+
});
|
|
745
|
+
Object.defineProperty(this, "_connections", {
|
|
746
|
+
enumerable: true,
|
|
747
|
+
configurable: true,
|
|
748
|
+
writable: true,
|
|
749
|
+
value: new Map()
|
|
369
750
|
});
|
|
370
751
|
Object.defineProperty(this, "_isOpen", {
|
|
371
752
|
enumerable: true,
|
|
@@ -373,12 +754,24 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
373
754
|
writable: true,
|
|
374
755
|
value: false
|
|
375
756
|
});
|
|
757
|
+
Object.defineProperty(this, "_isOpening", {
|
|
758
|
+
enumerable: true,
|
|
759
|
+
configurable: true,
|
|
760
|
+
writable: true,
|
|
761
|
+
value: false
|
|
762
|
+
});
|
|
376
763
|
Object.defineProperty(this, "_destroyed", {
|
|
377
764
|
enumerable: true,
|
|
378
765
|
configurable: true,
|
|
379
766
|
writable: true,
|
|
380
767
|
value: false
|
|
381
768
|
});
|
|
769
|
+
Object.defineProperty(this, "_socketOptions", {
|
|
770
|
+
enumerable: true,
|
|
771
|
+
configurable: true,
|
|
772
|
+
writable: true,
|
|
773
|
+
value: void 0
|
|
774
|
+
});
|
|
382
775
|
Object.defineProperty(this, "_port", {
|
|
383
776
|
enumerable: true,
|
|
384
777
|
configurable: true,
|
|
@@ -391,186 +784,225 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
391
784
|
writable: true,
|
|
392
785
|
value: void 0
|
|
393
786
|
});
|
|
394
|
-
Object.defineProperty(this, "
|
|
787
|
+
Object.defineProperty(this, "_idleTimeout", {
|
|
395
788
|
enumerable: true,
|
|
396
789
|
configurable: true,
|
|
397
790
|
writable: true,
|
|
398
791
|
value: void 0
|
|
399
792
|
});
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
resolve();
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
}));
|
|
793
|
+
Object.defineProperty(this, "isServer", {
|
|
794
|
+
enumerable: true,
|
|
795
|
+
configurable: true,
|
|
796
|
+
writable: true,
|
|
797
|
+
value: void 0
|
|
411
798
|
});
|
|
412
|
-
this.
|
|
413
|
-
this.
|
|
414
|
-
this.
|
|
799
|
+
this._socketOptions = options === null || options === void 0 ? void 0 : options.socket;
|
|
800
|
+
this.isServer = !(options === null || options === void 0 ? void 0 : options.remote);
|
|
801
|
+
this._port = (_b = (_a = options === null || options === void 0 ? void 0 : options.remote) === null || _a === void 0 ? void 0 : _a.port) !== null && _b !== void 0 ? _b : 502;
|
|
802
|
+
this._address = (_c = options === null || options === void 0 ? void 0 : options.remote) === null || _c === void 0 ? void 0 : _c.address;
|
|
803
|
+
this._idleTimeout = (_d = options === null || options === void 0 ? void 0 : options.idleTimeout) !== null && _d !== void 0 ? _d : 30000;
|
|
415
804
|
}
|
|
416
805
|
open(options) {
|
|
417
|
-
|
|
418
|
-
|
|
806
|
+
var _a, _b;
|
|
807
|
+
if (this._destroyed) {
|
|
808
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_DESTROYED, 'Port is destroyed'));
|
|
809
|
+
}
|
|
810
|
+
if (this._isOpen || this._isOpening) {
|
|
811
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_ALREADY_OPEN, 'Port is already open'));
|
|
419
812
|
}
|
|
813
|
+
this._isOpening = true;
|
|
814
|
+
this._connections.clear();
|
|
815
|
+
const socket = createSocket(Object.assign(Object.assign({}, this._socketOptions), { type: (_b = (_a = this._socketOptions) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : 'udp4' }), (msg, rinfo) => this._handleMessage(socket, msg, rinfo));
|
|
816
|
+
this._socket = socket;
|
|
420
817
|
return new Promise((resolve, reject) => {
|
|
421
818
|
var _a;
|
|
819
|
+
let started = false;
|
|
820
|
+
socket.on('error', (err) => {
|
|
821
|
+
if (started) {
|
|
822
|
+
this.emit('error', err);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
if (this._socket === socket) {
|
|
826
|
+
this._isOpening = false;
|
|
827
|
+
this._socket = null;
|
|
828
|
+
}
|
|
829
|
+
socket.removeAllListeners();
|
|
830
|
+
reject(err);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
socket.once('close', () => {
|
|
834
|
+
if (this._socket !== socket) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
this._isOpen = false;
|
|
838
|
+
this._isOpening = false;
|
|
839
|
+
this._socket = null;
|
|
840
|
+
socket.removeAllListeners();
|
|
841
|
+
const entries = Array.from(this._connections.values());
|
|
842
|
+
this._connections.clear();
|
|
843
|
+
for (const entry of entries) {
|
|
844
|
+
if (entry.idleTimer) {
|
|
845
|
+
clearTimeout(entry.idleTimer);
|
|
846
|
+
}
|
|
847
|
+
this.emit('connection-close', entry.conn);
|
|
848
|
+
}
|
|
849
|
+
this.emit('close');
|
|
850
|
+
});
|
|
422
851
|
if (this.isServer) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
852
|
+
socket.bind(Object.assign(Object.assign({}, options), { port: (_a = options === null || options === void 0 ? void 0 : options.port) !== null && _a !== void 0 ? _a : 502 }), () => {
|
|
853
|
+
started = true;
|
|
854
|
+
this._isOpening = false;
|
|
426
855
|
this._isOpen = true;
|
|
427
|
-
this._socket.on('close', () => {
|
|
428
|
-
this._isOpen = false;
|
|
429
|
-
this._socket.removeAllListeners();
|
|
430
|
-
this.emit('close');
|
|
431
|
-
});
|
|
432
856
|
resolve();
|
|
433
857
|
});
|
|
434
|
-
this._socket.on('error', (error) => {
|
|
435
|
-
if (called) {
|
|
436
|
-
this.emit('error', error);
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
reject(error);
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
858
|
}
|
|
443
859
|
else {
|
|
860
|
+
started = true;
|
|
861
|
+
this._isOpening = false;
|
|
444
862
|
this._isOpen = true;
|
|
445
863
|
resolve();
|
|
446
864
|
}
|
|
447
865
|
});
|
|
448
866
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
867
|
+
_handleMessage(socket, msg, rinfo) {
|
|
868
|
+
if (this._socket !== socket) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (!this.isServer) {
|
|
872
|
+
if (rinfo.port !== this._port) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (this._address !== undefined && rinfo.address !== this._address) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const key = `${rinfo.address}:${rinfo.port}`;
|
|
880
|
+
let entry = this._connections.get(key);
|
|
881
|
+
if (!entry) {
|
|
882
|
+
entry = { conn: { id: genConnectionId('udp') } };
|
|
883
|
+
this._connections.set(key, entry);
|
|
884
|
+
}
|
|
885
|
+
if (this.isServer && this._idleTimeout > 0) {
|
|
886
|
+
if (entry.idleTimer) {
|
|
887
|
+
clearTimeout(entry.idleTimer);
|
|
888
|
+
}
|
|
889
|
+
const conn = entry.conn;
|
|
890
|
+
entry.idleTimer = setTimeout(() => {
|
|
891
|
+
const e = this._connections.get(key);
|
|
892
|
+
if (!e || e.conn !== conn) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
this._connections.delete(key);
|
|
896
|
+
this.emit('connection-close', conn);
|
|
897
|
+
}, this._idleTimeout);
|
|
898
|
+
}
|
|
899
|
+
const conn = entry.conn;
|
|
900
|
+
const response = (data) => new Promise((resolve, reject) => {
|
|
901
|
+
socket.send(data, rinfo.port, rinfo.address, (error) => {
|
|
902
|
+
if (error) {
|
|
903
|
+
reject(error);
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
resolve();
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
this.emit('data', msg, response, conn);
|
|
911
|
+
}
|
|
912
|
+
write(data) {
|
|
913
|
+
return new Promise((resolve, reject) => {
|
|
914
|
+
if (this._isOpen && this._socket) {
|
|
915
|
+
this._socket.send(data, this._port, this._address, (error) => {
|
|
916
|
+
if (error) {
|
|
917
|
+
reject(error);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
this.emit('write', data);
|
|
921
|
+
resolve();
|
|
922
|
+
}
|
|
460
923
|
});
|
|
461
924
|
}
|
|
462
925
|
else {
|
|
463
|
-
reject(new
|
|
926
|
+
reject(new ModbusError(ModbusErrorCode.PORT_NOT_OPEN, 'Port is not open'));
|
|
464
927
|
}
|
|
465
928
|
});
|
|
466
929
|
}
|
|
467
930
|
close() {
|
|
931
|
+
if (!this._socket) {
|
|
932
|
+
return Promise.resolve();
|
|
933
|
+
}
|
|
934
|
+
const socket = this._socket;
|
|
468
935
|
return new Promise((resolve) => {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
936
|
+
socket.once('close', () => resolve());
|
|
937
|
+
try {
|
|
938
|
+
socket.close();
|
|
939
|
+
}
|
|
940
|
+
catch (_a) {
|
|
941
|
+
// Socket may already be closed; the registered 'close' listener still fires asynchronously.
|
|
942
|
+
}
|
|
474
943
|
});
|
|
475
944
|
}
|
|
476
945
|
destroy() {
|
|
477
946
|
this._destroyed = true;
|
|
478
|
-
this.
|
|
479
|
-
|
|
947
|
+
return this.close().then(() => {
|
|
948
|
+
this.removeAllListeners();
|
|
949
|
+
});
|
|
480
950
|
}
|
|
481
951
|
}
|
|
482
952
|
|
|
483
953
|
class AbstractApplicationLayer extends EventEmitter {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
954
|
+
constructor() {
|
|
955
|
+
super(...arguments);
|
|
956
|
+
Object.defineProperty(this, "_role", {
|
|
957
|
+
enumerable: true,
|
|
958
|
+
configurable: true,
|
|
959
|
+
writable: true,
|
|
960
|
+
value: void 0
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
get role() {
|
|
964
|
+
if (!this._role) {
|
|
965
|
+
throw new ModbusError(ModbusErrorCode.INVALID_ROLE, 'Application layer role not set');
|
|
492
966
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return false;
|
|
967
|
+
return this._role;
|
|
968
|
+
}
|
|
969
|
+
set role(value) {
|
|
970
|
+
if (this._role && this._role !== value) {
|
|
971
|
+
throw new ModbusError(ModbusErrorCode.INVALID_ROLE, `Application layer role already set to ${this._role}`);
|
|
502
972
|
}
|
|
973
|
+
this._role = value;
|
|
503
974
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581,
|
|
511
|
-
0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141,
|
|
512
|
-
0x3300, 0xf3c1, 0xf281, 0x3240, 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01,
|
|
513
|
-
0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0,
|
|
514
|
-
0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681,
|
|
515
|
-
0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
|
|
516
|
-
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
|
|
517
|
-
0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0,
|
|
518
|
-
0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381,
|
|
519
|
-
0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741,
|
|
520
|
-
0x5500, 0x95c1, 0x9481, 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901,
|
|
521
|
-
0x59c0, 0x5880, 0x9841, 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0,
|
|
522
|
-
0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081,
|
|
523
|
-
0x4040,
|
|
524
|
-
];
|
|
525
|
-
function crc(data) {
|
|
526
|
-
let crc = 0xffff;
|
|
527
|
-
for (let index = 0; index < data.length; index++) {
|
|
528
|
-
crc = (TABLE[(crc ^ data[index]) & 0xff] ^ (crc >> 8)) & 0xffff;
|
|
975
|
+
flush() {
|
|
976
|
+
// no-op — override in subclasses
|
|
977
|
+
}
|
|
978
|
+
addCustomFunctionCode(cfc) {
|
|
979
|
+
}
|
|
980
|
+
removeCustomFunctionCode(fc) {
|
|
529
981
|
}
|
|
530
|
-
return crc;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Get time interval between message frames witch well-known as 3.5T.
|
|
535
|
-
* @param baudRate Serial port baud rate.
|
|
536
|
-
* @param {number} [approximation=48] Approximate number of bits corresponding to 3.5T.
|
|
537
|
-
* @returns `ms`.
|
|
538
|
-
*/
|
|
539
|
-
function getThreePointFiveT(baudRate, approximation = 48) {
|
|
540
|
-
return (approximation * 1000) / baudRate;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function lrc(data) {
|
|
544
|
-
return (~data.reduce((sum, n) => sum + n, 0) + 1) & 0xff;
|
|
545
982
|
}
|
|
546
983
|
|
|
547
984
|
const MAX_FRAME_LENGTH = 256;
|
|
985
|
+
const MIN_FRAME_LENGTH = 4;
|
|
548
986
|
class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
549
|
-
constructor(physicalLayer,
|
|
550
|
-
/**
|
|
551
|
-
* The time interval between two frames, support two formats:
|
|
552
|
-
* - bit: `48bit` as default
|
|
553
|
-
* - millisecond: `20ms`
|
|
554
|
-
*/
|
|
555
|
-
intervalBetweenFrames) {
|
|
987
|
+
constructor(physicalLayer, options = {}) {
|
|
556
988
|
super();
|
|
557
|
-
Object.defineProperty(this, "
|
|
989
|
+
Object.defineProperty(this, "PROTOCOL", {
|
|
558
990
|
enumerable: true,
|
|
559
991
|
configurable: true,
|
|
560
992
|
writable: true,
|
|
561
|
-
value:
|
|
993
|
+
value: 'RTU'
|
|
562
994
|
});
|
|
563
|
-
Object.defineProperty(this, "
|
|
995
|
+
Object.defineProperty(this, "_states", {
|
|
564
996
|
enumerable: true,
|
|
565
997
|
configurable: true,
|
|
566
998
|
writable: true,
|
|
567
|
-
value:
|
|
999
|
+
value: new Map()
|
|
568
1000
|
});
|
|
569
|
-
Object.defineProperty(this, "
|
|
1001
|
+
Object.defineProperty(this, "_customFunctionCodes", {
|
|
570
1002
|
enumerable: true,
|
|
571
1003
|
configurable: true,
|
|
572
1004
|
writable: true,
|
|
573
|
-
value:
|
|
1005
|
+
value: new Map()
|
|
574
1006
|
});
|
|
575
1007
|
Object.defineProperty(this, "_removeAllListeners", {
|
|
576
1008
|
enumerable: true,
|
|
@@ -578,129 +1010,276 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
578
1010
|
writable: true,
|
|
579
1011
|
value: []
|
|
580
1012
|
});
|
|
1013
|
+
Object.defineProperty(this, "_threePointFiveT", {
|
|
1014
|
+
enumerable: true,
|
|
1015
|
+
configurable: true,
|
|
1016
|
+
writable: true,
|
|
1017
|
+
value: void 0
|
|
1018
|
+
});
|
|
1019
|
+
Object.defineProperty(this, "_onePointFiveT", {
|
|
1020
|
+
enumerable: true,
|
|
1021
|
+
configurable: true,
|
|
1022
|
+
writable: true,
|
|
1023
|
+
value: void 0
|
|
1024
|
+
});
|
|
1025
|
+
const { intervalBetweenFrames, interCharTimeout } = options;
|
|
581
1026
|
let threePointFiveT = 0;
|
|
1027
|
+
let onePointFiveT = 0;
|
|
582
1028
|
if (physicalLayer.TYPE === 'SERIAL') {
|
|
583
|
-
|
|
584
|
-
|
|
1029
|
+
const baudRate = physicalLayer.baudRate;
|
|
1030
|
+
if (intervalBetweenFrames && intervalBetweenFrames.unit === 'ms') {
|
|
1031
|
+
threePointFiveT = intervalBetweenFrames.value;
|
|
585
1032
|
}
|
|
586
1033
|
else {
|
|
587
|
-
threePointFiveT =
|
|
588
|
-
? 1.
|
|
589
|
-
: getThreePointFiveT(physicalLayer.baudRate, intervalBetweenFrames ? Number(intervalBetweenFrames.slice(0, -3)) : 48));
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
const handleData = (data, response) => {
|
|
593
|
-
this._bufferRx = Buffer.concat([this._bufferRx, data]);
|
|
594
|
-
clearTimeout(this._timerThreePointFive);
|
|
595
|
-
const handleData = () => {
|
|
596
|
-
this.framing(this._bufferRx, (error, frame) => {
|
|
597
|
-
if (this._waitingResponse) {
|
|
598
|
-
if (error && error.message === 'Insufficient data length') {
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
this._waitingResponse.callback(error, frame);
|
|
602
|
-
this._bufferRx = Buffer.alloc(0);
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
if (!error) {
|
|
606
|
-
this.emit('framing', frame, response);
|
|
607
|
-
}
|
|
608
|
-
this._bufferRx = Buffer.alloc(0);
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
};
|
|
612
|
-
if (this._bufferRx.length >= MAX_FRAME_LENGTH) {
|
|
613
|
-
handleData();
|
|
1034
|
+
threePointFiveT =
|
|
1035
|
+
baudRate > 19200 ? 1.75 : Math.ceil(bitsToMs(baudRate, intervalBetweenFrames ? intervalBetweenFrames.value : 38.5));
|
|
614
1036
|
}
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
|
|
1037
|
+
if (interCharTimeout) {
|
|
1038
|
+
if (interCharTimeout.unit === 'ms') {
|
|
1039
|
+
onePointFiveT = interCharTimeout.value;
|
|
618
1040
|
}
|
|
619
1041
|
else {
|
|
620
|
-
|
|
1042
|
+
onePointFiveT = baudRate > 19200 ? 0.75 : Math.ceil(bitsToMs(baudRate, interCharTimeout.value));
|
|
621
1043
|
}
|
|
622
1044
|
}
|
|
1045
|
+
}
|
|
1046
|
+
this._threePointFiveT = threePointFiveT;
|
|
1047
|
+
this._onePointFiveT = onePointFiveT;
|
|
1048
|
+
const handleData = (data, response, connection) => {
|
|
1049
|
+
let state = this.getState(connection.id);
|
|
1050
|
+
if (state.t15Expired && state.end > state.start) {
|
|
1051
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.T1_5_EXCEEDED, 'Inter-character timeout (t1.5) exceeded'));
|
|
1052
|
+
state.start = 0;
|
|
1053
|
+
state.end = 0;
|
|
1054
|
+
}
|
|
1055
|
+
state.t15Expired = false;
|
|
1056
|
+
let dataOffset = 0;
|
|
1057
|
+
while (dataOffset < data.length) {
|
|
1058
|
+
const space = state.pool.length - state.end;
|
|
1059
|
+
if (space === 0) {
|
|
1060
|
+
this.clearStateTimers(state);
|
|
1061
|
+
this.flushBuffer(connection.id, response, connection, this._threePointFiveT > 0);
|
|
1062
|
+
state = this.getState(connection.id);
|
|
1063
|
+
if (state.pool.length - state.end === 0) {
|
|
1064
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_DATA, 'Frame buffer exhausted before complete frame received'));
|
|
1065
|
+
state.start = 0;
|
|
1066
|
+
state.end = 0;
|
|
1067
|
+
state.t15Expired = false;
|
|
1068
|
+
}
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
const toCopy = Math.min(space, data.length - dataOffset);
|
|
1072
|
+
data.copy(state.pool, state.end, dataOffset, dataOffset + toCopy);
|
|
1073
|
+
state.end += toCopy;
|
|
1074
|
+
dataOffset += toCopy;
|
|
1075
|
+
}
|
|
1076
|
+
this.clearStateTimers(state);
|
|
1077
|
+
const commitFrame = () => {
|
|
1078
|
+
this.clearStateTimers(state);
|
|
1079
|
+
this.flushBuffer(connection.id, response, connection, this._threePointFiveT > 0);
|
|
1080
|
+
};
|
|
1081
|
+
if (state.end - state.start >= MAX_FRAME_LENGTH) {
|
|
1082
|
+
commitFrame();
|
|
1083
|
+
}
|
|
1084
|
+
else if (this._threePointFiveT) {
|
|
1085
|
+
if (this._onePointFiveT > 0) {
|
|
1086
|
+
state.interCharTimer = setTimeout(() => {
|
|
1087
|
+
state.interCharTimer = undefined;
|
|
1088
|
+
state.t15Expired = true;
|
|
1089
|
+
}, this._onePointFiveT);
|
|
1090
|
+
}
|
|
1091
|
+
state.timer = setTimeout(commitFrame, this._threePointFiveT);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
commitFrame();
|
|
1095
|
+
}
|
|
623
1096
|
};
|
|
624
1097
|
physicalLayer.on('data', handleData);
|
|
625
|
-
this._removeAllListeners.push(() =>
|
|
626
|
-
|
|
627
|
-
|
|
1098
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('data', handleData));
|
|
1099
|
+
const handleConnectionClose = (connection) => {
|
|
1100
|
+
this.clearState(connection.id);
|
|
1101
|
+
};
|
|
1102
|
+
physicalLayer.on('connection-close', handleConnectionClose);
|
|
1103
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('connection-close', handleConnectionClose));
|
|
628
1104
|
const handleClose = () => {
|
|
629
|
-
|
|
630
|
-
|
|
1105
|
+
for (const key of [...this._states.keys()]) {
|
|
1106
|
+
this.clearState(key);
|
|
1107
|
+
}
|
|
631
1108
|
};
|
|
632
1109
|
physicalLayer.on('close', handleClose);
|
|
633
|
-
this._removeAllListeners.push(() =>
|
|
634
|
-
physicalLayer.removeListener('close', handleClose);
|
|
635
|
-
});
|
|
1110
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('close', handleClose));
|
|
636
1111
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1112
|
+
getState(key) {
|
|
1113
|
+
let state = this._states.get(key);
|
|
1114
|
+
if (!state) {
|
|
1115
|
+
state = { pool: Buffer.alloc(MAX_FRAME_LENGTH * 2), start: 0, end: 0 };
|
|
1116
|
+
this._states.set(key, state);
|
|
1117
|
+
}
|
|
1118
|
+
return state;
|
|
1119
|
+
}
|
|
1120
|
+
clearStateTimers(state) {
|
|
1121
|
+
if (state.timer) {
|
|
1122
|
+
clearTimeout(state.timer);
|
|
1123
|
+
state.timer = undefined;
|
|
1124
|
+
}
|
|
1125
|
+
if (state.interCharTimer) {
|
|
1126
|
+
clearTimeout(state.interCharTimer);
|
|
1127
|
+
state.interCharTimer = undefined;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
clearState(key) {
|
|
1131
|
+
const state = this._states.get(key);
|
|
1132
|
+
if (state) {
|
|
1133
|
+
this.clearStateTimers(state);
|
|
1134
|
+
}
|
|
1135
|
+
this._states.delete(key);
|
|
1136
|
+
}
|
|
1137
|
+
flush() {
|
|
1138
|
+
for (const state of this._states.values()) {
|
|
1139
|
+
this.clearStateTimers(state);
|
|
1140
|
+
state.start = 0;
|
|
1141
|
+
state.end = 0;
|
|
1142
|
+
}
|
|
1143
|
+
this._states.clear();
|
|
1144
|
+
}
|
|
1145
|
+
addCustomFunctionCode(cfc) {
|
|
1146
|
+
if (!isUint8(cfc.fc)) {
|
|
1147
|
+
throw new ModbusError(ModbusErrorCode.RANGE, `fc must be an integer in 0..255, got ${cfc.fc}`);
|
|
1148
|
+
}
|
|
1149
|
+
this._customFunctionCodes.set(cfc.fc, cfc);
|
|
1150
|
+
}
|
|
1151
|
+
removeCustomFunctionCode(fc) {
|
|
1152
|
+
this._customFunctionCodes.delete(fc);
|
|
1153
|
+
}
|
|
1154
|
+
flushBuffer(key, response, connection, strict) {
|
|
1155
|
+
const state = this._states.get(key);
|
|
1156
|
+
if (!state) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
while (state.end - state.start > 0) {
|
|
1160
|
+
const buffer = state.pool.subarray(state.start, state.end);
|
|
1161
|
+
const result = this.tryExtract(buffer);
|
|
1162
|
+
if (result.kind === 'frame') {
|
|
1163
|
+
state.start += result.frame.length;
|
|
1164
|
+
this.deliverFrame(result.frame, response, connection);
|
|
1165
|
+
}
|
|
1166
|
+
else if (result.kind === 'skip') {
|
|
1167
|
+
if (strict) {
|
|
1168
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.CRC_MISMATCH, 'CRC mismatch'));
|
|
1169
|
+
state.start = 0;
|
|
1170
|
+
state.end = 0;
|
|
1171
|
+
state.t15Expired = false;
|
|
1172
|
+
return;
|
|
666
1173
|
}
|
|
1174
|
+
state.start += 1;
|
|
667
1175
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1176
|
+
else if (result.kind === 'insufficient') {
|
|
1177
|
+
if (state.end - state.start >= MAX_FRAME_LENGTH) {
|
|
1178
|
+
state.start += 1;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
if (strict) {
|
|
1182
|
+
this.emit('framing-error', new ModbusError(state.t15Expired ? ModbusErrorCode.T1_5_EXCEEDED : ModbusErrorCode.INCOMPLETE_FRAME, state.t15Expired ? 'Inter-character timeout (t1.5) exceeded' : 'Incomplete frame at t3.5'));
|
|
1183
|
+
state.start = 0;
|
|
1184
|
+
state.end = 0;
|
|
1185
|
+
state.t15Expired = false;
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (state.t15Expired) {
|
|
1189
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.T1_5_EXCEEDED, 'Inter-character timeout (t1.5) exceeded'));
|
|
1190
|
+
state.start = 0;
|
|
1191
|
+
state.end = 0;
|
|
1192
|
+
state.t15Expired = false;
|
|
1193
|
+
}
|
|
1194
|
+
break;
|
|
671
1195
|
}
|
|
672
1196
|
else {
|
|
673
|
-
|
|
1197
|
+
this.emit('framing-error', result.error);
|
|
1198
|
+
state.start = 0;
|
|
1199
|
+
state.end = 0;
|
|
1200
|
+
return;
|
|
674
1201
|
}
|
|
675
1202
|
}
|
|
676
|
-
|
|
677
|
-
|
|
1203
|
+
if (state.start > 0) {
|
|
1204
|
+
if (state.start < state.end) {
|
|
1205
|
+
state.pool.copy(state.pool, 0, state.start, state.end);
|
|
1206
|
+
}
|
|
1207
|
+
state.end -= state.start;
|
|
1208
|
+
state.start = 0;
|
|
1209
|
+
}
|
|
1210
|
+
if (state.end === 0 && !state.timer) {
|
|
1211
|
+
this._states.delete(key);
|
|
678
1212
|
}
|
|
679
1213
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1214
|
+
tryExtract(buffer) {
|
|
1215
|
+
if (buffer.length < MIN_FRAME_LENGTH) {
|
|
1216
|
+
return { kind: 'insufficient' };
|
|
1217
|
+
}
|
|
1218
|
+
const isResponse = this.role === 'MASTER';
|
|
1219
|
+
const fc = buffer[1];
|
|
1220
|
+
const cfc = this._customFunctionCodes.get(fc);
|
|
1221
|
+
if (cfc) {
|
|
1222
|
+
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1223
|
+
const expected = predictor(buffer);
|
|
1224
|
+
if (expected === null) {
|
|
1225
|
+
return { kind: 'insufficient' };
|
|
1226
|
+
}
|
|
1227
|
+
return this.checkExpected(buffer, expected);
|
|
1228
|
+
}
|
|
1229
|
+
const result = predictRtuFrameLength(buffer, isResponse);
|
|
1230
|
+
if (result.kind === 'length') {
|
|
1231
|
+
return this.checkExpected(buffer, result.length);
|
|
1232
|
+
}
|
|
1233
|
+
if (result.kind === 'need-more') {
|
|
1234
|
+
return { kind: 'insufficient' };
|
|
1235
|
+
}
|
|
1236
|
+
return {
|
|
1237
|
+
kind: 'error',
|
|
1238
|
+
error: new ModbusError(ModbusErrorCode.UNKNOWN_FC, `Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`),
|
|
1239
|
+
};
|
|
684
1240
|
}
|
|
685
|
-
|
|
686
|
-
|
|
1241
|
+
checkExpected(buffer, expected) {
|
|
1242
|
+
if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
|
|
1243
|
+
return { kind: 'error', error: new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data') };
|
|
1244
|
+
}
|
|
1245
|
+
if (buffer.length < expected) {
|
|
1246
|
+
return { kind: 'insufficient' };
|
|
1247
|
+
}
|
|
1248
|
+
if (this.crcMatches(buffer, expected)) {
|
|
1249
|
+
return { kind: 'frame', frame: buffer.subarray(0, expected), rest: buffer.subarray(expected) };
|
|
1250
|
+
}
|
|
1251
|
+
return { kind: 'skip', rest: buffer.subarray(1) };
|
|
1252
|
+
}
|
|
1253
|
+
crcMatches(buffer, length) {
|
|
1254
|
+
return buffer.readUInt16LE(length - 2) === crc(buffer.subarray(0, length - 2));
|
|
1255
|
+
}
|
|
1256
|
+
deliverFrame(buffer, response, connection) {
|
|
1257
|
+
const frameBuf = Buffer.from(buffer);
|
|
1258
|
+
const frame = {
|
|
1259
|
+
unit: frameBuf[0],
|
|
1260
|
+
fc: frameBuf[1],
|
|
1261
|
+
data: frameBuf.subarray(2, frameBuf.length - 2),
|
|
1262
|
+
buffer: frameBuf,
|
|
1263
|
+
};
|
|
1264
|
+
this.emit('framing', frame, response, connection);
|
|
687
1265
|
}
|
|
688
1266
|
encode(data) {
|
|
689
1267
|
const buffer = Buffer.alloc(data.data.length + 4);
|
|
690
1268
|
buffer.writeUInt8(data.unit, 0);
|
|
691
1269
|
buffer.writeUInt8(data.fc, 1);
|
|
692
|
-
data.data
|
|
693
|
-
buffer.writeUInt8(num, 2 + index);
|
|
694
|
-
});
|
|
1270
|
+
buffer.set(data.data, 2);
|
|
695
1271
|
buffer.writeUInt16LE(crc(buffer.subarray(0, -2)), buffer.length - 2);
|
|
696
1272
|
return buffer;
|
|
697
1273
|
}
|
|
698
1274
|
destroy() {
|
|
699
1275
|
this.removeAllListeners();
|
|
700
|
-
for (const
|
|
701
|
-
|
|
1276
|
+
for (const removeListener of this._removeAllListeners) {
|
|
1277
|
+
removeListener();
|
|
702
1278
|
}
|
|
703
|
-
|
|
1279
|
+
for (const state of this._states.values()) {
|
|
1280
|
+
this.clearStateTimers(state);
|
|
1281
|
+
}
|
|
1282
|
+
this._states.clear();
|
|
704
1283
|
}
|
|
705
1284
|
}
|
|
706
1285
|
|
|
@@ -709,26 +1288,43 @@ const CHAR_CODE = {
|
|
|
709
1288
|
CR: '\r'.charCodeAt(0),
|
|
710
1289
|
LF: '\n'.charCodeAt(0),
|
|
711
1290
|
};
|
|
1291
|
+
// Modbus ASCII frame encodes at most 256 PDU bytes as 512 hex chars between
|
|
1292
|
+
// `:` and `\r`. Cap per-connection buffering so a peer that never sends `\r`
|
|
1293
|
+
// cannot grow `state.frame` without bound.
|
|
1294
|
+
const MAX_ASCII_PAYLOAD = 512;
|
|
1295
|
+
const HEX_DECODE = new Uint8Array(256);
|
|
1296
|
+
HEX_DECODE.fill(0xff);
|
|
1297
|
+
for (let i = 0x30; i <= 0x39; i++) {
|
|
1298
|
+
HEX_DECODE[i] = i - 0x30;
|
|
1299
|
+
}
|
|
1300
|
+
for (let i = 0x41; i <= 0x46; i++) {
|
|
1301
|
+
HEX_DECODE[i] = i - 0x41 + 10;
|
|
1302
|
+
}
|
|
1303
|
+
for (let i = 0x61; i <= 0x66; i++) {
|
|
1304
|
+
HEX_DECODE[i] = i - 0x61 + 10;
|
|
1305
|
+
}
|
|
1306
|
+
const HEX_ENCODE = new Uint8Array('0123456789ABCDEF'.split('').map((c) => c.charCodeAt(0)));
|
|
712
1307
|
class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
713
|
-
constructor(physicalLayer) {
|
|
1308
|
+
constructor(physicalLayer, options = {}) {
|
|
1309
|
+
var _a;
|
|
714
1310
|
super();
|
|
715
|
-
Object.defineProperty(this, "
|
|
1311
|
+
Object.defineProperty(this, "PROTOCOL", {
|
|
716
1312
|
enumerable: true,
|
|
717
1313
|
configurable: true,
|
|
718
1314
|
writable: true,
|
|
719
|
-
value:
|
|
1315
|
+
value: 'ASCII'
|
|
720
1316
|
});
|
|
721
|
-
Object.defineProperty(this, "
|
|
1317
|
+
Object.defineProperty(this, "lenientHex", {
|
|
722
1318
|
enumerable: true,
|
|
723
1319
|
configurable: true,
|
|
724
1320
|
writable: true,
|
|
725
|
-
value:
|
|
1321
|
+
value: void 0
|
|
726
1322
|
});
|
|
727
|
-
Object.defineProperty(this, "
|
|
1323
|
+
Object.defineProperty(this, "_states", {
|
|
728
1324
|
enumerable: true,
|
|
729
1325
|
configurable: true,
|
|
730
1326
|
writable: true,
|
|
731
|
-
value:
|
|
1327
|
+
value: new Map()
|
|
732
1328
|
});
|
|
733
1329
|
Object.defineProperty(this, "_removeAllListeners", {
|
|
734
1330
|
enumerable: true,
|
|
@@ -736,44 +1332,62 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
736
1332
|
writable: true,
|
|
737
1333
|
value: []
|
|
738
1334
|
});
|
|
739
|
-
|
|
1335
|
+
this.lenientHex = (_a = options.lenientHex) !== null && _a !== void 0 ? _a : false;
|
|
1336
|
+
const lenientHex = this.lenientHex;
|
|
1337
|
+
const isHexChar = (value) => {
|
|
1338
|
+
if (value >= 0x30 && value <= 0x39) {
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
if (value >= 0x41 && value <= 0x46) {
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
if (lenientHex && value >= 0x61 && value <= 0x66) {
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
return false;
|
|
1348
|
+
};
|
|
1349
|
+
const handleData = (data, response, connection) => {
|
|
1350
|
+
const state = this.getState(connection.id);
|
|
740
1351
|
data.forEach((value) => {
|
|
741
|
-
switch (
|
|
1352
|
+
switch (state.status) {
|
|
742
1353
|
case 'idle': {
|
|
743
1354
|
if (value === CHAR_CODE.COLON) {
|
|
744
|
-
|
|
745
|
-
|
|
1355
|
+
state.status = 'reception';
|
|
1356
|
+
state.frame = [];
|
|
746
1357
|
}
|
|
747
1358
|
break;
|
|
748
1359
|
}
|
|
749
1360
|
case 'reception': {
|
|
750
1361
|
if (value === CHAR_CODE.COLON) {
|
|
751
|
-
|
|
1362
|
+
state.frame = [];
|
|
752
1363
|
}
|
|
753
1364
|
else if (value === CHAR_CODE.CR) {
|
|
754
|
-
|
|
1365
|
+
state.status = 'waiting end';
|
|
1366
|
+
}
|
|
1367
|
+
else if (state.frame.length >= MAX_ASCII_PAYLOAD) {
|
|
1368
|
+
state.status = 'idle';
|
|
1369
|
+
state.frame = [];
|
|
1370
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data'));
|
|
1371
|
+
}
|
|
1372
|
+
else if (!isHexChar(value)) {
|
|
1373
|
+
state.status = 'idle';
|
|
1374
|
+
state.frame = [];
|
|
1375
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_HEX, 'Invalid hex character'));
|
|
755
1376
|
}
|
|
756
1377
|
else {
|
|
757
|
-
|
|
1378
|
+
state.frame.push(value);
|
|
758
1379
|
}
|
|
759
1380
|
break;
|
|
760
1381
|
}
|
|
761
1382
|
case 'waiting end': {
|
|
762
1383
|
if (value === CHAR_CODE.COLON) {
|
|
763
|
-
|
|
764
|
-
|
|
1384
|
+
state.status = 'reception';
|
|
1385
|
+
state.frame = [];
|
|
765
1386
|
}
|
|
766
1387
|
else {
|
|
767
|
-
|
|
1388
|
+
state.status = 'idle';
|
|
768
1389
|
if (value === CHAR_CODE.LF) {
|
|
769
|
-
this.framing(Buffer.from(
|
|
770
|
-
if (this._waitingResponse) {
|
|
771
|
-
this._waitingResponse.callback(error, frame);
|
|
772
|
-
}
|
|
773
|
-
else if (!error) {
|
|
774
|
-
this.emit('framing', frame, response);
|
|
775
|
-
}
|
|
776
|
-
});
|
|
1390
|
+
this.framing(Buffer.from(state.frame), response, connection);
|
|
777
1391
|
}
|
|
778
1392
|
}
|
|
779
1393
|
break;
|
|
@@ -782,115 +1396,97 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
782
1396
|
});
|
|
783
1397
|
};
|
|
784
1398
|
physicalLayer.on('data', handleData);
|
|
785
|
-
this._removeAllListeners.push(() =>
|
|
786
|
-
|
|
787
|
-
|
|
1399
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('data', handleData));
|
|
1400
|
+
const handleConnectionClose = (connection) => {
|
|
1401
|
+
this._states.delete(connection.id);
|
|
1402
|
+
};
|
|
1403
|
+
physicalLayer.on('connection-close', handleConnectionClose);
|
|
1404
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('connection-close', handleConnectionClose));
|
|
788
1405
|
const handleClose = () => {
|
|
789
|
-
this.
|
|
790
|
-
this._frame = [];
|
|
1406
|
+
this._states.clear();
|
|
791
1407
|
};
|
|
792
1408
|
physicalLayer.on('close', handleClose);
|
|
793
|
-
this._removeAllListeners.push(() =>
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const ascii = [];
|
|
801
|
-
let num = '';
|
|
802
|
-
for (const value of _buffer) {
|
|
803
|
-
num += String.fromCharCode(value);
|
|
804
|
-
if (num.length === 2) {
|
|
805
|
-
ascii.push(Number('0x' + num));
|
|
806
|
-
num = '';
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
const buffer = Buffer.from(ascii);
|
|
810
|
-
const frame = {
|
|
811
|
-
unit: buffer[0],
|
|
812
|
-
fc: buffer[1],
|
|
813
|
-
data: Array.from(buffer.subarray(2, buffer.length - 1)),
|
|
814
|
-
buffer: _buffer,
|
|
815
|
-
};
|
|
816
|
-
if (this._waitingResponse) {
|
|
817
|
-
for (const check of this._waitingResponse.preCheck) {
|
|
818
|
-
const res = check(frame);
|
|
819
|
-
if (typeof res === 'undefined') {
|
|
820
|
-
callback(new Error('Insufficient data length'));
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
if (typeof res === 'number') {
|
|
824
|
-
if (frame.data.length < res) {
|
|
825
|
-
callback(new Error('Insufficient data length'));
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
if (frame.data.length !== res) {
|
|
829
|
-
callback(new Error('Invalid response'));
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
if (!res) {
|
|
834
|
-
callback(new Error('Invalid response'));
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
const lrcPassed = buffer[buffer.length - 1] === lrc(buffer.subarray(0, buffer.length - 1));
|
|
840
|
-
if (lrcPassed) {
|
|
841
|
-
callback(null, frame);
|
|
842
|
-
}
|
|
843
|
-
else {
|
|
844
|
-
callback(new Error('LRC check failed'));
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
else {
|
|
848
|
-
callback(new Error('Invalid data'));
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
else {
|
|
852
|
-
callback(new Error('Insufficient data length'));
|
|
1409
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('close', handleClose));
|
|
1410
|
+
}
|
|
1411
|
+
getState(key) {
|
|
1412
|
+
let state = this._states.get(key);
|
|
1413
|
+
if (!state) {
|
|
1414
|
+
state = { status: 'idle', frame: [] };
|
|
1415
|
+
this._states.set(key, state);
|
|
853
1416
|
}
|
|
1417
|
+
return state;
|
|
854
1418
|
}
|
|
855
|
-
|
|
856
|
-
this.
|
|
857
|
-
this._status = 'idle';
|
|
858
|
-
this._frame = [];
|
|
1419
|
+
flush() {
|
|
1420
|
+
this._states.clear();
|
|
859
1421
|
}
|
|
860
|
-
|
|
861
|
-
|
|
1422
|
+
framing(_buffer, response, connection) {
|
|
1423
|
+
if (_buffer.length < 6) {
|
|
1424
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INSUFFICIENT_DATA, 'Insufficient data length'));
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (_buffer.length % 2 !== 0) {
|
|
1428
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data'));
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const buffer = Buffer.allocUnsafe(_buffer.length / 2);
|
|
1432
|
+
for (let i = 0; i < _buffer.length; i += 2) {
|
|
1433
|
+
const hi = HEX_DECODE[_buffer[i]];
|
|
1434
|
+
const lo = HEX_DECODE[_buffer[i + 1]];
|
|
1435
|
+
if (hi === 0xff || lo === 0xff) {
|
|
1436
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_HEX, 'Invalid hex character'));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
buffer[i / 2] = (hi << 4) | lo;
|
|
1440
|
+
}
|
|
1441
|
+
const frame = {
|
|
1442
|
+
unit: buffer[0],
|
|
1443
|
+
fc: buffer[1],
|
|
1444
|
+
data: buffer.subarray(2, buffer.length - 1),
|
|
1445
|
+
buffer: _buffer,
|
|
1446
|
+
};
|
|
1447
|
+
const lrcPassed = buffer[buffer.length - 1] === lrc(buffer.subarray(0, buffer.length - 1));
|
|
1448
|
+
if (!lrcPassed) {
|
|
1449
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.LRC_MISMATCH, 'LRC check failed'));
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
this.emit('framing', frame, response, connection);
|
|
862
1453
|
}
|
|
863
1454
|
encode(data) {
|
|
864
1455
|
const buffer = Buffer.alloc(data.data.length + 3);
|
|
865
1456
|
buffer.writeUInt8(data.unit, 0);
|
|
866
1457
|
buffer.writeUInt8(data.fc, 1);
|
|
867
|
-
data.data
|
|
868
|
-
buffer.writeUInt8(num, 2 + index);
|
|
869
|
-
});
|
|
1458
|
+
buffer.set(data.data, 2);
|
|
870
1459
|
buffer.writeUInt8(lrc(buffer.subarray(0, -1)), buffer.length - 1);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1460
|
+
const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
|
|
1461
|
+
out[0] = CHAR_CODE.COLON;
|
|
1462
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
1463
|
+
const byte = buffer[i];
|
|
1464
|
+
out[1 + i * 2] = HEX_ENCODE[byte >> 4];
|
|
1465
|
+
out[2 + i * 2] = HEX_ENCODE[byte & 0x0f];
|
|
874
1466
|
}
|
|
875
|
-
|
|
876
|
-
|
|
1467
|
+
out[out.length - 2] = CHAR_CODE.CR;
|
|
1468
|
+
out[out.length - 1] = CHAR_CODE.LF;
|
|
1469
|
+
return out;
|
|
877
1470
|
}
|
|
878
1471
|
destroy() {
|
|
879
1472
|
this.removeAllListeners();
|
|
880
|
-
for (const
|
|
881
|
-
|
|
1473
|
+
for (const removeListener of this._removeAllListeners) {
|
|
1474
|
+
removeListener();
|
|
882
1475
|
}
|
|
1476
|
+
this._states.clear();
|
|
883
1477
|
}
|
|
884
1478
|
}
|
|
885
1479
|
|
|
1480
|
+
const MAX_TCP_FRAME = 260;
|
|
1481
|
+
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
886
1482
|
class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
887
1483
|
constructor(physicalLayer) {
|
|
888
1484
|
super();
|
|
889
|
-
Object.defineProperty(this, "
|
|
1485
|
+
Object.defineProperty(this, "PROTOCOL", {
|
|
890
1486
|
enumerable: true,
|
|
891
1487
|
configurable: true,
|
|
892
1488
|
writable: true,
|
|
893
|
-
value:
|
|
1489
|
+
value: 'TCP'
|
|
894
1490
|
});
|
|
895
1491
|
Object.defineProperty(this, "_transactionId", {
|
|
896
1492
|
enumerable: true,
|
|
@@ -898,98 +1494,209 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
898
1494
|
writable: true,
|
|
899
1495
|
value: 1
|
|
900
1496
|
});
|
|
1497
|
+
Object.defineProperty(this, "_buffers", {
|
|
1498
|
+
enumerable: true,
|
|
1499
|
+
configurable: true,
|
|
1500
|
+
writable: true,
|
|
1501
|
+
value: new Map()
|
|
1502
|
+
});
|
|
901
1503
|
Object.defineProperty(this, "_removeAllListeners", {
|
|
902
1504
|
enumerable: true,
|
|
903
1505
|
configurable: true,
|
|
904
1506
|
writable: true,
|
|
905
1507
|
value: []
|
|
906
1508
|
});
|
|
907
|
-
const handleData = (data, response) => {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1509
|
+
const handleData = (data, response, connection) => {
|
|
1510
|
+
var _a;
|
|
1511
|
+
let buffer = (_a = this._buffers.get(connection.id)) !== null && _a !== void 0 ? _a : EMPTY_BUFFER;
|
|
1512
|
+
buffer = buffer.length === 0 ? data : Buffer.concat([buffer, data]);
|
|
1513
|
+
while (buffer.length > 0) {
|
|
1514
|
+
const result = this.tryExtract(buffer);
|
|
1515
|
+
if (result.kind === 'frame') {
|
|
1516
|
+
this.processFrame(result.frame, response, connection);
|
|
1517
|
+
buffer = result.rest;
|
|
1518
|
+
}
|
|
1519
|
+
else if (result.kind === 'insufficient') {
|
|
1520
|
+
break;
|
|
914
1521
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
physicalLayer.removeListener('data', handleData);
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
framing(buffer, callback) {
|
|
923
|
-
if (buffer.length >= 8) {
|
|
924
|
-
if (buffer[2] === 0 && buffer[3] === 0 && buffer.readUInt16BE(4) === buffer.length - 6) {
|
|
925
|
-
const frame = {
|
|
926
|
-
transaction: buffer.readUInt16BE(0),
|
|
927
|
-
unit: buffer[6],
|
|
928
|
-
fc: buffer[7],
|
|
929
|
-
data: Array.from(buffer.subarray(8)),
|
|
930
|
-
buffer,
|
|
931
|
-
};
|
|
932
|
-
if (this._waitingResponse) {
|
|
933
|
-
for (const check of this._waitingResponse.preCheck) {
|
|
934
|
-
const res = check(frame);
|
|
935
|
-
if (typeof res === 'undefined') {
|
|
936
|
-
callback(new Error('Insufficient data length'));
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
if (typeof res === 'number') {
|
|
940
|
-
if (frame.data.length < res) {
|
|
941
|
-
callback(new Error('Insufficient data length'));
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
if (frame.data.length !== res) {
|
|
945
|
-
callback(new Error('Invalid response'));
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
if (!res) {
|
|
950
|
-
callback(new Error('Invalid response'));
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
1522
|
+
else {
|
|
1523
|
+
this.emit('framing-error', result.error);
|
|
1524
|
+
buffer = EMPTY_BUFFER;
|
|
1525
|
+
break;
|
|
954
1526
|
}
|
|
955
|
-
|
|
1527
|
+
}
|
|
1528
|
+
if (buffer.length === 0) {
|
|
1529
|
+
this._buffers.delete(connection.id);
|
|
956
1530
|
}
|
|
957
1531
|
else {
|
|
958
|
-
|
|
1532
|
+
this._buffers.set(connection.id, buffer);
|
|
959
1533
|
}
|
|
1534
|
+
};
|
|
1535
|
+
physicalLayer.on('data', handleData);
|
|
1536
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('data', handleData));
|
|
1537
|
+
const handleConnectionClose = (connection) => {
|
|
1538
|
+
this._buffers.delete(connection.id);
|
|
1539
|
+
};
|
|
1540
|
+
physicalLayer.on('connection-close', handleConnectionClose);
|
|
1541
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('connection-close', handleConnectionClose));
|
|
1542
|
+
const handleClose = () => {
|
|
1543
|
+
this._buffers.clear();
|
|
1544
|
+
};
|
|
1545
|
+
physicalLayer.on('close', handleClose);
|
|
1546
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('close', handleClose));
|
|
1547
|
+
}
|
|
1548
|
+
tryExtract(buffer) {
|
|
1549
|
+
if (buffer.length < 8) {
|
|
1550
|
+
return { kind: 'insufficient' };
|
|
1551
|
+
}
|
|
1552
|
+
if (buffer[2] !== 0 || buffer[3] !== 0) {
|
|
1553
|
+
return { kind: 'error', error: new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data') };
|
|
1554
|
+
}
|
|
1555
|
+
const length = buffer.readUInt16BE(4);
|
|
1556
|
+
const total = 6 + length;
|
|
1557
|
+
if (total > MAX_TCP_FRAME || length < 2) {
|
|
1558
|
+
return { kind: 'error', error: new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data') };
|
|
1559
|
+
}
|
|
1560
|
+
if (buffer.length < total) {
|
|
1561
|
+
return { kind: 'insufficient' };
|
|
1562
|
+
}
|
|
1563
|
+
return { kind: 'frame', frame: buffer.subarray(0, total), rest: buffer.subarray(total) };
|
|
1564
|
+
}
|
|
1565
|
+
processFrame(buffer, response, connection) {
|
|
1566
|
+
const frame = {
|
|
1567
|
+
transaction: buffer.readUInt16BE(0),
|
|
1568
|
+
unit: buffer[6],
|
|
1569
|
+
fc: buffer[7],
|
|
1570
|
+
data: buffer.subarray(8),
|
|
1571
|
+
buffer,
|
|
1572
|
+
};
|
|
1573
|
+
this.emit('framing', frame, response, connection);
|
|
1574
|
+
}
|
|
1575
|
+
encode(data) {
|
|
1576
|
+
var _a;
|
|
1577
|
+
const transaction = (_a = data.transaction) !== null && _a !== void 0 ? _a : this._transactionId;
|
|
1578
|
+
const buffer = Buffer.alloc(data.data.length + 8);
|
|
1579
|
+
buffer.writeUInt16BE(transaction, 0);
|
|
1580
|
+
buffer.writeUInt16BE(0, 2);
|
|
1581
|
+
buffer.writeUInt16BE(data.data.length + 2, 4);
|
|
1582
|
+
buffer.writeUInt8(data.unit, 6);
|
|
1583
|
+
buffer.writeUInt8(data.fc, 7);
|
|
1584
|
+
buffer.set(data.data, 8);
|
|
1585
|
+
if (data.transaction === undefined) {
|
|
1586
|
+
this._transactionId = (this._transactionId + 1) % 65536 || 1;
|
|
960
1587
|
}
|
|
961
|
-
|
|
962
|
-
|
|
1588
|
+
return buffer;
|
|
1589
|
+
}
|
|
1590
|
+
destroy() {
|
|
1591
|
+
this.removeAllListeners();
|
|
1592
|
+
for (const removeListener of this._removeAllListeners) {
|
|
1593
|
+
removeListener();
|
|
963
1594
|
}
|
|
1595
|
+
this._buffers.clear();
|
|
964
1596
|
}
|
|
965
|
-
|
|
966
|
-
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const FIFO_KEY = 'fifo';
|
|
1600
|
+
class MasterSession {
|
|
1601
|
+
constructor() {
|
|
1602
|
+
Object.defineProperty(this, "_waiters", {
|
|
1603
|
+
enumerable: true,
|
|
1604
|
+
configurable: true,
|
|
1605
|
+
writable: true,
|
|
1606
|
+
value: new Map()
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
start(key, preCheck, callback) {
|
|
1610
|
+
this._waiters.set(key, { preCheck, callback });
|
|
1611
|
+
}
|
|
1612
|
+
stop(key) {
|
|
1613
|
+
this._waiters.delete(key);
|
|
1614
|
+
}
|
|
1615
|
+
stopAll(error) {
|
|
1616
|
+
const waiters = [...this._waiters.values()];
|
|
1617
|
+
this._waiters.clear();
|
|
1618
|
+
for (const w of waiters) {
|
|
1619
|
+
w.callback(error);
|
|
1620
|
+
}
|
|
967
1621
|
}
|
|
968
|
-
|
|
969
|
-
this.
|
|
1622
|
+
has(key) {
|
|
1623
|
+
return this._waiters.has(key);
|
|
970
1624
|
}
|
|
971
|
-
|
|
1625
|
+
handleFrame(frame) {
|
|
972
1626
|
var _a;
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1627
|
+
const key = (_a = frame.transaction) !== null && _a !== void 0 ? _a : FIFO_KEY;
|
|
1628
|
+
const waiter = this._waiters.get(key);
|
|
1629
|
+
if (!waiter) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (!this.runPreChecks(waiter, frame)) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
waiter.callback(null, frame);
|
|
1636
|
+
}
|
|
1637
|
+
handleError(error) {
|
|
1638
|
+
// Framing errors lose transaction context; reject every in-flight waiter.
|
|
1639
|
+
this.stopAll(error);
|
|
1640
|
+
}
|
|
1641
|
+
runPreChecks(waiter, frame) {
|
|
1642
|
+
for (const check of waiter.preCheck) {
|
|
1643
|
+
const res = check(frame);
|
|
1644
|
+
if (typeof res === 'undefined') {
|
|
1645
|
+
waiter.callback(new ModbusError(ModbusErrorCode.INSUFFICIENT_DATA, 'Insufficient data length'));
|
|
1646
|
+
return false;
|
|
1647
|
+
}
|
|
1648
|
+
if (typeof res === 'number') {
|
|
1649
|
+
if (frame.data.length < res) {
|
|
1650
|
+
waiter.callback(new ModbusError(ModbusErrorCode.INSUFFICIENT_DATA, 'Insufficient data length'));
|
|
1651
|
+
return false;
|
|
1652
|
+
}
|
|
1653
|
+
if (frame.data.length !== res) {
|
|
1654
|
+
waiter.callback(new ModbusError(ModbusErrorCode.INVALID_RESPONSE, 'Invalid response'));
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (!res) {
|
|
1660
|
+
waiter.callback(new ModbusError(ModbusErrorCode.INVALID_RESPONSE, 'Invalid response'));
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
989
1663
|
}
|
|
1664
|
+
return true;
|
|
990
1665
|
}
|
|
991
1666
|
}
|
|
992
1667
|
|
|
1668
|
+
/******************************************************************************
|
|
1669
|
+
Copyright (c) Microsoft Corporation.
|
|
1670
|
+
|
|
1671
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
1672
|
+
purpose with or without fee is hereby granted.
|
|
1673
|
+
|
|
1674
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
1675
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
1676
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
1677
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
1678
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
1679
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
1680
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
1681
|
+
***************************************************************************** */
|
|
1682
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
1686
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
1687
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
1688
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
1689
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
1690
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
1691
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
1696
|
+
var e = new Error(message);
|
|
1697
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
1698
|
+
};
|
|
1699
|
+
|
|
993
1700
|
class ModbusMaster extends EventEmitter {
|
|
994
1701
|
get isOpen() {
|
|
995
1702
|
return this.physicalLayer.isOpen;
|
|
@@ -997,7 +1704,8 @@ class ModbusMaster extends EventEmitter {
|
|
|
997
1704
|
get destroyed() {
|
|
998
1705
|
return this.physicalLayer.destroyed;
|
|
999
1706
|
}
|
|
1000
|
-
constructor(applicationLayer, physicalLayer,
|
|
1707
|
+
constructor(applicationLayer, physicalLayer, options = {}) {
|
|
1708
|
+
var _a, _b;
|
|
1001
1709
|
super();
|
|
1002
1710
|
Object.defineProperty(this, "applicationLayer", {
|
|
1003
1711
|
enumerable: true,
|
|
@@ -1011,11 +1719,47 @@ class ModbusMaster extends EventEmitter {
|
|
|
1011
1719
|
writable: true,
|
|
1012
1720
|
value: physicalLayer
|
|
1013
1721
|
});
|
|
1722
|
+
Object.defineProperty(this, "_masterSession", {
|
|
1723
|
+
enumerable: true,
|
|
1724
|
+
configurable: true,
|
|
1725
|
+
writable: true,
|
|
1726
|
+
value: new MasterSession()
|
|
1727
|
+
});
|
|
1728
|
+
Object.defineProperty(this, "_queue", {
|
|
1729
|
+
enumerable: true,
|
|
1730
|
+
configurable: true,
|
|
1731
|
+
writable: true,
|
|
1732
|
+
value: []
|
|
1733
|
+
});
|
|
1734
|
+
Object.defineProperty(this, "_draining", {
|
|
1735
|
+
enumerable: true,
|
|
1736
|
+
configurable: true,
|
|
1737
|
+
writable: true,
|
|
1738
|
+
value: false
|
|
1739
|
+
});
|
|
1740
|
+
Object.defineProperty(this, "_nextTid", {
|
|
1741
|
+
enumerable: true,
|
|
1742
|
+
configurable: true,
|
|
1743
|
+
writable: true,
|
|
1744
|
+
value: 1
|
|
1745
|
+
});
|
|
1746
|
+
Object.defineProperty(this, "_closed", {
|
|
1747
|
+
enumerable: true,
|
|
1748
|
+
configurable: true,
|
|
1749
|
+
writable: true,
|
|
1750
|
+
value: false
|
|
1751
|
+
});
|
|
1014
1752
|
Object.defineProperty(this, "timeout", {
|
|
1015
1753
|
enumerable: true,
|
|
1016
1754
|
configurable: true,
|
|
1017
1755
|
writable: true,
|
|
1018
|
-
value:
|
|
1756
|
+
value: void 0
|
|
1757
|
+
});
|
|
1758
|
+
Object.defineProperty(this, "concurrent", {
|
|
1759
|
+
enumerable: true,
|
|
1760
|
+
configurable: true,
|
|
1761
|
+
writable: true,
|
|
1762
|
+
value: void 0
|
|
1019
1763
|
});
|
|
1020
1764
|
Object.defineProperty(this, "writeFC1", {
|
|
1021
1765
|
enumerable: true,
|
|
@@ -1089,6 +1833,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
1089
1833
|
writable: true,
|
|
1090
1834
|
value: void 0
|
|
1091
1835
|
});
|
|
1836
|
+
this.timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 1000;
|
|
1837
|
+
this.concurrent = (_b = options.concurrent) !== null && _b !== void 0 ? _b : false;
|
|
1838
|
+
if (this.concurrent && this.applicationLayer.PROTOCOL !== 'TCP') {
|
|
1839
|
+
throw new ModbusError(ModbusErrorCode.CONCURRENT_NOT_TCP, 'concurrent mode requires a Modbus TCP application layer');
|
|
1840
|
+
}
|
|
1841
|
+
this.applicationLayer.role = 'MASTER';
|
|
1092
1842
|
this.writeFC1 = this.readCoils;
|
|
1093
1843
|
this.writeFC2 = this.readDiscreteInputs;
|
|
1094
1844
|
this.writeFC3 = this.readHoldingRegisters;
|
|
@@ -1101,6 +1851,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
1101
1851
|
this.handleFC22 = this.maskWriteRegister;
|
|
1102
1852
|
this.handleFC23 = this.readAndWriteMultipleRegisters;
|
|
1103
1853
|
this.handleFC43_14 = this.readDeviceIdentification;
|
|
1854
|
+
applicationLayer.on('framing', (frame) => {
|
|
1855
|
+
this._masterSession.handleFrame(frame);
|
|
1856
|
+
});
|
|
1857
|
+
applicationLayer.on('framing-error', (error) => {
|
|
1858
|
+
this._masterSession.handleError(error);
|
|
1859
|
+
});
|
|
1104
1860
|
physicalLayer.on('error', (error) => {
|
|
1105
1861
|
this.emit('error', error);
|
|
1106
1862
|
});
|
|
@@ -1108,50 +1864,90 @@ class ModbusMaster extends EventEmitter {
|
|
|
1108
1864
|
this.emit('close');
|
|
1109
1865
|
});
|
|
1110
1866
|
}
|
|
1111
|
-
|
|
1867
|
+
send(adu, preCheck, timeout, broadcast) {
|
|
1868
|
+
const task = () => this._exchange(adu, preCheck, timeout, broadcast);
|
|
1869
|
+
if (this.concurrent) {
|
|
1870
|
+
return task();
|
|
1871
|
+
}
|
|
1872
|
+
return new Promise((resolve, reject) => {
|
|
1873
|
+
const run = () => __awaiter(this, void 0, void 0, function* () {
|
|
1874
|
+
try {
|
|
1875
|
+
resolve(yield task());
|
|
1876
|
+
}
|
|
1877
|
+
catch (error) {
|
|
1878
|
+
reject(error);
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
this._queue.push({ run, cancel: reject });
|
|
1882
|
+
this._drain();
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
_drain() {
|
|
1886
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1887
|
+
if (this._draining) {
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
this._draining = true;
|
|
1891
|
+
try {
|
|
1892
|
+
while (this._queue.length > 0) {
|
|
1893
|
+
const item = this._queue.shift();
|
|
1894
|
+
yield item.run();
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
finally {
|
|
1898
|
+
this._draining = false;
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
_exchange(adu, preCheck, timeout, broadcast) {
|
|
1112
1903
|
return new Promise((resolve, reject) => {
|
|
1904
|
+
if (this._closed) {
|
|
1905
|
+
reject(new ModbusError(ModbusErrorCode.MASTER_CLOSED, 'Master closed'));
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
const usesTid = this.applicationLayer.PROTOCOL === 'TCP' && !broadcast;
|
|
1909
|
+
let tid;
|
|
1910
|
+
if (usesTid) {
|
|
1911
|
+
tid = this._nextTid;
|
|
1912
|
+
this._nextTid = (this._nextTid + 1) % 65536 || 1;
|
|
1913
|
+
}
|
|
1914
|
+
const key = tid !== null && tid !== void 0 ? tid : FIFO_KEY;
|
|
1915
|
+
const payload = this.applicationLayer.encode(Object.assign(Object.assign({}, adu), { transaction: tid }));
|
|
1113
1916
|
let settled = false;
|
|
1114
|
-
const
|
|
1115
|
-
if (
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1917
|
+
const settle = (error, frame) => {
|
|
1918
|
+
if (settled) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
settled = true;
|
|
1922
|
+
clearTimeout(timer);
|
|
1923
|
+
this._masterSession.stop(key);
|
|
1924
|
+
if (error) {
|
|
1925
|
+
reject(error);
|
|
1926
|
+
}
|
|
1927
|
+
else {
|
|
1928
|
+
resolve(frame);
|
|
1119
1929
|
}
|
|
1120
|
-
}
|
|
1930
|
+
};
|
|
1931
|
+
const timer = setTimeout(() => settle(new ModbusError(ModbusErrorCode.TIMEOUT, 'Timeout')), timeout);
|
|
1932
|
+
// FIFO mode: clear stale buffer state before this request.
|
|
1933
|
+
// Concurrent mode: must not flush — other in-flight requests share the buffer.
|
|
1934
|
+
if (!this.concurrent) {
|
|
1935
|
+
this.applicationLayer.flush();
|
|
1936
|
+
}
|
|
1121
1937
|
this.physicalLayer
|
|
1122
|
-
.write(
|
|
1938
|
+
.write(payload)
|
|
1123
1939
|
.then(() => {
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
settled = true;
|
|
1127
|
-
clearTimeout(tid);
|
|
1128
|
-
resolve();
|
|
1129
|
-
}
|
|
1130
|
-
else {
|
|
1131
|
-
this.applicationLayer.startWaitingResponse(response.preCheck, (error, frame) => {
|
|
1132
|
-
if (!settled) {
|
|
1133
|
-
settled = true;
|
|
1134
|
-
clearTimeout(tid);
|
|
1135
|
-
this.applicationLayer.stopWaitingResponse();
|
|
1136
|
-
if (error) {
|
|
1137
|
-
reject(error);
|
|
1138
|
-
}
|
|
1139
|
-
else {
|
|
1140
|
-
resolve(frame);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1940
|
+
if (settled) {
|
|
1941
|
+
return;
|
|
1145
1942
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
settled = true;
|
|
1150
|
-
clearTimeout(tid);
|
|
1151
|
-
this.applicationLayer.stopWaitingResponse();
|
|
1152
|
-
reject(error);
|
|
1943
|
+
if (broadcast) {
|
|
1944
|
+
settle(null);
|
|
1945
|
+
return;
|
|
1153
1946
|
}
|
|
1154
|
-
|
|
1947
|
+
const finalChecks = usesTid ? [(frame) => frame.transaction === tid, ...preCheck] : preCheck;
|
|
1948
|
+
this._masterSession.start(key, finalChecks, (error, frame) => settle(error, frame));
|
|
1949
|
+
})
|
|
1950
|
+
.catch((error) => settle(error));
|
|
1155
1951
|
});
|
|
1156
1952
|
}
|
|
1157
1953
|
writeFC1Or2(unit, fc, address, length, timeout) {
|
|
@@ -1159,104 +1955,60 @@ class ModbusMaster extends EventEmitter {
|
|
|
1159
1955
|
const bufferTx = Buffer.alloc(4);
|
|
1160
1956
|
bufferTx.writeUInt16BE(address, 0);
|
|
1161
1957
|
bufferTx.writeUInt16BE(length, 2);
|
|
1162
|
-
return this.
|
|
1163
|
-
data: this.applicationLayer.encode({
|
|
1164
|
-
unit,
|
|
1165
|
-
fc,
|
|
1166
|
-
data: Array.from(bufferTx),
|
|
1167
|
-
}),
|
|
1168
|
-
broadcast: unit === 0,
|
|
1169
|
-
}, {
|
|
1170
|
-
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + byteCount, (frame) => frame.data[0] === byteCount],
|
|
1171
|
-
}, timeout).then((frame) => {
|
|
1958
|
+
return this.send({ unit, fc, data: bufferTx }, [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + byteCount, (frame) => frame.data[0] === byteCount], timeout, unit === 0).then((frame) => {
|
|
1172
1959
|
if (frame) {
|
|
1173
|
-
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }
|
|
1960
|
+
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }, (_, index) => (frame.data[1 + ~~(index / 8)] & (1 << index % 8)) > 0) });
|
|
1174
1961
|
}
|
|
1175
1962
|
});
|
|
1176
1963
|
}
|
|
1177
1964
|
readCoils(unit, address, length, timeout = this.timeout) {
|
|
1178
|
-
return this.writeFC1Or2(unit,
|
|
1965
|
+
return this.writeFC1Or2(unit, FunctionCode.READ_COILS, address, length, timeout);
|
|
1179
1966
|
}
|
|
1180
1967
|
readDiscreteInputs(unit, address, length, timeout = this.timeout) {
|
|
1181
|
-
return this.writeFC1Or2(unit,
|
|
1968
|
+
return this.writeFC1Or2(unit, FunctionCode.READ_DISCRETE_INPUTS, address, length, timeout);
|
|
1182
1969
|
}
|
|
1183
1970
|
writeFC3Or4(unit, fc, address, length, timeout) {
|
|
1184
1971
|
const byteCount = length * 2;
|
|
1185
1972
|
const bufferTx = Buffer.alloc(4);
|
|
1186
1973
|
bufferTx.writeUInt16BE(address, 0);
|
|
1187
1974
|
bufferTx.writeUInt16BE(length, 2);
|
|
1188
|
-
return this.
|
|
1189
|
-
data: this.applicationLayer.encode({
|
|
1190
|
-
unit,
|
|
1191
|
-
fc,
|
|
1192
|
-
data: Array.from(bufferTx),
|
|
1193
|
-
}),
|
|
1194
|
-
broadcast: unit === 0,
|
|
1195
|
-
}, {
|
|
1196
|
-
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + byteCount, (frame) => frame.data[0] === byteCount],
|
|
1197
|
-
}, timeout).then((frame) => {
|
|
1975
|
+
return this.send({ unit, fc, data: bufferTx }, [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + byteCount, (frame) => frame.data[0] === byteCount], timeout, unit === 0).then((frame) => {
|
|
1198
1976
|
if (frame) {
|
|
1199
|
-
const bufferRx =
|
|
1200
|
-
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }
|
|
1977
|
+
const bufferRx = frame.data.subarray(1);
|
|
1978
|
+
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }, (_, index) => bufferRx.readUInt16BE(index * 2)) });
|
|
1201
1979
|
}
|
|
1202
1980
|
});
|
|
1203
1981
|
}
|
|
1204
1982
|
readHoldingRegisters(unit, address, length, timeout = this.timeout) {
|
|
1205
|
-
return this.writeFC3Or4(unit,
|
|
1983
|
+
return this.writeFC3Or4(unit, FunctionCode.READ_HOLDING_REGISTERS, address, length, timeout);
|
|
1206
1984
|
}
|
|
1207
1985
|
readInputRegisters(unit, address, length, timeout = this.timeout) {
|
|
1208
|
-
return this.writeFC3Or4(unit,
|
|
1986
|
+
return this.writeFC3Or4(unit, FunctionCode.READ_INPUT_REGISTERS, address, length, timeout);
|
|
1209
1987
|
}
|
|
1210
1988
|
writeSingleCoil(unit, address, value, timeout = this.timeout) {
|
|
1211
|
-
const fc =
|
|
1989
|
+
const fc = FunctionCode.WRITE_SINGLE_COIL;
|
|
1212
1990
|
const bufferTx = Buffer.alloc(4);
|
|
1213
1991
|
bufferTx.writeUInt16BE(address, 0);
|
|
1214
|
-
bufferTx.writeUInt16BE(value ?
|
|
1215
|
-
return this.
|
|
1216
|
-
data: this.applicationLayer.encode({
|
|
1217
|
-
unit,
|
|
1218
|
-
fc,
|
|
1219
|
-
data: Array.from(bufferTx),
|
|
1220
|
-
}),
|
|
1221
|
-
broadcast: unit === 0,
|
|
1222
|
-
}, {
|
|
1223
|
-
preCheck: [
|
|
1224
|
-
(frame) => frame.unit === unit && frame.fc === fc,
|
|
1225
|
-
() => bufferTx.length,
|
|
1226
|
-
(frame) => frame.data.every((v, i) => v === bufferTx[i]),
|
|
1227
|
-
],
|
|
1228
|
-
}, timeout).then((frame) => {
|
|
1992
|
+
bufferTx.writeUInt16BE(value ? COIL_ON : COIL_OFF, 2);
|
|
1993
|
+
return this.send({ unit, fc, data: bufferTx }, [(frame) => frame.unit === unit && frame.fc === fc, () => bufferTx.length, (frame) => frame.data.equals(bufferTx)], timeout, unit === 0).then((frame) => {
|
|
1229
1994
|
if (frame) {
|
|
1230
1995
|
return Object.assign(Object.assign({}, frame), { data: value });
|
|
1231
1996
|
}
|
|
1232
1997
|
});
|
|
1233
1998
|
}
|
|
1234
1999
|
writeSingleRegister(unit, address, value, timeout = this.timeout) {
|
|
1235
|
-
const fc =
|
|
2000
|
+
const fc = FunctionCode.WRITE_SINGLE_REGISTER;
|
|
1236
2001
|
const bufferTx = Buffer.alloc(4);
|
|
1237
2002
|
bufferTx.writeUInt16BE(address, 0);
|
|
1238
2003
|
bufferTx.writeUInt16BE(value, 2);
|
|
1239
|
-
return this.
|
|
1240
|
-
data: this.applicationLayer.encode({
|
|
1241
|
-
unit,
|
|
1242
|
-
fc,
|
|
1243
|
-
data: Array.from(bufferTx),
|
|
1244
|
-
}),
|
|
1245
|
-
broadcast: unit === 0,
|
|
1246
|
-
}, {
|
|
1247
|
-
preCheck: [
|
|
1248
|
-
(frame) => frame.unit === unit && frame.fc === fc,
|
|
1249
|
-
() => bufferTx.length,
|
|
1250
|
-
(frame) => frame.data.every((v, i) => v === bufferTx[i]),
|
|
1251
|
-
],
|
|
1252
|
-
}, timeout).then((frame) => {
|
|
2004
|
+
return this.send({ unit, fc, data: bufferTx }, [(frame) => frame.unit === unit && frame.fc === fc, () => bufferTx.length, (frame) => frame.data.equals(bufferTx)], timeout, unit === 0).then((frame) => {
|
|
1253
2005
|
if (frame) {
|
|
1254
2006
|
return Object.assign(Object.assign({}, frame), { data: value });
|
|
1255
2007
|
}
|
|
1256
2008
|
});
|
|
1257
2009
|
}
|
|
1258
2010
|
writeMultipleCoils(unit, address, value, timeout = this.timeout) {
|
|
1259
|
-
const fc =
|
|
2011
|
+
const fc = FunctionCode.WRITE_MULTIPLE_COILS;
|
|
1260
2012
|
const byteCount = Math.ceil(value.length / 8);
|
|
1261
2013
|
const bufferTx = Buffer.alloc(5 + byteCount);
|
|
1262
2014
|
bufferTx.writeUInt16BE(address, 0);
|
|
@@ -1267,23 +2019,14 @@ class ModbusMaster extends EventEmitter {
|
|
|
1267
2019
|
bufferTx[5 + ~~(i / 8)] |= 1 << i % 8;
|
|
1268
2020
|
}
|
|
1269
2021
|
});
|
|
1270
|
-
return this.
|
|
1271
|
-
data: this.applicationLayer.encode({
|
|
1272
|
-
unit,
|
|
1273
|
-
fc,
|
|
1274
|
-
data: Array.from(bufferTx),
|
|
1275
|
-
}),
|
|
1276
|
-
broadcast: unit === 0,
|
|
1277
|
-
}, {
|
|
1278
|
-
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 4, (frame) => frame.data.every((v, i) => v === bufferTx[i])],
|
|
1279
|
-
}, timeout).then((frame) => {
|
|
2022
|
+
return this.send({ unit, fc, data: bufferTx }, [(frame) => frame.unit === unit && frame.fc === fc, () => 4, (frame) => frame.data.equals(bufferTx.subarray(0, 4))], timeout, unit === 0).then((frame) => {
|
|
1280
2023
|
if (frame) {
|
|
1281
2024
|
return Object.assign(Object.assign({}, frame), { data: value });
|
|
1282
2025
|
}
|
|
1283
2026
|
});
|
|
1284
2027
|
}
|
|
1285
2028
|
writeMultipleRegisters(unit, address, value, timeout = this.timeout) {
|
|
1286
|
-
const fc =
|
|
2029
|
+
const fc = FunctionCode.WRITE_MULTIPLE_REGISTERS;
|
|
1287
2030
|
const byteCount = value.length * 2;
|
|
1288
2031
|
const bufferTx = Buffer.alloc(5 + byteCount);
|
|
1289
2032
|
bufferTx.writeUInt16BE(address, 0);
|
|
@@ -1292,73 +2035,48 @@ class ModbusMaster extends EventEmitter {
|
|
|
1292
2035
|
value.forEach((v, i) => {
|
|
1293
2036
|
bufferTx.writeUInt16BE(v, 5 + i * 2);
|
|
1294
2037
|
});
|
|
1295
|
-
return this.
|
|
1296
|
-
data: this.applicationLayer.encode({
|
|
1297
|
-
unit,
|
|
1298
|
-
fc,
|
|
1299
|
-
data: Array.from(bufferTx),
|
|
1300
|
-
}),
|
|
1301
|
-
broadcast: unit === 0,
|
|
1302
|
-
}, {
|
|
1303
|
-
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 4, (frame) => frame.data.every((v, i) => v === bufferTx[i])],
|
|
1304
|
-
}, timeout).then((frame) => {
|
|
2038
|
+
return this.send({ unit, fc, data: bufferTx }, [(frame) => frame.unit === unit && frame.fc === fc, () => 4, (frame) => frame.data.equals(bufferTx.subarray(0, 4))], timeout, unit === 0).then((frame) => {
|
|
1305
2039
|
if (frame) {
|
|
1306
2040
|
return Object.assign(Object.assign({}, frame), { data: value });
|
|
1307
2041
|
}
|
|
1308
2042
|
});
|
|
1309
2043
|
}
|
|
1310
|
-
reportServerId(unit, timeout = this.timeout) {
|
|
1311
|
-
const fc =
|
|
1312
|
-
return this.
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
preCheck: [
|
|
1321
|
-
(frame) => frame.unit === unit && frame.fc === fc,
|
|
1322
|
-
(frame) => {
|
|
1323
|
-
if (frame.data.length >= 3) {
|
|
1324
|
-
return 1 + frame.data[0];
|
|
1325
|
-
}
|
|
1326
|
-
},
|
|
1327
|
-
],
|
|
1328
|
-
}, timeout).then((frame) => {
|
|
2044
|
+
reportServerId(unit, serverIdLength = 1, timeout = this.timeout) {
|
|
2045
|
+
const fc = FunctionCode.REPORT_SERVER_ID;
|
|
2046
|
+
return this.send({ unit, fc, data: Buffer.alloc(0) }, [
|
|
2047
|
+
(frame) => frame.unit === unit && frame.fc === fc,
|
|
2048
|
+
(frame) => {
|
|
2049
|
+
if (frame.data.length >= 2 + serverIdLength) {
|
|
2050
|
+
return 1 + frame.data[0];
|
|
2051
|
+
}
|
|
2052
|
+
},
|
|
2053
|
+
], timeout, unit === 0).then((frame) => {
|
|
1329
2054
|
if (frame) {
|
|
2055
|
+
const runStatusIndex = 1 + serverIdLength;
|
|
1330
2056
|
return Object.assign(Object.assign({}, frame), { data: {
|
|
1331
|
-
serverId: frame.data[1],
|
|
1332
|
-
runIndicatorStatus: frame.data[
|
|
1333
|
-
additionalData: frame.data.
|
|
2057
|
+
serverId: serverIdLength === 1 ? frame.data[1] : Array.from(frame.data.subarray(1, runStatusIndex)),
|
|
2058
|
+
runIndicatorStatus: frame.data[runStatusIndex] === 0xff,
|
|
2059
|
+
additionalData: Array.from(frame.data.subarray(runStatusIndex + 1)),
|
|
1334
2060
|
} });
|
|
1335
2061
|
}
|
|
1336
2062
|
});
|
|
1337
2063
|
}
|
|
1338
2064
|
maskWriteRegister(unit, address, andMask, orMask, timeout = this.timeout) {
|
|
1339
|
-
const fc =
|
|
2065
|
+
const fc = FunctionCode.MASK_WRITE_REGISTER;
|
|
1340
2066
|
const bufferTx = Buffer.alloc(6);
|
|
1341
2067
|
bufferTx.writeUInt16BE(address, 0);
|
|
1342
2068
|
bufferTx.writeUInt16BE(andMask, 2);
|
|
1343
2069
|
bufferTx.writeUInt16BE(orMask, 4);
|
|
1344
|
-
return this.
|
|
1345
|
-
data: this.applicationLayer.encode({
|
|
1346
|
-
unit,
|
|
1347
|
-
fc,
|
|
1348
|
-
data: Array.from(bufferTx),
|
|
1349
|
-
}),
|
|
1350
|
-
broadcast: unit === 0,
|
|
1351
|
-
}, {
|
|
1352
|
-
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 6, (frame) => frame.data.every((v, i) => v === bufferTx[i])],
|
|
1353
|
-
}, timeout).then((frame) => {
|
|
2070
|
+
return this.send({ unit, fc, data: bufferTx }, [(frame) => frame.unit === unit && frame.fc === fc, () => 6, (frame) => frame.data.equals(bufferTx)], timeout, unit === 0).then((frame) => {
|
|
1354
2071
|
if (frame) {
|
|
1355
2072
|
return Object.assign(Object.assign({}, frame), { data: { andMask, orMask } });
|
|
1356
2073
|
}
|
|
1357
2074
|
});
|
|
1358
2075
|
}
|
|
1359
2076
|
readAndWriteMultipleRegisters(unit, read, write, timeout = this.timeout) {
|
|
1360
|
-
const fc =
|
|
2077
|
+
const fc = FunctionCode.READ_WRITE_MULTIPLE_REGISTERS;
|
|
1361
2078
|
const byteCount = write.value.length * 2;
|
|
2079
|
+
const readByteCount = read.length * 2;
|
|
1362
2080
|
const bufferTx = Buffer.alloc(9 + byteCount);
|
|
1363
2081
|
bufferTx.writeUInt16BE(read.address, 0);
|
|
1364
2082
|
bufferTx.writeUInt16BE(read.length, 2);
|
|
@@ -1368,150 +2086,108 @@ class ModbusMaster extends EventEmitter {
|
|
|
1368
2086
|
write.value.forEach((v, i) => {
|
|
1369
2087
|
bufferTx.writeUInt16BE(v, 9 + i * 2);
|
|
1370
2088
|
});
|
|
1371
|
-
return this.
|
|
1372
|
-
data: this.applicationLayer.encode({
|
|
1373
|
-
unit,
|
|
1374
|
-
fc,
|
|
1375
|
-
data: Array.from(bufferTx),
|
|
1376
|
-
}),
|
|
1377
|
-
broadcast: unit === 0,
|
|
1378
|
-
}, {
|
|
1379
|
-
preCheck: [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + byteCount, (frame) => frame.data[0] === byteCount],
|
|
1380
|
-
}, timeout).then((frame) => {
|
|
2089
|
+
return this.send({ unit, fc, data: bufferTx }, [(frame) => frame.unit === unit && frame.fc === fc, () => 1 + readByteCount, (frame) => frame.data[0] === readByteCount], timeout, unit === 0).then((frame) => {
|
|
1381
2090
|
if (frame) {
|
|
1382
|
-
const bufferRx =
|
|
1383
|
-
return Object.assign(Object.assign({}, frame), { data: Array.from({ length: read.length }
|
|
2091
|
+
const bufferRx = frame.data.subarray(1);
|
|
2092
|
+
return Object.assign(Object.assign({}, frame), { data: Array.from({ length: read.length }, (_, index) => bufferRx.readUInt16BE(index * 2)) });
|
|
1384
2093
|
}
|
|
1385
2094
|
});
|
|
1386
2095
|
}
|
|
1387
2096
|
readDeviceIdentification(unit, readDeviceIDCode, objectId, timeout = this.timeout) {
|
|
1388
|
-
const fc =
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
data
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
case 3: {
|
|
1416
|
-
object[2].push(v);
|
|
1417
|
-
if (object[1] === object[2].length) {
|
|
1418
|
-
objects.push(2 + object[1]);
|
|
1419
|
-
object = [];
|
|
1420
|
-
}
|
|
1421
|
-
break;
|
|
2097
|
+
const fc = FunctionCode.READ_DEVICE_IDENTIFICATION;
|
|
2098
|
+
let parsed;
|
|
2099
|
+
return this.send({ unit, fc, data: Buffer.from([MEI_READ_DEVICE_ID, readDeviceIDCode, objectId]) }, [
|
|
2100
|
+
(frame) => frame.unit === unit && frame.fc === fc,
|
|
2101
|
+
(frame) => {
|
|
2102
|
+
if (frame.data.length >= 6) {
|
|
2103
|
+
if (frame.data[0] === MEI_READ_DEVICE_ID && frame.data[1] === readDeviceIDCode) {
|
|
2104
|
+
const objects = [];
|
|
2105
|
+
let object = [];
|
|
2106
|
+
let totalBytes = 0;
|
|
2107
|
+
for (const v of frame.data.subarray(6)) {
|
|
2108
|
+
switch (object.length) {
|
|
2109
|
+
case 0:
|
|
2110
|
+
case 1: {
|
|
2111
|
+
object.push(v);
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
case 2: {
|
|
2115
|
+
object.push([v]);
|
|
2116
|
+
break;
|
|
2117
|
+
}
|
|
2118
|
+
case 3: {
|
|
2119
|
+
object[2].push(v);
|
|
2120
|
+
if (object[1] === object[2].length) {
|
|
2121
|
+
objects.push({ id: object[0], value: Buffer.from(object[2]).toString() });
|
|
2122
|
+
totalBytes += 2 + object[1];
|
|
2123
|
+
object = [];
|
|
1422
2124
|
}
|
|
2125
|
+
break;
|
|
1423
2126
|
}
|
|
1424
2127
|
}
|
|
1425
|
-
if (objects.length === frame.data[5]) {
|
|
1426
|
-
return 6 + objects.reduce((previous, current) => previous + current, 0);
|
|
1427
|
-
}
|
|
1428
2128
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
2129
|
+
if (objects.length === frame.data[5]) {
|
|
2130
|
+
parsed = objects;
|
|
2131
|
+
return 6 + totalBytes;
|
|
1431
2132
|
}
|
|
1432
2133
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
}, timeout).then((frame) => {
|
|
1436
|
-
if (frame) {
|
|
1437
|
-
const objects = [];
|
|
1438
|
-
let object = [];
|
|
1439
|
-
for (const v of frame.data.slice(6)) {
|
|
1440
|
-
switch (object.length) {
|
|
1441
|
-
case 0:
|
|
1442
|
-
case 1: {
|
|
1443
|
-
object.push(v);
|
|
1444
|
-
break;
|
|
1445
|
-
}
|
|
1446
|
-
case 2: {
|
|
1447
|
-
object.push([v]);
|
|
1448
|
-
break;
|
|
1449
|
-
}
|
|
1450
|
-
case 3: {
|
|
1451
|
-
object[2].push(v);
|
|
1452
|
-
if (object[1] === object[2].length) {
|
|
1453
|
-
objects.push({ id: object[0], value: Buffer.from(object[2]).toString() });
|
|
1454
|
-
object = [];
|
|
1455
|
-
}
|
|
1456
|
-
break;
|
|
1457
|
-
}
|
|
2134
|
+
else {
|
|
2135
|
+
return false;
|
|
1458
2136
|
}
|
|
1459
2137
|
}
|
|
2138
|
+
},
|
|
2139
|
+
], timeout, unit === 0).then((frame) => {
|
|
2140
|
+
if (frame && parsed) {
|
|
1460
2141
|
return Object.assign(Object.assign({}, frame), { data: {
|
|
1461
2142
|
readDeviceIDCode,
|
|
1462
2143
|
conformityLevel: frame.data[2],
|
|
1463
2144
|
moreFollows: frame.data[3] === 0xff,
|
|
1464
2145
|
nextObjectId: frame.data[4],
|
|
1465
|
-
objects,
|
|
2146
|
+
objects: parsed,
|
|
1466
2147
|
} });
|
|
1467
2148
|
}
|
|
1468
2149
|
});
|
|
1469
2150
|
}
|
|
2151
|
+
addCustomFunctionCode(cfc) {
|
|
2152
|
+
this.applicationLayer.addCustomFunctionCode(cfc);
|
|
2153
|
+
}
|
|
2154
|
+
removeCustomFunctionCode(fc) {
|
|
2155
|
+
this.applicationLayer.removeCustomFunctionCode(fc);
|
|
2156
|
+
}
|
|
2157
|
+
sendCustomFC(unit, fc, data, timeout = this.timeout) {
|
|
2158
|
+
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
2159
|
+
return this.send({ unit, fc, data: payload }, [(frame) => frame.unit === unit && frame.fc === fc], timeout, unit === 0).then((frame) => {
|
|
2160
|
+
if (frame) {
|
|
2161
|
+
return frame.data;
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
1470
2165
|
open(...args) {
|
|
2166
|
+
this._closed = false;
|
|
1471
2167
|
return this.physicalLayer.open(...args);
|
|
1472
2168
|
}
|
|
1473
2169
|
close() {
|
|
2170
|
+
this._closed = true;
|
|
2171
|
+
const queued = this._queue.splice(0);
|
|
2172
|
+
for (const item of queued) {
|
|
2173
|
+
item.cancel(new ModbusError(ModbusErrorCode.MASTER_CLOSED, 'Master closed'));
|
|
2174
|
+
}
|
|
2175
|
+
this._masterSession.stopAll(new ModbusError(ModbusErrorCode.MASTER_CLOSED, 'Master closed'));
|
|
1474
2176
|
return this.physicalLayer.close();
|
|
1475
2177
|
}
|
|
1476
2178
|
destroy() {
|
|
2179
|
+
this._closed = true;
|
|
2180
|
+
const queued = this._queue.splice(0);
|
|
2181
|
+
for (const item of queued) {
|
|
2182
|
+
item.cancel(new ModbusError(ModbusErrorCode.MASTER_DESTROYED, 'Master destroyed'));
|
|
2183
|
+
}
|
|
2184
|
+
this._masterSession.stopAll(new ModbusError(ModbusErrorCode.MASTER_DESTROYED, 'Master destroyed'));
|
|
1477
2185
|
this.removeAllListeners();
|
|
1478
2186
|
this.applicationLayer.destroy();
|
|
1479
2187
|
return this.physicalLayer.destroy();
|
|
1480
2188
|
}
|
|
1481
2189
|
}
|
|
1482
2190
|
|
|
1483
|
-
/******************************************************************************
|
|
1484
|
-
Copyright (c) Microsoft Corporation.
|
|
1485
|
-
|
|
1486
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
1487
|
-
purpose with or without fee is hereby granted.
|
|
1488
|
-
|
|
1489
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
1490
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
1491
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
1492
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
1493
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
1494
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
1495
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
1496
|
-
***************************************************************************** */
|
|
1497
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
1501
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
1502
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
1503
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
1504
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
1505
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
1506
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
1511
|
-
var e = new Error(message);
|
|
1512
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
1513
|
-
};
|
|
1514
|
-
|
|
1515
2191
|
class ModbusSlave extends EventEmitter {
|
|
1516
2192
|
get isOpen() {
|
|
1517
2193
|
return this.physicalLayer.isOpen;
|
|
@@ -1519,7 +2195,8 @@ class ModbusSlave extends EventEmitter {
|
|
|
1519
2195
|
get destroyed() {
|
|
1520
2196
|
return this.physicalLayer.destroyed;
|
|
1521
2197
|
}
|
|
1522
|
-
constructor(applicationLayer, physicalLayer) {
|
|
2198
|
+
constructor(applicationLayer, physicalLayer, options = {}) {
|
|
2199
|
+
var _a;
|
|
1523
2200
|
super();
|
|
1524
2201
|
Object.defineProperty(this, "applicationLayer", {
|
|
1525
2202
|
enumerable: true,
|
|
@@ -1539,444 +2216,493 @@ class ModbusSlave extends EventEmitter {
|
|
|
1539
2216
|
writable: true,
|
|
1540
2217
|
value: new Map()
|
|
1541
2218
|
});
|
|
1542
|
-
Object.defineProperty(this, "
|
|
2219
|
+
Object.defineProperty(this, "concurrent", {
|
|
1543
2220
|
enumerable: true,
|
|
1544
2221
|
configurable: true,
|
|
1545
2222
|
writable: true,
|
|
1546
|
-
value:
|
|
2223
|
+
value: void 0
|
|
1547
2224
|
});
|
|
1548
|
-
Object.defineProperty(this, "
|
|
2225
|
+
Object.defineProperty(this, "_queues", {
|
|
1549
2226
|
enumerable: true,
|
|
1550
2227
|
configurable: true,
|
|
1551
2228
|
writable: true,
|
|
1552
|
-
value:
|
|
2229
|
+
value: new Map()
|
|
2230
|
+
});
|
|
2231
|
+
Object.defineProperty(this, "_customFunctionCodes", {
|
|
2232
|
+
enumerable: true,
|
|
2233
|
+
configurable: true,
|
|
2234
|
+
writable: true,
|
|
2235
|
+
value: new Map()
|
|
2236
|
+
});
|
|
2237
|
+
Object.defineProperty(this, "_locks", {
|
|
2238
|
+
enumerable: true,
|
|
2239
|
+
configurable: true,
|
|
2240
|
+
writable: true,
|
|
2241
|
+
value: new Map()
|
|
1553
2242
|
});
|
|
1554
|
-
|
|
2243
|
+
this.concurrent = (_a = options.concurrent) !== null && _a !== void 0 ? _a : false;
|
|
2244
|
+
if (this.concurrent && this.applicationLayer.PROTOCOL !== 'TCP') {
|
|
2245
|
+
throw new ModbusError(ModbusErrorCode.CONCURRENT_NOT_TCP, 'concurrent mode requires a Modbus TCP application layer');
|
|
2246
|
+
}
|
|
2247
|
+
this.applicationLayer.role = 'SLAVE';
|
|
2248
|
+
applicationLayer.on('framing', (frame, response, connection) => {
|
|
1555
2249
|
if (!(frame.unit === 0x00 || this.models.has(frame.unit))) {
|
|
1556
2250
|
return;
|
|
1557
2251
|
}
|
|
1558
|
-
this.
|
|
1559
|
-
|
|
2252
|
+
if (this.concurrent) {
|
|
2253
|
+
this._processFrame(frame, response).catch((error) => this.emit('error', error));
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
let q = this._queues.get(connection.id);
|
|
2257
|
+
if (!q) {
|
|
2258
|
+
q = { items: [], processing: false };
|
|
2259
|
+
this._queues.set(connection.id, q);
|
|
2260
|
+
}
|
|
2261
|
+
q.items.push({ frame, response });
|
|
2262
|
+
this._drain(connection.id, q);
|
|
1560
2263
|
});
|
|
1561
2264
|
physicalLayer.on('error', (error) => {
|
|
1562
2265
|
this.emit('error', error);
|
|
1563
2266
|
});
|
|
2267
|
+
physicalLayer.on('connection-close', (connection) => {
|
|
2268
|
+
const q = this._queues.get(connection.id);
|
|
2269
|
+
if (!q) {
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
q.items.length = 0;
|
|
2273
|
+
if (!q.processing) {
|
|
2274
|
+
this._queues.delete(connection.id);
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
1564
2277
|
physicalLayer.on('close', () => {
|
|
2278
|
+
for (const q of this._queues.values()) {
|
|
2279
|
+
q.items.length = 0;
|
|
2280
|
+
}
|
|
2281
|
+
this._queues.clear();
|
|
1565
2282
|
this.emit('close');
|
|
1566
2283
|
});
|
|
1567
2284
|
}
|
|
1568
2285
|
handleFC1(model, frame, response) {
|
|
1569
2286
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1570
2287
|
var _a;
|
|
1571
|
-
if (frame.data.length
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
else {
|
|
1597
|
-
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2288
|
+
if (frame.data.length !== 4) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
if (!model.readCoils) {
|
|
2292
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2296
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2297
|
+
if (length < LIMITS.READ_COILS_MIN || length > LIMITS.READ_COILS_MAX) {
|
|
2298
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).coils)) {
|
|
2302
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
try {
|
|
2306
|
+
const coils = yield model.readCoils(address, length);
|
|
2307
|
+
const bufferTx = Buffer.alloc(Math.ceil(length / 8));
|
|
2308
|
+
coils.forEach((coil, index) => {
|
|
2309
|
+
if (coil) {
|
|
2310
|
+
bufferTx[Math.floor(index / 8)] |= 1 << index % 8;
|
|
1598
2311
|
}
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
2312
|
+
});
|
|
2313
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2314
|
+
}
|
|
2315
|
+
catch (error) {
|
|
2316
|
+
yield this.responseError(frame, response, error);
|
|
1603
2317
|
}
|
|
1604
2318
|
});
|
|
1605
2319
|
}
|
|
1606
2320
|
handleFC2(model, frame, response) {
|
|
1607
2321
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1608
2322
|
var _a;
|
|
1609
|
-
if (frame.data.length
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
else {
|
|
1635
|
-
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2323
|
+
if (frame.data.length !== 4) {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
if (!model.readDiscreteInputs) {
|
|
2327
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2331
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2332
|
+
if (length < LIMITS.READ_COILS_MIN || length > LIMITS.READ_COILS_MAX) {
|
|
2333
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).discreteInputs)) {
|
|
2337
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
try {
|
|
2341
|
+
const discreteInputs = yield model.readDiscreteInputs(address, length);
|
|
2342
|
+
const bufferTx = Buffer.alloc(Math.ceil(length / 8));
|
|
2343
|
+
discreteInputs.forEach((discreteInput, index) => {
|
|
2344
|
+
if (discreteInput) {
|
|
2345
|
+
bufferTx[Math.floor(index / 8)] |= 1 << index % 8;
|
|
1636
2346
|
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
2347
|
+
});
|
|
2348
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2349
|
+
}
|
|
2350
|
+
catch (error) {
|
|
2351
|
+
yield this.responseError(frame, response, error);
|
|
1641
2352
|
}
|
|
1642
2353
|
});
|
|
1643
2354
|
}
|
|
1644
2355
|
handleFC3(model, frame, response) {
|
|
1645
2356
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1646
2357
|
var _a;
|
|
1647
|
-
if (frame.data.length
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
else {
|
|
1675
|
-
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
1676
|
-
}
|
|
2358
|
+
if (frame.data.length !== 4) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
if (!model.readHoldingRegisters) {
|
|
2362
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2366
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2367
|
+
if (length < LIMITS.READ_REGISTERS_MIN || length > LIMITS.READ_REGISTERS_MAX) {
|
|
2368
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2372
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
try {
|
|
2376
|
+
const registers = yield model.readHoldingRegisters(address, length);
|
|
2377
|
+
const bufferTx = Buffer.alloc(length * 2);
|
|
2378
|
+
registers.forEach((register, index) => {
|
|
2379
|
+
bufferTx.writeUInt16BE(register, index * 2);
|
|
2380
|
+
});
|
|
2381
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2382
|
+
}
|
|
2383
|
+
catch (error) {
|
|
2384
|
+
yield this.responseError(frame, response, error);
|
|
1677
2385
|
}
|
|
1678
2386
|
});
|
|
1679
2387
|
}
|
|
1680
2388
|
handleFC4(model, frame, response) {
|
|
1681
2389
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1682
2390
|
var _a;
|
|
1683
|
-
if (frame.data.length
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
else {
|
|
1711
|
-
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
1712
|
-
}
|
|
2391
|
+
if (frame.data.length !== 4) {
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (!model.readInputRegisters) {
|
|
2395
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2399
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2400
|
+
if (length < LIMITS.READ_REGISTERS_MIN || length > LIMITS.READ_REGISTERS_MAX) {
|
|
2401
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).inputRegisters)) {
|
|
2405
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
try {
|
|
2409
|
+
const registers = yield model.readInputRegisters(address, length);
|
|
2410
|
+
const bufferTx = Buffer.alloc(length * 2);
|
|
2411
|
+
registers.forEach((register, index) => {
|
|
2412
|
+
bufferTx.writeUInt16BE(register, index * 2);
|
|
2413
|
+
});
|
|
2414
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2415
|
+
}
|
|
2416
|
+
catch (error) {
|
|
2417
|
+
yield this.responseError(frame, response, error);
|
|
1713
2418
|
}
|
|
1714
2419
|
});
|
|
1715
2420
|
}
|
|
1716
2421
|
handleFC5(model, frame, response) {
|
|
1717
2422
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1718
2423
|
var _a;
|
|
1719
|
-
if (frame.data.length
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
else {
|
|
1743
|
-
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
1744
|
-
}
|
|
2424
|
+
if (frame.data.length !== 4) {
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
if (!model.writeSingleCoil) {
|
|
2428
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2432
|
+
const value = (frame.data[2] << 8) | frame.data[3];
|
|
2433
|
+
if (value !== COIL_OFF && value !== COIL_ON) {
|
|
2434
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
if (!checkRange(address, (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).coils)) {
|
|
2438
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
try {
|
|
2442
|
+
yield model.writeSingleCoil(address, value === COIL_ON);
|
|
2443
|
+
yield response(this.applicationLayer.encode(frame));
|
|
2444
|
+
}
|
|
2445
|
+
catch (error) {
|
|
2446
|
+
yield this.responseError(frame, response, error);
|
|
1745
2447
|
}
|
|
1746
2448
|
});
|
|
1747
2449
|
}
|
|
1748
2450
|
handleFC6(model, frame, response) {
|
|
1749
2451
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1750
2452
|
var _a;
|
|
1751
|
-
if (frame.data.length
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
else {
|
|
1771
|
-
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
else {
|
|
1775
|
-
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
1776
|
-
}
|
|
2453
|
+
if (frame.data.length !== 4) {
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
if (!model.writeSingleRegister) {
|
|
2457
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2461
|
+
const value = (frame.data[2] << 8) | frame.data[3];
|
|
2462
|
+
if (!checkRange(address, (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2463
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
try {
|
|
2467
|
+
yield model.writeSingleRegister(address, value);
|
|
2468
|
+
yield response(this.applicationLayer.encode(frame));
|
|
2469
|
+
}
|
|
2470
|
+
catch (error) {
|
|
2471
|
+
yield this.responseError(frame, response, error);
|
|
1777
2472
|
}
|
|
1778
2473
|
});
|
|
1779
2474
|
}
|
|
1780
2475
|
handleFC15(model, frame, response) {
|
|
1781
2476
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1782
2477
|
var _a;
|
|
1783
|
-
if (frame.data.length
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
2478
|
+
if (frame.data.length <= 5 || frame.data.length !== 5 + frame.data[4]) {
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
if (!model.writeMultipleCoils && !model.writeSingleCoil) {
|
|
2482
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2486
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2487
|
+
const byteCount = frame.data[4];
|
|
2488
|
+
if (length < LIMITS.READ_COILS_MIN || length > LIMITS.WRITE_COILS_MAX || byteCount !== Math.ceil(length / 8)) {
|
|
2489
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).coils)) {
|
|
2493
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
const value = new Array(length);
|
|
2497
|
+
for (let i = 0; i < length; i++) {
|
|
2498
|
+
value[i] = (frame.data[5 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
|
|
2499
|
+
}
|
|
2500
|
+
try {
|
|
2501
|
+
if (model.writeMultipleCoils) {
|
|
2502
|
+
yield model.writeMultipleCoils(address, value);
|
|
1809
2503
|
}
|
|
1810
2504
|
else {
|
|
1811
|
-
|
|
2505
|
+
for (let i = 0; i < value.length; i++) {
|
|
2506
|
+
yield model.writeSingleCoil(address + i, value[i]);
|
|
2507
|
+
}
|
|
1812
2508
|
}
|
|
2509
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: frame.data.subarray(0, 4) })));
|
|
2510
|
+
}
|
|
2511
|
+
catch (error) {
|
|
2512
|
+
yield this.responseError(frame, response, error);
|
|
1813
2513
|
}
|
|
1814
2514
|
});
|
|
1815
2515
|
}
|
|
1816
2516
|
handleFC16(model, frame, response) {
|
|
1817
2517
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1818
2518
|
var _a;
|
|
1819
|
-
if (frame.data.length
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
}
|
|
2519
|
+
if (frame.data.length <= 5 || frame.data.length !== 5 + frame.data[4]) {
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
if (!model.writeMultipleRegisters && !model.writeSingleRegister) {
|
|
2523
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2527
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2528
|
+
const byteCount = frame.data[4];
|
|
2529
|
+
if (length < LIMITS.READ_REGISTERS_MIN || length > LIMITS.WRITE_REGISTERS_MAX || byteCount !== length * 2) {
|
|
2530
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2534
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
const value = new Array(length);
|
|
2538
|
+
for (let i = 0; i < length; i++) {
|
|
2539
|
+
value[i] = (frame.data[5 + i * 2] << 8) | frame.data[6 + i * 2];
|
|
2540
|
+
}
|
|
2541
|
+
try {
|
|
2542
|
+
if (model.writeMultipleRegisters) {
|
|
2543
|
+
yield model.writeMultipleRegisters(address, value);
|
|
1845
2544
|
}
|
|
1846
2545
|
else {
|
|
1847
|
-
|
|
2546
|
+
for (let i = 0; i < value.length; i++) {
|
|
2547
|
+
yield model.writeSingleRegister(address + i, value[i]);
|
|
2548
|
+
}
|
|
1848
2549
|
}
|
|
2550
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: frame.data.subarray(0, 4) })));
|
|
2551
|
+
}
|
|
2552
|
+
catch (error) {
|
|
2553
|
+
yield this.responseError(frame, response, error);
|
|
1849
2554
|
}
|
|
1850
2555
|
});
|
|
1851
2556
|
}
|
|
1852
2557
|
handleFC17(model, frame, response) {
|
|
1853
2558
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1854
2559
|
var _a;
|
|
1855
|
-
if (frame.data.length
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2560
|
+
if (frame.data.length !== 0) {
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
if (!model.reportServerId) {
|
|
2564
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
try {
|
|
2568
|
+
const { serverId = (_a = model.unit) !== null && _a !== void 0 ? _a : 1, runIndicatorStatus = true, additionalData = [] } = yield model.reportServerId();
|
|
2569
|
+
const serverIdBytes = Array.isArray(serverId) ? serverId : [serverId];
|
|
2570
|
+
const byteCount = serverIdBytes.length + 1 + additionalData.length;
|
|
2571
|
+
if (byteCount > 255) {
|
|
2572
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
2573
|
+
return;
|
|
1864
2574
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
2575
|
+
const allBytes = [...serverIdBytes, runIndicatorStatus ? 0xff : 0x00, ...additionalData];
|
|
2576
|
+
if (allBytes.some((b) => !isUint8(b))) {
|
|
2577
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
2578
|
+
return;
|
|
1867
2579
|
}
|
|
2580
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([byteCount]), Buffer.from(allBytes)]) })));
|
|
2581
|
+
}
|
|
2582
|
+
catch (error) {
|
|
2583
|
+
yield this.responseError(frame, response, error);
|
|
1868
2584
|
}
|
|
1869
2585
|
});
|
|
1870
2586
|
}
|
|
1871
2587
|
handleFC22(model, frame, response) {
|
|
1872
2588
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1873
2589
|
var _a;
|
|
1874
|
-
if (frame.data.length
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2590
|
+
if (frame.data.length !== 6) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
if (!model.maskWriteRegister && !(model.readHoldingRegisters && model.writeSingleRegister)) {
|
|
2594
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2598
|
+
const andMask = (frame.data[2] << 8) | frame.data[3];
|
|
2599
|
+
const orMask = (frame.data[4] << 8) | frame.data[5];
|
|
2600
|
+
if (!checkRange(address, (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2601
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
try {
|
|
2605
|
+
yield this.withAddressLock([address], () => __awaiter(this, void 0, void 0, function* () {
|
|
2606
|
+
if (model.maskWriteRegister) {
|
|
2607
|
+
yield model.maskWriteRegister(address, andMask, orMask);
|
|
1892
2608
|
}
|
|
1893
2609
|
else {
|
|
1894
|
-
yield
|
|
2610
|
+
const [value] = yield model.readHoldingRegisters(address, 1);
|
|
2611
|
+
yield model.writeSingleRegister(address, (value & andMask) | (orMask & (~andMask & 0xffff)));
|
|
1895
2612
|
}
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2613
|
+
}));
|
|
2614
|
+
yield response(this.applicationLayer.encode(frame));
|
|
2615
|
+
}
|
|
2616
|
+
catch (error) {
|
|
2617
|
+
yield this.responseError(frame, response, error);
|
|
1900
2618
|
}
|
|
1901
2619
|
});
|
|
1902
2620
|
}
|
|
1903
2621
|
handleFC23(model, frame, response) {
|
|
1904
2622
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1905
2623
|
var _a;
|
|
1906
|
-
if (frame.data.length
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2624
|
+
if (frame.data.length <= 9 || frame.data.length !== 9 + frame.data[8]) {
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
if (!model.readHoldingRegisters || (!model.writeMultipleRegisters && !model.writeSingleRegister)) {
|
|
2628
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
const address = {
|
|
2632
|
+
read: (frame.data[0] << 8) | frame.data[1],
|
|
2633
|
+
write: (frame.data[4] << 8) | frame.data[5],
|
|
2634
|
+
};
|
|
2635
|
+
const length = {
|
|
2636
|
+
read: (frame.data[2] << 8) | frame.data[3],
|
|
2637
|
+
write: (frame.data[6] << 8) | frame.data[7],
|
|
2638
|
+
};
|
|
2639
|
+
const byteCount = frame.data[8];
|
|
2640
|
+
if (length.read < LIMITS.READ_REGISTERS_MIN ||
|
|
2641
|
+
length.read > LIMITS.READ_REGISTERS_MAX ||
|
|
2642
|
+
length.write < LIMITS.READ_REGISTERS_MIN ||
|
|
2643
|
+
length.write > LIMITS.RW_REGISTERS_WRITE_MAX ||
|
|
2644
|
+
byteCount !== length.write * 2) {
|
|
2645
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
if (!checkRange([address.read, address.read + length.read, address.write, address.write + length.write], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2649
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
const value = new Array(length.write);
|
|
2653
|
+
for (let i = 0; i < length.write; i++) {
|
|
2654
|
+
value[i] = (frame.data[9 + i * 2] << 8) | frame.data[10 + i * 2];
|
|
2655
|
+
}
|
|
2656
|
+
try {
|
|
2657
|
+
const writeAddresses = Array.from({ length: length.write }, (_, i) => address.write + i);
|
|
2658
|
+
yield this.withAddressLock(writeAddresses, () => __awaiter(this, void 0, void 0, function* () {
|
|
2659
|
+
if (model.writeMultipleRegisters) {
|
|
2660
|
+
yield model.writeMultipleRegisters(address.write, value);
|
|
1943
2661
|
}
|
|
1944
2662
|
else {
|
|
1945
|
-
|
|
2663
|
+
for (let i = 0; i < value.length; i++) {
|
|
2664
|
+
yield model.writeSingleRegister(address.write + i, value[i]);
|
|
2665
|
+
}
|
|
1946
2666
|
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2667
|
+
}));
|
|
2668
|
+
const registers = yield model.readHoldingRegisters(address.read, length.read);
|
|
2669
|
+
const bufferTx = Buffer.alloc(length.read * 2);
|
|
2670
|
+
registers.forEach((register, index) => {
|
|
2671
|
+
bufferTx.writeUInt16BE(register, index * 2);
|
|
2672
|
+
});
|
|
2673
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2674
|
+
}
|
|
2675
|
+
catch (error) {
|
|
2676
|
+
yield this.responseError(frame, response, error);
|
|
1951
2677
|
}
|
|
1952
2678
|
});
|
|
1953
2679
|
}
|
|
1954
2680
|
handleFC43_14(model, frame, response) {
|
|
1955
2681
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1956
2682
|
if (frame.data.length === 3) {
|
|
1957
|
-
if (frame.data[0] ===
|
|
2683
|
+
if (frame.data[0] === MEI_READ_DEVICE_ID && model.readDeviceIdentification) {
|
|
1958
2684
|
const readDeviceIDCode = frame.data[1];
|
|
1959
2685
|
let objectID = frame.data[2];
|
|
1960
2686
|
switch (readDeviceIDCode) {
|
|
1961
|
-
case
|
|
2687
|
+
case ReadDeviceIDCode.BASIC_STREAM: {
|
|
1962
2688
|
if (objectID > 0x02 || (objectID > 0x06 && objectID < 0x80)) {
|
|
1963
2689
|
objectID = 0x00;
|
|
1964
2690
|
}
|
|
1965
2691
|
break;
|
|
1966
2692
|
}
|
|
1967
|
-
case
|
|
2693
|
+
case ReadDeviceIDCode.REGULAR_STREAM: {
|
|
1968
2694
|
if (objectID >= 0x80 || (objectID > 0x06 && objectID < 0x80)) {
|
|
1969
2695
|
objectID = 0x00;
|
|
1970
2696
|
}
|
|
1971
2697
|
break;
|
|
1972
2698
|
}
|
|
1973
|
-
case
|
|
2699
|
+
case ReadDeviceIDCode.EXTENDED_STREAM: {
|
|
1974
2700
|
if (objectID > 0x06 && objectID < 0x80) {
|
|
1975
2701
|
objectID = 0x00;
|
|
1976
2702
|
}
|
|
1977
2703
|
break;
|
|
1978
2704
|
}
|
|
1979
|
-
case
|
|
2705
|
+
case ReadDeviceIDCode.SPECIFIC_ACCESS: {
|
|
1980
2706
|
if (objectID > 0x06 && objectID < 0x80) {
|
|
1981
2707
|
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
1982
2708
|
return;
|
|
@@ -1989,70 +2715,74 @@ class ModbusSlave extends EventEmitter {
|
|
|
1989
2715
|
}
|
|
1990
2716
|
}
|
|
1991
2717
|
try {
|
|
1992
|
-
const identification = yield
|
|
1993
|
-
const objects = new Map(
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2718
|
+
const identification = yield model.readDeviceIdentification();
|
|
2719
|
+
const objects = new Map();
|
|
2720
|
+
for (const key of Object.keys(identification)) {
|
|
2721
|
+
const id = Number(key);
|
|
2722
|
+
if (id >= 0 && id <= 255) {
|
|
2723
|
+
objects.set(id, identification[id]);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
for (let id = 0x00; id <= 0x02; id++) {
|
|
2727
|
+
if (!objects.has(id)) {
|
|
2728
|
+
objects.set(id, 'null');
|
|
2002
2729
|
}
|
|
2003
2730
|
}
|
|
2004
2731
|
if (!objects.has(objectID)) {
|
|
2005
|
-
if (readDeviceIDCode ===
|
|
2732
|
+
if (readDeviceIDCode === ReadDeviceIDCode.SPECIFIC_ACCESS) {
|
|
2006
2733
|
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2007
2734
|
return;
|
|
2008
2735
|
}
|
|
2009
2736
|
objectID = 0x00;
|
|
2010
2737
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
let conformityLevel = 0x81;
|
|
2015
|
-
for (const [id, value] of objects.entries()) {
|
|
2016
|
-
if (id < 0x00 || (id >= 0x07 && id <= 0x7f) || id > 0xff) {
|
|
2738
|
+
let maxId = 0;
|
|
2739
|
+
for (const id of objects.keys()) {
|
|
2740
|
+
if ((id >= 0x07 && id <= 0x7f) || id > 0xff) {
|
|
2017
2741
|
yield this.responseError(frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
2018
2742
|
return;
|
|
2019
2743
|
}
|
|
2020
|
-
if (id >
|
|
2021
|
-
|
|
2022
|
-
}
|
|
2023
|
-
if (id > 0x80) {
|
|
2024
|
-
conformityLevel = 0x83;
|
|
2744
|
+
if (id > maxId) {
|
|
2745
|
+
maxId = id;
|
|
2025
2746
|
}
|
|
2747
|
+
}
|
|
2748
|
+
const conformityLevel = maxId >= 0x80 ? ConformityLevel.EXTENDED : maxId > 0x02 ? ConformityLevel.REGULAR : ConformityLevel.BASIC;
|
|
2749
|
+
const ids = [];
|
|
2750
|
+
let totalLength = 10;
|
|
2751
|
+
let lastID = 0;
|
|
2752
|
+
for (const [id, value] of objects.entries()) {
|
|
2026
2753
|
if (objectID > id) {
|
|
2027
2754
|
continue;
|
|
2028
2755
|
}
|
|
2029
|
-
|
|
2756
|
+
const byteLength = Buffer.byteLength(value);
|
|
2757
|
+
if (byteLength > 245) {
|
|
2030
2758
|
yield this.responseError(frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
2031
2759
|
return;
|
|
2032
2760
|
}
|
|
2033
2761
|
if (lastID !== 0) {
|
|
2034
2762
|
continue;
|
|
2035
2763
|
}
|
|
2036
|
-
if (
|
|
2764
|
+
if (byteLength + 2 > 253 - totalLength) {
|
|
2037
2765
|
if (lastID === 0) {
|
|
2038
2766
|
lastID = id;
|
|
2039
2767
|
}
|
|
2040
2768
|
}
|
|
2041
2769
|
else {
|
|
2042
|
-
totalLength +=
|
|
2770
|
+
totalLength += byteLength + 2;
|
|
2043
2771
|
ids.push(id);
|
|
2044
|
-
if (readDeviceIDCode ===
|
|
2772
|
+
if (readDeviceIDCode === ReadDeviceIDCode.SPECIFIC_ACCESS) {
|
|
2045
2773
|
break;
|
|
2046
2774
|
}
|
|
2047
2775
|
}
|
|
2048
2776
|
}
|
|
2049
2777
|
ids.sort((a, b) => a - b);
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2778
|
+
const parts = [];
|
|
2779
|
+
parts.push(Buffer.from([MEI_READ_DEVICE_ID, readDeviceIDCode, conformityLevel, lastID === 0 ? 0x00 : 0xff, lastID, ids.length]));
|
|
2780
|
+
for (const id of ids) {
|
|
2781
|
+
const value = objects.get(id);
|
|
2782
|
+
parts.push(Buffer.from([id, Buffer.byteLength(value)]));
|
|
2783
|
+
parts.push(Buffer.from(value));
|
|
2784
|
+
}
|
|
2785
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat(parts) })));
|
|
2056
2786
|
}
|
|
2057
2787
|
catch (error) {
|
|
2058
2788
|
yield this.responseError(frame, response, error);
|
|
@@ -2066,26 +2796,32 @@ class ModbusSlave extends EventEmitter {
|
|
|
2066
2796
|
}
|
|
2067
2797
|
responseError(frame, response, error) {
|
|
2068
2798
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2069
|
-
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { fc: frame.fc |
|
|
2799
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { fc: frame.fc | EXCEPTION_OFFSET, data: Buffer.from([getCodeByError(error)]) })));
|
|
2070
2800
|
});
|
|
2071
2801
|
}
|
|
2072
|
-
_drain() {
|
|
2802
|
+
_drain(key, q) {
|
|
2073
2803
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2074
|
-
if (
|
|
2804
|
+
if (q.processing) {
|
|
2075
2805
|
return;
|
|
2076
2806
|
}
|
|
2077
|
-
|
|
2078
|
-
const { frame, response: _response } = this._queue.shift();
|
|
2807
|
+
q.processing = true;
|
|
2079
2808
|
try {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2809
|
+
while (q.items.length > 0) {
|
|
2810
|
+
const { frame, response } = q.items.shift();
|
|
2811
|
+
try {
|
|
2812
|
+
yield this._processFrame(frame, response);
|
|
2813
|
+
}
|
|
2814
|
+
catch (error) {
|
|
2815
|
+
this.emit('error', error);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2084
2818
|
}
|
|
2085
2819
|
finally {
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2820
|
+
q.processing = false;
|
|
2821
|
+
// Cleanup empty queue entries so the map doesn't grow unbounded across
|
|
2822
|
+
// ephemeral connections (UDP rinfo, brief TCP clients).
|
|
2823
|
+
if (q.items.length === 0 && this._queues.get(key) === q) {
|
|
2824
|
+
this._queues.delete(key);
|
|
2089
2825
|
}
|
|
2090
2826
|
}
|
|
2091
2827
|
});
|
|
@@ -2102,8 +2838,8 @@ class ModbusSlave extends EventEmitter {
|
|
|
2102
2838
|
catch (error) { }
|
|
2103
2839
|
});
|
|
2104
2840
|
for (const model of frame.unit === 0x00 ? this.models.values() : [this.models.get(frame.unit)]) {
|
|
2105
|
-
const
|
|
2106
|
-
if (
|
|
2841
|
+
const intercepted = yield this._intercept(model, frame, response);
|
|
2842
|
+
if (!intercepted) {
|
|
2107
2843
|
yield this._handleFC(model, frame, response);
|
|
2108
2844
|
}
|
|
2109
2845
|
}
|
|
@@ -2111,17 +2847,42 @@ class ModbusSlave extends EventEmitter {
|
|
|
2111
2847
|
}
|
|
2112
2848
|
_intercept(model, frame, response) {
|
|
2113
2849
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2114
|
-
if (model.interceptor) {
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
}
|
|
2850
|
+
if (!model.interceptor) {
|
|
2851
|
+
return false;
|
|
2852
|
+
}
|
|
2853
|
+
try {
|
|
2854
|
+
const data = yield model.interceptor(frame.fc, frame.data);
|
|
2855
|
+
if (data !== undefined) {
|
|
2856
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data })));
|
|
2857
|
+
return true;
|
|
2121
2858
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2859
|
+
return false;
|
|
2860
|
+
}
|
|
2861
|
+
catch (error) {
|
|
2862
|
+
yield this.responseError(frame, response, error);
|
|
2863
|
+
return true;
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
withAddressLock(addresses, fn) {
|
|
2868
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2869
|
+
const sorted = [...new Set(addresses)].sort((a, b) => a - b);
|
|
2870
|
+
const previous = sorted.map((addr) => { var _a; return (_a = this._locks.get(addr)) !== null && _a !== void 0 ? _a : Promise.resolve(); });
|
|
2871
|
+
const work = Promise.all(previous).then(() => fn());
|
|
2872
|
+
const cleanup = work.catch(() => {
|
|
2873
|
+
/* ignore */
|
|
2874
|
+
});
|
|
2875
|
+
for (const addr of sorted) {
|
|
2876
|
+
this._locks.set(addr, cleanup);
|
|
2877
|
+
}
|
|
2878
|
+
try {
|
|
2879
|
+
return yield work;
|
|
2880
|
+
}
|
|
2881
|
+
finally {
|
|
2882
|
+
for (const addr of sorted) {
|
|
2883
|
+
if (this._locks.get(addr) === cleanup) {
|
|
2884
|
+
this._locks.delete(addr);
|
|
2885
|
+
}
|
|
2125
2886
|
}
|
|
2126
2887
|
}
|
|
2127
2888
|
});
|
|
@@ -2129,56 +2890,68 @@ class ModbusSlave extends EventEmitter {
|
|
|
2129
2890
|
_handleFC(model, frame, response) {
|
|
2130
2891
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2131
2892
|
switch (frame.fc) {
|
|
2132
|
-
case
|
|
2893
|
+
case FunctionCode.READ_COILS: {
|
|
2133
2894
|
yield this.handleFC1(model, frame, response);
|
|
2134
2895
|
break;
|
|
2135
2896
|
}
|
|
2136
|
-
case
|
|
2897
|
+
case FunctionCode.READ_DISCRETE_INPUTS: {
|
|
2137
2898
|
yield this.handleFC2(model, frame, response);
|
|
2138
2899
|
break;
|
|
2139
2900
|
}
|
|
2140
|
-
case
|
|
2901
|
+
case FunctionCode.READ_HOLDING_REGISTERS: {
|
|
2141
2902
|
yield this.handleFC3(model, frame, response);
|
|
2142
2903
|
break;
|
|
2143
2904
|
}
|
|
2144
|
-
case
|
|
2905
|
+
case FunctionCode.READ_INPUT_REGISTERS: {
|
|
2145
2906
|
yield this.handleFC4(model, frame, response);
|
|
2146
2907
|
break;
|
|
2147
2908
|
}
|
|
2148
|
-
case
|
|
2909
|
+
case FunctionCode.WRITE_SINGLE_COIL: {
|
|
2149
2910
|
yield this.handleFC5(model, frame, response);
|
|
2150
2911
|
break;
|
|
2151
2912
|
}
|
|
2152
|
-
case
|
|
2913
|
+
case FunctionCode.WRITE_SINGLE_REGISTER: {
|
|
2153
2914
|
yield this.handleFC6(model, frame, response);
|
|
2154
2915
|
break;
|
|
2155
2916
|
}
|
|
2156
|
-
case
|
|
2917
|
+
case FunctionCode.WRITE_MULTIPLE_COILS: {
|
|
2157
2918
|
yield this.handleFC15(model, frame, response);
|
|
2158
2919
|
break;
|
|
2159
2920
|
}
|
|
2160
|
-
case
|
|
2921
|
+
case FunctionCode.WRITE_MULTIPLE_REGISTERS: {
|
|
2161
2922
|
yield this.handleFC16(model, frame, response);
|
|
2162
2923
|
break;
|
|
2163
2924
|
}
|
|
2164
|
-
case
|
|
2925
|
+
case FunctionCode.REPORT_SERVER_ID: {
|
|
2165
2926
|
yield this.handleFC17(model, frame, response);
|
|
2166
2927
|
break;
|
|
2167
2928
|
}
|
|
2168
|
-
case
|
|
2929
|
+
case FunctionCode.MASK_WRITE_REGISTER: {
|
|
2169
2930
|
yield this.handleFC22(model, frame, response);
|
|
2170
2931
|
break;
|
|
2171
2932
|
}
|
|
2172
|
-
case
|
|
2933
|
+
case FunctionCode.READ_WRITE_MULTIPLE_REGISTERS: {
|
|
2173
2934
|
yield this.handleFC23(model, frame, response);
|
|
2174
2935
|
break;
|
|
2175
2936
|
}
|
|
2176
|
-
case
|
|
2937
|
+
case FunctionCode.READ_DEVICE_IDENTIFICATION: {
|
|
2177
2938
|
yield this.handleFC43_14(model, frame, response);
|
|
2178
2939
|
break;
|
|
2179
2940
|
}
|
|
2180
2941
|
default: {
|
|
2181
|
-
|
|
2942
|
+
const cfc = this._customFunctionCodes.get(frame.fc);
|
|
2943
|
+
if (cfc === null || cfc === void 0 ? void 0 : cfc.handle) {
|
|
2944
|
+
try {
|
|
2945
|
+
const responseData = yield cfc.handle(frame.data, frame.unit);
|
|
2946
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: responseData })));
|
|
2947
|
+
}
|
|
2948
|
+
catch (error) {
|
|
2949
|
+
yield this.responseError(frame, response, error);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
else {
|
|
2953
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2954
|
+
}
|
|
2182
2955
|
break;
|
|
2183
2956
|
}
|
|
2184
2957
|
}
|
|
@@ -2191,6 +2964,14 @@ class ModbusSlave extends EventEmitter {
|
|
|
2191
2964
|
remove(unit) {
|
|
2192
2965
|
this.models.delete(unit);
|
|
2193
2966
|
}
|
|
2967
|
+
addCustomFunctionCode(cfc) {
|
|
2968
|
+
this._customFunctionCodes.set(cfc.fc, cfc);
|
|
2969
|
+
this.applicationLayer.addCustomFunctionCode(cfc);
|
|
2970
|
+
}
|
|
2971
|
+
removeCustomFunctionCode(fc) {
|
|
2972
|
+
this._customFunctionCodes.delete(fc);
|
|
2973
|
+
this.applicationLayer.removeCustomFunctionCode(fc);
|
|
2974
|
+
}
|
|
2194
2975
|
open(...args) {
|
|
2195
2976
|
return this.physicalLayer.open(...args);
|
|
2196
2977
|
}
|
|
@@ -2198,10 +2979,11 @@ class ModbusSlave extends EventEmitter {
|
|
|
2198
2979
|
return this.physicalLayer.close();
|
|
2199
2980
|
}
|
|
2200
2981
|
destroy() {
|
|
2982
|
+
this._locks.clear();
|
|
2201
2983
|
this.removeAllListeners();
|
|
2202
2984
|
this.applicationLayer.destroy();
|
|
2203
2985
|
return this.physicalLayer.destroy();
|
|
2204
2986
|
}
|
|
2205
2987
|
}
|
|
2206
2988
|
|
|
2207
|
-
export { AbstractApplicationLayer, AbstractPhysicalLayer, AsciiApplicationLayer, ErrorCode, ModbusMaster, ModbusSlave, RtuApplicationLayer, SerialPhysicalLayer, TcpApplicationLayer, TcpClientPhysicalLayer, TcpServerPhysicalLayer, UdpPhysicalLayer, getCodeByError, getErrorByCode };
|
|
2989
|
+
export { AbstractApplicationLayer, AbstractPhysicalLayer, AsciiApplicationLayer, COIL_OFF, COIL_ON, ConformityLevel, EXCEPTION_OFFSET, ErrorCode, FunctionCode, LIMITS, MEI_READ_DEVICE_ID, MasterSession, ModbusError, ModbusErrorCode, ModbusMaster, ModbusSlave, ReadDeviceIDCode, RtuApplicationLayer, SerialPhysicalLayer, TcpApplicationLayer, TcpClientPhysicalLayer, TcpServerPhysicalLayer, UdpPhysicalLayer, getCodeByError, getErrorByCode };
|