njs-modbus 1.5.0 → 2.0.1
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 +2033 -1136
- package/dist/index.d.ts +315 -89
- package/dist/index.mjs +2026 -1137
- 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 +15 -10
- package/dist/src/layers/application/index.d.ts +2 -0
- package/dist/src/layers/application/rtu-application-layer.d.ts +53 -18
- package/dist/src/layers/application/tcp-application-layer.d.ts +5 -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 +29 -5
- package/dist/src/slave/index.d.ts +1 -1
- package/dist/src/slave/slave.d.ts +20 -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,27 @@ 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() {
|
|
424
|
+
if (this._destroyed) {
|
|
425
|
+
return Promise.resolve();
|
|
426
|
+
}
|
|
123
427
|
this._destroyed = true;
|
|
124
|
-
this.
|
|
125
|
-
|
|
428
|
+
return this.close().then(() => {
|
|
429
|
+
this.removeAllListeners();
|
|
430
|
+
});
|
|
126
431
|
}
|
|
127
432
|
}
|
|
128
433
|
|
|
@@ -145,7 +450,13 @@ class TcpClientPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
145
450
|
enumerable: true,
|
|
146
451
|
configurable: true,
|
|
147
452
|
writable: true,
|
|
148
|
-
value:
|
|
453
|
+
value: null
|
|
454
|
+
});
|
|
455
|
+
Object.defineProperty(this, "_connection", {
|
|
456
|
+
enumerable: true,
|
|
457
|
+
configurable: true,
|
|
458
|
+
writable: true,
|
|
459
|
+
value: null
|
|
149
460
|
});
|
|
150
461
|
Object.defineProperty(this, "_isOpen", {
|
|
151
462
|
enumerable: true,
|
|
@@ -153,46 +464,79 @@ class TcpClientPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
153
464
|
writable: true,
|
|
154
465
|
value: false
|
|
155
466
|
});
|
|
467
|
+
Object.defineProperty(this, "_isOpening", {
|
|
468
|
+
enumerable: true,
|
|
469
|
+
configurable: true,
|
|
470
|
+
writable: true,
|
|
471
|
+
value: false
|
|
472
|
+
});
|
|
156
473
|
Object.defineProperty(this, "_destroyed", {
|
|
157
474
|
enumerable: true,
|
|
158
475
|
configurable: true,
|
|
159
476
|
writable: true,
|
|
160
477
|
value: false
|
|
161
478
|
});
|
|
162
|
-
this
|
|
479
|
+
Object.defineProperty(this, "_socketOptions", {
|
|
480
|
+
enumerable: true,
|
|
481
|
+
configurable: true,
|
|
482
|
+
writable: true,
|
|
483
|
+
value: void 0
|
|
484
|
+
});
|
|
485
|
+
this._socketOptions = options;
|
|
163
486
|
}
|
|
164
487
|
open(options) {
|
|
165
|
-
if (this.
|
|
166
|
-
return Promise.reject(new
|
|
488
|
+
if (this._destroyed) {
|
|
489
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_DESTROYED, 'Port is destroyed'));
|
|
167
490
|
}
|
|
491
|
+
if (this._isOpen || this._isOpening) {
|
|
492
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_ALREADY_OPEN, 'Port is already open'));
|
|
493
|
+
}
|
|
494
|
+
this._isOpening = true;
|
|
495
|
+
const socket = new Socket(this._socketOptions);
|
|
496
|
+
const connection = { id: genConnectionId('tcp-client') };
|
|
497
|
+
this._socket = socket;
|
|
498
|
+
this._connection = connection;
|
|
168
499
|
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();
|
|
500
|
+
let connected = false;
|
|
501
|
+
socket.on('data', (data) => {
|
|
502
|
+
this.emit('data', data, (d) => this.write(d), connection);
|
|
182
503
|
});
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
this.emit('error',
|
|
504
|
+
socket.on('error', (err) => {
|
|
505
|
+
if (connected) {
|
|
506
|
+
this.emit('error', err);
|
|
186
507
|
}
|
|
187
508
|
else {
|
|
188
|
-
|
|
509
|
+
if (this._connection === connection) {
|
|
510
|
+
this._isOpening = false;
|
|
511
|
+
this._socket = null;
|
|
512
|
+
this._connection = null;
|
|
513
|
+
}
|
|
514
|
+
socket.removeAllListeners();
|
|
515
|
+
reject(err);
|
|
189
516
|
}
|
|
190
517
|
});
|
|
518
|
+
socket.once('close', () => {
|
|
519
|
+
if (this._connection === connection) {
|
|
520
|
+
this._isOpen = false;
|
|
521
|
+
this._isOpening = false;
|
|
522
|
+
this._socket = null;
|
|
523
|
+
this._connection = null;
|
|
524
|
+
socket.removeAllListeners();
|
|
525
|
+
this.emit('connection-close', connection);
|
|
526
|
+
this.emit('close');
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
socket.connect(options !== null && options !== void 0 ? options : { port: 502 }, () => {
|
|
530
|
+
connected = true;
|
|
531
|
+
this._isOpening = false;
|
|
532
|
+
this._isOpen = true;
|
|
533
|
+
resolve();
|
|
534
|
+
});
|
|
191
535
|
});
|
|
192
536
|
}
|
|
193
537
|
write(data) {
|
|
194
538
|
return new Promise((resolve, reject) => {
|
|
195
|
-
if (this.
|
|
539
|
+
if (this._isOpen && this._socket) {
|
|
196
540
|
this._socket.write(data, (error) => {
|
|
197
541
|
if (error) {
|
|
198
542
|
reject(error);
|
|
@@ -204,22 +548,28 @@ class TcpClientPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
204
548
|
});
|
|
205
549
|
}
|
|
206
550
|
else {
|
|
207
|
-
reject(new
|
|
551
|
+
reject(new ModbusError(ModbusErrorCode.PORT_NOT_OPEN, 'Port is not open'));
|
|
208
552
|
}
|
|
209
553
|
});
|
|
210
554
|
}
|
|
211
555
|
close() {
|
|
556
|
+
if (!this._socket) {
|
|
557
|
+
return Promise.resolve();
|
|
558
|
+
}
|
|
559
|
+
const socket = this._socket;
|
|
212
560
|
return new Promise((resolve) => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this._socket.destroy();
|
|
216
|
-
resolve();
|
|
561
|
+
socket.once('close', () => resolve());
|
|
562
|
+
socket.destroy();
|
|
217
563
|
});
|
|
218
564
|
}
|
|
219
565
|
destroy() {
|
|
566
|
+
if (this._destroyed) {
|
|
567
|
+
return Promise.resolve();
|
|
568
|
+
}
|
|
220
569
|
this._destroyed = true;
|
|
221
|
-
this.
|
|
222
|
-
|
|
570
|
+
return this.close().then(() => {
|
|
571
|
+
this.removeAllListeners();
|
|
572
|
+
});
|
|
223
573
|
}
|
|
224
574
|
}
|
|
225
575
|
|
|
@@ -242,7 +592,7 @@ class TcpServerPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
242
592
|
enumerable: true,
|
|
243
593
|
configurable: true,
|
|
244
594
|
writable: true,
|
|
245
|
-
value:
|
|
595
|
+
value: null
|
|
246
596
|
});
|
|
247
597
|
Object.defineProperty(this, "_isOpen", {
|
|
248
598
|
enumerable: true,
|
|
@@ -250,92 +600,137 @@ class TcpServerPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
250
600
|
writable: true,
|
|
251
601
|
value: false
|
|
252
602
|
});
|
|
603
|
+
Object.defineProperty(this, "_isOpening", {
|
|
604
|
+
enumerable: true,
|
|
605
|
+
configurable: true,
|
|
606
|
+
writable: true,
|
|
607
|
+
value: false
|
|
608
|
+
});
|
|
253
609
|
Object.defineProperty(this, "_destroyed", {
|
|
254
610
|
enumerable: true,
|
|
255
611
|
configurable: true,
|
|
256
612
|
writable: true,
|
|
257
613
|
value: false
|
|
258
614
|
});
|
|
259
|
-
Object.defineProperty(this, "
|
|
615
|
+
Object.defineProperty(this, "_connections", {
|
|
616
|
+
enumerable: true,
|
|
617
|
+
configurable: true,
|
|
618
|
+
writable: true,
|
|
619
|
+
value: new Map()
|
|
620
|
+
});
|
|
621
|
+
Object.defineProperty(this, "_serverOptions", {
|
|
260
622
|
enumerable: true,
|
|
261
623
|
configurable: true,
|
|
262
624
|
writable: true,
|
|
263
|
-
value:
|
|
625
|
+
value: void 0
|
|
264
626
|
});
|
|
265
|
-
this.
|
|
266
|
-
|
|
627
|
+
this._serverOptions = options;
|
|
628
|
+
}
|
|
629
|
+
open(options) {
|
|
630
|
+
if (this._destroyed) {
|
|
631
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_DESTROYED, 'Port is destroyed'));
|
|
632
|
+
}
|
|
633
|
+
if (this._isOpen || this._isOpening) {
|
|
634
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_ALREADY_OPEN, 'Port is already open'));
|
|
635
|
+
}
|
|
636
|
+
this._isOpening = true;
|
|
637
|
+
this._connections.clear();
|
|
638
|
+
const server = createServer(this._serverOptions, (socket) => {
|
|
639
|
+
const conn = { id: genConnectionId('tcp-server') };
|
|
640
|
+
this._connections.set(socket, conn);
|
|
641
|
+
const response = (data) => new Promise((resolve, reject) => {
|
|
642
|
+
socket.write(data, (error) => {
|
|
643
|
+
if (error) {
|
|
644
|
+
reject(error);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
resolve();
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
});
|
|
267
651
|
socket.on('data', (data) => {
|
|
268
|
-
this.emit('data', data,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
resolve();
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
}));
|
|
652
|
+
this.emit('data', data, response, conn);
|
|
653
|
+
});
|
|
654
|
+
socket.on('error', (err) => {
|
|
655
|
+
this.emit('error', err);
|
|
278
656
|
});
|
|
279
657
|
socket.once('close', () => {
|
|
658
|
+
if (this._connections.delete(socket)) {
|
|
659
|
+
this.emit('connection-close', conn);
|
|
660
|
+
}
|
|
280
661
|
socket.removeAllListeners();
|
|
281
|
-
this._sockets.delete(socket);
|
|
282
662
|
});
|
|
283
663
|
});
|
|
284
|
-
|
|
285
|
-
open(options) {
|
|
286
|
-
if (this.destroyed) {
|
|
287
|
-
return Promise.reject(new Error('Port is destroyed'));
|
|
288
|
-
}
|
|
664
|
+
this._server = server;
|
|
289
665
|
return new Promise((resolve, reject) => {
|
|
290
666
|
var _a;
|
|
291
|
-
let
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
667
|
+
let listening = false;
|
|
668
|
+
server.once('error', (err) => {
|
|
669
|
+
if (listening) {
|
|
670
|
+
this.emit('error', err);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
if (this._server === server) {
|
|
674
|
+
this._isOpening = false;
|
|
675
|
+
this._server = null;
|
|
676
|
+
}
|
|
677
|
+
server.removeAllListeners();
|
|
678
|
+
reject(err);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
server.once('close', () => {
|
|
682
|
+
if (this._server === server) {
|
|
297
683
|
this._isOpen = false;
|
|
298
|
-
this.
|
|
299
|
-
|
|
300
|
-
|
|
684
|
+
this._isOpening = false;
|
|
685
|
+
this._server = null;
|
|
686
|
+
const conns = Array.from(this._connections.values());
|
|
687
|
+
this._connections.clear();
|
|
688
|
+
for (const conn of conns) {
|
|
689
|
+
this.emit('connection-close', conn);
|
|
301
690
|
}
|
|
302
691
|
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
692
|
}
|
|
313
693
|
});
|
|
694
|
+
server.listen(Object.assign(Object.assign({}, options), { port: (_a = options === null || options === void 0 ? void 0 : options.port) !== null && _a !== void 0 ? _a : 502 }), () => {
|
|
695
|
+
listening = true;
|
|
696
|
+
this._isOpening = false;
|
|
697
|
+
this._isOpen = true;
|
|
698
|
+
resolve();
|
|
699
|
+
});
|
|
314
700
|
});
|
|
315
701
|
}
|
|
316
702
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
317
703
|
write(data) {
|
|
318
704
|
return new Promise((resolve, reject) => {
|
|
319
|
-
reject(new
|
|
705
|
+
reject(new ModbusError(ModbusErrorCode.NOT_SUPPORTED, 'Not supported'));
|
|
320
706
|
});
|
|
321
707
|
}
|
|
322
708
|
close() {
|
|
709
|
+
if (!this._server) {
|
|
710
|
+
return Promise.resolve();
|
|
711
|
+
}
|
|
712
|
+
const server = this._server;
|
|
323
713
|
return new Promise((resolve) => {
|
|
324
|
-
|
|
325
|
-
this.
|
|
326
|
-
for (const socket of this._sockets) {
|
|
327
|
-
socket.removeAllListeners();
|
|
714
|
+
server.once('close', () => resolve());
|
|
715
|
+
for (const [socket] of this._connections) {
|
|
328
716
|
socket.destroy();
|
|
329
717
|
}
|
|
330
|
-
|
|
718
|
+
try {
|
|
719
|
+
server.close();
|
|
720
|
+
}
|
|
721
|
+
catch (_a) {
|
|
331
722
|
resolve();
|
|
332
|
-
}
|
|
723
|
+
}
|
|
333
724
|
});
|
|
334
725
|
}
|
|
335
726
|
destroy() {
|
|
727
|
+
if (this._destroyed) {
|
|
728
|
+
return Promise.resolve();
|
|
729
|
+
}
|
|
336
730
|
this._destroyed = true;
|
|
337
|
-
this.
|
|
338
|
-
|
|
731
|
+
return this.close().then(() => {
|
|
732
|
+
this.removeAllListeners();
|
|
733
|
+
});
|
|
339
734
|
}
|
|
340
735
|
}
|
|
341
736
|
|
|
@@ -346,14 +741,8 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
346
741
|
get destroyed() {
|
|
347
742
|
return this._destroyed;
|
|
348
743
|
}
|
|
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;
|
|
744
|
+
constructor(options) {
|
|
745
|
+
var _a, _b, _c, _d;
|
|
357
746
|
super();
|
|
358
747
|
Object.defineProperty(this, "TYPE", {
|
|
359
748
|
enumerable: true,
|
|
@@ -365,7 +754,13 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
365
754
|
enumerable: true,
|
|
366
755
|
configurable: true,
|
|
367
756
|
writable: true,
|
|
368
|
-
value:
|
|
757
|
+
value: null
|
|
758
|
+
});
|
|
759
|
+
Object.defineProperty(this, "_connections", {
|
|
760
|
+
enumerable: true,
|
|
761
|
+
configurable: true,
|
|
762
|
+
writable: true,
|
|
763
|
+
value: new Map()
|
|
369
764
|
});
|
|
370
765
|
Object.defineProperty(this, "_isOpen", {
|
|
371
766
|
enumerable: true,
|
|
@@ -373,12 +768,24 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
373
768
|
writable: true,
|
|
374
769
|
value: false
|
|
375
770
|
});
|
|
771
|
+
Object.defineProperty(this, "_isOpening", {
|
|
772
|
+
enumerable: true,
|
|
773
|
+
configurable: true,
|
|
774
|
+
writable: true,
|
|
775
|
+
value: false
|
|
776
|
+
});
|
|
376
777
|
Object.defineProperty(this, "_destroyed", {
|
|
377
778
|
enumerable: true,
|
|
378
779
|
configurable: true,
|
|
379
780
|
writable: true,
|
|
380
781
|
value: false
|
|
381
782
|
});
|
|
783
|
+
Object.defineProperty(this, "_socketOptions", {
|
|
784
|
+
enumerable: true,
|
|
785
|
+
configurable: true,
|
|
786
|
+
writable: true,
|
|
787
|
+
value: void 0
|
|
788
|
+
});
|
|
382
789
|
Object.defineProperty(this, "_port", {
|
|
383
790
|
enumerable: true,
|
|
384
791
|
configurable: true,
|
|
@@ -391,67 +798,137 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
391
798
|
writable: true,
|
|
392
799
|
value: void 0
|
|
393
800
|
});
|
|
394
|
-
Object.defineProperty(this, "
|
|
801
|
+
Object.defineProperty(this, "_idleTimeout", {
|
|
395
802
|
enumerable: true,
|
|
396
803
|
configurable: true,
|
|
397
804
|
writable: true,
|
|
398
805
|
value: void 0
|
|
399
806
|
});
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
resolve();
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
}));
|
|
807
|
+
Object.defineProperty(this, "isServer", {
|
|
808
|
+
enumerable: true,
|
|
809
|
+
configurable: true,
|
|
810
|
+
writable: true,
|
|
811
|
+
value: void 0
|
|
411
812
|
});
|
|
412
|
-
this.
|
|
413
|
-
this.
|
|
414
|
-
this.
|
|
813
|
+
this._socketOptions = options === null || options === void 0 ? void 0 : options.socket;
|
|
814
|
+
this.isServer = !(options === null || options === void 0 ? void 0 : options.remote);
|
|
815
|
+
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;
|
|
816
|
+
this._address = (_c = options === null || options === void 0 ? void 0 : options.remote) === null || _c === void 0 ? void 0 : _c.address;
|
|
817
|
+
this._idleTimeout = (_d = options === null || options === void 0 ? void 0 : options.idleTimeout) !== null && _d !== void 0 ? _d : 30000;
|
|
415
818
|
}
|
|
416
819
|
open(options) {
|
|
417
|
-
|
|
418
|
-
|
|
820
|
+
var _a, _b;
|
|
821
|
+
if (this._destroyed) {
|
|
822
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_DESTROYED, 'Port is destroyed'));
|
|
419
823
|
}
|
|
824
|
+
if (this._isOpen || this._isOpening) {
|
|
825
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_ALREADY_OPEN, 'Port is already open'));
|
|
826
|
+
}
|
|
827
|
+
this._isOpening = true;
|
|
828
|
+
this._connections.clear();
|
|
829
|
+
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));
|
|
830
|
+
this._socket = socket;
|
|
420
831
|
return new Promise((resolve, reject) => {
|
|
421
832
|
var _a;
|
|
833
|
+
let started = false;
|
|
834
|
+
socket.on('error', (err) => {
|
|
835
|
+
if (started) {
|
|
836
|
+
this.emit('error', err);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
if (this._socket === socket) {
|
|
840
|
+
this._isOpening = false;
|
|
841
|
+
this._socket = null;
|
|
842
|
+
}
|
|
843
|
+
socket.removeAllListeners();
|
|
844
|
+
reject(err);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
socket.once('close', () => {
|
|
848
|
+
if (this._socket !== socket) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
this._isOpen = false;
|
|
852
|
+
this._isOpening = false;
|
|
853
|
+
this._socket = null;
|
|
854
|
+
socket.removeAllListeners();
|
|
855
|
+
const entries = Array.from(this._connections.values());
|
|
856
|
+
this._connections.clear();
|
|
857
|
+
for (const entry of entries) {
|
|
858
|
+
if (entry.idleTimer) {
|
|
859
|
+
clearTimeout(entry.idleTimer);
|
|
860
|
+
}
|
|
861
|
+
this.emit('connection-close', entry.conn);
|
|
862
|
+
}
|
|
863
|
+
this.emit('close');
|
|
864
|
+
});
|
|
422
865
|
if (this.isServer) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
866
|
+
socket.bind(Object.assign(Object.assign({}, options), { port: (_a = options === null || options === void 0 ? void 0 : options.port) !== null && _a !== void 0 ? _a : 502 }), () => {
|
|
867
|
+
started = true;
|
|
868
|
+
this._isOpening = false;
|
|
426
869
|
this._isOpen = true;
|
|
427
|
-
this._socket.on('close', () => {
|
|
428
|
-
this._isOpen = false;
|
|
429
|
-
this._socket.removeAllListeners();
|
|
430
|
-
this.emit('close');
|
|
431
|
-
});
|
|
432
870
|
resolve();
|
|
433
871
|
});
|
|
434
|
-
this._socket.on('error', (error) => {
|
|
435
|
-
if (called) {
|
|
436
|
-
this.emit('error', error);
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
reject(error);
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
872
|
}
|
|
443
873
|
else {
|
|
874
|
+
started = true;
|
|
875
|
+
this._isOpening = false;
|
|
444
876
|
this._isOpen = true;
|
|
445
877
|
resolve();
|
|
446
878
|
}
|
|
447
879
|
});
|
|
448
880
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
881
|
+
_handleMessage(socket, msg, rinfo) {
|
|
882
|
+
if (this._socket !== socket) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (!this.isServer) {
|
|
886
|
+
if (rinfo.port !== this._port) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (this._address !== undefined && rinfo.address !== this._address) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const key = `${rinfo.address}:${rinfo.port}`;
|
|
894
|
+
let entry = this._connections.get(key);
|
|
895
|
+
if (!entry) {
|
|
896
|
+
entry = { conn: { id: genConnectionId('udp') } };
|
|
897
|
+
this._connections.set(key, entry);
|
|
898
|
+
}
|
|
899
|
+
if (this.isServer && this._idleTimeout > 0) {
|
|
900
|
+
if (entry.idleTimer) {
|
|
901
|
+
clearTimeout(entry.idleTimer);
|
|
902
|
+
}
|
|
903
|
+
const conn = entry.conn;
|
|
904
|
+
entry.idleTimer = setTimeout(() => {
|
|
905
|
+
const e = this._connections.get(key);
|
|
906
|
+
if (!e || e.conn !== conn) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
this._connections.delete(key);
|
|
910
|
+
this.emit('connection-close', conn);
|
|
911
|
+
}, this._idleTimeout);
|
|
912
|
+
}
|
|
913
|
+
const conn = entry.conn;
|
|
914
|
+
const response = (data) => new Promise((resolve, reject) => {
|
|
915
|
+
socket.send(data, rinfo.port, rinfo.address, (error) => {
|
|
916
|
+
if (error) {
|
|
917
|
+
reject(error);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
resolve();
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
this.emit('data', msg, response, conn);
|
|
925
|
+
}
|
|
926
|
+
write(data) {
|
|
927
|
+
return new Promise((resolve, reject) => {
|
|
928
|
+
if (this._isOpen && this._socket) {
|
|
929
|
+
this._socket.send(data, this._port, this._address, (error) => {
|
|
930
|
+
if (error) {
|
|
931
|
+
reject(error);
|
|
455
932
|
}
|
|
456
933
|
else {
|
|
457
934
|
this.emit('write', data);
|
|
@@ -460,117 +937,89 @@ class UdpPhysicalLayer extends AbstractPhysicalLayer {
|
|
|
460
937
|
});
|
|
461
938
|
}
|
|
462
939
|
else {
|
|
463
|
-
reject(new
|
|
940
|
+
reject(new ModbusError(ModbusErrorCode.PORT_NOT_OPEN, 'Port is not open'));
|
|
464
941
|
}
|
|
465
942
|
});
|
|
466
943
|
}
|
|
467
944
|
close() {
|
|
945
|
+
if (!this._socket) {
|
|
946
|
+
return Promise.resolve();
|
|
947
|
+
}
|
|
948
|
+
const socket = this._socket;
|
|
468
949
|
return new Promise((resolve) => {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
950
|
+
socket.once('close', () => resolve());
|
|
951
|
+
try {
|
|
952
|
+
socket.close();
|
|
953
|
+
}
|
|
954
|
+
catch (_a) {
|
|
955
|
+
// Socket may already be closed; the registered 'close' listener still fires asynchronously.
|
|
956
|
+
}
|
|
474
957
|
});
|
|
475
958
|
}
|
|
476
959
|
destroy() {
|
|
960
|
+
if (this._destroyed) {
|
|
961
|
+
return Promise.resolve();
|
|
962
|
+
}
|
|
477
963
|
this._destroyed = true;
|
|
478
|
-
this.
|
|
479
|
-
|
|
964
|
+
return this.close().then(() => {
|
|
965
|
+
this.removeAllListeners();
|
|
966
|
+
});
|
|
480
967
|
}
|
|
481
968
|
}
|
|
482
969
|
|
|
483
970
|
class AbstractApplicationLayer extends EventEmitter {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
971
|
+
constructor() {
|
|
972
|
+
super(...arguments);
|
|
973
|
+
Object.defineProperty(this, "_role", {
|
|
974
|
+
enumerable: true,
|
|
975
|
+
configurable: true,
|
|
976
|
+
writable: true,
|
|
977
|
+
value: void 0
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
get role() {
|
|
981
|
+
if (!this._role) {
|
|
982
|
+
throw new ModbusError(ModbusErrorCode.INVALID_ROLE, 'Application layer role not set');
|
|
492
983
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return false;
|
|
984
|
+
return this._role;
|
|
985
|
+
}
|
|
986
|
+
set role(value) {
|
|
987
|
+
if (this._role && this._role !== value) {
|
|
988
|
+
throw new ModbusError(ModbusErrorCode.INVALID_ROLE, `Application layer role already set to ${this._role}`);
|
|
502
989
|
}
|
|
990
|
+
this._role = value;
|
|
503
991
|
}
|
|
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;
|
|
992
|
+
flush() {
|
|
993
|
+
// no-op — override in subclasses
|
|
994
|
+
}
|
|
995
|
+
addCustomFunctionCode(cfc) {
|
|
996
|
+
}
|
|
997
|
+
removeCustomFunctionCode(fc) {
|
|
529
998
|
}
|
|
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
999
|
}
|
|
546
1000
|
|
|
547
1001
|
const MAX_FRAME_LENGTH = 256;
|
|
1002
|
+
const MIN_FRAME_LENGTH = 4;
|
|
548
1003
|
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) {
|
|
1004
|
+
constructor(physicalLayer, options = {}) {
|
|
556
1005
|
super();
|
|
557
|
-
Object.defineProperty(this, "
|
|
1006
|
+
Object.defineProperty(this, "PROTOCOL", {
|
|
558
1007
|
enumerable: true,
|
|
559
1008
|
configurable: true,
|
|
560
1009
|
writable: true,
|
|
561
|
-
value:
|
|
1010
|
+
value: 'RTU'
|
|
562
1011
|
});
|
|
563
|
-
Object.defineProperty(this, "
|
|
1012
|
+
Object.defineProperty(this, "_states", {
|
|
564
1013
|
enumerable: true,
|
|
565
1014
|
configurable: true,
|
|
566
1015
|
writable: true,
|
|
567
|
-
value:
|
|
1016
|
+
value: new Map()
|
|
568
1017
|
});
|
|
569
|
-
Object.defineProperty(this, "
|
|
1018
|
+
Object.defineProperty(this, "_customFunctionCodes", {
|
|
570
1019
|
enumerable: true,
|
|
571
1020
|
configurable: true,
|
|
572
1021
|
writable: true,
|
|
573
|
-
value:
|
|
1022
|
+
value: new Map()
|
|
574
1023
|
});
|
|
575
1024
|
Object.defineProperty(this, "_removeAllListeners", {
|
|
576
1025
|
enumerable: true,
|
|
@@ -578,129 +1027,288 @@ class RtuApplicationLayer extends AbstractApplicationLayer {
|
|
|
578
1027
|
writable: true,
|
|
579
1028
|
value: []
|
|
580
1029
|
});
|
|
1030
|
+
Object.defineProperty(this, "_threePointFiveT", {
|
|
1031
|
+
enumerable: true,
|
|
1032
|
+
configurable: true,
|
|
1033
|
+
writable: true,
|
|
1034
|
+
value: void 0
|
|
1035
|
+
});
|
|
1036
|
+
Object.defineProperty(this, "_onePointFiveT", {
|
|
1037
|
+
enumerable: true,
|
|
1038
|
+
configurable: true,
|
|
1039
|
+
writable: true,
|
|
1040
|
+
value: void 0
|
|
1041
|
+
});
|
|
1042
|
+
Object.defineProperty(this, "_destroyed", {
|
|
1043
|
+
enumerable: true,
|
|
1044
|
+
configurable: true,
|
|
1045
|
+
writable: true,
|
|
1046
|
+
value: false
|
|
1047
|
+
});
|
|
1048
|
+
const { intervalBetweenFrames, interCharTimeout } = options;
|
|
581
1049
|
let threePointFiveT = 0;
|
|
1050
|
+
let onePointFiveT = 0;
|
|
582
1051
|
if (physicalLayer.TYPE === 'SERIAL') {
|
|
583
|
-
|
|
584
|
-
|
|
1052
|
+
const baudRate = physicalLayer.baudRate;
|
|
1053
|
+
if (intervalBetweenFrames && intervalBetweenFrames.unit === 'ms') {
|
|
1054
|
+
threePointFiveT = intervalBetweenFrames.value;
|
|
585
1055
|
}
|
|
586
1056
|
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();
|
|
1057
|
+
threePointFiveT =
|
|
1058
|
+
baudRate > 19200 ? 1.75 : Math.ceil(bitsToMs(baudRate, intervalBetweenFrames ? intervalBetweenFrames.value : 38.5));
|
|
614
1059
|
}
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
|
|
1060
|
+
if (interCharTimeout) {
|
|
1061
|
+
if (interCharTimeout.unit === 'ms') {
|
|
1062
|
+
onePointFiveT = interCharTimeout.value;
|
|
618
1063
|
}
|
|
619
1064
|
else {
|
|
620
|
-
|
|
1065
|
+
onePointFiveT = baudRate > 19200 ? 0.75 : Math.ceil(bitsToMs(baudRate, interCharTimeout.value));
|
|
621
1066
|
}
|
|
622
1067
|
}
|
|
1068
|
+
}
|
|
1069
|
+
this._threePointFiveT = threePointFiveT;
|
|
1070
|
+
this._onePointFiveT = onePointFiveT;
|
|
1071
|
+
const handleData = (data, response, connection) => {
|
|
1072
|
+
let state = this.getState(connection.id);
|
|
1073
|
+
if (state.t15Expired && state.end > state.start) {
|
|
1074
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.T1_5_EXCEEDED, 'Inter-character timeout (t1.5) exceeded'));
|
|
1075
|
+
state.start = 0;
|
|
1076
|
+
state.end = 0;
|
|
1077
|
+
}
|
|
1078
|
+
state.t15Expired = false;
|
|
1079
|
+
let dataOffset = 0;
|
|
1080
|
+
while (dataOffset < data.length) {
|
|
1081
|
+
const space = state.pool.length - state.end;
|
|
1082
|
+
if (space === 0) {
|
|
1083
|
+
this.clearStateTimers(state);
|
|
1084
|
+
this.flushBuffer(connection.id, response, connection, this._threePointFiveT > 0);
|
|
1085
|
+
state = this.getState(connection.id);
|
|
1086
|
+
if (state.pool.length - state.end === 0) {
|
|
1087
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_DATA, 'Frame buffer exhausted before complete frame received'));
|
|
1088
|
+
state.start = 0;
|
|
1089
|
+
state.end = 0;
|
|
1090
|
+
state.t15Expired = false;
|
|
1091
|
+
}
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const toCopy = Math.min(space, data.length - dataOffset);
|
|
1095
|
+
data.copy(state.pool, state.end, dataOffset, dataOffset + toCopy);
|
|
1096
|
+
state.end += toCopy;
|
|
1097
|
+
dataOffset += toCopy;
|
|
1098
|
+
}
|
|
1099
|
+
this.clearStateTimers(state);
|
|
1100
|
+
const commitFrame = () => {
|
|
1101
|
+
this.clearStateTimers(state);
|
|
1102
|
+
this.flushBuffer(connection.id, response, connection, this._threePointFiveT > 0);
|
|
1103
|
+
};
|
|
1104
|
+
if (state.end - state.start >= MAX_FRAME_LENGTH) {
|
|
1105
|
+
commitFrame();
|
|
1106
|
+
}
|
|
1107
|
+
else if (this._threePointFiveT) {
|
|
1108
|
+
if (this._onePointFiveT > 0) {
|
|
1109
|
+
state.interCharTimer = setTimeout(() => {
|
|
1110
|
+
state.interCharTimer = undefined;
|
|
1111
|
+
state.t15Expired = true;
|
|
1112
|
+
}, this._onePointFiveT);
|
|
1113
|
+
}
|
|
1114
|
+
state.timer = setTimeout(commitFrame, this._threePointFiveT);
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
commitFrame();
|
|
1118
|
+
}
|
|
623
1119
|
};
|
|
624
1120
|
physicalLayer.on('data', handleData);
|
|
625
|
-
this._removeAllListeners.push(() =>
|
|
626
|
-
|
|
627
|
-
|
|
1121
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('data', handleData));
|
|
1122
|
+
const handleConnectionClose = (connection) => {
|
|
1123
|
+
this.clearState(connection.id);
|
|
1124
|
+
};
|
|
1125
|
+
physicalLayer.on('connection-close', handleConnectionClose);
|
|
1126
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('connection-close', handleConnectionClose));
|
|
628
1127
|
const handleClose = () => {
|
|
629
|
-
|
|
630
|
-
|
|
1128
|
+
for (const key of [...this._states.keys()]) {
|
|
1129
|
+
this.clearState(key);
|
|
1130
|
+
}
|
|
631
1131
|
};
|
|
632
1132
|
physicalLayer.on('close', handleClose);
|
|
633
|
-
this._removeAllListeners.push(() =>
|
|
634
|
-
physicalLayer.removeListener('close', handleClose);
|
|
635
|
-
});
|
|
1133
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('close', handleClose));
|
|
636
1134
|
}
|
|
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
|
-
|
|
1135
|
+
getState(key) {
|
|
1136
|
+
let state = this._states.get(key);
|
|
1137
|
+
if (!state) {
|
|
1138
|
+
state = { pool: Buffer.alloc(MAX_FRAME_LENGTH * 2), start: 0, end: 0 };
|
|
1139
|
+
this._states.set(key, state);
|
|
1140
|
+
}
|
|
1141
|
+
return state;
|
|
1142
|
+
}
|
|
1143
|
+
clearStateTimers(state) {
|
|
1144
|
+
if (state.timer) {
|
|
1145
|
+
clearTimeout(state.timer);
|
|
1146
|
+
state.timer = undefined;
|
|
1147
|
+
}
|
|
1148
|
+
if (state.interCharTimer) {
|
|
1149
|
+
clearTimeout(state.interCharTimer);
|
|
1150
|
+
state.interCharTimer = undefined;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
clearState(key) {
|
|
1154
|
+
const state = this._states.get(key);
|
|
1155
|
+
if (state) {
|
|
1156
|
+
this.clearStateTimers(state);
|
|
1157
|
+
}
|
|
1158
|
+
this._states.delete(key);
|
|
1159
|
+
}
|
|
1160
|
+
flush() {
|
|
1161
|
+
for (const state of this._states.values()) {
|
|
1162
|
+
this.clearStateTimers(state);
|
|
1163
|
+
state.start = 0;
|
|
1164
|
+
state.end = 0;
|
|
1165
|
+
}
|
|
1166
|
+
this._states.clear();
|
|
1167
|
+
}
|
|
1168
|
+
addCustomFunctionCode(cfc) {
|
|
1169
|
+
if (!isUint8(cfc.fc)) {
|
|
1170
|
+
throw new ModbusError(ModbusErrorCode.RANGE, `fc must be an integer in 0..255, got ${cfc.fc}`);
|
|
1171
|
+
}
|
|
1172
|
+
this._customFunctionCodes.set(cfc.fc, cfc);
|
|
1173
|
+
}
|
|
1174
|
+
removeCustomFunctionCode(fc) {
|
|
1175
|
+
this._customFunctionCodes.delete(fc);
|
|
1176
|
+
}
|
|
1177
|
+
flushBuffer(key, response, connection, strict) {
|
|
1178
|
+
const state = this._states.get(key);
|
|
1179
|
+
if (!state) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
while (state.end - state.start > 0) {
|
|
1183
|
+
const buffer = state.pool.subarray(state.start, state.end);
|
|
1184
|
+
const result = this.tryExtract(buffer);
|
|
1185
|
+
if (result.kind === 'frame') {
|
|
1186
|
+
state.start += result.frame.length;
|
|
1187
|
+
this.deliverFrame(result.frame, response, connection);
|
|
1188
|
+
}
|
|
1189
|
+
else if (result.kind === 'skip') {
|
|
1190
|
+
if (strict) {
|
|
1191
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.CRC_MISMATCH, 'CRC mismatch'));
|
|
1192
|
+
state.start = 0;
|
|
1193
|
+
state.end = 0;
|
|
1194
|
+
state.t15Expired = false;
|
|
1195
|
+
return;
|
|
666
1196
|
}
|
|
1197
|
+
state.start += 1;
|
|
667
1198
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1199
|
+
else if (result.kind === 'insufficient') {
|
|
1200
|
+
if (state.end - state.start >= MAX_FRAME_LENGTH) {
|
|
1201
|
+
state.start += 1;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (strict) {
|
|
1205
|
+
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'));
|
|
1206
|
+
state.start = 0;
|
|
1207
|
+
state.end = 0;
|
|
1208
|
+
state.t15Expired = false;
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (state.t15Expired) {
|
|
1212
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.T1_5_EXCEEDED, 'Inter-character timeout (t1.5) exceeded'));
|
|
1213
|
+
state.start = 0;
|
|
1214
|
+
state.end = 0;
|
|
1215
|
+
state.t15Expired = false;
|
|
1216
|
+
}
|
|
1217
|
+
break;
|
|
671
1218
|
}
|
|
672
1219
|
else {
|
|
673
|
-
|
|
1220
|
+
this.emit('framing-error', result.error);
|
|
1221
|
+
state.start = 0;
|
|
1222
|
+
state.end = 0;
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (state.start > 0) {
|
|
1227
|
+
if (state.start < state.end) {
|
|
1228
|
+
state.pool.copy(state.pool, 0, state.start, state.end);
|
|
674
1229
|
}
|
|
1230
|
+
state.end -= state.start;
|
|
1231
|
+
state.start = 0;
|
|
675
1232
|
}
|
|
676
|
-
|
|
677
|
-
|
|
1233
|
+
if (state.end === 0 && !state.timer) {
|
|
1234
|
+
this._states.delete(key);
|
|
678
1235
|
}
|
|
679
1236
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1237
|
+
tryExtract(buffer) {
|
|
1238
|
+
if (buffer.length < MIN_FRAME_LENGTH) {
|
|
1239
|
+
return { kind: 'insufficient' };
|
|
1240
|
+
}
|
|
1241
|
+
const isResponse = this.role === 'MASTER';
|
|
1242
|
+
const fc = buffer[1];
|
|
1243
|
+
const cfc = this._customFunctionCodes.get(fc);
|
|
1244
|
+
if (cfc) {
|
|
1245
|
+
const predictor = isResponse ? cfc.predictResponseLength : cfc.predictRequestLength;
|
|
1246
|
+
const expected = predictor(buffer);
|
|
1247
|
+
if (expected === null) {
|
|
1248
|
+
return { kind: 'insufficient' };
|
|
1249
|
+
}
|
|
1250
|
+
return this.checkExpected(buffer, expected);
|
|
1251
|
+
}
|
|
1252
|
+
const result = predictRtuFrameLength(buffer, isResponse);
|
|
1253
|
+
if (result.kind === 'length') {
|
|
1254
|
+
return this.checkExpected(buffer, result.length);
|
|
1255
|
+
}
|
|
1256
|
+
if (result.kind === 'need-more') {
|
|
1257
|
+
return { kind: 'insufficient' };
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
kind: 'error',
|
|
1261
|
+
error: new ModbusError(ModbusErrorCode.UNKNOWN_FC, `Unknown function code 0x${fc.toString(16).padStart(2, '0')} — register a CustomFunctionCode to frame this FC`),
|
|
1262
|
+
};
|
|
684
1263
|
}
|
|
685
|
-
|
|
686
|
-
|
|
1264
|
+
checkExpected(buffer, expected) {
|
|
1265
|
+
if (expected > MAX_FRAME_LENGTH || expected < MIN_FRAME_LENGTH) {
|
|
1266
|
+
return { kind: 'error', error: new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data') };
|
|
1267
|
+
}
|
|
1268
|
+
if (buffer.length < expected) {
|
|
1269
|
+
return { kind: 'insufficient' };
|
|
1270
|
+
}
|
|
1271
|
+
if (this.crcMatches(buffer, expected)) {
|
|
1272
|
+
return { kind: 'frame', frame: buffer.subarray(0, expected), rest: buffer.subarray(expected) };
|
|
1273
|
+
}
|
|
1274
|
+
return { kind: 'skip', rest: buffer.subarray(1) };
|
|
1275
|
+
}
|
|
1276
|
+
crcMatches(buffer, length) {
|
|
1277
|
+
return buffer.readUInt16LE(length - 2) === crc(buffer.subarray(0, length - 2));
|
|
1278
|
+
}
|
|
1279
|
+
deliverFrame(buffer, response, connection) {
|
|
1280
|
+
const frameBuf = Buffer.from(buffer);
|
|
1281
|
+
const frame = {
|
|
1282
|
+
unit: frameBuf[0],
|
|
1283
|
+
fc: frameBuf[1],
|
|
1284
|
+
data: frameBuf.subarray(2, frameBuf.length - 2),
|
|
1285
|
+
buffer: frameBuf,
|
|
1286
|
+
};
|
|
1287
|
+
this.emit('framing', frame, response, connection);
|
|
687
1288
|
}
|
|
688
1289
|
encode(data) {
|
|
689
1290
|
const buffer = Buffer.alloc(data.data.length + 4);
|
|
690
1291
|
buffer.writeUInt8(data.unit, 0);
|
|
691
1292
|
buffer.writeUInt8(data.fc, 1);
|
|
692
|
-
data.data
|
|
693
|
-
buffer.writeUInt8(num, 2 + index);
|
|
694
|
-
});
|
|
1293
|
+
buffer.set(data.data, 2);
|
|
695
1294
|
buffer.writeUInt16LE(crc(buffer.subarray(0, -2)), buffer.length - 2);
|
|
696
1295
|
return buffer;
|
|
697
1296
|
}
|
|
698
1297
|
destroy() {
|
|
1298
|
+
if (this._destroyed) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
this._destroyed = true;
|
|
699
1302
|
this.removeAllListeners();
|
|
700
|
-
for (const
|
|
701
|
-
|
|
1303
|
+
for (const removeListener of this._removeAllListeners) {
|
|
1304
|
+
removeListener();
|
|
702
1305
|
}
|
|
703
|
-
|
|
1306
|
+
this._removeAllListeners.length = 0;
|
|
1307
|
+
for (const state of this._states.values()) {
|
|
1308
|
+
this.clearStateTimers(state);
|
|
1309
|
+
}
|
|
1310
|
+
this._states.clear();
|
|
1311
|
+
this._customFunctionCodes.clear();
|
|
704
1312
|
}
|
|
705
1313
|
}
|
|
706
1314
|
|
|
@@ -709,26 +1317,43 @@ const CHAR_CODE = {
|
|
|
709
1317
|
CR: '\r'.charCodeAt(0),
|
|
710
1318
|
LF: '\n'.charCodeAt(0),
|
|
711
1319
|
};
|
|
1320
|
+
// Modbus ASCII frame encodes at most 256 PDU bytes as 512 hex chars between
|
|
1321
|
+
// `:` and `\r`. Cap per-connection buffering so a peer that never sends `\r`
|
|
1322
|
+
// cannot grow `state.frame` without bound.
|
|
1323
|
+
const MAX_ASCII_PAYLOAD = 512;
|
|
1324
|
+
const HEX_DECODE = new Uint8Array(256);
|
|
1325
|
+
HEX_DECODE.fill(0xff);
|
|
1326
|
+
for (let i = 0x30; i <= 0x39; i++) {
|
|
1327
|
+
HEX_DECODE[i] = i - 0x30;
|
|
1328
|
+
}
|
|
1329
|
+
for (let i = 0x41; i <= 0x46; i++) {
|
|
1330
|
+
HEX_DECODE[i] = i - 0x41 + 10;
|
|
1331
|
+
}
|
|
1332
|
+
for (let i = 0x61; i <= 0x66; i++) {
|
|
1333
|
+
HEX_DECODE[i] = i - 0x61 + 10;
|
|
1334
|
+
}
|
|
1335
|
+
const HEX_ENCODE = new Uint8Array('0123456789ABCDEF'.split('').map((c) => c.charCodeAt(0)));
|
|
712
1336
|
class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
713
|
-
constructor(physicalLayer) {
|
|
1337
|
+
constructor(physicalLayer, options = {}) {
|
|
1338
|
+
var _a;
|
|
714
1339
|
super();
|
|
715
|
-
Object.defineProperty(this, "
|
|
1340
|
+
Object.defineProperty(this, "PROTOCOL", {
|
|
716
1341
|
enumerable: true,
|
|
717
1342
|
configurable: true,
|
|
718
1343
|
writable: true,
|
|
719
|
-
value:
|
|
1344
|
+
value: 'ASCII'
|
|
720
1345
|
});
|
|
721
|
-
Object.defineProperty(this, "
|
|
1346
|
+
Object.defineProperty(this, "lenientHex", {
|
|
722
1347
|
enumerable: true,
|
|
723
1348
|
configurable: true,
|
|
724
1349
|
writable: true,
|
|
725
|
-
value:
|
|
1350
|
+
value: void 0
|
|
726
1351
|
});
|
|
727
|
-
Object.defineProperty(this, "
|
|
1352
|
+
Object.defineProperty(this, "_states", {
|
|
728
1353
|
enumerable: true,
|
|
729
1354
|
configurable: true,
|
|
730
1355
|
writable: true,
|
|
731
|
-
value:
|
|
1356
|
+
value: new Map()
|
|
732
1357
|
});
|
|
733
1358
|
Object.defineProperty(this, "_removeAllListeners", {
|
|
734
1359
|
enumerable: true,
|
|
@@ -736,44 +1361,68 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
736
1361
|
writable: true,
|
|
737
1362
|
value: []
|
|
738
1363
|
});
|
|
739
|
-
|
|
1364
|
+
Object.defineProperty(this, "_destroyed", {
|
|
1365
|
+
enumerable: true,
|
|
1366
|
+
configurable: true,
|
|
1367
|
+
writable: true,
|
|
1368
|
+
value: false
|
|
1369
|
+
});
|
|
1370
|
+
this.lenientHex = (_a = options.lenientHex) !== null && _a !== void 0 ? _a : false;
|
|
1371
|
+
const lenientHex = this.lenientHex;
|
|
1372
|
+
const isHexChar = (value) => {
|
|
1373
|
+
if (value >= 0x30 && value <= 0x39) {
|
|
1374
|
+
return true;
|
|
1375
|
+
}
|
|
1376
|
+
if (value >= 0x41 && value <= 0x46) {
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
if (lenientHex && value >= 0x61 && value <= 0x66) {
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
return false;
|
|
1383
|
+
};
|
|
1384
|
+
const handleData = (data, response, connection) => {
|
|
1385
|
+
const state = this.getState(connection.id);
|
|
740
1386
|
data.forEach((value) => {
|
|
741
|
-
switch (
|
|
1387
|
+
switch (state.status) {
|
|
742
1388
|
case 'idle': {
|
|
743
1389
|
if (value === CHAR_CODE.COLON) {
|
|
744
|
-
|
|
745
|
-
|
|
1390
|
+
state.status = 'reception';
|
|
1391
|
+
state.frame = [];
|
|
746
1392
|
}
|
|
747
1393
|
break;
|
|
748
1394
|
}
|
|
749
1395
|
case 'reception': {
|
|
750
1396
|
if (value === CHAR_CODE.COLON) {
|
|
751
|
-
|
|
1397
|
+
state.frame = [];
|
|
752
1398
|
}
|
|
753
1399
|
else if (value === CHAR_CODE.CR) {
|
|
754
|
-
|
|
1400
|
+
state.status = 'waiting end';
|
|
1401
|
+
}
|
|
1402
|
+
else if (state.frame.length >= MAX_ASCII_PAYLOAD) {
|
|
1403
|
+
state.status = 'idle';
|
|
1404
|
+
state.frame = [];
|
|
1405
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data'));
|
|
1406
|
+
}
|
|
1407
|
+
else if (!isHexChar(value)) {
|
|
1408
|
+
state.status = 'idle';
|
|
1409
|
+
state.frame = [];
|
|
1410
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_HEX, 'Invalid hex character'));
|
|
755
1411
|
}
|
|
756
1412
|
else {
|
|
757
|
-
|
|
1413
|
+
state.frame.push(value);
|
|
758
1414
|
}
|
|
759
1415
|
break;
|
|
760
1416
|
}
|
|
761
1417
|
case 'waiting end': {
|
|
762
1418
|
if (value === CHAR_CODE.COLON) {
|
|
763
|
-
|
|
764
|
-
|
|
1419
|
+
state.status = 'reception';
|
|
1420
|
+
state.frame = [];
|
|
765
1421
|
}
|
|
766
1422
|
else {
|
|
767
|
-
|
|
1423
|
+
state.status = 'idle';
|
|
768
1424
|
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
|
-
});
|
|
1425
|
+
this.framing(Buffer.from(state.frame), response, connection);
|
|
777
1426
|
}
|
|
778
1427
|
}
|
|
779
1428
|
break;
|
|
@@ -782,115 +1431,102 @@ class AsciiApplicationLayer extends AbstractApplicationLayer {
|
|
|
782
1431
|
});
|
|
783
1432
|
};
|
|
784
1433
|
physicalLayer.on('data', handleData);
|
|
785
|
-
this._removeAllListeners.push(() =>
|
|
786
|
-
|
|
787
|
-
|
|
1434
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('data', handleData));
|
|
1435
|
+
const handleConnectionClose = (connection) => {
|
|
1436
|
+
this._states.delete(connection.id);
|
|
1437
|
+
};
|
|
1438
|
+
physicalLayer.on('connection-close', handleConnectionClose);
|
|
1439
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('connection-close', handleConnectionClose));
|
|
788
1440
|
const handleClose = () => {
|
|
789
|
-
this.
|
|
790
|
-
this._frame = [];
|
|
1441
|
+
this._states.clear();
|
|
791
1442
|
};
|
|
792
1443
|
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'));
|
|
1444
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('close', handleClose));
|
|
1445
|
+
}
|
|
1446
|
+
getState(key) {
|
|
1447
|
+
let state = this._states.get(key);
|
|
1448
|
+
if (!state) {
|
|
1449
|
+
state = { status: 'idle', frame: [] };
|
|
1450
|
+
this._states.set(key, state);
|
|
853
1451
|
}
|
|
1452
|
+
return state;
|
|
854
1453
|
}
|
|
855
|
-
|
|
856
|
-
this.
|
|
857
|
-
this._status = 'idle';
|
|
858
|
-
this._frame = [];
|
|
1454
|
+
flush() {
|
|
1455
|
+
this._states.clear();
|
|
859
1456
|
}
|
|
860
|
-
|
|
861
|
-
|
|
1457
|
+
framing(_buffer, response, connection) {
|
|
1458
|
+
if (_buffer.length < 6) {
|
|
1459
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INSUFFICIENT_DATA, 'Insufficient data length'));
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (_buffer.length % 2 !== 0) {
|
|
1463
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data'));
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const buffer = Buffer.allocUnsafe(_buffer.length / 2);
|
|
1467
|
+
for (let i = 0; i < _buffer.length; i += 2) {
|
|
1468
|
+
const hi = HEX_DECODE[_buffer[i]];
|
|
1469
|
+
const lo = HEX_DECODE[_buffer[i + 1]];
|
|
1470
|
+
if (hi === 0xff || lo === 0xff) {
|
|
1471
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.INVALID_HEX, 'Invalid hex character'));
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
buffer[i / 2] = (hi << 4) | lo;
|
|
1475
|
+
}
|
|
1476
|
+
const frame = {
|
|
1477
|
+
unit: buffer[0],
|
|
1478
|
+
fc: buffer[1],
|
|
1479
|
+
data: buffer.subarray(2, buffer.length - 1),
|
|
1480
|
+
buffer: _buffer,
|
|
1481
|
+
};
|
|
1482
|
+
const lrcPassed = buffer[buffer.length - 1] === lrc(buffer.subarray(0, buffer.length - 1));
|
|
1483
|
+
if (!lrcPassed) {
|
|
1484
|
+
this.emit('framing-error', new ModbusError(ModbusErrorCode.LRC_MISMATCH, 'LRC check failed'));
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
this.emit('framing', frame, response, connection);
|
|
862
1488
|
}
|
|
863
1489
|
encode(data) {
|
|
864
1490
|
const buffer = Buffer.alloc(data.data.length + 3);
|
|
865
1491
|
buffer.writeUInt8(data.unit, 0);
|
|
866
1492
|
buffer.writeUInt8(data.fc, 1);
|
|
867
|
-
data.data
|
|
868
|
-
buffer.writeUInt8(num, 2 + index);
|
|
869
|
-
});
|
|
1493
|
+
buffer.set(data.data, 2);
|
|
870
1494
|
buffer.writeUInt8(lrc(buffer.subarray(0, -1)), buffer.length - 1);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1495
|
+
const out = Buffer.allocUnsafe(1 + buffer.length * 2 + 2);
|
|
1496
|
+
out[0] = CHAR_CODE.COLON;
|
|
1497
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
1498
|
+
const byte = buffer[i];
|
|
1499
|
+
out[1 + i * 2] = HEX_ENCODE[byte >> 4];
|
|
1500
|
+
out[2 + i * 2] = HEX_ENCODE[byte & 0x0f];
|
|
874
1501
|
}
|
|
875
|
-
|
|
876
|
-
|
|
1502
|
+
out[out.length - 2] = CHAR_CODE.CR;
|
|
1503
|
+
out[out.length - 1] = CHAR_CODE.LF;
|
|
1504
|
+
return out;
|
|
877
1505
|
}
|
|
878
1506
|
destroy() {
|
|
1507
|
+
if (this._destroyed) {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
this._destroyed = true;
|
|
879
1511
|
this.removeAllListeners();
|
|
880
|
-
for (const
|
|
881
|
-
|
|
1512
|
+
for (const removeListener of this._removeAllListeners) {
|
|
1513
|
+
removeListener();
|
|
882
1514
|
}
|
|
1515
|
+
this._removeAllListeners.length = 0;
|
|
1516
|
+
this._states.clear();
|
|
883
1517
|
}
|
|
884
1518
|
}
|
|
885
1519
|
|
|
1520
|
+
const MAX_TCP_FRAME = 260;
|
|
1521
|
+
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
886
1522
|
class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
887
1523
|
constructor(physicalLayer) {
|
|
888
1524
|
super();
|
|
889
|
-
Object.defineProperty(this, "
|
|
1525
|
+
Object.defineProperty(this, "PROTOCOL", {
|
|
890
1526
|
enumerable: true,
|
|
891
1527
|
configurable: true,
|
|
892
1528
|
writable: true,
|
|
893
|
-
value:
|
|
1529
|
+
value: 'TCP'
|
|
894
1530
|
});
|
|
895
1531
|
Object.defineProperty(this, "_transactionId", {
|
|
896
1532
|
enumerable: true,
|
|
@@ -898,106 +1534,229 @@ class TcpApplicationLayer extends AbstractApplicationLayer {
|
|
|
898
1534
|
writable: true,
|
|
899
1535
|
value: 1
|
|
900
1536
|
});
|
|
1537
|
+
Object.defineProperty(this, "_buffers", {
|
|
1538
|
+
enumerable: true,
|
|
1539
|
+
configurable: true,
|
|
1540
|
+
writable: true,
|
|
1541
|
+
value: new Map()
|
|
1542
|
+
});
|
|
901
1543
|
Object.defineProperty(this, "_removeAllListeners", {
|
|
902
1544
|
enumerable: true,
|
|
903
1545
|
configurable: true,
|
|
904
1546
|
writable: true,
|
|
905
1547
|
value: []
|
|
906
1548
|
});
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1549
|
+
Object.defineProperty(this, "_destroyed", {
|
|
1550
|
+
enumerable: true,
|
|
1551
|
+
configurable: true,
|
|
1552
|
+
writable: true,
|
|
1553
|
+
value: false
|
|
1554
|
+
});
|
|
1555
|
+
const handleData = (data, response, connection) => {
|
|
1556
|
+
var _a;
|
|
1557
|
+
let buffer = (_a = this._buffers.get(connection.id)) !== null && _a !== void 0 ? _a : EMPTY_BUFFER;
|
|
1558
|
+
buffer = buffer.length === 0 ? data : Buffer.concat([buffer, data]);
|
|
1559
|
+
while (buffer.length > 0) {
|
|
1560
|
+
const result = this.tryExtract(buffer);
|
|
1561
|
+
if (result.kind === 'frame') {
|
|
1562
|
+
this.processFrame(result.frame, response, connection);
|
|
1563
|
+
buffer = result.rest;
|
|
1564
|
+
}
|
|
1565
|
+
else if (result.kind === 'insufficient') {
|
|
1566
|
+
break;
|
|
914
1567
|
}
|
|
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
|
-
}
|
|
1568
|
+
else {
|
|
1569
|
+
this.emit('framing-error', result.error);
|
|
1570
|
+
buffer = EMPTY_BUFFER;
|
|
1571
|
+
break;
|
|
954
1572
|
}
|
|
955
|
-
|
|
1573
|
+
}
|
|
1574
|
+
if (buffer.length === 0) {
|
|
1575
|
+
this._buffers.delete(connection.id);
|
|
956
1576
|
}
|
|
957
1577
|
else {
|
|
958
|
-
|
|
1578
|
+
this._buffers.set(connection.id, buffer);
|
|
959
1579
|
}
|
|
1580
|
+
};
|
|
1581
|
+
physicalLayer.on('data', handleData);
|
|
1582
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('data', handleData));
|
|
1583
|
+
const handleConnectionClose = (connection) => {
|
|
1584
|
+
this._buffers.delete(connection.id);
|
|
1585
|
+
};
|
|
1586
|
+
physicalLayer.on('connection-close', handleConnectionClose);
|
|
1587
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('connection-close', handleConnectionClose));
|
|
1588
|
+
const handleClose = () => {
|
|
1589
|
+
this._buffers.clear();
|
|
1590
|
+
};
|
|
1591
|
+
physicalLayer.on('close', handleClose);
|
|
1592
|
+
this._removeAllListeners.push(() => physicalLayer.removeListener('close', handleClose));
|
|
1593
|
+
}
|
|
1594
|
+
tryExtract(buffer) {
|
|
1595
|
+
if (buffer.length < 8) {
|
|
1596
|
+
return { kind: 'insufficient' };
|
|
960
1597
|
}
|
|
961
|
-
|
|
962
|
-
|
|
1598
|
+
if (buffer[2] !== 0 || buffer[3] !== 0) {
|
|
1599
|
+
return { kind: 'error', error: new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data') };
|
|
963
1600
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1601
|
+
const length = buffer.readUInt16BE(4);
|
|
1602
|
+
const total = 6 + length;
|
|
1603
|
+
if (total > MAX_TCP_FRAME || length < 2) {
|
|
1604
|
+
return { kind: 'error', error: new ModbusError(ModbusErrorCode.INVALID_DATA, 'Invalid data') };
|
|
1605
|
+
}
|
|
1606
|
+
if (buffer.length < total) {
|
|
1607
|
+
return { kind: 'insufficient' };
|
|
1608
|
+
}
|
|
1609
|
+
return { kind: 'frame', frame: buffer.subarray(0, total), rest: buffer.subarray(total) };
|
|
1610
|
+
}
|
|
1611
|
+
processFrame(buffer, response, connection) {
|
|
1612
|
+
const frame = {
|
|
1613
|
+
transaction: buffer.readUInt16BE(0),
|
|
1614
|
+
unit: buffer[6],
|
|
1615
|
+
fc: buffer[7],
|
|
1616
|
+
data: buffer.subarray(8),
|
|
1617
|
+
buffer,
|
|
1618
|
+
};
|
|
1619
|
+
this.emit('framing', frame, response, connection);
|
|
970
1620
|
}
|
|
971
1621
|
encode(data) {
|
|
972
1622
|
var _a;
|
|
1623
|
+
const transaction = (_a = data.transaction) !== null && _a !== void 0 ? _a : this._transactionId;
|
|
973
1624
|
const buffer = Buffer.alloc(data.data.length + 8);
|
|
974
|
-
buffer.writeUInt16BE(
|
|
1625
|
+
buffer.writeUInt16BE(transaction, 0);
|
|
975
1626
|
buffer.writeUInt16BE(0, 2);
|
|
976
1627
|
buffer.writeUInt16BE(data.data.length + 2, 4);
|
|
977
1628
|
buffer.writeUInt8(data.unit, 6);
|
|
978
1629
|
buffer.writeUInt8(data.fc, 7);
|
|
979
|
-
data.data
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1630
|
+
buffer.set(data.data, 8);
|
|
1631
|
+
if (data.transaction === undefined) {
|
|
1632
|
+
this._transactionId = (this._transactionId + 1) % 65536 || 1;
|
|
1633
|
+
}
|
|
983
1634
|
return buffer;
|
|
984
1635
|
}
|
|
985
1636
|
destroy() {
|
|
1637
|
+
if (this._destroyed) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
this._destroyed = true;
|
|
986
1641
|
this.removeAllListeners();
|
|
987
|
-
for (const
|
|
988
|
-
|
|
1642
|
+
for (const removeListener of this._removeAllListeners) {
|
|
1643
|
+
removeListener();
|
|
989
1644
|
}
|
|
1645
|
+
this._removeAllListeners.length = 0;
|
|
1646
|
+
this._buffers.clear();
|
|
990
1647
|
}
|
|
991
1648
|
}
|
|
992
1649
|
|
|
1650
|
+
const FIFO_KEY = 'fifo';
|
|
1651
|
+
class MasterSession {
|
|
1652
|
+
constructor() {
|
|
1653
|
+
Object.defineProperty(this, "_waiters", {
|
|
1654
|
+
enumerable: true,
|
|
1655
|
+
configurable: true,
|
|
1656
|
+
writable: true,
|
|
1657
|
+
value: new Map()
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
start(key, preCheck, callback) {
|
|
1661
|
+
this._waiters.set(key, { preCheck, callback });
|
|
1662
|
+
}
|
|
1663
|
+
stop(key) {
|
|
1664
|
+
this._waiters.delete(key);
|
|
1665
|
+
}
|
|
1666
|
+
stopAll(error) {
|
|
1667
|
+
const waiters = [...this._waiters.values()];
|
|
1668
|
+
this._waiters.clear();
|
|
1669
|
+
for (const w of waiters) {
|
|
1670
|
+
w.callback(error);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
has(key) {
|
|
1674
|
+
return this._waiters.has(key);
|
|
1675
|
+
}
|
|
1676
|
+
handleFrame(frame) {
|
|
1677
|
+
var _a;
|
|
1678
|
+
const key = (_a = frame.transaction) !== null && _a !== void 0 ? _a : FIFO_KEY;
|
|
1679
|
+
const waiter = this._waiters.get(key);
|
|
1680
|
+
if (!waiter) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (!this.runPreChecks(waiter, frame)) {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
waiter.callback(null, frame);
|
|
1687
|
+
}
|
|
1688
|
+
handleError(error) {
|
|
1689
|
+
// Framing errors lose transaction context; reject every in-flight waiter.
|
|
1690
|
+
this.stopAll(error);
|
|
1691
|
+
}
|
|
1692
|
+
runPreChecks(waiter, frame) {
|
|
1693
|
+
for (const check of waiter.preCheck) {
|
|
1694
|
+
const res = check(frame);
|
|
1695
|
+
if (typeof res === 'undefined') {
|
|
1696
|
+
waiter.callback(new ModbusError(ModbusErrorCode.INSUFFICIENT_DATA, 'Insufficient data length'));
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
if (typeof res === 'number') {
|
|
1700
|
+
if (frame.data.length < res) {
|
|
1701
|
+
waiter.callback(new ModbusError(ModbusErrorCode.INSUFFICIENT_DATA, 'Insufficient data length'));
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
if (frame.data.length !== res) {
|
|
1705
|
+
waiter.callback(new ModbusError(ModbusErrorCode.INVALID_RESPONSE, 'Invalid response'));
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
if (!res) {
|
|
1711
|
+
waiter.callback(new ModbusError(ModbusErrorCode.INVALID_RESPONSE, 'Invalid response'));
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/******************************************************************************
|
|
1720
|
+
Copyright (c) Microsoft Corporation.
|
|
1721
|
+
|
|
1722
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
1723
|
+
purpose with or without fee is hereby granted.
|
|
1724
|
+
|
|
1725
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
1726
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
1727
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
1728
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
1729
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
1730
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
1731
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
1732
|
+
***************************************************************************** */
|
|
1733
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
1737
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
1738
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
1739
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
1740
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
1741
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
1742
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
1747
|
+
var e = new Error(message);
|
|
1748
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
1749
|
+
};
|
|
1750
|
+
|
|
993
1751
|
class ModbusMaster extends EventEmitter {
|
|
994
1752
|
get isOpen() {
|
|
995
1753
|
return this.physicalLayer.isOpen;
|
|
996
1754
|
}
|
|
997
1755
|
get destroyed() {
|
|
998
|
-
return this.physicalLayer.destroyed;
|
|
1756
|
+
return this._cleanLevel === 'destroy' || this.physicalLayer.destroyed;
|
|
999
1757
|
}
|
|
1000
|
-
constructor(applicationLayer, physicalLayer,
|
|
1758
|
+
constructor(applicationLayer, physicalLayer, options = {}) {
|
|
1759
|
+
var _a, _b;
|
|
1001
1760
|
super();
|
|
1002
1761
|
Object.defineProperty(this, "applicationLayer", {
|
|
1003
1762
|
enumerable: true,
|
|
@@ -1011,11 +1770,53 @@ class ModbusMaster extends EventEmitter {
|
|
|
1011
1770
|
writable: true,
|
|
1012
1771
|
value: physicalLayer
|
|
1013
1772
|
});
|
|
1773
|
+
Object.defineProperty(this, "_masterSession", {
|
|
1774
|
+
enumerable: true,
|
|
1775
|
+
configurable: true,
|
|
1776
|
+
writable: true,
|
|
1777
|
+
value: new MasterSession()
|
|
1778
|
+
});
|
|
1779
|
+
Object.defineProperty(this, "_queue", {
|
|
1780
|
+
enumerable: true,
|
|
1781
|
+
configurable: true,
|
|
1782
|
+
writable: true,
|
|
1783
|
+
value: []
|
|
1784
|
+
});
|
|
1785
|
+
Object.defineProperty(this, "_draining", {
|
|
1786
|
+
enumerable: true,
|
|
1787
|
+
configurable: true,
|
|
1788
|
+
writable: true,
|
|
1789
|
+
value: false
|
|
1790
|
+
});
|
|
1791
|
+
Object.defineProperty(this, "_nextTid", {
|
|
1792
|
+
enumerable: true,
|
|
1793
|
+
configurable: true,
|
|
1794
|
+
writable: true,
|
|
1795
|
+
value: 1
|
|
1796
|
+
});
|
|
1797
|
+
Object.defineProperty(this, "_closed", {
|
|
1798
|
+
enumerable: true,
|
|
1799
|
+
configurable: true,
|
|
1800
|
+
writable: true,
|
|
1801
|
+
value: false
|
|
1802
|
+
});
|
|
1803
|
+
Object.defineProperty(this, "_cleanLevel", {
|
|
1804
|
+
enumerable: true,
|
|
1805
|
+
configurable: true,
|
|
1806
|
+
writable: true,
|
|
1807
|
+
value: 'none'
|
|
1808
|
+
});
|
|
1014
1809
|
Object.defineProperty(this, "timeout", {
|
|
1015
1810
|
enumerable: true,
|
|
1016
1811
|
configurable: true,
|
|
1017
1812
|
writable: true,
|
|
1018
|
-
value:
|
|
1813
|
+
value: void 0
|
|
1814
|
+
});
|
|
1815
|
+
Object.defineProperty(this, "concurrent", {
|
|
1816
|
+
enumerable: true,
|
|
1817
|
+
configurable: true,
|
|
1818
|
+
writable: true,
|
|
1819
|
+
value: void 0
|
|
1019
1820
|
});
|
|
1020
1821
|
Object.defineProperty(this, "writeFC1", {
|
|
1021
1822
|
enumerable: true,
|
|
@@ -1089,6 +1890,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
1089
1890
|
writable: true,
|
|
1090
1891
|
value: void 0
|
|
1091
1892
|
});
|
|
1893
|
+
this.timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 1000;
|
|
1894
|
+
this.concurrent = (_b = options.concurrent) !== null && _b !== void 0 ? _b : false;
|
|
1895
|
+
if (this.concurrent && this.applicationLayer.PROTOCOL !== 'TCP') {
|
|
1896
|
+
throw new ModbusError(ModbusErrorCode.CONCURRENT_NOT_TCP, 'concurrent mode requires a Modbus TCP application layer');
|
|
1897
|
+
}
|
|
1898
|
+
this.applicationLayer.role = 'MASTER';
|
|
1092
1899
|
this.writeFC1 = this.readCoils;
|
|
1093
1900
|
this.writeFC2 = this.readDiscreteInputs;
|
|
1094
1901
|
this.writeFC3 = this.readHoldingRegisters;
|
|
@@ -1101,6 +1908,12 @@ class ModbusMaster extends EventEmitter {
|
|
|
1101
1908
|
this.handleFC22 = this.maskWriteRegister;
|
|
1102
1909
|
this.handleFC23 = this.readAndWriteMultipleRegisters;
|
|
1103
1910
|
this.handleFC43_14 = this.readDeviceIdentification;
|
|
1911
|
+
applicationLayer.on('framing', (frame) => {
|
|
1912
|
+
this._masterSession.handleFrame(frame);
|
|
1913
|
+
});
|
|
1914
|
+
applicationLayer.on('framing-error', (error) => {
|
|
1915
|
+
this._masterSession.handleError(error);
|
|
1916
|
+
});
|
|
1104
1917
|
physicalLayer.on('error', (error) => {
|
|
1105
1918
|
this.emit('error', error);
|
|
1106
1919
|
});
|
|
@@ -1108,50 +1921,90 @@ class ModbusMaster extends EventEmitter {
|
|
|
1108
1921
|
this.emit('close');
|
|
1109
1922
|
});
|
|
1110
1923
|
}
|
|
1111
|
-
|
|
1924
|
+
send(adu, preCheck, timeout, broadcast) {
|
|
1925
|
+
const task = () => this._exchange(adu, preCheck, timeout, broadcast);
|
|
1926
|
+
if (this.concurrent) {
|
|
1927
|
+
return task();
|
|
1928
|
+
}
|
|
1929
|
+
return new Promise((resolve, reject) => {
|
|
1930
|
+
const run = () => __awaiter(this, void 0, void 0, function* () {
|
|
1931
|
+
try {
|
|
1932
|
+
resolve(yield task());
|
|
1933
|
+
}
|
|
1934
|
+
catch (error) {
|
|
1935
|
+
reject(error);
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
this._queue.push({ run, cancel: reject });
|
|
1939
|
+
this._drain();
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
_drain() {
|
|
1943
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1944
|
+
if (this._draining) {
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
this._draining = true;
|
|
1948
|
+
try {
|
|
1949
|
+
while (this._queue.length > 0) {
|
|
1950
|
+
const item = this._queue.shift();
|
|
1951
|
+
yield item.run();
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
finally {
|
|
1955
|
+
this._draining = false;
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
_exchange(adu, preCheck, timeout, broadcast) {
|
|
1112
1960
|
return new Promise((resolve, reject) => {
|
|
1961
|
+
if (this._closed) {
|
|
1962
|
+
reject(new ModbusError(ModbusErrorCode.MASTER_CLOSED, 'Master closed'));
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const usesTid = this.applicationLayer.PROTOCOL === 'TCP' && !broadcast;
|
|
1966
|
+
let tid;
|
|
1967
|
+
if (usesTid) {
|
|
1968
|
+
tid = this._nextTid;
|
|
1969
|
+
this._nextTid = (this._nextTid + 1) % 65536 || 1;
|
|
1970
|
+
}
|
|
1971
|
+
const key = tid !== null && tid !== void 0 ? tid : FIFO_KEY;
|
|
1972
|
+
const payload = this.applicationLayer.encode(Object.assign(Object.assign({}, adu), { transaction: tid }));
|
|
1113
1973
|
let settled = false;
|
|
1114
|
-
const
|
|
1115
|
-
if (
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1974
|
+
const settle = (error, frame) => {
|
|
1975
|
+
if (settled) {
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
settled = true;
|
|
1979
|
+
clearTimeout(timer);
|
|
1980
|
+
this._masterSession.stop(key);
|
|
1981
|
+
if (error) {
|
|
1982
|
+
reject(error);
|
|
1983
|
+
}
|
|
1984
|
+
else {
|
|
1985
|
+
resolve(frame);
|
|
1119
1986
|
}
|
|
1120
|
-
}
|
|
1987
|
+
};
|
|
1988
|
+
const timer = setTimeout(() => settle(new ModbusError(ModbusErrorCode.TIMEOUT, 'Timeout')), timeout);
|
|
1989
|
+
// FIFO mode: clear stale buffer state before this request.
|
|
1990
|
+
// Concurrent mode: must not flush — other in-flight requests share the buffer.
|
|
1991
|
+
if (!this.concurrent) {
|
|
1992
|
+
this.applicationLayer.flush();
|
|
1993
|
+
}
|
|
1121
1994
|
this.physicalLayer
|
|
1122
|
-
.write(
|
|
1995
|
+
.write(payload)
|
|
1123
1996
|
.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
|
-
}
|
|
1997
|
+
if (settled) {
|
|
1998
|
+
return;
|
|
1145
1999
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
settled = true;
|
|
1150
|
-
clearTimeout(tid);
|
|
1151
|
-
this.applicationLayer.stopWaitingResponse();
|
|
1152
|
-
reject(error);
|
|
2000
|
+
if (broadcast) {
|
|
2001
|
+
settle(null);
|
|
2002
|
+
return;
|
|
1153
2003
|
}
|
|
1154
|
-
|
|
2004
|
+
const finalChecks = usesTid ? [(frame) => frame.transaction === tid, ...preCheck] : preCheck;
|
|
2005
|
+
this._masterSession.start(key, finalChecks, (error, frame) => settle(error, frame));
|
|
2006
|
+
})
|
|
2007
|
+
.catch((error) => settle(error));
|
|
1155
2008
|
});
|
|
1156
2009
|
}
|
|
1157
2010
|
writeFC1Or2(unit, fc, address, length, timeout) {
|
|
@@ -1159,104 +2012,60 @@ class ModbusMaster extends EventEmitter {
|
|
|
1159
2012
|
const bufferTx = Buffer.alloc(4);
|
|
1160
2013
|
bufferTx.writeUInt16BE(address, 0);
|
|
1161
2014
|
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) => {
|
|
2015
|
+
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
2016
|
if (frame) {
|
|
1173
|
-
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }
|
|
2017
|
+
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }, (_, index) => (frame.data[1 + ~~(index / 8)] & (1 << index % 8)) > 0) });
|
|
1174
2018
|
}
|
|
1175
2019
|
});
|
|
1176
2020
|
}
|
|
1177
2021
|
readCoils(unit, address, length, timeout = this.timeout) {
|
|
1178
|
-
return this.writeFC1Or2(unit,
|
|
2022
|
+
return this.writeFC1Or2(unit, FunctionCode.READ_COILS, address, length, timeout);
|
|
1179
2023
|
}
|
|
1180
2024
|
readDiscreteInputs(unit, address, length, timeout = this.timeout) {
|
|
1181
|
-
return this.writeFC1Or2(unit,
|
|
2025
|
+
return this.writeFC1Or2(unit, FunctionCode.READ_DISCRETE_INPUTS, address, length, timeout);
|
|
1182
2026
|
}
|
|
1183
2027
|
writeFC3Or4(unit, fc, address, length, timeout) {
|
|
1184
2028
|
const byteCount = length * 2;
|
|
1185
2029
|
const bufferTx = Buffer.alloc(4);
|
|
1186
2030
|
bufferTx.writeUInt16BE(address, 0);
|
|
1187
2031
|
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) => {
|
|
2032
|
+
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
2033
|
if (frame) {
|
|
1199
|
-
const bufferRx =
|
|
1200
|
-
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }
|
|
2034
|
+
const bufferRx = frame.data.subarray(1);
|
|
2035
|
+
return Object.assign(Object.assign({}, frame), { data: Array.from({ length }, (_, index) => bufferRx.readUInt16BE(index * 2)) });
|
|
1201
2036
|
}
|
|
1202
2037
|
});
|
|
1203
2038
|
}
|
|
1204
2039
|
readHoldingRegisters(unit, address, length, timeout = this.timeout) {
|
|
1205
|
-
return this.writeFC3Or4(unit,
|
|
2040
|
+
return this.writeFC3Or4(unit, FunctionCode.READ_HOLDING_REGISTERS, address, length, timeout);
|
|
1206
2041
|
}
|
|
1207
2042
|
readInputRegisters(unit, address, length, timeout = this.timeout) {
|
|
1208
|
-
return this.writeFC3Or4(unit,
|
|
2043
|
+
return this.writeFC3Or4(unit, FunctionCode.READ_INPUT_REGISTERS, address, length, timeout);
|
|
1209
2044
|
}
|
|
1210
2045
|
writeSingleCoil(unit, address, value, timeout = this.timeout) {
|
|
1211
|
-
const fc =
|
|
2046
|
+
const fc = FunctionCode.WRITE_SINGLE_COIL;
|
|
1212
2047
|
const bufferTx = Buffer.alloc(4);
|
|
1213
2048
|
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) => {
|
|
2049
|
+
bufferTx.writeUInt16BE(value ? COIL_ON : COIL_OFF, 2);
|
|
2050
|
+
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
2051
|
if (frame) {
|
|
1230
2052
|
return Object.assign(Object.assign({}, frame), { data: value });
|
|
1231
2053
|
}
|
|
1232
2054
|
});
|
|
1233
2055
|
}
|
|
1234
2056
|
writeSingleRegister(unit, address, value, timeout = this.timeout) {
|
|
1235
|
-
const fc =
|
|
2057
|
+
const fc = FunctionCode.WRITE_SINGLE_REGISTER;
|
|
1236
2058
|
const bufferTx = Buffer.alloc(4);
|
|
1237
2059
|
bufferTx.writeUInt16BE(address, 0);
|
|
1238
2060
|
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) => {
|
|
2061
|
+
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
2062
|
if (frame) {
|
|
1254
2063
|
return Object.assign(Object.assign({}, frame), { data: value });
|
|
1255
2064
|
}
|
|
1256
2065
|
});
|
|
1257
2066
|
}
|
|
1258
2067
|
writeMultipleCoils(unit, address, value, timeout = this.timeout) {
|
|
1259
|
-
const fc =
|
|
2068
|
+
const fc = FunctionCode.WRITE_MULTIPLE_COILS;
|
|
1260
2069
|
const byteCount = Math.ceil(value.length / 8);
|
|
1261
2070
|
const bufferTx = Buffer.alloc(5 + byteCount);
|
|
1262
2071
|
bufferTx.writeUInt16BE(address, 0);
|
|
@@ -1267,23 +2076,14 @@ class ModbusMaster extends EventEmitter {
|
|
|
1267
2076
|
bufferTx[5 + ~~(i / 8)] |= 1 << i % 8;
|
|
1268
2077
|
}
|
|
1269
2078
|
});
|
|
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) => {
|
|
2079
|
+
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
2080
|
if (frame) {
|
|
1281
2081
|
return Object.assign(Object.assign({}, frame), { data: value });
|
|
1282
2082
|
}
|
|
1283
2083
|
});
|
|
1284
2084
|
}
|
|
1285
2085
|
writeMultipleRegisters(unit, address, value, timeout = this.timeout) {
|
|
1286
|
-
const fc =
|
|
2086
|
+
const fc = FunctionCode.WRITE_MULTIPLE_REGISTERS;
|
|
1287
2087
|
const byteCount = value.length * 2;
|
|
1288
2088
|
const bufferTx = Buffer.alloc(5 + byteCount);
|
|
1289
2089
|
bufferTx.writeUInt16BE(address, 0);
|
|
@@ -1292,73 +2092,48 @@ class ModbusMaster extends EventEmitter {
|
|
|
1292
2092
|
value.forEach((v, i) => {
|
|
1293
2093
|
bufferTx.writeUInt16BE(v, 5 + i * 2);
|
|
1294
2094
|
});
|
|
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) => {
|
|
2095
|
+
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
2096
|
if (frame) {
|
|
1306
2097
|
return Object.assign(Object.assign({}, frame), { data: value });
|
|
1307
2098
|
}
|
|
1308
2099
|
});
|
|
1309
2100
|
}
|
|
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) => {
|
|
2101
|
+
reportServerId(unit, serverIdLength = 1, timeout = this.timeout) {
|
|
2102
|
+
const fc = FunctionCode.REPORT_SERVER_ID;
|
|
2103
|
+
return this.send({ unit, fc, data: Buffer.alloc(0) }, [
|
|
2104
|
+
(frame) => frame.unit === unit && frame.fc === fc,
|
|
2105
|
+
(frame) => {
|
|
2106
|
+
if (frame.data.length >= 2 + serverIdLength) {
|
|
2107
|
+
return 1 + frame.data[0];
|
|
2108
|
+
}
|
|
2109
|
+
},
|
|
2110
|
+
], timeout, unit === 0).then((frame) => {
|
|
1329
2111
|
if (frame) {
|
|
2112
|
+
const runStatusIndex = 1 + serverIdLength;
|
|
1330
2113
|
return Object.assign(Object.assign({}, frame), { data: {
|
|
1331
|
-
serverId: frame.data[1],
|
|
1332
|
-
runIndicatorStatus: frame.data[
|
|
1333
|
-
additionalData: frame.data.
|
|
2114
|
+
serverId: serverIdLength === 1 ? frame.data[1] : Array.from(frame.data.subarray(1, runStatusIndex)),
|
|
2115
|
+
runIndicatorStatus: frame.data[runStatusIndex] === 0xff,
|
|
2116
|
+
additionalData: Array.from(frame.data.subarray(runStatusIndex + 1)),
|
|
1334
2117
|
} });
|
|
1335
2118
|
}
|
|
1336
2119
|
});
|
|
1337
2120
|
}
|
|
1338
2121
|
maskWriteRegister(unit, address, andMask, orMask, timeout = this.timeout) {
|
|
1339
|
-
const fc =
|
|
2122
|
+
const fc = FunctionCode.MASK_WRITE_REGISTER;
|
|
1340
2123
|
const bufferTx = Buffer.alloc(6);
|
|
1341
2124
|
bufferTx.writeUInt16BE(address, 0);
|
|
1342
2125
|
bufferTx.writeUInt16BE(andMask, 2);
|
|
1343
2126
|
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) => {
|
|
2127
|
+
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
2128
|
if (frame) {
|
|
1355
2129
|
return Object.assign(Object.assign({}, frame), { data: { andMask, orMask } });
|
|
1356
2130
|
}
|
|
1357
2131
|
});
|
|
1358
2132
|
}
|
|
1359
2133
|
readAndWriteMultipleRegisters(unit, read, write, timeout = this.timeout) {
|
|
1360
|
-
const fc =
|
|
2134
|
+
const fc = FunctionCode.READ_WRITE_MULTIPLE_REGISTERS;
|
|
1361
2135
|
const byteCount = write.value.length * 2;
|
|
2136
|
+
const readByteCount = read.length * 2;
|
|
1362
2137
|
const bufferTx = Buffer.alloc(9 + byteCount);
|
|
1363
2138
|
bufferTx.writeUInt16BE(read.address, 0);
|
|
1364
2139
|
bufferTx.writeUInt16BE(read.length, 2);
|
|
@@ -1368,158 +2143,135 @@ class ModbusMaster extends EventEmitter {
|
|
|
1368
2143
|
write.value.forEach((v, i) => {
|
|
1369
2144
|
bufferTx.writeUInt16BE(v, 9 + i * 2);
|
|
1370
2145
|
});
|
|
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) => {
|
|
2146
|
+
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
2147
|
if (frame) {
|
|
1382
|
-
const bufferRx =
|
|
1383
|
-
return Object.assign(Object.assign({}, frame), { data: Array.from({ length: read.length }
|
|
2148
|
+
const bufferRx = frame.data.subarray(1);
|
|
2149
|
+
return Object.assign(Object.assign({}, frame), { data: Array.from({ length: read.length }, (_, index) => bufferRx.readUInt16BE(index * 2)) });
|
|
1384
2150
|
}
|
|
1385
2151
|
});
|
|
1386
2152
|
}
|
|
1387
2153
|
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;
|
|
2154
|
+
const fc = FunctionCode.READ_DEVICE_IDENTIFICATION;
|
|
2155
|
+
let parsed;
|
|
2156
|
+
return this.send({ unit, fc, data: Buffer.from([MEI_READ_DEVICE_ID, readDeviceIDCode, objectId]) }, [
|
|
2157
|
+
(frame) => frame.unit === unit && frame.fc === fc,
|
|
2158
|
+
(frame) => {
|
|
2159
|
+
if (frame.data.length >= 6) {
|
|
2160
|
+
if (frame.data[0] === MEI_READ_DEVICE_ID && frame.data[1] === readDeviceIDCode) {
|
|
2161
|
+
const objects = [];
|
|
2162
|
+
let object = [];
|
|
2163
|
+
let totalBytes = 0;
|
|
2164
|
+
for (const v of frame.data.subarray(6)) {
|
|
2165
|
+
switch (object.length) {
|
|
2166
|
+
case 0:
|
|
2167
|
+
case 1: {
|
|
2168
|
+
object.push(v);
|
|
2169
|
+
break;
|
|
2170
|
+
}
|
|
2171
|
+
case 2: {
|
|
2172
|
+
object.push([v]);
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
case 3: {
|
|
2176
|
+
object[2].push(v);
|
|
2177
|
+
if (object[1] === object[2].length) {
|
|
2178
|
+
objects.push({ id: object[0], value: Buffer.from(object[2]).toString() });
|
|
2179
|
+
totalBytes += 2 + object[1];
|
|
2180
|
+
object = [];
|
|
1422
2181
|
}
|
|
2182
|
+
break;
|
|
1423
2183
|
}
|
|
1424
2184
|
}
|
|
1425
|
-
if (objects.length === frame.data[5]) {
|
|
1426
|
-
return 6 + objects.reduce((previous, current) => previous + current, 0);
|
|
1427
|
-
}
|
|
1428
2185
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
2186
|
+
if (objects.length === frame.data[5]) {
|
|
2187
|
+
parsed = objects;
|
|
2188
|
+
return 6 + totalBytes;
|
|
1431
2189
|
}
|
|
1432
2190
|
}
|
|
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
|
-
}
|
|
2191
|
+
else {
|
|
2192
|
+
return false;
|
|
1458
2193
|
}
|
|
1459
2194
|
}
|
|
2195
|
+
},
|
|
2196
|
+
], timeout, unit === 0).then((frame) => {
|
|
2197
|
+
if (frame && parsed) {
|
|
1460
2198
|
return Object.assign(Object.assign({}, frame), { data: {
|
|
1461
2199
|
readDeviceIDCode,
|
|
1462
2200
|
conformityLevel: frame.data[2],
|
|
1463
2201
|
moreFollows: frame.data[3] === 0xff,
|
|
1464
2202
|
nextObjectId: frame.data[4],
|
|
1465
|
-
objects,
|
|
2203
|
+
objects: parsed,
|
|
1466
2204
|
} });
|
|
1467
2205
|
}
|
|
1468
2206
|
});
|
|
1469
2207
|
}
|
|
2208
|
+
addCustomFunctionCode(cfc) {
|
|
2209
|
+
this.applicationLayer.addCustomFunctionCode(cfc);
|
|
2210
|
+
}
|
|
2211
|
+
removeCustomFunctionCode(fc) {
|
|
2212
|
+
this.applicationLayer.removeCustomFunctionCode(fc);
|
|
2213
|
+
}
|
|
2214
|
+
sendCustomFC(unit, fc, data, timeout = this.timeout) {
|
|
2215
|
+
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
2216
|
+
return this.send({ unit, fc, data: payload }, [(frame) => frame.unit === unit && frame.fc === fc], timeout, unit === 0).then((frame) => {
|
|
2217
|
+
if (frame) {
|
|
2218
|
+
return frame.data;
|
|
2219
|
+
}
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
_clean(level) {
|
|
2223
|
+
if (this._cleanLevel === 'destroy') {
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (this._cleanLevel === 'close' && level === 'close') {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const errorCode = level === 'destroy' ? ModbusErrorCode.MASTER_DESTROYED : ModbusErrorCode.MASTER_CLOSED;
|
|
2230
|
+
const message = level === 'destroy' ? 'Master destroyed' : 'Master closed';
|
|
2231
|
+
this._closed = true;
|
|
2232
|
+
const queued = this._queue.splice(0);
|
|
2233
|
+
for (const item of queued) {
|
|
2234
|
+
item.cancel(new ModbusError(errorCode, message));
|
|
2235
|
+
}
|
|
2236
|
+
this._masterSession.stopAll(new ModbusError(errorCode, message));
|
|
2237
|
+
this._cleanLevel = level;
|
|
2238
|
+
}
|
|
1470
2239
|
open(...args) {
|
|
2240
|
+
if (this._cleanLevel === 'destroy') {
|
|
2241
|
+
return Promise.reject(new ModbusError(ModbusErrorCode.PORT_DESTROYED, 'Master is destroyed'));
|
|
2242
|
+
}
|
|
2243
|
+
this._cleanLevel = 'none';
|
|
2244
|
+
this._closed = false;
|
|
2245
|
+
this._nextTid = 1;
|
|
1471
2246
|
return this.physicalLayer.open(...args);
|
|
1472
2247
|
}
|
|
1473
2248
|
close() {
|
|
2249
|
+
if (this._cleanLevel === 'destroy') {
|
|
2250
|
+
return Promise.resolve();
|
|
2251
|
+
}
|
|
2252
|
+
this._clean('close');
|
|
1474
2253
|
return this.physicalLayer.close();
|
|
1475
2254
|
}
|
|
1476
2255
|
destroy() {
|
|
2256
|
+
if (this._cleanLevel === 'destroy') {
|
|
2257
|
+
return Promise.resolve();
|
|
2258
|
+
}
|
|
2259
|
+
this._clean('destroy');
|
|
1477
2260
|
this.removeAllListeners();
|
|
1478
2261
|
this.applicationLayer.destroy();
|
|
1479
2262
|
return this.physicalLayer.destroy();
|
|
1480
2263
|
}
|
|
1481
2264
|
}
|
|
1482
2265
|
|
|
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
2266
|
class ModbusSlave extends EventEmitter {
|
|
1516
2267
|
get isOpen() {
|
|
1517
2268
|
return this.physicalLayer.isOpen;
|
|
1518
2269
|
}
|
|
1519
2270
|
get destroyed() {
|
|
1520
|
-
return this.physicalLayer.destroyed;
|
|
2271
|
+
return this._cleanLevel === 'destroy' || this.physicalLayer.destroyed;
|
|
1521
2272
|
}
|
|
1522
|
-
constructor(applicationLayer, physicalLayer) {
|
|
2273
|
+
constructor(applicationLayer, physicalLayer, options = {}) {
|
|
2274
|
+
var _a;
|
|
1523
2275
|
super();
|
|
1524
2276
|
Object.defineProperty(this, "applicationLayer", {
|
|
1525
2277
|
enumerable: true,
|
|
@@ -1539,444 +2291,499 @@ class ModbusSlave extends EventEmitter {
|
|
|
1539
2291
|
writable: true,
|
|
1540
2292
|
value: new Map()
|
|
1541
2293
|
});
|
|
1542
|
-
Object.defineProperty(this, "
|
|
2294
|
+
Object.defineProperty(this, "concurrent", {
|
|
1543
2295
|
enumerable: true,
|
|
1544
2296
|
configurable: true,
|
|
1545
2297
|
writable: true,
|
|
1546
|
-
value:
|
|
2298
|
+
value: void 0
|
|
1547
2299
|
});
|
|
1548
|
-
Object.defineProperty(this, "
|
|
2300
|
+
Object.defineProperty(this, "_queues", {
|
|
1549
2301
|
enumerable: true,
|
|
1550
2302
|
configurable: true,
|
|
1551
2303
|
writable: true,
|
|
1552
|
-
value:
|
|
2304
|
+
value: new Map()
|
|
2305
|
+
});
|
|
2306
|
+
Object.defineProperty(this, "_customFunctionCodes", {
|
|
2307
|
+
enumerable: true,
|
|
2308
|
+
configurable: true,
|
|
2309
|
+
writable: true,
|
|
2310
|
+
value: new Map()
|
|
2311
|
+
});
|
|
2312
|
+
Object.defineProperty(this, "_locks", {
|
|
2313
|
+
enumerable: true,
|
|
2314
|
+
configurable: true,
|
|
2315
|
+
writable: true,
|
|
2316
|
+
value: new Map()
|
|
2317
|
+
});
|
|
2318
|
+
Object.defineProperty(this, "_cleanLevel", {
|
|
2319
|
+
enumerable: true,
|
|
2320
|
+
configurable: true,
|
|
2321
|
+
writable: true,
|
|
2322
|
+
value: 'none'
|
|
1553
2323
|
});
|
|
1554
|
-
|
|
2324
|
+
this.concurrent = (_a = options.concurrent) !== null && _a !== void 0 ? _a : false;
|
|
2325
|
+
if (this.concurrent && this.applicationLayer.PROTOCOL !== 'TCP') {
|
|
2326
|
+
throw new ModbusError(ModbusErrorCode.CONCURRENT_NOT_TCP, 'concurrent mode requires a Modbus TCP application layer');
|
|
2327
|
+
}
|
|
2328
|
+
this.applicationLayer.role = 'SLAVE';
|
|
2329
|
+
applicationLayer.on('framing', (frame, response, connection) => {
|
|
1555
2330
|
if (!(frame.unit === 0x00 || this.models.has(frame.unit))) {
|
|
1556
2331
|
return;
|
|
1557
2332
|
}
|
|
1558
|
-
this.
|
|
1559
|
-
|
|
2333
|
+
if (this.concurrent) {
|
|
2334
|
+
this._processFrame(frame, response).catch((error) => this.emit('error', error));
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
let q = this._queues.get(connection.id);
|
|
2338
|
+
if (!q) {
|
|
2339
|
+
q = { items: [], processing: false };
|
|
2340
|
+
this._queues.set(connection.id, q);
|
|
2341
|
+
}
|
|
2342
|
+
q.items.push({ frame, response });
|
|
2343
|
+
this._drain(connection.id, q);
|
|
1560
2344
|
});
|
|
1561
2345
|
physicalLayer.on('error', (error) => {
|
|
1562
2346
|
this.emit('error', error);
|
|
1563
2347
|
});
|
|
2348
|
+
physicalLayer.on('connection-close', (connection) => {
|
|
2349
|
+
const q = this._queues.get(connection.id);
|
|
2350
|
+
if (!q) {
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
q.items.length = 0;
|
|
2354
|
+
if (!q.processing) {
|
|
2355
|
+
this._queues.delete(connection.id);
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
1564
2358
|
physicalLayer.on('close', () => {
|
|
2359
|
+
for (const q of this._queues.values()) {
|
|
2360
|
+
q.items.length = 0;
|
|
2361
|
+
}
|
|
2362
|
+
this._queues.clear();
|
|
1565
2363
|
this.emit('close');
|
|
1566
2364
|
});
|
|
1567
2365
|
}
|
|
1568
2366
|
handleFC1(model, frame, response) {
|
|
1569
2367
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1570
2368
|
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));
|
|
2369
|
+
if (frame.data.length !== 4) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (!model.readCoils) {
|
|
2373
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2377
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2378
|
+
if (length < LIMITS.READ_COILS_MIN || length > LIMITS.READ_COILS_MAX) {
|
|
2379
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).coils)) {
|
|
2383
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
try {
|
|
2387
|
+
const coils = yield model.readCoils(address, length);
|
|
2388
|
+
const bufferTx = Buffer.alloc(Math.ceil(length / 8));
|
|
2389
|
+
coils.forEach((coil, index) => {
|
|
2390
|
+
if (coil) {
|
|
2391
|
+
bufferTx[Math.floor(index / 8)] |= 1 << index % 8;
|
|
1598
2392
|
}
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
2393
|
+
});
|
|
2394
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2395
|
+
}
|
|
2396
|
+
catch (error) {
|
|
2397
|
+
yield this.responseError(frame, response, error);
|
|
1603
2398
|
}
|
|
1604
2399
|
});
|
|
1605
2400
|
}
|
|
1606
2401
|
handleFC2(model, frame, response) {
|
|
1607
2402
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1608
2403
|
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));
|
|
2404
|
+
if (frame.data.length !== 4) {
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
if (!model.readDiscreteInputs) {
|
|
2408
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2412
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2413
|
+
if (length < LIMITS.READ_COILS_MIN || length > LIMITS.READ_COILS_MAX) {
|
|
2414
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).discreteInputs)) {
|
|
2418
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
try {
|
|
2422
|
+
const discreteInputs = yield model.readDiscreteInputs(address, length);
|
|
2423
|
+
const bufferTx = Buffer.alloc(Math.ceil(length / 8));
|
|
2424
|
+
discreteInputs.forEach((discreteInput, index) => {
|
|
2425
|
+
if (discreteInput) {
|
|
2426
|
+
bufferTx[Math.floor(index / 8)] |= 1 << index % 8;
|
|
1636
2427
|
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
2428
|
+
});
|
|
2429
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2430
|
+
}
|
|
2431
|
+
catch (error) {
|
|
2432
|
+
yield this.responseError(frame, response, error);
|
|
1641
2433
|
}
|
|
1642
2434
|
});
|
|
1643
2435
|
}
|
|
1644
2436
|
handleFC3(model, frame, response) {
|
|
1645
2437
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1646
2438
|
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
|
-
}
|
|
2439
|
+
if (frame.data.length !== 4) {
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
if (!model.readHoldingRegisters) {
|
|
2443
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2447
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2448
|
+
if (length < LIMITS.READ_REGISTERS_MIN || length > LIMITS.READ_REGISTERS_MAX) {
|
|
2449
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2453
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
try {
|
|
2457
|
+
const registers = yield model.readHoldingRegisters(address, length);
|
|
2458
|
+
const bufferTx = Buffer.alloc(length * 2);
|
|
2459
|
+
registers.forEach((register, index) => {
|
|
2460
|
+
bufferTx.writeUInt16BE(register, index * 2);
|
|
2461
|
+
});
|
|
2462
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2463
|
+
}
|
|
2464
|
+
catch (error) {
|
|
2465
|
+
yield this.responseError(frame, response, error);
|
|
1677
2466
|
}
|
|
1678
2467
|
});
|
|
1679
2468
|
}
|
|
1680
2469
|
handleFC4(model, frame, response) {
|
|
1681
2470
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1682
2471
|
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
|
-
}
|
|
2472
|
+
if (frame.data.length !== 4) {
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (!model.readInputRegisters) {
|
|
2476
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2480
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2481
|
+
if (length < LIMITS.READ_REGISTERS_MIN || length > LIMITS.READ_REGISTERS_MAX) {
|
|
2482
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).inputRegisters)) {
|
|
2486
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
try {
|
|
2490
|
+
const registers = yield model.readInputRegisters(address, length);
|
|
2491
|
+
const bufferTx = Buffer.alloc(length * 2);
|
|
2492
|
+
registers.forEach((register, index) => {
|
|
2493
|
+
bufferTx.writeUInt16BE(register, index * 2);
|
|
2494
|
+
});
|
|
2495
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2496
|
+
}
|
|
2497
|
+
catch (error) {
|
|
2498
|
+
yield this.responseError(frame, response, error);
|
|
1713
2499
|
}
|
|
1714
2500
|
});
|
|
1715
2501
|
}
|
|
1716
2502
|
handleFC5(model, frame, response) {
|
|
1717
2503
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1718
2504
|
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
|
-
}
|
|
2505
|
+
if (frame.data.length !== 4) {
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
if (!model.writeSingleCoil) {
|
|
2509
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2513
|
+
const value = (frame.data[2] << 8) | frame.data[3];
|
|
2514
|
+
if (value !== COIL_OFF && value !== COIL_ON) {
|
|
2515
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
if (!checkRange(address, (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).coils)) {
|
|
2519
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
try {
|
|
2523
|
+
yield model.writeSingleCoil(address, value === COIL_ON);
|
|
2524
|
+
yield response(this.applicationLayer.encode(frame));
|
|
2525
|
+
}
|
|
2526
|
+
catch (error) {
|
|
2527
|
+
yield this.responseError(frame, response, error);
|
|
1745
2528
|
}
|
|
1746
2529
|
});
|
|
1747
2530
|
}
|
|
1748
2531
|
handleFC6(model, frame, response) {
|
|
1749
2532
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1750
2533
|
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
|
-
}
|
|
2534
|
+
if (frame.data.length !== 4) {
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (!model.writeSingleRegister) {
|
|
2538
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2542
|
+
const value = (frame.data[2] << 8) | frame.data[3];
|
|
2543
|
+
if (!checkRange(address, (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2544
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
try {
|
|
2548
|
+
yield model.writeSingleRegister(address, value);
|
|
2549
|
+
yield response(this.applicationLayer.encode(frame));
|
|
2550
|
+
}
|
|
2551
|
+
catch (error) {
|
|
2552
|
+
yield this.responseError(frame, response, error);
|
|
1777
2553
|
}
|
|
1778
2554
|
});
|
|
1779
2555
|
}
|
|
1780
2556
|
handleFC15(model, frame, response) {
|
|
1781
2557
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1782
2558
|
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
|
-
}
|
|
2559
|
+
if (frame.data.length <= 5 || frame.data.length !== 5 + frame.data[4]) {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (!model.writeMultipleCoils && !model.writeSingleCoil) {
|
|
2563
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2567
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2568
|
+
const byteCount = frame.data[4];
|
|
2569
|
+
if (length < LIMITS.READ_COILS_MIN || length > LIMITS.WRITE_COILS_MAX || byteCount !== Math.ceil(length / 8)) {
|
|
2570
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).coils)) {
|
|
2574
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
const value = new Array(length);
|
|
2578
|
+
for (let i = 0; i < length; i++) {
|
|
2579
|
+
value[i] = (frame.data[5 + Math.floor(i / 8)] & (1 << i % 8)) > 0;
|
|
2580
|
+
}
|
|
2581
|
+
try {
|
|
2582
|
+
if (model.writeMultipleCoils) {
|
|
2583
|
+
yield model.writeMultipleCoils(address, value);
|
|
1809
2584
|
}
|
|
1810
2585
|
else {
|
|
1811
|
-
|
|
2586
|
+
for (let i = 0; i < value.length; i++) {
|
|
2587
|
+
yield model.writeSingleCoil(address + i, value[i]);
|
|
2588
|
+
}
|
|
1812
2589
|
}
|
|
2590
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: frame.data.subarray(0, 4) })));
|
|
2591
|
+
}
|
|
2592
|
+
catch (error) {
|
|
2593
|
+
yield this.responseError(frame, response, error);
|
|
1813
2594
|
}
|
|
1814
2595
|
});
|
|
1815
2596
|
}
|
|
1816
2597
|
handleFC16(model, frame, response) {
|
|
1817
2598
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1818
2599
|
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
|
-
}
|
|
2600
|
+
if (frame.data.length <= 5 || frame.data.length !== 5 + frame.data[4]) {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
if (!model.writeMultipleRegisters && !model.writeSingleRegister) {
|
|
2604
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2608
|
+
const length = (frame.data[2] << 8) | frame.data[3];
|
|
2609
|
+
const byteCount = frame.data[4];
|
|
2610
|
+
if (length < LIMITS.READ_REGISTERS_MIN || length > LIMITS.WRITE_REGISTERS_MAX || byteCount !== length * 2) {
|
|
2611
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
if (!checkRange([address, address + length], (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2615
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const value = new Array(length);
|
|
2619
|
+
for (let i = 0; i < length; i++) {
|
|
2620
|
+
value[i] = (frame.data[5 + i * 2] << 8) | frame.data[6 + i * 2];
|
|
2621
|
+
}
|
|
2622
|
+
try {
|
|
2623
|
+
if (model.writeMultipleRegisters) {
|
|
2624
|
+
yield model.writeMultipleRegisters(address, value);
|
|
1845
2625
|
}
|
|
1846
2626
|
else {
|
|
1847
|
-
|
|
2627
|
+
for (let i = 0; i < value.length; i++) {
|
|
2628
|
+
yield model.writeSingleRegister(address + i, value[i]);
|
|
2629
|
+
}
|
|
1848
2630
|
}
|
|
2631
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: frame.data.subarray(0, 4) })));
|
|
2632
|
+
}
|
|
2633
|
+
catch (error) {
|
|
2634
|
+
yield this.responseError(frame, response, error);
|
|
1849
2635
|
}
|
|
1850
2636
|
});
|
|
1851
2637
|
}
|
|
1852
2638
|
handleFC17(model, frame, response) {
|
|
1853
2639
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1854
2640
|
var _a;
|
|
1855
|
-
if (frame.data.length
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2641
|
+
if (frame.data.length !== 0) {
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
if (!model.reportServerId) {
|
|
2645
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
try {
|
|
2649
|
+
const { serverId = (_a = model.unit) !== null && _a !== void 0 ? _a : 1, runIndicatorStatus = true, additionalData = [] } = yield model.reportServerId();
|
|
2650
|
+
const serverIdBytes = Array.isArray(serverId) ? serverId : [serverId];
|
|
2651
|
+
const byteCount = serverIdBytes.length + 1 + additionalData.length;
|
|
2652
|
+
if (byteCount > 255) {
|
|
2653
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
2654
|
+
return;
|
|
1864
2655
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
2656
|
+
const allBytes = [...serverIdBytes, runIndicatorStatus ? 0xff : 0x00, ...additionalData];
|
|
2657
|
+
if (allBytes.some((b) => !isUint8(b))) {
|
|
2658
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
2659
|
+
return;
|
|
1867
2660
|
}
|
|
2661
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([byteCount]), Buffer.from(allBytes)]) })));
|
|
2662
|
+
}
|
|
2663
|
+
catch (error) {
|
|
2664
|
+
yield this.responseError(frame, response, error);
|
|
1868
2665
|
}
|
|
1869
2666
|
});
|
|
1870
2667
|
}
|
|
1871
2668
|
handleFC22(model, frame, response) {
|
|
1872
2669
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1873
2670
|
var _a;
|
|
1874
|
-
if (frame.data.length
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2671
|
+
if (frame.data.length !== 6) {
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
if (!model.maskWriteRegister && !(model.readHoldingRegisters && model.writeSingleRegister)) {
|
|
2675
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const address = (frame.data[0] << 8) | frame.data[1];
|
|
2679
|
+
const andMask = (frame.data[2] << 8) | frame.data[3];
|
|
2680
|
+
const orMask = (frame.data[4] << 8) | frame.data[5];
|
|
2681
|
+
if (!checkRange(address, (_a = model.getAddressRange) === null || _a === void 0 ? void 0 : _a.call(model).holdingRegisters)) {
|
|
2682
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
try {
|
|
2686
|
+
yield this.withAddressLock([address], () => __awaiter(this, void 0, void 0, function* () {
|
|
2687
|
+
if (model.maskWriteRegister) {
|
|
2688
|
+
yield model.maskWriteRegister(address, andMask, orMask);
|
|
1892
2689
|
}
|
|
1893
2690
|
else {
|
|
1894
|
-
yield
|
|
2691
|
+
const [value] = yield model.readHoldingRegisters(address, 1);
|
|
2692
|
+
yield model.writeSingleRegister(address, (value & andMask) | (orMask & (~andMask & 0xffff)));
|
|
1895
2693
|
}
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2694
|
+
}));
|
|
2695
|
+
yield response(this.applicationLayer.encode(frame));
|
|
2696
|
+
}
|
|
2697
|
+
catch (error) {
|
|
2698
|
+
yield this.responseError(frame, response, error);
|
|
1900
2699
|
}
|
|
1901
2700
|
});
|
|
1902
2701
|
}
|
|
1903
2702
|
handleFC23(model, frame, response) {
|
|
1904
2703
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1905
2704
|
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
|
-
|
|
2705
|
+
if (frame.data.length <= 9 || frame.data.length !== 9 + frame.data[8]) {
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
if (!model.readHoldingRegisters || (!model.writeMultipleRegisters && !model.writeSingleRegister)) {
|
|
2709
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
const address = {
|
|
2713
|
+
read: (frame.data[0] << 8) | frame.data[1],
|
|
2714
|
+
write: (frame.data[4] << 8) | frame.data[5],
|
|
2715
|
+
};
|
|
2716
|
+
const length = {
|
|
2717
|
+
read: (frame.data[2] << 8) | frame.data[3],
|
|
2718
|
+
write: (frame.data[6] << 8) | frame.data[7],
|
|
2719
|
+
};
|
|
2720
|
+
const byteCount = frame.data[8];
|
|
2721
|
+
if (length.read < LIMITS.READ_REGISTERS_MIN ||
|
|
2722
|
+
length.read > LIMITS.READ_REGISTERS_MAX ||
|
|
2723
|
+
length.write < LIMITS.READ_REGISTERS_MIN ||
|
|
2724
|
+
length.write > LIMITS.RW_REGISTERS_WRITE_MAX ||
|
|
2725
|
+
byteCount !== length.write * 2) {
|
|
2726
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_VALUE));
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
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)) {
|
|
2730
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
const value = new Array(length.write);
|
|
2734
|
+
for (let i = 0; i < length.write; i++) {
|
|
2735
|
+
value[i] = (frame.data[9 + i * 2] << 8) | frame.data[10 + i * 2];
|
|
2736
|
+
}
|
|
2737
|
+
try {
|
|
2738
|
+
const writeAddresses = Array.from({ length: length.write }, (_, i) => address.write + i);
|
|
2739
|
+
yield this.withAddressLock(writeAddresses, () => __awaiter(this, void 0, void 0, function* () {
|
|
2740
|
+
if (model.writeMultipleRegisters) {
|
|
2741
|
+
yield model.writeMultipleRegisters(address.write, value);
|
|
1943
2742
|
}
|
|
1944
2743
|
else {
|
|
1945
|
-
|
|
2744
|
+
for (let i = 0; i < value.length; i++) {
|
|
2745
|
+
yield model.writeSingleRegister(address.write + i, value[i]);
|
|
2746
|
+
}
|
|
1946
2747
|
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2748
|
+
}));
|
|
2749
|
+
const registers = yield model.readHoldingRegisters(address.read, length.read);
|
|
2750
|
+
const bufferTx = Buffer.alloc(length.read * 2);
|
|
2751
|
+
registers.forEach((register, index) => {
|
|
2752
|
+
bufferTx.writeUInt16BE(register, index * 2);
|
|
2753
|
+
});
|
|
2754
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat([Buffer.from([bufferTx.length]), bufferTx]) })));
|
|
2755
|
+
}
|
|
2756
|
+
catch (error) {
|
|
2757
|
+
yield this.responseError(frame, response, error);
|
|
1951
2758
|
}
|
|
1952
2759
|
});
|
|
1953
2760
|
}
|
|
1954
2761
|
handleFC43_14(model, frame, response) {
|
|
1955
2762
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1956
2763
|
if (frame.data.length === 3) {
|
|
1957
|
-
if (frame.data[0] ===
|
|
2764
|
+
if (frame.data[0] === MEI_READ_DEVICE_ID && model.readDeviceIdentification) {
|
|
1958
2765
|
const readDeviceIDCode = frame.data[1];
|
|
1959
2766
|
let objectID = frame.data[2];
|
|
1960
2767
|
switch (readDeviceIDCode) {
|
|
1961
|
-
case
|
|
2768
|
+
case ReadDeviceIDCode.BASIC_STREAM: {
|
|
1962
2769
|
if (objectID > 0x02 || (objectID > 0x06 && objectID < 0x80)) {
|
|
1963
2770
|
objectID = 0x00;
|
|
1964
2771
|
}
|
|
1965
2772
|
break;
|
|
1966
2773
|
}
|
|
1967
|
-
case
|
|
2774
|
+
case ReadDeviceIDCode.REGULAR_STREAM: {
|
|
1968
2775
|
if (objectID >= 0x80 || (objectID > 0x06 && objectID < 0x80)) {
|
|
1969
2776
|
objectID = 0x00;
|
|
1970
2777
|
}
|
|
1971
2778
|
break;
|
|
1972
2779
|
}
|
|
1973
|
-
case
|
|
2780
|
+
case ReadDeviceIDCode.EXTENDED_STREAM: {
|
|
1974
2781
|
if (objectID > 0x06 && objectID < 0x80) {
|
|
1975
2782
|
objectID = 0x00;
|
|
1976
2783
|
}
|
|
1977
2784
|
break;
|
|
1978
2785
|
}
|
|
1979
|
-
case
|
|
2786
|
+
case ReadDeviceIDCode.SPECIFIC_ACCESS: {
|
|
1980
2787
|
if (objectID > 0x06 && objectID < 0x80) {
|
|
1981
2788
|
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
1982
2789
|
return;
|
|
@@ -1989,70 +2796,74 @@ class ModbusSlave extends EventEmitter {
|
|
|
1989
2796
|
}
|
|
1990
2797
|
}
|
|
1991
2798
|
try {
|
|
1992
|
-
const identification = yield
|
|
1993
|
-
const objects = new Map(
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2799
|
+
const identification = yield model.readDeviceIdentification();
|
|
2800
|
+
const objects = new Map();
|
|
2801
|
+
for (const key of Object.keys(identification)) {
|
|
2802
|
+
const id = Number(key);
|
|
2803
|
+
if (id >= 0 && id <= 255) {
|
|
2804
|
+
objects.set(id, identification[id]);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
for (let id = 0x00; id <= 0x02; id++) {
|
|
2808
|
+
if (!objects.has(id)) {
|
|
2809
|
+
objects.set(id, 'null');
|
|
2002
2810
|
}
|
|
2003
2811
|
}
|
|
2004
2812
|
if (!objects.has(objectID)) {
|
|
2005
|
-
if (readDeviceIDCode ===
|
|
2813
|
+
if (readDeviceIDCode === ReadDeviceIDCode.SPECIFIC_ACCESS) {
|
|
2006
2814
|
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_DATA_ADDRESS));
|
|
2007
2815
|
return;
|
|
2008
2816
|
}
|
|
2009
2817
|
objectID = 0x00;
|
|
2010
2818
|
}
|
|
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) {
|
|
2819
|
+
let maxId = 0;
|
|
2820
|
+
for (const id of objects.keys()) {
|
|
2821
|
+
if ((id >= 0x07 && id <= 0x7f) || id > 0xff) {
|
|
2017
2822
|
yield this.responseError(frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
2018
2823
|
return;
|
|
2019
2824
|
}
|
|
2020
|
-
if (id >
|
|
2021
|
-
|
|
2022
|
-
}
|
|
2023
|
-
if (id > 0x80) {
|
|
2024
|
-
conformityLevel = 0x83;
|
|
2825
|
+
if (id > maxId) {
|
|
2826
|
+
maxId = id;
|
|
2025
2827
|
}
|
|
2828
|
+
}
|
|
2829
|
+
const conformityLevel = maxId >= 0x80 ? ConformityLevel.EXTENDED : maxId > 0x02 ? ConformityLevel.REGULAR : ConformityLevel.BASIC;
|
|
2830
|
+
const ids = [];
|
|
2831
|
+
let totalLength = 10;
|
|
2832
|
+
let lastID = 0;
|
|
2833
|
+
for (const [id, value] of objects.entries()) {
|
|
2026
2834
|
if (objectID > id) {
|
|
2027
2835
|
continue;
|
|
2028
2836
|
}
|
|
2029
|
-
|
|
2837
|
+
const byteLength = Buffer.byteLength(value);
|
|
2838
|
+
if (byteLength > 245) {
|
|
2030
2839
|
yield this.responseError(frame, response, getErrorByCode(ErrorCode.SERVER_DEVICE_FAILURE));
|
|
2031
2840
|
return;
|
|
2032
2841
|
}
|
|
2033
2842
|
if (lastID !== 0) {
|
|
2034
2843
|
continue;
|
|
2035
2844
|
}
|
|
2036
|
-
if (
|
|
2845
|
+
if (byteLength + 2 > 253 - totalLength) {
|
|
2037
2846
|
if (lastID === 0) {
|
|
2038
2847
|
lastID = id;
|
|
2039
2848
|
}
|
|
2040
2849
|
}
|
|
2041
2850
|
else {
|
|
2042
|
-
totalLength +=
|
|
2851
|
+
totalLength += byteLength + 2;
|
|
2043
2852
|
ids.push(id);
|
|
2044
|
-
if (readDeviceIDCode ===
|
|
2853
|
+
if (readDeviceIDCode === ReadDeviceIDCode.SPECIFIC_ACCESS) {
|
|
2045
2854
|
break;
|
|
2046
2855
|
}
|
|
2047
2856
|
}
|
|
2048
2857
|
}
|
|
2049
2858
|
ids.sort((a, b) => a - b);
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2859
|
+
const parts = [];
|
|
2860
|
+
parts.push(Buffer.from([MEI_READ_DEVICE_ID, readDeviceIDCode, conformityLevel, lastID === 0 ? 0x00 : 0xff, lastID, ids.length]));
|
|
2861
|
+
for (const id of ids) {
|
|
2862
|
+
const value = objects.get(id);
|
|
2863
|
+
parts.push(Buffer.from([id, Buffer.byteLength(value)]));
|
|
2864
|
+
parts.push(Buffer.from(value));
|
|
2865
|
+
}
|
|
2866
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: Buffer.concat(parts) })));
|
|
2056
2867
|
}
|
|
2057
2868
|
catch (error) {
|
|
2058
2869
|
yield this.responseError(frame, response, error);
|
|
@@ -2066,26 +2877,32 @@ class ModbusSlave extends EventEmitter {
|
|
|
2066
2877
|
}
|
|
2067
2878
|
responseError(frame, response, error) {
|
|
2068
2879
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2069
|
-
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { fc: frame.fc |
|
|
2880
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { fc: frame.fc | EXCEPTION_OFFSET, data: Buffer.from([getCodeByError(error)]) })));
|
|
2070
2881
|
});
|
|
2071
2882
|
}
|
|
2072
|
-
_drain() {
|
|
2883
|
+
_drain(key, q) {
|
|
2073
2884
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2074
|
-
if (
|
|
2885
|
+
if (q.processing) {
|
|
2075
2886
|
return;
|
|
2076
2887
|
}
|
|
2077
|
-
|
|
2078
|
-
const { frame, response: _response } = this._queue.shift();
|
|
2888
|
+
q.processing = true;
|
|
2079
2889
|
try {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2890
|
+
while (q.items.length > 0) {
|
|
2891
|
+
const { frame, response } = q.items.shift();
|
|
2892
|
+
try {
|
|
2893
|
+
yield this._processFrame(frame, response);
|
|
2894
|
+
}
|
|
2895
|
+
catch (error) {
|
|
2896
|
+
this.emit('error', error);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2084
2899
|
}
|
|
2085
2900
|
finally {
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2901
|
+
q.processing = false;
|
|
2902
|
+
// Cleanup empty queue entries so the map doesn't grow unbounded across
|
|
2903
|
+
// ephemeral connections (UDP rinfo, brief TCP clients).
|
|
2904
|
+
if (q.items.length === 0 && this._queues.get(key) === q) {
|
|
2905
|
+
this._queues.delete(key);
|
|
2089
2906
|
}
|
|
2090
2907
|
}
|
|
2091
2908
|
});
|
|
@@ -2102,8 +2919,8 @@ class ModbusSlave extends EventEmitter {
|
|
|
2102
2919
|
catch (error) { }
|
|
2103
2920
|
});
|
|
2104
2921
|
for (const model of frame.unit === 0x00 ? this.models.values() : [this.models.get(frame.unit)]) {
|
|
2105
|
-
const
|
|
2106
|
-
if (
|
|
2922
|
+
const intercepted = yield this._intercept(model, frame, response);
|
|
2923
|
+
if (!intercepted) {
|
|
2107
2924
|
yield this._handleFC(model, frame, response);
|
|
2108
2925
|
}
|
|
2109
2926
|
}
|
|
@@ -2111,17 +2928,42 @@ class ModbusSlave extends EventEmitter {
|
|
|
2111
2928
|
}
|
|
2112
2929
|
_intercept(model, frame, response) {
|
|
2113
2930
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2114
|
-
if (model.interceptor) {
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
}
|
|
2931
|
+
if (!model.interceptor) {
|
|
2932
|
+
return false;
|
|
2933
|
+
}
|
|
2934
|
+
try {
|
|
2935
|
+
const data = yield model.interceptor(frame.fc, frame.data);
|
|
2936
|
+
if (data !== undefined) {
|
|
2937
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data })));
|
|
2938
|
+
return true;
|
|
2121
2939
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2940
|
+
return false;
|
|
2941
|
+
}
|
|
2942
|
+
catch (error) {
|
|
2943
|
+
yield this.responseError(frame, response, error);
|
|
2944
|
+
return true;
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
withAddressLock(addresses, fn) {
|
|
2949
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2950
|
+
const sorted = [...new Set(addresses)].sort((a, b) => a - b);
|
|
2951
|
+
const previous = sorted.map((addr) => { var _a; return (_a = this._locks.get(addr)) !== null && _a !== void 0 ? _a : Promise.resolve(); });
|
|
2952
|
+
const work = Promise.all(previous).then(() => fn());
|
|
2953
|
+
const cleanup = work.catch(() => {
|
|
2954
|
+
/* ignore */
|
|
2955
|
+
});
|
|
2956
|
+
for (const addr of sorted) {
|
|
2957
|
+
this._locks.set(addr, cleanup);
|
|
2958
|
+
}
|
|
2959
|
+
try {
|
|
2960
|
+
return yield work;
|
|
2961
|
+
}
|
|
2962
|
+
finally {
|
|
2963
|
+
for (const addr of sorted) {
|
|
2964
|
+
if (this._locks.get(addr) === cleanup) {
|
|
2965
|
+
this._locks.delete(addr);
|
|
2966
|
+
}
|
|
2125
2967
|
}
|
|
2126
2968
|
}
|
|
2127
2969
|
});
|
|
@@ -2129,56 +2971,68 @@ class ModbusSlave extends EventEmitter {
|
|
|
2129
2971
|
_handleFC(model, frame, response) {
|
|
2130
2972
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2131
2973
|
switch (frame.fc) {
|
|
2132
|
-
case
|
|
2974
|
+
case FunctionCode.READ_COILS: {
|
|
2133
2975
|
yield this.handleFC1(model, frame, response);
|
|
2134
2976
|
break;
|
|
2135
2977
|
}
|
|
2136
|
-
case
|
|
2978
|
+
case FunctionCode.READ_DISCRETE_INPUTS: {
|
|
2137
2979
|
yield this.handleFC2(model, frame, response);
|
|
2138
2980
|
break;
|
|
2139
2981
|
}
|
|
2140
|
-
case
|
|
2982
|
+
case FunctionCode.READ_HOLDING_REGISTERS: {
|
|
2141
2983
|
yield this.handleFC3(model, frame, response);
|
|
2142
2984
|
break;
|
|
2143
2985
|
}
|
|
2144
|
-
case
|
|
2986
|
+
case FunctionCode.READ_INPUT_REGISTERS: {
|
|
2145
2987
|
yield this.handleFC4(model, frame, response);
|
|
2146
2988
|
break;
|
|
2147
2989
|
}
|
|
2148
|
-
case
|
|
2990
|
+
case FunctionCode.WRITE_SINGLE_COIL: {
|
|
2149
2991
|
yield this.handleFC5(model, frame, response);
|
|
2150
2992
|
break;
|
|
2151
2993
|
}
|
|
2152
|
-
case
|
|
2994
|
+
case FunctionCode.WRITE_SINGLE_REGISTER: {
|
|
2153
2995
|
yield this.handleFC6(model, frame, response);
|
|
2154
2996
|
break;
|
|
2155
2997
|
}
|
|
2156
|
-
case
|
|
2998
|
+
case FunctionCode.WRITE_MULTIPLE_COILS: {
|
|
2157
2999
|
yield this.handleFC15(model, frame, response);
|
|
2158
3000
|
break;
|
|
2159
3001
|
}
|
|
2160
|
-
case
|
|
3002
|
+
case FunctionCode.WRITE_MULTIPLE_REGISTERS: {
|
|
2161
3003
|
yield this.handleFC16(model, frame, response);
|
|
2162
3004
|
break;
|
|
2163
3005
|
}
|
|
2164
|
-
case
|
|
3006
|
+
case FunctionCode.REPORT_SERVER_ID: {
|
|
2165
3007
|
yield this.handleFC17(model, frame, response);
|
|
2166
3008
|
break;
|
|
2167
3009
|
}
|
|
2168
|
-
case
|
|
3010
|
+
case FunctionCode.MASK_WRITE_REGISTER: {
|
|
2169
3011
|
yield this.handleFC22(model, frame, response);
|
|
2170
3012
|
break;
|
|
2171
3013
|
}
|
|
2172
|
-
case
|
|
3014
|
+
case FunctionCode.READ_WRITE_MULTIPLE_REGISTERS: {
|
|
2173
3015
|
yield this.handleFC23(model, frame, response);
|
|
2174
3016
|
break;
|
|
2175
3017
|
}
|
|
2176
|
-
case
|
|
3018
|
+
case FunctionCode.READ_DEVICE_IDENTIFICATION: {
|
|
2177
3019
|
yield this.handleFC43_14(model, frame, response);
|
|
2178
3020
|
break;
|
|
2179
3021
|
}
|
|
2180
3022
|
default: {
|
|
2181
|
-
|
|
3023
|
+
const cfc = this._customFunctionCodes.get(frame.fc);
|
|
3024
|
+
if (cfc === null || cfc === void 0 ? void 0 : cfc.handle) {
|
|
3025
|
+
try {
|
|
3026
|
+
const responseData = yield cfc.handle(frame.data, frame.unit);
|
|
3027
|
+
yield response(this.applicationLayer.encode(Object.assign(Object.assign({}, frame), { data: responseData })));
|
|
3028
|
+
}
|
|
3029
|
+
catch (error) {
|
|
3030
|
+
yield this.responseError(frame, response, error);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
else {
|
|
3034
|
+
yield this.responseError(frame, response, getErrorByCode(ErrorCode.ILLEGAL_FUNCTION));
|
|
3035
|
+
}
|
|
2182
3036
|
break;
|
|
2183
3037
|
}
|
|
2184
3038
|
}
|
|
@@ -2191,17 +3045,52 @@ class ModbusSlave extends EventEmitter {
|
|
|
2191
3045
|
remove(unit) {
|
|
2192
3046
|
this.models.delete(unit);
|
|
2193
3047
|
}
|
|
3048
|
+
addCustomFunctionCode(cfc) {
|
|
3049
|
+
this._customFunctionCodes.set(cfc.fc, cfc);
|
|
3050
|
+
this.applicationLayer.addCustomFunctionCode(cfc);
|
|
3051
|
+
}
|
|
3052
|
+
removeCustomFunctionCode(fc) {
|
|
3053
|
+
this._customFunctionCodes.delete(fc);
|
|
3054
|
+
this.applicationLayer.removeCustomFunctionCode(fc);
|
|
3055
|
+
}
|
|
3056
|
+
_clean(level) {
|
|
3057
|
+
if (this._cleanLevel === 'destroy') {
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
if (this._cleanLevel === 'close' && level === 'close') {
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
for (const q of this._queues.values()) {
|
|
3064
|
+
q.items.length = 0;
|
|
3065
|
+
}
|
|
3066
|
+
this._queues.clear();
|
|
3067
|
+
this._locks.clear();
|
|
3068
|
+
if (level === 'destroy') {
|
|
3069
|
+
this._customFunctionCodes.clear();
|
|
3070
|
+
this.models.clear();
|
|
3071
|
+
}
|
|
3072
|
+
this._cleanLevel = level;
|
|
3073
|
+
}
|
|
2194
3074
|
open(...args) {
|
|
3075
|
+
this._cleanLevel = 'none';
|
|
2195
3076
|
return this.physicalLayer.open(...args);
|
|
2196
3077
|
}
|
|
2197
3078
|
close() {
|
|
3079
|
+
if (this._cleanLevel === 'destroy') {
|
|
3080
|
+
return Promise.resolve();
|
|
3081
|
+
}
|
|
3082
|
+
this._clean('close');
|
|
2198
3083
|
return this.physicalLayer.close();
|
|
2199
3084
|
}
|
|
2200
3085
|
destroy() {
|
|
3086
|
+
if (this._cleanLevel === 'destroy') {
|
|
3087
|
+
return Promise.resolve();
|
|
3088
|
+
}
|
|
3089
|
+
this._clean('destroy');
|
|
2201
3090
|
this.removeAllListeners();
|
|
2202
3091
|
this.applicationLayer.destroy();
|
|
2203
3092
|
return this.physicalLayer.destroy();
|
|
2204
3093
|
}
|
|
2205
3094
|
}
|
|
2206
3095
|
|
|
2207
|
-
export { AbstractApplicationLayer, AbstractPhysicalLayer, AsciiApplicationLayer, ErrorCode, ModbusMaster, ModbusSlave, RtuApplicationLayer, SerialPhysicalLayer, TcpApplicationLayer, TcpClientPhysicalLayer, TcpServerPhysicalLayer, UdpPhysicalLayer, getCodeByError, getErrorByCode };
|
|
3096
|
+
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 };
|