@vitormnm/node-red-simple-opcua 1.7.0 → 1.8.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.md +17 -2
- package/client/lib/opcua-client-browser.js +19 -4
- package/client/lib/opcua-client-write-service.js +14 -4
- package/client/opcua-client-config.js +38 -1
- package/client/opcua-client-utils.js +124 -0
- 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())
|
|
@@ -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)
|
|
@@ -601,5 +724,6 @@ module.exports = {
|
|
|
601
724
|
resolveNodeId,
|
|
602
725
|
resolveSecurityMode,
|
|
603
726
|
resolveSecurityPolicy,
|
|
727
|
+
reshapeArray,
|
|
604
728
|
statusCodeToString
|
|
605
729
|
};
|
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
|
|
|
@@ -657,7 +657,7 @@
|
|
|
657
657
|
}).fail(function (xhr) {
|
|
658
658
|
var message = xhr && xhr.responseJSON && xhr.responseJSON.error
|
|
659
659
|
? xhr.responseJSON.error
|
|
660
|
-
: "
|
|
660
|
+
: "Failed to browse the OPC UA server.";
|
|
661
661
|
browseState = null;
|
|
662
662
|
container.html('<div class="opcua-tree-empty">' + escapeHtml(message) + '</div>');
|
|
663
663
|
RED.notify(message, "error");
|
|
@@ -675,6 +675,7 @@
|
|
|
675
675
|
contextMenuPath = path || "";
|
|
676
676
|
$("#node-input-browse-context-refresh").toggle(!!item && !isVariable(item));
|
|
677
677
|
$("#node-input-browse-context-copy-nodeid").toggle(!!nodeIdOf(item));
|
|
678
|
+
$("#node-input-browse-context-read-value").toggle(!!item && isVariable(item) && !!nodeIdOf(item));
|
|
678
679
|
menu.css({ left: x + "px", top: y + "px" }).show();
|
|
679
680
|
}
|
|
680
681
|
|
|
@@ -682,15 +683,15 @@
|
|
|
682
683
|
var item = getItemAtPath(path);
|
|
683
684
|
var nodeId = nodeIdOf(item);
|
|
684
685
|
if (!nodeId) {
|
|
685
|
-
RED.notify("NodeID
|
|
686
|
+
RED.notify("NodeID not found for the selected item.", "warning");
|
|
686
687
|
return;
|
|
687
688
|
}
|
|
688
689
|
|
|
689
690
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
690
691
|
navigator.clipboard.writeText(nodeId).then(function () {
|
|
691
|
-
RED.notify("NodeID
|
|
692
|
+
RED.notify("NodeID copied.", "success");
|
|
692
693
|
}).catch(function () {
|
|
693
|
-
RED.notify("
|
|
694
|
+
RED.notify("Failed to copy NodeID.", "error");
|
|
694
695
|
});
|
|
695
696
|
return;
|
|
696
697
|
}
|
|
@@ -704,13 +705,48 @@
|
|
|
704
705
|
input[0].select();
|
|
705
706
|
try {
|
|
706
707
|
document.execCommand("copy");
|
|
707
|
-
RED.notify("NodeID
|
|
708
|
+
RED.notify("NodeID copied.", "success");
|
|
708
709
|
} catch (error) {
|
|
709
|
-
RED.notify("
|
|
710
|
+
RED.notify("Failed to copy NodeID.", "error");
|
|
710
711
|
}
|
|
711
712
|
input.remove();
|
|
712
713
|
}
|
|
713
714
|
|
|
715
|
+
function readValueFromPath(path) {
|
|
716
|
+
var item = getItemAtPath(path);
|
|
717
|
+
var nodeId = nodeIdOf(item);
|
|
718
|
+
if (!nodeId) {
|
|
719
|
+
RED.notify("NodeID not found for the selected item.", "warning");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
var connectionId = $("#node-input-connection").val();
|
|
724
|
+
if (!connectionId) {
|
|
725
|
+
RED.notify("Select an OPC UA connection before reading.", "warning");
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
$.getJSON("opcua-client-config/" + encodeURIComponent(connectionId) + "/read", {
|
|
730
|
+
nodeId: nodeId
|
|
731
|
+
}).done(function (payload) {
|
|
732
|
+
if (payload && payload.error) {
|
|
733
|
+
RED.notify("Read failed: " + payload.error, "error");
|
|
734
|
+
} else if (payload) {
|
|
735
|
+
var valueText = (payload.valueEnumeration !== undefined && payload.valueEnumeration !== null)
|
|
736
|
+
? payload.valueEnumeration + " (" + payload.value + ")"
|
|
737
|
+
: (payload.value !== undefined ? String(payload.value) : "undefined");
|
|
738
|
+
RED.notify("Variable value: " + valueText, "success");
|
|
739
|
+
} else {
|
|
740
|
+
RED.notify("No value returned from the server.", "warning");
|
|
741
|
+
}
|
|
742
|
+
}).fail(function (xhr) {
|
|
743
|
+
var message = xhr && xhr.responseJSON && xhr.responseJSON.error
|
|
744
|
+
? xhr.responseJSON.error
|
|
745
|
+
: "Failed to read variable value.";
|
|
746
|
+
RED.notify(message, "error");
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
714
750
|
function setBrowseSelectedPath(path) {
|
|
715
751
|
browseSelectedPath = path || "";
|
|
716
752
|
$(".opcua-tree-row").removeClass("is-selected");
|
|
@@ -759,20 +795,51 @@
|
|
|
759
795
|
renderBrowseTree();
|
|
760
796
|
var browseNodeId = item.nodeID || item.nodeId;
|
|
761
797
|
loadBrowse(browseNodeId).done(function (payload) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
798
|
+
try {
|
|
799
|
+
if (!payload) {
|
|
800
|
+
console.error("Browse returned empty payload for node " + browseNodeId);
|
|
801
|
+
RED.notify("Browse returned empty payload.", "error");
|
|
802
|
+
item.browse = [];
|
|
803
|
+
} else if (payload.error) {
|
|
804
|
+
console.error("Browse returned error for node " + browseNodeId + ":", payload.error);
|
|
805
|
+
RED.notify(payload.error, "error");
|
|
806
|
+
item.browse = [];
|
|
807
|
+
} else {
|
|
808
|
+
item.browse = Array.isArray(payload.browse) ? payload.browse : [];
|
|
809
|
+
}
|
|
810
|
+
saveBrowseSession();
|
|
811
|
+
renderBrowseTree();
|
|
812
|
+
triggerChildrenExpansion(item.browse, path);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
console.error("Error handling browse payload for node " + browseNodeId + ":", err);
|
|
815
|
+
RED.notify("Error handling browse response: " + err.message, "error");
|
|
816
|
+
expansionState[path] = false;
|
|
817
|
+
saveBrowseSession();
|
|
818
|
+
renderBrowseTree();
|
|
819
|
+
}
|
|
768
820
|
}).fail(function (xhr) {
|
|
769
821
|
expansionState[path] = false;
|
|
770
822
|
saveBrowseSession();
|
|
771
823
|
renderBrowseTree();
|
|
772
824
|
var message = xhr && xhr.responseJSON && xhr.responseJSON.error
|
|
773
825
|
? xhr.responseJSON.error
|
|
774
|
-
: "
|
|
826
|
+
: "Failed to expand the node.";
|
|
775
827
|
RED.notify(message, "error");
|
|
828
|
+
console.error("Browse request failed for node " + browseNodeId + ":", xhr);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function triggerChildrenExpansion(children, parentPath) {
|
|
833
|
+
if (!Array.isArray(children)) return;
|
|
834
|
+
children.forEach(function (child, index) {
|
|
835
|
+
var childPath = parentPath + ".browse." + index;
|
|
836
|
+
if (isExpanded(childPath, false)) {
|
|
837
|
+
if (!Array.isArray(child.browse)) {
|
|
838
|
+
expandNode(childPath);
|
|
839
|
+
} else {
|
|
840
|
+
triggerChildrenExpansion(child.browse, childPath);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
776
843
|
});
|
|
777
844
|
}
|
|
778
845
|
|
|
@@ -1057,7 +1124,24 @@
|
|
|
1057
1124
|
if ($(event.target).closest(".opcua-client-toggle-tree, .opcua-client-toggle-tag, .opcua-tree-actions, #node-input-browse-context-menu").length) {
|
|
1058
1125
|
return;
|
|
1059
1126
|
}
|
|
1060
|
-
|
|
1127
|
+
var path = $(this).attr("data-path");
|
|
1128
|
+
setBrowseSelectedPath(path);
|
|
1129
|
+
|
|
1130
|
+
if (event.ctrlKey || event.metaKey) {
|
|
1131
|
+
event.preventDefault();
|
|
1132
|
+
if ($("#node-input-mode").val() === "method") {
|
|
1133
|
+
var item = getItemAtPath(path);
|
|
1134
|
+
if (item && item.nodeClass === "Method") {
|
|
1135
|
+
var parentPath = path.split(".");
|
|
1136
|
+
parentPath.splice(parentPath.length - 2, 2);
|
|
1137
|
+
parentPath = parentPath.join(".");
|
|
1138
|
+
var parentItem = getItemAtPath(parentPath);
|
|
1139
|
+
addMethodFromTree(item, parentItem);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
toggleSelectedNode(path);
|
|
1144
|
+
}
|
|
1061
1145
|
});
|
|
1062
1146
|
|
|
1063
1147
|
$(document).on("contextmenu", ".opcua-tree-row", function (event) {
|
|
@@ -1091,6 +1175,14 @@
|
|
|
1091
1175
|
copyNodeIdFromPath(path);
|
|
1092
1176
|
});
|
|
1093
1177
|
|
|
1178
|
+
$(document).on("click", "#node-input-browse-context-read-value", function (event) {
|
|
1179
|
+
event.preventDefault();
|
|
1180
|
+
var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
|
|
1181
|
+
var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
|
|
1182
|
+
hideTreeContextMenu();
|
|
1183
|
+
readValueFromPath(path);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1094
1186
|
$(document).on("click", function (event) {
|
|
1095
1187
|
if (!$(event.target).closest("#node-input-browse-context-menu").length) {
|
|
1096
1188
|
hideTreeContextMenu();
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitormnm/node-red-simple-opcua",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"description": "
|
|
7
|
+
"description": "OPC UA server and client nodes with a graphical interface, powered by node-opcua",
|
|
8
8
|
"main": "server/opcua-server.js",
|
|
9
9
|
"node-red": {
|
|
10
10
|
"version": ">=3.0.0",
|