@vitormnm/node-red-simple-opcua 1.0.2 → 1.0.3
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/client/lib/opcua-client-browser.js +291 -0
- package/client/lib/opcua-client-read-service.js +16 -0
- package/client/lib/opcua-client-subscription-id-service.js +25 -0
- package/client/lib/opcua-client-subscription-service.js +171 -0
- package/client/lib/opcua-client-write-service.js +53 -0
- package/client/opcua-client-config.html +80 -0
- package/client/opcua-client-config.js +159 -0
- package/client/opcua-client-utils.js +320 -0
- package/client/opcua-client.html +1225 -0
- package/client/opcua-client.js +380 -0
- package/object.json +65 -0
- package/package.json +1 -5
- package/server/lib/opcua-address-space-alarm.js +341 -0
- package/server/lib/opcua-address-space-builder.js +1456 -0
- package/server/lib/opcua-config.js +546 -0
- package/server/lib/opcua-constants.js +109 -0
- package/server/lib/opcua-server-events-child.js +140 -0
- package/server/lib/opcua-server-methods.js +198 -0
- package/server/lib/opcua-server-runtime-child.js +729 -0
- package/server/lib/opcua-server-runtime.js +311 -0
- package/server/lib/opcua-server-status-child.js +188 -0
- package/server/lib/server-node-utils.js +16 -0
- package/server/opcua-server-io.html +347 -0
- package/server/opcua-server-io.js +463 -0
- package/server/opcua-server-registry.js +270 -0
- package/server/opcua-server.css +265 -0
- package/server/opcua-server.html +1548 -0
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
DATA_TYPE_MAP,
|
|
5
|
+
StatusCodes,
|
|
6
|
+
Variant,
|
|
7
|
+
VariantArrayType,
|
|
8
|
+
sanitizeNodeIdPath,
|
|
9
|
+
DataType,
|
|
10
|
+
coerceNodeId
|
|
11
|
+
} = require("./opcua-constants");
|
|
12
|
+
|
|
13
|
+
const { OpcUaAddressSpaceAlarm } = require("./opcua-address-space-alarm")
|
|
14
|
+
|
|
15
|
+
class OpcUaAddressSpaceBuilder {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.namespace = options.namespace;
|
|
18
|
+
this.namespaces = options.namespaces || new Map([[2, this.namespace]]);
|
|
19
|
+
this.server = options.server;
|
|
20
|
+
this.registry = options.registry;
|
|
21
|
+
this.node = options.node;
|
|
22
|
+
this.serverName = options.serverName;
|
|
23
|
+
this.nodeEntries = new Map();
|
|
24
|
+
this.variableStore = new Map();
|
|
25
|
+
this.variableNodeIdStore = new Map();
|
|
26
|
+
this.objectTypeStore = new Map();
|
|
27
|
+
this.alarmStore = new Map();
|
|
28
|
+
this.pendingAlarms = [];
|
|
29
|
+
|
|
30
|
+
this.addressSpace = options.addressSpace;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
this.addressSpaceAlarm = new OpcUaAddressSpaceAlarm({
|
|
35
|
+
namespace: this.namespace,
|
|
36
|
+
registry: this.registry,
|
|
37
|
+
server: this.server,
|
|
38
|
+
node: this.node
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
rebuild(treeConfig) {
|
|
43
|
+
|
|
44
|
+
this.sync(treeConfig, { fullReset: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sync(treeConfig, options) {
|
|
48
|
+
if (!this.namespace || !this.server) {
|
|
49
|
+
throw new Error("Address space is not initialized");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const settings = options || {};
|
|
53
|
+
const desiredEntries = this.collectDesiredEntries(treeConfig);
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if (settings.fullReset) {
|
|
58
|
+
this.clearDynamicNodes();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const removalRoots = this.collectRemovalRoots(desiredEntries);
|
|
62
|
+
removalRoots.forEach((path) => this.removeSubtree(path));
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
this.updateExistingNodes(desiredEntries);
|
|
67
|
+
this.createMissingNodes(desiredEntries);
|
|
68
|
+
this.flushPendingAlarms();
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
clearDynamicNodes() {
|
|
75
|
+
const paths = Array.from(this.nodeEntries.keys()).sort(comparePathDepthDesc);
|
|
76
|
+
|
|
77
|
+
paths.forEach((path) => this.removeSingleNode(path));
|
|
78
|
+
this.nodeEntries.clear();
|
|
79
|
+
this.variableStore.clear();
|
|
80
|
+
this.variableNodeIdStore.clear();
|
|
81
|
+
this.objectTypeStore.clear();
|
|
82
|
+
this.alarmStore.clear(); // Adicione esta linha
|
|
83
|
+
this.pendingAlarms = [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
readValueByPath(path) {
|
|
87
|
+
return this.getVariableRecordByPath(path).getValue();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
readValueByNodeId(nodeId) {
|
|
91
|
+
return this.getVariableRecordByNodeId(nodeId).getValue();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
eventValueByPath(valuePayload) {
|
|
95
|
+
|
|
96
|
+
const path = valuePayload.nodePath
|
|
97
|
+
const value = valuePayload.value
|
|
98
|
+
const message = valuePayload.message
|
|
99
|
+
const severity = valuePayload.severity
|
|
100
|
+
if (!this.nodeEntries.has(path)) {
|
|
101
|
+
throw new Error("Unknown path: " + path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const entry = this.nodeEntries.get(path);
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
entry.node.raiseEvent("BaseEventType", {
|
|
109
|
+
sourceName: {
|
|
110
|
+
dataType: DataType.String,
|
|
111
|
+
value: path
|
|
112
|
+
},
|
|
113
|
+
message: {
|
|
114
|
+
dataType: DataType.LocalizedText,
|
|
115
|
+
value: { text: message }
|
|
116
|
+
},
|
|
117
|
+
severity: {
|
|
118
|
+
dataType: DataType.UInt16,
|
|
119
|
+
value: severity
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
writeValueByPath(path, value) {
|
|
126
|
+
return this.getVariableRecordByPath(path).setValue(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
writeValueByNodeId(nodeId, value) {
|
|
130
|
+
return this.getVariableRecordByNodeId(nodeId).setValue(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
readValue(identifierType, identifier) {
|
|
134
|
+
return this.getVariableRecord(identifierType, identifier).getValue();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeValue(identifierType, identifier, value) {
|
|
138
|
+
return this.getVariableRecord(identifierType, identifier).setValue(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
collectDesiredEntries(treeConfig) {
|
|
142
|
+
const desiredEntries = new Map();
|
|
143
|
+
const objectTypeConfigs = this.buildObjectTypeConfigMap(treeConfig);
|
|
144
|
+
|
|
145
|
+
(Array.isArray(treeConfig.objectsTypes) ? treeConfig.objectsTypes : []).forEach((objectTypeConfig) => {
|
|
146
|
+
this.collectObjectTypeDefinition(desiredEntries, objectTypeConfig, objectTypeConfigs);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
(Array.isArray(treeConfig.folders) ? treeConfig.folders : []).forEach((folderConfig) => {
|
|
150
|
+
this.collectBranch(desiredEntries, "folder", folderConfig, "", "organizedBy", objectTypeConfigs);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
(Array.isArray(treeConfig.objects) ? treeConfig.objects : []).forEach((objectConfig) => {
|
|
154
|
+
this.collectBranch(desiredEntries, "object", objectConfig, "", "organizedBy", objectTypeConfigs);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return desiredEntries;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
buildObjectTypeConfigMap(treeConfig) {
|
|
161
|
+
const objectTypeConfigs = new Map();
|
|
162
|
+
|
|
163
|
+
(Array.isArray(treeConfig.objectsTypes) ? treeConfig.objectsTypes : []).forEach((objectTypeConfig) => {
|
|
164
|
+
if (objectTypeConfigs.has(objectTypeConfig.name)) {
|
|
165
|
+
throw new Error("Duplicate object type name: " + objectTypeConfig.name);
|
|
166
|
+
}
|
|
167
|
+
objectTypeConfigs.set(objectTypeConfig.name, objectTypeConfig);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return objectTypeConfigs;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
collectObjectTypeDefinition(desiredEntries, config, objectTypeConfigs) {
|
|
174
|
+
const path = this.buildObjectTypePath(config.name);
|
|
175
|
+
desiredEntries.set(path, this.buildEntryDefinition("objectTypeDefinition", config, path, "", "typeDefinition"));
|
|
176
|
+
this.collectBranchChildren(desiredEntries, config, path, "componentOf", objectTypeConfigs, {
|
|
177
|
+
skipAlarms: false,
|
|
178
|
+
preserveCollectionNames: true,
|
|
179
|
+
typeRootPath: path
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
collectBranch(desiredEntries, kind, config, parentPath, relationship, objectTypeConfigs, options) {
|
|
184
|
+
const settings = options || {};
|
|
185
|
+
const path = settings.preserveCollectionNames
|
|
186
|
+
? this.buildCollectionPath(parentPath, kind === "folder" ? "folders" : "objects", config.name)
|
|
187
|
+
: this.buildPath(parentPath, config.name);
|
|
188
|
+
desiredEntries.set(path, this.buildEntryDefinition(kind, config, path, parentPath, relationship));
|
|
189
|
+
this.collectBranchChildren(
|
|
190
|
+
desiredEntries,
|
|
191
|
+
config,
|
|
192
|
+
path,
|
|
193
|
+
kind === "folder" ? "organizedBy" : "componentOf",
|
|
194
|
+
objectTypeConfigs,
|
|
195
|
+
settings
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
collectBranchChildren(desiredEntries, config, path, childRelationship, objectTypeConfigs, options) {
|
|
200
|
+
const settings = options || {};
|
|
201
|
+
const preserveCollectionNames = !!settings.preserveCollectionNames;
|
|
202
|
+
(Array.isArray(config.folders) ? config.folders : []).forEach((folderConfig) => {
|
|
203
|
+
this.collectBranch(
|
|
204
|
+
desiredEntries,
|
|
205
|
+
"folder",
|
|
206
|
+
folderConfig,
|
|
207
|
+
path,
|
|
208
|
+
childRelationship,
|
|
209
|
+
objectTypeConfigs,
|
|
210
|
+
settings
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
(Array.isArray(config.objects) ? config.objects : []).forEach((objectConfig) => {
|
|
215
|
+
this.collectBranch(
|
|
216
|
+
desiredEntries,
|
|
217
|
+
"object",
|
|
218
|
+
objectConfig,
|
|
219
|
+
path,
|
|
220
|
+
childRelationship,
|
|
221
|
+
objectTypeConfigs,
|
|
222
|
+
settings
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
(Array.isArray(config.variables) ? config.variables : []).forEach((variableConfig) => {
|
|
227
|
+
const childPath = preserveCollectionNames
|
|
228
|
+
? this.buildCollectionPath(path, "variables", variableConfig.name)
|
|
229
|
+
: this.buildPath(path, variableConfig.name);
|
|
230
|
+
desiredEntries.set(
|
|
231
|
+
childPath,
|
|
232
|
+
this.buildEntryDefinition("variable", variableConfig, childPath, path, "componentOf")
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
(Array.isArray(config.methods) ? config.methods : []).forEach((methodConfig) => {
|
|
237
|
+
const childPath = preserveCollectionNames
|
|
238
|
+
? this.buildCollectionPath(path, "methods", methodConfig.name)
|
|
239
|
+
: this.buildPath(path, methodConfig.name);
|
|
240
|
+
desiredEntries.set(
|
|
241
|
+
childPath,
|
|
242
|
+
this.buildEntryDefinition("method", methodConfig, childPath, path, "componentOf")
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
(Array.isArray(config.objectsTypes) ? config.objectsTypes : []).forEach((objectTypeInstanceConfig) => {
|
|
247
|
+
this.collectObjectTypeInstance(
|
|
248
|
+
desiredEntries,
|
|
249
|
+
objectTypeInstanceConfig,
|
|
250
|
+
path,
|
|
251
|
+
childRelationship,
|
|
252
|
+
objectTypeConfigs,
|
|
253
|
+
settings
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (!settings.skipAlarms) {
|
|
258
|
+
//alarmes
|
|
259
|
+
(Array.isArray(config.alarms) ? config.alarms : []).forEach((alarmConfig) => {
|
|
260
|
+
const childPath = preserveCollectionNames
|
|
261
|
+
? this.buildCollectionPath(path, "alarms", alarmConfig.name)
|
|
262
|
+
: this.buildPath(path, alarmConfig.name);
|
|
263
|
+
|
|
264
|
+
desiredEntries.set(
|
|
265
|
+
childPath,
|
|
266
|
+
this.buildEntryDefinition("alarm", alarmConfig, childPath, path, "componentOf")
|
|
267
|
+
);
|
|
268
|
+
//this.addAlarmPlaceholder(path, alarmConfig);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
collectObjectTypeInstance(desiredEntries, instanceConfig, parentPath, relationship, objectTypeConfigs, options) {
|
|
274
|
+
const path = this.buildPath(parentPath, instanceConfig.name);
|
|
275
|
+
desiredEntries.set(
|
|
276
|
+
path,
|
|
277
|
+
this.buildEntryDefinition("objectTypeInstance", instanceConfig, path, parentPath, relationship)
|
|
278
|
+
);
|
|
279
|
+
// Variables, methods and nested objectsTypes inherited from the type definition
|
|
280
|
+
// are instantiated automatically by node-opcua when addObject({ typeDefinition })
|
|
281
|
+
// is called. We must NOT add them as independent desiredEntries — that would
|
|
282
|
+
// create duplicate nodes on top of the ones the lib already created.
|
|
283
|
+
// Only extra children defined directly on the instance (not inherited) are added here.
|
|
284
|
+
const instanceOnlyConfig = {
|
|
285
|
+
folders: Array.isArray(instanceConfig.folders) ? instanceConfig.folders : [],
|
|
286
|
+
objects: Array.isArray(instanceConfig.objects) ? instanceConfig.objects : [],
|
|
287
|
+
variables: Array.isArray(instanceConfig.variables) ? instanceConfig.variables : [],
|
|
288
|
+
methods: Array.isArray(instanceConfig.methods) ? instanceConfig.methods : [],
|
|
289
|
+
alarms: Array.isArray(instanceConfig.alarms) ? instanceConfig.alarms : [],
|
|
290
|
+
objectsTypes: Array.isArray(instanceConfig.objectsTypes) ? instanceConfig.objectsTypes : []
|
|
291
|
+
};
|
|
292
|
+
this.collectBranchChildren(desiredEntries, instanceOnlyConfig, path, "componentOf", objectTypeConfigs, Object.assign({}, options, {
|
|
293
|
+
skipAlarms: false,
|
|
294
|
+
preserveCollectionNames: false
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
expandObjectTypeInstanceConfig(instanceConfig, objectTypeConfigs, stack) {
|
|
299
|
+
const typeName = instanceConfig.objectsType;
|
|
300
|
+
if (stack.indexOf(typeName) !== -1) {
|
|
301
|
+
throw new Error("Circular object type reference: " + stack.concat(typeName).join(" -> "));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const baseConfig = objectTypeConfigs.get(typeName);
|
|
305
|
+
if (!baseConfig) {
|
|
306
|
+
throw new Error("Unknown object type: " + typeName);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return this.mergeObjectTypeConfigs(baseConfig, instanceConfig, objectTypeConfigs, stack.concat(typeName));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
mergeObjectTypeConfigs(baseConfig, instanceConfig, objectTypeConfigs, stack) {
|
|
313
|
+
const merged = {
|
|
314
|
+
name: instanceConfig.name,
|
|
315
|
+
displayName: instanceConfig.displayName || instanceConfig.name,
|
|
316
|
+
description: instanceConfig.description || "",
|
|
317
|
+
folders: this.cloneConfigs(baseConfig.folders),
|
|
318
|
+
objects: this.cloneConfigs(baseConfig.objects),
|
|
319
|
+
variables: this.cloneConfigs(baseConfig.variables),
|
|
320
|
+
methods: this.cloneConfigs(baseConfig.methods),
|
|
321
|
+
alarms: this.cloneConfigs(baseConfig.alarms),
|
|
322
|
+
objectsTypes: []
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const baseInstances = Array.isArray(baseConfig.objectsTypes) ? baseConfig.objectsTypes : [];
|
|
326
|
+
const instanceOverrides = Array.isArray(instanceConfig.objectsTypes) ? instanceConfig.objectsTypes : [];
|
|
327
|
+
|
|
328
|
+
merged.objectsTypes = baseInstances
|
|
329
|
+
.map((nestedInstanceConfig) => this.mergeNestedObjectTypeInstance(nestedInstanceConfig, objectTypeConfigs, stack))
|
|
330
|
+
.concat(this.cloneConfigs(instanceOverrides));
|
|
331
|
+
|
|
332
|
+
merged.folders = merged.folders.concat(this.cloneConfigs(instanceConfig.folders));
|
|
333
|
+
merged.objects = merged.objects.concat(this.cloneConfigs(instanceConfig.objects));
|
|
334
|
+
merged.variables = merged.variables.concat(this.cloneConfigs(instanceConfig.variables));
|
|
335
|
+
merged.methods = merged.methods.concat(this.cloneConfigs(instanceConfig.methods));
|
|
336
|
+
merged.alarms = merged.alarms.concat(this.cloneConfigs(instanceConfig.alarms));
|
|
337
|
+
|
|
338
|
+
return merged;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
mergeNestedObjectTypeInstance(instanceConfig, objectTypeConfigs, stack) {
|
|
342
|
+
const cloned = this.cloneConfig(instanceConfig);
|
|
343
|
+
if (cloned && cloned.objectsType) {
|
|
344
|
+
const typeName = cloned.objectsType;
|
|
345
|
+
if (stack.indexOf(typeName) !== -1) {
|
|
346
|
+
throw new Error("Circular object type reference: " + stack.concat(typeName).join(" -> "));
|
|
347
|
+
}
|
|
348
|
+
if (!objectTypeConfigs.has(typeName)) {
|
|
349
|
+
throw new Error("Unknown object type: " + typeName);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return cloned;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
cloneConfigs(items) {
|
|
356
|
+
return Array.isArray(items) ? items.map((item) => this.cloneConfig(item)) : [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
cloneConfig(item) {
|
|
360
|
+
return item ? JSON.parse(JSON.stringify(item)) : item;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
rewriteAlarmVariablePaths(branchConfig, currentPath, rootInstancePath) {
|
|
364
|
+
if (!branchConfig || typeof branchConfig !== "object") {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
(Array.isArray(branchConfig.alarms) ? branchConfig.alarms : []).forEach((alarmConfig) => {
|
|
369
|
+
const variableNodeId = String(alarmConfig.variableNodeId || "").trim();
|
|
370
|
+
if (!variableNodeId) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (variableNodeId.indexOf(".") === 0) {
|
|
375
|
+
alarmConfig.variableNodeId = this.resolveObjectTypeRelativeReference(rootInstancePath, variableNodeId);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (variableNodeId.indexOf(currentPath + ".") === 0 || variableNodeId === currentPath) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (variableNodeId.indexOf(rootInstancePath + ".") === 0 || variableNodeId === rootInstancePath) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (variableNodeId.indexOf("__objectTypes.") === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
alarmConfig.variableNodeId = currentPath + "." + variableNodeId;
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
(Array.isArray(branchConfig.folders) ? branchConfig.folders : []).forEach((folderConfig) => {
|
|
395
|
+
this.rewriteAlarmVariablePaths(folderConfig, this.buildPath(currentPath, folderConfig.name), rootInstancePath);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
(Array.isArray(branchConfig.objects) ? branchConfig.objects : []).forEach((objectConfig) => {
|
|
399
|
+
this.rewriteAlarmVariablePaths(objectConfig, this.buildPath(currentPath, objectConfig.name), rootInstancePath);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
buildEntryDefinition(kind, config, path, parentPath, relationship) {
|
|
404
|
+
return {
|
|
405
|
+
kind,
|
|
406
|
+
path,
|
|
407
|
+
parentPath,
|
|
408
|
+
relationship,
|
|
409
|
+
config,
|
|
410
|
+
signature: this.buildSignature(kind, config, parentPath, relationship)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
buildSignature(kind, config, parentPath, relationship) {
|
|
415
|
+
if (kind === "variable") {
|
|
416
|
+
return JSON.stringify({
|
|
417
|
+
kind,
|
|
418
|
+
parentPath,
|
|
419
|
+
relationship,
|
|
420
|
+
nodeId: config.nodeId || "",
|
|
421
|
+
namespaceId: this.resolveNamespaceId(config),
|
|
422
|
+
displayName: config.displayName || config.name,
|
|
423
|
+
description: config.description || "",
|
|
424
|
+
type: config.type,
|
|
425
|
+
access: config.access,
|
|
426
|
+
isArray: this.isArrayValue(config.value)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (kind === "method") {
|
|
431
|
+
return JSON.stringify({
|
|
432
|
+
kind,
|
|
433
|
+
parentPath,
|
|
434
|
+
relationship,
|
|
435
|
+
nodeId: config.nodeId || "",
|
|
436
|
+
namespaceId: this.resolveNamespaceId(config),
|
|
437
|
+
displayName: config.displayName || config.name,
|
|
438
|
+
description: config.description || "",
|
|
439
|
+
inputs: Array.isArray(config.inputs) ? config.inputs : [],
|
|
440
|
+
outputs: Array.isArray(config.outputs) ? config.outputs : []
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (kind === "objectTypeInstance") {
|
|
445
|
+
return JSON.stringify({
|
|
446
|
+
kind,
|
|
447
|
+
parentPath,
|
|
448
|
+
relationship,
|
|
449
|
+
nodeId: config.nodeId || "",
|
|
450
|
+
namespaceId: this.resolveNamespaceId(config),
|
|
451
|
+
displayName: config.displayName || config.name,
|
|
452
|
+
description: config.description || "",
|
|
453
|
+
objectsType: config.objectsType
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return JSON.stringify({
|
|
458
|
+
kind,
|
|
459
|
+
parentPath,
|
|
460
|
+
relationship,
|
|
461
|
+
nodeId: config.nodeId || "",
|
|
462
|
+
namespaceId: this.resolveNamespaceId(config),
|
|
463
|
+
displayName: config.displayName || config.name,
|
|
464
|
+
description: config.description || ""
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
collectRemovalRoots(desiredEntries) {
|
|
469
|
+
const candidates = [];
|
|
470
|
+
|
|
471
|
+
this.nodeEntries.forEach((entry, path) => {
|
|
472
|
+
const desired = desiredEntries.get(path);
|
|
473
|
+
|
|
474
|
+
if (!desired) {
|
|
475
|
+
candidates.push(path);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (entry.kind !== desired.kind || entry.signature !== desired.signature) {
|
|
480
|
+
candidates.push(path);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
return collapsePaths(candidates);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
updateExistingNodes(desiredEntries) {
|
|
488
|
+
desiredEntries.forEach((desired, path) => {
|
|
489
|
+
const existing = this.nodeEntries.get(path);
|
|
490
|
+
if (!existing || existing.kind !== desired.kind) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (existing.kind === "variable") {
|
|
495
|
+
const record = this.variableStore.get(path);
|
|
496
|
+
if (record) {
|
|
497
|
+
record.setRuntimeValue(desired.config.value);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
createMissingNodes(desiredEntries) {
|
|
504
|
+
const ordered = Array.from(desiredEntries.values()).sort(compareEntryCreationOrder);
|
|
505
|
+
ordered.forEach((definition) => {
|
|
506
|
+
if (this.nodeEntries.has(definition.path)) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
this.createNode(definition);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
createNode(definition) {
|
|
515
|
+
const parentNode = this.resolveParentNode(definition.parentPath);
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
if (definition.kind === "folder") {
|
|
520
|
+
this.addFolder(parentNode, definition.config, definition.parentPath, definition.relationship, definition.path);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (definition.kind === "objectTypeDefinition") {
|
|
525
|
+
|
|
526
|
+
this.addObjectTypeDefinition(definition.config);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (definition.kind === "object") {
|
|
531
|
+
this.addObject(parentNode, definition.config, definition.parentPath, definition.relationship, definition.path);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (definition.kind === "objectTypeInstance") {
|
|
536
|
+
this.addObjectTypeInstance(parentNode, definition.config, definition.parentPath, definition.relationship, definition.path);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (definition.kind === "alarm") {
|
|
541
|
+
this.addAlarm(parentNode, definition.config, definition.parentPath, definition.relationship, definition.path);
|
|
542
|
+
return;
|
|
543
|
+
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (definition.kind === "variable") {
|
|
547
|
+
this.addVariable(parentNode, definition.config, definition.parentPath, definition.path);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (definition.kind === "method") {
|
|
552
|
+
this.addMethod(parentNode, definition.config, definition.parentPath, definition.path);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
resolveParentNode(parentPath) {
|
|
559
|
+
if (!parentPath) {
|
|
560
|
+
return this.server.engine.addressSpace.rootFolder.objects;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const parentEntry = this.nodeEntries.get(parentPath);
|
|
564
|
+
if (!parentEntry || !parentEntry.node) {
|
|
565
|
+
throw new Error("Parent node is not available for path: " + parentPath);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return parentEntry.node;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
removeSubtree(rootPath) {
|
|
572
|
+
const affectedPaths = Array.from(this.nodeEntries.keys())
|
|
573
|
+
.filter((path) => isSamePathOrDescendant(path, rootPath))
|
|
574
|
+
.sort(comparePathDepthDesc);
|
|
575
|
+
|
|
576
|
+
affectedPaths.forEach((path) => this.removeSingleNode(path));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
removeSingleNode(path) {
|
|
580
|
+
const entry = this.nodeEntries.get(path);
|
|
581
|
+
if (!entry) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (entry.kind === "variable") {
|
|
586
|
+
const record = this.variableStore.get(path);
|
|
587
|
+
if (record && record.nodeIdKey) {
|
|
588
|
+
this.variableNodeIdStore.delete(record.nodeIdKey);
|
|
589
|
+
}
|
|
590
|
+
this.variableStore.delete(path);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (entry.kind === "objectTypeDefinition") {
|
|
594
|
+
this.objectTypeStore.delete(entry.config.name);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
entry.namespace.deleteNode(entry.node);
|
|
599
|
+
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.error("error removeSingleNode")
|
|
602
|
+
console.error(error)
|
|
603
|
+
this.node.debug("Ignoring deleteNode error for " + path + ": " + error.message);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
this.nodeEntries.delete(path);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
addObject(parentNode, objectConfig, parentPath, relationship, pathOverride) {
|
|
612
|
+
const namespace = this.getNamespaceForConfig(objectConfig);
|
|
613
|
+
|
|
614
|
+
const addressSpace = this.server.engine.addressSpace;
|
|
615
|
+
const serverNode = addressSpace.rootFolder.objects.server;
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
const objectName = objectConfig.name;
|
|
619
|
+
const nextPath = pathOverride || this.buildPath(parentPath, objectName);
|
|
620
|
+
const options = {
|
|
621
|
+
browseName: objectConfig.displayName || objectName,
|
|
622
|
+
displayName: objectConfig.displayName || objectName,
|
|
623
|
+
description: objectConfig.description || "",
|
|
624
|
+
nodeId: this.resolveNodeId(objectConfig, nextPath, namespace),
|
|
625
|
+
eventNotifier: 1, //enabled_events,
|
|
626
|
+
eventSourceOf: serverNode
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
if (relationship === "organizedBy") {
|
|
630
|
+
options.organizedBy = parentNode;
|
|
631
|
+
} else {
|
|
632
|
+
options.componentOf = parentNode;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (this.isObjectTypePath(parentPath)) {
|
|
636
|
+
options.modellingRule = "Mandatory";
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const objectNode = namespace.addObject(options);
|
|
640
|
+
this.registerNodeEntry("object", nextPath, parentPath, relationship, objectConfig, objectNode, namespace);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
addObjectTypeDefinition(objectTypeConfig) {
|
|
644
|
+
const namespace = this.getNamespaceForConfig(objectTypeConfig);
|
|
645
|
+
const objectTypeNode = namespace.addObjectType({
|
|
646
|
+
browseName: objectTypeConfig.name,
|
|
647
|
+
displayName: objectTypeConfig.displayName || objectTypeConfig.name,
|
|
648
|
+
description: objectTypeConfig.description || "",
|
|
649
|
+
nodeId: this.resolveNodeId(objectTypeConfig, this.buildObjectTypePath(objectTypeConfig.name), namespace),
|
|
650
|
+
subtypeOf: "BaseObjectType"
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
const path = this.buildObjectTypePath(objectTypeConfig.name);
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
this.registerNodeEntry("objectTypeDefinition", path, "", "typeDefinition", objectTypeConfig, objectTypeNode, namespace);
|
|
659
|
+
this.objectTypeStore.set(objectTypeConfig.name, {
|
|
660
|
+
node: objectTypeNode,
|
|
661
|
+
config: objectTypeConfig,
|
|
662
|
+
path: path,
|
|
663
|
+
namespace: namespace
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
addObjectTypeInstance(parentNode, instanceConfig, parentPath, relationship, pathOverride) {
|
|
668
|
+
const objectTypeEntry = this.objectTypeStore.get(instanceConfig.objectsType);
|
|
669
|
+
if (!objectTypeEntry || !objectTypeEntry.node) {
|
|
670
|
+
throw new Error("Object type is not available for instance " + instanceConfig.name + ": " + instanceConfig.objectsType);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const objectName = instanceConfig.name;
|
|
674
|
+
const nextPath = pathOverride || this.buildPath(parentPath, objectName);
|
|
675
|
+
const namespace = this.getNamespaceForConfig(instanceConfig);
|
|
676
|
+
|
|
677
|
+
const options = {
|
|
678
|
+
browseName: instanceConfig.displayName || objectName,
|
|
679
|
+
displayName: instanceConfig.displayName || objectName,
|
|
680
|
+
description: instanceConfig.description || "",
|
|
681
|
+
nodeId: this.resolveNodeId(instanceConfig, nextPath, namespace),
|
|
682
|
+
typeDefinition: objectTypeEntry.node.nodeId,
|
|
683
|
+
eventNotifier: 1
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
if (relationship === "organizedBy") {
|
|
687
|
+
options.organizedBy = parentNode;
|
|
688
|
+
} else {
|
|
689
|
+
options.componentOf = parentNode;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const objectNode = namespace.addObject(options);
|
|
693
|
+
this.registerNodeEntry("objectTypeInstance", nextPath, parentPath, relationship, instanceConfig, objectNode, namespace);
|
|
694
|
+
|
|
695
|
+
// Create the inherited children explicitly using the configs that opcua-config.js
|
|
696
|
+
// already rewrote with the correct instance nodeIds (e.g. server1.newObjectType.cmd_on).
|
|
697
|
+
// We do NOT call instantiate() because node-opcua cannot auto-assign nodeIds for the
|
|
698
|
+
// cloned children when the type children already have explicit string nodeIds registered.
|
|
699
|
+
this.createInheritedChildren(objectNode, nextPath, instanceConfig, namespace);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
createInheritedChildren(instanceNode, instancePath, instanceConfig, namespace) {
|
|
703
|
+
const typeEntry = this.objectTypeStore.get(instanceConfig.objectsType);
|
|
704
|
+
if (!typeEntry) return;
|
|
705
|
+
|
|
706
|
+
// Extract the type's nodeId value prefix (e.g. "Motor_type2") and the
|
|
707
|
+
// instance's nodeId value prefix (e.g. "server1.newObjectType") so we can
|
|
708
|
+
// rewrite every child nodeId from the type to the instance on-the-fly.
|
|
709
|
+
const typeNodeId = typeEntry.config.nodeId || "";
|
|
710
|
+
const instanceNodeId = instanceConfig.nodeId || "";
|
|
711
|
+
const typePrefix = this.extractNodeIdStringValue(typeNodeId);
|
|
712
|
+
const instancePrefix = this.extractNodeIdStringValue(instanceNodeId);
|
|
713
|
+
|
|
714
|
+
this.createInheritedBranchChildren(typeEntry.config, instanceNode, instancePath, typePrefix, instancePrefix);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
extractNodeIdStringValue(nodeId) {
|
|
718
|
+
const m = String(nodeId || "").match(/(?:^|;)s=(.+)$/);
|
|
719
|
+
return m ? m[1] : "";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
rewriteInheritedNodeId(nodeId, typePrefix, instancePrefix) {
|
|
723
|
+
if (!nodeId || !typePrefix || !instancePrefix) return nodeId;
|
|
724
|
+
const m = String(nodeId).match(/^(ns=\d+;s=)([\s\S]*)$/);
|
|
725
|
+
if (m && m[2].startsWith(typePrefix)) {
|
|
726
|
+
return m[1] + instancePrefix + m[2].slice(typePrefix.length);
|
|
727
|
+
}
|
|
728
|
+
return nodeId;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
createInheritedBranchChildren(typeConfig, parentOpcNode, parentPath, typePrefix, instancePrefix) {
|
|
732
|
+
const variables = Array.isArray(typeConfig.variables) ? typeConfig.variables : [];
|
|
733
|
+
const methods = Array.isArray(typeConfig.methods) ? typeConfig.methods : [];
|
|
734
|
+
const folders = Array.isArray(typeConfig.folders) ? typeConfig.folders : [];
|
|
735
|
+
const objects = Array.isArray(typeConfig.objects) ? typeConfig.objects : [];
|
|
736
|
+
|
|
737
|
+
variables.forEach((varConfig) => {
|
|
738
|
+
const childPath = this.buildPath(parentPath, varConfig.name);
|
|
739
|
+
const rewrittenConfig = Object.assign({}, varConfig, {
|
|
740
|
+
nodeId: this.rewriteInheritedNodeId(varConfig.nodeId, typePrefix, instancePrefix)
|
|
741
|
+
});
|
|
742
|
+
this.addVariable(parentOpcNode, rewrittenConfig, parentPath, childPath);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
methods.forEach((methodConfig) => {
|
|
746
|
+
const childPath = this.buildPath(parentPath, methodConfig.name);
|
|
747
|
+
const rewrittenConfig = Object.assign({}, methodConfig, {
|
|
748
|
+
nodeId: this.rewriteInheritedNodeId(methodConfig.nodeId, typePrefix, instancePrefix)
|
|
749
|
+
});
|
|
750
|
+
this.addMethod(parentOpcNode, rewrittenConfig, parentPath, childPath);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
folders.forEach((folderConfig) => {
|
|
754
|
+
const childPath = this.buildPath(parentPath, folderConfig.name);
|
|
755
|
+
const rewrittenConfig = Object.assign({}, folderConfig, {
|
|
756
|
+
nodeId: this.rewriteInheritedNodeId(folderConfig.nodeId, typePrefix, instancePrefix)
|
|
757
|
+
});
|
|
758
|
+
this.addFolder(parentOpcNode, rewrittenConfig, parentPath, "componentOf", childPath);
|
|
759
|
+
const childNode = this.nodeEntries.get(childPath) && this.nodeEntries.get(childPath).node;
|
|
760
|
+
if (childNode) {
|
|
761
|
+
this.createInheritedBranchChildren(folderConfig, childNode, childPath, typePrefix, instancePrefix);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
objects.forEach((objectConfig) => {
|
|
766
|
+
const childPath = this.buildPath(parentPath, objectConfig.name);
|
|
767
|
+
const rewrittenConfig = Object.assign({}, objectConfig, {
|
|
768
|
+
nodeId: this.rewriteInheritedNodeId(objectConfig.nodeId, typePrefix, instancePrefix)
|
|
769
|
+
});
|
|
770
|
+
this.addObject(parentOpcNode, rewrittenConfig, parentPath, "componentOf", childPath);
|
|
771
|
+
const childNode = this.nodeEntries.get(childPath) && this.nodeEntries.get(childPath).node;
|
|
772
|
+
if (childNode) {
|
|
773
|
+
this.createInheritedBranchChildren(objectConfig, childNode, childPath, typePrefix, instancePrefix);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
addAlarm(parentNode, alarmConfig, parentPath, relationship, pathOverride) {
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
|
|
784
|
+
const name = alarmConfig.name;
|
|
785
|
+
const variableNodeId = alarmConfig.variableNodeId;
|
|
786
|
+
const objectName = alarmConfig.name;
|
|
787
|
+
const nextPath = pathOverride || this.buildPath(parentPath, objectName);
|
|
788
|
+
const path = pathOverride || this.buildPath(parentPath, name);
|
|
789
|
+
const namespace = this.getNamespaceForConfig(alarmConfig);
|
|
790
|
+
const options = {
|
|
791
|
+
browseName: alarmConfig.displayName || objectName,
|
|
792
|
+
displayName: alarmConfig.displayName || objectName,
|
|
793
|
+
description: alarmConfig.description || "",
|
|
794
|
+
nodeId: this.resolveNodeId(alarmConfig, nextPath, namespace),
|
|
795
|
+
eventNotifier: 1 //enabled_events
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
if (relationship === "organizedBy") {
|
|
799
|
+
options.organizedBy = parentNode;
|
|
800
|
+
} else {
|
|
801
|
+
options.componentOf = parentNode;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const variableToMonitor = this.resolveAlarmVariableRecord(parentPath, parentNode, variableNodeId)
|
|
805
|
+
|
|
806
|
+
const sourceName = variableToMonitor.node.displayName[0].text
|
|
807
|
+
|
|
808
|
+
const browseName = alarmConfig.displayName || objectName
|
|
809
|
+
const inputNode = variableToMonitor.node
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
const conditionName = alarmConfig.displayName || objectName
|
|
814
|
+
const nodeId = this.resolveNodeId(alarmConfig, nextPath, namespace)
|
|
815
|
+
|
|
816
|
+
const alarmNode = this.addressSpaceAlarm.createAlarm(namespace, browseName, parentNode, inputNode, conditionName, nodeId, sourceName, alarmConfig)
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
// register alarm
|
|
822
|
+
variableToMonitor.alarm = {
|
|
823
|
+
node: alarmNode,
|
|
824
|
+
alarmConfig: alarmConfig,
|
|
825
|
+
}
|
|
826
|
+
// const alarmNode = this.namespace.instantiateOffNormalAlarm(options);
|
|
827
|
+
this.registerNodeEntry("alarm", nextPath, parentPath, relationship, alarmConfig, alarmNode, namespace);
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
} catch (error) {
|
|
833
|
+
|
|
834
|
+
if (error && error.message && error.message.indexOf("Unknown alarm variable reference:") === 0) {
|
|
835
|
+
this.queuePendingAlarm(parentNode, alarmConfig, parentPath, relationship);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
console.error("erro addAlarm")
|
|
839
|
+
console.error(error)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
queuePendingAlarm(parentNode, alarmConfig, parentPath, relationship) {
|
|
846
|
+
const path = this.buildPath(parentPath, alarmConfig.name);
|
|
847
|
+
const alreadyQueued = this.pendingAlarms.some((item) => item.path === path);
|
|
848
|
+
if (alreadyQueued) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
this.pendingAlarms.push({
|
|
853
|
+
path: path,
|
|
854
|
+
parentNode: parentNode,
|
|
855
|
+
alarmConfig: this.cloneConfig(alarmConfig),
|
|
856
|
+
parentPath: parentPath,
|
|
857
|
+
relationship: relationship
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
flushPendingAlarms() {
|
|
862
|
+
if (!this.pendingAlarms.length) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const pending = this.pendingAlarms.slice();
|
|
867
|
+
this.pendingAlarms = [];
|
|
868
|
+
|
|
869
|
+
pending.forEach((item) => {
|
|
870
|
+
if (this.nodeEntries.has(item.path)) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
this.addAlarm(item.parentNode, item.alarmConfig, item.parentPath, item.relationship);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
if (this.pendingAlarms.length) {
|
|
878
|
+
const unresolved = this.pendingAlarms.map((item) => item.path).join(", ");
|
|
879
|
+
this.pendingAlarms = [];
|
|
880
|
+
throw new Error("Unresolved alarms after build: " + unresolved);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
addFolder(parentNode, folderConfig, parentPath, relationship, pathOverride) {
|
|
888
|
+
const namespace = this.getNamespaceForConfig(folderConfig);
|
|
889
|
+
|
|
890
|
+
const addressSpace = this.server.engine.addressSpace;
|
|
891
|
+
const serverNode = addressSpace.rootFolder.objects.server;
|
|
892
|
+
|
|
893
|
+
const folderName = folderConfig.name;
|
|
894
|
+
const nextPath = pathOverride || this.buildPath(parentPath, folderName);
|
|
895
|
+
const options = {
|
|
896
|
+
browseName: folderConfig.displayName || folderName,
|
|
897
|
+
displayName: folderConfig.displayName || folderName,
|
|
898
|
+
description: folderConfig.description || "",
|
|
899
|
+
nodeId: this.resolveNodeId(folderConfig, nextPath, namespace),
|
|
900
|
+
typeDefinition: "FolderType",
|
|
901
|
+
eventSourceOf: serverNode,
|
|
902
|
+
eventNotifier: 1 //enabled_events
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
if (relationship === "organizedBy") {
|
|
906
|
+
options.organizedBy = parentNode;
|
|
907
|
+
} else {
|
|
908
|
+
options.componentOf = parentNode;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (this.isObjectTypePath(parentPath)) {
|
|
912
|
+
options.modellingRule = "Mandatory";
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const folderNode = namespace.addObject(options);
|
|
916
|
+
this.registerNodeEntry("folder", nextPath, parentPath, relationship, folderConfig, folderNode, namespace);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
addVariable(parentNode, variableConfig, parentPath, pathOverride) {
|
|
920
|
+
const namespace = this.getNamespaceForConfig(variableConfig);
|
|
921
|
+
const name = variableConfig.name;
|
|
922
|
+
const type = variableConfig.type;
|
|
923
|
+
const access = variableConfig.access;
|
|
924
|
+
const path = pathOverride || this.buildPath(parentPath, name);
|
|
925
|
+
const nodeId = this.resolveNodeId(variableConfig, path, namespace);
|
|
926
|
+
const browseName = variableConfig.displayName || name;
|
|
927
|
+
const state = {
|
|
928
|
+
type,
|
|
929
|
+
access,
|
|
930
|
+
isArray: this.isArrayValue(variableConfig.value),
|
|
931
|
+
currentValue: variableConfig.value
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const variableNode = namespace.addVariable({
|
|
935
|
+
componentOf: parentNode,
|
|
936
|
+
browseName,
|
|
937
|
+
displayName: browseName,
|
|
938
|
+
description: variableConfig.description || "",
|
|
939
|
+
nodeId,
|
|
940
|
+
dataType: type,
|
|
941
|
+
modellingRule: this.isObjectTypePath(parentPath) ? "Mandatory" : undefined,
|
|
942
|
+
valueRank: state.isArray ? 1 : -1,
|
|
943
|
+
accessLevel: access === "readwrite" ? "CurrentRead | CurrentWrite" : "CurrentRead",
|
|
944
|
+
userAccessLevel: access === "readwrite" ? "CurrentRead | CurrentWrite" : "CurrentRead",
|
|
945
|
+
minimumSamplingInterval: 500,
|
|
946
|
+
value: {
|
|
947
|
+
get: () => {
|
|
948
|
+
this.emitTagAccess("read", {
|
|
949
|
+
path,
|
|
950
|
+
nodeID: nodeId,
|
|
951
|
+
browseName,
|
|
952
|
+
dataType: state.type,
|
|
953
|
+
value: state.currentValue
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const variantOptions = {
|
|
957
|
+
dataType: DATA_TYPE_MAP[state.type],
|
|
958
|
+
value: state.currentValue
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
if (this.isArrayValue(state.currentValue)) {
|
|
962
|
+
variantOptions.arrayType = VariantArrayType.Array;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return new Variant(variantOptions);
|
|
966
|
+
},
|
|
967
|
+
set: (variant) => {
|
|
968
|
+
if (state.access !== "readwrite") {
|
|
969
|
+
return StatusCodes.BadNotWritable;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
try {
|
|
973
|
+
state.currentValue = this.coerceValue(variant.value, state.type, state.isArray);
|
|
974
|
+
this.emitTagAccess("write", {
|
|
975
|
+
path,
|
|
976
|
+
nodeID: nodeId,
|
|
977
|
+
browseName,
|
|
978
|
+
dataType: state.type,
|
|
979
|
+
value: state.currentValue
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const alarm = this.variableStore.get(path).alarm
|
|
983
|
+
this.addressSpaceAlarm.checkAlarm(alarm, variant.value)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
return StatusCodes.Good;
|
|
988
|
+
} catch (error) {
|
|
989
|
+
console.error("addVariable")
|
|
990
|
+
console.error(error)
|
|
991
|
+
// this.node.warn("Rejected OPC UA write for " + path + ": " + error.message);
|
|
992
|
+
return StatusCodes.BadTypeMismatch;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
this.registerNodeEntry("variable", path, parentPath, "componentOf", variableConfig, variableNode, namespace);
|
|
999
|
+
const record = {
|
|
1000
|
+
node: variableNode,
|
|
1001
|
+
path: path,
|
|
1002
|
+
nodeId: nodeId,
|
|
1003
|
+
nodeIdKey: this.normalizeNodeIdKey(nodeId),
|
|
1004
|
+
getValue: () => state.currentValue,
|
|
1005
|
+
setRuntimeValue: (nextValue) => {
|
|
1006
|
+
state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
|
|
1007
|
+
return state.currentValue;
|
|
1008
|
+
},
|
|
1009
|
+
setValue: (nextValue) => {
|
|
1010
|
+
if (state.access !== "readwrite") {
|
|
1011
|
+
throw new Error("Tag is read-only: " + path);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
|
|
1015
|
+
|
|
1016
|
+
const alarm = this.variableStore.get(path).alarm
|
|
1017
|
+
this.addressSpaceAlarm.checkAlarm(alarm, state.currentValue)
|
|
1018
|
+
|
|
1019
|
+
return state.currentValue;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
this.variableStore.set(path, record);
|
|
1023
|
+
this.variableNodeIdStore.set(record.nodeIdKey, record);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
addMethod(parentNode, methodConfig, parentPath, pathOverride) {
|
|
1031
|
+
const namespace = this.getNamespaceForConfig(methodConfig);
|
|
1032
|
+
const methodName = methodConfig.name;
|
|
1033
|
+
const path = pathOverride || this.buildPath(parentPath, methodName);
|
|
1034
|
+
const methodNode = namespace.addMethod(parentNode, {
|
|
1035
|
+
browseName: methodConfig.displayName || methodName,
|
|
1036
|
+
displayName: methodConfig.displayName || methodName,
|
|
1037
|
+
description: { text: methodConfig.description || "" },
|
|
1038
|
+
nodeId: this.resolveNodeId(methodConfig, path, namespace),
|
|
1039
|
+
modellingRule: this.isObjectTypePath(parentPath) ? "Mandatory" : undefined,
|
|
1040
|
+
inputArguments: methodConfig.inputs.map((arg) => ({
|
|
1041
|
+
name: arg.name,
|
|
1042
|
+
description: { text: arg.description || "" },
|
|
1043
|
+
dataType: DATA_TYPE_MAP[arg.type]
|
|
1044
|
+
})),
|
|
1045
|
+
outputArguments: methodConfig.outputs.map((arg) => ({
|
|
1046
|
+
name: arg.name,
|
|
1047
|
+
description: { text: arg.description || "" },
|
|
1048
|
+
dataType: DATA_TYPE_MAP[arg.type]
|
|
1049
|
+
}))
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
methodNode.bindMethod((inputArguments, context, callback) => {
|
|
1053
|
+
const callId = Date.now() + "_" + Math.random();
|
|
1054
|
+
|
|
1055
|
+
this.registry.emitMethodCall({
|
|
1056
|
+
methodName: methodConfig.name,
|
|
1057
|
+
callId,
|
|
1058
|
+
inputArguments,
|
|
1059
|
+
serverName: this.serverName
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
this.registry.waitForMethodResponse(callId)
|
|
1063
|
+
.then((outputs) => {
|
|
1064
|
+
callback(null, {
|
|
1065
|
+
statusCode: StatusCodes.Good,
|
|
1066
|
+
outputArguments: outputs.map((output) => new Variant(output))
|
|
1067
|
+
});
|
|
1068
|
+
})
|
|
1069
|
+
.catch(() => {
|
|
1070
|
+
callback(null, {
|
|
1071
|
+
statusCode: StatusCodes.BadInternalError
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
this.registerNodeEntry("method", path, parentPath, "componentOf", methodConfig, methodNode, namespace);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
addAlarmPlaceholder(parentPath, alarmConfig) {
|
|
1080
|
+
const path = this.buildPath(parentPath, alarmConfig.name);
|
|
1081
|
+
this.node.debug("Alarm definition received but not implemented yet: " + path + " (" + alarmConfig.type + ")");
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
registerNodeEntry(kind, path, parentPath, relationship, config, node, namespace) {
|
|
1085
|
+
this.nodeEntries.set(path, {
|
|
1086
|
+
kind,
|
|
1087
|
+
path,
|
|
1088
|
+
parentPath,
|
|
1089
|
+
relationship,
|
|
1090
|
+
config,
|
|
1091
|
+
signature: this.buildSignature(kind, config, parentPath, relationship),
|
|
1092
|
+
node,
|
|
1093
|
+
namespace
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
buildPath(parentPath, name) {
|
|
1098
|
+
return parentPath ? parentPath + "." + name : name;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
buildObjectTypePath(name) {
|
|
1102
|
+
return "__objectTypes." + name;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
buildCollectionPath(parentPath, collectionName, name) {
|
|
1106
|
+
|
|
1107
|
+
// return parentPath ? parentPath + "." + collectionName + "." + name : collectionName + "." + name;
|
|
1108
|
+
//change nodeId path
|
|
1109
|
+
return parentPath ? parentPath + "." + name : name + "." + name;
|
|
1110
|
+
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
isObjectTypePath(path) {
|
|
1114
|
+
return String(path || "").indexOf("__objectTypes.") === 0;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
buildStableNodeId(path, namespace) {
|
|
1118
|
+
return "ns=" + namespace.index + ";s=" + sanitizeNodeIdPath(path);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
resolveNodeId(config, path, namespace) {
|
|
1122
|
+
const customNodeId = config && typeof config.nodeId === "string" ? config.nodeId.trim() : "";
|
|
1123
|
+
return customNodeId || this.buildStableNodeId(path, namespace);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
resolveNamespaceId(config) {
|
|
1127
|
+
const namespaceId = config && config.namespaceId !== undefined ? Number(config.namespaceId) : 2;
|
|
1128
|
+
return Number.isInteger(namespaceId) && namespaceId >= 2 ? namespaceId : 2;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
getNamespaceForConfig(config) {
|
|
1132
|
+
const namespaceId = this.resolveNamespaceId(config);
|
|
1133
|
+
if (!this.namespaces.has(namespaceId)) {
|
|
1134
|
+
throw new Error("Namespace " + namespaceId + " is not available");
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return this.namespaces.get(namespaceId);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
emitTagAccess(operation, details) {
|
|
1141
|
+
this.registry.emitTagAccess({
|
|
1142
|
+
operation,
|
|
1143
|
+
serverId: this.node.id,
|
|
1144
|
+
serverNodeName: this.node.name || "",
|
|
1145
|
+
serverName: this.serverName,
|
|
1146
|
+
timestamp: new Date().toISOString(),
|
|
1147
|
+
path: details.path,
|
|
1148
|
+
nodeID: details.nodeID,
|
|
1149
|
+
browseName: details.browseName,
|
|
1150
|
+
dataType: details.dataType,
|
|
1151
|
+
value: details.value
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
getVariableRecord(identifierType, identifier) {
|
|
1156
|
+
if (identifierType === "nodeId") {
|
|
1157
|
+
return this.getVariableRecordByNodeId(identifier);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return this.getVariableRecordByPath(identifier);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
getVariableRecordByPath(path) {
|
|
1164
|
+
if (!this.variableStore.has(path)) {
|
|
1165
|
+
throw new Error("Unknown path: " + path);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return this.variableStore.get(path);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
getVariableRecordByNodeId(nodeId) {
|
|
1172
|
+
const key = this.normalizeNodeIdKey(nodeId);
|
|
1173
|
+
if (!this.variableNodeIdStore.has(key)) {
|
|
1174
|
+
throw new Error("Unknown nodeId: " + nodeId);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
return this.variableNodeIdStore.get(key);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
normalizeNodeIdKey(nodeId) {
|
|
1181
|
+
return coerceNodeId(nodeId).toString();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
resolveAlarmVariableRecord(parentPath, parentNode, variableNodeId) {
|
|
1185
|
+
const reference = String(variableNodeId || "").trim();
|
|
1186
|
+
if (!reference) {
|
|
1187
|
+
throw new Error("Alarm variableNodeId is required");
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (reference.indexOf(".") === 0) {
|
|
1191
|
+
const relativeReference = this.resolveObjectTypeRelativeReference(parentPath, reference);
|
|
1192
|
+
|
|
1193
|
+
//not work
|
|
1194
|
+
var corrente = parentNode.getComponentByName("corrent")
|
|
1195
|
+
|
|
1196
|
+
return {
|
|
1197
|
+
node: corrente
|
|
1198
|
+
}
|
|
1199
|
+
if (this.variableStore.has(relativeReference)) {
|
|
1200
|
+
return this.variableStore.get(relativeReference);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (this.variableStore.has(reference)) {
|
|
1205
|
+
return this.variableStore.get(reference);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
try {
|
|
1209
|
+
return this.getVariableRecordByNodeId(reference);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
// ignore and continue with path heuristics
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const stringNodeIdPath = this.extractStringNodeIdPath(reference);
|
|
1215
|
+
if (stringNodeIdPath) {
|
|
1216
|
+
if (this.variableStore.has(stringNodeIdPath)) {
|
|
1217
|
+
return this.variableStore.get(stringNodeIdPath);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const relativeStringNodeIdPath = this.buildPath(parentPath, stringNodeIdPath);
|
|
1221
|
+
if (this.variableStore.has(relativeStringNodeIdPath)) {
|
|
1222
|
+
return this.variableStore.get(relativeStringNodeIdPath);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const relativePath = this.buildPath(parentPath, reference);
|
|
1227
|
+
if (this.variableStore.has(relativePath)) {
|
|
1228
|
+
return this.variableStore.get(relativePath);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const suffixMatches = Array.from(this.variableStore.entries())
|
|
1232
|
+
.filter(([path]) => {
|
|
1233
|
+
return path === reference ||
|
|
1234
|
+
path.indexOf("." + reference) !== -1 ||
|
|
1235
|
+
(stringNodeIdPath && (path === stringNodeIdPath || path.indexOf("." + stringNodeIdPath) !== -1));
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
if (suffixMatches.length === 1) {
|
|
1239
|
+
return suffixMatches[0][1];
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
throw new Error("Unknown alarm variable reference: " + reference);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
extractStringNodeIdPath(reference) {
|
|
1246
|
+
const match = /^ns=\d+;s=(.+)$/.exec(String(reference || "").trim());
|
|
1247
|
+
return match ? match[1] : "";
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
resolveObjectTypeRelativeReference(rootInstancePath, reference) {
|
|
1251
|
+
const tokens = String(reference || "")
|
|
1252
|
+
.trim()
|
|
1253
|
+
.replace(/^\.+/, "")
|
|
1254
|
+
.split(".")
|
|
1255
|
+
.filter((token) => token !== "")
|
|
1256
|
+
.map((token) => this.normalizeCollectionToken(token));
|
|
1257
|
+
|
|
1258
|
+
if (!tokens.length) {
|
|
1259
|
+
return rootInstancePath;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return rootInstancePath + "." + tokens.join(".");
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
normalizeCollectionToken(token) {
|
|
1266
|
+
const normalized = String(token || "").trim().toLowerCase();
|
|
1267
|
+
const aliases = {
|
|
1268
|
+
variaveis: "variables",
|
|
1269
|
+
variables: "variables",
|
|
1270
|
+
objetos: "objects",
|
|
1271
|
+
objects: "objects",
|
|
1272
|
+
pastas: "folders",
|
|
1273
|
+
folders: "folders",
|
|
1274
|
+
metodos: "methods",
|
|
1275
|
+
methods: "methods",
|
|
1276
|
+
alarmes: "alarms",
|
|
1277
|
+
alarms: "alarms",
|
|
1278
|
+
objecttypes: "objectsTypes",
|
|
1279
|
+
objectstypes: "objectsTypes",
|
|
1280
|
+
tiposobjetos: "objectsTypes"
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
return aliases[normalized] || token;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
coerceValue(value, type, expectArray) {
|
|
1287
|
+
if (expectArray) {
|
|
1288
|
+
const items = this.extractArrayItems(value);
|
|
1289
|
+
if (!items) {
|
|
1290
|
+
throw new Error("Expected array value for type " + type);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return items.map((item) => this.coerceScalarValue(item, type));
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (typeof value === "string") {
|
|
1297
|
+
const trimmed = value.trim();
|
|
1298
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
1299
|
+
try {
|
|
1300
|
+
const parsed = JSON.parse(trimmed);
|
|
1301
|
+
if (Array.isArray(parsed)) {
|
|
1302
|
+
throw new Error("Expected scalar value for type " + type + " but received array");
|
|
1303
|
+
}
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (error.message.indexOf("Expected scalar value") === 0) {
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
throw new Error("Invalid array value for type " + type + ": " + error.message);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (this.extractArrayItems(value)) {
|
|
1315
|
+
throw new Error("Expected scalar value for type " + type + " but received array");
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return this.coerceScalarValue(value, type);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
coerceScalarValue(value, type) {
|
|
1322
|
+
|
|
1323
|
+
if (type === "Int16") {
|
|
1324
|
+
const parsed = Number(value);
|
|
1325
|
+
if (!Number.isFinite(parsed)) {
|
|
1326
|
+
return 0;
|
|
1327
|
+
}
|
|
1328
|
+
return Math.trunc(parsed);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
if (type === "Int32") {
|
|
1333
|
+
const parsed = Number(value);
|
|
1334
|
+
if (!Number.isFinite(parsed)) {
|
|
1335
|
+
return 0;
|
|
1336
|
+
}
|
|
1337
|
+
return Math.trunc(parsed);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (type === "Float") {
|
|
1341
|
+
const parsed = Number(value);
|
|
1342
|
+
if (!Number.isFinite(parsed)) {
|
|
1343
|
+
return 0;
|
|
1344
|
+
}
|
|
1345
|
+
return parsed;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (type === "Boolean") {
|
|
1349
|
+
if (typeof value === "string") {
|
|
1350
|
+
const normalized = value.trim().toLowerCase();
|
|
1351
|
+
if (normalized === "false" || normalized === "0" || normalized === "") {
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
if (normalized === "true" || normalized === "1") {
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return Boolean(value);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return value === undefined || value === null ? "" : String(value);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
isArrayValue(value) {
|
|
1366
|
+
return this.extractArrayItems(value) !== null;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
extractArrayItems(value) {
|
|
1370
|
+
if (Array.isArray(value)) {
|
|
1371
|
+
return value;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (typeof value === "string") {
|
|
1375
|
+
const trimmed = value.trim();
|
|
1376
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
1377
|
+
const parsed = JSON.parse(trimmed);
|
|
1378
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
1379
|
+
}
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (ArrayBuffer.isView(value)) {
|
|
1384
|
+
return Array.from(value);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function collapsePaths(paths) {
|
|
1392
|
+
return Array.from(new Set(paths))
|
|
1393
|
+
.sort(comparePathDepthAsc)
|
|
1394
|
+
.filter((path, index, ordered) => {
|
|
1395
|
+
for (let current = 0; current < index; current += 1) {
|
|
1396
|
+
if (isSamePathOrDescendant(path, ordered[current])) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return true;
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function isSamePathOrDescendant(path, rootPath) {
|
|
1405
|
+
return path === rootPath || path.indexOf(rootPath + ".") === 0;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function comparePathDepthAsc(left, right) {
|
|
1409
|
+
return pathDepth(left) - pathDepth(right);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function comparePathDepthDesc(left, right) {
|
|
1413
|
+
return pathDepth(right) - pathDepth(left);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function pathDepth(path) {
|
|
1417
|
+
return String(path || "").split(".").length;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function compareEntryCreationOrder(left, right) {
|
|
1421
|
+
const depthDelta = pathDepth(left.path) - pathDepth(right.path);
|
|
1422
|
+
if (depthDelta !== 0) {
|
|
1423
|
+
return depthDelta;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
return kindRank(left.kind) - kindRank(right.kind);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function kindRank(kind) {
|
|
1430
|
+
if (kind === "objectTypeDefinition") {
|
|
1431
|
+
return -1;
|
|
1432
|
+
}
|
|
1433
|
+
if (kind === "folder") {
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1436
|
+
if (kind === "object") {
|
|
1437
|
+
return 1;
|
|
1438
|
+
}
|
|
1439
|
+
if (kind === "objectTypeInstance") {
|
|
1440
|
+
return 1;
|
|
1441
|
+
}
|
|
1442
|
+
if (kind === "variable") {
|
|
1443
|
+
return 2;
|
|
1444
|
+
}
|
|
1445
|
+
if (kind === "method") {
|
|
1446
|
+
return 3;
|
|
1447
|
+
}
|
|
1448
|
+
if (kind === "alarm") {
|
|
1449
|
+
return 4;
|
|
1450
|
+
}
|
|
1451
|
+
return 10;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
module.exports = {
|
|
1455
|
+
OpcUaAddressSpaceBuilder
|
|
1456
|
+
};
|