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