@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
@@ -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
 
@@ -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
- : "Falha ao navegar no servidor OPC UA.";
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 nao encontrado para o item selecionado.", "warning");
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 copiado.", "success");
692
+ RED.notify("NodeID copied.", "success");
692
693
  }).catch(function () {
693
- RED.notify("Falha ao copiar NodeID.", "error");
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 copiado.", "success");
708
+ RED.notify("NodeID copied.", "success");
708
709
  } catch (error) {
709
- RED.notify("Falha ao copiar NodeID.", "error");
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
- item.browse = Array.isArray(payload.browse) ? payload.browse : [];
766
- saveBrowseSession();
767
- renderBrowseTree();
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
- : "Falha ao expandir o node.";
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
- setBrowseSelectedPath($(this).attr("data-path"));
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();