@vitormnm/node-red-simple-opcua 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/client/lib/opcua-client-browser.js +21 -6
- package/client/lib/opcua-client-write-service.js +14 -4
- package/client/opcua-client-config.js +38 -1
- package/client/opcua-client-utils.js +195 -66
- package/client/opcua-client.html +1 -0
- package/client/view/opcua-client.js +106 -14
- package/package.json +2 -2
- package/server/lib/opcua-address-space-alarm.js +85 -28
- package/server/lib/opcua-address-space-builder.js +653 -45
- package/server/lib/opcua-config.js +29 -12
- package/server/lib/opcua-server-events-child.js +30 -4
- package/server/lib/opcua-server-runtime-child.js +141 -9
- package/server/lib/opcua-server-runtime.js +3 -0
- package/server/lib/opcua-server-status-child.js +32 -1
- package/server/opcua-server-io.html +21 -5
- package/server/opcua-server-io.js +23 -2
- package/server/opcua-server-registry.js +8 -2
- package/server/opcua-server.css +12 -0
- package/server/opcua-server.html +2 -0
- package/server/view/opcua-server.css +11 -0
- package/server/view/opcua-server.js +240 -23
package/README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# @vitormnm/node-red-simple-opcua
|
|
2
|
-
|
|
3
1
|
OPC UA client and server with a simple graphical interface for Node-RED.
|
|
4
2
|
|
|
5
3
|
This package provides a simplified, highly parameterized set of nodes to establish OPC UA Servers and Clients in Node-RED without complex coding.
|
|
@@ -40,6 +38,23 @@ Interacts with an active local server instance. It supports the following modes:
|
|
|
40
38
|
|
|
41
39
|
---
|
|
42
40
|
|
|
41
|
+
### Supported Variable Data Types, Arrays & Matrices
|
|
42
|
+
|
|
43
|
+
The server supports the following standard OPC UA data types, including arrays and matrices:
|
|
44
|
+
|
|
45
|
+
- **Integers**: `Int16`, `UInt16`, `Int32`, `UInt32`, `Int64`
|
|
46
|
+
- **Floats**: `Float`
|
|
47
|
+
- **Others**: `Boolean`, `String`, `ByteString`
|
|
48
|
+
- **Arrays (1D)**: Pass standard JavaScript arrays or JSON arrays as values (e.g., `[1, 2, 3, 4]` or `"[1, 2, 3, 4]"`).
|
|
49
|
+
- **Multi-Dimensional Arrays (Matrices)**: Pass nested arrays. The dimensions are automatically detected based on the shape (e.g., `[[1, 2], [3, 4]]` is registered as a 2x2 matrix with dimensions `[2, 2]`, and `[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]` as a 2x2x2 matrix).
|
|
50
|
+
|
|
51
|
+
*Note on 64-bit Integers (`Int64`/`UInt64`):*
|
|
52
|
+
- *Scalars are represented as `[high, low]` arrays (e.g., `[0, 100]` for the value 100).*
|
|
53
|
+
- *Arrays of 64-bit integers are represented as arrays of doublets (e.g., `[[0, 100], [0, 200]]`).*
|
|
54
|
+
- *They are handled internally using JavaScript `BigInt`.*
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
43
58
|
## Client Functionalities
|
|
44
59
|
|
|
45
60
|
The client implementation consists of `opcua-client-config` (shared connection configuration) and `opcua-client` (the action node).
|
|
@@ -6,10 +6,11 @@ const {
|
|
|
6
6
|
DataType,
|
|
7
7
|
NodeClass,
|
|
8
8
|
coerceNodeId,
|
|
9
|
-
makeNodeId
|
|
9
|
+
makeNodeId,
|
|
10
|
+
VariantArrayType
|
|
10
11
|
} = require("node-opcua");
|
|
11
12
|
|
|
12
|
-
const { enrichItemResultWithEnumeration } = require("../opcua-client-utils");
|
|
13
|
+
const { enrichItemResultWithEnumeration, reshapeArray } = require("../opcua-client-utils");
|
|
13
14
|
|
|
14
15
|
async function browseNode(session, root) {
|
|
15
16
|
const nodeID = normalizeNodeId(root.nodeID || root.nodeId || ROOT_NODE_ID);
|
|
@@ -123,7 +124,14 @@ async function browseNode(session, root) {
|
|
|
123
124
|
if (nodeClass === "Variable") {
|
|
124
125
|
const dataTypeValue = dataValues[i * 3 + 1]?.value?.value;
|
|
125
126
|
const rawValueVariant = dataValues[i * 3 + 2]?.value;
|
|
126
|
-
|
|
127
|
+
let rawValue = rawValueVariant?.value;
|
|
128
|
+
|
|
129
|
+
if (rawValueVariant && rawValueVariant.arrayType === VariantArrayType.Matrix && rawValueVariant.dimensions && (Array.isArray(rawValue) || ArrayBuffer.isView(rawValue))) {
|
|
130
|
+
if (ArrayBuffer.isView(rawValue)) {
|
|
131
|
+
rawValue = Array.from(rawValue);
|
|
132
|
+
}
|
|
133
|
+
rawValue = reshapeArray(rawValue, rawValueVariant.dimensions);
|
|
134
|
+
}
|
|
127
135
|
|
|
128
136
|
item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
|
|
129
137
|
? (DataType[dataTypeValue.value] || dataTypeValue.toString())
|
|
@@ -525,7 +533,14 @@ async function browseRecursiveNode(session, root) {
|
|
|
525
533
|
if (item.nodeClass === "Variable") {
|
|
526
534
|
const dataTypeValue = dataValues[index * 3 + 1]?.value?.value;
|
|
527
535
|
const rawValueVariant = dataValues[index * 3 + 2]?.value;
|
|
528
|
-
|
|
536
|
+
let rawValue = rawValueVariant?.value;
|
|
537
|
+
|
|
538
|
+
if (rawValueVariant && rawValueVariant.arrayType === VariantArrayType.Matrix && rawValueVariant.dimensions && (Array.isArray(rawValue) || ArrayBuffer.isView(rawValue))) {
|
|
539
|
+
if (ArrayBuffer.isView(rawValue)) {
|
|
540
|
+
rawValue = Array.from(rawValue);
|
|
541
|
+
}
|
|
542
|
+
rawValue = reshapeArray(rawValue, rawValueVariant.dimensions);
|
|
543
|
+
}
|
|
529
544
|
|
|
530
545
|
item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
|
|
531
546
|
? (DataType[dataTypeValue.value] || dataTypeValue.toString())
|
|
@@ -569,11 +584,11 @@ async function browseRecursiveNode(session, root) {
|
|
|
569
584
|
}
|
|
570
585
|
|
|
571
586
|
const methods = allItems.filter(item => item.nodeClass === "Method");
|
|
572
|
-
|
|
587
|
+
await Promise.all(methods.map(async (method) => {
|
|
573
588
|
const definition = await readMethodArguments(session, method.nodeID);
|
|
574
589
|
method.inputArguments = definition.inputArguments;
|
|
575
590
|
method.outputArguments = definition.outputArguments;
|
|
576
|
-
}
|
|
591
|
+
}));
|
|
577
592
|
}
|
|
578
593
|
|
|
579
594
|
return {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { AttributeIds, DataType, coerceNodeId } = require("node-opcua");
|
|
3
|
+
const { AttributeIds, DataType, coerceNodeId, VariantArrayType } = require("node-opcua");
|
|
4
4
|
const {
|
|
5
5
|
buildVariantFromItem,
|
|
6
6
|
normalizeTypeName,
|
|
7
7
|
resolveNodeId,
|
|
8
|
-
statusCodeToString
|
|
8
|
+
statusCodeToString,
|
|
9
|
+
reshapeArray
|
|
9
10
|
} = require("../opcua-client-utils");
|
|
10
11
|
|
|
11
12
|
// Máximo de tags por chamada session.write (ajuste conforme limite do servidor)
|
|
@@ -84,7 +85,7 @@ async function writeBatches(session, items, variants) {
|
|
|
84
85
|
allStatusCodes[start + i] = sc;
|
|
85
86
|
});
|
|
86
87
|
} catch (batchError) {
|
|
87
|
-
|
|
88
|
+
|
|
88
89
|
for (let i = start; i < end; i++) {
|
|
89
90
|
allStatusCodes[i] = { name: batchError.message, value: -1 };
|
|
90
91
|
}
|
|
@@ -109,10 +110,19 @@ function buildResults(items, variants, statusCodes) {
|
|
|
109
110
|
const scName = sc && sc.name ? sc.name : "Good";
|
|
110
111
|
const typeName = DataType[variants[index].dataType] || null;
|
|
111
112
|
|
|
113
|
+
let val = variants[index].value;
|
|
114
|
+
const variant = variants[index];
|
|
115
|
+
if (variant && variant.arrayType === VariantArrayType.Matrix && variant.dimensions && (Array.isArray(val) || ArrayBuffer.isView(val))) {
|
|
116
|
+
if (ArrayBuffer.isView(val)) {
|
|
117
|
+
val = Array.from(val);
|
|
118
|
+
}
|
|
119
|
+
val = reshapeArray(val, variant.dimensions);
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
return {
|
|
113
123
|
name: item.name || nodeId,
|
|
114
124
|
nodeID: nodeId,
|
|
115
|
-
value:
|
|
125
|
+
value: val,
|
|
116
126
|
type: typeName,
|
|
117
127
|
status: scName,
|
|
118
128
|
sourceTimestamp: null,
|
|
@@ -8,7 +8,10 @@ const {
|
|
|
8
8
|
getMethodArgumentDefinition,
|
|
9
9
|
resolveMethodObjectId,
|
|
10
10
|
resolveSecurityMode,
|
|
11
|
-
resolveSecurityPolicy
|
|
11
|
+
resolveSecurityPolicy,
|
|
12
|
+
AttributeIds,
|
|
13
|
+
dataValueToItemResult,
|
|
14
|
+
enrichItemResultWithEnumeration
|
|
12
15
|
} = require("./opcua-client-utils");
|
|
13
16
|
|
|
14
17
|
const {
|
|
@@ -214,6 +217,40 @@ module.exports = function (RED) {
|
|
|
214
217
|
res.status(500).json({ error: error.message });
|
|
215
218
|
}
|
|
216
219
|
});
|
|
220
|
+
|
|
221
|
+
RED.httpAdmin.get("/opcua-client-config/:id/read", RED.auth.needsPermission("flows.read"), async function (req, res) {
|
|
222
|
+
try {
|
|
223
|
+
const configNode = RED.nodes.getNode(req.params.id);
|
|
224
|
+
|
|
225
|
+
if (!configNode) {
|
|
226
|
+
res.status(404).json({ error: "OPC UA client configuration not found" });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const nodeId = req.query.nodeId;
|
|
231
|
+
if (!nodeId) {
|
|
232
|
+
throw new Error("Missing nodeId parameter");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const session = await configNode.getSession();
|
|
236
|
+
const dataValue = await session.read({
|
|
237
|
+
nodeId: nodeId,
|
|
238
|
+
attributeId: AttributeIds.Value
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (dataValue.statusCode && !dataValue.statusCode.isGood()) {
|
|
242
|
+
throw new Error("Read failed: " + dataValue.statusCode.toString());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const cache = new Map();
|
|
246
|
+
let result = dataValueToItemResult({ nodeID: nodeId }, dataValue);
|
|
247
|
+
result = await enrichItemResultWithEnumeration(result, session, cache, nodeId);
|
|
248
|
+
|
|
249
|
+
res.json({ value: result.value, valueEnumeration: result.valueEnumeration });
|
|
250
|
+
} catch (error) {
|
|
251
|
+
res.status(500).json({ error: error.message });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
217
254
|
};
|
|
218
255
|
|
|
219
256
|
async function browseForEditor(configNode, nodeId) {
|
|
@@ -204,6 +204,70 @@ function coerceValue(value, typeName) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
function getArrayDimensions(value, typeName) {
|
|
208
|
+
if (!Array.isArray(value)) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
// Check if it is a 64-bit scalar represented as [high, low]
|
|
212
|
+
if ((typeName === "Int64" || typeName === "UInt64") && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number") {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const hasNestedArray = value.some(item => {
|
|
217
|
+
if (Array.isArray(item)) {
|
|
218
|
+
if ((typeName === "Int64" || typeName === "UInt64") && item.length === 2 && typeof item[0] === "number" && typeof item[1] === "number") {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!hasNestedArray) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const dimensions = [];
|
|
231
|
+
let current = value;
|
|
232
|
+
while (Array.isArray(current)) {
|
|
233
|
+
if ((typeName === "Int64" || typeName === "UInt64") && current.length === 2 && typeof current[0] === "number" && typeof current[1] === "number") {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
dimensions.push(current.length);
|
|
237
|
+
if (current.length === 0) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
current = current[0];
|
|
241
|
+
}
|
|
242
|
+
return dimensions;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function flattenMatrix(value, typeName) {
|
|
246
|
+
if (!Array.isArray(value)) {
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
if ((typeName === "Int64" || typeName === "UInt64") && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number") {
|
|
250
|
+
return [value];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const flat = [];
|
|
254
|
+
const recurse = (a) => {
|
|
255
|
+
for (const item of a) {
|
|
256
|
+
if (Array.isArray(item)) {
|
|
257
|
+
if ((typeName === "Int64" || typeName === "UInt64") && item.length === 2 && typeof item[0] === "number" && typeof item[1] === "number") {
|
|
258
|
+
flat.push(item);
|
|
259
|
+
} else {
|
|
260
|
+
recurse(item);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
flat.push(item);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
recurse(value);
|
|
268
|
+
return flat;
|
|
269
|
+
}
|
|
270
|
+
|
|
207
271
|
function buildVariantFromItem(item, fallbackTypeName) {
|
|
208
272
|
const typeName = normalizeTypeName(item.type || fallbackTypeName || inferTypeName(item.value));
|
|
209
273
|
const dataType = DataType[typeName];
|
|
@@ -212,6 +276,23 @@ function buildVariantFromItem(item, fallbackTypeName) {
|
|
|
212
276
|
throw new Error("Unsupported OPC UA data type: " + typeName);
|
|
213
277
|
}
|
|
214
278
|
|
|
279
|
+
const dimensions = getArrayDimensions(item.value, typeName);
|
|
280
|
+
if (dimensions) {
|
|
281
|
+
// Multi-dimensional array (Matrix)
|
|
282
|
+
const flatVal = flattenMatrix(item.value, typeName);
|
|
283
|
+
const coercedArray = coerceValue(flatVal, typeName);
|
|
284
|
+
const TypedArrayCtor = TYPED_ARRAY_MAP[dataType];
|
|
285
|
+
|
|
286
|
+
return new Variant({
|
|
287
|
+
dataType,
|
|
288
|
+
arrayType: VariantArrayType.Matrix,
|
|
289
|
+
dimensions,
|
|
290
|
+
value: TypedArrayCtor
|
|
291
|
+
? TypedArrayCtor.from(coercedArray)
|
|
292
|
+
: coercedArray
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
215
296
|
const isArray = Array.isArray(item.value);
|
|
216
297
|
|
|
217
298
|
if (isArray) {
|
|
@@ -311,12 +392,48 @@ function resolve64BitValue(value, isUnsigned) {
|
|
|
311
392
|
return value;
|
|
312
393
|
}
|
|
313
394
|
|
|
395
|
+
function reshapeArray(flatArray, dimensions) {
|
|
396
|
+
if (!dimensions || dimensions.length <= 1) {
|
|
397
|
+
return flatArray;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const reshape = (arr, dims, offset) => {
|
|
401
|
+
const size = dims[0];
|
|
402
|
+
if (dims.length === 1) {
|
|
403
|
+
return {
|
|
404
|
+
result: arr.slice(offset, offset + size),
|
|
405
|
+
nextOffset: offset + size
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = [];
|
|
410
|
+
let currentOffset = offset;
|
|
411
|
+
for (let i = 0; i < size; i++) {
|
|
412
|
+
const step = reshape(arr, dims.slice(1), currentOffset);
|
|
413
|
+
result.push(step.result);
|
|
414
|
+
currentOffset = step.nextOffset;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
result: result,
|
|
418
|
+
nextOffset: currentOffset
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
return reshape(flatArray, dimensions, 0).result;
|
|
423
|
+
}
|
|
424
|
+
|
|
314
425
|
function dataValueToItemResult(item, dataValue) {
|
|
315
426
|
const variant = dataValue && dataValue.value ? dataValue.value : null;
|
|
316
427
|
let val = variant ? variant.value : null;
|
|
317
428
|
if (variant && (variant.dataType === DataType.Int64 || variant.dataType === DataType.UInt64)) {
|
|
318
429
|
val = resolve64BitValue(val, variant.dataType === DataType.UInt64);
|
|
319
430
|
}
|
|
431
|
+
if (variant && variant.arrayType === VariantArrayType.Matrix && variant.dimensions && (Array.isArray(val) || ArrayBuffer.isView(val))) {
|
|
432
|
+
if (ArrayBuffer.isView(val)) {
|
|
433
|
+
val = Array.from(val);
|
|
434
|
+
}
|
|
435
|
+
val = reshapeArray(val, variant.dimensions);
|
|
436
|
+
}
|
|
320
437
|
return {
|
|
321
438
|
name: resolveName(item, resolveNodeId(item)),
|
|
322
439
|
nodeID: resolveNodeId(item),
|
|
@@ -374,6 +491,12 @@ function callResultToItemResult(item, callResult, argumentDefinition) {
|
|
|
374
491
|
if (variant && (variant.dataType === DataType.Int64 || variant.dataType === DataType.UInt64)) {
|
|
375
492
|
val = resolve64BitValue(val, variant.dataType === DataType.UInt64);
|
|
376
493
|
}
|
|
494
|
+
if (variant && variant.arrayType === VariantArrayType.Matrix && variant.dimensions && (Array.isArray(val) || ArrayBuffer.isView(val))) {
|
|
495
|
+
if (ArrayBuffer.isView(val)) {
|
|
496
|
+
val = Array.from(val);
|
|
497
|
+
}
|
|
498
|
+
val = reshapeArray(val, variant.dimensions);
|
|
499
|
+
}
|
|
377
500
|
return {
|
|
378
501
|
name: outputDefinitions[index] && outputDefinitions[index].name
|
|
379
502
|
? String(outputDefinitions[index].name)
|
|
@@ -468,12 +591,22 @@ async function getMethodArgumentDefinition(session, methodNodeId, cache) {
|
|
|
468
591
|
return definition;
|
|
469
592
|
}
|
|
470
593
|
|
|
594
|
+
const PRIMITIVE_TYPES = new Set([
|
|
595
|
+
"Null", "Boolean", "SByte", "Byte", "Int16", "UInt16", "Int32", "UInt32", "Int64", "UInt64",
|
|
596
|
+
"Float", "Double", "String", "DateTime", "Guid", "ByteString", "XmlElement",
|
|
597
|
+
"NodeId", "ExpandedNodeId", "StatusCode", "QualifiedName", "LocalizedText"
|
|
598
|
+
]);
|
|
599
|
+
|
|
471
600
|
async function enrichItemResultWithEnumeration(result, session, cache, nodeId) {
|
|
472
601
|
const type = result.type || result.dataType;
|
|
473
602
|
if (!type) {
|
|
474
603
|
return result;
|
|
475
604
|
}
|
|
476
605
|
|
|
606
|
+
if (result.dataType && PRIMITIVE_TYPES.has(result.dataType)) {
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
|
|
477
610
|
const isStandardEnum = type === "Int32" || type === "Enumeration";
|
|
478
611
|
const isCustomNodeId = typeof type === "string" && (type.includes("i=") || type.includes("ns="));
|
|
479
612
|
|
|
@@ -487,88 +620,83 @@ async function enrichItemResultWithEnumeration(result, session, cache, nodeId) {
|
|
|
487
620
|
|
|
488
621
|
try {
|
|
489
622
|
const cacheKeyType = "dt:" + nodeId;
|
|
490
|
-
let
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
result.dataType
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
623
|
+
let dtNodeIdPromise = cache ? cache.get(cacheKeyType) : undefined;
|
|
624
|
+
|
|
625
|
+
if (dtNodeIdPromise === undefined) {
|
|
626
|
+
dtNodeIdPromise = (async () => {
|
|
627
|
+
const isNodeIdLike = result.dataType && (
|
|
628
|
+
typeof result.dataType !== "string" ||
|
|
629
|
+
result.dataType.includes("i=") ||
|
|
630
|
+
result.dataType.includes("ns=")
|
|
631
|
+
);
|
|
632
|
+
if (isNodeIdLike) {
|
|
633
|
+
try {
|
|
634
|
+
return coerceNodeId(result.dataType);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
502
638
|
}
|
|
503
|
-
}
|
|
504
|
-
if (dtNodeId === undefined) {
|
|
505
639
|
const dv = await session.read({
|
|
506
640
|
nodeId: nodeId,
|
|
507
641
|
attributeId: AttributeIds.DataType
|
|
508
642
|
});
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
} else {
|
|
513
|
-
if (cache) cache.set(cacheKeyType, null);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
643
|
+
return dv.statusCode.isGood() ? dv.value.value : null;
|
|
644
|
+
})();
|
|
645
|
+
if (cache) cache.set(cacheKeyType, dtNodeIdPromise);
|
|
516
646
|
}
|
|
517
647
|
|
|
648
|
+
const dtNodeId = await dtNodeIdPromise;
|
|
518
649
|
if (!dtNodeId) return result;
|
|
519
650
|
|
|
520
651
|
const cacheKeyStrings = "enumStrings:" + dtNodeId.toString();
|
|
521
|
-
let
|
|
652
|
+
let enumStringsPromise = cache ? cache.get(cacheKeyStrings) : undefined;
|
|
522
653
|
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const enumStringsRef = browseResult.references ? browseResult.references.find(r => r.browseName.name === "EnumStrings") : null;
|
|
533
|
-
const enumValuesRef = browseResult.references ? browseResult.references.find(r => r.browseName.name === "EnumValues") : null;
|
|
534
|
-
|
|
535
|
-
if (enumStringsRef) {
|
|
536
|
-
const dataValue = await session.read({
|
|
537
|
-
nodeId: enumStringsRef.nodeId,
|
|
538
|
-
attributeId: AttributeIds.Value
|
|
654
|
+
if (enumStringsPromise === undefined) {
|
|
655
|
+
enumStringsPromise = (async () => {
|
|
656
|
+
const browseResult = await session.browse({
|
|
657
|
+
nodeId: dtNodeId,
|
|
658
|
+
referenceTypeId: "HasProperty",
|
|
659
|
+
browseDirection: BrowseDirection.Forward,
|
|
660
|
+
includeSubtypes: true,
|
|
661
|
+
resultMask: 63
|
|
539
662
|
});
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
nodeId: enumValuesRef.nodeId,
|
|
549
|
-
attributeId: AttributeIds.Value
|
|
550
|
-
});
|
|
551
|
-
if (dataValue.statusCode.isGood() && dataValue.value.value) {
|
|
552
|
-
const map = {};
|
|
553
|
-
dataValue.value.value.forEach(ev => {
|
|
554
|
-
let val;
|
|
555
|
-
if (Array.isArray(ev.value) && ev.value.length === 2) {
|
|
556
|
-
val = ev.value[1]; // low part of Int64
|
|
557
|
-
} else {
|
|
558
|
-
val = Number(ev.value);
|
|
559
|
-
}
|
|
560
|
-
map[val] = ev.displayName.text;
|
|
663
|
+
|
|
664
|
+
const enumStringsRef = browseResult.references ? browseResult.references.find(r => r.browseName.name === "EnumStrings") : null;
|
|
665
|
+
const enumValuesRef = browseResult.references ? browseResult.references.find(r => r.browseName.name === "EnumValues") : null;
|
|
666
|
+
|
|
667
|
+
if (enumStringsRef) {
|
|
668
|
+
const dataValue = await session.read({
|
|
669
|
+
nodeId: enumStringsRef.nodeId,
|
|
670
|
+
attributeId: AttributeIds.Value
|
|
561
671
|
});
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
672
|
+
if (dataValue.statusCode.isGood() && dataValue.value.value) {
|
|
673
|
+
return dataValue.value.value.map(lt => lt.text);
|
|
674
|
+
}
|
|
675
|
+
} else if (enumValuesRef) {
|
|
676
|
+
const dataValue = await session.read({
|
|
677
|
+
nodeId: enumValuesRef.nodeId,
|
|
678
|
+
attributeId: AttributeIds.Value
|
|
679
|
+
});
|
|
680
|
+
if (dataValue.statusCode.isGood() && dataValue.value.value) {
|
|
681
|
+
const map = {};
|
|
682
|
+
dataValue.value.value.forEach(ev => {
|
|
683
|
+
let val;
|
|
684
|
+
if (Array.isArray(ev.value) && ev.value.length === 2) {
|
|
685
|
+
val = ev.value[1]; // low part of Int64
|
|
686
|
+
} else {
|
|
687
|
+
val = Number(ev.value);
|
|
688
|
+
}
|
|
689
|
+
map[val] = ev.displayName.text;
|
|
690
|
+
});
|
|
691
|
+
return map;
|
|
692
|
+
}
|
|
566
693
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
694
|
+
return null;
|
|
695
|
+
})();
|
|
696
|
+
if (cache) cache.set(cacheKeyStrings, enumStringsPromise);
|
|
570
697
|
}
|
|
571
698
|
|
|
699
|
+
const enumStrings = await enumStringsPromise;
|
|
572
700
|
if (enumStrings && enumStrings[result.value] !== undefined) {
|
|
573
701
|
result.valueEnumeration = enumStrings[result.value];
|
|
574
702
|
if (result.type) result.type = "Enumeration";
|
|
@@ -601,5 +729,6 @@ module.exports = {
|
|
|
601
729
|
resolveNodeId,
|
|
602
730
|
resolveSecurityMode,
|
|
603
731
|
resolveSecurityPolicy,
|
|
732
|
+
reshapeArray,
|
|
604
733
|
statusCodeToString
|
|
605
734
|
};
|
package/client/opcua-client.html
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
<div id="node-input-browse-context-menu" class="opcua-tree-context-menu" style="display:none;">
|
|
51
51
|
<a href="#" id="node-input-browse-context-refresh"><i class="fa fa-refresh"></i> Refresh node</a>
|
|
52
52
|
<a href="#" id="node-input-browse-context-copy-nodeid"><i class="fa fa-copy"></i> Copy NodeID</a>
|
|
53
|
+
<a href="#" id="node-input-browse-context-read-value"><i class="fa fa-eye"></i> Read value</a>
|
|
53
54
|
</div>
|
|
54
55
|
</div>
|
|
55
56
|
|