@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,911 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { DEVICE_CODES, DeviceUnit } = require("./constants");
|
|
4
|
+
const { ValueError, deviceToString, parseDevice } = require("./core");
|
|
5
|
+
|
|
6
|
+
const WORD_DTYPES = new Set(["U", "S"]);
|
|
7
|
+
const DWORD_DTYPES = new Set(["D", "L", "F"]);
|
|
8
|
+
const STRING_DTYPES = new Set(["STR", "STRING"]);
|
|
9
|
+
const UNBATCHED_DEVICE_CODES = new Set(["G", "HG"]);
|
|
10
|
+
const DEFAULT_DWORD_DEVICE_CODES = new Set(["LTN", "LSTN", "LCN"]);
|
|
11
|
+
const LEGACY_STRING_DEVICE_CODES = Object.keys(DEVICE_CODES).sort((left, right) => right.length - left.length);
|
|
12
|
+
const ADDRESS_LIST_TOKEN_RE = /[A-Z][A-Z0-9]*(?:\.[0-9A-F]+|:[A-Z]+)?(?:,\d+)?/iy;
|
|
13
|
+
const LONG_TIMER_READ_FAMILIES = Object.freeze({
|
|
14
|
+
LTN: { baseCode: "LTN", role: "current" },
|
|
15
|
+
LTS: { baseCode: "LTN", role: "contact" },
|
|
16
|
+
LTC: { baseCode: "LTN", role: "coil" },
|
|
17
|
+
LSTN: { baseCode: "LSTN", role: "current" },
|
|
18
|
+
LSTS: { baseCode: "LSTN", role: "contact" },
|
|
19
|
+
LSTC: { baseCode: "LSTN", role: "coil" },
|
|
20
|
+
LCN: { baseCode: "LCN", role: "current" },
|
|
21
|
+
LCS: { baseCode: "LCN", role: "contact" },
|
|
22
|
+
LCC: { baseCode: "LCN", role: "coil" },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function readTyped(client, device, dtype, options = {}) {
|
|
26
|
+
const key = canonicalizeDtype(dtype || "U");
|
|
27
|
+
if (isStringDtype(key)) {
|
|
28
|
+
throw new ValueError("String reads require readNamed with '<device>:STR,<length>' or '<device>STR<number>,<length>'.");
|
|
29
|
+
}
|
|
30
|
+
const resolvedDevice = typeof device === "string" ? parseDevice(device) : device;
|
|
31
|
+
const longTimerRead = getLongTimerReadAccess(resolvedDevice.code);
|
|
32
|
+
if (longTimerRead) {
|
|
33
|
+
return readLongTimerScalar(client, resolvedDevice, key, longTimerRead, options);
|
|
34
|
+
}
|
|
35
|
+
if (key === "BIT") {
|
|
36
|
+
const values = await client.readDevices(resolvedDevice, 1, { ...options, bitUnit: true });
|
|
37
|
+
return Boolean(values[0]);
|
|
38
|
+
}
|
|
39
|
+
if (isDwordDtype(key)) {
|
|
40
|
+
const words = await client.readDevices(resolvedDevice, 2, { ...options, bitUnit: false });
|
|
41
|
+
return decodeDwordWords(words, 0, key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const words = await client.readDevices(resolvedDevice, 1, { ...options, bitUnit: false });
|
|
45
|
+
return decodeWordValue(words[0], key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function writeTyped(client, device, dtype, value, options = {}) {
|
|
49
|
+
const key = canonicalizeDtype(dtype || "U");
|
|
50
|
+
if (isStringDtype(key)) {
|
|
51
|
+
throw new ValueError("String writes require writeNamed with '<device>:STR,<length>' or '<device>STR<number>,<length>'.");
|
|
52
|
+
}
|
|
53
|
+
if (key === "BIT") {
|
|
54
|
+
await client.writeDevices(device, [Boolean(value)], { ...options, bitUnit: true });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await client.writeDevices(device, encodeWriteWords(key, value), { ...options, bitUnit: false });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readBits(client, device, count, options = {}) {
|
|
61
|
+
const values = await client.readDevices(device, count, { ...options, bitUnit: true });
|
|
62
|
+
return values.map((value) => Boolean(value));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function writeBits(client, device, values, options = {}) {
|
|
66
|
+
await client.writeDevices(device, Array.from(values || [], (value) => Boolean(value)), { ...options, bitUnit: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function writeBitInWord(client, device, bitIndex, value, options = {}) {
|
|
70
|
+
if (!Number.isInteger(bitIndex) || bitIndex < 0 || bitIndex > 15) {
|
|
71
|
+
throw new ValueError(`bitIndex must be 0-15, got ${bitIndex}`);
|
|
72
|
+
}
|
|
73
|
+
const words = await client.readDevices(device, 1, { ...options, bitUnit: false });
|
|
74
|
+
let current = Number(words[0]) & 0xffff;
|
|
75
|
+
if (value) {
|
|
76
|
+
current |= 1 << bitIndex;
|
|
77
|
+
} else {
|
|
78
|
+
current &= ~(1 << bitIndex);
|
|
79
|
+
}
|
|
80
|
+
await client.writeDevices(device, [current & 0xffff], { ...options, bitUnit: false });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function canonicalizeDtype(dtype) {
|
|
84
|
+
const key = String(dtype || "U").trim().toUpperCase();
|
|
85
|
+
return key === "STRING" ? "STR" : key;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function addressHasExplicitDtype(address) {
|
|
89
|
+
const text = String(address || "").trim();
|
|
90
|
+
const countMatch = /^(.*?),\s*(\d+)$/.exec(text);
|
|
91
|
+
const core = countMatch ? countMatch[1].trim() : text;
|
|
92
|
+
return core.includes(":");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isStringDtype(dtype) {
|
|
96
|
+
return STRING_DTYPES.has(canonicalizeDtype(dtype));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parsePositiveCount(value, address) {
|
|
100
|
+
const parsed = Number.parseInt(String(value).trim(), 10);
|
|
101
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
102
|
+
throw new ValueError(`Address '${address}' has an invalid count: ${value}`);
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseLegacyStringAddress(text) {
|
|
108
|
+
const normalized = String(text || "").trim().toUpperCase();
|
|
109
|
+
for (const code of LEGACY_STRING_DEVICE_CODES) {
|
|
110
|
+
const prefix = `${code}STR`;
|
|
111
|
+
if (!normalized.startsWith(prefix)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const match = /^([0-9A-F]+)\s*,\s*(\d+)$/.exec(normalized.slice(prefix.length));
|
|
115
|
+
if (!match) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
base: `${code}${match[1]}`,
|
|
120
|
+
dtype: "STR",
|
|
121
|
+
bitIndex: null,
|
|
122
|
+
count: parsePositiveCount(match[2], text),
|
|
123
|
+
hasCount: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseAddress(address) {
|
|
130
|
+
const text = String(address || "").trim();
|
|
131
|
+
const legacyString = parseLegacyStringAddress(text);
|
|
132
|
+
if (legacyString) {
|
|
133
|
+
return legacyString;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let core = text;
|
|
137
|
+
let count = 1;
|
|
138
|
+
let hasCount = false;
|
|
139
|
+
const countMatch = /^(.*?),\s*(\d+)$/.exec(text);
|
|
140
|
+
if (countMatch) {
|
|
141
|
+
core = countMatch[1].trim();
|
|
142
|
+
count = parsePositiveCount(countMatch[2], text);
|
|
143
|
+
hasCount = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (core.includes(":")) {
|
|
147
|
+
const [base, dtype] = core.split(":", 2);
|
|
148
|
+
return { base: base.trim(), dtype: canonicalizeDtype(dtype), bitIndex: null, count, hasCount };
|
|
149
|
+
}
|
|
150
|
+
if (core.includes(".")) {
|
|
151
|
+
const [base, bitText] = core.split(".", 2);
|
|
152
|
+
const parsed = Number.parseInt(bitText, 16);
|
|
153
|
+
if (!Number.isNaN(parsed)) {
|
|
154
|
+
return { base: base.trim(), dtype: "BIT_IN_WORD", bitIndex: parsed, count, hasCount };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { base: core, dtype: "U", bitIndex: null, count, hasCount };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeDtypeForDevice(device, dtype) {
|
|
161
|
+
const info = DEVICE_CODES[device.code];
|
|
162
|
+
const key = canonicalizeDtype(dtype);
|
|
163
|
+
if (info && info.unit === DeviceUnit.BIT && key === "U") {
|
|
164
|
+
return "BIT";
|
|
165
|
+
}
|
|
166
|
+
return key;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveEntryDtype(address, device, parsed) {
|
|
170
|
+
const normalized = normalizeDtypeForDevice(device, parsed.dtype || "U");
|
|
171
|
+
if (!addressHasExplicitDtype(address) && parsed.bitIndex == null && DEFAULT_DWORD_DEVICE_CODES.has(device.code)) {
|
|
172
|
+
return "D";
|
|
173
|
+
}
|
|
174
|
+
return normalized;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getLongTimerReadAccess(deviceCode) {
|
|
178
|
+
return LONG_TIMER_READ_FAMILIES[deviceCode] || null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validateBitInWordTarget(address, device) {
|
|
182
|
+
const info = DEVICE_CODES[device.code];
|
|
183
|
+
if (!info || info.unit !== DeviceUnit.WORD) {
|
|
184
|
+
throw new ValueError(
|
|
185
|
+
`Address '${address}' uses '.bit' notation, which is only valid for word devices. Use M1000 instead of M1000.0.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validateStringTarget(address, device) {
|
|
191
|
+
const info = DEVICE_CODES[device.code];
|
|
192
|
+
if (!info || info.unit !== DeviceUnit.WORD) {
|
|
193
|
+
throw new ValueError(`Address '${address}' uses string notation, which is only valid for word devices.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function validateParsedEntry(address, device, dtype, parsed) {
|
|
198
|
+
if (dtype === "BIT_IN_WORD") {
|
|
199
|
+
validateBitInWordTarget(address, device);
|
|
200
|
+
if (parsed.hasCount) {
|
|
201
|
+
throw new ValueError(`Address '${address}' does not support ',count' together with '.bit' notation.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (isStringDtype(dtype)) {
|
|
205
|
+
validateStringTarget(address, device);
|
|
206
|
+
if (!parsed.hasCount) {
|
|
207
|
+
throw new ValueError(`Address '${address}' requires ',<length>' for string access.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function validateLongTimerEntry(address, device, dtype) {
|
|
213
|
+
const access = getLongTimerReadAccess(device.code);
|
|
214
|
+
if (!access) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (access.role === "current") {
|
|
218
|
+
if (dtype !== "D" && dtype !== "L") {
|
|
219
|
+
throw new ValueError(`Address '${address}' uses a 32-bit long current value. Use the default form or ':D' / ':L'.`);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (dtype !== "BIT") {
|
|
224
|
+
throw new ValueError(`Address '${address}' is a long timer state device. Use the plain device form without a dtype override.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isBatchableWordDevice(device) {
|
|
229
|
+
const info = DEVICE_CODES[device.code];
|
|
230
|
+
return Boolean(info && info.unit === DeviceUnit.WORD && !UNBATCHED_DEVICE_CODES.has(device.code));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isDwordDtype(dtype) {
|
|
234
|
+
return DWORD_DTYPES.has(canonicalizeDtype(dtype));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getScalarSpanLength(dtype) {
|
|
238
|
+
return isDwordDtype(dtype) ? 2 : 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getSpanLength(dtype, count) {
|
|
242
|
+
if (isStringDtype(dtype)) {
|
|
243
|
+
return Math.ceil(count / 2);
|
|
244
|
+
}
|
|
245
|
+
return getScalarSpanLength(dtype) * count;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function createReadEntry(address, index) {
|
|
249
|
+
const parsed = parseAddress(address);
|
|
250
|
+
const device = parseDevice(parsed.base);
|
|
251
|
+
const dtype = resolveEntryDtype(address, device, parsed);
|
|
252
|
+
validateParsedEntry(address, device, dtype, parsed);
|
|
253
|
+
validateLongTimerEntry(address, device, dtype);
|
|
254
|
+
const info = DEVICE_CODES[device.code];
|
|
255
|
+
return {
|
|
256
|
+
address,
|
|
257
|
+
index,
|
|
258
|
+
device,
|
|
259
|
+
dtype,
|
|
260
|
+
bitIndex: parsed.bitIndex,
|
|
261
|
+
count: parsed.count,
|
|
262
|
+
hasCount: parsed.hasCount,
|
|
263
|
+
info,
|
|
264
|
+
longTimerRead: getLongTimerReadAccess(device.code),
|
|
265
|
+
spanStart: device.number,
|
|
266
|
+
spanLength: getSpanLength(dtype, parsed.count),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createWriteEntry(address, value, index) {
|
|
271
|
+
const parsed = parseAddress(address);
|
|
272
|
+
const device = parseDevice(parsed.base);
|
|
273
|
+
const dtype = resolveEntryDtype(address, device, parsed);
|
|
274
|
+
validateParsedEntry(address, device, dtype, parsed);
|
|
275
|
+
validateLongTimerEntry(address, device, dtype);
|
|
276
|
+
const info = DEVICE_CODES[device.code];
|
|
277
|
+
return {
|
|
278
|
+
address,
|
|
279
|
+
value,
|
|
280
|
+
index,
|
|
281
|
+
device,
|
|
282
|
+
dtype,
|
|
283
|
+
bitIndex: parsed.bitIndex,
|
|
284
|
+
count: parsed.count,
|
|
285
|
+
hasCount: parsed.hasCount,
|
|
286
|
+
info,
|
|
287
|
+
longTimerRead: getLongTimerReadAccess(device.code),
|
|
288
|
+
spanStart: device.number,
|
|
289
|
+
spanLength: getSpanLength(dtype, parsed.count),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isDirectBitEntry(entry) {
|
|
294
|
+
return Boolean(entry.info && entry.info.unit === DeviceUnit.BIT && entry.dtype === "BIT");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isWordEntry(entry) {
|
|
298
|
+
return Boolean(entry.info && entry.info.unit === DeviceUnit.WORD);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isRandomWordEntry(entry) {
|
|
302
|
+
return (
|
|
303
|
+
isWordEntry(entry) &&
|
|
304
|
+
!entry.longTimerRead &&
|
|
305
|
+
!entry.hasCount &&
|
|
306
|
+
entry.dtype !== "BIT_IN_WORD" &&
|
|
307
|
+
!isStringDtype(entry.dtype) &&
|
|
308
|
+
isBatchableWordDevice(entry.device)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isLongTimerReadEntry(entry) {
|
|
313
|
+
return Boolean(entry.longTimerRead);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isLongTimerCurrentWriteEntry(entry) {
|
|
317
|
+
return Boolean(entry.longTimerRead && entry.longTimerRead.role === "current");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildClusters(entries) {
|
|
321
|
+
const byCode = new Map();
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
const list = byCode.get(entry.device.code) || [];
|
|
324
|
+
list.push(entry);
|
|
325
|
+
byCode.set(entry.device.code, list);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const clusters = [];
|
|
329
|
+
for (const [code, list] of byCode.entries()) {
|
|
330
|
+
const sorted = [...list].sort((left, right) => left.spanStart - right.spanStart || left.index - right.index);
|
|
331
|
+
let current = null;
|
|
332
|
+
for (const entry of sorted) {
|
|
333
|
+
const start = entry.spanStart;
|
|
334
|
+
const end = entry.spanStart + entry.spanLength;
|
|
335
|
+
if (!current || start > current.end) {
|
|
336
|
+
if (current) {
|
|
337
|
+
clusters.push(current);
|
|
338
|
+
}
|
|
339
|
+
current = { code, start, end, entries: [entry] };
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
current.end = Math.max(current.end, end);
|
|
343
|
+
current.entries.push(entry);
|
|
344
|
+
}
|
|
345
|
+
if (current) {
|
|
346
|
+
clusters.push(current);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return clusters;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildLongTimerClusters(entries) {
|
|
353
|
+
const byBaseCode = new Map();
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
const baseCode = entry.longTimerRead.baseCode;
|
|
356
|
+
const list = byBaseCode.get(baseCode) || [];
|
|
357
|
+
list.push(entry);
|
|
358
|
+
byBaseCode.set(baseCode, list);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const clusters = [];
|
|
362
|
+
for (const [baseCode, list] of byBaseCode.entries()) {
|
|
363
|
+
const sorted = [...list].sort((left, right) => left.device.number - right.device.number || left.index - right.index);
|
|
364
|
+
let current = null;
|
|
365
|
+
for (const entry of sorted) {
|
|
366
|
+
const start = entry.device.number;
|
|
367
|
+
const end = entry.device.number + entry.count;
|
|
368
|
+
if (!current || start > current.end) {
|
|
369
|
+
if (current) {
|
|
370
|
+
clusters.push(current);
|
|
371
|
+
}
|
|
372
|
+
current = { baseCode, start, end, entries: [entry] };
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
current.end = Math.max(current.end, end);
|
|
376
|
+
current.entries.push(entry);
|
|
377
|
+
}
|
|
378
|
+
if (current) {
|
|
379
|
+
clusters.push(current);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return clusters;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function compileReadPlan(addresses) {
|
|
386
|
+
const entries = addresses.map((address, index) => createReadEntry(address, index));
|
|
387
|
+
const longTimerClusters = buildLongTimerClusters(entries.filter(isLongTimerReadEntry));
|
|
388
|
+
const plainEntries = entries.filter((entry) => !entry.longTimerRead);
|
|
389
|
+
const bitClusters = buildClusters(plainEntries.filter(isDirectBitEntry));
|
|
390
|
+
const wordClusters = buildClusters(plainEntries.filter(isWordEntry));
|
|
391
|
+
const randomEntries = [];
|
|
392
|
+
const blockWordClusters = [];
|
|
393
|
+
|
|
394
|
+
for (const cluster of wordClusters) {
|
|
395
|
+
if (cluster.entries.length === 1 && isRandomWordEntry(cluster.entries[0])) {
|
|
396
|
+
randomEntries.push(cluster.entries[0]);
|
|
397
|
+
} else {
|
|
398
|
+
blockWordClusters.push(cluster);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
entries,
|
|
404
|
+
longTimerClusters,
|
|
405
|
+
bitClusters,
|
|
406
|
+
blockWordClusters,
|
|
407
|
+
randomEntries,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function decodeWordValue(value, dtype) {
|
|
412
|
+
const key = canonicalizeDtype(dtype);
|
|
413
|
+
if (key === "S") {
|
|
414
|
+
const raw = Buffer.alloc(2);
|
|
415
|
+
raw.writeUInt16LE(Number(value) & 0xffff, 0);
|
|
416
|
+
return raw.readInt16LE(0);
|
|
417
|
+
}
|
|
418
|
+
return Number(value);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function decodeDwordValue(value, dtype) {
|
|
422
|
+
const key = canonicalizeDtype(dtype);
|
|
423
|
+
const raw = Buffer.alloc(4);
|
|
424
|
+
raw.writeUInt32LE(Number(value) >>> 0, 0);
|
|
425
|
+
if (key === "F") {
|
|
426
|
+
return raw.readFloatLE(0);
|
|
427
|
+
}
|
|
428
|
+
if (key === "L") {
|
|
429
|
+
return raw.readInt32LE(0);
|
|
430
|
+
}
|
|
431
|
+
return raw.readUInt32LE(0);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function decodeDwordWords(words, offset, dtype) {
|
|
435
|
+
const key = canonicalizeDtype(dtype);
|
|
436
|
+
const raw = Buffer.alloc(4);
|
|
437
|
+
raw.writeUInt16LE(Number(words[offset]) & 0xffff, 0);
|
|
438
|
+
raw.writeUInt16LE(Number(words[offset + 1]) & 0xffff, 2);
|
|
439
|
+
if (key === "F") {
|
|
440
|
+
return raw.readFloatLE(0);
|
|
441
|
+
}
|
|
442
|
+
if (key === "L") {
|
|
443
|
+
return raw.readInt32LE(0);
|
|
444
|
+
}
|
|
445
|
+
return raw.readUInt32LE(0);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function decodeStringWords(words, offset, byteLength) {
|
|
449
|
+
const raw = Buffer.alloc(Math.ceil(byteLength / 2) * 2, 0);
|
|
450
|
+
for (let index = 0; index < raw.length; index += 2) {
|
|
451
|
+
raw.writeUInt16LE(Number(words[offset + index / 2]) & 0xffff, index);
|
|
452
|
+
}
|
|
453
|
+
const bytes = raw.subarray(0, byteLength);
|
|
454
|
+
let end = bytes.length;
|
|
455
|
+
while (end > 0 && bytes[end - 1] === 0x00) {
|
|
456
|
+
end -= 1;
|
|
457
|
+
}
|
|
458
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function decodeRepeatedValues(words, offset, entry) {
|
|
462
|
+
const values = [];
|
|
463
|
+
const stride = getScalarSpanLength(entry.dtype);
|
|
464
|
+
for (let index = 0; index < entry.count; index += 1) {
|
|
465
|
+
const itemOffset = offset + index * stride;
|
|
466
|
+
if (isDwordDtype(entry.dtype)) {
|
|
467
|
+
values.push(decodeDwordWords(words, itemOffset, entry.dtype));
|
|
468
|
+
} else {
|
|
469
|
+
values.push(decodeWordValue(words[itemOffset], entry.dtype));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return values;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function decodeBlockWordEntry(words, clusterStart, entry) {
|
|
476
|
+
const offset = entry.device.number - clusterStart;
|
|
477
|
+
if (entry.dtype === "BIT_IN_WORD") {
|
|
478
|
+
return Boolean((Number(words[offset]) >> (entry.bitIndex || 0)) & 0x1);
|
|
479
|
+
}
|
|
480
|
+
if (isStringDtype(entry.dtype)) {
|
|
481
|
+
return decodeStringWords(words, offset, entry.count);
|
|
482
|
+
}
|
|
483
|
+
if (entry.hasCount) {
|
|
484
|
+
return decodeRepeatedValues(words, offset, entry);
|
|
485
|
+
}
|
|
486
|
+
if (isDwordDtype(entry.dtype)) {
|
|
487
|
+
return decodeDwordWords(words, offset, entry.dtype);
|
|
488
|
+
}
|
|
489
|
+
return decodeWordValue(words[offset], entry.dtype);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function decodeLongTimerPoint(words, offset, entry) {
|
|
493
|
+
const base = offset * 4;
|
|
494
|
+
if (entry.longTimerRead.role === "current") {
|
|
495
|
+
return decodeDwordWords(words, base, entry.dtype);
|
|
496
|
+
}
|
|
497
|
+
const statusWord = Number(words[base + 2] || 0) & 0xffff;
|
|
498
|
+
return entry.longTimerRead.role === "contact" ? Boolean(statusWord & 0x0002) : Boolean(statusWord & 0x0001);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function decodeLongTimerEntry(words, clusterStart, entry) {
|
|
502
|
+
const startOffset = entry.device.number - clusterStart;
|
|
503
|
+
if (entry.hasCount) {
|
|
504
|
+
const values = [];
|
|
505
|
+
for (let index = 0; index < entry.count; index += 1) {
|
|
506
|
+
values.push(decodeLongTimerPoint(words, startOffset + index, entry));
|
|
507
|
+
}
|
|
508
|
+
return values;
|
|
509
|
+
}
|
|
510
|
+
return decodeLongTimerPoint(words, startOffset, entry);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function readLongTimerScalar(client, device, dtype, longTimerRead, options = {}) {
|
|
514
|
+
const words = await client.readDevices(makeDeviceRef(longTimerRead.baseCode, device.number), 4, {
|
|
515
|
+
...options,
|
|
516
|
+
bitUnit: false,
|
|
517
|
+
});
|
|
518
|
+
return decodeLongTimerEntry(
|
|
519
|
+
words,
|
|
520
|
+
device.number,
|
|
521
|
+
{
|
|
522
|
+
device,
|
|
523
|
+
dtype,
|
|
524
|
+
count: 1,
|
|
525
|
+
hasCount: false,
|
|
526
|
+
longTimerRead,
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function encodeWriteWords(dtype, value) {
|
|
532
|
+
const key = canonicalizeDtype(dtype || "U");
|
|
533
|
+
if (isStringDtype(key)) {
|
|
534
|
+
throw new ValueError("String values require a length-qualified address such as 'D100:STR,10'.");
|
|
535
|
+
}
|
|
536
|
+
if (key === "F" || key === "L" || key === "D") {
|
|
537
|
+
const raw = Buffer.alloc(4);
|
|
538
|
+
if (key === "F") {
|
|
539
|
+
raw.writeFloatLE(Number(value), 0);
|
|
540
|
+
} else if (key === "L") {
|
|
541
|
+
raw.writeInt32LE(Number(value), 0);
|
|
542
|
+
} else {
|
|
543
|
+
raw.writeUInt32LE(Number(value) >>> 0, 0);
|
|
544
|
+
}
|
|
545
|
+
return [raw.readUInt16LE(0), raw.readUInt16LE(2)];
|
|
546
|
+
}
|
|
547
|
+
if (key === "S") {
|
|
548
|
+
const raw = Buffer.alloc(2);
|
|
549
|
+
raw.writeInt16LE(Number(value), 0);
|
|
550
|
+
return [raw.readUInt16LE(0)];
|
|
551
|
+
}
|
|
552
|
+
return [Number(value) & 0xffff];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function encodeRandomWriteValue(dtype, value) {
|
|
556
|
+
const words = encodeWriteWords(dtype, value);
|
|
557
|
+
if (words.length === 1) {
|
|
558
|
+
return words[0];
|
|
559
|
+
}
|
|
560
|
+
const raw = Buffer.alloc(4);
|
|
561
|
+
raw.writeUInt16LE(words[0], 0);
|
|
562
|
+
raw.writeUInt16LE(words[1], 2);
|
|
563
|
+
return raw.readUInt32LE(0);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function encodeStringWords(address, value, byteLength) {
|
|
567
|
+
if (typeof value !== "string") {
|
|
568
|
+
throw new ValueError(`Address '${address}' expects a string value.`);
|
|
569
|
+
}
|
|
570
|
+
const bytes = Buffer.from(value, "utf8");
|
|
571
|
+
if (bytes.length > byteLength) {
|
|
572
|
+
throw new ValueError(`Address '${address}' accepts at most ${byteLength} UTF-8 byte(s), got ${bytes.length}.`);
|
|
573
|
+
}
|
|
574
|
+
const raw = Buffer.alloc(Math.ceil(byteLength / 2) * 2, 0);
|
|
575
|
+
bytes.copy(raw, 0, 0, bytes.length);
|
|
576
|
+
const words = [];
|
|
577
|
+
for (let index = 0; index < raw.length; index += 2) {
|
|
578
|
+
words.push(raw.readUInt16LE(index));
|
|
579
|
+
}
|
|
580
|
+
return words;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function normalizeArrayValue(entry) {
|
|
584
|
+
if (entry.count === 1 && !Array.isArray(entry.value)) {
|
|
585
|
+
return [entry.value];
|
|
586
|
+
}
|
|
587
|
+
if (!Array.isArray(entry.value)) {
|
|
588
|
+
throw new ValueError(`Address '${entry.address}' expects an array with ${entry.count} item(s).`);
|
|
589
|
+
}
|
|
590
|
+
if (entry.value.length !== entry.count) {
|
|
591
|
+
throw new ValueError(`Address '${entry.address}' expects ${entry.count} item(s), got ${entry.value.length}.`);
|
|
592
|
+
}
|
|
593
|
+
return entry.value;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function encodeEntryWords(entry) {
|
|
597
|
+
if (isStringDtype(entry.dtype)) {
|
|
598
|
+
return encodeStringWords(entry.address, entry.value, entry.count);
|
|
599
|
+
}
|
|
600
|
+
if (entry.hasCount) {
|
|
601
|
+
const values = normalizeArrayValue(entry);
|
|
602
|
+
const words = [];
|
|
603
|
+
for (const value of values) {
|
|
604
|
+
words.push(...encodeWriteWords(entry.dtype, value));
|
|
605
|
+
}
|
|
606
|
+
return words;
|
|
607
|
+
}
|
|
608
|
+
return encodeWriteWords(entry.dtype, entry.value);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function makeDeviceRef(code, number) {
|
|
612
|
+
return { code, number };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function chunkArray(values, size) {
|
|
616
|
+
const chunks = [];
|
|
617
|
+
for (let index = 0; index < values.length; index += size) {
|
|
618
|
+
chunks.push(values.slice(index, index + size));
|
|
619
|
+
}
|
|
620
|
+
return chunks;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function readRandomMaps(client, plan, options = {}) {
|
|
624
|
+
const wordValues = {};
|
|
625
|
+
const dwordValues = {};
|
|
626
|
+
const wordChunks = chunkArray(plan.wordDevices || [], 0xff);
|
|
627
|
+
const dwordChunks = chunkArray(plan.dwordDevices || [], 0xff);
|
|
628
|
+
const chunkCount = Math.max(wordChunks.length, dwordChunks.length);
|
|
629
|
+
const tasks = [];
|
|
630
|
+
|
|
631
|
+
for (let index = 0; index < chunkCount; index += 1) {
|
|
632
|
+
const wordChunk = wordChunks[index] || [];
|
|
633
|
+
const dwordChunk = dwordChunks[index] || [];
|
|
634
|
+
if (wordChunk.length === 0 && dwordChunk.length === 0) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
tasks.push(
|
|
638
|
+
client.readRandom({ wordDevices: wordChunk, dwordDevices: dwordChunk, ...options }).then((result) => {
|
|
639
|
+
Object.assign(wordValues, result.word);
|
|
640
|
+
Object.assign(dwordValues, result.dword);
|
|
641
|
+
})
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
await Promise.all(tasks);
|
|
646
|
+
return { wordValues, dwordValues };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function executeRandomReadEntries(client, entries, result, options = {}) {
|
|
650
|
+
const wordDevices = [];
|
|
651
|
+
const dwordDevices = [];
|
|
652
|
+
const seenWords = new Set();
|
|
653
|
+
const seenDwords = new Set();
|
|
654
|
+
|
|
655
|
+
for (const entry of entries) {
|
|
656
|
+
const key = deviceToString(entry.device);
|
|
657
|
+
if (isDwordDtype(entry.dtype)) {
|
|
658
|
+
if (!seenDwords.has(key)) {
|
|
659
|
+
seenDwords.add(key);
|
|
660
|
+
dwordDevices.push(entry.device);
|
|
661
|
+
}
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (!seenWords.has(key)) {
|
|
665
|
+
seenWords.add(key);
|
|
666
|
+
wordDevices.push(entry.device);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const { wordValues, dwordValues } = await readRandomMaps(client, { wordDevices, dwordDevices }, options);
|
|
671
|
+
for (const entry of entries) {
|
|
672
|
+
const key = deviceToString(entry.device);
|
|
673
|
+
if (isDwordDtype(entry.dtype)) {
|
|
674
|
+
result[entry.address] = decodeDwordValue(dwordValues[key], entry.dtype);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (entry.dtype === "BIT_IN_WORD") {
|
|
678
|
+
const word = wordValues[key];
|
|
679
|
+
result[entry.address] = Boolean((word >> (entry.bitIndex || 0)) & 0x1);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
result[entry.address] = decodeWordValue(wordValues[key], entry.dtype);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function executeBitReadCluster(client, cluster, result, options = {}) {
|
|
687
|
+
const values = await client.readDevices(makeDeviceRef(cluster.code, cluster.start), cluster.end - cluster.start, {
|
|
688
|
+
...options,
|
|
689
|
+
bitUnit: true,
|
|
690
|
+
});
|
|
691
|
+
for (const entry of cluster.entries) {
|
|
692
|
+
const offset = entry.device.number - cluster.start;
|
|
693
|
+
if (entry.hasCount) {
|
|
694
|
+
result[entry.address] = values.slice(offset, offset + entry.count).map((value) => Boolean(value));
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
result[entry.address] = Boolean(values[offset]);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function executeWordReadCluster(client, cluster, result, options = {}) {
|
|
702
|
+
const words = await client.readDevices(makeDeviceRef(cluster.code, cluster.start), cluster.end - cluster.start, {
|
|
703
|
+
...options,
|
|
704
|
+
bitUnit: false,
|
|
705
|
+
});
|
|
706
|
+
for (const entry of cluster.entries) {
|
|
707
|
+
result[entry.address] = decodeBlockWordEntry(words, cluster.start, entry);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function executeLongTimerReadCluster(client, cluster, result, options = {}) {
|
|
712
|
+
const pointCount = cluster.end - cluster.start;
|
|
713
|
+
const words = await client.readDevices(makeDeviceRef(cluster.baseCode, cluster.start), pointCount * 4, {
|
|
714
|
+
...options,
|
|
715
|
+
bitUnit: false,
|
|
716
|
+
});
|
|
717
|
+
for (const entry of cluster.entries) {
|
|
718
|
+
result[entry.address] = decodeLongTimerEntry(words, cluster.start, entry);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function readNamed(client, addresses, options = {}) {
|
|
723
|
+
const list = normalizeAddressList(addresses);
|
|
724
|
+
const plan = compileReadPlan(list);
|
|
725
|
+
const result = Object.fromEntries(plan.entries.map((entry) => [entry.address, undefined]));
|
|
726
|
+
const tasks = [];
|
|
727
|
+
|
|
728
|
+
if (plan.randomEntries.length > 0) {
|
|
729
|
+
tasks.push(executeRandomReadEntries(client, plan.randomEntries, result, options));
|
|
730
|
+
}
|
|
731
|
+
tasks.push(...plan.longTimerClusters.map((cluster) => executeLongTimerReadCluster(client, cluster, result, options)));
|
|
732
|
+
tasks.push(...plan.bitClusters.map((cluster) => executeBitReadCluster(client, cluster, result, options)));
|
|
733
|
+
tasks.push(...plan.blockWordClusters.map((cluster) => executeWordReadCluster(client, cluster, result, options)));
|
|
734
|
+
|
|
735
|
+
await Promise.all(tasks);
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function executeRandomWrites(client, entries, options = {}) {
|
|
740
|
+
const wordValues = [];
|
|
741
|
+
const dwordValues = [];
|
|
742
|
+
for (const entry of entries) {
|
|
743
|
+
if (isDwordDtype(entry.dtype)) {
|
|
744
|
+
dwordValues.push([entry.device, encodeRandomWriteValue(entry.dtype, entry.value)]);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
wordValues.push([entry.device, encodeWriteWords(entry.dtype, entry.value)[0]]);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const wordChunks = chunkArray(wordValues, 0xff);
|
|
751
|
+
const dwordChunks = chunkArray(dwordValues, 0xff);
|
|
752
|
+
const chunkCount = Math.max(wordChunks.length, dwordChunks.length);
|
|
753
|
+
const tasks = [];
|
|
754
|
+
|
|
755
|
+
for (let index = 0; index < chunkCount; index += 1) {
|
|
756
|
+
const wordChunk = wordChunks[index] || [];
|
|
757
|
+
const dwordChunk = dwordChunks[index] || [];
|
|
758
|
+
if (wordChunk.length === 0 && dwordChunk.length === 0) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
tasks.push(client.writeRandomWords({ wordValues: wordChunk, dwordValues: dwordChunk, ...options }));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
await Promise.all(tasks);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function normalizeBitEntryValues(entry) {
|
|
768
|
+
if (!entry.hasCount) {
|
|
769
|
+
return [Boolean(entry.value)];
|
|
770
|
+
}
|
|
771
|
+
return normalizeArrayValue(entry).map((value) => Boolean(value));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function executeBitWriteCluster(client, cluster, options = {}) {
|
|
775
|
+
const values = new Array(cluster.end - cluster.start).fill(false);
|
|
776
|
+
const entries = [...cluster.entries].sort((left, right) => left.index - right.index);
|
|
777
|
+
for (const entry of entries) {
|
|
778
|
+
const offset = entry.device.number - cluster.start;
|
|
779
|
+
const entryValues = normalizeBitEntryValues(entry);
|
|
780
|
+
for (let index = 0; index < entryValues.length; index += 1) {
|
|
781
|
+
values[offset + index] = entryValues[index];
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
await client.writeDevices(makeDeviceRef(cluster.code, cluster.start), values, { ...options, bitUnit: true });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function executeWordWriteCluster(client, cluster, options = {}) {
|
|
788
|
+
const entries = [...cluster.entries].sort((left, right) => left.index - right.index);
|
|
789
|
+
const wordCount = cluster.end - cluster.start;
|
|
790
|
+
const needsRead = entries.some((entry) => entry.dtype === "BIT_IN_WORD");
|
|
791
|
+
const words = needsRead
|
|
792
|
+
? Array.from(
|
|
793
|
+
await client.readDevices(makeDeviceRef(cluster.code, cluster.start), wordCount, { ...options, bitUnit: false })
|
|
794
|
+
)
|
|
795
|
+
: new Array(wordCount).fill(0);
|
|
796
|
+
|
|
797
|
+
for (const entry of entries) {
|
|
798
|
+
const offset = entry.device.number - cluster.start;
|
|
799
|
+
if (entry.dtype === "BIT_IN_WORD") {
|
|
800
|
+
let current = Number(words[offset]) & 0xffff;
|
|
801
|
+
if (entry.value) {
|
|
802
|
+
current |= 1 << (entry.bitIndex || 0);
|
|
803
|
+
} else {
|
|
804
|
+
current &= ~(1 << (entry.bitIndex || 0));
|
|
805
|
+
}
|
|
806
|
+
words[offset] = current;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const encoded = encodeEntryWords(entry);
|
|
811
|
+
for (let index = 0; index < encoded.length; index += 1) {
|
|
812
|
+
words[offset + index] = encoded[index];
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
await client.writeDevices(makeDeviceRef(cluster.code, cluster.start), words, { ...options, bitUnit: false });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function executeLongCurrentWrites(client, entries, options = {}) {
|
|
820
|
+
const dwordValues = [];
|
|
821
|
+
for (const entry of entries) {
|
|
822
|
+
const values = entry.hasCount ? normalizeArrayValue(entry) : [entry.value];
|
|
823
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
824
|
+
dwordValues.push([
|
|
825
|
+
makeDeviceRef(entry.device.code, entry.device.number + index),
|
|
826
|
+
encodeRandomWriteValue(entry.dtype, values[index]),
|
|
827
|
+
]);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
for (const chunk of chunkArray(dwordValues, 0xff)) {
|
|
832
|
+
if (chunk.length === 0) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
await client.writeRandomWords({ wordValues: [], dwordValues: chunk, ...options });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function writeNamed(client, updates, options = {}) {
|
|
840
|
+
const entries = Object.entries(updates || {}).map(([address, value], index) => createWriteEntry(address, value, index));
|
|
841
|
+
const longCurrentEntries = entries.filter(isLongTimerCurrentWriteEntry);
|
|
842
|
+
const plainEntries = entries.filter((entry) => !isLongTimerCurrentWriteEntry(entry));
|
|
843
|
+
const bitClusters = buildClusters(plainEntries.filter(isDirectBitEntry));
|
|
844
|
+
const wordClusters = buildClusters(plainEntries.filter(isWordEntry));
|
|
845
|
+
const randomEntries = [];
|
|
846
|
+
const blockWordClusters = [];
|
|
847
|
+
|
|
848
|
+
for (const cluster of wordClusters) {
|
|
849
|
+
if (cluster.entries.length === 1 && isRandomWordEntry(cluster.entries[0])) {
|
|
850
|
+
randomEntries.push(cluster.entries[0]);
|
|
851
|
+
} else {
|
|
852
|
+
blockWordClusters.push(cluster);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const tasks = [];
|
|
857
|
+
if (longCurrentEntries.length > 0) {
|
|
858
|
+
tasks.push(executeLongCurrentWrites(client, longCurrentEntries, options));
|
|
859
|
+
}
|
|
860
|
+
if (randomEntries.length > 0) {
|
|
861
|
+
tasks.push(executeRandomWrites(client, randomEntries, options));
|
|
862
|
+
}
|
|
863
|
+
tasks.push(...bitClusters.map((cluster) => executeBitWriteCluster(client, cluster, options)));
|
|
864
|
+
tasks.push(...blockWordClusters.map((cluster) => executeWordWriteCluster(client, cluster, options)));
|
|
865
|
+
|
|
866
|
+
await Promise.all(tasks);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function tokenizeAddressList(text) {
|
|
870
|
+
const result = [];
|
|
871
|
+
let index = 0;
|
|
872
|
+
const source = String(text || "");
|
|
873
|
+
|
|
874
|
+
while (index < source.length) {
|
|
875
|
+
while (index < source.length && /[\s,;]+/.test(source[index])) {
|
|
876
|
+
index += 1;
|
|
877
|
+
}
|
|
878
|
+
if (index >= source.length) {
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
ADDRESS_LIST_TOKEN_RE.lastIndex = index;
|
|
882
|
+
const match = ADDRESS_LIST_TOKEN_RE.exec(source);
|
|
883
|
+
if (!match || match.index !== index) {
|
|
884
|
+
throw new ValueError(`Invalid address list near ${JSON.stringify(source.slice(index, index + 20))}.`);
|
|
885
|
+
}
|
|
886
|
+
result.push(match[0].trim());
|
|
887
|
+
index = ADDRESS_LIST_TOKEN_RE.lastIndex;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return result;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function normalizeAddressList(addresses) {
|
|
894
|
+
if (Array.isArray(addresses)) {
|
|
895
|
+
return addresses.map((item) => String(item).trim()).filter(Boolean);
|
|
896
|
+
}
|
|
897
|
+
return tokenizeAddressList(addresses).filter(Boolean);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
module.exports = {
|
|
901
|
+
compileReadPlan,
|
|
902
|
+
normalizeAddressList,
|
|
903
|
+
parseAddress,
|
|
904
|
+
readBits,
|
|
905
|
+
readNamed,
|
|
906
|
+
readTyped,
|
|
907
|
+
writeBitInWord,
|
|
908
|
+
writeBits,
|
|
909
|
+
writeNamed,
|
|
910
|
+
writeTyped,
|
|
911
|
+
};
|