@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
@@ -3,6 +3,7 @@
3
3
  var editorState = { objects: [], folders: [], objectsTypes: [], enumerations: [], nameSpaces: [] };
4
4
  var expansionState = {};
5
5
  var selectedPath = "";
6
+ var draggedPath = null;
6
7
  var pendingCreate = null;
7
8
  var pendingPasswordHashes = 0;
8
9
  var authGroups = [];
@@ -12,13 +13,136 @@
12
13
  var isSyncing = false;
13
14
  var DEFAULT_NAMESPACE_ID = 2;
14
15
 
16
+ var selectedCertFolder = "rejected";
17
+ var selectedCertName = "";
18
+ var certificatesData = { trusted: [], rejected: [] };
19
+
15
20
  function syncModalBodyClass() {
16
- $("body").toggleClass("opcua-tree-modal-open", $("#node-input-tree-modal").is(":visible") || $("#node-input-auth-modal").is(":visible"));
21
+ $("body").toggleClass("opcua-tree-modal-open",
22
+ $("#node-input-tree-modal").is(":visible") ||
23
+ $("#node-input-auth-modal").is(":visible") ||
24
+ $("#node-input-cert-modal").is(":visible") ||
25
+ $("#node-input-settings-modal").is(":visible")
26
+ );
17
27
  }
18
28
  function openTreeModal() { $("#node-input-tree-modal").show(); syncModalBodyClass(); }
19
29
  function closeTreeModal() { $("#node-input-tree-modal").hide(); syncModalBodyClass(); }
20
30
  function openAuthModal() { $("#node-input-auth-modal").show(); syncModalBodyClass(); renderAuthEditor(); }
21
31
  function closeAuthModal() { $("#node-input-auth-modal").hide(); syncModalBodyClass(); }
32
+ function openCertModal() { $("#node-input-cert-modal").show(); syncModalBodyClass(); initCertEditor(); }
33
+ function closeCertModal() { $("#node-input-cert-modal").hide(); syncModalBodyClass(); }
34
+ function openSettingsModal() { $("#node-input-settings-modal").show(); syncModalBodyClass(); }
35
+ function closeSettingsModal() { $("#node-input-settings-modal").hide(); syncModalBodyClass(); }
36
+
37
+ function initCertEditor() {
38
+ selectedCertFolder = "rejected";
39
+ selectedCertName = "";
40
+ $("#opcua-cert-details").hide();
41
+ $("#opcua-cert-folders .opcua-cert-item").removeClass("is-selected");
42
+ $('#opcua-cert-folders .opcua-cert-item[data-folder="rejected"]').addClass("is-selected");
43
+ fetchCertificates();
44
+ }
45
+
46
+ function fetchCertificates() {
47
+ var filesContainer = $("#opcua-cert-files");
48
+ filesContainer.empty().append('<div class="opcua-tree-empty"><i class="fa fa-spinner fa-spin"></i> Loading certificates...</div>');
49
+
50
+ var currentServerName = $("#node-input-serverName").val() || "";
51
+
52
+ $.ajax({
53
+ url: "opc-ua-server/certificates",
54
+ type: "GET",
55
+ data: {
56
+ serverName: currentServerName
57
+ },
58
+ dataType: "json",
59
+ success: function (data) {
60
+ certificatesData = data || { trusted: [], rejected: [] };
61
+ renderCertificatesList();
62
+ },
63
+ error: function (xhr, textStatus, errorThrown) {
64
+ filesContainer.empty().append('<div class="opcua-tree-empty" style="color: #d9534f;"><i class="fa fa-exclamation-triangle"></i> Failed to load certificates.</div>');
65
+ }
66
+ });
67
+ }
68
+
69
+ function renderCertificatesList() {
70
+ var filesContainer = $("#opcua-cert-files");
71
+ filesContainer.empty();
72
+
73
+ var list = certificatesData[selectedCertFolder] || [];
74
+ if (list.length === 0) {
75
+ filesContainer.append('<div class="opcua-tree-empty">No certificates found in this folder.</div>');
76
+ $("#opcua-cert-details").hide();
77
+ return;
78
+ }
79
+
80
+ list.forEach(function (filename) {
81
+ var item = $('<div class="opcua-cert-item"></div>');
82
+ item.attr("data-name", filename);
83
+ item.append('<i class="fa fa-certificate"></i> ' + escapeHtml(filename));
84
+ if (filename === selectedCertName) {
85
+ item.addClass("is-selected");
86
+ }
87
+ filesContainer.append(item);
88
+ });
89
+
90
+ // If previously selected cert is not in the list, hide details
91
+ if (selectedCertName && list.indexOf(selectedCertName) === -1) {
92
+ selectedCertName = "";
93
+ $("#opcua-cert-details").hide();
94
+ } else if (selectedCertName) {
95
+ showCertificateDetails();
96
+ }
97
+ }
98
+
99
+ function showCertificateDetails() {
100
+ $("#opcua-selected-cert-name").text(selectedCertName);
101
+ var targetSelect = $("#opcua-cert-target-folder");
102
+ targetSelect.empty();
103
+
104
+ if (selectedCertFolder === "rejected") {
105
+ targetSelect.append('<option value="trusted">Trusted Certificates</option>');
106
+ } else {
107
+ targetSelect.append('<option value="rejected">Rejected Certificates</option>');
108
+ }
109
+
110
+ $("#opcua-cert-details").show();
111
+ }
112
+
113
+ function moveCertificate() {
114
+ if (!selectedCertName) return;
115
+ var targetFolder = $("#opcua-cert-target-folder").val();
116
+ if (!targetFolder) return;
117
+
118
+ var currentServerName = $("#node-input-serverName").val() || "";
119
+ var moveBtn = $("#opcua-cert-move-btn");
120
+ moveBtn.addClass("disabled").append(' <i class="fa fa-spinner fa-spin"></i>');
121
+
122
+ $.ajax({
123
+ url: "opc-ua-server/certificates/move",
124
+ type: "POST",
125
+ contentType: "application/json",
126
+ data: JSON.stringify({
127
+ serverName: currentServerName,
128
+ filename: selectedCertName,
129
+ fromFolder: selectedCertFolder,
130
+ toFolder: targetFolder
131
+ }),
132
+ success: function (res) {
133
+ moveBtn.removeClass("disabled").find("i.fa-spin").remove();
134
+ selectedCertName = "";
135
+ $("#opcua-cert-details").hide();
136
+ RED.notify("Certificate moved successfully.", "success");
137
+ fetchCertificates();
138
+ },
139
+ error: function (xhr, textStatus, errorThrown) {
140
+ moveBtn.removeClass("disabled").find("i.fa-spin").remove();
141
+ var errMsg = xhr.responseJSON && xhr.responseJSON.error ? xhr.responseJSON.error : "Failed to move certificate.";
142
+ RED.notify(errMsg, "error");
143
+ }
144
+ });
145
+ }
22
146
 
