@vitormnm/node-red-simple-opcua 1.6.3 → 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.
Files changed (42) hide show
  1. package/README.md +89 -136
  2. package/client/lib/opcua-client-browser.js +238 -10
  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/opcua-client-config.html +118 -1
  6. package/client/opcua-client-config.js +74 -8
  7. package/client/opcua-client-help.html +6 -0
  8. package/client/opcua-client-utils.js +34 -10
  9. package/client/opcua-client.html +7 -0
  10. package/client/opcua-client.js +97 -1
  11. package/examples/flows_simple_opc.json +1 -1
  12. package/package.json +1 -1
  13. package/server/lib/opcua-address-space-alarm.js +11 -5
  14. package/server/lib/opcua-address-space-builder.js +65 -15
  15. package/server/lib/opcua-config.js +81 -23
  16. package/server/lib/opcua-server-events-child.js +1 -1
  17. package/server/lib/opcua-server-runtime-child.js +284 -19
  18. package/server/lib/opcua-server-runtime.js +49 -5
  19. package/server/lib/opcua-server-status-child.js +14 -14
  20. package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
  21. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
  22. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  23. package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
  24. package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
  25. package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  26. package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
  27. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
  28. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  29. package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
  30. package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
  31. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
  32. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
  33. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
  34. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  35. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
  36. package/server/opcua-server-io.html +76 -0
  37. package/server/opcua-server-io.js +130 -27
  38. package/server/opcua-server.css +52 -0
  39. package/server/opcua-server.html +166 -44
  40. package/server/opcua-server.js +115 -5
  41. package/server/view/opcua-server.css +89 -6
  42. package/server/view/opcua-server.js +523 -42
@@ -3,16 +3,28 @@
3
3
  function toggleCredentials() {
4
4
  var authType = $("#node-config-input-authType").val();
5
5
  $(".opcua-client-auth-row").toggle(authType === "username");
6
+ if (authType === "anonymous") {
7
+ $("#node-config-input-username").val("");
8
+ $("#node-config-input-password").val("");
9
+ }
6
10
  }
7
11
 
8
12
  RED.nodes.registerType("opcua-client-config", {
9
13
  category: "config",
10
14
  defaults: {
11
15
  name: { value: "" },
16
+ sessionName: { value: "ClientSession1" },
12
17
  endpoint: { value: "opc.tcp://localhost:4840", required: true },
13
18
  securityPolicy: { value: "None", required: true },
14
19
  securityMode: { value: "None", required: true },
15
- authType: { value: "anonymous", required: true }
20
+ authType: { value: "anonymous", required: true },
21
+ initialDelay: { value: 1000, validate: RED.validators.number() },
22
+ maxDelay: { value: 10000, validate: RED.validators.number() },
23
+ maxRetry: { value: -1, validate: RED.validators.number() },
24
+ requestedSessionTimeout: { value: 300000, validate: RED.validators.number() },
25
+ keepSessionAlive: { value: true },
26
+ autoReconnect: { value: true },
27
+ endpointMustExist: { value: false }
16
28
  },
17
29
  credentials: {
18
30
  username: { type: "text" },
@@ -22,22 +34,87 @@
22
34
  return this.name || this.endpoint || "opcua-client-config";
23
35
  },
24
36
  oneditprepare: function () {
37
+ if (this.keepSessionAlive !== false) {
38
+ $("#node-config-input-keepSessionAlive").prop("checked", true);
39
+ }
40
+ if (this.autoReconnect !== false) {
41
+ $("#node-config-input-autoReconnect").prop("checked", true);
42
+ }
43
+ if (this.endpointMustExist === true) {
44
+ $("#node-config-input-endpointMustExist").prop("checked", true);
45
+ }
46
+ if (this.maxRetry === undefined || Number(this.maxRetry) === 10) {
47
+ $("#node-config-input-maxRetry").val(-1);
48
+ }
49
+
25
50
  $("#node-config-input-authType").on("change", toggleCredentials);
26
51
  toggleCredentials();
52
+
53
+ $("#node-config-input-securityPolicy").on("change", function () {
54
+ var policy = $(this).val();
55
+ var modeSelect = $("#node-config-input-securityMode");
56
+ var mode = modeSelect.val();
57
+ if (policy === "None") {
58
+ if (mode !== "None") {
59
+ modeSelect.val("None");
60
+ }
61
+ } else {
62
+ if (mode === "None") {
63
+ modeSelect.val("SignAndEncrypt");
64
+ }
65
+ }
66
+ });
67
+
68
+ $("#node-config-input-securityMode").on("change", function () {
69
+ var mode = $(this).val();
70
+ var policySelect = $("#node-config-input-securityPolicy");
71
+ var policy = policySelect.val();
72
+ if (mode === "None") {
73
+ if (policy !== "None") {
74
+ policySelect.val("None");
75
+ }
76
+ } else {
77
+ if (policy === "None") {
78
+ policySelect.val("Basic256Sha256");
79
+ }
80
+ }
81
+ });
82
+
83
+ function toggleReconnect() {
84
+ var autoReconnect = $("#node-config-input-autoReconnect").prop("checked");
85
+ $(".opcua-client-reconnect-row").toggle(autoReconnect);
86
+ }
87
+ $("#node-config-input-autoReconnect").on("change", toggleReconnect);
88
+ toggleReconnect();
27
89
  }
28
90
  });
