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