23
147
  function parseTree(rawValue, strict) {
24
148
  if (!rawValue) return { objects: [], folders: [], objectsTypes: [], enumerations: [], nameSpaces: [] };
@@ -174,19 +298,20 @@
174
298
  variableNodeId: alarm.variableNodeId ? String(alarm.variableNodeId) : "",
175
299
  type: type,
176
300
  enabled: alarm.enabled !== undefined ? !!alarm.enabled : true,
301
+ sendValue: alarm.sendValue !== undefined ? !!alarm.sendValue : true,
177
302
  severity: alarm.severity !== undefined ? alarm.severity : 500,
178
303
  description: alarm.description ? String(alarm.description) : "",
179
304
  displayName: alarm.displayName ? String(alarm.displayName) : "",
180
305
  nodeId: alarm.nodeId ? String(alarm.nodeId) : "",
181
306
  namespaceId: normalizeNamespaceId(alarm.namespaceId),
182
307
  accessPermission: normalizeAccessPermissionValues(alarm.accessPermission || alarm.accessPermissions),
183
- highHighLimit: alarm.highHighLimit !== undefined ? alarm.highHighLimit : 100,
308
+ highHighLimit: alarm.highHighLimit !== undefined ? alarm.highHighLimit : 90,
184
309
  highHighMessage: alarm.highHighMessage ? String(alarm.highHighMessage) : "High High alarm",
185
310
  highLimit: alarm.highLimit !== undefined ? alarm.highLimit : 80,
186
311
  highMessage: alarm.highMessage ? String(alarm.highMessage) : "High alarm",
187
312
  lowLimit: alarm.lowLimit !== undefined ? alarm.lowLimit : 20,
188
313
  lowMessage: alarm.lowMessage ? String(alarm.lowMessage) : "Low alarm",
189
- lowLowLimit: alarm.lowLowLimit !== undefined ? alarm.lowLowLimit : 0,
314
+ lowLowLimit: alarm.lowLowLimit !== undefined ? alarm.lowLowLimit : 10,
190
315
  lowLowMessage: alarm.lowLowMessage ? String(alarm.lowLowMessage) : "Low Low alarm",
191
316
  normalStateValue: alarm.normalStateValue !== undefined ? alarm.normalStateValue : 0,
192
317
  digitalMessage: alarm.digitalMessage ? String(alarm.digitalMessage) : "Digital alarm"
@@ -319,7 +444,73 @@
319
444
 
320
445
  function normalizeSearchTerm(value) { return String(value || "").trim().toLowerCase(); }
321
446
  function isExpanded(path, defaultValue) { if (expansionState[path] === undefined) expansionState[path] = !!defaultValue; return expansionState[path]; }
447
+ function isAncestorPath(ancestorPath, descendantPath) {
448
+ if (ancestorPath === descendantPath) return true;
449
+ return descendantPath.indexOf(ancestorPath + ".") === 0;
450
+ }
451
+
452
+ function isValidDropTarget(srcPath, destPath) {
453
+ if (srcPath === destPath) return false;
454
+ if (isAncestorPath(srcPath, destPath)) return false;
455
+ if (srcPath.indexOf("virtual:") === 0 || srcPath.indexOf("nameSpaces.") === 0) return false;
456
+
457
+ var srcClass = nodeClassFromPath(srcPath);
458
+
459
+ if (destPath === "virtual:Objects") {
460
+ return srcClass === "Folder" || srcClass === "Object";
461
+ }
462
+ if (destPath === "virtual:Types.ObjectTypes") {
463
+ var srcTokens = pathToTokens(srcPath);
464
+ return srcClass === "ObjectType" && srcTokens.length === 2 && srcTokens[0] === "objectsTypes";
465
+ }
466
+ if (destPath === "virtual:Types.DataTypes") {
467
+ var srcTokens = pathToTokens(srcPath);
468
+ return srcClass === "Enumeration" && srcTokens.length === 2 && srcTokens[0] === "enumerations";
469
+ }
470
+
471
+ if (destPath.indexOf("virtual:") === 0 || destPath.indexOf("nameSpaces.") === 0) {
472
+ return false;
473
+ }
474
+
475
+ // Both are real paths. Make sure they are in the same scope (Objects vs Types/ObjectTypes)
476
+ var srcIsTemplate = isObjectTypeModelPath(srcPath);
477
+ var destIsTemplate = isObjectTypeModelPath(destPath);
478
+ if (srcIsTemplate !== destIsTemplate) {
479
+ return false;
480
+ }
481
+
482
+ var destClass = nodeClassFromPath(destPath);
483
+ if (destClass === "Folder" || destClass === "Object" || destClass === "ObjectType") {
484
+ return srcClass !== "Enumeration" && srcClass !== "Namespace";
485
+ }
486
+
487
+ return false;
488
+ }
489
+
490
+ function findObjectPath(obj, targetObj, currentPath) {
491
+ if (obj === targetObj) return currentPath;
492
+ if (typeof obj !== "object" || obj === null) return null;
493
+
494
+ if (Array.isArray(obj)) {
495
+ for (var i = 0; i < obj.length; i++) {
496
+ var path = findObjectPath(obj[i], targetObj, currentPath ? currentPath + "." + i : String(i));
497
+ if (path) return path;
498
+ }
499
+ } else {
500
+ var keys = Object.keys(obj);
501
+ for (var i = 0; i < keys.length; i++) {
502
+ var k = keys[i];
503
+ if (k === "folders" || k === "objects" || k === "variables" || k === "methods" || k === "alarms" || k === "objectsTypes" || k === "enumerations") {
504
+ var path = findObjectPath(obj[k], targetObj, currentPath ? currentPath + "." + k : k);
505
+ if (path) return path;
506
+ }
507
+ }
508
+ }
509
+ return null;
510
+ }
511
+
322
512
  function nodeClassFromPath(path) {
513
+ if (path && path.indexOf("virtual:") === 0) return "VisualFolder";
323
514
  var tokens = pathToTokens(path);
324
515
  if (!tokens.length) return "Object";
325
516
 
@@ -333,7 +524,18 @@
333
524
  if (collectionToken === "folders") return "Folder";
334
525
  return "Object";
335
526
  }
336
- function getNodeDisplayName(path) { var item = getAtPath(editorState, path); return item ? (item.name || "(unnamed)") : ""; }
527
+ function getVirtualNodeName(path) {
528
+ if (path === "virtual:Objects") return "Objects";
529
+ if (path === "virtual:Types") return "Types";
530
+ if (path === "virtual:Types.ObjectTypes") return "ObjectTypes";
531
+ if (path === "virtual:Types.DataTypes") return "DataTypes";
532
+ return "";
533
+ }
534
+ function getNodeDisplayName(path) {
535
+ if (path && path.indexOf("virtual:") === 0) return getVirtualNodeName(path);
536
+ var item = getAtPath(editorState, path);
537
+ return item ? (item.name || "(unnamed)") : "";
538
+ }
337
539
  function getNamespaceOptions() {
338
540
  return Array.isArray(editorState.nameSpaces) ? editorState.nameSpaces.slice().sort(function (left, right) { return left.id - right.id; }) : [];
339
541
  }
@@ -444,7 +646,7 @@
444
646
  function nodeIdSuffixFromValue(nodeId, defaultSuffix) {
445
647
  var raw = String(nodeId || "").trim();
446
648
  if (!raw) return defaultSuffix;
447
- var match = /^ns=\d+;s=(.*)$/.exec(raw);
649
+ var match = /^ns=\d+;[si]=(.*)$/.exec(raw);
448
650
  if (match) return match[1];
449
651
  return raw;
450
652
  }
@@ -485,8 +687,69 @@
485
687
  var item = getAtPath(editorState, path);
486
688
  if (isObjectTypeModelPath(path)) return buildGeneratedNodeIdForPath(path);
487
689
  var customNodeId = item && item.nodeId ? String(item.nodeId).trim() : "";
488
- var suffix = nodeIdSuffixFromValue(customNodeId, buildDefaultNodeIdSuffixFromEditorPath(path));
489
- return getNodeIdPrefix(getNodeNamespaceId(path)) + suffix;
690
+ if (customNodeId) {
691
+ if (/^ns=\d+;[si]=/.test(customNodeId)) {
692
+ return customNodeId;
693
+ }
694
+ return getNodeIdPrefix(getNodeNamespaceId(path)) + customNodeId;
695
+ }
696
+ return buildGeneratedNodeIdForPath(path);
697
+ }
698
+ function parseNodeId(nodeId, defaultSuffix) {
699
+ var raw = String(nodeId || "").trim();
700
+ if (!raw) {
701
+ return { type: "s", value: defaultSuffix };
702
+ }
703
+ var match = /^ns=\d+;([si])=(.*)$/.exec(raw);
704
+ if (match) {
705
+ return { type: match[1], value: match[2] };
706
+ }
707
+ return { type: "s", value: raw };
708
+ }
709
+ function updateNodeIdValueInputState(mode, type) {
710
+ var inputId = "#opcua-" + mode + "-nodeid-value";
711
+ var labelId = "#opcua-" + mode + "-nodeid-value-label";
712
+ var input = $(inputId);
713
+ var label = $(labelId);
714
+
715
+ if (type === "i") {
716
+ if (label.length) label.text("nodeId Value (Numeric)");
717
+ input.attr("type", "number");
718
+ input.attr("step", "1");
719
+ input.attr("placeholder", "Enter a number");
720
+ } else {
721
+ if (label.length) label.text("nodeId Value (String)");
722
+ input.attr("type", "text");
723
+ input.removeAttr("step");
724
+ if (mode === "create") {
725
+ input.attr("placeholder", "Leave blank for default (s)");
726
+ } else {
727
+ input.attr("placeholder", "Enter a string");
728
+ }
729
+ }
730
+ }
731
+ function saveDetailNodeId(path) {
732
+ if (!path) return;
733
+ var type = $("#opcua-detail-nodeid-type").val();
734
+ var rawVal = $("#opcua-detail-nodeid-value").val();
735
+ var nsId = getNodeNamespaceId(path);
736
+
737
+ var customNodeId = "";
738
+ if (rawVal) {
739
+ if (type === "i") {
740
+ var numVal = parseInt(rawVal, 10);
741
+ if (!isNaN(numVal)) {
742
+ customNodeId = "ns=" + nsId + ";i=" + numVal;
743
+ }
744
+ } else {
745
+ var defaultSuffix = buildDefaultNodeIdSuffixFromEditorPath(path);
746
+ var nextSuffix = String(rawVal).trim();
747
+ if (nextSuffix && nextSuffix !== defaultSuffix) {
748
+ customNodeId = "ns=" + nsId + ";s=" + nextSuffix;
749
+ }
750
+ }
751
+ }
752
+ updateNode(path, { nodeId: customNodeId });
490
753
  }