29
91
  })();
30
92
  </script>
31
93
 
32
94
  <script type="text/html" data-template-name="opcua-client-config">
95
+ <!-- Connection Section -->
96
+ <div class="form-row" style="margin-top: 10px; margin-bottom: 10px;">
97
+ <span style="font-weight: bold;"><i class="fa fa-plug"></i> Connection Settings</span>
98
+ <hr style="margin-top: 5px; margin-bottom: 10px; border: 0; border-top: 1px solid #ccc;"/>
99
+ </div>
33
100
  <div class="form-row">
34
101
  <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
35
102
  <input type="text" id="node-config-input-name" placeholder="OPC UA Client">
36
103
  </div>
104
+ <div class="form-row">
105
+ <label for="node-config-input-sessionName"><i class="fa fa-info-circle"></i> Session Name</label>
106
+ <input type="text" id="node-config-input-sessionName" placeholder="ClientSession1">
107
+ </div>
37
108
  <div class="form-row">
38
109
  <label for="node-config-input-endpoint"><i class="fa fa-plug"></i> Endpoint</label>
39
110
  <input type="text" id="node-config-input-endpoint" placeholder="opc.tcp://localhost:4840">
40
111
  </div>
112
+
113
+ <!-- Security Section -->
114
+ <div class="form-row" style="margin-top: 20px; margin-bottom: 10px;">
115
+ <span style="font-weight: bold;"><i class="fa fa-shield"></i> Security & Authentication</span>
116
+ <hr style="margin-top: 5px; margin-bottom: 10px; border: 0; border-top: 1px solid #ccc;"/>
117
+ </div>
41
118
  <div class="form-row">
42
119
  <label for="node-config-input-securityPolicy"><i class="fa fa-lock"></i> Security Policy</label>
43
120
  <select id="node-config-input-securityPolicy">
@@ -72,6 +149,46 @@
72
149
  <label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
73
150
  <input type="password" id="node-config-input-password">
74
151
  </div>
