node-red-contrib-my-tools 1.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.
Files changed (67) hide show
  1. package/README.txt +69 -0
  2. package/node-red-contrib-mysql-extended-test/mysql-extended.html +443 -0
  3. package/node-red-contrib-mysql-extended-test/mysql-extended.js +355 -0
  4. package/node-red-contrib-rfid-helper/rfid-lazy.html +153 -0
  5. package/node-red-contrib-rfid-helper/rfid-lazy.js +361 -0
  6. package/node-red-contrib-s7-plus/LICENSE +235 -0
  7. package/node-red-contrib-s7-plus/README.md +141 -0
  8. package/node-red-contrib-s7-plus/examples/read-multiple-values-flow.json +346 -0
  9. package/node-red-contrib-s7-plus/examples/read-write-test-flow.json +172 -0
  10. package/node-red-contrib-s7-plus/examples/write-single-values-flow.json +5583 -0
  11. package/node-red-contrib-s7-plus/lib/s7plus/browse/README.md +72 -0
  12. package/node-red-contrib-s7-plus/lib/s7plus/browse/areas.js +36 -0
  13. package/node-red-contrib-s7-plus/lib/s7plus/browse/datatypes.js +91 -0
  14. package/node-red-contrib-s7-plus/lib/s7plus/browse/flat-browser.js +433 -0
  15. package/node-red-contrib-s7-plus/lib/s7plus/browse/index.js +22 -0
  16. package/node-red-contrib-s7-plus/lib/s7plus/browse/lazy.js +397 -0
  17. package/node-red-contrib-s7-plus/lib/s7plus/browse/node-id.js +22 -0
  18. package/node-red-contrib-s7-plus/lib/s7plus/browse/resolve-symbolic.js +181 -0
  19. package/node-red-contrib-s7-plus/lib/s7plus/browse/vte-helpers.js +49 -0
  20. package/node-red-contrib-s7-plus/lib/s7plus/buffer-stream.js +96 -0
  21. package/node-red-contrib-s7-plus/lib/s7plus/client.js +1238 -0
  22. package/node-red-contrib-s7-plus/lib/s7plus/constants.js +155 -0
  23. package/node-red-contrib-s7-plus/lib/s7plus/crc/index.js +3 -0
  24. package/node-red-contrib-s7-plus/lib/s7plus/crc/s7-crc32.js +70 -0
  25. package/node-red-contrib-s7-plus/lib/s7plus/crc/symbol-crc.js +271 -0
  26. package/node-red-contrib-s7-plus/lib/s7plus/debug.js +88 -0
  27. package/node-red-contrib-s7-plus/lib/s7plus/explore-result.js +41 -0
  28. package/node-red-contrib-s7-plus/lib/s7plus/item-address.js +55 -0
  29. package/node-red-contrib-s7-plus/lib/s7plus/pdu-explore.js +77 -0
  30. package/node-red-contrib-s7-plus/lib/s7plus/pdu-messages.js +359 -0
  31. package/node-red-contrib-s7-plus/lib/s7plus/pobject.js +197 -0
  32. package/node-red-contrib-s7-plus/lib/s7plus/pvalue-codec.js +435 -0
  33. package/node-red-contrib-s7-plus/lib/s7plus/pvalue.js +653 -0
  34. package/node-red-contrib-s7-plus/lib/s7plus/pvar-lists.js +336 -0
  35. package/node-red-contrib-s7-plus/lib/s7plus/read-result.js +91 -0
  36. package/node-red-contrib-s7-plus/lib/s7plus/s7p.js +266 -0
  37. package/node-red-contrib-s7-plus/lib/s7plus/tag-routing.js +30 -0
  38. package/node-red-contrib-s7-plus/lib/s7plus/transport/cotp-stream.js +335 -0
  39. package/node-red-contrib-s7-plus/lib/s7plus/transport/s7-transport.js +335 -0
  40. package/node-red-contrib-s7-plus/nodes/icons/s7complus.png +0 -0
  41. package/node-red-contrib-s7-plus/nodes/icons/s7complus.svg +17 -0
  42. package/node-red-contrib-s7-plus/nodes/s7complus-endpoint.js +825 -0
  43. package/node-red-contrib-s7-plus/nodes/s7complus-explore.js +91 -0
  44. package/node-red-contrib-s7-plus/nodes/s7complus-in.js +255 -0
  45. package/node-red-contrib-s7-plus/nodes/s7complus-out.js +182 -0
  46. package/node-red-contrib-s7-plus/nodes/s7complus.html +1541 -0
  47. package/node-red-contrib-s7-plus/nodes/s7complus.js +10 -0
  48. package/node-red-contrib-s7-plus/package.json +50 -0
  49. package/node-red-contrib-s7-plus/plc/s7-1500/DB_Arrays.db +17 -0
  50. package/node-red-contrib-s7-plus/plc/s7-1500/DB_Binary.db +16 -0
  51. package/node-red-contrib-s7-plus/plc/s7-1500/DB_BitStrings.db +36 -0
  52. package/node-red-contrib-s7-plus/plc/s7-1500/DB_CharacterStrings.db +42 -0
  53. package/node-red-contrib-s7-plus/plc/s7-1500/DB_DateAndTime.db +70 -0
  54. package/node-red-contrib-s7-plus/plc/s7-1500/DB_FloatingPoint.db +42 -0
  55. package/node-red-contrib-s7-plus/plc/s7-1500/DB_Integers.db +72 -0
  56. package/node-red-contrib-s7-plus/plc/s7-1500/DB_Timers.db +40 -0
  57. package/node-red-contrib-s7-plus/scripts/example-flow-shared.js +39 -0
  58. package/node-red-contrib-s7-plus/scripts/generate-read-multiple-flow.js +331 -0
  59. package/node-red-contrib-s7-plus/scripts/generate-rw-test-flow.js +690 -0
  60. package/node-red-contrib-s7-plus/scripts/generate-write-flow.js +476 -0
  61. package/node-red-contrib-s7-plus/scripts/layout-write-flow.js +156 -0
  62. package/node-red-contrib-s7-plus/scripts/run-tests.js +16 -0
  63. package/node-red-flowgobal-change/variable-change.html +425 -0
  64. package/node-red-flowgobal-change/variable-change.js +92 -0
  65. package/node-red-ui_media_viewer/ui_media_viewer.html +87 -0
  66. package/node-red-ui_media_viewer/ui_media_viewer.js +136 -0
  67. package/package.json +12 -0
