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