@vitormnm/node-red-simple-opcua 1.6.2 → 1.7.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 +89 -136
- package/client/lib/opcua-client-browser.js +238 -10
- package/client/lib/opcua-client-method-service.js +1 -1
- package/client/lib/opcua-client-subscription-service.js +0 -2
- package/client/opcua-client-config.html +118 -1
- package/client/opcua-client-config.js +74 -8
- package/client/opcua-client-help.html +6 -0
- package/client/opcua-client-utils.js +34 -10
- package/client/opcua-client.html +7 -0
- package/client/opcua-client.js +97 -1
- package/examples/flows_simple_opc.json +1 -1
- package/package.json +1 -1
- package/server/lib/opcua-address-space-alarm.js +11 -5
- package/server/lib/opcua-address-space-builder.js +65 -15
- package/server/lib/opcua-config.js +81 -23
- package/server/lib/opcua-server-events-child.js +1 -1
- package/server/lib/opcua-server-runtime-child.js +429 -59
- package/server/lib/opcua-server-runtime.js +49 -5
- package/server/lib/opcua-server-status-child.js +14 -14
- package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
- package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
- package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
- package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
- package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
- package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
- package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
- package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
- package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
- package/server/opcua-server-io.html +76 -0
- package/server/opcua-server-io.js +135 -23
- package/server/opcua-server.css +52 -0
- package/server/opcua-server.html +166 -44
- package/server/opcua-server.js +142 -7
- package/server/view/opcua-server.css +89 -6
- package/server/view/opcua-server.js +523 -42
package/README.md
CHANGED
|
@@ -1,136 +1,89 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
# @vitormnm/node-red-simple-opcua
|
|
2
|
+
|
|
3
|
+
OPC UA client and server with a simple graphical interface for Node-RED.
|
|
4
|
+
|
|
5
|
+
This package provides a simplified, highly parameterized set of nodes to establish OPC UA Servers and Clients in Node-RED without complex coding.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features Overview
|
|
10
|
+
|
|
11
|
+
- **Dynamic OPC UA Address Space**: Build hierarchical folders, objects, variables, and alarms dynamically from a visual tree editor or JSON.
|
|
12
|
+
- **IPC Process Separation**: Server runtime runs in a child process, preventing heavy OPC UA operations from blocking the main Node-RED event loop.
|
|
13
|
+
- **Rich Node-RED Integration**: Support for reading, writing, subscribing, calling methods, and managing sessions directly in Node-RED flows.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Server Functionalities
|
|
18
|
+
|
|
19
|
+
The server implementation consists of two nodes: `opc-ua-server` (the server runtime) and `opcua-server-io` (the data input/output node).
|
|
20
|
+
|
|
21
|
+
### 1. OPC UA Server (`opc-ua-server`)
|
|
22
|
+
This node instantiates the OPC UA Server process.
|
|
23
|
+
- **Visual Tree Editor**: Build folders, variables, custom objects, ObjectTypes (templates), alarms, methods, and enumerations visually.
|
|
24
|
+
- **Dynamic Address Space**: Rebuild the address space at runtime by passing a new JSON tree configuration in `msg.payload`.
|
|
25
|
+
- **Authentication & Authorization**: Configure users, groups, and comma-separated access permissions for folders, variables, and methods.
|
|
26
|
+
- **Security Policies**: Supports multiple security policies (`None`, `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, etc.) and security modes (`None`, `Sign`, `SignAndEncrypt`).
|
|
27
|
+
- **Certificate Management**: Manage trusted/rejected client certificates.
|
|
28
|
+
|
|
29
|
+
### 2. Server I/O Node (`opcua-server-io`)
|
|
30
|
+
Interacts with an active local server instance. It supports the following modes:
|
|
31
|
+
- **Read**: Read values from server variables using a tag path or NodeId.
|
|
32
|
+
- **Write**: Write values to server variables.
|
|
33
|
+
- **Event**: Fire custom OPC UA events on target nodes.
|
|
34
|
+
- **Events**: Stream variable read, write, and alarm events.
|
|
35
|
+
- **Status**: Monitor server variable status snapshots.
|
|
36
|
+
- **Active Alarms**: Query active alarms on the server.
|
|
37
|
+
- **Get Sessions**: Retrieve active OPC UA client sessions.
|
|
38
|
+
- **Delete Sessions**: Forcefully close specific active sessions by ID.
|
|
39
|
+
- **Method Input / Output**: Route OPC UA method calls from clients into Node-RED flows and return output results.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Client Functionalities
|
|
44
|
+
|
|
45
|
+
The client implementation consists of `opcua-client-config` (shared connection configuration) and `opcua-client` (the action node).
|
|
46
|
+
|
|
47
|
+
### 1. Client Connection Config (`opcua-client-config`)
|
|
48
|
+
Manages connection configuration to an external OPC UA server.
|
|
49
|
+
- **Connection Strategy**: Limit reconnect retries to fail fast when servers are offline.
|
|
50
|
+
- **Authentication**: Supports anonymous or username/password authentication.
|
|
51
|
+
- **Session Caching**: Caches and reuse client sessions and method definitions.
|
|
52
|
+
- **Address Space Browser**: Provides an endpoint for browsing the server address space directly in the Node-RED editor.
|
|
53
|
+
|
|
54
|
+
### 2. OPC UA Client (`opcua-client`)
|
|
55
|
+
Performs actions on the configured OPC UA server. It operates in the following modes:
|
|
56
|
+
- **Read**: Batch read selected variables.
|
|
57
|
+
- **Write**: Batch write values with automatic type resolution.
|
|
58
|
+
- **Browse**: Browse child nodes of one or more node paths.
|
|
59
|
+
- **Browse Recursive**: Recursively search all nodes starting from specified NodeIds, returning resolved type definitions, values, descriptions, and method arguments in optimized batched reads.
|
|
60
|
+
- **Method**: Call OPC UA methods on the server.
|
|
61
|
+
- **Subscription**: Subscribe to variable value updates (minimum 50ms sampling/publishing).
|
|
62
|
+
- **Events**: Subscribe to OPC UA events.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Editor Interfaces
|
|
67
|
+
|
|
68
|
+
### Server Editor
|
|
69
|
+

|
|
70
|
+
|
|
71
|
+
### Client Editor
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
### Support the Project
|
|
80
|
+
If you find this project useful, please support its development:
|
|
81
|
+
|
|
82
|
+
<a href="https://buymeacoffee.com/vitormnm">
|
|
83
|
+
<img src="./resources/bmc-button.svg" alt="Buy Me A Coffee" width="200">
|
|
84
|
+
</a>
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
[@vitormnm](https://vitormiao.com/)
|
|
88
|
+
|
|
89
|
+
[](https://choosealicense.com/licenses/mit/)
|
|
@@ -9,12 +9,15 @@ const {
|
|
|
9
9
|
makeNodeId
|
|
10
10
|
} = require("node-opcua");
|
|
11
11
|
|
|
12
|
+
const { enrichItemResultWithEnumeration } = require("../opcua-client-utils");
|
|
13
|
+
|
|
12
14
|
async function browseNode(session, root) {
|
|
13
15
|
const nodeID = normalizeNodeId(root.nodeID || root.nodeId || ROOT_NODE_ID);
|
|
14
16
|
const result = {
|
|
15
17
|
name: root.name || await readBrowseName(session, nodeID, "RootFolder"),
|
|
16
18
|
nodeID,
|
|
17
|
-
|
|
19
|
+
status: "Good",
|
|
20
|
+
children: []
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
|
|
@@ -30,11 +33,16 @@ async function browseNode(session, root) {
|
|
|
30
33
|
|
|
31
34
|
let browseResult = await session.browse({
|
|
32
35
|
nodeId: nodeID,
|
|
36
|
+
referenceTypeId: makeNodeId(33, 0), // HierarchicalReferences
|
|
33
37
|
browseDirection: BrowseDirection.Forward,
|
|
34
38
|
includeSubtypes: true,
|
|
35
39
|
resultMask: 63
|
|
36
40
|
});
|
|
37
41
|
|
|
42
|
+
if (browseResult.statusCode && !browseResult.statusCode.isGood()) {
|
|
43
|
+
throw new Error("Browse failed: " + browseResult.statusCode.toString());
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
let references = [
|
|
39
47
|
...(browseResult.references || [])
|
|
40
48
|
];
|
|
@@ -61,19 +69,34 @@ async function browseNode(session, root) {
|
|
|
61
69
|
|
|
62
70
|
// Monta lista de todos os atributos de todos os nós de uma vez
|
|
63
71
|
const nodeIds = references.map(ref => normalizeNodeId(ref.nodeId));
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
const typeIds = references
|
|
73
|
+
.map(ref => ref.typeDefinition ? normalizeNodeId(ref.typeDefinition) : null)
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
const uniqueTypeIds = [...new Set(typeIds)];
|
|
76
|
+
|
|
77
|
+
const attributesToRead = [
|
|
78
|
+
...nodeIds.flatMap(nodeId => [
|
|
79
|
+
{ nodeId, attributeId: AttributeIds.Description },
|
|
80
|
+
{ nodeId, attributeId: AttributeIds.DataType },
|
|
81
|
+
{ nodeId, attributeId: AttributeIds.Value },
|
|
82
|
+
]),
|
|
83
|
+
...uniqueTypeIds.map(nodeId => ({ nodeId, attributeId: AttributeIds.BrowseName }))
|
|
84
|
+
];
|
|
71
85
|
|
|
72
86
|
// UMA única chamada para todos os nós e atributos
|
|
73
87
|
const dataValues = await session.read(attributesToRead);
|
|
74
88
|
|
|
89
|
+
const typeNamesMap = new Map();
|
|
90
|
+
const typeStartIdx = nodeIds.length * 3;
|
|
91
|
+
uniqueTypeIds.forEach((typeId, index) => {
|
|
92
|
+
const browseNameVal = dataValues[typeStartIdx + index]?.value?.value;
|
|
93
|
+
const name = browseNameVal?.name || typeId;
|
|
94
|
+
typeNamesMap.set(typeId, name);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const cache = new Map();
|
|
75
98
|
// Distribui os resultados por nó (3 atributos por nó)
|
|
76
|
-
result.
|
|
99
|
+
result.children = await Promise.all(references.map(async (reference, i) => {
|
|
77
100
|
const childNodeId = nodeIds[i];
|
|
78
101
|
const nodeClass = resolveNodeClassName(reference.nodeClass);
|
|
79
102
|
const browseName = extractBrowseName(reference.browseName, childNodeId);
|
|
@@ -86,15 +109,33 @@ async function browseNode(session, root) {
|
|
|
86
109
|
|
|
87
110
|
const item = { nodeID: childNodeId, nodeClass, browseName, displayName, description };
|
|
88
111
|
|
|
112
|
+
const typeNodeId = reference.typeDefinition ? normalizeNodeId(reference.typeDefinition) : null;
|
|
113
|
+
if (typeNodeId) {
|
|
114
|
+
const typeName = typeNamesMap.get(typeNodeId) || typeNodeId;
|
|
115
|
+
item.typeDefinition = typeNodeId;
|
|
116
|
+
item.hasTypeDefinition = {
|
|
117
|
+
nodeID: typeNodeId,
|
|
118
|
+
browseName: typeName,
|
|
119
|
+
displayName: typeName
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
89
123
|
if (nodeClass === "Variable") {
|
|
90
124
|
const dataTypeValue = dataValues[i * 3 + 1]?.value?.value;
|
|
91
|
-
const
|
|
125
|
+
const rawValueVariant = dataValues[i * 3 + 2]?.value;
|
|
126
|
+
const rawValue = rawValueVariant?.value;
|
|
92
127
|
|
|
93
128
|
item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
|
|
94
129
|
? (DataType[dataTypeValue.value] || dataTypeValue.toString())
|
|
95
130
|
: (dataTypeValue?.toString() ?? "");
|
|
96
131
|
|
|
132
|
+
if (rawValueVariant?.dataType === DataType.Enumeration) {
|
|
133
|
+
item.dataType = "Enumeration";
|
|
134
|
+
}
|
|
135
|
+
|
|
97
136
|
item.value = rawValue ?? "";
|
|
137
|
+
|
|
138
|
+
await enrichItemResultWithEnumeration(item, session, cache, childNodeId);
|
|
98
139
|
}
|
|
99
140
|
|
|
100
141
|
if (nodeClass === "Method") {
|
|
@@ -358,11 +399,198 @@ function resolveArgumentDataType(dataType) {
|
|
|
358
399
|
return dataType.toString();
|
|
359
400
|
}
|
|
360
401
|
|
|
402
|
+
async function browseRecursiveNode(session, root) {
|
|
403
|
+
const startNodeId = normalizeNodeId(root.nodeID || root.nodeId || ROOT_NODE_ID);
|
|
404
|
+
const rootName = root.name || await readBrowseName(session, startNodeId, "RootFolder");
|
|
405
|
+
|
|
406
|
+
const visited = new Set();
|
|
407
|
+
visited.add(startNodeId);
|
|
408
|
+
|
|
409
|
+
const allItems = [];
|
|
410
|
+
|
|
411
|
+
async function traverse(nodeID) {
|
|
412
|
+
let browseResult;
|
|
413
|
+
try {
|
|
414
|
+
browseResult = await session.browse({
|
|
415
|
+
nodeId: nodeID,
|
|
416
|
+
referenceTypeId: makeNodeId(33, 0), // HierarchicalReferences
|
|
417
|
+
browseDirection: BrowseDirection.Forward,
|
|
418
|
+
includeSubtypes: true,
|
|
419
|
+
resultMask: 63
|
|
420
|
+
});
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (nodeID === startNodeId) {
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (browseResult.statusCode && !browseResult.statusCode.isGood()) {
|
|
429
|
+
if (nodeID === startNodeId) {
|
|
430
|
+
throw new Error("Browse failed: " + browseResult.statusCode.toString());
|
|
431
|
+
}
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let references = [...(browseResult.references || [])];
|
|
436
|
+
|
|
437
|
+
while (browseResult.continuationPoint) {
|
|
438
|
+
try {
|
|
439
|
+
browseResult = await session.browseNext(
|
|
440
|
+
browseResult.continuationPoint,
|
|
441
|
+
false
|
|
442
|
+
);
|
|
443
|
+
references.push(...(browseResult.references || []));
|
|
444
|
+
} catch (err) {
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!references.length) {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const items = [];
|
|
454
|
+
for (const ref of references) {
|
|
455
|
+
const childNodeId = normalizeNodeId(ref.nodeId);
|
|
456
|
+
const nodeClass = resolveNodeClassName(ref.nodeClass);
|
|
457
|
+
const browseName = extractBrowseName(ref.browseName, childNodeId);
|
|
458
|
+
const displayName = extractDisplayName(ref.displayName, browseName);
|
|
459
|
+
const typeNodeId = ref.typeDefinition ? normalizeNodeId(ref.typeDefinition) : null;
|
|
460
|
+
|
|
461
|
+
const item = {
|
|
462
|
+
nodeID: childNodeId,
|
|
463
|
+
nodeClass,
|
|
464
|
+
browseName,
|
|
465
|
+
displayName
|
|
466
|
+
};
|
|
467
|
+
if (typeNodeId) {
|
|
468
|
+
item.typeDefinition = typeNodeId;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
items.push(item);
|
|
472
|
+
allItems.push(item);
|
|
473
|
+
|
|
474
|
+
const expandable = nodeClass === "Object" || nodeClass === "Folder" || nodeClass === "View" || nodeClass === "ObjectType";
|
|
475
|
+
if (expandable && !visited.has(childNodeId)) {
|
|
476
|
+
visited.add(childNodeId);
|
|
477
|
+
item.children = await traverse(childNodeId);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return items;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const browseResult = await traverse(startNodeId);
|
|
485
|
+
|
|
486
|
+
if (allItems.length > 0) {
|
|
487
|
+
const cache = new Map();
|
|
488
|
+
const typeIds = allItems
|
|
489
|
+
.map(item => item.typeDefinition)
|
|
490
|
+
.filter(Boolean);
|
|
491
|
+
const uniqueTypeIds = [...new Set(typeIds)];
|
|
492
|
+
|
|
493
|
+
const typeNamesMap = new Map();
|
|
494
|
+
if (uniqueTypeIds.length > 0) {
|
|
495
|
+
try {
|
|
496
|
+
const typeAttributes = uniqueTypeIds.map(nodeId => ({ nodeId, attributeId: AttributeIds.BrowseName }));
|
|
497
|
+
const typeDataValues = await session.read(typeAttributes);
|
|
498
|
+
uniqueTypeIds.forEach((typeId, index) => {
|
|
499
|
+
const browseNameVal = typeDataValues[index]?.value?.value;
|
|
500
|
+
const name = browseNameVal?.name || typeId;
|
|
501
|
+
typeNamesMap.set(typeId, name);
|
|
502
|
+
});
|
|
503
|
+
} catch (err) {
|
|
504
|
+
// Ignore type names read error, we'll fallback to NodeId below
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const BATCH_SIZE = 100;
|
|
509
|
+
for (let i = 0; i < allItems.length; i += BATCH_SIZE) {
|
|
510
|
+
const chunk = allItems.slice(i, i + BATCH_SIZE);
|
|
511
|
+
const attributesToRead = chunk.flatMap(item => [
|
|
512
|
+
{ nodeId: item.nodeID, attributeId: AttributeIds.Description },
|
|
513
|
+
{ nodeId: item.nodeID, attributeId: AttributeIds.DataType },
|
|
514
|
+
{ nodeId: item.nodeID, attributeId: AttributeIds.Value }
|
|
515
|
+
]);
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
const dataValues = await session.read(attributesToRead);
|
|
519
|
+
await Promise.all(chunk.map(async (item, index) => {
|
|
520
|
+
const descValue = dataValues[index * 3]?.value?.value;
|
|
521
|
+
item.description = typeof descValue === "string"
|
|
522
|
+
? descValue
|
|
523
|
+
: (descValue?.text ?? "");
|
|
524
|
+
|
|
525
|
+
if (item.nodeClass === "Variable") {
|
|
526
|
+
const dataTypeValue = dataValues[index * 3 + 1]?.value?.value;
|
|
527
|
+
const rawValueVariant = dataValues[index * 3 + 2]?.value;
|
|
528
|
+
const rawValue = rawValueVariant?.value;
|
|
529
|
+
|
|
530
|
+
item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
|
|
531
|
+
? (DataType[dataTypeValue.value] || dataTypeValue.toString())
|
|
532
|
+
: (dataTypeValue?.toString() ?? "");
|
|
533
|
+
|
|
534
|
+
if (rawValueVariant?.dataType === DataType.Enumeration) {
|
|
535
|
+
item.dataType = "Enumeration";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
item.value = rawValue ?? "";
|
|
539
|
+
|
|
540
|
+
await enrichItemResultWithEnumeration(item, session, cache, item.nodeID);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (item.typeDefinition) {
|
|
544
|
+
const typeName = typeNamesMap.get(item.typeDefinition) || item.typeDefinition;
|
|
545
|
+
item.hasTypeDefinition = {
|
|
546
|
+
nodeID: item.typeDefinition,
|
|
547
|
+
browseName: typeName,
|
|
548
|
+
displayName: typeName
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}));
|
|
552
|
+
} catch (readError) {
|
|
553
|
+
chunk.forEach(item => {
|
|
554
|
+
item.description = "";
|
|
555
|
+
if (item.nodeClass === "Variable") {
|
|
556
|
+
item.dataType = "";
|
|
557
|
+
item.value = "";
|
|
558
|
+
}
|
|
559
|
+
if (item.typeDefinition) {
|
|
560
|
+
const typeName = typeNamesMap.get(item.typeDefinition) || item.typeDefinition;
|
|
561
|
+
item.hasTypeDefinition = {
|
|
562
|
+
nodeID: item.typeDefinition,
|
|
563
|
+
browseName: typeName,
|
|
564
|
+
displayName: typeName
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const methods = allItems.filter(item => item.nodeClass === "Method");
|
|
572
|
+
for (const method of methods) {
|
|
573
|
+
const definition = await readMethodArguments(session, method.nodeID);
|
|
574
|
+
method.inputArguments = definition.inputArguments;
|
|
575
|
+
method.outputArguments = definition.outputArguments;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
name: rootName,
|
|
581
|
+
nodeID: startNodeId,
|
|
582
|
+
status: "Good",
|
|
583
|
+
children: browseResult
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
361
587
|
const ROOT_NODE_ID = "i=84";
|
|
362
588
|
|
|
363
589
|
module.exports = {
|
|
364
590
|
browseNode,
|
|
591
|
+
browseRecursiveNode,
|
|
365
592
|
normalizeBrowseRoots,
|
|
366
593
|
normalizeNodeId,
|
|
367
594
|
ROOT_NODE_ID
|
|
368
595
|
};
|
|
596
|
+
|
|
@@ -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;
|