@@ -0,0 +1,397 @@
1
+ 'use strict';
2
+
3
+ const { Ids } = require('../constants');
4
+ const { MEMORY_AREAS } = require('./areas');
5
+ const { encodeNodeId } = require('./node-id');
6
+ const {
7
+ eNodeType,
8
+ softdatatypeName,
9
+ isSoftdatatypeSupported,
10
+ isPackedLeafDatatype,
11
+ getSizeOfDatatype
12
+ } = require('./datatypes');
13
+
14
+ const ARRAY_PAGE_SIZE = 32;
15
+
16
+ /**
17
+ * Lazy browse: one tree level at a time, arrays as single node or paginated indices.
18
+ * exploreChildsRecursive=0 on PLC; fallback loads subtree from cache (see client).
19
+ */
20
+
21
+ function pathNames(path) {
22
+ return path.map(p => p.name).join('');
23
+ }
24
+
25
+ function pathAccess(path) {
26
+ let accessIds = '';
27
+ for (const p of path) {
28
+ if (p.nodeType === eNodeType.Root) {
29
+ accessIds += p.accessId.toString(16).toUpperCase();
30
+ } else {
31
+ accessIds += '.' + p.accessId.toString(16).toUpperCase();
32
+ if (p.nodeType === eNodeType.StructArray) accessIds += '.1';
33
+ }
34
+ }
35
+ return accessIds;
36
+ }
37
+
38
+ function publicNode(id, label, nodeKind, hasChildren, datatype, isLeaf) {
39
+ return { id, label, nodeKind, hasChildren, datatype: datatype || undefined, isLeaf: !!isLeaf };
40
+ }
41
+
42
+ function resolveTypeName(cache, relationId, fallbackSoftdatatype) {
43
+ const ob = relationId ? cache.get(relationId) || null : null;
44
+ if (ob && typeof ob.getAttribute === 'function') {
45
+ const nameAttr = ob.getAttribute(Ids.ObjectVariableTypeName);
46
+ if (nameAttr) return String(nameAttr.toJs());
47
+ }
48
+ return softdatatypeName(fallbackSoftdatatype);
49
+ }
50
+
51
+ function listBlockRoots(dbList) {
52
+ const nodes = [];
53
+ for (const ed of dbList) {
54
+ if (!ed.db_block_ti_relid) continue;
55
+ const path = [{
56
+ nodeType: eNodeType.Root,
57
+ name: ed.db_name,
58
+ accessId: ed.db_block_relid,
59
+ tiRelId: ed.db_block_ti_relid
60
+ }];
61
+ nodes.push(publicNode(
62
+ encodeNodeId({ t: 'block', path, tiRelId: ed.db_block_ti_relid }),
63
+ ed.db_name,
64
+ 'block',
65
+ true,
66
+ undefined,
67
+ false
68
+ ));
69
+ }
70
+ for (const a of MEMORY_AREAS) {
71
+ const path = [{ nodeType: eNodeType.Root, name: a.name, accessId: a.accessId, tiRelId: a.tiRelId }];
72
+ nodes.push(publicNode(
73
+ encodeNodeId({ t: 'block', path, tiRelId: a.tiRelId }),
74
+ a.name,
75
+ 'area',
76
+ true,
77
+ undefined,
78
+ false
79
+ ));
80
+ }
81
+ return nodes;
82
+ }
83
+
84
+ function listMembersForTypeInfo(typeOb, parentPath, cache) {
85
+ const nodes = [];
86
+ if (!typeOb || !typeOb.vartypeList) return nodes;
87
+
88
+ let elementIndex = 0;
89
+ for (const vte of typeOb.vartypeList.elements) {
90
+ const name = typeOb.varnameList.names[elementIndex];
91
+ const oit = vte.offsetInfoType;
92
+ const path = parentPath.concat([{
93
+ nodeType: eNodeType.Var,
94
+ name: '.' + name,
95
+ accessId: vte.lid,
96
+ softdatatype: vte.softdatatype,
97
+ vte
98
+ }]);
99
+
100
+ if (oit.is1Dim && oit.is1Dim()) {
101
+ const count = oit.getArrayElementCount();
102
+ const lower = oit.getArrayLowerBounds();
103
+ const hasRel = oit.hasRelation();
104
+ const relId = hasRel ? oit.getRelationId() : 0;
105
+ const dtName = hasRel
106
+ ? resolveTypeName(cache, relId, vte.softdatatype)
107
+ : softdatatypeName(vte.softdatatype);
108
+ const desc = {
109
+ t: 'array',
110
+ path,
111
+ elementIndex,
112
+ lower,
113
+ count,
114
+ hasRelation: hasRel,
115
+ relationId: relId
116
+ };
117
+ nodes.push(publicNode(
118
+ encodeNodeId(desc),
119
+ name,
120
+ 'array',
121
+ count > 0,
122
+ `Array[${lower}..${lower + count - 1}] of ${dtName}`,
123
+ false
124
+ ));
125
+ } else if (oit.isMDim && oit.isMDim()) {
126
+ const count = oit.getArrayElementCount();
127
+ const hasRel = oit.hasRelation();
128
+ const relId = hasRel ? oit.getRelationId() : 0;
129
+ const dtName = hasRel
130
+ ? resolveTypeName(cache, relId, vte.softdatatype)
131
+ : softdatatypeName(vte.softdatatype);
132
+ const desc = {
133
+ t: 'array',
134
+ path,
135
+ elementIndex,
136
+ lower: 0,
137
+ count,
138
+ mdim: true,
139
+ hasRelation: hasRel,
140
+ relationId: relId
141
+ };
142
+ nodes.push(publicNode(
143
+ encodeNodeId(desc),
144
+ name,
145
+ 'array',
146
+ count > 0,
147
+ `Array[${count}] of ${dtName}`,
148
+ false
149
+ ));
150
+ } else if (oit.hasRelation && oit.hasRelation() && !isPackedLeafDatatype(vte.softdatatype)) {
151
+ const relId = oit.getRelationId();
152
+ const childOb = relId ? cache.get(relId) || null : null;
153
+ const desc = { t: 'struct', path, tiRelId: relId };
154
+ nodes.push(publicNode(
155
+ encodeNodeId(desc),
156
+ name,
157
+ 'struct',
158
+ !!(childOb && childOb.vartypeList) || relId !== 0,
159
+ resolveTypeName(cache, relId, vte.softdatatype),
160
+ false
161
+ ));
162
+ } else if (isSoftdatatypeSupported(vte.softdatatype)) {
163
+ const desc = { t: 'leaf', path };
164
+ nodes.push(publicNode(
165
+ encodeNodeId(desc),
166
+ name,
167
+ 'leaf',
168
+ false,
169
+ softdatatypeName(vte.softdatatype),
170
+ true
171
+ ));
172
+ } else {
173
+ const desc = { t: 'unsupported', path };
174
+ nodes.push(publicNode(
175
+ encodeNodeId(desc),
176
+ name,
177
+ 'unsupported',
178
+ false,
179
+ softdatatypeName(vte.softdatatype),
180
+ false
181
+ ));
182
+ }
183
+ elementIndex++;
184
+ }
185
+ return nodes;
186
+ }
187
+
188
+ function listArrayPages(desc, cache) {
189
+ const { lower, count } = desc;
190
+ const nodes = [];
191
+ const lastSeg = desc.path[desc.path.length - 1];
192
+ const dtLabel = desc.hasRelation
193
+ ? resolveTypeName(cache, desc.relationId, lastSeg.softdatatype)
194
+ : softdatatypeName(lastSeg.softdatatype);
195
+ for (let start = 0; start < count; start += ARRAY_PAGE_SIZE) {
196
+ const end = Math.min(start + ARRAY_PAGE_SIZE - 1, count - 1);
197
+ const label = `[${lower + start}..${lower + end}]`;
198
+ const pageDesc = {
199
+ t: 'arrpage',
200
+ path: desc.path,
201
+ lower: desc.lower,
202
+ count: desc.count,
203
+ hasRelation: desc.hasRelation,
204
+ relationId: desc.relationId,
205
+ mdim: desc.mdim,
206
+ start,
207
+ end
208
+ };
209
+ nodes.push(publicNode(
210
+ encodeNodeId(pageDesc),
211
+ label,
212
+ 'arrpage',
213
+ true,
214
+ dtLabel,
215
+ false
216
+ ));
217
+ }
218
+ return nodes;
219
+ }
220
+
221
+ function listArrayElements(pageDesc, cache) {
222
+ const vte = pageDesc.path[pageDesc.path.length - 1].vte;
223
+ const nodes = [];
224
+ const parentPath = pageDesc.path;
225
+
226
+ for (let i = pageDesc.start; i <= pageDesc.end; i++) {
227
+ const elemName = `[${pageDesc.lower + i}]`;
228
+
229
+ if (pageDesc.hasRelation) {
230
+ const arrayPath = parentPath.concat([{
231
+ nodeType: eNodeType.StructArray,
232
+ name: elemName,
233
+ accessId: i,
234
+ softdatatype: vte.softdatatype,
235
+ vte
236
+ }]);
237
+ const desc = {
238
+ t: 'struct',
239
+ path: arrayPath,
240
+ tiRelId: pageDesc.relationId,
241
+ arrayIndex: i
242
+ };
243
+ nodes.push(publicNode(
244
+ encodeNodeId(desc),
245
+ elemName,
246
+ 'struct',
247
+ true,
248
+ resolveTypeName(cache, pageDesc.relationId, vte.softdatatype),
249
+ false
250
+ ));
251
+ } else {
252
+ const tcomSize = getSizeOfDatatype(vte);
253
+ const arrayPath = parentPath.concat([{
254
+ nodeType: eNodeType.Array,
255
+ name: elemName,
256
+ accessId: i,
257
+ softdatatype: vte.softdatatype,
258
+ vte,
259
+ arrayAdrOffsetOpt: i * tcomSize,
260
+ arrayAdrOffsetNonOpt: i * tcomSize
261
+ }]);
262
+ if (isSoftdatatypeSupported(vte.softdatatype)) {
263
+ const leafDesc = { t: 'leaf', path: arrayPath };
264
+ nodes.push(publicNode(
265
+ encodeNodeId(leafDesc),
266
+ elemName,
267
+ 'leaf',
268
+ false,
269
+ softdatatypeName(vte.softdatatype),
270
+ true
271
+ ));
272
+ } else {
273
+ nodes.push(publicNode(
274
+ encodeNodeId({ t: 'unsupported', path: arrayPath }),
275
+ elemName,
276
+ 'unsupported',
277
+ false,
278
+ softdatatypeName(vte.softdatatype),
279
+ false
280
+ ));
281
+ }
282
+ }
283
+ }
284
+ return nodes;
285
+ }
286
+
287
+ function listChildren(desc, cache) {
288
+ switch (desc.t) {
289
+ case 'block':
290
+ case 'struct': {
291
+ const typeOb = desc.tiRelId ? cache.get(desc.tiRelId) || null : null;
292
+ if (!typeOb) return [];
293
+ const parentPath = desc.path || [];
294
+ return listMembersForTypeInfo(typeOb, parentPath, cache);
295
+ }
296
+ case 'array':
297
+ return listArrayPages(desc, cache);
298
+ case 'arrpage':
299
+ return listArrayElements(desc, cache);
300
+ default:
301
+ return [];
302
+ }
303
+ }
304
+
305
+ function resolveLeaf(desc) {
306
+ if (desc.t === 'unsupported') throw new Error('Datatype not supported for symbolic access');
307
+ if (desc.t !== 'leaf') throw new Error('Node is not a leaf symbol');
308
+ const name = pathNames(desc.path);
309
+ const address = pathAccess(desc.path);
310
+ const sd = desc.path[desc.path.length - 1].softdatatype;
311
+
312
+ const crcMeta = buildCrcMeta(desc.path);
313
+
314
+ return {
315
+ name,
316
+ address,
317
+ datatype: softdatatypeName(sd),
318
+ crcMeta
319
+ };
320
+ }
321
+
322
+ // Robust accessor: works both on a live POffsetInfoType* instance (has
323
+ // getArrayLowerBounds method) and on a plain object recovered from
324
+ // JSON.stringify/parse (encodeNodeId path) where methods are lost but
325
+ // the underlying data field arrayLowerBounds survives. Without this
326
+ // fallback CRC computation for arrays whose lower bound differs from 0
327
+ // (e.g. TIA `Array[1..N]`) silently produces lowerBound=0 and the PLC
328
+ // rejects the read with 0x8009890012cbffef (CRC mismatch).
329
+ function oitLowerBound(oit) {
330
+ if (!oit) return 0;
331
+ if (typeof oit.getArrayLowerBounds === 'function') return oit.getArrayLowerBounds() | 0;
332
+ if (typeof oit.arrayLowerBounds === 'number') return oit.arrayLowerBounds | 0;
333
+ return 0;
334
+ }
335
+
336
+ function buildCrcMeta(path) {
337
+ const STRUCT_SD = 17;
338
+ const segments = [];
339
+
340
+ for (let i = 1; i < path.length; i++) {
341
+ const seg = path[i];
342
+ const isArrayElem = seg.nodeType === eNodeType.Array
343
+ || seg.nodeType === eNodeType.StructArray;
344
+
345
+ if (isArrayElem) {
346
+ const arraySeg = path[i - 1];
347
+ const rawName = arraySeg.name.startsWith('.') ? arraySeg.name.slice(1) : arraySeg.name;
348
+ const vte = arraySeg.vte;
349
+ const oit = vte ? vte.offsetInfoType : null;
350
+ const lower = oitLowerBound(oit);
351
+ if (segments.length > 0 && segments[segments.length - 1].memberName === rawName) {
352
+ segments[segments.length - 1] = {
353
+ memberName: rawName,
354
+ softdatatype: arraySeg.softdatatype,
355
+ isArray: true,
356
+ elementSoftdatatype: seg.softdatatype,
357
+ lowerBound: lower
358
+ };
359
+ } else {
360
+ segments.push({
361
+ memberName: rawName,
362
+ softdatatype: arraySeg.softdatatype,
363
+ isArray: true,
364
+ elementSoftdatatype: seg.softdatatype,
365
+ lowerBound: lower
366
+ });
367
+ }
368
+ } else if (seg.nodeType === eNodeType.Var) {
369
+ const rawName = seg.name.startsWith('.') ? seg.name.slice(1) : seg.name;
370
+ segments.push({
371
+ memberName: rawName,
372
+ softdatatype: seg.softdatatype,
373
+ isArray: false,
374
+ elementSoftdatatype: 0,
375
+ lowerBound: 0
376
+ });
377
+ }
378
+ }
379
+
380
+ if (segments.length === 0) {
381
+ return { memberName: '', softdatatype: 0, isArray: false, elementSoftdatatype: 0, lowerBound: 0, pathSegments: [] };
382
+ }
383
+
384
+ if (segments.length === 1) {
385
+ return segments[0];
386
+ }
387
+
388
+ return { pathSegments: segments };
389
+ }
390
+
391
+ module.exports = {
392
+ ARRAY_PAGE_SIZE,
393
+ listBlockRoots,
394
+ listChildren,
395
+ resolveLeaf,
396
+ resolveTypeName
397
+ };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stable node ids for lazy PLC browse (base64url JSON descriptors).
5
+ */
6
+
7
+ function encodeNodeId(descriptor) {
8
+ return Buffer.from(JSON.stringify(descriptor), 'utf8').toString('base64url');
9
+ }
10
+
11
+ function decodeNodeId(nodeId) {
12
+ if (!nodeId || typeof nodeId !== 'string') {
13
+ throw new Error('Invalid browse node id');
14
+ }
15
+ try {
16
+ return JSON.parse(Buffer.from(nodeId, 'base64url').toString('utf8'));
17
+ } catch {
18
+ throw new Error('Invalid browse node id');
19
+ }
20
+ }
21
+
22
+ module.exports = { encodeNodeId, decodeNodeId };
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Resolve a symbolic PLC path (e.g. "DB1.readings[0]") by walking
5
+ * the browse tree. Handles quoted DB names, arrays with index notation,
6
+ * and struct nesting.
7
+ */
8
+
9
+ /**
10
+ * Parse a TIA-Portal-style symbol path into segments, splitting array
11
+ * indices into separate segments.
12
+ * "DB1.readings[0]" -> ['DB1', 'readings', '[0]']
13
+ * '"DB Name".struct.x' -> ['DB Name', 'struct', 'x']
14
+ * "DB1.arr[3].member" -> ['DB1', 'arr', '[3]', 'member']
15
+ */
16
+ function parseSymbolSegments(symbolPath) {
17
+ const segments = [];
18
+ let i = 0;
19
+ while (i < symbolPath.length) {
20
+ if (symbolPath[i] === '"') {
21
+ const end = symbolPath.indexOf('"', i + 1);
22
+ if (end === -1) {
23
+ segments.push(symbolPath.slice(i + 1));
24
+ break;
25
+ }
26
+ segments.push(symbolPath.slice(i + 1, end));
27
+ i = end + 1;
28
+ if (i < symbolPath.length && symbolPath[i] === '.') i++;
29
+ } else if (symbolPath[i] === '[') {
30
+ const end = symbolPath.indexOf(']', i);
31
+ if (end === -1) {
32
+ segments.push(symbolPath.slice(i));
33
+ break;
34
+ }
35
+ segments.push(symbolPath.slice(i, end + 1));
36
+ i = end + 1;
37
+ if (i < symbolPath.length && symbolPath[i] === '.') i++;
38
+ } else {
39
+ let end = symbolPath.length;
40
+ for (let j = i; j < symbolPath.length; j++) {
41
+ if (symbolPath[j] === '.' || symbolPath[j] === '[') {
42
+ end = j;
43
+ break;
44
+ }
45
+ }
46
+ const seg = symbolPath.slice(i, end);
47
+ if (seg) segments.push(seg);
48
+ i = end;
49
+ if (i < symbolPath.length && symbolPath[i] === '.') i++;
50
+ }
51
+ }
52
+ return segments;
53
+ }
54
+
55
+ /**
56
+ * Navigate the browse tree to find a node by label among children,
57
+ * transparently expanding arrpage nodes for array index segments.
58
+ */
59
+ async function findChildByLabel(client, parentId, label) {
60
+ const { nodes: children } = await client.browseChildren(parentId);
61
+
62
+ const direct = children.find(n => n.label === label);
63
+ if (direct) return direct;
64
+
65
+ for (const child of children) {
66
+ if (child.nodeKind === 'arrpage') {
67
+ const { nodes: pageElements } = await client.browseChildren(child.id);
68
+ const match = pageElements.find(n => n.label === label);
69
+ if (match) return match;
70
+ }
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Walk the browse tree along a symbolic path and resolve the leaf node.
78
+ * @param {object} client - S7CommPlusClient instance
79
+ * @param {string} symbolPath - e.g. "DB1.readings[0]"
80
+ * @returns {Promise<{name, address, datatype, crcMeta}>}
81
+ */
82
+ async function resolveSymbolicPath(client, symbolPath) {
83
+ const segments = parseSymbolSegments(symbolPath);
84
+ if (segments.length < 2) {
85
+ throw new Error(`Invalid symbolic path: '${symbolPath}' (need at least DB.member)`);
86
+ }
87
+
88
+ const { nodes: roots } = await client.browseRoots();
89
+ const rootNode = roots.find(n => n.label === segments[0]);
90
+ if (!rootNode) {
91
+ throw new Error(`Symbol root '${segments[0]}' not found among PLC roots`);
92
+ }
93
+
94
+ let current = rootNode;
95
+ for (let i = 1; i < segments.length; i++) {
96
+ if (!current.hasChildren) {
97
+ throw new Error(
98
+ `Symbol segment '${segments[i]}' not reachable — '${current.label}' has no children`
99
+ );
100
+ }
101
+ const next = await findChildByLabel(client, current.id, segments[i]);
102
+ if (!next) {
103
+ throw new Error(
104
+ `Symbol segment '${segments[i]}' not found in '${current.label}'`
105
+ );
106
+ }
107
+ current = next;
108
+ }
109
+
110
+ if (!current.isLeaf) {
111
+ throw new Error(`Symbol path '${symbolPath}' does not resolve to a readable leaf symbol`);
112
+ }
113
+
114
+ return client.browseResolve(current.id);
115
+ }
116
+
117
+ /**
118
+ * Batch-resolve multiple symbolic paths sharing a single browseRootsCached()
119
+ * call and the accumulated type-info cache. Each symbol is resolved
120
+ * independently so a failure in one does not block the others.
121
+ * @param {object} client - S7CommPlusClient instance
122
+ * @param {string[]} symbolPaths - e.g. ["DB1.speed", "DB2.temp"]
123
+ * @returns {Promise<Array<{name, address, datatype, crcMeta} | {error: string}>>}
124
+ */
125
+ async function resolveSymbolicBatch(client, symbolPaths) {
126
+ const { nodes: roots } = await client.browseRootsCached();
127
+ const results = [];
128
+
129
+ for (const symbolPath of symbolPaths) {
130
+ try {
131
+ const segments = parseSymbolSegments(symbolPath);
132
+ if (segments.length < 2) {
133
+ results.push({ error: `Invalid symbolic path: '${symbolPath}' (need at least DB.member)` });
134
+ continue;
135
+ }
136
+
137
+ const rootNode = roots.find(n => n.label === segments[0]);
138
+ if (!rootNode) {
139
+ results.push({ error: `Symbol root '${segments[0]}' not found among PLC roots` });
140
+ continue;
141
+ }
142
+
143
+ let current = rootNode;
144
+ let failed = false;
145
+ for (let i = 1; i < segments.length; i++) {
146
+ if (!current.hasChildren) {
147
+ results.push({
148
+ error: `Symbol segment '${segments[i]}' not reachable — '${current.label}' has no children`
149
+ });
150
+ failed = true;
151
+ break;
152
+ }
153
+ const next = await findChildByLabel(client, current.id, segments[i]);
154
+ if (!next) {
155
+ results.push({
156
+ error: `Symbol segment '${segments[i]}' not found in '${current.label}'`
157
+ });
158
+ failed = true;
159
+ break;
160
+ }
161
+ current = next;
162
+ }
163
+ if (failed) continue;
164
+
165
+ if (!current.isLeaf) {
166
+ results.push({
167
+ error: `Symbol path '${symbolPath}' does not resolve to a readable leaf symbol`
168
+ });
169
+ continue;
170
+ }
171
+
172
+ results.push(await client.browseResolve(current.id));
173
+ } catch (e) {
174
+ results.push({ error: e.message });
175
+ }
176
+ }
177
+
178
+ return results;
179
+ }
180
+
181
+ module.exports = { resolveSymbolicPath, parseSymbolSegments, resolveSymbolicBatch };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const TAGDESCR_ATTRIBUTE2_BITOFFSET = 0x0007;
4
+ const BITOFFSETINFO_CLASSIC = 0x08;
5
+ const BITOFFSETINFO_NONOPTBITOFFSET = 0x70;
6
+ const BITOFFSETINFO_OPTBITOFFSET = 0x07;
7
+
8
+ function getAttributeBitoffset(vte) {
9
+ return (vte.attributeFlags || 0) & TAGDESCR_ATTRIBUTE2_BITOFFSET;
10
+ }
11
+
12
+ function getBitoffsetinfoFlagClassic(vte) {
13
+ return ((vte.bitoffsetinfoFlags || 0) & BITOFFSETINFO_CLASSIC) !== 0;
14
+ }
15
+
16
+ function getBitoffsetinfoNonoptimizedBitoffset(vte) {
17
+ return ((vte.bitoffsetinfoFlags || 0) & BITOFFSETINFO_NONOPTBITOFFSET) >> 4;
18
+ }
19
+
20
+ function getBitoffsetinfoOptimizedBitoffset(vte) {
21
+ return (vte.bitoffsetinfoFlags || 0) & BITOFFSETINFO_OPTBITOFFSET;
22
+ }
23
+
24
+ function applyBoolBitoffsets(info, vte) {
25
+ const BOOL = 1;
26
+ const BBOOL = 40;
27
+ if (vte.softdatatype === BOOL) {
28
+ info.optBitoffset = getAttributeBitoffset(vte);
29
+ if (getBitoffsetinfoFlagClassic(vte)) {
30
+ info.nonOptBitoffset = getBitoffsetinfoNonoptimizedBitoffset(vte);
31
+ } else {
32
+ info.nonOptBitoffset = getAttributeBitoffset(vte);
33
+ }
34
+ } else if (vte.softdatatype === BBOOL) {
35
+ info.optBitoffset = getBitoffsetinfoOptimizedBitoffset(vte);
36
+ info.nonOptBitoffset = 0;
37
+ } else {
38
+ info.optBitoffset = 0;
39
+ info.nonOptBitoffset = 0;
40
+ }
41
+ }
42
+
43
+ module.exports = {
44
+ getAttributeBitoffset,
45
+ getBitoffsetinfoFlagClassic,
46
+ getBitoffsetinfoNonoptimizedBitoffset,
47
+ getBitoffsetinfoOptimizedBitoffset,
48
+ applyBoolBitoffsets
49
+ };