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.
- package/README.txt +69 -0
- package/node-red-contrib-mysql-extended-test/mysql-extended.html +443 -0
- package/node-red-contrib-mysql-extended-test/mysql-extended.js +355 -0
- package/node-red-contrib-rfid-helper/rfid-lazy.html +153 -0
- package/node-red-contrib-rfid-helper/rfid-lazy.js +361 -0
- package/node-red-contrib-s7-plus/LICENSE +235 -0
- package/node-red-contrib-s7-plus/README.md +141 -0
- package/node-red-contrib-s7-plus/examples/read-multiple-values-flow.json +346 -0
- package/node-red-contrib-s7-plus/examples/read-write-test-flow.json +172 -0
- package/node-red-contrib-s7-plus/examples/write-single-values-flow.json +5583 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/README.md +72 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/areas.js +36 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/datatypes.js +91 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/flat-browser.js +433 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/index.js +22 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/lazy.js +397 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/node-id.js +22 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/resolve-symbolic.js +181 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/vte-helpers.js +49 -0
- package/node-red-contrib-s7-plus/lib/s7plus/buffer-stream.js +96 -0
- package/node-red-contrib-s7-plus/lib/s7plus/client.js +1238 -0
- package/node-red-contrib-s7-plus/lib/s7plus/constants.js +155 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/index.js +3 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/s7-crc32.js +70 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/symbol-crc.js +271 -0
- package/node-red-contrib-s7-plus/lib/s7plus/debug.js +88 -0
- package/node-red-contrib-s7-plus/lib/s7plus/explore-result.js +41 -0
- package/node-red-contrib-s7-plus/lib/s7plus/item-address.js +55 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pdu-explore.js +77 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pdu-messages.js +359 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pobject.js +197 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvalue-codec.js +435 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvalue.js +653 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvar-lists.js +336 -0
- package/node-red-contrib-s7-plus/lib/s7plus/read-result.js +91 -0
- package/node-red-contrib-s7-plus/lib/s7plus/s7p.js +266 -0
- package/node-red-contrib-s7-plus/lib/s7plus/tag-routing.js +30 -0
- package/node-red-contrib-s7-plus/lib/s7plus/transport/cotp-stream.js +335 -0
- package/node-red-contrib-s7-plus/lib/s7plus/transport/s7-transport.js +335 -0
- package/node-red-contrib-s7-plus/nodes/icons/s7complus.png +0 -0
- package/node-red-contrib-s7-plus/nodes/icons/s7complus.svg +17 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-endpoint.js +825 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-explore.js +91 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-in.js +255 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-out.js +182 -0
- package/node-red-contrib-s7-plus/nodes/s7complus.html +1541 -0
- package/node-red-contrib-s7-plus/nodes/s7complus.js +10 -0
- package/node-red-contrib-s7-plus/package.json +50 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Arrays.db +17 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Binary.db +16 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_BitStrings.db +36 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_CharacterStrings.db +42 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_DateAndTime.db +70 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_FloatingPoint.db +42 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Integers.db +72 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Timers.db +40 -0
- package/node-red-contrib-s7-plus/scripts/example-flow-shared.js +39 -0
- package/node-red-contrib-s7-plus/scripts/generate-read-multiple-flow.js +331 -0
- package/node-red-contrib-s7-plus/scripts/generate-rw-test-flow.js +690 -0
- package/node-red-contrib-s7-plus/scripts/generate-write-flow.js +476 -0
- package/node-red-contrib-s7-plus/scripts/layout-write-flow.js +156 -0
- package/node-red-contrib-s7-plus/scripts/run-tests.js +16 -0
- package/node-red-flowgobal-change/variable-change.html +425 -0
- package/node-red-flowgobal-change/variable-change.js +92 -0
- package/node-red-ui_media_viewer/ui_media_viewer.html +87 -0
- package/node-red-ui_media_viewer/ui_media_viewer.js +136 -0
- 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
|
+
};
|