@fa_yoshinobu/node-red-contrib-plc-comm-slmp 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/docsrc/assets/README.md +10 -0
- package/docsrc/assets/node-red-slmp.png +0 -0
- package/docsrc/index.md +11 -0
- package/docsrc/maintainer/ARCHITECTURE.md +36 -0
- package/docsrc/user/USER_GUIDE.md +238 -0
- package/docsrc/user/toc.yml +2 -0
- package/docsrc/validation/reports/README.md +15 -0
- package/examples/flows/README.md +24 -0
- package/examples/flows/slmp-array-string.json +185 -0
- package/examples/flows/slmp-basic-read-write.json +185 -0
- package/examples/flows/slmp-control-error.json +211 -0
- package/examples/flows/slmp-demo.json +260 -0
- package/examples/flows/slmp-device-matrix.json +514 -0
- package/examples/flows/slmp-routing.json +118 -0
- package/examples/flows/slmp-udp-read-write.json +185 -0
- package/lib/index.js +6 -0
- package/lib/slmp/client.js +642 -0
- package/lib/slmp/constants.js +121 -0
- package/lib/slmp/core.js +406 -0
- package/lib/slmp/errors.js +21 -0
- package/lib/slmp/high-level.js +911 -0
- package/lib/slmp/index.js +10 -0
- package/nodes/slmp-connection.html +142 -0
- package/nodes/slmp-connection.js +78 -0
- package/nodes/slmp-read.html +274 -0
- package/nodes/slmp-read.js +207 -0
- package/nodes/slmp-write.html +267 -0
- package/nodes/slmp-write.js +275 -0
- package/package.json +53 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const FRAME_3E_REQUEST_SUBHEADER = Buffer.from([0x50, 0x00]);
|
|
4
|
+
const FRAME_3E_RESPONSE_SUBHEADER = Buffer.from([0xd0, 0x00]);
|
|
5
|
+
const FRAME_4E_REQUEST_SUBHEADER = Buffer.from([0x54, 0x00]);
|
|
6
|
+
const FRAME_4E_RESPONSE_SUBHEADER = Buffer.from([0xd4, 0x00]);
|
|
7
|
+
|
|
8
|
+
const FrameType = Object.freeze({
|
|
9
|
+
FRAME_3E: "3e",
|
|
10
|
+
FRAME_4E: "4e",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const PLCSeries = Object.freeze({
|
|
14
|
+
QL: "ql",
|
|
15
|
+
IQR: "iqr",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const DeviceUnit = Object.freeze({
|
|
19
|
+
BIT: "bit",
|
|
20
|
+
WORD: "word",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const ModuleIONo = Object.freeze({
|
|
24
|
+
OWN_STATION: 0x03ff,
|
|
25
|
+
CONTROL_CPU: 0x03ff,
|
|
26
|
+
MULTIPLE_CPU_1: 0x03e0,
|
|
27
|
+
MULTIPLE_CPU_2: 0x03e1,
|
|
28
|
+
MULTIPLE_CPU_3: 0x03e2,
|
|
29
|
+
MULTIPLE_CPU_4: 0x03e3,
|
|
30
|
+
CONTROL_SYSTEM_CPU: 0x03d0,
|
|
31
|
+
STANDBY_SYSTEM_CPU: 0x03d1,
|
|
32
|
+
SYSTEM_A_CPU: 0x03d2,
|
|
33
|
+
SYSTEM_B_CPU: 0x03d3,
|
|
34
|
+
REMOTE_HEAD_1: 0x03e0,
|
|
35
|
+
REMOTE_HEAD_2: 0x03e1,
|
|
36
|
+
CONTROL_SYSTEM_REMOTE_HEAD: 0x03d0,
|
|
37
|
+
STANDBY_SYSTEM_REMOTE_HEAD: 0x03d1,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const Command = Object.freeze({
|
|
41
|
+
DEVICE_READ: 0x0401,
|
|
42
|
+
DEVICE_WRITE: 0x1401,
|
|
43
|
+
DEVICE_READ_RANDOM: 0x0403,
|
|
44
|
+
DEVICE_WRITE_RANDOM: 0x1402,
|
|
45
|
+
READ_TYPE_NAME: 0x0101,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const SUBCOMMAND_DEVICE_WORD_QL = 0x0000;
|
|
49
|
+
const SUBCOMMAND_DEVICE_BIT_QL = 0x0001;
|
|
50
|
+
const SUBCOMMAND_DEVICE_WORD_IQR = 0x0002;
|
|
51
|
+
const SUBCOMMAND_DEVICE_BIT_IQR = 0x0003;
|
|
52
|
+
|
|
53
|
+
const SUBCOMMAND_DEVICE_WORD_QL_EXT = 0x0080;
|
|
54
|
+
const SUBCOMMAND_DEVICE_BIT_QL_EXT = 0x0081;
|
|
55
|
+
const SUBCOMMAND_DEVICE_WORD_IQR_EXT = 0x0082;
|
|
56
|
+
const SUBCOMMAND_DEVICE_BIT_IQR_EXT = 0x0083;
|
|
57
|
+
|
|
58
|
+
const DEVICE_CODES = Object.freeze({
|
|
59
|
+
SM: { code: 0x0091, radix: 10, unit: DeviceUnit.BIT },
|
|
60
|
+
SD: { code: 0x00a9, radix: 10, unit: DeviceUnit.WORD },
|
|
61
|
+
X: { code: 0x009c, radix: 16, unit: DeviceUnit.BIT },
|
|
62
|
+
Y: { code: 0x009d, radix: 16, unit: DeviceUnit.BIT },
|
|
63
|
+
M: { code: 0x0090, radix: 10, unit: DeviceUnit.BIT },
|
|
64
|
+
L: { code: 0x0092, radix: 10, unit: DeviceUnit.BIT },
|
|
65
|
+
F: { code: 0x0093, radix: 10, unit: DeviceUnit.BIT },
|
|
66
|
+
V: { code: 0x0094, radix: 10, unit: DeviceUnit.BIT },
|
|
67
|
+
B: { code: 0x00a0, radix: 16, unit: DeviceUnit.BIT },
|
|
68
|
+
D: { code: 0x00a8, radix: 10, unit: DeviceUnit.WORD },
|
|
69
|
+
W: { code: 0x00b4, radix: 16, unit: DeviceUnit.WORD },
|
|
70
|
+
TS: { code: 0x00c1, radix: 10, unit: DeviceUnit.BIT },
|
|
71
|
+
TC: { code: 0x00c0, radix: 10, unit: DeviceUnit.BIT },
|
|
72
|
+
TN: { code: 0x00c2, radix: 10, unit: DeviceUnit.WORD },
|
|
73
|
+
LTS: { code: 0x0051, radix: 10, unit: DeviceUnit.BIT },
|
|
74
|
+
LTC: { code: 0x0050, radix: 10, unit: DeviceUnit.BIT },
|
|
75
|
+
LTN: { code: 0x0052, radix: 10, unit: DeviceUnit.WORD },
|
|
76
|
+
STS: { code: 0x00c7, radix: 10, unit: DeviceUnit.BIT },
|
|
77
|
+
STC: { code: 0x00c6, radix: 10, unit: DeviceUnit.BIT },
|
|
78
|
+
STN: { code: 0x00c8, radix: 10, unit: DeviceUnit.WORD },
|
|
79
|
+
LSTS: { code: 0x0059, radix: 10, unit: DeviceUnit.BIT },
|
|
80
|
+
LSTC: { code: 0x0058, radix: 10, unit: DeviceUnit.BIT },
|
|
81
|
+
LSTN: { code: 0x005a, radix: 10, unit: DeviceUnit.WORD },
|
|
82
|
+
CS: { code: 0x00c4, radix: 10, unit: DeviceUnit.BIT },
|
|
83
|
+
CC: { code: 0x00c3, radix: 10, unit: DeviceUnit.BIT },
|
|
84
|
+
CN: { code: 0x00c5, radix: 10, unit: DeviceUnit.WORD },
|
|
85
|
+
LCS: { code: 0x0055, radix: 10, unit: DeviceUnit.BIT },
|
|
86
|
+
LCC: { code: 0x0054, radix: 10, unit: DeviceUnit.BIT },
|
|
87
|
+
LCN: { code: 0x0056, radix: 10, unit: DeviceUnit.WORD },
|
|
88
|
+
SB: { code: 0x00a1, radix: 16, unit: DeviceUnit.BIT },
|
|
89
|
+
SW: { code: 0x00b5, radix: 16, unit: DeviceUnit.WORD },
|
|
90
|
+
DX: { code: 0x00a2, radix: 16, unit: DeviceUnit.BIT },
|
|
91
|
+
DY: { code: 0x00a3, radix: 16, unit: DeviceUnit.BIT },
|
|
92
|
+
Z: { code: 0x00cc, radix: 10, unit: DeviceUnit.WORD },
|
|
93
|
+
LZ: { code: 0x0062, radix: 10, unit: DeviceUnit.WORD },
|
|
94
|
+
R: { code: 0x00af, radix: 10, unit: DeviceUnit.WORD },
|
|
95
|
+
ZR: { code: 0x00b0, radix: 10, unit: DeviceUnit.WORD },
|
|
96
|
+
RD: { code: 0x002c, radix: 10, unit: DeviceUnit.WORD },
|
|
97
|
+
G: { code: 0x00ab, radix: 10, unit: DeviceUnit.WORD },
|
|
98
|
+
HG: { code: 0x002e, radix: 10, unit: DeviceUnit.WORD },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
Command,
|
|
103
|
+
DEVICE_CODES,
|
|
104
|
+
DeviceUnit,
|
|
105
|
+
FRAME_3E_REQUEST_SUBHEADER,
|
|
106
|
+
FRAME_3E_RESPONSE_SUBHEADER,
|
|
107
|
+
FRAME_4E_REQUEST_SUBHEADER,
|
|
108
|
+
FRAME_4E_RESPONSE_SUBHEADER,
|
|
109
|
+
FrameType,
|
|
110
|
+
ModuleIONo,
|
|
111
|
+
PLCSeries,
|
|
112
|
+
SUBCOMMAND_DEVICE_BIT_IQR,
|
|
113
|
+
SUBCOMMAND_DEVICE_BIT_IQR_EXT,
|
|
114
|
+
SUBCOMMAND_DEVICE_BIT_QL,
|
|
115
|
+
SUBCOMMAND_DEVICE_BIT_QL_EXT,
|
|
116
|
+
SUBCOMMAND_DEVICE_WORD_IQR,
|
|
117
|
+
SUBCOMMAND_DEVICE_WORD_IQR_EXT,
|
|
118
|
+
SUBCOMMAND_DEVICE_WORD_QL,
|
|
119
|
+
SUBCOMMAND_DEVICE_WORD_QL_EXT,
|
|
120
|
+
};
|
|
121
|
+
|
package/lib/slmp/core.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
DEVICE_CODES,
|
|
5
|
+
FRAME_3E_REQUEST_SUBHEADER,
|
|
6
|
+
FRAME_3E_RESPONSE_SUBHEADER,
|
|
7
|
+
FRAME_4E_REQUEST_SUBHEADER,
|
|
8
|
+
FRAME_4E_RESPONSE_SUBHEADER,
|
|
9
|
+
FrameType,
|
|
10
|
+
ModuleIONo,
|
|
11
|
+
PLCSeries,
|
|
12
|
+
SUBCOMMAND_DEVICE_BIT_IQR,
|
|
13
|
+
SUBCOMMAND_DEVICE_BIT_IQR_EXT,
|
|
14
|
+
SUBCOMMAND_DEVICE_BIT_QL,
|
|
15
|
+
SUBCOMMAND_DEVICE_BIT_QL_EXT,
|
|
16
|
+
SUBCOMMAND_DEVICE_WORD_IQR,
|
|
17
|
+
SUBCOMMAND_DEVICE_WORD_IQR_EXT,
|
|
18
|
+
SUBCOMMAND_DEVICE_WORD_QL,
|
|
19
|
+
SUBCOMMAND_DEVICE_WORD_QL_EXT,
|
|
20
|
+
} = require("./constants");
|
|
21
|
+
const { SlmpError } = require("./errors");
|
|
22
|
+
|
|
23
|
+
function normalizeFrameType(value) {
|
|
24
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
25
|
+
if (normalized === FrameType.FRAME_3E || normalized === FrameType.FRAME_4E) {
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
throw new ValueError(`Unsupported frame type: ${value}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizePlcSeries(value) {
|
|
32
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
33
|
+
if (normalized === PLCSeries.QL || normalized === PLCSeries.IQR) {
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
throw new ValueError(`Unsupported PLC series: ${value}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeTransport(value) {
|
|
40
|
+
const normalized = String(value || "tcp").trim().toLowerCase();
|
|
41
|
+
if (normalized === "tcp" || normalized === "udp") {
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
throw new ValueError(`Unsupported transport: ${value}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseNumber(value, name, options = {}) {
|
|
48
|
+
const { defaultValue, base = 10 } = options;
|
|
49
|
+
if (value === undefined || value === null || value === "") {
|
|
50
|
+
if (defaultValue !== undefined) {
|
|
51
|
+
return defaultValue;
|
|
52
|
+
}
|
|
53
|
+
throw new ValueError(`${name} is required`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof value === "number") {
|
|
57
|
+
if (!Number.isFinite(value)) {
|
|
58
|
+
throw new ValueError(`${name} must be finite`);
|
|
59
|
+
}
|
|
60
|
+
return Math.trunc(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const text = String(value).trim();
|
|
64
|
+
if (!text) {
|
|
65
|
+
if (defaultValue !== undefined) {
|
|
66
|
+
return defaultValue;
|
|
67
|
+
}
|
|
68
|
+
throw new ValueError(`${name} is required`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let radix = base;
|
|
72
|
+
let normalized = text;
|
|
73
|
+
if (/^0x/i.test(text)) {
|
|
74
|
+
radix = 16;
|
|
75
|
+
normalized = text.slice(2);
|
|
76
|
+
} else if (/[a-f]/i.test(text)) {
|
|
77
|
+
radix = 16;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parsed = Number.parseInt(normalized, radix);
|
|
81
|
+
if (!Number.isFinite(parsed)) {
|
|
82
|
+
throw new ValueError(`${name} must be numeric: ${value}`);
|
|
83
|
+
}
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ensureRange(value, min, max, name) {
|
|
88
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
89
|
+
throw new ValueError(`${name} out of range (${min}..${max}): ${value}`);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeTarget(target = {}) {
|
|
95
|
+
const moduleIOSource = target.moduleIO ?? target.module_io ?? ModuleIONo.OWN_STATION;
|
|
96
|
+
return {
|
|
97
|
+
network: ensureRange(parseNumber(target.network, "target.network", { defaultValue: 0x00 }), 0, 0xff, "target.network"),
|
|
98
|
+
station: ensureRange(parseNumber(target.station, "target.station", { defaultValue: 0xff }), 0, 0xff, "target.station"),
|
|
99
|
+
moduleIO: ensureRange(
|
|
100
|
+
parseNumber(moduleIOSource, "target.moduleIO", { defaultValue: ModuleIONo.OWN_STATION, base: 16 }),
|
|
101
|
+
0,
|
|
102
|
+
0xffff,
|
|
103
|
+
"target.moduleIO"
|
|
104
|
+
),
|
|
105
|
+
multidrop: ensureRange(parseNumber(target.multidrop, "target.multidrop", { defaultValue: 0x00 }), 0, 0xff, "target.multidrop"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseDevice(value) {
|
|
110
|
+
if (value && typeof value === "object" && typeof value.code === "string" && Number.isInteger(value.number)) {
|
|
111
|
+
return { code: value.code.toUpperCase(), number: value.number };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const text = String(value || "").trim().toUpperCase();
|
|
115
|
+
const match = /^([A-Z]+)([0-9A-F]+)$/.exec(text);
|
|
116
|
+
if (!match) {
|
|
117
|
+
throw new ValueError(
|
|
118
|
+
`Invalid SLMP device string ${JSON.stringify(value)}. Expected <DeviceCode><Number> such as D100 or X1F.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const [, code, numberText] = match;
|
|
123
|
+
const deviceCode = DEVICE_CODES[code];
|
|
124
|
+
if (!deviceCode) {
|
|
125
|
+
throw new ValueError(`Unknown SLMP device code '${code}' in ${JSON.stringify(value)}`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
code,
|
|
129
|
+
number: Number.parseInt(numberText, deviceCode.radix),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function deviceToString(value) {
|
|
134
|
+
const ref = parseDevice(value);
|
|
135
|
+
const info = DEVICE_CODES[ref.code];
|
|
136
|
+
if (!info) {
|
|
137
|
+
return `${ref.code}${ref.number}`;
|
|
138
|
+
}
|
|
139
|
+
if (info.radix === 16) {
|
|
140
|
+
return `${ref.code}${ref.number.toString(16).toUpperCase()}`;
|
|
141
|
+
}
|
|
142
|
+
return `${ref.code}${ref.number}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function encode3ERequest({ target, monitoringTimer, command, subcommand, data = Buffer.alloc(0) }) {
|
|
146
|
+
const normalizedTarget = normalizeTarget(target);
|
|
147
|
+
const payload = Buffer.from(data);
|
|
148
|
+
ensureRange(monitoringTimer, 0, 0xffff, "monitoringTimer");
|
|
149
|
+
ensureRange(command, 0, 0xffff, "command");
|
|
150
|
+
ensureRange(subcommand, 0, 0xffff, "subcommand");
|
|
151
|
+
|
|
152
|
+
const reqLength = 2 + 2 + 2 + payload.length;
|
|
153
|
+
ensureRange(reqLength, 0, 0xffff, "requestDataLength");
|
|
154
|
+
|
|
155
|
+
const header = Buffer.alloc(11);
|
|
156
|
+
FRAME_3E_REQUEST_SUBHEADER.copy(header, 0);
|
|
157
|
+
header.writeUInt8(normalizedTarget.network, 2);
|
|
158
|
+
header.writeUInt8(normalizedTarget.station, 3);
|
|
159
|
+
header.writeUInt16LE(normalizedTarget.moduleIO, 4);
|
|
160
|
+
header.writeUInt8(normalizedTarget.multidrop, 6);
|
|
161
|
+
header.writeUInt16LE(reqLength, 7);
|
|
162
|
+
header.writeUInt16LE(monitoringTimer, 9);
|
|
163
|
+
|
|
164
|
+
const commandBuffer = Buffer.alloc(4);
|
|
165
|
+
commandBuffer.writeUInt16LE(command, 0);
|
|
166
|
+
commandBuffer.writeUInt16LE(subcommand, 2);
|
|
167
|
+
return Buffer.concat([header, commandBuffer, payload]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function encode4ERequest({ serial, target, monitoringTimer, command, subcommand, data = Buffer.alloc(0) }) {
|
|
171
|
+
const normalizedTarget = normalizeTarget(target);
|
|
172
|
+
const payload = Buffer.from(data);
|
|
173
|
+
ensureRange(serial, 0, 0xffff, "serial");
|
|
174
|
+
ensureRange(monitoringTimer, 0, 0xffff, "monitoringTimer");
|
|
175
|
+
ensureRange(command, 0, 0xffff, "command");
|
|
176
|
+
ensureRange(subcommand, 0, 0xffff, "subcommand");
|
|
177
|
+
|
|
178
|
+
const reqLength = 2 + 2 + 2 + payload.length;
|
|
179
|
+
ensureRange(reqLength, 0, 0xffff, "requestDataLength");
|
|
180
|
+
|
|
181
|
+
const header = Buffer.alloc(15);
|
|
182
|
+
FRAME_4E_REQUEST_SUBHEADER.copy(header, 0);
|
|
183
|
+
header.writeUInt16LE(serial, 2);
|
|
184
|
+
header.writeUInt16LE(0x0000, 4);
|
|
185
|
+
header.writeUInt8(normalizedTarget.network, 6);
|
|
186
|
+
header.writeUInt8(normalizedTarget.station, 7);
|
|
187
|
+
header.writeUInt16LE(normalizedTarget.moduleIO, 8);
|
|
188
|
+
header.writeUInt8(normalizedTarget.multidrop, 10);
|
|
189
|
+
header.writeUInt16LE(reqLength, 11);
|
|
190
|
+
header.writeUInt16LE(monitoringTimer, 13);
|
|
191
|
+
|
|
192
|
+
const commandBuffer = Buffer.alloc(4);
|
|
193
|
+
commandBuffer.writeUInt16LE(command, 0);
|
|
194
|
+
commandBuffer.writeUInt16LE(subcommand, 2);
|
|
195
|
+
return Buffer.concat([header, commandBuffer, payload]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function encodeRequest({ frameType, serial, target, monitoringTimer, command, subcommand, data = Buffer.alloc(0) }) {
|
|
199
|
+
const normalizedFrameType = normalizeFrameType(frameType);
|
|
200
|
+
if (normalizedFrameType === FrameType.FRAME_3E) {
|
|
201
|
+
return encode3ERequest({ target, monitoringTimer, command, subcommand, data });
|
|
202
|
+
}
|
|
203
|
+
return encode4ERequest({ serial, target, monitoringTimer, command, subcommand, data });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function decode3EResponse(frame) {
|
|
207
|
+
const buffer = Buffer.from(frame);
|
|
208
|
+
if (buffer.length < 11) {
|
|
209
|
+
throw new SlmpError(`response too short: ${buffer.length} bytes`);
|
|
210
|
+
}
|
|
211
|
+
if (!buffer.subarray(0, 2).equals(FRAME_3E_RESPONSE_SUBHEADER)) {
|
|
212
|
+
throw new SlmpError(`unexpected 3E response subheader: ${buffer.subarray(0, 2).toString("hex")}`);
|
|
213
|
+
}
|
|
214
|
+
const responseDataLength = buffer.readUInt16LE(7);
|
|
215
|
+
if (buffer.length !== 9 + responseDataLength) {
|
|
216
|
+
throw new SlmpError(
|
|
217
|
+
`response size mismatch: actual=${buffer.length}, expected=${9 + responseDataLength}, responseDataLength=${responseDataLength}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
serial: 0,
|
|
222
|
+
target: normalizeTarget({
|
|
223
|
+
network: buffer.readUInt8(2),
|
|
224
|
+
station: buffer.readUInt8(3),
|
|
225
|
+
moduleIO: buffer.readUInt16LE(4),
|
|
226
|
+
multidrop: buffer.readUInt8(6),
|
|
227
|
+
}),
|
|
228
|
+
endCode: buffer.readUInt16LE(9),
|
|
229
|
+
data: buffer.subarray(11),
|
|
230
|
+
raw: buffer,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function decode4EResponse(frame) {
|
|
235
|
+
const buffer = Buffer.from(frame);
|
|
236
|
+
if (buffer.length < 15) {
|
|
237
|
+
throw new SlmpError(`response too short: ${buffer.length} bytes`);
|
|
238
|
+
}
|
|
239
|
+
if (!buffer.subarray(0, 2).equals(FRAME_4E_RESPONSE_SUBHEADER)) {
|
|
240
|
+
throw new SlmpError(`unexpected 4E response subheader: ${buffer.subarray(0, 2).toString("hex")}`);
|
|
241
|
+
}
|
|
242
|
+
const responseDataLength = buffer.readUInt16LE(11);
|
|
243
|
+
if (buffer.length !== 13 + responseDataLength) {
|
|
244
|
+
throw new SlmpError(
|
|
245
|
+
`response size mismatch: actual=${buffer.length}, expected=${13 + responseDataLength}, responseDataLength=${responseDataLength}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
serial: buffer.readUInt16LE(2),
|
|
250
|
+
target: normalizeTarget({
|
|
251
|
+
network: buffer.readUInt8(6),
|
|
252
|
+
station: buffer.readUInt8(7),
|
|
253
|
+
moduleIO: buffer.readUInt16LE(8),
|
|
254
|
+
multidrop: buffer.readUInt8(10),
|
|
255
|
+
}),
|
|
256
|
+
endCode: buffer.readUInt16LE(13),
|
|
257
|
+
data: buffer.subarray(15),
|
|
258
|
+
raw: buffer,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function decodeResponse(frame, options) {
|
|
263
|
+
const normalizedFrameType = normalizeFrameType(options.frameType);
|
|
264
|
+
if (normalizedFrameType === FrameType.FRAME_3E) {
|
|
265
|
+
return decode3EResponse(frame);
|
|
266
|
+
}
|
|
267
|
+
return decode4EResponse(frame);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveDeviceSubcommand({ bitUnit, series, extension = false }) {
|
|
271
|
+
const normalizedSeries = normalizePlcSeries(series);
|
|
272
|
+
if (extension) {
|
|
273
|
+
if (normalizedSeries === PLCSeries.QL) {
|
|
274
|
+
return bitUnit ? SUBCOMMAND_DEVICE_BIT_QL_EXT : SUBCOMMAND_DEVICE_WORD_QL_EXT;
|
|
275
|
+
}
|
|
276
|
+
return bitUnit ? SUBCOMMAND_DEVICE_BIT_IQR_EXT : SUBCOMMAND_DEVICE_WORD_IQR_EXT;
|
|
277
|
+
}
|
|
278
|
+
if (normalizedSeries === PLCSeries.QL) {
|
|
279
|
+
return bitUnit ? SUBCOMMAND_DEVICE_BIT_QL : SUBCOMMAND_DEVICE_WORD_QL;
|
|
280
|
+
}
|
|
281
|
+
return bitUnit ? SUBCOMMAND_DEVICE_BIT_IQR : SUBCOMMAND_DEVICE_WORD_IQR;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function encodeDeviceSpec(device, options) {
|
|
285
|
+
const ref = parseDevice(device);
|
|
286
|
+
const info = DEVICE_CODES[ref.code];
|
|
287
|
+
const normalizedSeries = normalizePlcSeries(options.series);
|
|
288
|
+
if (!info) {
|
|
289
|
+
throw new ValueError(`Unknown SLMP device code '${ref.code}'`);
|
|
290
|
+
}
|
|
291
|
+
if (ref.code === "R" && ref.number > 32767) {
|
|
292
|
+
throw new ValueError(`R device number out of supported range (0..32767): ${ref.number}`);
|
|
293
|
+
}
|
|
294
|
+
if (normalizedSeries === PLCSeries.QL) {
|
|
295
|
+
ensureRange(ref.number, 0, 0xffffff, "device.number");
|
|
296
|
+
const buffer = Buffer.alloc(4);
|
|
297
|
+
buffer.writeUIntLE(ref.number, 0, 3);
|
|
298
|
+
buffer.writeUInt8(info.code & 0xff, 3);
|
|
299
|
+
return buffer;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
ensureRange(ref.number, 0, 0xffffffff, "device.number");
|
|
303
|
+
const buffer = Buffer.alloc(6);
|
|
304
|
+
buffer.writeUInt32LE(ref.number, 0);
|
|
305
|
+
buffer.writeUInt16LE(info.code, 4);
|
|
306
|
+
return buffer;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function decodeDeviceWords(data) {
|
|
310
|
+
const buffer = Buffer.from(data);
|
|
311
|
+
if (buffer.length % 2 !== 0) {
|
|
312
|
+
throw new SlmpError(`word data length must be even: ${buffer.length}`);
|
|
313
|
+
}
|
|
314
|
+
const values = [];
|
|
315
|
+
for (let index = 0; index < buffer.length; index += 2) {
|
|
316
|
+
values.push(buffer.readUInt16LE(index));
|
|
317
|
+
}
|
|
318
|
+
return values;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function decodeDeviceDwords(data) {
|
|
322
|
+
const buffer = Buffer.from(data);
|
|
323
|
+
if (buffer.length % 4 !== 0) {
|
|
324
|
+
throw new SlmpError(`dword data length must be multiple of 4: ${buffer.length}`);
|
|
325
|
+
}
|
|
326
|
+
const values = [];
|
|
327
|
+
for (let index = 0; index < buffer.length; index += 4) {
|
|
328
|
+
values.push(buffer.readUInt32LE(index));
|
|
329
|
+
}
|
|
330
|
+
return values;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function packBitValues(values) {
|
|
334
|
+
const bits = Array.from(values, (value) => (value ? 1 : 0));
|
|
335
|
+
const packed = [];
|
|
336
|
+
for (let index = 0; index < bits.length; index += 2) {
|
|
337
|
+
const hi = bits[index] & 0x1;
|
|
338
|
+
const lo = index + 1 < bits.length ? bits[index + 1] & 0x1 : 0;
|
|
339
|
+
packed.push((hi << 4) | lo);
|
|
340
|
+
}
|
|
341
|
+
return Buffer.from(packed);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function unpackBitValues(data, count) {
|
|
345
|
+
const buffer = Buffer.from(data);
|
|
346
|
+
const values = [];
|
|
347
|
+
for (const byte of buffer) {
|
|
348
|
+
values.push(Boolean((byte >> 4) & 0x1));
|
|
349
|
+
if (values.length >= count) {
|
|
350
|
+
return values;
|
|
351
|
+
}
|
|
352
|
+
values.push(Boolean(byte & 0x1));
|
|
353
|
+
if (values.length >= count) {
|
|
354
|
+
return values;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (values.length !== count) {
|
|
358
|
+
throw new SlmpError(`bit data too short: needed ${count}, got ${values.length}`);
|
|
359
|
+
}
|
|
360
|
+
return values;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function extractFrameFromBuffer(buffer, options) {
|
|
364
|
+
const normalizedFrameType = normalizeFrameType(options.frameType);
|
|
365
|
+
const source = Buffer.from(buffer);
|
|
366
|
+
const headSize = normalizedFrameType === FrameType.FRAME_4E ? 13 : 9;
|
|
367
|
+
if (source.length < headSize) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const responseDataLength = source.readUInt16LE(headSize - 2);
|
|
371
|
+
const totalLength = headSize + responseDataLength;
|
|
372
|
+
if (source.length < totalLength) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
frame: source.subarray(0, totalLength),
|
|
377
|
+
rest: source.subarray(totalLength),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
class ValueError extends Error {
|
|
382
|
+
constructor(message) {
|
|
383
|
+
super(message);
|
|
384
|
+
this.name = "ValueError";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {
|
|
389
|
+
ValueError,
|
|
390
|
+
decodeDeviceDwords,
|
|
391
|
+
decodeDeviceWords,
|
|
392
|
+
decodeResponse,
|
|
393
|
+
deviceToString,
|
|
394
|
+
encodeDeviceSpec,
|
|
395
|
+
encodeRequest,
|
|
396
|
+
extractFrameFromBuffer,
|
|
397
|
+
normalizeFrameType,
|
|
398
|
+
normalizePlcSeries,
|
|
399
|
+
normalizeTarget,
|
|
400
|
+
normalizeTransport,
|
|
401
|
+
packBitValues,
|
|
402
|
+
parseDevice,
|
|
403
|
+
parseNumber,
|
|
404
|
+
resolveDeviceSubcommand,
|
|
405
|
+
unpackBitValues,
|
|
406
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
class SlmpError extends Error {
|
|
4
|
+
constructor(message, options = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "SlmpError";
|
|
7
|
+
if (options.endCode !== undefined) {
|
|
8
|
+
this.endCode = options.endCode;
|
|
9
|
+
}
|
|
10
|
+
if (options.data !== undefined) {
|
|
11
|
+
this.data = options.data;
|
|
12
|
+
}
|
|
13
|
+
if (options.cause !== undefined) {
|
|
14
|
+
this.cause = options.cause;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
SlmpError,
|
|
21
|
+
};
|