491
754
  function normalizeCustomNodeIdFromSuffix(path, suffix) {
492
755
  if (isObjectTypeModelPath(path)) return "";
@@ -527,6 +790,25 @@
527
790
  }
528
791
 
529
792
  function getChildrenByPath(path) {
793
+ if (path === "virtual:Objects") {
794
+ var children = [];
795
+ (editorState.folders || []).forEach(function (_, i) { children.push("folders." + i); });
796
+ (editorState.objects || []).forEach(function (_, i) { children.push("objects." + i); });
797
+ return children;
798
+ }
799
+ if (path === "virtual:Types") {
800
+ return ["virtual:Types.ObjectTypes", "virtual:Types.DataTypes"];
801
+ }
802
+ if (path === "virtual:Types.ObjectTypes") {
803
+ var children = [];
804
+ (editorState.objectsTypes || []).forEach(function (_, i) { children.push("objectsTypes." + i); });
805
+ return children;
806
+ }
807
+ if (path === "virtual:Types.DataTypes") {
808
+ var children = [];
809
+ (editorState.enumerations || []).forEach(function (_, i) { children.push("enumerations." + i); });
810
+ return children;
811
+ }
530
812
  var item = getAtPath(editorState, path);
531
813
  if (!item) return [];
532
814
  var children = [];
@@ -540,11 +822,7 @@
540
822
  }
541
823
 
542
824
  function getTopLevelPaths() {
543
- var paths = [];
544
- (editorState.folders || []).forEach(function (_, i) { paths.push("folders." + i); });
545
- (editorState.objects || []).forEach(function (_, i) { paths.push("objects." + i); });
546
- (editorState.objectsTypes || []).forEach(function (_, i) { paths.push("objectsTypes." + i); });
547
- (editorState.enumerations || []).forEach(function (_, i) { paths.push("enumerations." + i); });
825
+ var paths = ["virtual:Objects", "virtual:Types"];
548
826
  (editorState.nameSpaces || []).forEach(function (_, i) { paths.push("nameSpaces." + i); });
549
827
  return paths;
550
828
  }
@@ -556,6 +834,9 @@
556
834
 
557
835
  function nodeMatchesSearch(path) {
558
836
  if (!treeSearchTerm) return true;
837
+ if (path && path.indexOf("virtual:") === 0) {
838
+ return getVirtualNodeName(path).toLowerCase().indexOf(treeSearchTerm) !== -1;
839
+ }
559
840
  var item = getAtPath(editorState, path);
560
841
  if (!item) return false;
561
842
  var values = [path, item.name, item.displayName, item.description, nodeClassFromPath(path), item.type, item.value, item.id, item.namespaceId];
@@ -568,7 +849,7 @@
568
849
  }
569
850
 
570
851
  function iconForNodeClass(nodeClass) {
571
- if (nodeClass === "Folder") return "fa-folder";
852
+ if (nodeClass === "Folder" || nodeClass === "VisualFolder") return "fa-folder";
572
853
  if (nodeClass === "Object") return "fa-cube";
573
854
  if (nodeClass === "Variable") return "fa-tag";
574
855
  if (nodeClass === "ObjectType") return "fa-cubes";
@@ -594,7 +875,7 @@
594
875
  }
595
876
 
