@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.
- package/README.md +104 -136
- package/client/lib/opcua-client-browser.js +254 -11
- package/client/lib/opcua-client-method-service.js +1 -1
- package/client/lib/opcua-client-subscription-service.js +0 -2
- package/client/lib/opcua-client-write-service.js +14 -4
- package/client/opcua-client-config.html +118 -1
- package/client/opcua-client-config.js +112 -9
- package/client/opcua-client-help.html +6 -0
- package/client/opcua-client-utils.js +158 -10
- package/client/opcua-client.html +8 -0
- package/client/opcua-client.js +97 -1
- package/client/view/opcua-client.js +106 -14
- package/examples/flows_simple_opc.json +1 -1
- package/package.json +2 -2
- package/server/lib/opcua-address-space-alarm.js +95 -32
- package/server/lib/opcua-address-space-builder.js +717 -59
- package/server/lib/opcua-config.js +110 -35
- package/server/lib/opcua-server-events-child.js +31 -5
- package/server/lib/opcua-server-runtime-child.js +424 -27
- package/server/lib/opcua-server-runtime.js +52 -5
- package/server/lib/opcua-server-status-child.js +46 -15
- 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 +93 -1
- package/server/opcua-server-io.js +153 -29
- package/server/opcua-server-registry.js +8 -2
- package/server/opcua-server.css +64 -0
- package/server/opcua-server.html +168 -44
- package/server/opcua-server.js +115 -5
- package/server/view/opcua-server.css +100 -6
- package/server/view/opcua-server.js +746 -48
package/client/opcua-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
: "
|
|
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();
|