@vitormnm/node-red-simple-opcua 1.6.3 → 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.
Files changed (45) hide show
  1. package/README.md +104 -136
  2. package/client/lib/opcua-client-browser.js +254 -11
  3. package/client/lib/opcua-client-method-service.js +1 -1
  4. package/client/lib/opcua-client-subscription-service.js +0 -2
  5. package/client/lib/opcua-client-write-service.js +14 -4
  6. package/client/opcua-client-config.html +118 -1
  7. package/client/opcua-client-config.js +112 -9
  8. package/client/opcua-client-help.html +6 -0
  9. package/client/opcua-client-utils.js +158 -10
  10. package/client/opcua-client.html +8 -0
  11. package/client/opcua-client.js +97 -1
  12. package/client/view/opcua-client.js +106 -14
  13. package/examples/flows_simple_opc.json +1 -1
  14. package/package.json +2 -2
  15. package/server/lib/opcua-address-space-alarm.js +95 -32
  16. package/server/lib/opcua-address-space-builder.js +717 -59
  17. package/server/lib/opcua-config.js +110 -35
  18. package/server/lib/opcua-server-events-child.js +31 -5
  19. package/server/lib/opcua-server-runtime-child.js +424 -27
  20. package/server/lib/opcua-server-runtime.js +52 -5
  21. package/server/lib/opcua-server-status-child.js +46 -15
  22. package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
  23. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
  24. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  25. package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
  26. package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
  27. package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  28. package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
  29. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
  30. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  31. package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
  32. package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
  33. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
  34. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
  35. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
  36. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  37. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
  38. package/server/opcua-server-io.html +93 -1
  39. package/server/opcua-server-io.js +153 -29
  40. package/server/opcua-server-registry.js +8 -2
  41. package/server/opcua-server.css +64 -0
  42. package/server/opcua-server.html +168 -44
  43. package/server/opcua-server.js +115 -5
  44. package/server/view/opcua-server.css +100 -6
  45. package/server/view/opcua-server.js +746 -48