596
877
  function appendNodeToFrag(frag, path, depth, ancestorMatched) {
597
- var item = getAtPath(editorState, path);
878
+ var item = (path && path.indexOf("virtual:") === 0) ? {} : getAtPath(editorState, path);
598
879
  if (!item) return;
599
880
  var nodeClass = nodeClassFromPath(path);
600
881
  var hasChildren = nodeClass !== "Variable" && nodeClass !== "Alarm" && nodeClass !== "Namespace" && getChildrenByPath(path).length > 0;
@@ -607,11 +888,16 @@
607
888
  var row = document.createElement("div");
608
889
  row.className = "opcua-tree-row" + (path === selectedPath ? " is-selected" : "");
609
890
  row.setAttribute("data-path", path);
891
+ if (path && path.indexOf("virtual:") !== 0 && path.indexOf("nameSpaces.") !== 0) {
892
+ row.setAttribute("draggable", "true");
893
+ }
894
+ var label = (path && path.indexOf("virtual:") === 0) ? getVirtualNodeName(path) : (item.name || "(unnamed)");
895
+ var displayClass = (path && path.indexOf("virtual:") === 0) ? "Folder" : nodeClass;
610
896
  row.innerHTML = indents
611
897
  + '<span class="opcua-tree-twisty">' + (hasChildren ? '<i class="fa ' + (expanded ? "fa-caret-down" : "fa-caret-right") + '"></i>' : "") + "</span>"
612
898
  + '<span class="opcua-tree-icon"><i class="fa ' + iconForNodeClass(nodeClass) + '"></i></span>'
613
- + '<span class="opcua-tree-label">' + escapeHtml(item.name || "(unnamed)") + "</span>"
614
- + '<span class="opcua-tree-type">' + escapeHtml(nodeClass) + "</span>";
899
+ + '<span class="opcua-tree-label">' + escapeHtml(label) + "</span>"
900
+ + '<span class="opcua-tree-type">' + escapeHtml(displayClass) + "</span>";
615
901
  frag.appendChild(row);
616
902
 
617
903
  if (hasChildren && expanded) {
@@ -650,9 +936,28 @@
650
936
  function renderBreadcrumbs() {
651
937
  var el = $("#opcua-tree-breadcrumbs");
652
938
  if (!selectedPath) { el.text("No selection"); return; }
939
+ if (selectedPath.indexOf("virtual:") === 0) {
940
+ var parts = [];
941
+ if (selectedPath === "virtual:Objects") parts = ["Objects"];
942
+ else if (selectedPath === "virtual:Types") parts = ["Types"];
943
+ else if (selectedPath === "virtual:Types.ObjectTypes") parts = ["Types", "ObjectTypes"];
944
+ else if (selectedPath === "virtual:Types.DataTypes") parts = ["Types", "DataTypes"];
945
+ el.text(parts.join("."));
946
+ return;
947
+ }
653
948
  var tokens = pathToTokens(selectedPath);
654
949
  var cursor = [];
655
950
  var parts = [];
951
+ var rootToken = tokens[0];
952
+ if (rootToken === "folders" || rootToken === "objects") {
953
+ parts.push("Objects");
954
+ } else if (rootToken === "objectsTypes") {
955
+ parts.push("Types");
956
+ parts.push("ObjectTypes");
957
+ } else if (rootToken === "enumerations") {
958
+ parts.push("Types");
959
+ parts.push("DataTypes");
960
+ }
656
961
  tokens.forEach(function (token) {
657
962
  cursor.push(token);
658
963
  if (/^\d+$/.test(token)) parts.push(getNodeDisplayName(cursor.join(".")) || ("#" + token));
@@ -672,14 +977,38 @@
672
977
 
673
978
  function openCreateForm(path, kind) {
674
979
  if (!path) return;
675
- if (nodeClassFromPath(path) === "Variable" || nodeClassFromPath(path) === "Namespace") {
980
+ if (path.indexOf("virtual:") === 0) {
981
+ if (path === "virtual:Objects") {
982
+ if (kind !== "folder" && kind !== "object") {
983
+ RED.notify("Only Folders and Objects can be added directly under Objects", "warning");
984
+ return;
985
+ }
986
+ } else if (path === "virtual:Types.ObjectTypes") {
987
+ if (kind !== "objecttype") {
988
+ RED.notify("Only ObjectTypes can be added under ObjectTypes", "warning");
989
+ return;
990
+ }
991
+ } else if (path === "virtual:Types.DataTypes") {
992
+ if (kind !== "enumeration") {
993
+ RED.notify("Only Enumerations can be added under DataTypes", "warning");
994
+ return;
995
+ }
996
+ } else {
997
+ RED.notify("Cannot add children to this visual folder", "warning");
998
+ return;
999
+ }
1000
+ } else if (nodeClassFromPath(path) === "Variable" || nodeClassFromPath(path) === "Namespace") {
676
1001
  RED.notify("Selected item cannot have children", "warning");
677
1002
  return;
678
1003
  }
1004
+ var parentSuffix = path.indexOf("virtual:") === 0 ? "" : buildDefaultNodeIdSuffixFromEditorPath(path);
1005
+ var defaultName = kind === "variable" ? "newVariable" : kind === "enum-variable" ? "newEnumVariable" : kind === "folder" ? "newFolder" : kind === "method" ? "newMethod" : kind === "alarm" ? "newAlarm" : "newObject";
1006
+ var defaultSuffix = parentSuffix ? parentSuffix + "." + defaultName : defaultName;
1007
+
679
1008
  pendingCreate = {
680
1009
  parentPath: path,
681
1010
  kind: kind,
682
- name: kind === "variable" ? "newVariable" : kind === "enum-variable" ? "newEnumVariable" : kind === "folder" ? "newFolder" : kind === "objecttype" ? "newObjectType" : kind === "alarm" ? "newAlarm" : kind === "method" ? "newMethod" : "newObject",
1011
+ name: defaultName,
683
1012
  displayName: "",
684
1013
  dataType: kind === "enum-variable" ? (getDefinedEnumerationNames()[0] || "") : "Int32",
685
1014
  value: "",
@@ -689,16 +1018,19 @@
689
1018
  alarmType: "levelAlarm",
690
1019
  variableNodeId: "",
691
1020
  severity: 500,
692
- highHighLimit: 100,
1021
+ sendValue: true,
1022
+ highHighLimit: 90,
693
1023
  highHighMessage: "High High alarm",
694
1024
  highLimit: 80,
695
1025
  highMessage: "High alarm",
696
1026
  lowLimit: 20,
697
1027
  lowMessage: "Low alarm",
698
- lowLowLimit: 0,
1028
+ lowLowLimit: 10,
699
1029
  lowLowMessage: "Low Low alarm",
700
1030
  normalStateValue: 0,
701
- digitalMessage: "Digital alarm"
1031
+ digitalMessage: "Digital alarm",
1032
+ nodeIdType: "s",
1033
+ nodeIdValue: defaultSuffix
702
1034
  };
703
1035
  renderDetails();
704
1036
  }
@@ -707,19 +1039,45 @@
707
1039
  if (!pendingCreate) return;
708
1040
  var parentPath = pendingCreate.parentPath;
709
1041
  var kind = pendingCreate.kind;
710
- var branchTargetPath = (kind === "variable" || kind === "enum-variable")
711
- ? (parentPath + ".variables")
712
- : kind === "folder"
713
- ? (parentPath + ".folders")
714
- : kind === "objecttype"
715
- ? (parentPath + ".objectsTypes")
716
- : kind === "alarm"
717
- ? (parentPath + ".alarms")
718
- : kind === "method"
719
- ? (parentPath + ".methods")
720
- : (parentPath + ".objects");
1042
+ var branchTargetPath;
1043
+ if (parentPath.indexOf("virtual:") === 0) {
1044
+ if (parentPath === "virtual:Objects") {
1045
+ branchTargetPath = kind === "folder" ? "folders" : "objects";
1046
+ } else if (parentPath === "virtual:Types.ObjectTypes") {
1047
+ branchTargetPath = "objectsTypes";
1048
+ } else {
1049
+ return;
1050
+ }
1051
+ } else {
1052
+ branchTargetPath = (kind === "variable" || kind === "enum-variable")
1053
+ ? (parentPath + ".variables")
1054
+ : kind === "folder"
1055
+ ? (parentPath + ".folders")
1056
+ : kind === "objecttype"
1057
+ ? (parentPath + ".objectsTypes")
1058
+ : kind === "alarm"
1059
+ ? (parentPath + ".alarms")
1060
+ : kind === "method"
1061
+ ? (parentPath + ".methods")
1062
+ : (parentPath + ".objects");
1063
+ }
721
1064
  var target = getAtPath(editorState, branchTargetPath);
722
1065
  if (!Array.isArray(target)) return;
1066
+
1067
+ var customNodeId = "";
1068
+ var parentIsObjectTypeModel = isObjectTypeModelPath(parentPath);
1069
+ if (!parentIsObjectTypeModel && pendingCreate.nodeIdValue) {
1070
+ var nsId = getNodeNamespaceId(parentPath);
1071
+ if (pendingCreate.nodeIdType === "i") {
1072
+ var numVal = parseInt(pendingCreate.nodeIdValue, 10);
1073
+ if (!isNaN(numVal)) {
1074
+ customNodeId = "ns=" + nsId + ";i=" + numVal;
1075
+ }
1076
+ } else {
1077
+ customNodeId = "ns=" + nsId + ";s=" + pendingCreate.nodeIdValue.trim();
1078
+ }
1079
+ }
1080
+
723
1081
  if (kind === "variable" || kind === "enum-variable") {
724
1082
  target.push(normalizeVariable({
725
1083
  name: pendingCreate.name,
@@ -727,13 +1085,16 @@
727
1085
  type: pendingCreate.dataType,
728
1086
  value: pendingCreate.value,
729
1087
  access: pendingCreate.access || "readwrite",
730
- accessPermission: pendingCreate.accessPermission
1088
+ accessPermission: pendingCreate.accessPermission,
1089
+ nodeId: customNodeId
731
1090
  }));
732
1091
  } else if (kind === "folder") {
733
- target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
1092
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission, nodeId: customNodeId }));
734
1093
  } else if (kind === "objecttype") {
735
- target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", objectsType: pendingCreate.objectsType || "", accessPermission: pendingCreate.accessPermission }));
736
- target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(branchTargetPath + "." + (target.length - 1));
1094
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", objectsType: pendingCreate.objectsType || "", accessPermission: pendingCreate.accessPermission, nodeId: customNodeId }));
1095
+ if (!customNodeId) {
1096
+ target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(branchTargetPath + "." + (target.length - 1));
1097
+ }
737
1098
  } else if (kind === "alarm") {
738
1099
  target.push(normalizeAlarm({
739
1100
  displayName: pendingCreate.displayName || "",
@@ -742,6 +1103,7 @@
742
1103
  type: pendingCreate.alarmType,
743
1104
  variableNodeId: pendingCreate.variableNodeId,
744
1105
  severity: Number(pendingCreate.severity || 500),
1106
+ sendValue: pendingCreate.sendValue,
745
1107
  highHighLimit: pendingCreate.highHighLimit,
746
1108
  highHighMessage: pendingCreate.highHighMessage,
747
1109
  highLimit: pendingCreate.highLimit,
@@ -751,12 +1113,13 @@
751
1113
  lowLowLimit: pendingCreate.lowLowLimit,
752
1114
  lowLowMessage: pendingCreate.lowLowMessage,
753
1115
  normalStateValue: pendingCreate.normalStateValue,
754
- digitalMessage: pendingCreate.digitalMessage
1116
+ digitalMessage: pendingCreate.digitalMessage,
1117
+ nodeId: customNodeId
755
1118
  }));
756
1119
  } else if (kind === "method") {
757
- target.push(normalizeMethod({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
1120
+ target.push(normalizeMethod({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission, nodeId: customNodeId }));
758
1121
  } else {
759
- target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
1122
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission, nodeId: customNodeId }));
760
1123
  }
761
1124
  expansionState[parentPath] = true;
762
1125
  pendingCreate = null;
@@ -778,6 +1141,11 @@
778
1141
  panel.append('<div class="form-row"><label>Name</label><input type="text" id="opcua-create-name"></div>');
779
1142
  panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-create-displayname" placeholder="Leave blank to use browseName"></div>');
780
1143
  panel.append('<div class="form-row"><label>accessPermission</label>' + buildAccessPermissionSelect("opcua-create-accesspermission", pendingCreate.accessPermission || ["public"]) + '</div>');
1144
+ var parentIsObjectTypeModel = isObjectTypeModelPath(pendingCreate.parentPath);
1145
+ if (!parentIsObjectTypeModel) {
1146
+ panel.append('<div class="form-row"><label>nodeId Type</label><select id="opcua-create-nodeid-type"><option value="s">s (String)</option><option value="i">i (Numeric)</option></select></div>');
1147
+ panel.append('<div class="form-row" id="opcua-create-nodeid-value-row"><label id="opcua-create-nodeid-value-label">nodeId Value</label><input type="text" id="opcua-create-nodeid-value" placeholder="Leave blank for default (s)"></div>');
1148
+ }
781
1149
  if (pendingCreate.kind === "variable" || pendingCreate.kind === "enum-variable") {
782
1150
  var isEnum = pendingCreate.kind === "enum-variable";
783
1151
  var typeHtml = isEnum ? buildEnumerationSelect("opcua-create-type", pendingCreate.dataType) : '<select id="opcua-create-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Int64">Int64</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select>';
@@ -792,6 +1160,7 @@
792
1160
  panel.append('<div class="form-row"><label>alarmType</label><select id="opcua-create-alarm-type"><option value="levelAlarm">levelAlarm</option><option value="digitalAlarm">digitalAlarm</option></select></div>');
793
1161
  panel.append('<div class="form-row"><label>variablePath</label><input type="text" id="opcua-create-variable-nodeid"></div>');
794
1162
  panel.append('<div class="form-row"><label>severity</label><input type="number" id="opcua-create-severity"></div>');
1163
+ panel.append('<div class="form-row"><label for="opcua-create-sendvalue">Send alarm value</label><input type="checkbox" id="opcua-create-sendvalue" style="width: auto; flex: 0 0 auto; min-width: 0;"></div>');
795
1164
  if (pendingCreate.alarmType === "levelAlarm") {
796
1165
  panel.append('<div class="form-row"><label>highHighLimit</label><input type="number" id="opcua-create-highhighlimit"></div>');
797
1166
  panel.append('<div class="form-row"><label>highHighMessage</label><input type="text" id="opcua-create-highhighmessage"></div>');
@@ -812,6 +1181,11 @@
812
1181
  $("#opcua-create-name").val(pendingCreate.name);
813
1182
  $("#opcua-create-displayname").val(pendingCreate.displayName || "");
814
1183
  $("#opcua-create-accesspermission").val(normalizeAccessPermissionValues(pendingCreate.accessPermission));
1184
+ if (!parentIsObjectTypeModel) {
1185
+ $("#opcua-create-nodeid-type").val(pendingCreate.nodeIdType || "s");
1186
+ $("#opcua-create-nodeid-value").val(pendingCreate.nodeIdValue || "");
1187
+ updateNodeIdValueInputState("create", pendingCreate.nodeIdType || "s");
1188
+ }
815
1189
  $("#opcua-create-type").val(pendingCreate.dataType);
816
1190
  $("#opcua-create-value").val(pendingCreate.value);
817
1191
  $("#opcua-create-objectstype").val(pendingCreate.objectsType);
@@ -819,6 +1193,7 @@
819
1193
  $("#opcua-create-alarm-type").val(pendingCreate.alarmType);
820
1194
  $("#opcua-create-variable-nodeid").val(pendingCreate.variableNodeId);
821
1195
  $("#opcua-create-severity").val(pendingCreate.severity);
1196
+ $("#opcua-create-sendvalue").prop("checked", pendingCreate.sendValue !== false);
822
1197
  $("#opcua-create-highhighlimit").val(pendingCreate.highHighLimit);
823
1198
  $("#opcua-create-highhighmessage").val(pendingCreate.highHighMessage);
824
1199
  $("#opcua-create-highlimit").val(pendingCreate.highLimit);
@@ -832,6 +1207,11 @@
832
1207
  return;
833
1208
  }
834
1209
  if (!selectedPath) { panel.append('<div class="opcua-tree-empty">Select a node to edit browseName, namespace, nodeId, and description.</div>'); return; }
1210
+ if (selectedPath.indexOf("virtual:") === 0) {
1211
+ var visualName = getVirtualNodeName(selectedPath);
1212
+ panel.append('<div class="opcua-tree-empty">Visual folder: <strong>' + escapeHtml(visualName) + '</strong><br><span style="font-size: 11px; color: #666;">This folder is used strictly for visual organization within the modal.</span></div>');
1213
+ return;
1214
+ }
835
1215
  var item = getAtPath(editorState, selectedPath);
836
1216
  if (!item) { panel.append('<div class="opcua-tree-empty">Selected node not found.</div>'); return; }
837
1217
  var nodeClass = nodeClassFromPath(selectedPath);
@@ -883,7 +1263,18 @@
883
1263
  panel.append('<div class="form-row"><label>browseName</label><input type="text" id="opcua-detail-name"></div>');
884
1264
  panel.append('<div class="form-row"><label>nodeClass</label><input type="text" id="opcua-detail-class" readonly></div>');
885
1265
  panel.append('<div class="form-row"><label>namespace</label><select id="opcua-detail-namespace"></select></div>');
886
- panel.append('<div class="form-row"><label>nodeId</label><div class="opcua-nodeid-field"><span class="opcua-nodeid-prefix">' + getNodeIdPrefix(namespaceId) + '</span><input type="text" id="opcua-detail-nodeid"' + (nodeIdLocked ? ' readonly title="Generated automatically for object type models."' : '') + '><a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a></div></div>');
1266
+ if (!nodeIdLocked) {
1267
+ panel.append('<div class="form-row"><label>nodeId</label>' +
1268
+ '<div class="opcua-nodeid-field">' +
1269
+ '<span class="opcua-nodeid-prefix">ns=' + namespaceId + ';</span>' +
1270
+ '<select id="opcua-detail-nodeid-type" style="width: 70px; flex: 0 0 auto;"><option value="s">s</option><option value="i">i</option></select>' +
1271
+ '<span style="padding: 0 4px; flex: 0 0 auto;">=</span>' +
1272
+ '<input type="text" id="opcua-detail-nodeid-value" style="flex: 1 1 auto; font-family: monospace;">' +
1273
+ '<a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a>' +
1274
+ '</div></div>');
1275
+ } else {
1276
+ panel.append('<div class="form-row"><label>nodeId</label><div class="opcua-nodeid-field"><span class="opcua-nodeid-prefix">' + getNodeIdPrefix(namespaceId) + '</span><input type="text" id="opcua-detail-nodeid"' + (nodeIdLocked ? ' readonly title="Generated automatically for object type models."' : '') + '><a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a></div></div>');
1277
+ }
887
1278
  panel.append('<div class="form-row"><label>Description</label><input type="text" id="opcua-detail-description"></div>');
888
1279
  panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-detail-displayname" placeholder="Leave blank to use browseName"></div>');
889
1280
  panel.append('<div class="form-row"><label>accessPermission</label>' + buildAccessPermissionSelect("opcua-detail-accesspermission", item.accessPermission || ["public"]) + '</div>');
@@ -942,6 +1333,7 @@
942
1333
  panel.append('<div class="form-row"><label>alarmType</label><select id="opcua-detail-alarm-type"><option value="levelAlarm">levelAlarm</option><option value="digitalAlarm">digitalAlarm</option></select></div>');
943
1334
  panel.append('<div class="form-row"><label>variablePath</label><input type="text" id="opcua-detail-variable-nodeid"></div>');
944
1335
  panel.append('<div class="form-row"><label>severity</label><input type="number" id="opcua-detail-severity"></div>');
1336
+ panel.append('<div class="form-row"><label for="opcua-detail-sendvalue">Send alarm value</label><input type="checkbox" id="opcua-detail-sendvalue" style="width: auto; flex: 0 0 auto; min-width: 0;"></div>');
945
1337
  if ((item.type || "levelAlarm") === "levelAlarm") {
946
1338
  panel.append('<div class="form-row"><label>highHighLimit</label><input type="number" id="opcua-detail-highhighlimit"></div>');
947
1339
  panel.append('<div class="form-row"><label>highHighMessage</label><input type="text" id="opcua-detail-highhighmessage"></div>');
@@ -959,7 +1351,14 @@
959
1351
  panel.append('<div class="form-row"><label style="width:90px;">Actions</label><div><a href="#" id="opcua-detail-edit" class="editor-button editor-button-small"><i class="fa fa-pencil"></i> Edit</a> <a href="#" id="opcua-detail-remove" class="editor-button editor-button-small"><i class="fa fa-trash"></i> Remove</a></div></div>');
960
1352
  $("#opcua-detail-name").val(item.name || "");
961
1353
  $("#opcua-detail-class").val(nodeClass);
962
- $("#opcua-detail-nodeid").val(nodeIdLocked ? buildDefaultNodeIdSuffixFromEditorPath(selectedPath) : nodeIdSuffix);
1354
+ if (!nodeIdLocked) {
1355
+ var parsed = parseNodeId(item.nodeId, buildDefaultNodeIdSuffixFromEditorPath(selectedPath));
1356
+ $("#opcua-detail-nodeid-type").val(parsed.type);
1357
+ $("#opcua-detail-nodeid-value").val(parsed.value);
1358
+ updateNodeIdValueInputState("detail", parsed.type);
1359
+ } else {
1360
+ $("#opcua-detail-nodeid").val(nodeIdLocked ? buildDefaultNodeIdSuffixFromEditorPath(selectedPath) : nodeIdSuffix);
1361
+ }
963
1362
  $("#opcua-detail-description").val(item.description || "");
964
1363
  namespaceOptions.forEach(function (option) {
965
1364
  $("#opcua-detail-namespace").append($("<option></option>").val(option.id).text(getNamespaceLabel(option.id)));
@@ -973,13 +1372,14 @@
973
1372
  $("#opcua-detail-alarm-type").val(item.type || "levelAlarm");
974
1373
  $("#opcua-detail-variable-nodeid").val(item.variableNodeId || "");
975
1374
  $("#opcua-detail-severity").val(item.severity !== undefined ? item.severity : 500);
976
- $("#opcua-detail-highhighlimit").val(item.highHighLimit !== undefined ? item.highHighLimit : 100);
1375
+ $("#opcua-detail-sendvalue").prop("checked", item.sendValue !== false);
1376
+ $("#opcua-detail-highhighlimit").val(item.highHighLimit !== undefined ? item.highHighLimit : 90);
977
1377
  $("#opcua-detail-highhighmessage").val(item.highHighMessage || "High High alarm");
978
1378
  $("#opcua-detail-highlimit").val(item.highLimit !== undefined ? item.highLimit : 80);
979
1379
  $("#opcua-detail-highmessage").val(item.highMessage || "High alarm");
980
1380
  $("#opcua-detail-lowlimit").val(item.lowLimit !== undefined ? item.lowLimit : 20);
981
1381
  $("#opcua-detail-lowmessage").val(item.lowMessage || "Low alarm");
982
- $("#opcua-detail-lowlowlimit").val(item.lowLowLimit !== undefined ? item.lowLowLimit : 0);
1382
+ $("#opcua-detail-lowlowlimit").val(item.lowLowLimit !== undefined ? item.lowLowLimit : 10);
983
1383
  $("#opcua-detail-lowlowmessage").val(item.lowLowMessage || "Low Low alarm");
984
1384
  $("#opcua-detail-normalstatevalue").val(item.normalStateValue !== undefined ? item.normalStateValue : 0);
985
1385
  $("#opcua-detail-digitalmessage").val(item.digitalMessage || "Digital alarm");
@@ -993,6 +1393,10 @@
993
1393
 
994
1394
  function removeNode(path) {
995
1395
  if (!path) return;
1396
+ if (path.indexOf("virtual:") === 0) {
1397
+ RED.notify("Visual folders cannot be removed.", "warning");
1398
+ return;
1399
+ }
996
1400
  if (nodeClassFromPath(path) === "Namespace") {
997
1401
  var namespaceItem = getAtPath(editorState, path);
998
1402
  if (normalizeNamespaceId(namespaceItem && namespaceItem.id) === DEFAULT_NAMESPACE_ID) {
@@ -1004,8 +1408,72 @@
1004
1408
  if (selectedPath === path) selectedPath = "";
1005
1409
  syncStateToJson(true);
1006
1410
  renderVisualEditor();
1411
+ }
1412
+
1413
+ function moveNode(srcPath, destPath) {
1414
+ var item = getAtPath(editorState, srcPath);
1415
+ if (!item) return;
1416
+
1417
+ var srcClass = nodeClassFromPath(srcPath);
1418
+ var targetCollection = null;
1419
+
1420
+ if (destPath === "virtual:Objects") {
1421
+ if (srcClass === "Folder") {
1422
+ editorState.folders = editorState.folders || [];
1423
+ targetCollection = editorState.folders;
1424
+ } else if (srcClass === "Object") {
1425
+ editorState.objects = editorState.objects || [];
1426
+ targetCollection = editorState.objects;
1427
+ }
1428
+ } else if (destPath === "virtual:Types.ObjectTypes") {
1429
+ editorState.objectsTypes = editorState.objectsTypes || [];
1430
+ targetCollection = editorState.objectsTypes;
1431
+ } else if (destPath === "virtual:Types.DataTypes") {
1432
+ editorState.enumerations = editorState.enumerations || [];
1433
+ targetCollection = editorState.enumerations;
1434
+ } else {
1435
+ var destParent = getAtPath(editorState, destPath);
1436
+ if (!destParent) return;
1437
+
1438
+ if (srcClass === "Folder") {
1439
+ destParent.folders = destParent.folders || [];
1440
+ targetCollection = destParent.folders;
1441
+ } else if (srcClass === "Object") {
1442
+ destParent.objects = destParent.objects || [];
1443
+ targetCollection = destParent.objects;
1444
+ } else if (srcClass === "Variable") {
1445
+ destParent.variables = destParent.variables || [];
1446
+ targetCollection = destParent.variables;
1447
+ } else if (srcClass === "Method") {
1448
+ destParent.methods = destParent.methods || [];
1449
+ targetCollection = destParent.methods;
1450
+ } else if (srcClass === "Alarm") {
1451
+ destParent.alarms = destParent.alarms || [];
1452
+ targetCollection = destParent.alarms;
1453
+ } else if (srcClass === "ObjectType") {
1454
+ destParent.objectsTypes = destParent.objectsTypes || [];
1455
+ targetCollection = destParent.objectsTypes;
1456
+ }
1457
+ }
1458
+
1459
+ if (targetCollection) {
1460
+ removeAtPath(editorState, srcPath);
1461
+ targetCollection.push(item);
1462
+
1463
+ var newPath = findObjectPath(editorState, item, "");
1464
+ if (newPath) {
1465
+ selectedPath = newPath;
1466
+ expansionState[destPath] = true;
1467
+ } else {
1468
+ selectedPath = "";
1469
+ }
1470
+
1471
+ syncStateToJson(true);
1472
+ renderVisualEditor();
1473
+ }
1007
1474
  }
1008
1475
 
1476
+
1009
1477
  function buildGroupOptions(selectedGroup) {
1010
1478
  var currentGroup = String(selectedGroup || "");
1011
1479
  var options = authGroups.map(function (groupName) {
@@ -1234,6 +1702,56 @@
1234
1702
  } catch (error) { RED.notify("Invalid JSON: " + error.message, "error"); }
1235
1703
  });
1236
1704
 
1705
+ $(document).on("dragstart", ".opcua-tree-row", function (event) {
1706
+ var path = $(this).attr("data-path");
1707
+ if (path && path.indexOf("virtual:") !== 0 && path.indexOf("nameSpaces.") !== 0) {
1708
+ draggedPath = path;
1709
+ if (event.originalEvent.dataTransfer) {
1710
+ event.originalEvent.dataTransfer.effectAllowed = "move";
1711
+ event.originalEvent.dataTransfer.setData("text/plain", path);
1712
+ }
1713
+ $(this).addClass("is-dragging");
1714
+ } else {
1715
+ event.preventDefault();
1716
+ }
1717
+ });
1718
+
1719
+ $(document).on("dragend", ".opcua-tree-row", function (event) {
1720
+ $(this).removeClass("is-dragging");
1721
+ draggedPath = null;
1722
+ $(".opcua-tree-row").removeClass("drag-over");
1723
+ });
1724
+
1725
+ $(document).on("dragover", ".opcua-tree-row", function (event) {
1726
+ if (!draggedPath) return;
1727
+ event.preventDefault();
1728
+
1729
+ var row = $(this);
1730
+ var targetPath = row.attr("data-path");
1731
+ if (isValidDropTarget(draggedPath, targetPath)) {
1732
+ if (event.originalEvent.dataTransfer) {
1733
+ event.originalEvent.dataTransfer.dropEffect = "move";
1734
+ }
1735
+ $(".opcua-tree-row").not(row).removeClass("drag-over");
1736
+ row.addClass("drag-over");
1737
+ }
1738
+ });
1739
+
1740
+ $(document).on("dragleave", ".opcua-tree-row", function (event) {
1741
+ $(this).removeClass("drag-over");
1742
+ });
1743
+
1744
+ $(document).on("drop", ".opcua-tree-row", function (event) {
1745
+ event.preventDefault();
1746
+ $(".opcua-tree-row").removeClass("drag-over");
1747
+
1748
+ if (!draggedPath) return;
1749
+ var targetPath = $(this).attr("data-path");
1750
+ if (isValidDropTarget(draggedPath, targetPath)) {
1751
+ moveNode(draggedPath, targetPath);
1752
+ }
1753
+ });
1754
+
1237
1755
  $(document).on("click", ".opcua-tree-row", function (event) {
1238
1756
  var path = $(this).attr("data-path");
1239
1757
  if ($(event.target).closest(".opcua-tree-twisty").length) {
@@ -1246,8 +1764,57 @@
1246
1764
 
1247
1765
  $(document).on("contextmenu", ".opcua-tree-row", function (event) {
1248
1766
  event.preventDefault();
1249
- selectNode($(this).attr("data-path"));
1250
- $("#opcua-tree-context-menu").css({ left: event.clientX + "px", top: event.clientY + "px" }).show();
1767
+ var path = $(this).attr("data-path");
1768
+ selectNode(path);
1769
+
1770
+ var contextMenu = $("#opcua-tree-context-menu");
1771
+ contextMenu.find("a").hide();
1772
+
1773
+ if (path.indexOf("virtual:") === 0) {
1774
+ if (path === "virtual:Objects") {
1775
+ contextMenu.find('[data-action="add-folder"]').show();
1776
+ contextMenu.find('[data-action="add-object"]').show();
1777
+ contextMenu.find('[data-action="expand-all-below"]').show();
1778
+ contextMenu.find('[data-action="collapse-all-below"]').show();
1779
+ } else if (path === "virtual:Types.ObjectTypes") {
1780
+ contextMenu.find('[data-action="add-objecttype"]').show();
1781
+ contextMenu.find('[data-action="expand-all-below"]').show();
1782
+ contextMenu.find('[data-action="collapse-all-below"]').show();
1783
+ } else if (path === "virtual:Types.DataTypes") {
1784
+ contextMenu.find('[data-action="add-enumeration"]').show();
1785
+ contextMenu.find('[data-action="expand-all-below"]').show();
1786
+ contextMenu.find('[data-action="collapse-all-below"]').show();
1787
+ }
1788
+ } else {
1789
+ var nodeClass = nodeClassFromPath(path);
1790
+ if (nodeClass === "Folder" || nodeClass === "Object" || nodeClass === "ObjectType") {
1791
+ contextMenu.find('[data-action="add-folder"]').show();
1792
+ contextMenu.find('[data-action="add-object"]').show();
1793
+ contextMenu.find('[data-action="add-variable"]').show();
1794
+ contextMenu.find('[data-action="add-objecttype"]').show();
1795
+ contextMenu.find('[data-action="add-enum-variable"]').show();
1796
+ contextMenu.find('[data-action="add-alarm"]').show();
1797
+ contextMenu.find('[data-action="add-method"]').show();
1798
+ contextMenu.find('[data-action="edit"]').show();
1799
+ contextMenu.find('[data-action="remove"]').show();
1800
+ contextMenu.find('[data-action="expand-all-below"]').show();
1801
+ contextMenu.find('[data-action="collapse-all-below"]').show();
1802
+ } else if (nodeClass === "Enumeration") {
1803
+ contextMenu.find('[data-action="edit"]').show();
1804
+ contextMenu.find('[data-action="remove"]').show();
1805
+ } else if (nodeClass === "Namespace") {
1806
+ contextMenu.find('[data-action="edit"]').show();
1807
+ var item = getAtPath(editorState, path);
1808
+ if (item && normalizeNamespaceId(item.id) !== DEFAULT_NAMESPACE_ID) {
1809
+ contextMenu.find('[data-action="remove"]').show();
1810
+ }
1811
+ } else {
1812
+ contextMenu.find('[data-action="edit"]').show();
1813
+ contextMenu.find('[data-action="remove"]').show();
1814
+ }
1815
+ }
1816
+
1817
+ contextMenu.css({ left: event.clientX + "px", top: event.clientY + "px" }).show();
1251
1818
  });
1252
1819
 
1253
1820
  $(document).on("click", function () { $("#opcua-tree-context-menu").hide(); });
@@ -1265,6 +1832,22 @@
1265
1832
  if (action === "add-method") addNode(selectedPath, "method");
1266
1833
  if (action === "remove") removeNode(selectedPath);
1267
1834
  if (action === "edit") renderDetails();
1835
+ if (action === "expand-all-below") {
1836
+ function walk(p) {
1837
+ expansionState[p] = true;
1838
+ getChildrenByPath(p).forEach(walk);
1839
+ }
1840
+ walk(selectedPath);
1841
+ renderTree();
1842
+ }
1843
+ if (action === "collapse-all-below") {
1844
+ function walk(p) {
1845
+ expansionState[p] = false;
1846
+ getChildrenByPath(p).forEach(walk);
1847
+ }
1848
+ walk(selectedPath);
1849
+ renderTree();
1850
+ }
1268
1851
  $("#opcua-tree-context-menu").hide();
1269
1852
  });
1270
1853
 
@@ -1344,13 +1927,25 @@
1344
1927
  var item = getAtPath(editorState, selectedPath);
1345
1928
  if (!item) return;
1346
1929
  item.namespaceId = nextNamespaceId;
1347
- item.nodeId = normalizeCustomNodeIdFromSuffix(selectedPath, $("#opcua-detail-nodeid").val());
1930
+ if (nodeClassFromPath(selectedPath) === "Variable" && !isObjectTypeModelPath(selectedPath)) {
1931
+ saveDetailNodeId(selectedPath);
1932
+ } else {
1933
+ item.nodeId = normalizeCustomNodeIdFromSuffix(selectedPath, $("#opcua-detail-nodeid").val());
1934
+ }
1348
1935
  syncStateToJson(false);
1349
1936
  renderTree();
1350
1937
  renderBreadcrumbs();
1351
1938
  renderDetails();
1352
1939
  });
1353
1940
  $(document).on("input", "#opcua-detail-nodeid", function () { updateNode(selectedPath, { nodeId: normalizeCustomNodeIdFromSuffix(selectedPath, $(this).val()) }); });
1941
+ $(document).on("change", "#opcua-detail-nodeid-type", function () {
1942
+ var type = $(this).val();
1943
+ updateNodeIdValueInputState("detail", type);
1944
+ saveDetailNodeId(selectedPath);
1945
+ });
1946
+ $(document).on("input", "#opcua-detail-nodeid-value", function () {
1947
+ saveDetailNodeId(selectedPath);
1948
+ });
1354
1949
  $(document).on("click", "#opcua-detail-copy-nodeid", function (event) {
1355
1950
  event.preventDefault();
1356
1951
  copyNodeIdValue(buildDisplayNodeIdFromEditorPath(selectedPath));
@@ -1389,6 +1984,7 @@
1389
1984
  });
1390
1985
  $(document).on("input", "#opcua-detail-variable-nodeid", function () { updateNode(selectedPath, { variableNodeId: $(this).val() }); });
1391
1986
  $(document).on("input", "#opcua-detail-severity", function () { updateNode(selectedPath, { severity: Number($(this).val() || 0) }); });
1987
+ $(document).on("change", "#opcua-detail-sendvalue", function () { updateNode(selectedPath, { sendValue: $(this).is(":checked") }); });
1392
1988
  $(document).on("input", "#opcua-detail-highhighlimit", function () { updateNode(selectedPath, { highHighLimit: Number($(this).val() || 0) }); });
1393
1989
  $(document).on("input", "#opcua-detail-highhighmessage", function () { updateNode(selectedPath, { highHighMessage: $(this).val() }); });
1394
1990
  $(document).on("input", "#opcua-detail-highlimit", function () { updateNode(selectedPath, { highLimit: Number($(this).val() || 0) }); });
@@ -1399,8 +1995,35 @@
1399
1995
  $(document).on("input", "#opcua-detail-lowlowmessage", function () { updateNode(selectedPath, { lowLowMessage: $(this).val() }); });
1400
1996
  $(document).on("input", "#opcua-detail-normalstatevalue", function () { updateNode(selectedPath, { normalStateValue: Number($(this).val() || 0) }); });
1401
1997
  $(document).on("input", "#opcua-detail-digitalmessage", function () { updateNode(selectedPath, { digitalMessage: $(this).val() }); });
1402
- $(document).on("input", "#opcua-create-name", function () { if (pendingCreate) pendingCreate.name = $(this).val(); });
1998
+ $(document).on("input", "#opcua-create-name", function () {
1999
+ if (pendingCreate) {
2000
+ var oldName = pendingCreate.name;
2001
+ var nextName = $(this).val();
2002
+ pendingCreate.name = nextName;
2003
+
2004
+ if (pendingCreate.kind === "variable" || pendingCreate.kind === "enum-variable") {
2005
+ if (pendingCreate.nodeIdType === "s") {
2006
+ var parentSuffix = buildDefaultNodeIdSuffixFromEditorPath(pendingCreate.parentPath);
2007
+ var oldAutoSuffix = parentSuffix ? parentSuffix + "." + oldName : oldName;
2008
+ var nextAutoSuffix = parentSuffix ? parentSuffix + "." + nextName : nextName;
2009
+
2010
+ if (!pendingCreate.nodeIdValue || pendingCreate.nodeIdValue === oldAutoSuffix) {
2011
+ pendingCreate.nodeIdValue = nextAutoSuffix;
2012
+ $("#opcua-create-nodeid-value").val(nextAutoSuffix);
2013
+ }
2014
+ }
2015
+ }
2016
+ }
2017
+ });
1403
2018
  $(document).on("input", "#opcua-create-displayname", function () { if (pendingCreate) pendingCreate.displayName = $(this).val(); });
2019
+ $(document).on("change", "#opcua-create-nodeid-type", function () {
2020
+ var type = $(this).val();
2021
+ if (pendingCreate) pendingCreate.nodeIdType = type;
2022
+ updateNodeIdValueInputState("create", type);
2023
+ });
2024
+ $(document).on("input", "#opcua-create-nodeid-value", function () {
2025
+ if (pendingCreate) pendingCreate.nodeIdValue = $(this).val();
2026
+ });
1404
2027
  $(document).on("change", "#opcua-create-accesspermission", function () { if (pendingCreate) pendingCreate.accessPermission = normalizeAccessPermissionValues($(this).val()); });
1405
2028
  $(document).on("change", "#opcua-create-type", function () { if (pendingCreate) pendingCreate.dataType = $(this).val(); });
1406
2029
  $(document).on("input", "#opcua-create-value", function () { if (pendingCreate) pendingCreate.value = $(this).val(); });
@@ -1414,6 +2037,7 @@
1414
2037
  });
1415
2038
  $(document).on("input", "#opcua-create-variable-nodeid", function () { if (pendingCreate) pendingCreate.variableNodeId = $(this).val(); });
1416
2039
  $(document).on("input", "#opcua-create-severity", function () { if (pendingCreate) pendingCreate.severity = Number($(this).val() || 0); });
2040
+ $(document).on("change", "#opcua-create-sendvalue", function () { if (pendingCreate) pendingCreate.sendValue = $(this).is(":checked"); });
1417
2041
  $(document).on("input", "#opcua-create-highhighlimit", function () { if (pendingCreate) pendingCreate.highHighLimit = Number($(this).val() || 0); });
1418
2042
  $(document).on("input", "#opcua-create-highhighmessage", function () { if (pendingCreate) pendingCreate.highHighMessage = $(this).val(); });
1419
2043
  $(document).on("input", "#opcua-create-highlimit", function () { if (pendingCreate) pendingCreate.highLimit = Number($(this).val() || 0); });
@@ -1475,9 +2099,12 @@
1475
2099
  color: "#d9edf7",
1476
2100
  credentials: { username: { type: "text" }, password: { type: "password" }, users: { type: "text" }, groups: { type: "text" } },
1477
2101
  defaults: {
1478
- name: { value: "" }, resourcePath: { value: "/" }, serverName: { value: "Node-RED OPC UA Server", required: true }, allowAnonymous: { value: true },
2102
+ name: { value: "" }, resourcePath: { value: "/" }, serverName: { value: "Node-RED OPC UA Server", required: true }, allowAnonymous: { value: true }, automaticallyAcceptUnknownCertificate: { value: true },
1479
2103
  port: { value: 4840, required: true, validate: function (value) { var port = Number(value); return Number.isInteger(port) && port > 0 && port < 65536; } },
1480
2104
  maxConnections: { value: 10, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n > 0; } },
2105
+ minSessionTimeout: { value: 100, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n >= 0; } },
2106
+ defaultSessionTimeout: { value: 30000, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n >= 0; } },
2107
+ maxSessionTimeout: { value: 3000000, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n >= 0; } },
1481
2108
  securityPolicy: { value: "None", required: true }, securityMode: { value: "None", required: true }, namespaceUri: { value: "urn:node-red:opc-ua-server", required: true },
1482
2109
  tree: {
1483
2110
  value: "{\n \"folders\": [],\n \"objects\": [],\n \"objectsTypes\": [],\n \"nameSpaces\": [\n {\n \"id\": 2,\n \"name\": \"urn:node-red:opc-ua-server\"\n }\n ]\n}",
@@ -1493,6 +2120,15 @@
1493
2120
  label: function () { return this.name || this.serverName || "opc-ua-server"; },
1494
2121
  oneditprepare: function () {
1495
2122
  var node = this;
2123
+ if (!$("#node-input-minSessionTimeout").val()) {
2124
+ $("#node-input-minSessionTimeout").val(100);
2125
+ }
2126
+ if (!$("#node-input-defaultSessionTimeout").val()) {
2127
+ $("#node-input-defaultSessionTimeout").val(30000);
2128
+ }
2129
+ if (!$("#node-input-maxSessionTimeout").val()) {
2130
+ $("#node-input-maxSessionTimeout").val(3000000);
2131
+ }
1496
2132
  editorState = cloneTree(normalizeTree(parseTree(node.tree)));
1497
2133
  authGroups = normalizeAuthGroups($("#node-input-groups").val());
1498
2134
  authUsers = normalizeAuthUsers($("#node-input-users").val());
@@ -1501,6 +2137,35 @@
1501
2137
  $("#node-input-namespaceUri").val(defaultNamespaceEntry.name);
1502
2138
  }
1503
2139
  syncAuthCredentialFields();
2140
+ $("#node-input-securityPolicy").typedInput({
2141
+ types: [
2142
+ {
2143
+ value: "securityPolicy",
2144
+ multiple: "true",
2145
+ options: [
2146
+ { value: "None", label: "None" },
2147
+ { value: "Basic128Rsa15", label: "Basic128Rsa15" },
2148
+ { value: "Basic256", label: "Basic256" },
2149
+ { value: "Basic256Sha256", label: "Basic256Sha256" },
2150
+ { value: "Aes128_Sha256_RsaOaep", label: "Aes128_Sha256_RsaOaep" },
2151
+ { value: "Aes256_Sha256_RsaPss", label: "Aes256_Sha256_RsaPss" }
2152
+ ]
2153
+ }
2154
+ ]
2155
+ });
2156
+ $("#node-input-securityMode").typedInput({
2157
+ types: [
2158
+ {
2159
+ value: "securityMode",
2160
+ multiple: "true",
2161
+ options: [
2162
+ { value: "None", label: "None" },
2163
+ { value: "Sign", label: "Sign" },
2164
+ { value: "SignAndEncrypt", label: "SignAndEncrypt" }
2165
+ ]
2166
+ }
2167
+ ]
2168
+ });
1504
2169
  updateTreeField(prettyTree(editorState), false);
1505
2170
  $("#node-input-tree-editor").typedInput({ type: "json", types: ["json"] });
1506
2171
  $("#node-input-tree-editor").typedInput("value", prettyTree(editorState));
@@ -1524,6 +2189,35 @@
1524
2189
  $("#node-input-add-auth-group").off("click").on("click", function (event) { event.preventDefault(); addAuthGroup(); });
1525
2190
  $("#node-input-add-auth-user").off("click").on("click", function (event) { event.preventDefault(); addAuthUser(); });
1526
2191
 
2192
+ $("#node-input-open-cert-modal").off("click").on("click", function (event) { event.preventDefault(); openCertModal(); });
2193
+ $("#node-input-close-cert-modal").off("click").on("click", function (event) { event.preventDefault(); closeCertModal(); });
2194
+ $("#node-input-cert-modal").off("click").on("click", function (event) { if (event.target === this) closeCertModal(); });
2195
+
2196
+ $("#node-input-open-settings-modal").off("click").on("click", function (event) { event.preventDefault(); openSettingsModal(); });
2197
+ $("#node-input-close-settings-modal").off("click").on("click", function (event) { event.preventDefault(); closeSettingsModal(); });
2198
+ $("#node-input-settings-modal").off("click").on("click", function (event) { if (event.target === this) closeSettingsModal(); });
2199
+
2200
+ $("#opcua-cert-folders").off("click", ".opcua-cert-item").on("click", ".opcua-cert-item", function () {
2201
+ $("#opcua-cert-folders .opcua-cert-item").removeClass("is-selected");
2202
+ $(this).addClass("is-selected");
2203
+ selectedCertFolder = $(this).attr("data-folder");
2204
+ selectedCertName = "";
2205
+ $("#opcua-cert-details").hide();
2206
+ renderCertificatesList();
2207
+ });
2208
+
2209
+ $("#opcua-cert-files").off("click", ".opcua-cert-item").on("click", ".opcua-cert-item", function () {
2210
+ $("#opcua-cert-files .opcua-cert-item").removeClass("is-selected");
2211
+ $(this).addClass("is-selected");
2212
+ selectedCertName = $(this).attr("data-name");
2213
+ showCertificateDetails();
2214
+ });
2215
+
2216
+ $("#opcua-cert-move-btn").off("click").on("click", function (event) {
2217
+ event.preventDefault();
2218
+ moveCertificate();
2219
+ });
2220
+
1527
2221
  $("#node-input-tree-search").off("input").on("input", debounce(function () {
1528
2222
  treeSearchValue = $(this).val(); treeSearchTerm = normalizeSearchTerm(treeSearchValue);
1529
2223
  $("#node-input-tree-search-clear").toggle(!!treeSearchTerm); renderTree();
@@ -1546,6 +2240,10 @@
1546
2240
  closeAuthModal();
1547
2241
  return;
1548
2242
  }
2243
+ if ($("#node-input-cert-modal").is(":visible")) {
2244
+ closeCertModal();
2245
+ return;
2246
+ }
1549
2247
  closeTreeModal();
1550
2248
  });
1551
2249