152
+
153
+ <!-- Client Settings Section -->
154
+ <div class="form-row" style="margin-top: 20px; margin-bottom: 10px;">
155
+ <span style="font-weight: bold;"><i class="fa fa-cog"></i> Client Settings</span>
156
+ <hr style="margin-top: 5px; margin-bottom: 10px; border: 0; border-top: 1px solid #ccc;"/>
157
+ </div>
158
+ <div class="form-row">
159
+ <label for="node-config-input-requestedSessionTimeout"><i class="fa fa-clock-o"></i> Session Timeout (ms)</label>
160
+ <input type="number" id="node-config-input-requestedSessionTimeout" placeholder="300000">
161
+ </div>
162
+ <div class="form-row" style="display: flex; align-items: center;">
163
+ <label for="node-config-input-keepSessionAlive" style="width: auto; margin-right: 10px;"><i class="fa fa-heartbeat"></i> Keep Session Alive</label>
164
+ <input type="checkbox" id="node-config-input-keepSessionAlive" style="width: auto; margin: 0;">
165
+ </div>
166
+ <div class="form-row" style="display: flex; align-items: center; margin-bottom: 5px;">
167
+ <label for="node-config-input-endpointMustExist" style="width: auto; margin-right: 10px;"><i class="fa fa-exclamation-circle"></i> Endpoint Must Exist</label>
168
+ <input type="checkbox" id="node-config-input-endpointMustExist" style="width: auto; margin: 0;">
169
+ </div>
170
+ <div style="font-size: 0.85em; color: #666; margin-left: 20px; margin-top: -5px; margin-bottom: 10px;">
171
+ Verify that the server endpoint matches the requested security policy and mode before connecting.
172
+ </div>
173
+ <div class="form-row" style="display: flex; align-items: center;">
174
+ <label for="node-config-input-autoReconnect" style="width: auto; margin-right: 10px;"><i class="fa fa-refresh"></i> Automatic Reconnect</label>
175
+ <input type="checkbox" id="node-config-input-autoReconnect" style="width: auto; margin: 0;">
176
+ </div>
177
+ <div class="form-row opcua-client-reconnect-row">
178
+ <label for="node-config-input-initialDelay"><i class="fa fa-clock-o"></i> Initial Delay (ms)</label>
179
+ <input type="number" id="node-config-input-initialDelay" placeholder="1000">
180
+ </div>
181
+ <div class="form-row opcua-client-reconnect-row">
182
+ <label for="node-config-input-maxDelay"><i class="fa fa-clock-o"></i> Max Delay (ms)</label>
183
+ <input type="number" id="node-config-input-maxDelay" placeholder="10000">
184
+ </div>
185
+ <div class="form-row opcua-client-reconnect-row">
186
+ <label for="node-config-input-maxRetry"><i class="fa fa-repeat"></i> Max Retries</label>
187
+ <input type="number" id="node-config-input-maxRetry" placeholder="-1">
188
+ <div style="font-size: 0.85em; color: #666; margin-left: 110px; margin-top: 2px;">
189
+ -1: Retry indefinitely. 0: Do not attempt to reconnect.
190
+ </div>
191
+ </div>
75
192
  </script>
76
193
 
77
194
  <script type="text/html" data-help-name="opcua-client-config">
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
 