package/README.md CHANGED
@@ -1,136 +1,104 @@
1
- OPC UA client and server with a simple graphical interface for Node-RED.
2
- Fully parameterized in JSON.
3
-
4
- It supports the following OPC UA items on only 3 nodes.
5
-
6
- - alarms
7
- - events
8
- - events read and write tags in server(See which tags are being written to or read from the client directly on the server in a simple workflow)
9
- - methods(write methods in node-red flow)
10
- - variables
11
- - variables arrays
12
- - description and displayname nodes
13
- - objects
14
- - simple objectsType
15
- - custom namespace
16
- - custom nodeID
17
- - Subscription Variables and events in client
18
-
19
- **Server editor**
20
- ![node-red-si](/resources/editorServer.PNG)
21
-
22
- **Client editor**
23
- ![node-red-si](/resources/editorClient.PNG)
24
-
25
- ### Support the development of this project and others if you found it useful.
26
- <a href="https://buymeacoffee.com/vitormnm">
27
- <img src="./resources/bmc-button.svg" alt="Logo" width="200">
28
- </a>
29
-
30
- ---
31
-
32
- example json server config
33
- ```
34
- {
35
- "objects": [],
36
- "folders": [
37
- {
38
- "name": "MyServer",
39
- "displayName": "",
40
- "description": "",
41
- "nodeId": "",
42
- "namespaceId": 2,
43
- "objectsType": "",
44
- "folders": [
45
- {
46
- "name": "newFolder",
47
- "displayName": "",
48
- "description": "",
49
- "nodeId": "",
50
- "namespaceId": 2,
51
- "objectsType": "",
52
- "folders": [],
53
- "objects": [],
54
- "variables": [
55
- {
56
- "name": "newVariable",
57
- "type": "Int32",
58
- "value": "",
59
- "access": "readwrite",
60
- "description": "",
61
- "displayName": "",
62
- "nodeId": "",
63
- "namespaceId": 2
64
- }
65
- ],
66
- "alarms": [],
67
- "methods": [],
68
- "objectsTypes": []
69
- }
70
- ],
71
- "objects": [
72
- {
73
- "name": "newObject",
74
- "displayName": "",
75
- "description": "",
76
- "nodeId": "",
77
- "namespaceId": 2,
78
- "objectsType": "",
79
- "folders": [],
80
- "objects": [],
81
- "variables": [
82
- {
83
- "name": "newVariable",
84
- "type": "Int32",
85
- "value": "",
86
- "access": "readwrite",
87
- "description": "",
88
- "displayName": "",
89
- "nodeId": "",
90
- "namespaceId": 2
91
- }
92
- ],
93
- "alarms": [],
94
- "methods": [],
95
- "objectsTypes": []
96
- }
97
- ],
98
- "variables": [
99
- {
100
- "name": "newVariable",
101
- "type": "Int32",
102
- "value": "",
103
- "access": "readwrite",
104
- "description": "",
105
- "displayName": "",
106
- "nodeId": "",
107
- "namespaceId": 2
108
- }
109
- ],
110
- "alarms": [],
111
- "methods": [
112
- {
113
- "name": "newMethod",
114
- "description": "",
115
- "displayName": "",
116
- "nodeId": "",
117
- "namespaceId": 2,
118
- "inputs": [],
119
- "outputs": []
120
- }
121
- ],
122
- "objectsTypes": []
123
- }
124
- ],
125
- "objectsTypes": [],
126
- "nameSpaces": [
127
- {
128
- "id": 2,
129
- "name": "urn:node-red:opc-ua-server"
130
- }
131
- ]
132
- }
133
- ```
134
- Disclaimer
135
- This node was only used in simulation and testing environments.
136
-
1
+ OPC UA client and server with a simple graphical interface for Node-RED.
2
+
3
+ This package provides a simplified, highly parameterized set of nodes to establish OPC UA Servers and Clients in Node-RED without complex coding.
4
+
5
+ ---
6
+
7
+ ## Features Overview
8
+
9
+ - **Dynamic OPC UA Address Space**: Build hierarchical folders, objects, variables, and alarms dynamically from a visual tree editor or JSON.
10
+ - **IPC Process Separation**: Server runtime runs in a child process, preventing heavy OPC UA operations from blocking the main Node-RED event loop.
11
+ - **Rich Node-RED Integration**: Support for reading, writing, subscribing, calling methods, and managing sessions directly in Node-RED flows.
12
+
13
+ ---
14
+
15
+ ## Server Functionalities
16
+
17
+ The server implementation consists of two nodes: `opc-ua-server` (the server runtime) and `opcua-server-io` (the data input/output node).
18
+
19
+ ### 1. OPC UA Server (`opc-ua-server`)
20
+ This node instantiates the OPC UA Server process.
21
+ - **Visual Tree Editor**: Build folders, variables, custom objects, ObjectTypes (templates), alarms, methods, and enumerations visually.
22
+ - **Dynamic Address Space**: Rebuild the address space at runtime by passing a new JSON tree configuration in `msg.payload`.
23
+ - **Authentication & Authorization**: Configure users, groups, and comma-separated access permissions for folders, variables, and methods.
24
+ - **Security Policies**: Supports multiple security policies (`None`, `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, etc.) and security modes (`None`, `Sign`, `SignAndEncrypt`).
25
+ - **Certificate Management**: Manage trusted/rejected client certificates.
26
+
27
+ ### 2. Server I/O Node (`opcua-server-io`)
28
+ Interacts with an active local server instance. It supports the following modes:
29
+ - **Read**: Read values from server variables using a tag path or NodeId.
30
+ - **Write**: Write values to server variables.
31
+ - **Event**: Fire custom OPC UA events on target nodes.
32
+ - **Events**: Stream variable read, write, and alarm events.
33
+ - **Status**: Monitor server variable status snapshots.
34
+ - **Active Alarms**: Query active alarms on the server.
35
+ - **Get Sessions**: Retrieve active OPC UA client sessions.
36
+ - **Delete Sessions**: Forcefully close specific active sessions by ID.
37
+ - **Method Input / Output**: Route OPC UA method calls from clients into Node-RED flows and return output results.
38
+
39
+ ---
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
+
58
+ ## Client Functionalities
59
+
60
+ The client implementation consists of `opcua-client-config` (shared connection configuration) and `opcua-client` (the action node).
61
+
62
+ ### 1. Client Connection Config (`opcua-client-config`)
63
+ Manages connection configuration to an external OPC UA server.
64
+ - **Connection Strategy**: Limit reconnect retries to fail fast when servers are offline.
65
+ - **Authentication**: Supports anonymous or username/password authentication.
66
+ - **Session Caching**: Caches and reuse client sessions and method definitions.
67
+ - **Address Space Browser**: Provides an endpoint for browsing the server address space directly in the Node-RED editor.
68
+
69
+ ### 2. OPC UA Client (`opcua-client`)
70
+ Performs actions on the configured OPC UA server. It operates in the following modes:
71
+ - **Read**: Batch read selected variables.
72
+ - **Write**: Batch write values with automatic type resolution.
73
+ - **Browse**: Browse child nodes of one or more node paths.
74
+ - **Browse Recursive**: Recursively search all nodes starting from specified NodeIds, returning resolved type definitions, values, descriptions, and method arguments in optimized batched reads.
75
+ - **Method**: Call OPC UA methods on the server.
76
+ - **Subscription**: Subscribe to variable value updates (minimum 50ms sampling/publishing).
77
+ - **Events**: Subscribe to OPC UA events.
78
+
79
+ ---
80
+
81
+ ## Editor Interfaces
82
+
83
+ ### Server Editor
84
+ ![Server Editor](./resources/editorServer.PNG)
85
+
86
+ ### Client Editor
87
+ ![Client Editor](./resources/editorClient.PNG)
88
+
89
+ ---
90
+
91
+
92
+
93
+
94
+ ### Support the Project
95
+ If you find this project useful, please support its development:
96
+
97
+ <a href="https://buymeacoffee.com/vitormnm">
98
+ <img src="./resources/bmc-button.svg" alt="Buy Me A Coffee" width="200">
99
+ </a>
100
+
101
+
102
+ [@vitormnm](https://vitormiao.com/)
103
+
104
+ [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/)
@@ -6,15 +6,19 @@ const {
6
6
  DataType,
7
7
  NodeClass,
8
8
  coerceNodeId,
9
- makeNodeId
9
+ makeNodeId,
10
+ VariantArrayType
10
11
  } = require("node-opcua");
11
12
 
13
+ const { enrichItemResultWithEnumeration, reshapeArray } = require("../opcua-client-utils");
14
+
12
15
  async function browseNode(session, root) {
13
16
  const nodeID = normalizeNodeId(root.nodeID || root.nodeId || ROOT_NODE_ID);
14
17
  const result = {
15
18
  name: root.name || await readBrowseName(session, nodeID, "RootFolder"),
16
19
  nodeID,
17
- browse: []
20
+ status: "Good",
21
+ children: []
18
22
  };
19
23
 
20
24
 
@@ -30,11 +34,16 @@ async function browseNode(session, root) {
30
34
 
31
35
  let browseResult = await session.browse({
32
36
  nodeId: nodeID,
37
+ referenceTypeId: makeNodeId(33, 0), // HierarchicalReferences
33
38
  browseDirection: BrowseDirection.Forward,
34
39
  includeSubtypes: true,
35
40
  resultMask: 63
36
41
  });
37
42
 
43
+ if (browseResult.statusCode && !browseResult.statusCode.isGood()) {
44
+ throw new Error("Browse failed: " + browseResult.statusCode.toString());
45
+ }
46
+
38
47
  let references = [
39
48
  ...(browseResult.references || [])
40
49
  ];
@@ -61,19 +70,34 @@ async function browseNode(session, root) {
61
70
 
62
71
  // Monta lista de todos os atributos de todos os nós de uma vez
63
72
  const nodeIds = references.map(ref => normalizeNodeId(ref.nodeId));
64
-
65
-
66
- const attributesToRead = nodeIds.flatMap(nodeId => [
67
- { nodeId, attributeId: AttributeIds.Description },
68
- { nodeId, attributeId: AttributeIds.DataType },
69
- { nodeId, attributeId: AttributeIds.Value },
70
- ]);
73
+ const typeIds = references
74
+ .map(ref => ref.typeDefinition ? normalizeNodeId(ref.typeDefinition) : null)
75
+ .filter(Boolean);
76
+ const uniqueTypeIds = [...new Set(typeIds)];
77
+
78
+ const attributesToRead = [
79
+ ...nodeIds.flatMap(nodeId => [
80
+ { nodeId, attributeId: AttributeIds.Description },
81
+ { nodeId, attributeId: AttributeIds.DataType },
82
+ { nodeId, attributeId: AttributeIds.Value },
83
+ ]),
84
+ ...uniqueTypeIds.map(nodeId => ({ nodeId, attributeId: AttributeIds.BrowseName }))
85
+ ];
71
86
 
72
87
  // UMA única chamada para todos os nós e atributos
73
88
  const dataValues = await session.read(attributesToRead);
74
89
 
90
+ const typeNamesMap = new Map();
91
+ const typeStartIdx = nodeIds.length * 3;
92
+ uniqueTypeIds.forEach((typeId, index) => {
93
+ const browseNameVal = dataValues[typeStartIdx + index]?.value?.value;
94
+ const name = browseNameVal?.name || typeId;
95
+ typeNamesMap.set(typeId, name);
96
+ });
97
+
98
+ const cache = new Map();
75
99
  // Distribui os resultados por nó (3 atributos por nó)
76
- result.browse = await Promise.all(references.map(async (reference, i) => {
100
+ result.children = await Promise.all(references.map(async (reference, i) => {
77
101
  const childNodeId = nodeIds[i];
78
102
  const nodeClass = resolveNodeClassName(reference.nodeClass);
79
103
  const browseName = extractBrowseName(reference.browseName, childNodeId);
@@ -86,15 +110,40 @@ async function browseNode(session, root) {
86
110
 
87
111
  const item = { nodeID: childNodeId, nodeClass, browseName, displayName, description };
88
112
 
113
+ const typeNodeId = reference.typeDefinition ? normalizeNodeId(reference.typeDefinition) : null;
114
+ if (typeNodeId) {
115
+ const typeName = typeNamesMap.get(typeNodeId) || typeNodeId;
116
+ item.typeDefinition = typeNodeId;
117
+ item.hasTypeDefinition = {
118
+ nodeID: typeNodeId,
119
+ browseName: typeName,
120
+ displayName: typeName
121
+ };
122
+ }
123
+
89
124
  if (nodeClass === "Variable") {
90
125
  const dataTypeValue = dataValues[i * 3 + 1]?.value?.value;
91
- const rawValue = dataValues[i * 3 + 2]?.value?.value;
126
+ const rawValueVariant = dataValues[i * 3 + 2]?.value;
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
+ }
92
135
 
93
136
  item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
94
137
  ? (DataType[dataTypeValue.value] || dataTypeValue.toString())
95
138
  : (dataTypeValue?.toString() ?? "");
96
139
 
140
+ if (rawValueVariant?.dataType === DataType.Enumeration) {
141
+ item.dataType = "Enumeration";
142
+ }
143
+
97
144
  item.value = rawValue ?? "";
145
+
146
+ await enrichItemResultWithEnumeration(item, session, cache, childNodeId);
98
147
  }
99
148
 
100
149
  if (nodeClass === "Method") {
@@ -358,11 +407,205 @@ function resolveArgumentDataType(dataType) {
358
407
  return dataType.toString();
359
408
  }
360
409
 
410
+ async function browseRecursiveNode(session, root) {
411
+ const startNodeId = normalizeNodeId(root.nodeID || root.nodeId || ROOT_NODE_ID);
412
+ const rootName = root.name || await readBrowseName(session, startNodeId, "RootFolder");
413
+
414
+ const visited = new Set();
415
+ visited.add(startNodeId);
416
+
417
+ const allItems = [];
418
+
419
+ async function traverse(nodeID) {
420
+ let browseResult;
421
+ try {
422
+ browseResult = await session.browse({
423
+ nodeId: nodeID,
424
+ referenceTypeId: makeNodeId(33, 0), // HierarchicalReferences
425
+ browseDirection: BrowseDirection.Forward,
426
+ includeSubtypes: true,
427
+ resultMask: 63
428
+ });
429
+ } catch (err) {
430
+ if (nodeID === startNodeId) {
431
+ throw err;
432
+ }
433
+ return [];
434
+ }
435
+
436
+ if (browseResult.statusCode && !browseResult.statusCode.isGood()) {
437
+ if (nodeID === startNodeId) {
438
+ throw new Error("Browse failed: " + browseResult.statusCode.toString());
439
+ }
440
+ return [];
441
+ }
442
+
443
+ let references = [...(browseResult.references || [])];
444
+
445
+ while (browseResult.continuationPoint) {
446
+ try {
447
+ browseResult = await session.browseNext(
448
+ browseResult.continuationPoint,
449
+ false
450
+ );
451
+ references.push(...(browseResult.references || []));
452
+ } catch (err) {
453
+ break;
454
+ }
455
+ }
456
+
457
+ if (!references.length) {
458
+ return [];
459
+ }
460
+
461
+ const items = [];
462
+ for (const ref of references) {
463
+ const childNodeId = normalizeNodeId(ref.nodeId);
464
+ const nodeClass = resolveNodeClassName(ref.nodeClass);
465
+ const browseName = extractBrowseName(ref.browseName, childNodeId);
466
+ const displayName = extractDisplayName(ref.displayName, browseName);
467
+ const typeNodeId = ref.typeDefinition ? normalizeNodeId(ref.typeDefinition) : null;
468
+
469
+ const item = {
470
+ nodeID: childNodeId,
471
+ nodeClass,
472
+ browseName,
473
+ displayName
474
+ };
475
+ if (typeNodeId) {
476
+ item.typeDefinition = typeNodeId;
477
+ }
478
+
479
+ items.push(item);
480
+ allItems.push(item);
481
+
482
+ const expandable = nodeClass === "Object" || nodeClass === "Folder" || nodeClass === "View" || nodeClass === "ObjectType";
483
+ if (expandable && !visited.has(childNodeId)) {
484
+ visited.add(childNodeId);
485
+ item.children = await traverse(childNodeId);
486
+ }
487
+ }
488
+
489
+ return items;
490
+ }
491
+
492
+ const browseResult = await traverse(startNodeId);
493
+
494
+ if (allItems.length > 0) {
495
+ const cache = new Map();
496
+ const typeIds = allItems
497
+ .map(item => item.typeDefinition)
498
+ .filter(Boolean);
499
+ const uniqueTypeIds = [...new Set(typeIds)];
500
+
501
+ const typeNamesMap = new Map();
502
+ if (uniqueTypeIds.length > 0) {
503
+ try {
504
+ const typeAttributes = uniqueTypeIds.map(nodeId => ({ nodeId, attributeId: AttributeIds.BrowseName }));
505
+ const typeDataValues = await session.read(typeAttributes);
506
+ uniqueTypeIds.forEach((typeId, index) => {
507
+ const browseNameVal = typeDataValues[index]?.value?.value;
508
+ const name = browseNameVal?.name || typeId;
509
+ typeNamesMap.set(typeId, name);
510
+ });
511
+ } catch (err) {
512
+ // Ignore type names read error, we'll fallback to NodeId below
513
+ }
514
+ }
515
+
516
+ const BATCH_SIZE = 100;
517
+ for (let i = 0; i < allItems.length; i += BATCH_SIZE) {
518
+ const chunk = allItems.slice(i, i + BATCH_SIZE);
519
+ const attributesToRead = chunk.flatMap(item => [
520
+ { nodeId: item.nodeID, attributeId: AttributeIds.Description },
521
+ { nodeId: item.nodeID, attributeId: AttributeIds.DataType },
522
+ { nodeId: item.nodeID, attributeId: AttributeIds.Value }
523
+ ]);
524
+
525
+ try {
526
+ const dataValues = await session.read(attributesToRead);
527
+ await Promise.all(chunk.map(async (item, index) => {
528
+ const descValue = dataValues[index * 3]?.value?.value;
529
+ item.description = typeof descValue === "string"
530
+ ? descValue
531
+ : (descValue?.text ?? "");
532
+
533
+ if (item.nodeClass === "Variable") {
534
+ const dataTypeValue = dataValues[index * 3 + 1]?.value?.value;
535
+ const rawValueVariant = dataValues[index * 3 + 2]?.value;
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
+ }
544
+
545
+ item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
546
+ ? (DataType[dataTypeValue.value] || dataTypeValue.toString())
547
+ : (dataTypeValue?.toString() ?? "");
548
+
549
+ if (rawValueVariant?.dataType === DataType.Enumeration) {
550
+ item.dataType = "Enumeration";
551
+ }
552
+
553
+ item.value = rawValue ?? "";
554
+
555
+ await enrichItemResultWithEnumeration(item, session, cache, item.nodeID);
556
+ }
557
+
558
+ if (item.typeDefinition) {
559
+ const typeName = typeNamesMap.get(item.typeDefinition) || item.typeDefinition;
560
+ item.hasTypeDefinition = {
561
+ nodeID: item.typeDefinition,
562
+ browseName: typeName,
563
+ displayName: typeName
564
+ };
565
+ }
566
+ }));
567
+ } catch (readError) {
568
+ chunk.forEach(item => {
569
+ item.description = "";
570
+ if (item.nodeClass === "Variable") {
571
+ item.dataType = "";
572
+ item.value = "";
573
+ }
574
+ if (item.typeDefinition) {
575
+ const typeName = typeNamesMap.get(item.typeDefinition) || item.typeDefinition;
576
+ item.hasTypeDefinition = {
577
+ nodeID: item.typeDefinition,
578
+ browseName: typeName,
579
+ displayName: typeName
580
+ };
581
+ }
582
+ });
583
+ }
584
+ }
585
+
586
+ const methods = allItems.filter(item => item.nodeClass === "Method");
587
+ for (const method of methods) {
588
+ const definition = await readMethodArguments(session, method.nodeID);
589
+ method.inputArguments = definition.inputArguments;
590
+ method.outputArguments = definition.outputArguments;
591
+ }
592
+ }
593
+
594
+ return {
595
+ name: rootName,
596
+ nodeID: startNodeId,
597
+ status: "Good",
598
+ children: browseResult
599
+ };
600
+ }
601
+
361
602
  const ROOT_NODE_ID = "i=84";
362
603
 
363
604
  module.exports = {
364
605
  browseNode,
606
+ browseRecursiveNode,
365
607
  normalizeBrowseRoots,
366
608
  normalizeNodeId,
367
609
  ROOT_NODE_ID
368
610
  };
611
+
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const { dataValueToItemResult, ensureArrayPayload, resolveNodeId, resolveMethodObjectId, buildVariantFromItem, callResultToItemResult } = require("../opcua-client-utils");
3
+ const { dataValueToItemResult, ensureArrayPayload, resolveNodeId, resolveMethodObjectId, buildVariantFromItem, callResultToItemResult, getMethodArgumentDefinition } = require("../opcua-client-utils");
4
4
 
5
5
  class OpcUaClientMethodService {
6
6
  async execute(node, msg, session, itemsResolver) {
@@ -34,7 +34,6 @@ class OpcUaClientSubscriptionService {
34
34
 
35
35
  subscription.on("error", (error) => {
36
36
  node.status({ fill: "red", shape: "ring", text: "subscription error" });
37
- node.error(error);
38
37
  });
39
38
 
40
39
  node.subscription = subscription;
@@ -97,7 +96,6 @@ class OpcUaClientSubscriptionService {
97
96
 
98
97
  subscription.on("error", (error) => {
99
98
  node.status({ fill: "red", shape: "ring", text: "subscription error" });
100
- node.error(error);
101
99
  });
102
100
 
103
101
  node.subscription = subscription;
@@ -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
- // Se o batch falhar por completo, marca todos com erro
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: variants[index].value,
125
+ value: val,
116
126
  type: typeName,
117
127
  status: scName,
118
128
  sourceTimestamp: null,