@vitormnm/node-red-simple-opcua 1.4.2 → 1.4.3
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 +5 -0
- package/client/icons/opcua.svg +132 -132
- package/client/lib/opcua-client-browser.js +330 -330
- package/client/lib/opcua-client-method-service.js +88 -88
- package/client/lib/opcua-client-read-service.js +15 -15
- package/client/lib/opcua-client-subscription-id-service.js +24 -24
- package/client/lib/opcua-client-subscription-service.js +170 -170
- package/client/lib/opcua-client-write-service.js +146 -146
- package/client/opcua-client-config.html +80 -80
- package/client/opcua-client.html +140 -140
- package/client/view/opcua-client.js +1140 -1140
- package/icons/opcua.svg +132 -132
- package/icons/opcua2.svg +132 -132
- package/package.json +42 -42
- package/resources/bmc-button.svg +22 -0
- package/server/icons/opcua.svg +132 -132
- package/server/lib/opcua-address-space-alarm.js +341 -341
- package/server/lib/opcua-address-space-builder.js +1484 -1484
- package/server/lib/opcua-config.js +546 -546
- package/server/lib/opcua-constants.js +109 -109
- package/server/lib/opcua-server-events-child.js +139 -139
- package/server/lib/opcua-server-runtime-child.js +819 -819
- package/server/lib/opcua-server-runtime.js +311 -311
- package/server/lib/opcua-server-status-child.js +187 -187
- package/server/lib/server-node-utils.js +16 -16
- package/server/opcua-server-io.html +346 -346
- package/server/opcua-server-io.js +496 -496
- package/server/opcua-server-registry.js +270 -270
- package/server/opcua-server.css +265 -265
- package/server/opcua-server.html +1643 -1643
|
@@ -1,1141 +1,1141 @@
|
|
|
1
|
-
(function () {
|
|
2
|
-
var selectedItemsState = [];
|
|
3
|
-
var selectedNodeIdSet = {};
|
|
4
|
-
var browseState = null;
|
|
5
|
-
var expansionState = {};
|
|
6
|
-
var browseSearchValue = "";
|
|
7
|
-
var browseSearchTerm = "";
|
|
8
|
-
var contextMenuPath = "";
|
|
9
|
-
var browseSelectedPath = "";
|
|
10
|
-
var renderPending = false;
|
|
11
|
-
|
|
12
|
-
function debounce(fn, delay) {
|
|
13
|
-
var timer;
|
|
14
|
-
return function () {
|
|
15
|
-
var ctx = this, args = arguments;
|
|
16
|
-
clearTimeout(timer);
|
|
17
|
-
timer = setTimeout(function () { fn.apply(ctx, args); }, delay);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function rebuildNodeIdIndex() {
|
|
22
|
-
selectedNodeIdSet = {};
|
|
23
|
-
selectedItemsState.forEach(function (item, i) {
|
|
24
|
-
if (item.nodeID) selectedNodeIdSet[item.nodeID] = i;
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function openBrowseModal() { $("#node-input-browse-modal").show(); $("body").addClass("opcua-tree-modal-open"); }
|
|
29
|
-
function closeBrowseModal() {
|
|
30
|
-
hideTreeContextMenu();
|
|
31
|
-
$("#node-input-browse-modal").hide();
|
|
32
|
-
$("body").removeClass("opcua-tree-modal-open");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function getBrowseCacheKey() {
|
|
36
|
-
var connectionId = $("#node-input-connection").val() || "";
|
|
37
|
-
if (!connectionId) return "";
|
|
38
|
-
return "opcua-client-browse-cache:" + connectionId;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function saveBrowseSession() {
|
|
42
|
-
var key = getBrowseCacheKey();
|
|
43
|
-
if (!key || !window.sessionStorage) return;
|
|
44
|
-
try {
|
|
45
|
-
sessionStorage.setItem(key, JSON.stringify({
|
|
46
|
-
browseState: browseState,
|
|
47
|
-
expansionState: expansionState
|
|
48
|
-
}));
|
|
49
|
-
} catch (error) { }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function loadBrowseSession() {
|
|
53
|
-
var key = getBrowseCacheKey();
|
|
54
|
-
if (!key || !window.sessionStorage) return false;
|
|
55
|
-
try {
|
|
56
|
-
var raw = sessionStorage.getItem(key);
|
|
57
|
-
if (!raw) return false;
|
|
58
|
-
var parsed = JSON.parse(raw);
|
|
59
|
-
if (!parsed || typeof parsed !== "object") return false;
|
|
60
|
-
browseState = parsed.browseState && typeof parsed.browseState === "object" ? parsed.browseState : null;
|
|
61
|
-
expansionState = parsed.expansionState && typeof parsed.expansionState === "object" ? parsed.expansionState : {};
|
|
62
|
-
return !!browseState;
|
|
63
|
-
} catch (error) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parseSelectedItems(rawValue) {
|
|
69
|
-
if (!rawValue) return [];
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
var parsed = JSON.parse(rawValue);
|
|
73
|
-
if (!Array.isArray(parsed)) return [];
|
|
74
|
-
|
|
75
|
-
return parsed
|
|
76
|
-
.filter(function (item) {
|
|
77
|
-
return item && typeof item === "object" && !Array.isArray(item);
|
|
78
|
-
})
|
|
79
|
-
.map(function (item) {
|
|
80
|
-
var res = {
|
|
81
|
-
name: typeof item.name === "string" ? item.name.trim() : "",
|
|
82
|
-
nodeID: typeof (item.nodeID || item.nodeId) === "string" ? String(item.nodeID || item.nodeId).trim() : "",
|
|
83
|
-
type: typeof (item.type || item.dataType) === "string" ? String(item.type || item.dataType).trim() : "",
|
|
84
|
-
nodeClass: typeof item.nodeClass === "string" ? item.nodeClass.trim() : "",
|
|
85
|
-
typeDefinition: typeof (item.typeDefinition || item.typeDefinitionName) === "string" ? String(item.typeDefinition || item.typeDefinitionName).trim() : "",
|
|
86
|
-
hasTypeDefinition: item.hasTypeDefinition && typeof item.hasTypeDefinition === "object" ? item.hasTypeDefinition : null,
|
|
87
|
-
valueProperty: typeof item.valueProperty === "string" && item.valueProperty.trim() ? item.valueProperty.trim() : "payload",
|
|
88
|
-
valuePropertyType: (item.valuePropertyType === "msg" || item.valuePropertyType === "flow" || item.valuePropertyType === "global") ? item.valuePropertyType : "msg"
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
if (item.objectId) res.objectId = item.objectId;
|
|
92
|
-
if (Array.isArray(item.inputs)) res.inputs = item.inputs;
|
|
93
|
-
if (Array.isArray(item.outputs)) res.outputs = item.outputs;
|
|
94
|
-
|
|
95
|
-
return res;
|
|
96
|
-
})
|
|
97
|
-
.filter(function (item) {
|
|
98
|
-
return !!item.nodeID;
|
|
99
|
-
});
|
|
100
|
-
} catch (error) {
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function serializeSelectedItems(items) {
|
|
106
|
-
return JSON.stringify(items.map(function (item) {
|
|
107
|
-
var result = {
|
|
108
|
-
name: item.name || item.nodeID,
|
|
109
|
-
nodeID: item.nodeID
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
if (item.type) result.type = item.type;
|
|
113
|
-
if (item.nodeClass) result.nodeClass = item.nodeClass;
|
|
114
|
-
if (item.typeDefinition) result.typeDefinition = item.typeDefinition;
|
|
115
|
-
if (item.hasTypeDefinition) result.hasTypeDefinition = item.hasTypeDefinition;
|
|
116
|
-
|
|
117
|
-
if (item.valueProperty && item.nodeClass !== "Method") result.valueProperty = item.valueProperty;
|
|
118
|
-
if (item.valuePropertyType && item.nodeClass !== "Method") result.valuePropertyType = item.valuePropertyType;
|
|
119
|
-
|
|
120
|
-
if (item.objectId) result.objectId = item.objectId;
|
|
121
|
-
if (item.inputs) result.inputs = item.inputs;
|
|
122
|
-
if (item.outputs) result.outputs = item.outputs;
|
|
123
|
-
|
|
124
|
-
return result;
|
|
125
|
-
}), null, 2);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function escapeHtml(value) {
|
|
129
|
-
return String(value || "").replace(/[&<>"']/g, function (char) {
|
|
130
|
-
return {
|
|
131
|
-
"&": "&",
|
|
132
|
-
"<": "<",
|
|
133
|
-
">": ">",
|
|
134
|
-
'"': """,
|
|
135
|
-
"'": "'"
|
|
136
|
-
}[char];
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function updateSelectedItemsField() {
|
|
141
|
-
$("#node-input-selectedItems").val(serializeSelectedItems(selectedItemsState));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function normalizeSearchTerm(value) {
|
|
145
|
-
return String(value || "").trim().toLowerCase();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function textForSearch(item) {
|
|
149
|
-
if (!item || typeof item !== "object") return "";
|
|
150
|
-
return [
|
|
151
|
-
item.name,
|
|
152
|
-
item.displayName,
|
|
153
|
-
item.browseName,
|
|
154
|
-
item.nodeID,
|
|
155
|
-
item.nodeClass,
|
|
156
|
-
item.dataType,
|
|
157
|
-
item.description
|
|
158
|
-
].filter(Boolean).join(" ").toLowerCase();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function nodeMatchesSearch(item, term) {
|
|
162
|
-
if (!term) return true;
|
|
163
|
-
return textForSearch(item).indexOf(term) >= 0;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function branchHasSearchMatch(item, term) {
|
|
167
|
-
if (nodeMatchesSearch(item, term)) return true;
|
|
168
|
-
if (!item || !Array.isArray(item.browse)) return false;
|
|
169
|
-
for (var i = 0; i < item.browse.length; i += 1) {
|
|
170
|
-
if (branchHasSearchMatch(item.browse[i], term)) return true;
|
|
171
|
-
}
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function renderSelectedItems() {
|
|
176
|
-
var container = $("#node-input-selected-tags");
|
|
177
|
-
if (!container.length) return;
|
|
178
|
-
container.empty();
|
|
179
|
-
|
|
180
|
-
if (!selectedItemsState.length) {
|
|
181
|
-
container.append('<div class="opcua-tree-empty">No items selected.</div>');
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
var writeMode = $("#node-input-mode").val() === "write";
|
|
186
|
-
|
|
187
|
-
selectedItemsState.forEach(function (item, index) {
|
|
188
|
-
if (item.nodeClass === "Method") {
|
|
189
|
-
// Renderiza o item do tipo Method estruturado na mesma lista
|
|
190
|
-
var chip = $('<div class="opcua-client-method-chip" style="margin-bottom:12px; border: 1px solid #ccc; padding: 10px; background: #fff; border-radius: 4px;"></div>');
|
|
191
|
-
var header = $('<div class="opcua-client-method-header" style="display:flex; align-items:center; margin-bottom:8px;"></div>');
|
|
192
|
-
header.append('<span class="opcua-tree-icon" style="margin-right:6px;"><i class="fa fa-cog"></i></span>');
|
|
193
|
-
header.append('<span class="opcua-client-method-title" style="font-weight:bold; flex-grow:1;">' + escapeHtml(item.name || item.nodeID) + '</span>');
|
|
194
|
-
|
|
195
|
-
var nodeLabel = escapeHtml(item.nodeID);
|
|
196
|
-
if (item.objectId) nodeLabel += ' · <span style="color:#aaa;font-weight:400;">obj: ' + escapeHtml(item.objectId) + '</span>';
|
|
197
|
-
header.append('<span class="opcua-client-nodeid-label" style="font-size:11px; color:#666; margin-right:10px;">' + nodeLabel + '</span>');
|
|
198
|
-
header.append('<div class="opcua-tree-actions" style="margin:0;"><a href="#" class="editor-button editor-button-small opcua-method-remove" data-mindex="' + index + '"><i class="fa fa-trash"></i></a></div>');
|
|
199
|
-
chip.append(header);
|
|
200
|
-
|
|
201
|
-
chip.append('<div class="opcua-client-method-section-label" style="font-size:12px; font-weight:bold; margin-top:6px; margin-bottom:4px;"><i class="fa fa-arrow-right" style="font-size:10px;margin-right:3px;"></i>Input arguments</div>');
|
|
202
|
-
|
|
203
|
-
if (!item.inputs || !item.inputs.length) {
|
|
204
|
-
chip.append('<div style="font-size:11px;color:#aaa;padding:1px 0 3px 26px;">No input arguments</div>');
|
|
205
|
-
} else {
|
|
206
|
-
item.inputs.forEach(function (inp, iIndex) {
|
|
207
|
-
var propId = "opcua-method-inp-prop-" + index + "-" + iIndex;
|
|
208
|
-
var typeId = "opcua-method-inp-type-" + index + "-" + iIndex;
|
|
209
|
-
var type = (inp.valuePropertyType === "flow" || inp.valuePropertyType === "global") ? inp.valuePropertyType : "msg";
|
|
210
|
-
var prop = inp.valueProperty || "payload";
|
|
211
|
-
|
|
212
|
-
var row = $('<div class="opcua-client-tag-chip opcua-client-method-inp-chip" style="margin-bottom:4px;"></div>');
|
|
213
|
-
row.append('<div class="opcua-tree-icon"><i class="fa fa-tag"></i></div>');
|
|
214
|
-
row.append('<div class="opcua-tree-title" title="' + escapeHtml(inp.dataType || "") + '">'
|
|
215
|
-
+ escapeHtml(inp.name)
|
|
216
|
-
+ '<span style="color:#aaa;font-size:11px;font-weight:400;margin-left:4px;">(' + escapeHtml(inp.dataType || "?") + ')</span>'
|
|
217
|
-
+ '</div>');
|
|
218
|
-
row.append('<div class="opcua-client-tag-write">'
|
|
219
|
-
+ '<input type="text" class="opcua-method-inp-prop" id="' + propId + '" data-mindex="' + index + '" data-iindex="' + iIndex + '" value="' + escapeHtml(prop) + '" placeholder="payload">'
|
|
220
|
-
+ '<input type="hidden" class="opcua-method-inp-type" id="' + typeId + '" data-mindex="' + index + '" data-iindex="' + iIndex + '" value="' + escapeHtml(type) + '">'
|
|
221
|
-
+ '</div>');
|
|
222
|
-
row.append('<div class="opcua-client-tag-right">'
|
|
223
|
-
+ '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-method-remove-input" data-mindex="' + index + '" data-iindex="' + iIndex + '"><i class="fa fa-times"></i></a></div>'
|
|
224
|
-
+ '</div>');
|
|
225
|
-
chip.append(row);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
chip.append('<div class="opcua-method-section-row" style="margin-top:6px;"><a href="#" class="editor-button editor-button-small opcua-method-add-input" data-mindex="' + index + '"><i class="fa fa-plus"></i> Add input</a></div>');
|
|
230
|
-
|
|
231
|
-
var outputs = item.outputs || [];
|
|
232
|
-
chip.append('<div class="opcua-client-method-section-label" style="font-size:12px; font-weight:bold; margin-top:10px; margin-bottom:4px;"><i class="fa fa-arrow-left" style="font-size:10px;margin-right:3px;"></i>Output arguments</div>');
|
|
233
|
-
|
|
234
|
-
if (!outputs.length) {
|
|
235
|
-
chip.append('<div style="font-size:11px;color:#aaa;padding:1px 0 3px 26px;">No output arguments</div>');
|
|
236
|
-
} else {
|
|
237
|
-
outputs.forEach(function (out) {
|
|
238
|
-
var row = $('<div class="opcua-client-tag-chip" style="margin-bottom:4px;background:#f3f3f3;border-color:#e8e8e8;"></div>');
|
|
239
|
-
row.append('<div class="opcua-tree-icon" style="color:#bbb;"><i class="fa fa-tag"></i></div>');
|
|
240
|
-
row.append('<div class="opcua-tree-title" style="color:#888;" title="' + escapeHtml(out.dataType || "") + '">'
|
|
241
|
-
+ escapeHtml(out.name)
|
|
242
|
-
+ '<span style="color:#bbb;font-size:11px;font-weight:400;margin-left:4px;">(' + escapeHtml(out.dataType || "?") + ')</span>'
|
|
243
|
-
+ '</div>');
|
|
244
|
-
row.append('<div class="opcua-client-tag-right"><span style="font-size:11px;color:#bbb;font-style:italic;">read-only</span></div>');
|
|
245
|
-
chip.append(row);
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
container.append(chip);
|
|
249
|
-
} else {
|
|
250
|
-
// Renderiza as Tags / Variáveis normais
|
|
251
|
-
var row = $('<div class="opcua-client-tag-chip" style="margin-bottom:4px;"></div>');
|
|
252
|
-
var icon = browseIconFor(item);
|
|
253
|
-
row.append('<div class="opcua-tree-icon"><i class="fa ' + icon + '"></i></div>');
|
|
254
|
-
row.append('<div class="opcua-tree-title">' + escapeHtml(item.name || item.nodeID) + '</div>');
|
|
255
|
-
if (writeMode) {
|
|
256
|
-
var type = (item.valuePropertyType === "flow" || item.valuePropertyType === "global") ? item.valuePropertyType : "msg";
|
|
257
|
-
var prop = item.valueProperty || "payload";
|
|
258
|
-
row.append('<div class="opcua-client-tag-write">'
|
|
259
|
-
+ '<input type="text" class="opcua-client-item-value-prop" id="opcua-client-item-value-prop-' + index + '" data-index="' + index + '" value="' + escapeHtml(prop) + '" placeholder="payload">'
|
|
260
|
-
+ '<input type="hidden" class="opcua-client-item-value-type" id="opcua-client-item-value-type-' + index + '" data-index="' + index + '" value="' + escapeHtml(type) + '">'
|
|
261
|
-
+ "</div>");
|
|
262
|
-
}
|
|
263
|
-
row.append('<div class="opcua-client-tag-right">'
|
|
264
|
-
+ '<div class="opcua-client-nodeid-label">' + escapeHtml(item.nodeID) + '</div>'
|
|
265
|
-
+ '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-remove-tag" data-index="' + index + '"><i class="fa fa-trash"></i></a></div>'
|
|
266
|
-
+ "</div>");
|
|
267
|
-
container.append(row);
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
initializeSelectedItemTypedInputs();
|
|
272
|
-
|
|
273
|
-
// Inicializa dinamicamente os typedInputs dos parâmetros dos Métodos inseridos na lista unificada
|
|
274
|
-
$(".opcua-method-inp-prop").each(function () {
|
|
275
|
-
var input = $(this);
|
|
276
|
-
if (input.data("typedInputInitialized")) {
|
|
277
|
-
input.typedInput("types", ["msg", "flow", "global"]);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
var mindex = input.attr("data-mindex");
|
|
281
|
-
var iindex = input.attr("data-iindex");
|
|
282
|
-
var typeField = "#opcua-method-inp-type-" + mindex + "-" + iindex;
|
|
283
|
-
input.typedInput({ type: $(typeField).val() || "msg", types: ["msg", "flow", "global"], typeField: typeField });
|
|
284
|
-
input.data("typedInputInitialized", true);
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function initializeSelectedItemTypedInputs() {
|
|
289
|
-
$(".opcua-client-item-value-prop").each(function () {
|
|
290
|
-
var input = $(this);
|
|
291
|
-
var index = Number(input.attr("data-index"));
|
|
292
|
-
var typeField = "#opcua-client-item-value-type-" + index;
|
|
293
|
-
if (input.data("typedInputInitialized")) {
|
|
294
|
-
input.typedInput("types", ["msg", "flow", "global"]);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
input.typedInput({
|
|
299
|
-
type: $(typeField).val() || "msg",
|
|
300
|
-
types: ["msg", "flow", "global"],
|
|
301
|
-
typeField: typeField
|
|
302
|
-
});
|
|
303
|
-
input.data("typedInputInitialized", true);
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function syncSelectedItems() {
|
|
308
|
-
rebuildNodeIdIndex();
|
|
309
|
-
updateSelectedItemsField();
|
|
310
|
-
renderSelectedItems();
|
|
311
|
-
renderBrowseTree();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function isExpanded(path, defaultValue) {
|
|
315
|
-
if (expansionState[path] === undefined) {
|
|
316
|
-
expansionState[path] = !!defaultValue;
|
|
317
|
-
}
|
|
318
|
-
return expansionState[path];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function selectedIndexByNodeId(nodeId) {
|
|
322
|
-
var idx = selectedNodeIdSet[nodeId];
|
|
323
|
-
return (idx !== undefined) ? idx : -1;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function canExpand(item) {
|
|
327
|
-
return item && item.nodeClass !== "Variable"
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function isVariable(item) {
|
|
331
|
-
return String(item && item.nodeClass || "").toLowerCase() === "variable";
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function nodeIdOf(item) {
|
|
335
|
-
return item && (item.nodeID || item.nodeId) ? String(item.nodeID || item.nodeId) : "";
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function loadBrowse(nodeId) {
|
|
339
|
-
var connectionId = $("#node-input-connection").val();
|
|
340
|
-
if (!connectionId) {
|
|
341
|
-
RED.notify("Select an OPC UA connection before browsing.", "warning");
|
|
342
|
-
return $.Deferred().reject().promise();
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return $.getJSON("opcua-client-config/" + encodeURIComponent(connectionId) + "/browse", {
|
|
346
|
-
nodeId: nodeId || "i=84"
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function renderBrowseTree() {
|
|
351
|
-
if (renderPending) return;
|
|
352
|
-
renderPending = true;
|
|
353
|
-
setTimeout(function () {
|
|
354
|
-
renderPending = false;
|
|
355
|
-
_doRenderBrowseTree();
|
|
356
|
-
}, 0);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function _doRenderBrowseTree() {
|
|
360
|
-
var container = $("#node-input-browse-tree");
|
|
361
|
-
var frag = document.createDocumentFragment();
|
|
362
|
-
|
|
363
|
-
if (!browseState) {
|
|
364
|
-
var empty = document.createElement("div");
|
|
365
|
-
empty.className = "opcua-tree-empty";
|
|
366
|
-
empty.textContent = "Click Browse to load the server tree.";
|
|
367
|
-
frag.appendChild(empty);
|
|
368
|
-
container[0].innerHTML = "";
|
|
369
|
-
container[0].appendChild(frag);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (browseSearchTerm) {
|
|
374
|
-
if (!branchHasSearchMatch(browseState, browseSearchTerm)) {
|
|
375
|
-
var noMatch = document.createElement("div");
|
|
376
|
-
noMatch.className = "opcua-tree-empty";
|
|
377
|
-
noMatch.textContent = "No items found in the already explored items.";
|
|
378
|
-
frag.appendChild(noMatch);
|
|
379
|
-
container[0].innerHTML = "";
|
|
380
|
-
container[0].appendChild(frag);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
renderBrowseRootFiltered(browseState, "root", 0, frag, browseSearchTerm);
|
|
384
|
-
container[0].innerHTML = "";
|
|
385
|
-
container[0].appendChild(frag);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
renderBrowseRoot(browseState, "root", 0, frag);
|
|
390
|
-
container[0].innerHTML = "";
|
|
391
|
-
container[0].appendChild(frag);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function isFolderNode(item) {
|
|
395
|
-
if (!item) return false;
|
|
396
|
-
var nodeClass = String(item.nodeClass || "");
|
|
397
|
-
var typeDefinition = String(item.typeDefinition || item.typeDefinitionName || "").toLowerCase();
|
|
398
|
-
var hasTypeDefinitionBrowseName = String(
|
|
399
|
-
item.hasTypeDefinition && item.hasTypeDefinition.browseName || ""
|
|
400
|
-
).toLowerCase();
|
|
401
|
-
var explicitType = String(item.type || item.kind || "").toLowerCase();
|
|
402
|
-
if (nodeClass === "Folder") return true;
|
|
403
|
-
if (hasTypeDefinitionBrowseName === "foldertype") return true;
|
|
404
|
-
if (typeDefinition.indexOf("folder") >= 0) return true;
|
|
405
|
-
if (explicitType === "folder") return true;
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function browseIconFor(item) {
|
|
410
|
-
var nodeClass = String((item && item.nodeClass) || "");
|
|
411
|
-
if (isFolderNode(item)) return "fa-folder";
|
|
412
|
-
if (nodeClass === "Object") return "fa-cube";
|
|
413
|
-
if (nodeClass === "Method") return "fa-cog";
|
|
414
|
-
if (nodeClass === "Variable") return "fa-tag";
|
|
415
|
-
if (nodeClass === "ObjectType") return "fa-cubes";
|
|
416
|
-
if (nodeClass === "View") return "fa-eye";
|
|
417
|
-
if (nodeClass === "DataType") return "fa-database";
|
|
418
|
-
if (nodeClass === "ReferenceType") return "fa-random";
|
|
419
|
-
return "fa-tag";
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function makeEl(tag, className, html) {
|
|
423
|
-
var el = document.createElement(tag);
|
|
424
|
-
if (className) el.className = className;
|
|
425
|
-
if (html !== undefined) el.innerHTML = html;
|
|
426
|
-
return el;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function makeTreeRow(path, extraClass) {
|
|
430
|
-
var row = document.createElement("div");
|
|
431
|
-
row.className = "opcua-tree-row" + (extraClass ? " " + extraClass : "");
|
|
432
|
-
row.setAttribute("data-path", path);
|
|
433
|
-
return row;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function renderBrowseRoot(root, path, depth, frag) {
|
|
437
|
-
var expanded = isExpanded(path, true);
|
|
438
|
-
var row = makeTreeRow(path);
|
|
439
|
-
row.innerHTML = '<span class="opcua-tree-indent"></span>'
|
|
440
|
-
+ '<span class="opcua-tree-twisty opcua-client-toggle-tree" data-path="' + escapeHtml(path) + '">'
|
|
441
|
-
+ (expanded ? '<i class="fa fa-caret-down"></i>' : '<i class="fa fa-caret-right"></i>') + '</span>'
|
|
442
|
-
+ '<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>'
|
|
443
|
-
+ '<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>'
|
|
444
|
-
+ '<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>';
|
|
445
|
-
frag.appendChild(row);
|
|
446
|
-
|
|
447
|
-
if (!expanded) return;
|
|
448
|
-
|
|
449
|
-
if (!Array.isArray(root.browse) || !root.browse.length) {
|
|
450
|
-
frag.appendChild(makeEl("div", "opcua-tree-empty", "No items found.."));
|
|
451
|
-
} else {
|
|
452
|
-
root.browse.forEach(function (item, index) {
|
|
453
|
-
renderBrowseItem(item, path + ".browse." + index, depth + 1, frag);
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function renderBrowseItem(item, path, depth, frag) {
|
|
459
|
-
var expanded = isExpanded(path, false);
|
|
460
|
-
var nodeId = nodeIdOf(item);
|
|
461
|
-
var selectedIndex = selectedIndexByNodeId(nodeId);
|
|
462
|
-
var hasChildren = canExpand(item);
|
|
463
|
-
var row = makeTreeRow(path, selectedIndex >= 0 ? "is-selected" : "");
|
|
464
|
-
|
|
465
|
-
var indents = "";
|
|
466
|
-
for (var i = 0; i < depth; i += 1) indents += '<span class="opcua-tree-indent"></span>';
|
|
467
|
-
|
|
468
|
-
var twisty = '<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + escapeHtml(path) + '">'
|
|
469
|
-
+ (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>';
|
|
470
|
-
|
|
471
|
-
var actions = nodeId
|
|
472
|
-
? '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + escapeHtml(path) + '"><i class="fa ' + (selectedIndex >= 0 ? 'fa-minus' : 'fa-plus') + '"></i> ' + (selectedIndex >= 0 ? 'Remove' : 'Add') + '</a></div>'
|
|
473
|
-
: '';
|
|
474
|
-
|
|
475
|
-
row.innerHTML = indents + twisty
|
|
476
|
-
+ '<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>'
|
|
477
|
-
+ '<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>'
|
|
478
|
-
+ '<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>'
|
|
479
|
-
+ '<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>'
|
|
480
|
-
+ actions;
|
|
481
|
-
frag.appendChild(row);
|
|
482
|
-
|
|
483
|
-
if (item.description) {
|
|
484
|
-
var desc = makeEl("div", "opcua-client-description");
|
|
485
|
-
desc.style.padding = "0 10px 8px " + String((depth + 2) * 14) + "px";
|
|
486
|
-
desc.textContent = item.description;
|
|
487
|
-
frag.appendChild(desc);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (expanded && hasChildren) {
|
|
491
|
-
if (Array.isArray(item.browse)) {
|
|
492
|
-
if (!item.browse.length) {
|
|
493
|
-
frag.appendChild(makeEl("div", "opcua-tree-empty", "No children found.."));
|
|
494
|
-
} else {
|
|
495
|
-
item.browse.forEach(function (child, index) {
|
|
496
|
-
renderBrowseItem(child, path + ".browse." + index, depth + 1, frag);
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
frag.appendChild(makeEl("div", "opcua-tree-empty", "Searching for items..."));
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function renderBrowseRootFiltered(root, path, depth, frag, term) {
|
|
506
|
-
var row = makeTreeRow(path);
|
|
507
|
-
row.innerHTML = '<span class="opcua-tree-indent"></span>'
|
|
508
|
-
+ '<span class="opcua-tree-twisty"><i class="fa fa-caret-down"></i></span>'
|
|
509
|
-
+ '<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>'
|
|
510
|
-
+ '<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>'
|
|
511
|
-
+ '<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>';
|
|
512
|
-
frag.appendChild(row);
|
|
513
|
-
|
|
514
|
-
if (!Array.isArray(root.browse) || !root.browse.length) {
|
|
515
|
-
frag.appendChild(makeEl("div", "opcua-tree-empty", "Nenhum item encontrado."));
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
root.browse.forEach(function (item, index) {
|
|
520
|
-
if (branchHasSearchMatch(item, term)) {
|
|
521
|
-
renderBrowseItemFiltered(item, path + ".browse." + index, depth + 1, frag, term, false);
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function renderBrowseItemFiltered(item, path, depth, frag, term, ancestorMatched) {
|
|
527
|
-
if (!branchHasSearchMatch(item, term)) return;
|
|
528
|
-
|
|
529
|
-
var nodeId = nodeIdOf(item);
|
|
530
|
-
var selectedIndex = selectedIndexByNodeId(nodeId);
|
|
531
|
-
var hasChildren = canExpand(item);
|
|
532
|
-
var subtreeVisible = !!ancestorMatched || nodeMatchesSearch(item, term);
|
|
533
|
-
var hasMatchingLoadedChild = hasChildren && Array.isArray(item.browse) && item.browse.some(function (child) {
|
|
534
|
-
return branchHasSearchMatch(child, term);
|
|
535
|
-
});
|
|
536
|
-
var hasExplicitExpansion = expansionState[path] !== undefined;
|
|
537
|
-
var expanded = hasChildren && (hasExplicitExpansion
|
|
538
|
-
? !!expansionState[path]
|
|
539
|
-
: ((subtreeVisible && Array.isArray(item.browse)) || hasMatchingLoadedChild));
|
|
540
|
-
|
|
541
|
-
var row = makeTreeRow(path, selectedIndex >= 0 ? "is-selected" : "");
|
|
542
|
-
|
|
543
|
-
var indents = "";
|
|
544
|
-
for (var i = 0; i < depth; i += 1) indents += '<span class="opcua-tree-indent"></span>';
|
|
545
|
-
|
|
546
|
-
var twisty = '<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + escapeHtml(path) + '">'
|
|
547
|
-
+ (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>';
|
|
548
|
-
|
|
549
|
-
var actions = nodeId
|
|
550
|
-
? '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + escapeHtml(path) + '"><i class="fa ' + (selectedIndex >= 0 ? 'fa-minus' : 'fa-plus') + '"></i> ' + (selectedIndex >= 0 ? 'Remove' : 'Add') + '</a></div>'
|
|
551
|
-
: '';
|
|
552
|
-
|
|
553
|
-
row.innerHTML = indents + twisty
|
|
554
|
-
+ '<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>'
|
|
555
|
-
+ '<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>'
|
|
556
|
-
+ '<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>'
|
|
557
|
-
+ '<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>'
|
|
558
|
-
+ actions;
|
|
559
|
-
frag.appendChild(row);
|
|
560
|
-
|
|
561
|
-
if (item.description) {
|
|
562
|
-
var desc = makeEl("div", "opcua-client-description");
|
|
563
|
-
desc.style.padding = "0 10px 8px " + String((depth + 2) * 14) + "px";
|
|
564
|
-
desc.textContent = item.description;
|
|
565
|
-
frag.appendChild(desc);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (expanded && hasChildren) {
|
|
569
|
-
if (Array.isArray(item.browse)) {
|
|
570
|
-
if (!item.browse.length) {
|
|
571
|
-
frag.appendChild(makeEl("div", "opcua-tree-empty", "Nenhum filho encontrado."));
|
|
572
|
-
} else {
|
|
573
|
-
item.browse.forEach(function (child, index) {
|
|
574
|
-
if (subtreeVisible || branchHasSearchMatch(child, term)) {
|
|
575
|
-
renderBrowseItemFiltered(child, path + ".browse." + index, depth + 1, frag, term, subtreeVisible);
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
} else {
|
|
580
|
-
frag.appendChild(makeEl("div", "opcua-tree-empty", "Expandindo..."));
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function getItemAtPath(path) {
|
|
586
|
-
var tokens = String(path || "").split(".");
|
|
587
|
-
var current = browseState;
|
|
588
|
-
|
|
589
|
-
for (var index = 0; index < tokens.length; index += 1) {
|
|
590
|
-
var token = tokens[index];
|
|
591
|
-
if (!token || token === "root") continue;
|
|
592
|
-
if (!current) return null;
|
|
593
|
-
if (/^\d+$/.test(token)) {
|
|
594
|
-
current = current[Number(token)];
|
|
595
|
-
} else {
|
|
596
|
-
current = current[token];
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
return current || null;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function addSelectedItem(item) {
|
|
604
|
-
var normalized = {
|
|
605
|
-
name: item.displayName || item.browseName || item.name || item.nodeID,
|
|
606
|
-
nodeID: item.nodeID || item.nodeId,
|
|
607
|
-
type: item.dataType || item.type || "",
|
|
608
|
-
nodeClass: item.nodeClass || "",
|
|
609
|
-
typeDefinition: item.typeDefinition || item.typeDefinitionName || "",
|
|
610
|
-
hasTypeDefinition: item.hasTypeDefinition || null,
|
|
611
|
-
valueProperty: item.valueProperty || "payload",
|
|
612
|
-
valuePropertyType: item.valuePropertyType || "msg"
|
|
613
|
-
};
|
|
614
|
-
var currentIndex = selectedIndexByNodeId(normalized.nodeID);
|
|
615
|
-
|
|
616
|
-
if (currentIndex >= 0) {
|
|
617
|
-
selectedItemsState[currentIndex] = normalized;
|
|
618
|
-
} else {
|
|
619
|
-
selectedItemsState.push(normalized);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
syncSelectedItems();
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function removeSelectedItemByIndex(index) {
|
|
626
|
-
selectedItemsState.splice(index, 1);
|
|
627
|
-
syncSelectedItems();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function toggleSelectedNode(path) {
|
|
631
|
-
var item = getItemAtPath(path);
|
|
632
|
-
var nodeId = nodeIdOf(item);
|
|
633
|
-
if (!item || !nodeId) return;
|
|
634
|
-
|
|
635
|
-
var currentIndex = selectedIndexByNodeId(nodeId);
|
|
636
|
-
if (currentIndex >= 0) {
|
|
637
|
-
selectedItemsState.splice(currentIndex, 1);
|
|
638
|
-
} else {
|
|
639
|
-
addSelectedItem(item);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
syncSelectedItems();
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function refreshBrowseRoot() {
|
|
647
|
-
var rootNodeId = "i=84";
|
|
648
|
-
var container = $("#node-input-browse-tree");
|
|
649
|
-
container.html('<div class="opcua-tree-empty">Carregando...</div>');
|
|
650
|
-
|
|
651
|
-
loadBrowse(rootNodeId).done(function (payload) {
|
|
652
|
-
browseState = payload;
|
|
653
|
-
expansionState = { root: true };
|
|
654
|
-
saveBrowseSession();
|
|
655
|
-
renderBrowseTree();
|
|
656
|
-
}).fail(function (xhr) {
|
|
657
|
-
var message = xhr && xhr.responseJSON && xhr.responseJSON.error
|
|
658
|
-
? xhr.responseJSON.error
|
|
659
|
-
: "Falha ao navegar no servidor OPC UA.";
|
|
660
|
-
browseState = null;
|
|
661
|
-
container.html('<div class="opcua-tree-empty">' + escapeHtml(message) + '</div>');
|
|
662
|
-
RED.notify(message, "error");
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function hideTreeContextMenu() {
|
|
667
|
-
contextMenuPath = "";
|
|
668
|
-
$("#node-input-browse-context-menu").hide();
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function showTreeContextMenu(x, y, path) {
|
|
672
|
-
var menu = $("#node-input-browse-context-menu");
|
|
673
|
-
var item = getItemAtPath(path);
|
|
674
|
-
contextMenuPath = path || "";
|
|
675
|
-
$("#node-input-browse-context-refresh").toggle(!!item && !isVariable(item));
|
|
676
|
-
$("#node-input-browse-context-copy-nodeid").toggle(!!nodeIdOf(item));
|
|
677
|
-
menu.css({ left: x + "px", top: y + "px" }).show();
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function copyNodeIdFromPath(path) {
|
|
681
|
-
var item = getItemAtPath(path);
|
|
682
|
-
var nodeId = nodeIdOf(item);
|
|
683
|
-
if (!nodeId) {
|
|
684
|
-
RED.notify("NodeID nao encontrado para o item selecionado.", "warning");
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
689
|
-
navigator.clipboard.writeText(nodeId).then(function () {
|
|
690
|
-
RED.notify("NodeID copiado.", "success");
|
|
691
|
-
}).catch(function () {
|
|
692
|
-
RED.notify("Falha ao copiar NodeID.", "error");
|
|
693
|
-
});
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
var input = $("<textarea readonly></textarea>").val(nodeId).css({
|
|
698
|
-
position: "fixed",
|
|
699
|
-
left: "-9999px",
|
|
700
|
-
top: "0"
|
|
701
|
-
});
|
|
702
|
-
$("body").append(input);
|
|
703
|
-
input[0].select();
|
|
704
|
-
try {
|
|
705
|
-
document.execCommand("copy");
|
|
706
|
-
RED.notify("NodeID copiado.", "success");
|
|
707
|
-
} catch (error) {
|
|
708
|
-
RED.notify("Falha ao copiar NodeID.", "error");
|
|
709
|
-
}
|
|
710
|
-
input.remove();
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function setBrowseSelectedPath(path) {
|
|
714
|
-
browseSelectedPath = path || "";
|
|
715
|
-
$(".opcua-tree-row").removeClass("is-selected");
|
|
716
|
-
if (browseSelectedPath) {
|
|
717
|
-
$('.opcua-tree-row[data-path="' + browseSelectedPath + '"]').addClass("is-selected");
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
function refreshNode(path) {
|
|
722
|
-
if (!path) return;
|
|
723
|
-
if (path === "root") {
|
|
724
|
-
refreshBrowseRoot();
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
var item = getItemAtPath(path);
|
|
729
|
-
if (!item || isVariable(item)) return;
|
|
730
|
-
|
|
731
|
-
item.browse = undefined;
|
|
732
|
-
expansionState[path] = true;
|
|
733
|
-
saveBrowseSession();
|
|
734
|
-
renderBrowseTree();
|
|
735
|
-
expandNode(path);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function expandNode(path) {
|
|
739
|
-
var item = getItemAtPath(path);
|
|
740
|
-
if (!item || !canExpand(item)) return;
|
|
741
|
-
|
|
742
|
-
if (isExpanded(path, false) && !Array.isArray(item.browse)) {
|
|
743
|
-
renderBrowseTree();
|
|
744
|
-
} else {
|
|
745
|
-
expansionState[path] = !isExpanded(path, false);
|
|
746
|
-
saveBrowseSession();
|
|
747
|
-
if (!expansionState[path]) {
|
|
748
|
-
renderBrowseTree();
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if (Array.isArray(item.browse)) {
|
|
754
|
-
renderBrowseTree();
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
renderBrowseTree();
|
|
759
|
-
var browseNodeId = item.nodeID || item.nodeId;
|
|
760
|
-
loadBrowse(browseNodeId).done(function (payload) {
|
|
761
|
-
item.browse = Array.isArray(payload.browse) ? payload.browse : [];
|
|
762
|
-
saveBrowseSession();
|
|
763
|
-
renderBrowseTree();
|
|
764
|
-
}).fail(function (xhr) {
|
|
765
|
-
expansionState[path] = false;
|
|
766
|
-
saveBrowseSession();
|
|
767
|
-
renderBrowseTree();
|
|
768
|
-
var message = xhr && xhr.responseJSON && xhr.responseJSON.error
|
|
769
|
-
? xhr.responseJSON.error
|
|
770
|
-
: "Falha ao expandir o node.";
|
|
771
|
-
RED.notify(message, "error");
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function methodNodeIdOf(item) {
|
|
776
|
-
return item && (item.nodeID || item.nodeId) ? String(item.nodeID || item.nodeId) : "";
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function addMethodFromTree(item, parentItem) {
|
|
780
|
-
var nodeID = methodNodeIdOf(item);
|
|
781
|
-
var objectId = parentItem ? methodNodeIdOf(parentItem) : "";
|
|
782
|
-
var methodName = item.displayName || item.browseName || item.name || nodeID;
|
|
783
|
-
|
|
784
|
-
for (var i = 0; i < selectedItemsState.length; i++) {
|
|
785
|
-
if (selectedItemsState[i].nodeID === nodeID && selectedItemsState[i].nodeClass === "Method") {
|
|
786
|
-
RED.notify("Method already added.", "warning");
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
var connectionId = $("#node-input-connection").val();
|
|
792
|
-
|
|
793
|
-
$.getJSON(
|
|
794
|
-
"opcua-client-config/" + encodeURIComponent(connectionId) + "/browse",
|
|
795
|
-
{ nodeId: nodeID }
|
|
796
|
-
)
|
|
797
|
-
.done(function (payload) {
|
|
798
|
-
var browseItems = Array.isArray(payload.browse) ? payload.browse : [];
|
|
799
|
-
var inputArgs = [];
|
|
800
|
-
var outputArgs = [];
|
|
801
|
-
|
|
802
|
-
browseItems.forEach(function (child) {
|
|
803
|
-
var name = String(child.displayName || child.browseName || "").toLowerCase();
|
|
804
|
-
|
|
805
|
-
if ((name === "inputarguments" || name === "inputargument") && Array.isArray(child.value)) {
|
|
806
|
-
inputArgs = child.value;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if ((name === "outputarguments" || name === "outputargument") && Array.isArray(child.value)) {
|
|
810
|
-
outputArgs = child.value;
|
|
811
|
-
}
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
var inputs = inputArgs.map(function (arg, idx) {
|
|
815
|
-
return {
|
|
816
|
-
name: arg.name || ("arg" + idx),
|
|
817
|
-
dataType: opcuaDataTypeName(arg.dataType),
|
|
818
|
-
valueProperty: "payload",
|
|
819
|
-
valuePropertyType: "msg"
|
|
820
|
-
};
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
var outputs = outputArgs.map(function (arg, idx) {
|
|
824
|
-
return {
|
|
825
|
-
name: arg.name || ("out" + idx),
|
|
826
|
-
dataType: opcuaDataTypeName(arg.dataType)
|
|
827
|
-
};
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
pushMethodItem(nodeID, objectId, methodName, inputs, outputs);
|
|
831
|
-
})
|
|
832
|
-
.fail(function () {
|
|
833
|
-
pushMethodItem(nodeID, objectId, methodName, [], []);
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function opcuaDataTypeName(dataType) {
|
|
838
|
-
switch (String(dataType || "")) {
|
|
839
|
-
case "ns=0;i=1": return "Boolean";
|
|
840
|
-
case "ns=0;i=2": return "SByte";
|
|
841
|
-
case "ns=0;i=3": return "Byte";
|
|
842
|
-
case "ns=0;i=4": return "Int16";
|
|
843
|
-
case "ns=0;i=5": return "UInt16";
|
|
844
|
-
case "ns=0;i=6": return "Int32";
|
|
845
|
-
case "ns=0;i=7": return "UInt32";
|
|
846
|
-
case "ns=0;i=8": return "Int64";
|
|
847
|
-
case "ns=0;i=9": return "UInt64";
|
|
848
|
-
case "ns=0;i=10": return "Float";
|
|
849
|
-
case "ns=0;i=11": return "Double";
|
|
850
|
-
case "ns=0;i=12": return "String";
|
|
851
|
-
case "ns=0;i=13": return "DateTime";
|
|
852
|
-
case "ns=0;i=14": return "Guid";
|
|
853
|
-
case "ns=0;i=15": return "ByteString";
|
|
854
|
-
case "ns=0;i=16": return "XmlElement";
|
|
855
|
-
case "ns=0;i=17": return "NodeId";
|
|
856
|
-
case "ns=0;i=18": return "ExpandedNodeId";
|
|
857
|
-
case "ns=0;i=19": return "StatusCode";
|
|
858
|
-
case "ns=0;i=20": return "QualifiedName";
|
|
859
|
-
case "ns=0;i=21": return "LocalizedText";
|
|
860
|
-
case "ns=0;i=22": return "ExtensionObject";
|
|
861
|
-
case "ns=0;i=26": return "Number";
|
|
862
|
-
case "ns=0;i=27": return "Integer";
|
|
863
|
-
case "ns=0;i=28": return "UInteger";
|
|
864
|
-
default: return String(dataType || "");
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
function pushMethodItem(nodeID, objectId, name, inputs, outputs) {
|
|
869
|
-
selectedItemsState.push({
|
|
870
|
-
nodeID: nodeID,
|
|
871
|
-
objectId: objectId,
|
|
872
|
-
name: name,
|
|
873
|
-
inputs: inputs || [],
|
|
874
|
-
outputs: outputs || [],
|
|
875
|
-
nodeClass: "Method"
|
|
876
|
-
});
|
|
877
|
-
syncSelectedItems();
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function toggleModeFields() {
|
|
881
|
-
var mode = $("#node-input-mode").val();
|
|
882
|
-
|
|
883
|
-
var isSubscription = mode === "subscription" || mode === "events";
|
|
884
|
-
var supportsSelection = mode !== "getSubscriptionId";
|
|
885
|
-
|
|
886
|
-
$(".opcua-client-subscription-row").toggle(isSubscription);
|
|
887
|
-
$(".opcua-client-selection-row").toggle(supportsSelection);
|
|
888
|
-
|
|
889
|
-
// Esconde permanentemente a linha de métodos antiga caso ainda exista no DOM
|
|
890
|
-
$(".opcua-client-method-row").hide();
|
|
891
|
-
|
|
892
|
-
renderSelectedItems();
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
RED.nodes.registerType("opcua-client", {
|
|
896
|
-
category: "network",
|
|
897
|
-
color: "#d9edf7",
|
|
898
|
-
defaults: {
|
|
899
|
-
name: { value: "" },
|
|
900
|
-
connection: { value: "", type: "opcua-client-config", required: true },
|
|
901
|
-
mode: { value: "read", required: true },
|
|
902
|
-
selectedItems: {
|
|
903
|
-
value: "[]",
|
|
904
|
-
validate: function (value) {
|
|
905
|
-
try {
|
|
906
|
-
return Array.isArray(JSON.parse(value || "[]"));
|
|
907
|
-
} catch (error) {
|
|
908
|
-
return false;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
},
|
|
912
|
-
samplingInterval: {
|
|
913
|
-
value: 250,
|
|
914
|
-
validate: RED.validators.number()
|
|
915
|
-
},
|
|
916
|
-
publishingInterval: {
|
|
917
|
-
value: 250,
|
|
918
|
-
validate: RED.validators.number()
|
|
919
|
-
}
|
|
920
|
-
},
|
|
921
|
-
inputs: 1,
|
|
922
|
-
outputs: 1,
|
|
923
|
-
icon: "opcua.svg",
|
|
924
|
-
label: function () {
|
|
925
|
-
return this.name || "opcua-client";
|
|
926
|
-
},
|
|
927
|
-
oneditprepare: function () {
|
|
928
|
-
$("#node-input-selectedItems").typedInput({
|
|
929
|
-
type: "json",
|
|
930
|
-
types: ["json"]
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
selectedItemsState = parseSelectedItems(this.selectedItems);
|
|
934
|
-
rebuildNodeIdIndex();
|
|
935
|
-
|
|
936
|
-
browseState = null;
|
|
937
|
-
expansionState = {};
|
|
938
|
-
browseSelectedPath = "";
|
|
939
|
-
browseSearchValue = "";
|
|
940
|
-
browseSearchTerm = "";
|
|
941
|
-
$("#node-input-browse-search").val("");
|
|
942
|
-
$("#node-input-browse-search-clear").hide();
|
|
943
|
-
loadBrowseSession();
|
|
944
|
-
syncSelectedItems();
|
|
945
|
-
renderBrowseTree();
|
|
946
|
-
|
|
947
|
-
$("#node-input-mode").off("change").on("change", toggleModeFields);
|
|
948
|
-
$("#node-input-browse-root").off("click").on("click", function (event) {
|
|
949
|
-
event.preventDefault();
|
|
950
|
-
refreshBrowseRoot();
|
|
951
|
-
});
|
|
952
|
-
$("#node-input-open-browse-modal").off("click").on("click", function (event) {
|
|
953
|
-
event.preventDefault();
|
|
954
|
-
openBrowseModal();
|
|
955
|
-
});
|
|
956
|
-
$("#node-input-close-browse-modal").off("click").on("click", function (event) {
|
|
957
|
-
event.preventDefault();
|
|
958
|
-
closeBrowseModal();
|
|
959
|
-
});
|
|
960
|
-
$("#node-input-browse-modal").off("click").on("click", function (event) {
|
|
961
|
-
hideTreeContextMenu();
|
|
962
|
-
if (event.target === this) closeBrowseModal();
|
|
963
|
-
});
|
|
964
|
-
$("#node-input-browse-search").off("input").on("input", debounce(function () {
|
|
965
|
-
browseSearchValue = $(this).val();
|
|
966
|
-
browseSearchTerm = normalizeSearchTerm(browseSearchValue);
|
|
967
|
-
$("#node-input-browse-search-clear").toggle(!!browseSearchTerm);
|
|
968
|
-
renderBrowseTree();
|
|
969
|
-
}, 200));
|
|
970
|
-
$("#node-input-browse-search-clear").off("click").on("click", function (event) {
|
|
971
|
-
event.preventDefault();
|
|
972
|
-
browseSearchValue = "";
|
|
973
|
-
browseSearchTerm = "";
|
|
974
|
-
$("#node-input-browse-search").val("");
|
|
975
|
-
$(this).hide();
|
|
976
|
-
renderBrowseTree();
|
|
977
|
-
});
|
|
978
|
-
$("#node-input-connection").off("change.opcuaClientBrowse").on("change.opcuaClientBrowse", function () {
|
|
979
|
-
browseState = null;
|
|
980
|
-
expansionState = {};
|
|
981
|
-
browseSelectedPath = "";
|
|
982
|
-
loadBrowseSession();
|
|
983
|
-
renderBrowseTree();
|
|
984
|
-
});
|
|
985
|
-
$(document).off("keydown.opcuaClientBrowseModal").on("keydown.opcuaClientBrowseModal", function (event) {
|
|
986
|
-
hideTreeContextMenu();
|
|
987
|
-
if (event.key === "Escape") closeBrowseModal();
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
toggleModeFields();
|
|
991
|
-
},
|
|
992
|
-
oneditsave: function () {
|
|
993
|
-
updateSelectedItemsField();
|
|
994
|
-
saveBrowseSession();
|
|
995
|
-
closeBrowseModal();
|
|
996
|
-
$(document).off("keydown.opcuaClientBrowseModal");
|
|
997
|
-
},
|
|
998
|
-
oneditcancel: function () {
|
|
999
|
-
closeBrowseModal();
|
|
1000
|
-
$(document).off("keydown.opcuaClientBrowseModal");
|
|
1001
|
-
}
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
$(document).on("change", "#node-input-selectedItems", function () {
|
|
1005
|
-
selectedItemsState = parseSelectedItems($(this).val());
|
|
1006
|
-
renderSelectedItems();
|
|
1007
|
-
renderBrowseTree();
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
|
-
$(document).on("click", ".opcua-client-remove-tag", function (event) {
|
|
1011
|
-
event.preventDefault();
|
|
1012
|
-
removeSelectedItemByIndex(Number($(this).attr("data-index")));
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
$(document).on("click", ".opcua-client-toggle-tag", function (event) {
|
|
1016
|
-
event.preventDefault();
|
|
1017
|
-
var path = $(this).attr("data-path");
|
|
1018
|
-
|
|
1019
|
-
if ($("#node-input-mode").val() === "method") {
|
|
1020
|
-
var item = getItemAtPath(path);
|
|
1021
|
-
if (item && item.nodeClass === "Method") {
|
|
1022
|
-
var parentPath = path.split(".");
|
|
1023
|
-
parentPath.splice(parentPath.length - 2, 2);
|
|
1024
|
-
parentPath = parentPath.join(".");
|
|
1025
|
-
var parentItem = getItemAtPath(parentPath);
|
|
1026
|
-
addMethodFromTree(item, parentItem);
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
toggleSelectedNode(path);
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
$(document).on("click", ".opcua-client-toggle-tree", function (event) {
|
|
1034
|
-
event.preventDefault();
|
|
1035
|
-
expandNode($(this).attr("data-path"));
|
|
1036
|
-
});
|
|
1037
|
-
$(document).on("change", ".opcua-client-item-value-type", function () {
|
|
1038
|
-
var index = Number($(this).attr("data-index"));
|
|
1039
|
-
if (!selectedItemsState[index]) return;
|
|
1040
|
-
selectedItemsState[index].valuePropertyType = $(this).val();
|
|
1041
|
-
updateSelectedItemsField();
|
|
1042
|
-
});
|
|
1043
|
-
$(document).on("change input", ".opcua-client-item-value-prop", function () {
|
|
1044
|
-
var index = Number($(this).attr("data-index"));
|
|
1045
|
-
if (!selectedItemsState[index]) return;
|
|
1046
|
-
selectedItemsState[index].valueProperty = $(this).typedInput ? $(this).typedInput("value") : $(this).val();
|
|
1047
|
-
var typeField = $("#opcua-client-item-value-type-" + index);
|
|
1048
|
-
selectedItemsState[index].valuePropertyType = (typeField.val() || "msg");
|
|
1049
|
-
updateSelectedItemsField();
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
$(document).on("click", ".opcua-tree-row", function (event) {
|
|
1053
|
-
if ($(event.target).closest(".opcua-client-toggle-tree, .opcua-client-toggle-tag, .opcua-tree-actions, #node-input-browse-context-menu").length) {
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
setBrowseSelectedPath($(this).attr("data-path"));
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
$(document).on("contextmenu", ".opcua-tree-row", function (event) {
|
|
1060
|
-
var clickedPath = $(this).attr("data-path");
|
|
1061
|
-
if (clickedPath) {
|
|
1062
|
-
setBrowseSelectedPath(clickedPath);
|
|
1063
|
-
}
|
|
1064
|
-
var path = browseSelectedPath || clickedPath;
|
|
1065
|
-
var item = getItemAtPath(path);
|
|
1066
|
-
if (!item) {
|
|
1067
|
-
hideTreeContextMenu();
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
event.preventDefault();
|
|
1071
|
-
showTreeContextMenu(event.clientX, event.clientY, path);
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
$(document).on("click", "#node-input-browse-context-refresh", function (event) {
|
|
1075
|
-
event.preventDefault();
|
|
1076
|
-
var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
|
|
1077
|
-
var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
|
|
1078
|
-
hideTreeContextMenu();
|
|
1079
|
-
refreshNode(path);
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
$(document).on("click", "#node-input-browse-context-copy-nodeid", function (event) {
|
|
1083
|
-
event.preventDefault();
|
|
1084
|
-
var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
|
|
1085
|
-
var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
|
|
1086
|
-
hideTreeContextMenu();
|
|
1087
|
-
copyNodeIdFromPath(path);
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
$(document).on("click", function (event) {
|
|
1091
|
-
if (!$(event.target).closest("#node-input-browse-context-menu").length) {
|
|
1092
|
-
hideTreeContextMenu();
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
|
|
1096
|
-
// ── Method browse tree events ──────────────────────────────────────
|
|
1097
|
-
|
|
1098
|
-
$(document).on("click", ".opcua-method-remove", function (event) {
|
|
1099
|
-
event.preventDefault();
|
|
1100
|
-
var idx = Number($(this).attr("data-mindex"));
|
|
1101
|
-
selectedItemsState.splice(idx, 1);
|
|
1102
|
-
syncSelectedItems();
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
$(document).on("click", ".opcua-method-add-input", function (event) {
|
|
1106
|
-
event.preventDefault();
|
|
1107
|
-
var idx = Number($(this).attr("data-mindex"));
|
|
1108
|
-
if (!selectedItemsState[idx]) return;
|
|
1109
|
-
selectedItemsState[idx].inputs = selectedItemsState[idx].inputs || [];
|
|
1110
|
-
selectedItemsState[idx].inputs.push({ name: "arg" + selectedItemsState[idx].inputs.length, dataType: "String", valueProperty: "payload", valuePropertyType: "msg" });
|
|
1111
|
-
syncSelectedItems();
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
$(document).on("click", ".opcua-method-remove-input", function (event) {
|
|
1115
|
-
event.preventDefault();
|
|
1116
|
-
var mi = Number($(this).attr("data-mindex"));
|
|
1117
|
-
var ii = Number($(this).attr("data-iindex"));
|
|
1118
|
-
if (!selectedItemsState[mi]) return;
|
|
1119
|
-
selectedItemsState[mi].inputs.splice(ii, 1);
|
|
1120
|
-
syncSelectedItems();
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
$(document).on("change input", ".opcua-method-inp-prop", function () {
|
|
1124
|
-
var mi = Number($(this).attr("data-mindex"));
|
|
1125
|
-
var ii = Number($(this).attr("data-iindex"));
|
|
1126
|
-
if (!selectedItemsState[mi] || !selectedItemsState[mi].inputs[ii]) return;
|
|
1127
|
-
selectedItemsState[mi].inputs[ii].valueProperty = $(this).typedInput ? $(this).typedInput("value") : $(this).val();
|
|
1128
|
-
var typeField = $("#opcua-method-inp-type-" + mi + "-" + ii);
|
|
1129
|
-
selectedItemsState[mi].inputs[ii].valuePropertyType = typeField.val() || "msg";
|
|
1130
|
-
updateSelectedItemsField();
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
$(document).on("change", ".opcua-method-inp-type", function () {
|
|
1134
|
-
var mi = Number($(this).attr("data-mindex"));
|
|
1135
|
-
var ii = Number($(this).attr("data-iindex"));
|
|
1136
|
-
if (!selectedItemsState[mi] || !selectedItemsState[mi].inputs[ii]) return;
|
|
1137
|
-
selectedItemsState[mi].inputs[ii].valuePropertyType = $(this).val();
|
|
1138
|
-
updateSelectedItemsField();
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1
|
+
(function () {
|
|
2
|
+
var selectedItemsState = [];
|
|
3
|
+
var selectedNodeIdSet = {};
|
|
4
|
+
var browseState = null;
|
|
5
|
+
var expansionState = {};
|
|
6
|
+
var browseSearchValue = "";
|
|
7
|
+
var browseSearchTerm = "";
|
|
8
|
+
var contextMenuPath = "";
|
|
9
|
+
var browseSelectedPath = "";
|
|
10
|
+
var renderPending = false;
|
|
11
|
+
|
|
12
|
+
function debounce(fn, delay) {
|
|
13
|
+
var timer;
|
|
14
|
+
return function () {
|
|
15
|
+
var ctx = this, args = arguments;
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
timer = setTimeout(function () { fn.apply(ctx, args); }, delay);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rebuildNodeIdIndex() {
|
|
22
|
+
selectedNodeIdSet = {};
|
|
23
|
+
selectedItemsState.forEach(function (item, i) {
|
|
24
|
+
if (item.nodeID) selectedNodeIdSet[item.nodeID] = i;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function openBrowseModal() { $("#node-input-browse-modal").show(); $("body").addClass("opcua-tree-modal-open"); }
|
|
29
|
+
function closeBrowseModal() {
|
|
30
|
+
hideTreeContextMenu();
|
|
31
|
+
$("#node-input-browse-modal").hide();
|
|
32
|
+
$("body").removeClass("opcua-tree-modal-open");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getBrowseCacheKey() {
|
|
36
|
+
var connectionId = $("#node-input-connection").val() || "";
|
|
37
|
+
if (!connectionId) return "";
|
|
38
|
+
return "opcua-client-browse-cache:" + connectionId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function saveBrowseSession() {
|
|
42
|
+
var key = getBrowseCacheKey();
|
|
43
|
+
if (!key || !window.sessionStorage) return;
|
|
44
|
+
try {
|
|
45
|
+
sessionStorage.setItem(key, JSON.stringify({
|
|
46
|
+
browseState: browseState,
|
|
47
|
+
expansionState: expansionState
|
|
48
|
+
}));
|
|
49
|
+
} catch (error) { }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadBrowseSession() {
|
|
53
|
+
var key = getBrowseCacheKey();
|
|
54
|
+
if (!key || !window.sessionStorage) return false;
|
|
55
|
+
try {
|
|
56
|
+
var raw = sessionStorage.getItem(key);
|
|
57
|
+
if (!raw) return false;
|
|
58
|
+
var parsed = JSON.parse(raw);
|
|
59
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
60
|
+
browseState = parsed.browseState && typeof parsed.browseState === "object" ? parsed.browseState : null;
|
|
61
|
+
expansionState = parsed.expansionState && typeof parsed.expansionState === "object" ? parsed.expansionState : {};
|
|
62
|
+
return !!browseState;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseSelectedItems(rawValue) {
|
|
69
|
+
if (!rawValue) return [];
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
var parsed = JSON.parse(rawValue);
|
|
73
|
+
if (!Array.isArray(parsed)) return [];
|
|
74
|
+
|
|
75
|
+
return parsed
|
|
76
|
+
.filter(function (item) {
|
|
77
|
+
return item && typeof item === "object" && !Array.isArray(item);
|
|
78
|
+
})
|
|
79
|
+
.map(function (item) {
|
|
80
|
+
var res = {
|
|
81
|
+
name: typeof item.name === "string" ? item.name.trim() : "",
|
|
82
|
+
nodeID: typeof (item.nodeID || item.nodeId) === "string" ? String(item.nodeID || item.nodeId).trim() : "",
|
|
83
|
+
type: typeof (item.type || item.dataType) === "string" ? String(item.type || item.dataType).trim() : "",
|
|
84
|
+
nodeClass: typeof item.nodeClass === "string" ? item.nodeClass.trim() : "",
|
|
85
|
+
typeDefinition: typeof (item.typeDefinition || item.typeDefinitionName) === "string" ? String(item.typeDefinition || item.typeDefinitionName).trim() : "",
|
|
86
|
+
hasTypeDefinition: item.hasTypeDefinition && typeof item.hasTypeDefinition === "object" ? item.hasTypeDefinition : null,
|
|
87
|
+
valueProperty: typeof item.valueProperty === "string" && item.valueProperty.trim() ? item.valueProperty.trim() : "payload",
|
|
88
|
+
valuePropertyType: (item.valuePropertyType === "msg" || item.valuePropertyType === "flow" || item.valuePropertyType === "global") ? item.valuePropertyType : "msg"
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (item.objectId) res.objectId = item.objectId;
|
|
92
|
+
if (Array.isArray(item.inputs)) res.inputs = item.inputs;
|
|
93
|
+
if (Array.isArray(item.outputs)) res.outputs = item.outputs;
|
|
94
|
+
|
|
95
|
+
return res;
|
|
96
|
+
})
|
|
97
|
+
.filter(function (item) {
|
|
98
|
+
return !!item.nodeID;
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function serializeSelectedItems(items) {
|
|
106
|
+
return JSON.stringify(items.map(function (item) {
|
|
107
|
+
var result = {
|
|
108
|
+
name: item.name || item.nodeID,
|
|
109
|
+
nodeID: item.nodeID
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (item.type) result.type = item.type;
|
|
113
|
+
if (item.nodeClass) result.nodeClass = item.nodeClass;
|
|
114
|
+
if (item.typeDefinition) result.typeDefinition = item.typeDefinition;
|
|
115
|
+
if (item.hasTypeDefinition) result.hasTypeDefinition = item.hasTypeDefinition;
|
|
116
|
+
|
|
117
|
+
if (item.valueProperty && item.nodeClass !== "Method") result.valueProperty = item.valueProperty;
|
|
118
|
+
if (item.valuePropertyType && item.nodeClass !== "Method") result.valuePropertyType = item.valuePropertyType;
|
|
119
|
+
|
|
120
|
+
if (item.objectId) result.objectId = item.objectId;
|
|
121
|
+
if (item.inputs) result.inputs = item.inputs;
|
|
122
|
+
if (item.outputs) result.outputs = item.outputs;
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}), null, 2);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function escapeHtml(value) {
|
|
129
|
+
return String(value || "").replace(/[&<>"']/g, function (char) {
|
|
130
|
+
return {
|
|
131
|
+
"&": "&",
|
|
132
|
+
"<": "<",
|
|
133
|
+
">": ">",
|
|
134
|
+
'"': """,
|
|
135
|
+
"'": "'"
|
|
136
|
+
}[char];
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function updateSelectedItemsField() {
|
|
141
|
+
$("#node-input-selectedItems").val(serializeSelectedItems(selectedItemsState));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeSearchTerm(value) {
|
|
145
|
+
return String(value || "").trim().toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function textForSearch(item) {
|
|
149
|
+
if (!item || typeof item !== "object") return "";
|
|
150
|
+
return [
|
|
151
|
+
item.name,
|
|
152
|
+
item.displayName,
|
|
153
|
+
item.browseName,
|
|
154
|
+
item.nodeID,
|
|
155
|
+
item.nodeClass,
|
|
156
|
+
item.dataType,
|
|
157
|
+
item.description
|
|
158
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function nodeMatchesSearch(item, term) {
|
|
162
|
+
if (!term) return true;
|
|
163
|
+
return textForSearch(item).indexOf(term) >= 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function branchHasSearchMatch(item, term) {
|
|
167
|
+
if (nodeMatchesSearch(item, term)) return true;
|
|
168
|
+
if (!item || !Array.isArray(item.browse)) return false;
|
|
169
|
+
for (var i = 0; i < item.browse.length; i += 1) {
|
|
170
|
+
if (branchHasSearchMatch(item.browse[i], term)) return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderSelectedItems() {
|
|
176
|
+
var container = $("#node-input-selected-tags");
|
|
177
|
+
if (!container.length) return;
|
|
178
|
+
container.empty();
|
|
179
|
+
|
|
180
|
+
if (!selectedItemsState.length) {
|
|
181
|
+
container.append('<div class="opcua-tree-empty">No items selected.</div>');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var writeMode = $("#node-input-mode").val() === "write";
|
|
186
|
+
|
|
187
|
+
selectedItemsState.forEach(function (item, index) {
|
|
188
|
+
if (item.nodeClass === "Method") {
|
|
189
|
+
// Renderiza o item do tipo Method estruturado na mesma lista
|
|
190
|
+
var chip = $('<div class="opcua-client-method-chip" style="margin-bottom:12px; border: 1px solid #ccc; padding: 10px; background: #fff; border-radius: 4px;"></div>');
|
|
191
|
+
var header = $('<div class="opcua-client-method-header" style="display:flex; align-items:center; margin-bottom:8px;"></div>');
|
|
192
|
+
header.append('<span class="opcua-tree-icon" style="margin-right:6px;"><i class="fa fa-cog"></i></span>');
|
|
193
|
+
header.append('<span class="opcua-client-method-title" style="font-weight:bold; flex-grow:1;">' + escapeHtml(item.name || item.nodeID) + '</span>');
|
|
194
|
+
|
|
195
|
+
var nodeLabel = escapeHtml(item.nodeID);
|
|
196
|
+
if (item.objectId) nodeLabel += ' · <span style="color:#aaa;font-weight:400;">obj: ' + escapeHtml(item.objectId) + '</span>';
|
|
197
|
+
header.append('<span class="opcua-client-nodeid-label" style="font-size:11px; color:#666; margin-right:10px;">' + nodeLabel + '</span>');
|
|
198
|
+
header.append('<div class="opcua-tree-actions" style="margin:0;"><a href="#" class="editor-button editor-button-small opcua-method-remove" data-mindex="' + index + '"><i class="fa fa-trash"></i></a></div>');
|
|
199
|
+
chip.append(header);
|
|
200
|
+
|
|
201
|
+
chip.append('<div class="opcua-client-method-section-label" style="font-size:12px; font-weight:bold; margin-top:6px; margin-bottom:4px;"><i class="fa fa-arrow-right" style="font-size:10px;margin-right:3px;"></i>Input arguments</div>');
|
|
202
|
+
|
|
203
|
+
if (!item.inputs || !item.inputs.length) {
|
|
204
|
+
chip.append('<div style="font-size:11px;color:#aaa;padding:1px 0 3px 26px;">No input arguments</div>');
|
|
205
|
+
} else {
|
|
206
|
+
item.inputs.forEach(function (inp, iIndex) {
|
|
207
|
+
var propId = "opcua-method-inp-prop-" + index + "-" + iIndex;
|
|
208
|
+
var typeId = "opcua-method-inp-type-" + index + "-" + iIndex;
|
|
209
|
+
var type = (inp.valuePropertyType === "flow" || inp.valuePropertyType === "global") ? inp.valuePropertyType : "msg";
|
|
210
|
+
var prop = inp.valueProperty || "payload";
|
|
211
|
+
|
|
212
|
+
var row = $('<div class="opcua-client-tag-chip opcua-client-method-inp-chip" style="margin-bottom:4px;"></div>');
|
|
213
|
+
row.append('<div class="opcua-tree-icon"><i class="fa fa-tag"></i></div>');
|
|
214
|
+
row.append('<div class="opcua-tree-title" title="' + escapeHtml(inp.dataType || "") + '">'
|
|
215
|
+
+ escapeHtml(inp.name)
|
|
216
|
+
+ '<span style="color:#aaa;font-size:11px;font-weight:400;margin-left:4px;">(' + escapeHtml(inp.dataType || "?") + ')</span>'
|
|
217
|
+
+ '</div>');
|
|
218
|
+
row.append('<div class="opcua-client-tag-write">'
|
|
219
|
+
+ '<input type="text" class="opcua-method-inp-prop" id="' + propId + '" data-mindex="' + index + '" data-iindex="' + iIndex + '" value="' + escapeHtml(prop) + '" placeholder="payload">'
|
|
220
|
+
+ '<input type="hidden" class="opcua-method-inp-type" id="' + typeId + '" data-mindex="' + index + '" data-iindex="' + iIndex + '" value="' + escapeHtml(type) + '">'
|
|
221
|
+
+ '</div>');
|
|
222
|
+
row.append('<div class="opcua-client-tag-right">'
|
|
223
|
+
+ '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-method-remove-input" data-mindex="' + index + '" data-iindex="' + iIndex + '"><i class="fa fa-times"></i></a></div>'
|
|
224
|
+
+ '</div>');
|
|
225
|
+
chip.append(row);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
chip.append('<div class="opcua-method-section-row" style="margin-top:6px;"><a href="#" class="editor-button editor-button-small opcua-method-add-input" data-mindex="' + index + '"><i class="fa fa-plus"></i> Add input</a></div>');
|
|
230
|
+
|
|
231
|
+
var outputs = item.outputs || [];
|
|
232
|
+
chip.append('<div class="opcua-client-method-section-label" style="font-size:12px; font-weight:bold; margin-top:10px; margin-bottom:4px;"><i class="fa fa-arrow-left" style="font-size:10px;margin-right:3px;"></i>Output arguments</div>');
|
|
233
|
+
|
|
234
|
+
if (!outputs.length) {
|
|
235
|
+
chip.append('<div style="font-size:11px;color:#aaa;padding:1px 0 3px 26px;">No output arguments</div>');
|
|
236
|
+
} else {
|
|
237
|
+
outputs.forEach(function (out) {
|
|
238
|
+
var row = $('<div class="opcua-client-tag-chip" style="margin-bottom:4px;background:#f3f3f3;border-color:#e8e8e8;"></div>');
|
|
239
|
+
row.append('<div class="opcua-tree-icon" style="color:#bbb;"><i class="fa fa-tag"></i></div>');
|
|
240
|
+
row.append('<div class="opcua-tree-title" style="color:#888;" title="' + escapeHtml(out.dataType || "") + '">'
|
|
241
|
+
+ escapeHtml(out.name)
|
|
242
|
+
+ '<span style="color:#bbb;font-size:11px;font-weight:400;margin-left:4px;">(' + escapeHtml(out.dataType || "?") + ')</span>'
|
|
243
|
+
+ '</div>');
|
|
244
|
+
row.append('<div class="opcua-client-tag-right"><span style="font-size:11px;color:#bbb;font-style:italic;">read-only</span></div>');
|
|
245
|
+
chip.append(row);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
container.append(chip);
|
|
249
|
+
} else {
|
|
250
|
+
// Renderiza as Tags / Variáveis normais
|
|
251
|
+
var row = $('<div class="opcua-client-tag-chip" style="margin-bottom:4px;"></div>');
|
|
252
|
+
var icon = browseIconFor(item);
|
|
253
|
+
row.append('<div class="opcua-tree-icon"><i class="fa ' + icon + '"></i></div>');
|
|
254
|
+
row.append('<div class="opcua-tree-title">' + escapeHtml(item.name || item.nodeID) + '</div>');
|
|
255
|
+
if (writeMode) {
|
|
256
|
+
var type = (item.valuePropertyType === "flow" || item.valuePropertyType === "global") ? item.valuePropertyType : "msg";
|
|
257
|
+
var prop = item.valueProperty || "payload";
|
|
258
|
+
row.append('<div class="opcua-client-tag-write">'
|
|
259
|
+
+ '<input type="text" class="opcua-client-item-value-prop" id="opcua-client-item-value-prop-' + index + '" data-index="' + index + '" value="' + escapeHtml(prop) + '" placeholder="payload">'
|
|
260
|
+
+ '<input type="hidden" class="opcua-client-item-value-type" id="opcua-client-item-value-type-' + index + '" data-index="' + index + '" value="' + escapeHtml(type) + '">'
|
|
261
|
+
+ "</div>");
|
|
262
|
+
}
|
|
263
|
+
row.append('<div class="opcua-client-tag-right">'
|
|
264
|
+
+ '<div class="opcua-client-nodeid-label">' + escapeHtml(item.nodeID) + '</div>'
|
|
265
|
+
+ '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-remove-tag" data-index="' + index + '"><i class="fa fa-trash"></i></a></div>'
|
|
266
|
+
+ "</div>");
|
|
267
|
+
container.append(row);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
initializeSelectedItemTypedInputs();
|
|
272
|
+
|
|
273
|
+
// Inicializa dinamicamente os typedInputs dos parâmetros dos Métodos inseridos na lista unificada
|
|
274
|
+
$(".opcua-method-inp-prop").each(function () {
|
|
275
|
+
var input = $(this);
|
|
276
|
+
if (input.data("typedInputInitialized")) {
|
|
277
|
+
input.typedInput("types", ["msg", "flow", "global"]);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
var mindex = input.attr("data-mindex");
|
|
281
|
+
var iindex = input.attr("data-iindex");
|
|
282
|
+
var typeField = "#opcua-method-inp-type-" + mindex + "-" + iindex;
|
|
283
|
+
input.typedInput({ type: $(typeField).val() || "msg", types: ["msg", "flow", "global"], typeField: typeField });
|
|
284
|
+
input.data("typedInputInitialized", true);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function initializeSelectedItemTypedInputs() {
|
|
289
|
+
$(".opcua-client-item-value-prop").each(function () {
|
|
290
|
+
var input = $(this);
|
|
291
|
+
var index = Number(input.attr("data-index"));
|
|
292
|
+
var typeField = "#opcua-client-item-value-type-" + index;
|
|
293
|
+
if (input.data("typedInputInitialized")) {
|
|
294
|
+
input.typedInput("types", ["msg", "flow", "global"]);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
input.typedInput({
|
|
299
|
+
type: $(typeField).val() || "msg",
|
|
300
|
+
types: ["msg", "flow", "global"],
|
|
301
|
+
typeField: typeField
|
|
302
|
+
});
|
|
303
|
+
input.data("typedInputInitialized", true);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function syncSelectedItems() {
|
|
308
|
+
rebuildNodeIdIndex();
|
|
309
|
+
updateSelectedItemsField();
|
|
310
|
+
renderSelectedItems();
|
|
311
|
+
renderBrowseTree();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isExpanded(path, defaultValue) {
|
|
315
|
+
if (expansionState[path] === undefined) {
|
|
316
|
+
expansionState[path] = !!defaultValue;
|
|
317
|
+
}
|
|
318
|
+
return expansionState[path];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function selectedIndexByNodeId(nodeId) {
|
|
322
|
+
var idx = selectedNodeIdSet[nodeId];
|
|
323
|
+
return (idx !== undefined) ? idx : -1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function canExpand(item) {
|
|
327
|
+
return item && item.nodeClass !== "Variable"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isVariable(item) {
|
|
331
|
+
return String(item && item.nodeClass || "").toLowerCase() === "variable";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function nodeIdOf(item) {
|
|
335
|
+
return item && (item.nodeID || item.nodeId) ? String(item.nodeID || item.nodeId) : "";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function loadBrowse(nodeId) {
|
|
339
|
+
var connectionId = $("#node-input-connection").val();
|
|
340
|
+
if (!connectionId) {
|
|
341
|
+
RED.notify("Select an OPC UA connection before browsing.", "warning");
|
|
342
|
+
return $.Deferred().reject().promise();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return $.getJSON("opcua-client-config/" + encodeURIComponent(connectionId) + "/browse", {
|
|
346
|
+
nodeId: nodeId || "i=84"
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderBrowseTree() {
|
|
351
|
+
if (renderPending) return;
|
|
352
|
+
renderPending = true;
|
|
353
|
+
setTimeout(function () {
|
|
354
|
+
renderPending = false;
|
|
355
|
+
_doRenderBrowseTree();
|
|
356
|
+
}, 0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function _doRenderBrowseTree() {
|
|
360
|
+
var container = $("#node-input-browse-tree");
|
|
361
|
+
var frag = document.createDocumentFragment();
|
|
362
|
+
|
|
363
|
+
if (!browseState) {
|
|
364
|
+
var empty = document.createElement("div");
|
|
365
|
+
empty.className = "opcua-tree-empty";
|
|
366
|
+
empty.textContent = "Click Browse to load the server tree.";
|
|
367
|
+
frag.appendChild(empty);
|
|
368
|
+
container[0].innerHTML = "";
|
|
369
|
+
container[0].appendChild(frag);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (browseSearchTerm) {
|
|
374
|
+
if (!branchHasSearchMatch(browseState, browseSearchTerm)) {
|
|
375
|
+
var noMatch = document.createElement("div");
|
|
376
|
+
noMatch.className = "opcua-tree-empty";
|
|
377
|
+
noMatch.textContent = "No items found in the already explored items.";
|
|
378
|
+
frag.appendChild(noMatch);
|
|
379
|
+
container[0].innerHTML = "";
|
|
380
|
+
container[0].appendChild(frag);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
renderBrowseRootFiltered(browseState, "root", 0, frag, browseSearchTerm);
|
|
384
|
+
container[0].innerHTML = "";
|
|
385
|
+
container[0].appendChild(frag);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
renderBrowseRoot(browseState, "root", 0, frag);
|
|
390
|
+
container[0].innerHTML = "";
|
|
391
|
+
container[0].appendChild(frag);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isFolderNode(item) {
|
|
395
|
+
if (!item) return false;
|
|
396
|
+
var nodeClass = String(item.nodeClass || "");
|
|
397
|
+
var typeDefinition = String(item.typeDefinition || item.typeDefinitionName || "").toLowerCase();
|
|
398
|
+
var hasTypeDefinitionBrowseName = String(
|
|
399
|
+
item.hasTypeDefinition && item.hasTypeDefinition.browseName || ""
|
|
400
|
+
).toLowerCase();
|
|
401
|
+
var explicitType = String(item.type || item.kind || "").toLowerCase();
|
|
402
|
+
if (nodeClass === "Folder") return true;
|
|
403
|
+
if (hasTypeDefinitionBrowseName === "foldertype") return true;
|
|
404
|
+
if (typeDefinition.indexOf("folder") >= 0) return true;
|
|
405
|
+
if (explicitType === "folder") return true;
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function browseIconFor(item) {
|
|
410
|
+
var nodeClass = String((item && item.nodeClass) || "");
|
|
411
|
+
if (isFolderNode(item)) return "fa-folder";
|
|
412
|
+
if (nodeClass === "Object") return "fa-cube";
|
|
413
|
+
if (nodeClass === "Method") return "fa-cog";
|
|
414
|
+
if (nodeClass === "Variable") return "fa-tag";
|
|
415
|
+
if (nodeClass === "ObjectType") return "fa-cubes";
|
|
416
|
+
if (nodeClass === "View") return "fa-eye";
|
|
417
|
+
if (nodeClass === "DataType") return "fa-database";
|
|
418
|
+
if (nodeClass === "ReferenceType") return "fa-random";
|
|
419
|
+
return "fa-tag";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function makeEl(tag, className, html) {
|
|
423
|
+
var el = document.createElement(tag);
|
|
424
|
+
if (className) el.className = className;
|
|
425
|
+
if (html !== undefined) el.innerHTML = html;
|
|
426
|
+
return el;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function makeTreeRow(path, extraClass) {
|
|
430
|
+
var row = document.createElement("div");
|
|
431
|
+
row.className = "opcua-tree-row" + (extraClass ? " " + extraClass : "");
|
|
432
|
+
row.setAttribute("data-path", path);
|
|
433
|
+
return row;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderBrowseRoot(root, path, depth, frag) {
|
|
437
|
+
var expanded = isExpanded(path, true);
|
|
438
|
+
var row = makeTreeRow(path);
|
|
439
|
+
row.innerHTML = '<span class="opcua-tree-indent"></span>'
|
|
440
|
+
+ '<span class="opcua-tree-twisty opcua-client-toggle-tree" data-path="' + escapeHtml(path) + '">'
|
|
441
|
+
+ (expanded ? '<i class="fa fa-caret-down"></i>' : '<i class="fa fa-caret-right"></i>') + '</span>'
|
|
442
|
+
+ '<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>'
|
|
443
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>'
|
|
444
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>';
|
|
445
|
+
frag.appendChild(row);
|
|
446
|
+
|
|
447
|
+
if (!expanded) return;
|
|
448
|
+
|
|
449
|
+
if (!Array.isArray(root.browse) || !root.browse.length) {
|
|
450
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "No items found.."));
|
|
451
|
+
} else {
|
|
452
|
+
root.browse.forEach(function (item, index) {
|
|
453
|
+
renderBrowseItem(item, path + ".browse." + index, depth + 1, frag);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function renderBrowseItem(item, path, depth, frag) {
|
|
459
|
+
var expanded = isExpanded(path, false);
|
|
460
|
+
var nodeId = nodeIdOf(item);
|
|
461
|
+
var selectedIndex = selectedIndexByNodeId(nodeId);
|
|
462
|
+
var hasChildren = canExpand(item);
|
|
463
|
+
var row = makeTreeRow(path, selectedIndex >= 0 ? "is-selected" : "");
|
|
464
|
+
|
|
465
|
+
var indents = "";
|
|
466
|
+
for (var i = 0; i < depth; i += 1) indents += '<span class="opcua-tree-indent"></span>';
|
|
467
|
+
|
|
468
|
+
var twisty = '<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + escapeHtml(path) + '">'
|
|
469
|
+
+ (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>';
|
|
470
|
+
|
|
471
|
+
var actions = nodeId
|
|
472
|
+
? '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + escapeHtml(path) + '"><i class="fa ' + (selectedIndex >= 0 ? 'fa-minus' : 'fa-plus') + '"></i> ' + (selectedIndex >= 0 ? 'Remove' : 'Add') + '</a></div>'
|
|
473
|
+
: '';
|
|
474
|
+
|
|
475
|
+
row.innerHTML = indents + twisty
|
|
476
|
+
+ '<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>'
|
|
477
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>'
|
|
478
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>'
|
|
479
|
+
+ '<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>'
|
|
480
|
+
+ actions;
|
|
481
|
+
frag.appendChild(row);
|
|
482
|
+
|
|
483
|
+
if (item.description) {
|
|
484
|
+
var desc = makeEl("div", "opcua-client-description");
|
|
485
|
+
desc.style.padding = "0 10px 8px " + String((depth + 2) * 14) + "px";
|
|
486
|
+
desc.textContent = item.description;
|
|
487
|
+
frag.appendChild(desc);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (expanded && hasChildren) {
|
|
491
|
+
if (Array.isArray(item.browse)) {
|
|
492
|
+
if (!item.browse.length) {
|
|
493
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "No children found.."));
|
|
494
|
+
} else {
|
|
495
|
+
item.browse.forEach(function (child, index) {
|
|
496
|
+
renderBrowseItem(child, path + ".browse." + index, depth + 1, frag);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "Searching for items..."));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function renderBrowseRootFiltered(root, path, depth, frag, term) {
|
|
506
|
+
var row = makeTreeRow(path);
|
|
507
|
+
row.innerHTML = '<span class="opcua-tree-indent"></span>'
|
|
508
|
+
+ '<span class="opcua-tree-twisty"><i class="fa fa-caret-down"></i></span>'
|
|
509
|
+
+ '<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>'
|
|
510
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>'
|
|
511
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>';
|
|
512
|
+
frag.appendChild(row);
|
|
513
|
+
|
|
514
|
+
if (!Array.isArray(root.browse) || !root.browse.length) {
|
|
515
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "Nenhum item encontrado."));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
root.browse.forEach(function (item, index) {
|
|
520
|
+
if (branchHasSearchMatch(item, term)) {
|
|
521
|
+
renderBrowseItemFiltered(item, path + ".browse." + index, depth + 1, frag, term, false);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function renderBrowseItemFiltered(item, path, depth, frag, term, ancestorMatched) {
|
|
527
|
+
if (!branchHasSearchMatch(item, term)) return;
|
|
528
|
+
|
|
529
|
+
var nodeId = nodeIdOf(item);
|
|
530
|
+
var selectedIndex = selectedIndexByNodeId(nodeId);
|
|
531
|
+
var hasChildren = canExpand(item);
|
|
532
|
+
var subtreeVisible = !!ancestorMatched || nodeMatchesSearch(item, term);
|
|
533
|
+
var hasMatchingLoadedChild = hasChildren && Array.isArray(item.browse) && item.browse.some(function (child) {
|
|
534
|
+
return branchHasSearchMatch(child, term);
|
|
535
|
+
});
|
|
536
|
+
var hasExplicitExpansion = expansionState[path] !== undefined;
|
|
537
|
+
var expanded = hasChildren && (hasExplicitExpansion
|
|
538
|
+
? !!expansionState[path]
|
|
539
|
+
: ((subtreeVisible && Array.isArray(item.browse)) || hasMatchingLoadedChild));
|
|
540
|
+
|
|
541
|
+
var row = makeTreeRow(path, selectedIndex >= 0 ? "is-selected" : "");
|
|
542
|
+
|
|
543
|
+
var indents = "";
|
|
544
|
+
for (var i = 0; i < depth; i += 1) indents += '<span class="opcua-tree-indent"></span>';
|
|
545
|
+
|
|
546
|
+
var twisty = '<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + escapeHtml(path) + '">'
|
|
547
|
+
+ (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>';
|
|
548
|
+
|
|
549
|
+
var actions = nodeId
|
|
550
|
+
? '<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + escapeHtml(path) + '"><i class="fa ' + (selectedIndex >= 0 ? 'fa-minus' : 'fa-plus') + '"></i> ' + (selectedIndex >= 0 ? 'Remove' : 'Add') + '</a></div>'
|
|
551
|
+
: '';
|
|
552
|
+
|
|
553
|
+
row.innerHTML = indents + twisty
|
|
554
|
+
+ '<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>'
|
|
555
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>'
|
|
556
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>'
|
|
557
|
+
+ '<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>'
|
|
558
|
+
+ actions;
|
|
559
|
+
frag.appendChild(row);
|
|
560
|
+
|
|
561
|
+
if (item.description) {
|
|
562
|
+
var desc = makeEl("div", "opcua-client-description");
|
|
563
|
+
desc.style.padding = "0 10px 8px " + String((depth + 2) * 14) + "px";
|
|
564
|
+
desc.textContent = item.description;
|
|
565
|
+
frag.appendChild(desc);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (expanded && hasChildren) {
|
|
569
|
+
if (Array.isArray(item.browse)) {
|
|
570
|
+
if (!item.browse.length) {
|
|
571
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "Nenhum filho encontrado."));
|
|
572
|
+
} else {
|
|
573
|
+
item.browse.forEach(function (child, index) {
|
|
574
|
+
if (subtreeVisible || branchHasSearchMatch(child, term)) {
|
|
575
|
+
renderBrowseItemFiltered(child, path + ".browse." + index, depth + 1, frag, term, subtreeVisible);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "Expandindo..."));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function getItemAtPath(path) {
|
|
586
|
+
var tokens = String(path || "").split(".");
|
|
587
|
+
var current = browseState;
|
|
588
|
+
|
|
589
|
+
for (var index = 0; index < tokens.length; index += 1) {
|
|
590
|
+
var token = tokens[index];
|
|
591
|
+
if (!token || token === "root") continue;
|
|
592
|
+
if (!current) return null;
|
|
593
|
+
if (/^\d+$/.test(token)) {
|
|
594
|
+
current = current[Number(token)];
|
|
595
|
+
} else {
|
|
596
|
+
current = current[token];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return current || null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function addSelectedItem(item) {
|
|
604
|
+
var normalized = {
|
|
605
|
+
name: item.displayName || item.browseName || item.name || item.nodeID,
|
|
606
|
+
nodeID: item.nodeID || item.nodeId,
|
|
607
|
+
type: item.dataType || item.type || "",
|
|
608
|
+
nodeClass: item.nodeClass || "",
|
|
609
|
+
typeDefinition: item.typeDefinition || item.typeDefinitionName || "",
|
|
610
|
+
hasTypeDefinition: item.hasTypeDefinition || null,
|
|
611
|
+
valueProperty: item.valueProperty || "payload",
|
|
612
|
+
valuePropertyType: item.valuePropertyType || "msg"
|
|
613
|
+
};
|
|
614
|
+
var currentIndex = selectedIndexByNodeId(normalized.nodeID);
|
|
615
|
+
|
|
616
|
+
if (currentIndex >= 0) {
|
|
617
|
+
selectedItemsState[currentIndex] = normalized;
|
|
618
|
+
} else {
|
|
619
|
+
selectedItemsState.push(normalized);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
syncSelectedItems();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function removeSelectedItemByIndex(index) {
|
|
626
|
+
selectedItemsState.splice(index, 1);
|
|
627
|
+
syncSelectedItems();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function toggleSelectedNode(path) {
|
|
631
|
+
var item = getItemAtPath(path);
|
|
632
|
+
var nodeId = nodeIdOf(item);
|
|
633
|
+
if (!item || !nodeId) return;
|
|
634
|
+
|
|
635
|
+
var currentIndex = selectedIndexByNodeId(nodeId);
|
|
636
|
+
if (currentIndex >= 0) {
|
|
637
|
+
selectedItemsState.splice(currentIndex, 1);
|
|
638
|
+
} else {
|
|
639
|
+
addSelectedItem(item);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
syncSelectedItems();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function refreshBrowseRoot() {
|
|
647
|
+
var rootNodeId = "i=84";
|
|
648
|
+
var container = $("#node-input-browse-tree");
|
|
649
|
+
container.html('<div class="opcua-tree-empty">Carregando...</div>');
|
|
650
|
+
|
|
651
|
+
loadBrowse(rootNodeId).done(function (payload) {
|
|
652
|
+
browseState = payload;
|
|
653
|
+
expansionState = { root: true };
|
|
654
|
+
saveBrowseSession();
|
|
655
|
+
renderBrowseTree();
|
|
656
|
+
}).fail(function (xhr) {
|
|
657
|
+
var message = xhr && xhr.responseJSON && xhr.responseJSON.error
|
|
658
|
+
? xhr.responseJSON.error
|
|
659
|
+
: "Falha ao navegar no servidor OPC UA.";
|
|
660
|
+
browseState = null;
|
|
661
|
+
container.html('<div class="opcua-tree-empty">' + escapeHtml(message) + '</div>');
|
|
662
|
+
RED.notify(message, "error");
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function hideTreeContextMenu() {
|
|
667
|
+
contextMenuPath = "";
|
|
668
|
+
$("#node-input-browse-context-menu").hide();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function showTreeContextMenu(x, y, path) {
|
|
672
|
+
var menu = $("#node-input-browse-context-menu");
|
|
673
|
+
var item = getItemAtPath(path);
|
|
674
|
+
contextMenuPath = path || "";
|
|
675
|
+
$("#node-input-browse-context-refresh").toggle(!!item && !isVariable(item));
|
|
676
|
+
$("#node-input-browse-context-copy-nodeid").toggle(!!nodeIdOf(item));
|
|
677
|
+
menu.css({ left: x + "px", top: y + "px" }).show();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function copyNodeIdFromPath(path) {
|
|
681
|
+
var item = getItemAtPath(path);
|
|
682
|
+
var nodeId = nodeIdOf(item);
|
|
683
|
+
if (!nodeId) {
|
|
684
|
+
RED.notify("NodeID nao encontrado para o item selecionado.", "warning");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
689
|
+
navigator.clipboard.writeText(nodeId).then(function () {
|
|
690
|
+
RED.notify("NodeID copiado.", "success");
|
|
691
|
+
}).catch(function () {
|
|
692
|
+
RED.notify("Falha ao copiar NodeID.", "error");
|
|
693
|
+
});
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
var input = $("<textarea readonly></textarea>").val(nodeId).css({
|
|
698
|
+
position: "fixed",
|
|
699
|
+
left: "-9999px",
|
|
700
|
+
top: "0"
|
|
701
|
+
});
|
|
702
|
+
$("body").append(input);
|
|
703
|
+
input[0].select();
|
|
704
|
+
try {
|
|
705
|
+
document.execCommand("copy");
|
|
706
|
+
RED.notify("NodeID copiado.", "success");
|
|
707
|
+
} catch (error) {
|
|
708
|
+
RED.notify("Falha ao copiar NodeID.", "error");
|
|
709
|
+
}
|
|
710
|
+
input.remove();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function setBrowseSelectedPath(path) {
|
|
714
|
+
browseSelectedPath = path || "";
|
|
715
|
+
$(".opcua-tree-row").removeClass("is-selected");
|
|
716
|
+
if (browseSelectedPath) {
|
|
717
|
+
$('.opcua-tree-row[data-path="' + browseSelectedPath + '"]').addClass("is-selected");
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function refreshNode(path) {
|
|
722
|
+
if (!path) return;
|
|
723
|
+
if (path === "root") {
|
|
724
|
+
refreshBrowseRoot();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
var item = getItemAtPath(path);
|
|
729
|
+
if (!item || isVariable(item)) return;
|
|
730
|
+
|
|
731
|
+
item.browse = undefined;
|
|
732
|
+
expansionState[path] = true;
|
|
733
|
+
saveBrowseSession();
|
|
734
|
+
renderBrowseTree();
|
|
735
|
+
expandNode(path);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function expandNode(path) {
|
|
739
|
+
var item = getItemAtPath(path);
|
|
740
|
+
if (!item || !canExpand(item)) return;
|
|
741
|
+
|
|
742
|
+
if (isExpanded(path, false) && !Array.isArray(item.browse)) {
|
|
743
|
+
renderBrowseTree();
|
|
744
|
+
} else {
|
|
745
|
+
expansionState[path] = !isExpanded(path, false);
|
|
746
|
+
saveBrowseSession();
|
|
747
|
+
if (!expansionState[path]) {
|
|
748
|
+
renderBrowseTree();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (Array.isArray(item.browse)) {
|
|
754
|
+
renderBrowseTree();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
renderBrowseTree();
|
|
759
|
+
var browseNodeId = item.nodeID || item.nodeId;
|
|
760
|
+
loadBrowse(browseNodeId).done(function (payload) {
|
|
761
|
+
item.browse = Array.isArray(payload.browse) ? payload.browse : [];
|
|
762
|
+
saveBrowseSession();
|
|
763
|
+
renderBrowseTree();
|
|
764
|
+
}).fail(function (xhr) {
|
|
765
|
+
expansionState[path] = false;
|
|
766
|
+
saveBrowseSession();
|
|
767
|
+
renderBrowseTree();
|
|
768
|
+
var message = xhr && xhr.responseJSON && xhr.responseJSON.error
|
|
769
|
+
? xhr.responseJSON.error
|
|
770
|
+
: "Falha ao expandir o node.";
|
|
771
|
+
RED.notify(message, "error");
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function methodNodeIdOf(item) {
|
|
776
|
+
return item && (item.nodeID || item.nodeId) ? String(item.nodeID || item.nodeId) : "";
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function addMethodFromTree(item, parentItem) {
|
|
780
|
+
var nodeID = methodNodeIdOf(item);
|
|
781
|
+
var objectId = parentItem ? methodNodeIdOf(parentItem) : "";
|
|
782
|
+
var methodName = item.displayName || item.browseName || item.name || nodeID;
|
|
783
|
+
|
|
784
|
+
for (var i = 0; i < selectedItemsState.length; i++) {
|
|
785
|
+
if (selectedItemsState[i].nodeID === nodeID && selectedItemsState[i].nodeClass === "Method") {
|
|
786
|
+
RED.notify("Method already added.", "warning");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
var connectionId = $("#node-input-connection").val();
|
|
792
|
+
|
|
793
|
+
$.getJSON(
|
|
794
|
+
"opcua-client-config/" + encodeURIComponent(connectionId) + "/browse",
|
|
795
|
+
{ nodeId: nodeID }
|
|
796
|
+
)
|
|
797
|
+
.done(function (payload) {
|
|
798
|
+
var browseItems = Array.isArray(payload.browse) ? payload.browse : [];
|
|
799
|
+
var inputArgs = [];
|
|
800
|
+
var outputArgs = [];
|
|
801
|
+
|
|
802
|
+
browseItems.forEach(function (child) {
|
|
803
|
+
var name = String(child.displayName || child.browseName || "").toLowerCase();
|
|
804
|
+
|
|
805
|
+
if ((name === "inputarguments" || name === "inputargument") && Array.isArray(child.value)) {
|
|
806
|
+
inputArgs = child.value;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if ((name === "outputarguments" || name === "outputargument") && Array.isArray(child.value)) {
|
|
810
|
+
outputArgs = child.value;
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
var inputs = inputArgs.map(function (arg, idx) {
|
|
815
|
+
return {
|
|
816
|
+
name: arg.name || ("arg" + idx),
|
|
817
|
+
dataType: opcuaDataTypeName(arg.dataType),
|
|
818
|
+
valueProperty: "payload",
|
|
819
|
+
valuePropertyType: "msg"
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
var outputs = outputArgs.map(function (arg, idx) {
|
|
824
|
+
return {
|
|
825
|
+
name: arg.name || ("out" + idx),
|
|
826
|
+
dataType: opcuaDataTypeName(arg.dataType)
|
|
827
|
+
};
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
pushMethodItem(nodeID, objectId, methodName, inputs, outputs);
|
|
831
|
+
})
|
|
832
|
+
.fail(function () {
|
|
833
|
+
pushMethodItem(nodeID, objectId, methodName, [], []);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function opcuaDataTypeName(dataType) {
|
|
838
|
+
switch (String(dataType || "")) {
|
|
839
|
+
case "ns=0;i=1": return "Boolean";
|
|
840
|
+
case "ns=0;i=2": return "SByte";
|
|
841
|
+
case "ns=0;i=3": return "Byte";
|
|
842
|
+
case "ns=0;i=4": return "Int16";
|
|
843
|
+
case "ns=0;i=5": return "UInt16";
|
|
844
|
+
case "ns=0;i=6": return "Int32";
|
|
845
|
+
case "ns=0;i=7": return "UInt32";
|
|
846
|
+
case "ns=0;i=8": return "Int64";
|
|
847
|
+
case "ns=0;i=9": return "UInt64";
|
|
848
|
+
case "ns=0;i=10": return "Float";
|
|
849
|
+
case "ns=0;i=11": return "Double";
|
|
850
|
+
case "ns=0;i=12": return "String";
|
|
851
|
+
case "ns=0;i=13": return "DateTime";
|
|
852
|
+
case "ns=0;i=14": return "Guid";
|
|
853
|
+
case "ns=0;i=15": return "ByteString";
|
|
854
|
+
case "ns=0;i=16": return "XmlElement";
|
|
855
|
+
case "ns=0;i=17": return "NodeId";
|
|
856
|
+
case "ns=0;i=18": return "ExpandedNodeId";
|
|
857
|
+
case "ns=0;i=19": return "StatusCode";
|
|
858
|
+
case "ns=0;i=20": return "QualifiedName";
|
|
859
|
+
case "ns=0;i=21": return "LocalizedText";
|
|
860
|
+
case "ns=0;i=22": return "ExtensionObject";
|
|
861
|
+
case "ns=0;i=26": return "Number";
|
|
862
|
+
case "ns=0;i=27": return "Integer";
|
|
863
|
+
case "ns=0;i=28": return "UInteger";
|
|
864
|
+
default: return String(dataType || "");
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function pushMethodItem(nodeID, objectId, name, inputs, outputs) {
|
|
869
|
+
selectedItemsState.push({
|
|
870
|
+
nodeID: nodeID,
|
|
871
|
+
objectId: objectId,
|
|
872
|
+
name: name,
|
|
873
|
+
inputs: inputs || [],
|
|
874
|
+
outputs: outputs || [],
|
|
875
|
+
nodeClass: "Method"
|
|
876
|
+
});
|
|
877
|
+
syncSelectedItems();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function toggleModeFields() {
|
|
881
|
+
var mode = $("#node-input-mode").val();
|
|
882
|
+
|
|
883
|
+
var isSubscription = mode === "subscription" || mode === "events";
|
|
884
|
+
var supportsSelection = mode !== "getSubscriptionId";
|
|
885
|
+
|
|
886
|
+
$(".opcua-client-subscription-row").toggle(isSubscription);
|
|
887
|
+
$(".opcua-client-selection-row").toggle(supportsSelection);
|
|
888
|
+
|
|
889
|
+
// Esconde permanentemente a linha de métodos antiga caso ainda exista no DOM
|
|
890
|
+
$(".opcua-client-method-row").hide();
|
|
891
|
+
|
|
892
|
+
renderSelectedItems();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
RED.nodes.registerType("opcua-client", {
|
|
896
|
+
category: "network",
|
|
897
|
+
color: "#d9edf7",
|
|
898
|
+
defaults: {
|
|
899
|
+
name: { value: "" },
|
|
900
|
+
connection: { value: "", type: "opcua-client-config", required: true },
|
|
901
|
+
mode: { value: "read", required: true },
|
|
902
|
+
selectedItems: {
|
|
903
|
+
value: "[]",
|
|
904
|
+
validate: function (value) {
|
|
905
|
+
try {
|
|
906
|
+
return Array.isArray(JSON.parse(value || "[]"));
|
|
907
|
+
} catch (error) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
samplingInterval: {
|
|
913
|
+
value: 250,
|
|
914
|
+
validate: RED.validators.number()
|
|
915
|
+
},
|
|
916
|
+
publishingInterval: {
|
|
917
|
+
value: 250,
|
|
918
|
+
validate: RED.validators.number()
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
inputs: 1,
|
|
922
|
+
outputs: 1,
|
|
923
|
+
icon: "opcua.svg",
|
|
924
|
+
label: function () {
|
|
925
|
+
return this.name || "opcua-client";
|
|
926
|
+
},
|
|
927
|
+
oneditprepare: function () {
|
|
928
|
+
$("#node-input-selectedItems").typedInput({
|
|
929
|
+
type: "json",
|
|
930
|
+
types: ["json"]
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
selectedItemsState = parseSelectedItems(this.selectedItems);
|
|
934
|
+
rebuildNodeIdIndex();
|
|
935
|
+
|
|
936
|
+
browseState = null;
|
|
937
|
+
expansionState = {};
|
|
938
|
+
browseSelectedPath = "";
|
|
939
|
+
browseSearchValue = "";
|
|
940
|
+
browseSearchTerm = "";
|
|
941
|
+
$("#node-input-browse-search").val("");
|
|
942
|
+
$("#node-input-browse-search-clear").hide();
|
|
943
|
+
loadBrowseSession();
|
|
944
|
+
syncSelectedItems();
|
|
945
|
+
renderBrowseTree();
|
|
946
|
+
|
|
947
|
+
$("#node-input-mode").off("change").on("change", toggleModeFields);
|
|
948
|
+
$("#node-input-browse-root").off("click").on("click", function (event) {
|
|
949
|
+
event.preventDefault();
|
|
950
|
+
refreshBrowseRoot();
|
|
951
|
+
});
|
|
952
|
+
$("#node-input-open-browse-modal").off("click").on("click", function (event) {
|
|
953
|
+
event.preventDefault();
|
|
954
|
+
openBrowseModal();
|
|
955
|
+
});
|
|
956
|
+
$("#node-input-close-browse-modal").off("click").on("click", function (event) {
|
|
957
|
+
event.preventDefault();
|
|
958
|
+
closeBrowseModal();
|
|
959
|
+
});
|
|
960
|
+
$("#node-input-browse-modal").off("click").on("click", function (event) {
|
|
961
|
+
hideTreeContextMenu();
|
|
962
|
+
if (event.target === this) closeBrowseModal();
|
|
963
|
+
});
|
|
964
|
+
$("#node-input-browse-search").off("input").on("input", debounce(function () {
|
|
965
|
+
browseSearchValue = $(this).val();
|
|
966
|
+
browseSearchTerm = normalizeSearchTerm(browseSearchValue);
|
|
967
|
+
$("#node-input-browse-search-clear").toggle(!!browseSearchTerm);
|
|
968
|
+
renderBrowseTree();
|
|
969
|
+
}, 200));
|
|
970
|
+
$("#node-input-browse-search-clear").off("click").on("click", function (event) {
|
|
971
|
+
event.preventDefault();
|
|
972
|
+
browseSearchValue = "";
|
|
973
|
+
browseSearchTerm = "";
|
|
974
|
+
$("#node-input-browse-search").val("");
|
|
975
|
+
$(this).hide();
|
|
976
|
+
renderBrowseTree();
|
|
977
|
+
});
|
|
978
|
+
$("#node-input-connection").off("change.opcuaClientBrowse").on("change.opcuaClientBrowse", function () {
|
|
979
|
+
browseState = null;
|
|
980
|
+
expansionState = {};
|
|
981
|
+
browseSelectedPath = "";
|
|
982
|
+
loadBrowseSession();
|
|
983
|
+
renderBrowseTree();
|
|
984
|
+
});
|
|
985
|
+
$(document).off("keydown.opcuaClientBrowseModal").on("keydown.opcuaClientBrowseModal", function (event) {
|
|
986
|
+
hideTreeContextMenu();
|
|
987
|
+
if (event.key === "Escape") closeBrowseModal();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
toggleModeFields();
|
|
991
|
+
},
|
|
992
|
+
oneditsave: function () {
|
|
993
|
+
updateSelectedItemsField();
|
|
994
|
+
saveBrowseSession();
|
|
995
|
+
closeBrowseModal();
|
|
996
|
+
$(document).off("keydown.opcuaClientBrowseModal");
|
|
997
|
+
},
|
|
998
|
+
oneditcancel: function () {
|
|
999
|
+
closeBrowseModal();
|
|
1000
|
+
$(document).off("keydown.opcuaClientBrowseModal");
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
$(document).on("change", "#node-input-selectedItems", function () {
|
|
1005
|
+
selectedItemsState = parseSelectedItems($(this).val());
|
|
1006
|
+
renderSelectedItems();
|
|
1007
|
+
renderBrowseTree();
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
$(document).on("click", ".opcua-client-remove-tag", function (event) {
|
|
1011
|
+
event.preventDefault();
|
|
1012
|
+
removeSelectedItemByIndex(Number($(this).attr("data-index")));
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
$(document).on("click", ".opcua-client-toggle-tag", function (event) {
|
|
1016
|
+
event.preventDefault();
|
|
1017
|
+
var path = $(this).attr("data-path");
|
|
1018
|
+
|
|
1019
|
+
if ($("#node-input-mode").val() === "method") {
|
|
1020
|
+
var item = getItemAtPath(path);
|
|
1021
|
+
if (item && item.nodeClass === "Method") {
|
|
1022
|
+
var parentPath = path.split(".");
|
|
1023
|
+
parentPath.splice(parentPath.length - 2, 2);
|
|
1024
|
+
parentPath = parentPath.join(".");
|
|
1025
|
+
var parentItem = getItemAtPath(parentPath);
|
|
1026
|
+
addMethodFromTree(item, parentItem);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
toggleSelectedNode(path);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
$(document).on("click", ".opcua-client-toggle-tree", function (event) {
|
|
1034
|
+
event.preventDefault();
|
|
1035
|
+
expandNode($(this).attr("data-path"));
|
|
1036
|
+
});
|
|
1037
|
+
$(document).on("change", ".opcua-client-item-value-type", function () {
|
|
1038
|
+
var index = Number($(this).attr("data-index"));
|
|
1039
|
+
if (!selectedItemsState[index]) return;
|
|
1040
|
+
selectedItemsState[index].valuePropertyType = $(this).val();
|
|
1041
|
+
updateSelectedItemsField();
|
|
1042
|
+
});
|
|
1043
|
+
$(document).on("change input", ".opcua-client-item-value-prop", function () {
|
|
1044
|
+
var index = Number($(this).attr("data-index"));
|
|
1045
|
+
if (!selectedItemsState[index]) return;
|
|
1046
|
+
selectedItemsState[index].valueProperty = $(this).typedInput ? $(this).typedInput("value") : $(this).val();
|
|
1047
|
+
var typeField = $("#opcua-client-item-value-type-" + index);
|
|
1048
|
+
selectedItemsState[index].valuePropertyType = (typeField.val() || "msg");
|
|
1049
|
+
updateSelectedItemsField();
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
$(document).on("click", ".opcua-tree-row", function (event) {
|
|
1053
|
+
if ($(event.target).closest(".opcua-client-toggle-tree, .opcua-client-toggle-tag, .opcua-tree-actions, #node-input-browse-context-menu").length) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
setBrowseSelectedPath($(this).attr("data-path"));
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
$(document).on("contextmenu", ".opcua-tree-row", function (event) {
|
|
1060
|
+
var clickedPath = $(this).attr("data-path");
|
|
1061
|
+
if (clickedPath) {
|
|
1062
|
+
setBrowseSelectedPath(clickedPath);
|
|
1063
|
+
}
|
|
1064
|
+
var path = browseSelectedPath || clickedPath;
|
|
1065
|
+
var item = getItemAtPath(path);
|
|
1066
|
+
if (!item) {
|
|
1067
|
+
hideTreeContextMenu();
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
event.preventDefault();
|
|
1071
|
+
showTreeContextMenu(event.clientX, event.clientY, path);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
$(document).on("click", "#node-input-browse-context-refresh", function (event) {
|
|
1075
|
+
event.preventDefault();
|
|
1076
|
+
var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
|
|
1077
|
+
var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
|
|
1078
|
+
hideTreeContextMenu();
|
|
1079
|
+
refreshNode(path);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
$(document).on("click", "#node-input-browse-context-copy-nodeid", function (event) {
|
|
1083
|
+
event.preventDefault();
|
|
1084
|
+
var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
|
|
1085
|
+
var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
|
|
1086
|
+
hideTreeContextMenu();
|
|
1087
|
+
copyNodeIdFromPath(path);
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
$(document).on("click", function (event) {
|
|
1091
|
+
if (!$(event.target).closest("#node-input-browse-context-menu").length) {
|
|
1092
|
+
hideTreeContextMenu();
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// ── Method browse tree events ──────────────────────────────────────
|
|
1097
|
+
|
|
1098
|
+
$(document).on("click", ".opcua-method-remove", function (event) {
|
|
1099
|
+
event.preventDefault();
|
|
1100
|
+
var idx = Number($(this).attr("data-mindex"));
|
|
1101
|
+
selectedItemsState.splice(idx, 1);
|
|
1102
|
+
syncSelectedItems();
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
$(document).on("click", ".opcua-method-add-input", function (event) {
|
|
1106
|
+
event.preventDefault();
|
|
1107
|
+
var idx = Number($(this).attr("data-mindex"));
|
|
1108
|
+
if (!selectedItemsState[idx]) return;
|
|
1109
|
+
selectedItemsState[idx].inputs = selectedItemsState[idx].inputs || [];
|
|
1110
|
+
selectedItemsState[idx].inputs.push({ name: "arg" + selectedItemsState[idx].inputs.length, dataType: "String", valueProperty: "payload", valuePropertyType: "msg" });
|
|
1111
|
+
syncSelectedItems();
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
$(document).on("click", ".opcua-method-remove-input", function (event) {
|
|
1115
|
+
event.preventDefault();
|
|
1116
|
+
var mi = Number($(this).attr("data-mindex"));
|
|
1117
|
+
var ii = Number($(this).attr("data-iindex"));
|
|
1118
|
+
if (!selectedItemsState[mi]) return;
|
|
1119
|
+
selectedItemsState[mi].inputs.splice(ii, 1);
|
|
1120
|
+
syncSelectedItems();
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
$(document).on("change input", ".opcua-method-inp-prop", function () {
|
|
1124
|
+
var mi = Number($(this).attr("data-mindex"));
|
|
1125
|
+
var ii = Number($(this).attr("data-iindex"));
|
|
1126
|
+
if (!selectedItemsState[mi] || !selectedItemsState[mi].inputs[ii]) return;
|
|
1127
|
+
selectedItemsState[mi].inputs[ii].valueProperty = $(this).typedInput ? $(this).typedInput("value") : $(this).val();
|
|
1128
|
+
var typeField = $("#opcua-method-inp-type-" + mi + "-" + ii);
|
|
1129
|
+
selectedItemsState[mi].inputs[ii].valuePropertyType = typeField.val() || "msg";
|
|
1130
|
+
updateSelectedItemsField();
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
$(document).on("change", ".opcua-method-inp-type", function () {
|
|
1134
|
+
var mi = Number($(this).attr("data-mindex"));
|
|
1135
|
+
var ii = Number($(this).attr("data-iindex"));
|
|
1136
|
+
if (!selectedItemsState[mi] || !selectedItemsState[mi].inputs[ii]) return;
|
|
1137
|
+
selectedItemsState[mi].inputs[ii].valuePropertyType = $(this).val();
|
|
1138
|
+
updateSelectedItemsField();
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
1141
|
})();
|