3
+ const path = require("path");
4
+ const { OPCUACertificateManager, UserTokenType } = require("node-opcua");
5
+
3
6
  const {
4
7
  OPCUAClient,
5
8
  getMethodArgumentDefinition,
@@ -23,10 +26,18 @@ module.exports = function (RED) {
23
26
 
24
27
 
25
28
  node.name = (config.name || "").trim();
29
+ node.sessionName = (config.sessionName || "ClientSession1").trim();
26
30
  node.endpoint = (config.endpoint || "").trim();
27
31
  node.securityPolicy = config.securityPolicy || "None";
28
32
  node.securityMode = config.securityMode || "None";
29
33
  node.authType = config.authType || "anonymous";
34
+ node.initialDelay = config.initialDelay !== undefined ? Number(config.initialDelay) : 1000;
35
+ node.maxDelay = config.maxDelay !== undefined ? Number(config.maxDelay) : 10000;
36
+ node.maxRetry = (config.maxRetry !== undefined && Number(config.maxRetry) !== 10) ? Number(config.maxRetry) : -1;
37
+ node.requestedSessionTimeout = config.requestedSessionTimeout !== undefined ? Number(config.requestedSessionTimeout) : 300000;
38
+ node.keepSessionAlive = config.keepSessionAlive !== false;
39
+ node.autoReconnect = config.autoReconnect !== false;
40
+ node.endpointMustExist = config.endpointMustExist === true;
30
41
  node.client = null;
31
42
  node.session = null;
32
43
  node.connectPromise = null;
@@ -52,23 +63,72 @@ module.exports = function (RED) {
52
63
  }
53
64
 
54
65
  this.connectPromise = (async () => {
66
+ const userDir = (RED.settings && RED.settings.userDir) || path.join(require('os').homedir(), ".node-red");
67
+ let flowFile = (RED.settings && RED.settings.flowFile) || "flows.json";
68
+ if (typeof flowFile !== "string") {
69
+ flowFile = "flows.json";
70
+ }
71
+ const flowFileFolder = path.isAbsolute(flowFile) ? path.dirname(flowFile) : path.join(userDir, path.dirname(flowFile));
72
+ const clientName = (this.name || "").trim() || this.sessionName || "default";
73
+ const safeClientName = clientName
74
+ .replace(/[\\/:\*\?"<>|]/g, "_")
75
+ .replace(/^\.+$/, "");
76
+ const clientCertificateFolder = path.join(flowFileFolder, "simple_opcua", "client", safeClientName);
77
+
78
+ // Ensure directories exist immediately
79
+ const fs = require("fs");
80
+ try {
81
+ const trustedDir = path.join(clientCertificateFolder, "trusted", "certs");
82
+ const rejectedDir = path.join(clientCertificateFolder, "rejected");
83
+ if (!fs.existsSync(trustedDir)) {
84
+ fs.mkdirSync(trustedDir, { recursive: true });
85
+ }
86
+ if (!fs.existsSync(rejectedDir)) {
87
+ fs.mkdirSync(rejectedDir, { recursive: true });
88
+ }
89
+ } catch (e) {
90
+ // Ignore directory creation errors
91
+ }
92
+
93
+ const clientCertificateManager = new OPCUACertificateManager({
94
+ rootFolder: clientCertificateFolder,
95
+ automaticallyAcceptUnknownCertificate: true
96
+ });
97
+ await clientCertificateManager.initialize();
98
+
55
99
  const client = OPCUAClient.create({
56
- endpointMustExist: false,
57
- keepSessionAlive: true,
100
+ endpointMustExist: this.endpointMustExist,
101
+ keepSessionAlive: this.keepSessionAlive,
58
102
  securityMode: resolveSecurityMode(this.securityMode),
59
- securityPolicy: resolveSecurityPolicy(this.securityPolicy)
103
+ securityPolicy: resolveSecurityPolicy(this.securityPolicy),
104
+ clientName: this.sessionName || "ClientSession",
105
+ clientCertificateManager: clientCertificateManager,
106
+ requestedSessionTimeout: this.requestedSessionTimeout,
107
+ connectionStrategy: {
108
+ maxRetry: this.autoReconnect ? this.maxRetry : 0,
109
+ initialDelay: this.initialDelay,
110
+ maxDelay: this.maxDelay
111
+ }
60
112
  });
61
113
 
114
+ client._nextSessionName = () => {
115
+ return this.sessionName || "ClientSession1";
116
+ };
117
+
62
118
  try {
63
119
  await client.connect(this.endpoint);
64
120
 
65
121
  const credentials = this.credentials || {};
66
- const session = this.authType === "username"
67
- ? await client.createSession({
122
+ let userIdentity = { type: UserTokenType.Anonymous };
123
+
124
+ if (this.authType === "username") {
125
+ userIdentity = {
126
+ type: UserTokenType.UserName,
68
127
  userName: credentials.username || "",
69
128
  password: credentials.password || ""
70
- })
71
- : await client.createSession();
129
+ };
130
+ }
131
+ const session = await client.createSession(userIdentity);
72
132
 
73
133
  session.on("session_closed", () => {
74
134
  this.session = null;
@@ -158,7 +218,13 @@ module.exports = function (RED) {
158
218
 
159
219
  async function browseForEditor(configNode, nodeId) {
160
220
  const session = await configNode.getSession();
161
- return browseNode(session, {
221
+ const result = await browseNode(session, {
162
222
  nodeID: nodeId || ROOT_NODE_ID
163
223
  });
224
+ if (result && result.children) {
225
+ result.browse = result.children;
226
+ delete result.children;
227
+ }
228
+ return result;
164
229
  }
230
+
@@ -5,6 +5,7 @@
5
5
  <p><b>Read</b>: reads one or more variable values.</p>
6
6
  <p><b>Write</b>: writes one or more variable values.</p>
7
7
  <p><b>Browse</b>: browses one or more OPC UA nodes and returns their children.</p>
8
+ <p><b>Browse Recursive</b>: perform a recursive search across all nodes starting from the specified node.</p>
8
9
  <p><b>Method</b>: calls one or more OPC UA methods.</p>
9
10
  <p><b>Subscription</b>: subscribes to one or more variable values and emits one message per change.</p>
10
11
 
@@ -33,6 +34,11 @@
33
34
  <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node browses that configured node.</p>
34
35
  <p>If <code>msg.payload = []</code>, the node browses the OPC UA <code>RootFolder</code>.</p>
35
36
 
37
+ <h3>Browse Recursive</h3>
38
+ <p>Perform a recursive search across all nodes starting from the specified <code>nodeId</code>. It will return the value (if it is a variable), display name, description, and all other item information.</p>
39
+ <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node browses recursively starting from that configured node.</p>
40
+ <p>If <code>msg.payload = []</code>, the node browses recursively starting from the OPC UA <code>RootFolder</code>.</p>
41
+
36
42
  <h3>Method</h3>
37
43
  <p><code>nodeID</code> and <code>objectId</code> are accepted explicitly. For backward compatibility, <code>nodeID</code> is also accepted as the method id.</p>
38
44
  <p><b>Input</b> <code>msg.payload</code>:</p>
@@ -469,7 +469,15 @@ async function getMethodArgumentDefinition(session, methodNodeId, cache) {
469
469
  }
470
470
 
471
471
  async function enrichItemResultWithEnumeration(result, session, cache, nodeId) {
472
- if (result.type !== "Int32" && result.type !== "Enumeration") {
472
+ const type = result.type || result.dataType;
473
+ if (!type) {
474
+ return result;
475
+ }
476
+
477
+ const isStandardEnum = type === "Int32" || type === "Enumeration";
478
+ const isCustomNodeId = typeof type === "string" && (type.includes("i=") || type.includes("ns="));
479
+
480
+ if (!isStandardEnum && !isCustomNodeId) {
473
481
  return result;
474
482
  }
475
483
 
@@ -481,15 +489,29 @@ async function enrichItemResultWithEnumeration(result, session, cache, nodeId) {
481
489
  const cacheKeyType = "dt:" + nodeId;
482
490
  let dtNodeId = cache ? cache.get(cacheKeyType) : undefined;
483
491
  if (dtNodeId === undefined) {
484
- const dv = await session.read({
485
- nodeId: nodeId,
486
- attributeId: AttributeIds.DataType
487
- });
488
- if (dv.statusCode.isGood()) {
489
- dtNodeId = dv.value.value;
490
- if (cache) cache.set(cacheKeyType, dtNodeId);
491
- } else {
492
- if (cache) cache.set(cacheKeyType, null);
492
+ const isNodeIdLike = result.dataType && (
493
+ typeof result.dataType !== "string" ||
494
+ result.dataType.includes("i=") ||
495
+ result.dataType.includes("ns=")
496
+ );
497
+ if (isNodeIdLike) {
498
+ try {
499
+ dtNodeId = coerceNodeId(result.dataType);
500
+ } catch (e) {
501
+ dtNodeId = undefined;
502
+ }
503
+ }
504
+ if (dtNodeId === undefined) {
505
+ const dv = await session.read({
506
+ nodeId: nodeId,
507
+ attributeId: AttributeIds.DataType
508
+ });
509
+ if (dv.statusCode.isGood()) {
510
+ dtNodeId = dv.value.value;
511
+ if (cache) cache.set(cacheKeyType, dtNodeId);
512
+ } else {
513
+ if (cache) cache.set(cacheKeyType, null);
514
+ }
493
515
  }
494
516
  }
495
517
 
@@ -549,6 +571,8 @@ async function enrichItemResultWithEnumeration(result, session, cache, nodeId) {
549
571
 
550
572
  if (enumStrings && enumStrings[result.value] !== undefined) {
551
573
  result.valueEnumeration = enumStrings[result.value];
574
+ if (result.type) result.type = "Enumeration";
575
+ if (result.dataType) result.dataType = "Enumeration";
552
576
  }
553
577
  } catch (e) {
554
578
  console.error("Error in enrichItemResultWithEnumeration:", e);
@@ -17,6 +17,7 @@
17
17
  <option value="read">Read</option>
18
18
  <option value="write">Write</option>
19
19
  <option value="browse">Browse</option>
20
+ <option value="browseRecursive">Browse Recursive</option>
20
21
  <option value="method">Method</option>
21
22
  <option value="getSubscriptionId">getSubscriptionId</option>
22
23
  <option value="subscription">Subscription</option>
@@ -85,6 +86,7 @@
85
86
  <p><b>Read</b>: reads one or more variable values.</p>
86
87
  <p><b>Write</b>: writes one or more variable values.</p>
87
88
  <p><b>Browse</b>: browses one or more OPC UA nodes and returns their children.</p>
89
+ <p><b>Browse Recursive</b>: perform a recursive search across all nodes starting from the specified node.</p>
88
90
  <p><b>Method</b>: calls one or more OPC UA methods.</p>
89
91
  <p><b>Subscription</b>: subscribes to one or more variable values and emits one message per change.</p>
90
92
 
@@ -113,6 +115,11 @@
113
115
  <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node browses that configured node.</p>
114
116
  <p>If <code>msg.payload = []</code>, the node browses the OPC UA <code>RootFolder</code>.</p>
115
117
 
118
+ <h3>Browse Recursive</h3>
119
+ <p>Perform a recursive search across all nodes starting from the specified <code>nodeId</code>. It will return the value (if it is a variable), display name, description, and all other item information.</p>
120
+ <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node browses recursively starting from that configured node.</p>
121
+ <p>If <code>msg.payload = []</code>, the node browses recursively starting from the OPC UA <code>RootFolder</code>.</p>
122
+
116
123
  <h3>Method</h3>
117
124
  <p><code>nodeID</code> and <code>objectId</code> are accepted explicitly. For backward compatibility, <code>nodeID</code> is also accepted as the method id.</p>
118
125
  <p><b>Input</b> <code>msg.payload</code>:</p>
@@ -15,6 +15,7 @@ const {
15
15
 
16
16
  const {
17
17
  browseNode: browseNodeWithSession,
18
+ browseRecursiveNode,
18
19
  ROOT_NODE_ID
19
20
  } = require("./lib/opcua-client-browser");
20
21
 
@@ -92,6 +93,9 @@ module.exports = function (RED) {
92
93
  } else if (node.mode === "browse") {
93
94
  payload = await executeBrowse(node, msg, session);
94
95
  node.status({ fill: "green", shape: "dot", text: "browsed " + payload.length + " nodes" });
96
+ } else if (node.mode === "browseRecursive") {
97
+ payload = await executeBrowseRecursive(node, msg, session);
98
+ node.status({ fill: "green", shape: "dot", text: "browsed recursive " + payload.length + " nodes" });
95
99
  } else if (node.mode === "method") {
96
100
  //payload = await executeMethod(node, msg, session);
97
101
 
@@ -109,7 +113,88 @@ module.exports = function (RED) {
109
113
  done();
110
114
  } catch (error) {
111
115
  node.status({ fill: "red", shape: "ring", text: node.mode + " failed" });
112
- done(error);
116
+
117
+ let errorPayload;
118
+ try {
119
+ if (node.mode === "read") {
120
+ const items = itemsResolver.ensureClientItems(node, msg, "OPC UA read");
121
+ errorPayload = items.map(item => ({
122
+ name: resolveName(item, resolveNodeId(item)),
123
+ nodeID: resolveNodeId(item),
124
+ value: null,
125
+ type: null,
126
+ status: error.message || String(error),
127
+ sourceTimestamp: null,
128
+ serverTimestamp: null
129
+ }));
130
+ } else if (node.mode === "write") {
131
+ const items = itemsResolver.ensureWriteItems(node, msg);
132
+ errorPayload = items.map(item => ({
133
+ name: item.name,
134
+ nodeID: item.nodeID,
135
+ value: item.value,
136
+ type: item.type,
137
+ status: error.message || String(error)
138
+ }));
139
+ } else if (node.mode === "browse" || node.mode === "browseRecursive") {
140
+ const roots = normalizeBrowseRoots(node, msg ? msg.payload : undefined);
141
+ errorPayload = roots.map(root => ({
142
+ name: root.name,
143
+ nodeID: root.nodeID,
144
+ status: error.message || String(error),
145
+ results: [],
146
+ children: []
147
+ }));
148
+ } else if (node.mode === "method") {
149
+ const items = itemsResolver.ensureMethodItems(node, msg);
150
+ errorPayload = items.map(item => ({
151
+ name: item.name,
152
+ nodeID: item.nodeID,
153
+ status: error.message || String(error),
154
+ value: null
155
+ }));
156
+ } else if (node.mode === "subscription" || node.mode === "events") {
157
+ const items = itemsResolver.ensureClientItems(node, msg, "OPC UA subscription");
158
+ errorPayload = items.map(item => ({
159
+ name: resolveName(item, resolveNodeId(item)),
160
+ nodeID: resolveNodeId(item),
161
+ value: null,
162
+ type: null,
163
+ status: error.message || String(error),
164
+ sourceTimestamp: null,
165
+ serverTimestamp: null
166
+ }));
167
+ } else {
168
+ errorPayload = {
169
+ status: "error",
170
+ error: error.message || String(error)
171
+ };
172
+ }
173
+ } catch (payloadError) {
174
+ errorPayload = {
175
+ status: "error",
176
+ error: error.message || String(error)
177
+ };
178
+ }
179
+
180
+ const safeClone = {};
181
+ for (const key in msg) {
182
+ if (msg.hasOwnProperty(key) && key !== "req" && key !== "res" && key !== "payload") {
183
+ safeClone[key] = msg[key];
184
+ }
185
+ }
186
+ safeClone.payload = msg.payload;
187
+
188
+ if (errorPayload && (Array.isArray(errorPayload) || typeof errorPayload === "object")) {
189
+ errorPayload.msg = safeClone;
190
+ }
191
+
192
+ msg.payload = errorPayload;
193
+
194
+ node.error(error.message || String(error), msg);
195
+ if (done) {
196
+ done();
197
+ }
113
198
  }
114
199
  });
115
200
 
@@ -134,6 +219,17 @@ module.exports = function (RED) {
134
219
  return payload;
135
220
  }
136
221
 
222
+ async function executeBrowseRecursive(node, msg, session) {
223
+ const roots = normalizeBrowseRoots(node, msg ? msg.payload : undefined);
224
+ const payload = [];
225
+
226
+ for (const root of roots) {
227
+ payload.push(await browseRecursiveNode(session, root));
228
+ }
229
+
230
+ return payload;
231
+ }
232
+
137
233
 
138
234
 
139
235