@vitormnm/node-red-simple-opcua 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1225 @@
1
+ <style>
2
+ body.opcua-tree-modal-open {
3
+ overflow: hidden;
4
+ }
5
+
6
+ .opcua-tree-editor {
7
+ width: 100%;
8
+ min-height: 120px;
9
+ border: 1px solid var(--red-ui-form-input-border-color, #d9d9d9);
10
+ background: var(--red-ui-form-input-background, #fff);
11
+ border-radius: 4px;
12
+ overflow: auto;
13
+ }
14
+
15
+ .opcua-tree-row {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: 5px;
19
+ min-height: 24px;
20
+ padding: 2px 6px;
21
+ font-size: 12px;
22
+ border-bottom: 1px solid transparent;
23
+ }
24
+
25
+ .opcua-tree-row:hover {
26
+ background: var(--red-ui-list-item-background-hover, #f3f7fd);
27
+ }
28
+
29
+ .opcua-tree-row.is-selected {
30
+ background: var(--red-ui-list-item-background-selected, #d9ecff);
31
+ color: var(--red-ui-list-item-color-selected, inherit);
32
+ }
33
+
34
+ .opcua-tree-indent { flex: 0 0 auto; width: 14px; }
35
+ .opcua-tree-twisty { width: 16px; text-align: center; color: #777; flex: 0 0 16px; cursor: pointer; }
36
+ .opcua-tree-icon { width: 14px; text-align: center; color: #777; flex: 0 0 14px; }
37
+ .opcua-tree-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
38
+ .opcua-tree-type { color: #777; font-size: 11px; flex: 0 0 auto; }
39
+
40
+ .opcua-tree-title {
41
+ min-width: 100px;
42
+ font-weight: 600;
43
+ color: #444;
44
+ }
45
+
46
+ .opcua-client-nodeid-label {
47
+ margin-left: auto;
48
+ text-align: right;
49
+ word-break: break-all;
50
+ max-width: 320px;
51
+ color: #777;
52
+ font-size: 12px;
53
+ }
54
+
55
+ .opcua-tree-actions {
56
+ margin-left: auto;
57
+ display: flex;
58
+ gap: 6px;
59
+ align-items: center;
60
+ }
61
+
62
+ .opcua-tree-empty {
63
+ padding: 14px;
64
+ border: 1px dashed #d9d9d9;
65
+ border-radius: 4px;
66
+ background: #fafafa;
67
+ color: #777;
68
+ text-align: center;
69
+ }
70
+
71
+ .opcua-client-json {
72
+ width: 100%;
73
+ box-sizing: border-box;
74
+ }
75
+
76
+ .opcua-client-tag-box {
77
+ width: 100%;
78
+ min-height: 70px;
79
+ }
80
+
81
+ .opcua-client-tag-chip {
82
+ display: grid;
83
+ grid-template-columns: 18px 1fr auto;
84
+ grid-template-areas:
85
+ "icon name name"
86
+ "icon write right";
87
+ column-gap: 8px;
88
+ row-gap: 6px;
89
+ align-items: center;
90
+ border: 1px solid #d9d9d9;
91
+ border-radius: 4px;
92
+ background: #fafafa;
93
+ padding: 6px 8px;
94
+ margin-bottom: 6px;
95
+ }
96
+ .opcua-client-tag-chip .opcua-tree-icon { grid-area: icon; }
97
+ .opcua-client-tag-chip .opcua-tree-title { grid-area: name; min-width: 0; }
98
+ .opcua-client-tag-right {
99
+ grid-area: right;
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 8px;
103
+ }
104
+ .opcua-client-tag-right .opcua-client-nodeid-label {
105
+ margin: 0;
106
+ max-width: 320px;
107
+ }
108
+ .opcua-client-tag-right .opcua-tree-actions {
109
+ margin: 0;
110
+ }
111
+
112
+ .opcua-client-tag-chip-meta,
113
+ .opcua-client-description {
114
+ color: #777;
115
+ font-size: 12px;
116
+ }
117
+ .opcua-client-tag-write {
118
+ display: flex;
119
+ gap: 8px;
120
+ align-items: center;
121
+ width: 100%;
122
+ margin-top: 0;
123
+ margin-left: 0;
124
+ grid-area: write;
125
+ }
126
+ .opcua-client-tag-write input {
127
+ font-size: 12px;
128
+ }
129
+ .opcua-client-tag-write input {
130
+ flex: 1 1 auto;
131
+ min-width: 120px;
132
+ }
133
+ .opcua-client-tag-write .red-ui-typedInput-container {
134
+ flex: 1 1 auto;
135
+ min-width: 220px;
136
+ width: 100% !important;
137
+ }
138
+ .opcua-client-tag-write .red-ui-typedInput-container input.red-ui-typedInput-input {
139
+ width: calc(100% - 92px);
140
+ min-width: 120px;
141
+ }
142
+
143
+ .opcua-client-load-row {
144
+ display: flex;
145
+ gap: 8px;
146
+ align-items: center;
147
+ }
148
+
149
+ .opcua-client-load-row input {
150
+ flex: 1 1 auto;
151
+ }
152
+
153
+ .opcua-tree-search {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 8px;
157
+ width: 100%;
158
+ padding: 6px 10px;
159
+ border: 1px solid #d9d9d9;
160
+ border-radius: 6px;
161
+ background: #fafafa;
162
+ }
163
+
164
+ .opcua-tree-search input {
165
+ flex: 1 1 auto;
166
+ min-width: 120px;
167
+ border: 0;
168
+ outline: 0;
169
+ background: transparent;
170
+ box-shadow: none;
171
+ margin: 0;
172
+ }
173
+
174
+ .opcua-tree-modal {
175
+ position: fixed;
176
+ inset: 0;
177
+ z-index: 2000;
178
+ background: rgba(0, 0, 0, 0.45);
179
+ padding: 24px;
180
+ box-sizing: border-box;
181
+ }
182
+
183
+ .opcua-tree-modal__dialog {
184
+ display: flex;
185
+ flex-direction: column;
186
+ width: 100%;
187
+ height: 100%;
188
+ background: #f3f3f3;
189
+ border-radius: 8px;
190
+ overflow: hidden;
191
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
192
+ }
193
+
194
+ .opcua-tree-modal__header,
195
+ .opcua-tree-modal__toolbar {
196
+ display: flex;
197
+ align-items: center;
198
+ flex-wrap: wrap;
199
+ gap: 10px;
200
+ padding: 14px 18px;
201
+ background: #fff;
202
+ border-bottom: 1px solid #d9d9d9;
203
+ }
204
+
205
+ .opcua-tree-modal__title {
206
+ font-size: 18px;
207
+ font-weight: 600;
208
+ color: #333;
209
+ }
210
+
211
+ .opcua-tree-modal__header .editor-button-small {
212
+ margin-left: auto;
213
+ }
214
+
215
+ .opcua-tree-modal__body {
216
+ flex: 1 1 auto;
217
+ overflow: auto;
218
+ padding: 18px;
219
+ }
220
+
221
+ .opcua-tree-context-menu {
222
+ position: fixed;
223
+ z-index: 2200;
224
+ min-width: 170px;
225
+ background: #fff;
226
+ border: 1px solid #d9d9d9;
227
+ border-radius: 6px;
228
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
229
+ padding: 6px 0;
230
+ }
231
+
232
+ .opcua-tree-context-menu a {
233
+ display: block;
234
+ padding: 7px 12px;
235
+ color: #333;
236
+ text-decoration: none;
237
+ font-size: 12px;
238
+ }
239
+
240
+ .opcua-tree-context-menu a:hover {
241
+ background: #f3f7fd;
242
+ }
243
+ </style>
244
+
245
+ <script type="text/javascript">
246
+ (function () {
247
+ var selectedItemsState = [];
248
+ var browseState = null;
249
+ var expansionState = {};
250
+ var browseSearchValue = "";
251
+ var browseSearchTerm = "";
252
+ var contextMenuPath = "";
253
+ var browseSelectedPath = "";
254
+
255
+ function openBrowseModal() { $("#node-input-browse-modal").show(); $("body").addClass("opcua-tree-modal-open"); }
256
+ function closeBrowseModal() {
257
+ hideTreeContextMenu();
258
+ $("#node-input-browse-modal").hide();
259
+ $("body").removeClass("opcua-tree-modal-open");
260
+ }
261
+
262
+ function getBrowseCacheKey() {
263
+ var connectionId = $("#node-input-connection").val() || "";
264
+ if (!connectionId) return "";
265
+ return "opcua-client-browse-cache:" + connectionId;
266
+ }
267
+
268
+ function saveBrowseSession() {
269
+ var key = getBrowseCacheKey();
270
+ if (!key || !window.sessionStorage) return;
271
+ try {
272
+ sessionStorage.setItem(key, JSON.stringify({
273
+ browseState: browseState,
274
+ expansionState: expansionState
275
+ }));
276
+ } catch (error) {}
277
+ }
278
+
279
+ function loadBrowseSession() {
280
+ var key = getBrowseCacheKey();
281
+ if (!key || !window.sessionStorage) return false;
282
+ try {
283
+ var raw = sessionStorage.getItem(key);
284
+ if (!raw) return false;
285
+ var parsed = JSON.parse(raw);
286
+ if (!parsed || typeof parsed !== "object") return false;
287
+ browseState = parsed.browseState && typeof parsed.browseState === "object" ? parsed.browseState : null;
288
+ expansionState = parsed.expansionState && typeof parsed.expansionState === "object" ? parsed.expansionState : {};
289
+ return !!browseState;
290
+ } catch (error) {
291
+ return false;
292
+ }
293
+ }
294
+
295
+ function parseSelectedItems(rawValue) {
296
+ if (!rawValue) {
297
+ return [];
298
+ }
299
+
300
+ try {
301
+ var parsed = JSON.parse(rawValue);
302
+ if (!Array.isArray(parsed)) {
303
+ return [];
304
+ }
305
+
306
+ return parsed
307
+ .filter(function (item) {
308
+ return item && typeof item === "object" && !Array.isArray(item);
309
+ })
310
+ .map(function (item) {
311
+ return {
312
+ name: typeof item.name === "string" ? item.name.trim() : "",
313
+ nodeID: typeof (item.nodeID || item.nodeId) === "string" ? String(item.nodeID || item.nodeId).trim() : "",
314
+ type: typeof (item.type || item.dataType) === "string" ? String(item.type || item.dataType).trim() : "",
315
+ nodeClass: typeof item.nodeClass === "string" ? item.nodeClass.trim() : "",
316
+ typeDefinition: typeof (item.typeDefinition || item.typeDefinitionName) === "string" ? String(item.typeDefinition || item.typeDefinitionName).trim() : "",
317
+ hasTypeDefinition: item.hasTypeDefinition && typeof item.hasTypeDefinition === "object" ? item.hasTypeDefinition : null,
318
+ valueProperty: typeof item.valueProperty === "string" && item.valueProperty.trim() ? item.valueProperty.trim() : "payload",
319
+ valuePropertyType: (item.valuePropertyType === "msg" || item.valuePropertyType === "flow" || item.valuePropertyType === "global") ? item.valuePropertyType : "msg"
320
+ };
321
+ })
322
+ .filter(function (item) {
323
+ return !!item.nodeID;
324
+ });
325
+ } catch (error) {
326
+ return [];
327
+ }
328
+ }
329
+
330
+ function serializeSelectedItems(items) {
331
+ return JSON.stringify(items.map(function (item) {
332
+ var result = {
333
+ name: item.name || item.nodeID,
334
+ nodeID: item.nodeID
335
+ };
336
+
337
+ if (item.type) {
338
+ result.type = item.type;
339
+ }
340
+ if (item.nodeClass) {
341
+ result.nodeClass = item.nodeClass;
342
+ }
343
+ if (item.typeDefinition) {
344
+ result.typeDefinition = item.typeDefinition;
345
+ }
346
+ if (item.hasTypeDefinition) {
347
+ result.hasTypeDefinition = item.hasTypeDefinition;
348
+ }
349
+ if (item.valueProperty) {
350
+ result.valueProperty = item.valueProperty;
351
+ }
352
+ if (item.valuePropertyType) {
353
+ result.valuePropertyType = item.valuePropertyType;
354
+ }
355
+
356
+ return result;
357
+ }), null, 2);
358
+ }
359
+
360
+ function escapeHtml(value) {
361
+ return String(value || "").replace(/[&<>"']/g, function (char) {
362
+ return {
363
+ "&": "&amp;",
364
+ "<": "&lt;",
365
+ ">": "&gt;",
366
+ '"': "&quot;",
367
+ "'": "&#39;"
368
+ }[char];
369
+ });
370
+ }
371
+
372
+ function updateSelectedItemsField() {
373
+ $("#node-input-selectedItems").val(serializeSelectedItems(selectedItemsState));
374
+ }
375
+
376
+ function normalizeSearchTerm(value) {
377
+ return String(value || "").trim().toLowerCase();
378
+ }
379
+
380
+ function textForSearch(item) {
381
+ if (!item || typeof item !== "object") return "";
382
+ return [
383
+ item.name,
384
+ item.displayName,
385
+ item.browseName,
386
+ item.nodeID,
387
+ item.nodeClass,
388
+ item.dataType,
389
+ item.description
390
+ ].filter(Boolean).join(" ").toLowerCase();
391
+ }
392
+
393
+ function nodeMatchesSearch(item, term) {
394
+ if (!term) return true;
395
+ return textForSearch(item).indexOf(term) >= 0;
396
+ }
397
+
398
+ function branchHasSearchMatch(item, term) {
399
+ if (nodeMatchesSearch(item, term)) return true;
400
+ if (!item || !Array.isArray(item.browse)) return false;
401
+ for (var i = 0; i < item.browse.length; i += 1) {
402
+ if (branchHasSearchMatch(item.browse[i], term)) return true;
403
+ }
404
+ return false;
405
+ }
406
+
407
+ function renderSelectedTags() {
408
+ var container = $("#node-input-selected-tags");
409
+ container.empty();
410
+ var writeMode = $("#node-input-mode").val() === "write";
411
+
412
+ if (!selectedItemsState.length) {
413
+ container.append('<div class="opcua-tree-empty">No tags selected.</div>');
414
+ return;
415
+ }
416
+
417
+ selectedItemsState.forEach(function (item, index) {
418
+ var row = $('<div class="opcua-client-tag-chip"></div>');
419
+ var icon = browseIconFor(item);
420
+ row.append('<div class="opcua-tree-icon"><i class="fa ' + icon + '"></i></div>');
421
+ row.append('<div class="opcua-tree-title">' + escapeHtml(item.name || item.nodeID) + '</div>');
422
+ if (writeMode) {
423
+ var type = (item.valuePropertyType === "flow" || item.valuePropertyType === "global") ? item.valuePropertyType : "msg";
424
+ var prop = item.valueProperty || "payload";
425
+ row.append('<div class="opcua-client-tag-write">'
426
+ + '<input type="text" class="opcua-client-item-value-prop" id="opcua-client-item-value-prop-' + index + '" data-index="' + index + '" value="' + escapeHtml(prop) + '" placeholder="payload">'
427
+ + '<input type="hidden" class="opcua-client-item-value-type" id="opcua-client-item-value-type-' + index + '" data-index="' + index + '" value="' + escapeHtml(type) + '">'
428
+ + "</div>");
429
+ }
430
+ row.append('<div class="opcua-client-tag-right">'
431
+ + '<div class="opcua-client-nodeid-label">' + escapeHtml(item.nodeID) + '</div>'
432
+ + '<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>'
433
+ + "</div>");
434
+ container.append(row);
435
+ });
436
+ initializeSelectedItemTypedInputs();
437
+ }
438
+
439
+ function initializeSelectedItemTypedInputs() {
440
+ $(".opcua-client-item-value-prop").each(function () {
441
+ var input = $(this);
442
+ var index = Number(input.attr("data-index"));
443
+ var typeField = "#opcua-client-item-value-type-" + index;
444
+ if (input.data("typedInputInitialized")) {
445
+ input.typedInput("types", ["msg", "flow", "global"]);
446
+ return;
447
+ }
448
+
449
+ input.typedInput({
450
+ type: $(typeField).val() || "msg",
451
+ types: ["msg", "flow", "global"],
452
+ typeField: typeField
453
+ });
454
+ input.data("typedInputInitialized", true);
455
+ });
456
+ }
457
+
458
+ function syncSelectedItems() {
459
+ updateSelectedItemsField();
460
+ renderSelectedTags();
461
+ renderBrowseTree();
462
+ }
463
+
464
+ function isExpanded(path, defaultValue) {
465
+ if (expansionState[path] === undefined) {
466
+ expansionState[path] = !!defaultValue;
467
+ }
468
+
469
+ return expansionState[path];
470
+ }
471
+
472
+ function selectedIndexByNodeId(nodeId) {
473
+ return selectedItemsState.findIndex(function (item) {
474
+ return item.nodeID === nodeId;
475
+ });
476
+ }
477
+
478
+ function canExpand(item) {
479
+ // return item && item.nodeClass !== "Variable" && item.nodeClass !== "Method";
480
+ return item && item.nodeClass !== "Variable"
481
+ }
482
+
483
+ function isVariable(item) {
484
+ return String(item && item.nodeClass || "").toLowerCase() === "variable";
485
+ }
486
+
487
+ function nodeIdOf(item) {
488
+ return item && (item.nodeID || item.nodeId) ? String(item.nodeID || item.nodeId) : "";
489
+ }
490
+
491
+ function loadBrowse(nodeId) {
492
+ var connectionId = $("#node-input-connection").val();
493
+ if (!connectionId) {
494
+ RED.notify("Select an OPC UA connection before browsing.", "warning");
495
+ return $.Deferred().reject().promise();
496
+ }
497
+
498
+ return $.getJSON("opcua-client-config/" + encodeURIComponent(connectionId) + "/browse", {
499
+ nodeId: nodeId || "i=84"
500
+ });
501
+ }
502
+
503
+ function renderBrowseTree() {
504
+ var container = $("#node-input-browse-tree");
505
+ container.empty();
506
+
507
+ if (!browseState) {
508
+ container.append('<div class="opcua-tree-empty">Click Browse to load the server tree.</div>');
509
+ return;
510
+ }
511
+
512
+ if (browseSearchTerm) {
513
+ if (!branchHasSearchMatch(browseState, browseSearchTerm)) {
514
+ container.append('<div class="opcua-tree-empty">No items found in the already explored items.</div>');
515
+ return;
516
+ }
517
+ renderBrowseRootFiltered(browseState, "root", 0, container, browseSearchTerm);
518
+ return;
519
+ }
520
+
521
+ renderBrowseRoot(browseState, "root", 0, container);
522
+ }
523
+
524
+ function isFolderNode(item) {
525
+ if (!item) return false;
526
+ var nodeClass = String(item.nodeClass || "");
527
+ var typeDefinition = String(item.typeDefinition || item.typeDefinitionName || "").toLowerCase();
528
+ var hasTypeDefinitionBrowseName = String(
529
+ item.hasTypeDefinition && item.hasTypeDefinition.browseName || ""
530
+ ).toLowerCase();
531
+ var explicitType = String(item.type || item.kind || "").toLowerCase();
532
+ if (nodeClass === "Folder") return true;
533
+ if (hasTypeDefinitionBrowseName === "foldertype") return true;
534
+ if (typeDefinition.indexOf("folder") >= 0) return true;
535
+ if (explicitType === "folder") return true;
536
+ return false;
537
+ }
538
+
539
+ function browseIconFor(item) {
540
+ var nodeClass = String((item && item.nodeClass) || "");
541
+ if (isFolderNode(item)) return "fa-folder";
542
+ if (nodeClass === "Object") return "fa-cube";
543
+ if (nodeClass === "Method") return "fa-cog";
544
+ if (nodeClass === "Variable") return "fa-tag";
545
+ if (nodeClass === "ObjectType") return "fa-cubes";
546
+ if (nodeClass === "View") return "fa-eye";
547
+ if (nodeClass === "DataType") return "fa-database";
548
+ if (nodeClass === "ReferenceType") return "fa-random";
549
+ return "fa-tag";
550
+ }
551
+
552
+ function renderBrowseRoot(root, path, depth, container) {
553
+ var expanded = isExpanded(path, true);
554
+ var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
555
+ row.append('<span class="opcua-tree-indent"></span>');
556
+ row.append('<span class="opcua-tree-twisty opcua-client-toggle-tree" data-path="' + path + '">' + (expanded ? '<i class="fa fa-caret-down"></i>' : '<i class="fa fa-caret-right"></i>') + '</span>');
557
+ row.append('<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>');
558
+ row.append('<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>');
559
+ row.append('<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>');
560
+ container.append(row);
561
+
562
+ if (!expanded) return;
563
+
564
+ if (!Array.isArray(root.browse) || !root.browse.length) {
565
+ container.append('<div class="opcua-tree-empty">No items found..</div>');
566
+ } else {
567
+ root.browse.forEach(function (item, index) {
568
+ renderBrowseItem(item, path + ".browse." + index, depth + 1, container);
569
+ });
570
+ }
571
+ }
572
+
573
+ function renderBrowseItem(item, path, depth, container) {
574
+ var expanded = isExpanded(path, false);
575
+ var nodeId = nodeIdOf(item);
576
+ var selectedIndex = selectedIndexByNodeId(nodeId);
577
+ var hasChildren = canExpand(item);
578
+ var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
579
+ if (selectedIndex >= 0) row.addClass("is-selected");
580
+
581
+ for (var i = 0; i < depth; i += 1) {
582
+ row.append('<span class="opcua-tree-indent"></span>');
583
+ }
584
+ row.append('<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + path + '">' + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>');
585
+ row.append('<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>');
586
+ row.append('<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>');
587
+ row.append('<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>');
588
+ row.append('<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>');
589
+
590
+ if (nodeId) {
591
+ var actionLabel = selectedIndex >= 0 ? "Remove" : "Add";
592
+ var actionIcon = selectedIndex >= 0 ? "fa-minus" : "fa-plus";
593
+ row.append('<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + path + '"><i class="fa ' + actionIcon + '"></i> ' + actionLabel + '</a></div>');
594
+ }
595
+ container.append(row);
596
+
597
+ if (item.description) {
598
+ container.append('<div class="opcua-client-description" style="padding: 0 10px 8px ' + String((depth + 2) * 14) + 'px;">' + escapeHtml(item.description) + '</div>');
599
+ }
600
+
601
+ if (expanded && hasChildren) {
602
+ if (Array.isArray(item.browse)) {
603
+ if (!item.browse.length) {
604
+ container.append('<div class="opcua-tree-empty">No children found..</div>');
605
+ } else {
606
+ item.browse.forEach(function (child, index) {
607
+ renderBrowseItem(child, path + ".browse." + index, depth + 1, container);
608
+ });
609
+ }
610
+ } else {
611
+ container.append('<div class="opcua-tree-empty">Searching for items...</div>');
612
+ }
613
+ }
614
+ }
615
+
616
+ function renderBrowseRootFiltered(root, path, depth, container, term) {
617
+ var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
618
+ row.append('<span class="opcua-tree-indent"></span>');
619
+ row.append('<span class="opcua-tree-twisty"><i class="fa fa-caret-down"></i></span>');
620
+ row.append('<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>');
621
+ row.append('<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>');
622
+ row.append('<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>');
623
+ container.append(row);
624
+
625
+ if (!Array.isArray(root.browse) || !root.browse.length) {
626
+ container.append('<div class="opcua-tree-empty">Nenhum item encontrado.</div>');
627
+ return;
628
+ }
629
+
630
+ root.browse.forEach(function (item, index) {
631
+ if (branchHasSearchMatch(item, term)) {
632
+ renderBrowseItemFiltered(item, path + ".browse." + index, depth + 1, container, term, false);
633
+ }
634
+ });
635
+ }
636
+
637
+ function renderBrowseItemFiltered(item, path, depth, container, term, ancestorMatched) {
638
+ if (!branchHasSearchMatch(item, term)) return;
639
+
640
+ var nodeId = nodeIdOf(item);
641
+ var selectedIndex = selectedIndexByNodeId(nodeId);
642
+ var hasChildren = canExpand(item);
643
+ var subtreeVisible = !!ancestorMatched || nodeMatchesSearch(item, term);
644
+ var hasMatchingLoadedChild = hasChildren && Array.isArray(item.browse) && item.browse.some(function (child) {
645
+ return branchHasSearchMatch(child, term);
646
+ });
647
+ var hasExplicitExpansion = expansionState[path] !== undefined;
648
+ var expanded = hasChildren && (hasExplicitExpansion
649
+ ? !!expansionState[path]
650
+ : ((subtreeVisible && Array.isArray(item.browse)) || hasMatchingLoadedChild));
651
+ var row = $('<div class="opcua-tree-row"></div>').attr("data-path", path);
652
+ if (selectedIndex >= 0) row.addClass("is-selected");
653
+
654
+ for (var i = 0; i < depth; i += 1) row.append('<span class="opcua-tree-indent"></span>');
655
+ row.append('<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + path + '">' + (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>');
656
+ row.append('<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>');
657
+ row.append('<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>');
658
+ row.append('<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>');
659
+ row.append('<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>');
660
+
661
+ if (nodeId) {
662
+ var actionLabel = selectedIndex >= 0 ? "Remove" : "Add";
663
+ var actionIcon = selectedIndex >= 0 ? "fa-minus" : "fa-plus";
664
+ row.append('<div class="opcua-tree-actions"><a href="#" class="editor-button editor-button-small opcua-client-toggle-tag" data-nodeid="' + escapeHtml(nodeId) + '" data-path="' + path + '"><i class="fa ' + actionIcon + '"></i> ' + actionLabel + '</a></div>');
665
+ }
666
+ container.append(row);
667
+
668
+ if (item.description) {
669
+ container.append('<div class="opcua-client-description" style="padding: 0 10px 8px ' + String((depth + 2) * 14) + 'px;">' + escapeHtml(item.description) + '</div>');
670
+ }
671
+
672
+ if (expanded && hasChildren) {
673
+ if (Array.isArray(item.browse)) {
674
+ if (!item.browse.length) {
675
+ container.append('<div class="opcua-tree-empty">Nenhum filho encontrado.</div>');
676
+ } else {
677
+ item.browse.forEach(function (child, index) {
678
+ if (subtreeVisible || branchHasSearchMatch(child, term)) {
679
+ renderBrowseItemFiltered(child, path + ".browse." + index, depth + 1, container, term, subtreeVisible);
680
+ }
681
+ });
682
+ }
683
+ } else {
684
+ container.append('<div class="opcua-tree-empty">Expandindo...</div>');
685
+ }
686
+ }
687
+ }
688
+
689
+ function getItemAtPath(path) {
690
+ var tokens = String(path || "").split(".");
691
+ var current = browseState;
692
+
693
+ for (var index = 0; index < tokens.length; index += 1) {
694
+ var token = tokens[index];
695
+ if (!token || token === "root") {
696
+ continue;
697
+ }
698
+
699
+ if (!current) {
700
+ return null;
701
+ }
702
+
703
+ if (/^\d+$/.test(token)) {
704
+ current = current[Number(token)];
705
+ } else {
706
+ current = current[token];
707
+ }
708
+ }
709
+
710
+ return current || null;
711
+ }
712
+
713
+ function addSelectedItem(item) {
714
+ var normalized = {
715
+ name: item.displayName || item.browseName || item.name || item.nodeID,
716
+ nodeID: item.nodeID || item.nodeId,
717
+ type: item.dataType || item.type || "",
718
+ nodeClass: item.nodeClass || "",
719
+ typeDefinition: item.typeDefinition || item.typeDefinitionName || "",
720
+ hasTypeDefinition: item.hasTypeDefinition || null,
721
+ valueProperty: item.valueProperty || "payload",
722
+ valuePropertyType: item.valuePropertyType || "msg"
723
+ };
724
+ var currentIndex = selectedIndexByNodeId(normalized.nodeID);
725
+
726
+ if (currentIndex >= 0) {
727
+ selectedItemsState[currentIndex] = normalized;
728
+ } else {
729
+ selectedItemsState.push(normalized);
730
+ }
731
+
732
+ syncSelectedItems();
733
+ }
734
+
735
+ function removeSelectedItemByIndex(index) {
736
+ selectedItemsState.splice(index, 1);
737
+ syncSelectedItems();
738
+ }
739
+
740
+ function toggleSelectedNode(path) {
741
+ var item = getItemAtPath(path);
742
+ var nodeId = nodeIdOf(item);
743
+ if (!item || !nodeId) {
744
+ return;
745
+ }
746
+
747
+ var currentIndex = selectedIndexByNodeId(nodeId);
748
+ if (currentIndex >= 0) {
749
+ selectedItemsState.splice(currentIndex, 1);
750
+ } else {
751
+ addSelectedItem(item);
752
+ return;
753
+ }
754
+
755
+ syncSelectedItems();
756
+ }
757
+
758
+ function refreshBrowseRoot() {
759
+ var rootNodeId = "i=84";
760
+ var container = $("#node-input-browse-tree");
761
+ container.html('<div class="opcua-tree-empty">Carregando...</div>');
762
+
763
+ loadBrowse(rootNodeId).done(function (payload) {
764
+ browseState = payload;
765
+ expansionState = { root: true };
766
+ saveBrowseSession();
767
+ renderBrowseTree();
768
+ }).fail(function (xhr) {
769
+ var message = xhr && xhr.responseJSON && xhr.responseJSON.error
770
+ ? xhr.responseJSON.error
771
+ : "Falha ao navegar no servidor OPC UA.";
772
+ browseState = null;
773
+ container.html('<div class="opcua-tree-empty">' + escapeHtml(message) + '</div>');
774
+ RED.notify(message, "error");
775
+ });
776
+ }
777
+
778
+ function hideTreeContextMenu() {
779
+ contextMenuPath = "";
780
+ $("#node-input-browse-context-menu").hide();
781
+ }
782
+
783
+ function showTreeContextMenu(x, y, path) {
784
+ var menu = $("#node-input-browse-context-menu");
785
+ var item = getItemAtPath(path);
786
+ contextMenuPath = path || "";
787
+ $("#node-input-browse-context-refresh").toggle(!!item && !isVariable(item));
788
+ $("#node-input-browse-context-copy-nodeid").toggle(!!nodeIdOf(item));
789
+ menu.css({ left: x + "px", top: y + "px" }).show();
790
+ }
791
+
792
+ function copyNodeIdFromPath(path) {
793
+ var item = getItemAtPath(path);
794
+ var nodeId = nodeIdOf(item);
795
+ if (!nodeId) {
796
+ RED.notify("NodeID nao encontrado para o item selecionado.", "warning");
797
+ return;
798
+ }
799
+
800
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
801
+ navigator.clipboard.writeText(nodeId).then(function () {
802
+ RED.notify("NodeID copiado.", "success");
803
+ }).catch(function () {
804
+ RED.notify("Falha ao copiar NodeID.", "error");
805
+ });
806
+ return;
807
+ }
808
+
809
+ var input = $("<textarea readonly></textarea>").val(nodeId).css({
810
+ position: "fixed",
811
+ left: "-9999px",
812
+ top: "0"
813
+ });
814
+ $("body").append(input);
815
+ input[0].select();
816
+ try {
817
+ document.execCommand("copy");
818
+ RED.notify("NodeID copiado.", "success");
819
+ } catch (error) {
820
+ RED.notify("Falha ao copiar NodeID.", "error");
821
+ }
822
+ input.remove();
823
+ }
824
+
825
+ function setBrowseSelectedPath(path) {
826
+ browseSelectedPath = path || "";
827
+ $(".opcua-tree-row").removeClass("is-selected");
828
+ if (browseSelectedPath) {
829
+ $('.opcua-tree-row[data-path="' + browseSelectedPath + '"]').addClass("is-selected");
830
+ }
831
+ }
832
+
833
+ function refreshNode(path) {
834
+ if (!path) {
835
+ return;
836
+ }
837
+ if (path === "root") {
838
+ refreshBrowseRoot();
839
+ return;
840
+ }
841
+
842
+ var item = getItemAtPath(path);
843
+ if (!item || isVariable(item)) {
844
+ return;
845
+ }
846
+
847
+ // Force a new discovery using the same lazy-load flow from expand.
848
+ item.browse = undefined;
849
+ expansionState[path] = true;
850
+ saveBrowseSession();
851
+ renderBrowseTree();
852
+ expandNode(path);
853
+ }
854
+
855
+ function expandNode(path) {
856
+ var item = getItemAtPath(path);
857
+ if (!item || !canExpand(item)) {
858
+ return;
859
+ }
860
+
861
+ if (isExpanded(path, false) && !Array.isArray(item.browse)) {
862
+ renderBrowseTree();
863
+ } else {
864
+ expansionState[path] = !isExpanded(path, false);
865
+ saveBrowseSession();
866
+ if (!expansionState[path]) {
867
+ renderBrowseTree();
868
+ return;
869
+ }
870
+ }
871
+
872
+ if (Array.isArray(item.browse)) {
873
+ renderBrowseTree();
874
+ return;
875
+ }
876
+
877
+ renderBrowseTree();
878
+ var browseNodeId = item.nodeID || item.nodeId;
879
+ loadBrowse(browseNodeId).done(function (payload) {
880
+ item.browse = Array.isArray(payload.browse) ? payload.browse : [];
881
+ saveBrowseSession();
882
+ renderBrowseTree();
883
+ }).fail(function (xhr) {
884
+ expansionState[path] = false;
885
+ saveBrowseSession();
886
+ renderBrowseTree();
887
+ var message = xhr && xhr.responseJSON && xhr.responseJSON.error
888
+ ? xhr.responseJSON.error
889
+ : "Falha ao expandir o node.";
890
+ RED.notify(message, "error");
891
+ });
892
+ }
893
+
894
+ function toggleModeFields() {
895
+ var isSubscription = $("#node-input-mode").val() === "subscription" || $("#node-input-mode").val() === "events";
896
+ var supportsSelection = $("#node-input-mode").val() !== "method" && $("#node-input-mode").val() !== "getSubscriptionId" ;
897
+ $(".opcua-client-subscription-row").toggle(isSubscription);
898
+ $(".opcua-client-selection-row").toggle(supportsSelection);
899
+ renderSelectedTags();
900
+ }
901
+
902
+ RED.nodes.registerType("opcua-client", {
903
+ category: "network",
904
+ color: "#d9edf7",
905
+ defaults: {
906
+ name: { value: "" },
907
+ connection: { value: "", type: "opcua-client-config", required: true },
908
+ mode: { value: "read", required: true },
909
+ selectedItems: {
910
+ value: "[]",
911
+ validate: function (value) {
912
+ try {
913
+ return Array.isArray(JSON.parse(value || "[]"));
914
+ } catch (error) {
915
+ return false;
916
+ }
917
+ }
918
+ },
919
+ samplingInterval: {
920
+ value: 250,
921
+ validate: RED.validators.number()
922
+ },
923
+ publishingInterval: {
924
+ value: 250,
925
+ validate: RED.validators.number()
926
+ }
927
+ },
928
+ inputs: 1,
929
+ outputs: 1,
930
+ icon: "bridge.svg",
931
+ label: function () {
932
+ return this.name || "opcua-client";
933
+ },
934
+ oneditprepare: function () {
935
+ $("#node-input-selectedItems").typedInput({
936
+ type: "json",
937
+ types: ["json"]
938
+ });
939
+
940
+ selectedItemsState = parseSelectedItems(this.selectedItems);
941
+
942
+ browseState = null;
943
+ expansionState = {};
944
+ browseSelectedPath = "";
945
+ browseSearchValue = "";
946
+ browseSearchTerm = "";
947
+ $("#node-input-browse-search").val("");
948
+ $("#node-input-browse-search-clear").hide();
949
+ loadBrowseSession();
950
+ syncSelectedItems();
951
+ renderBrowseTree();
952
+
953
+ $("#node-input-mode").off("change").on("change", toggleModeFields);
954
+ $("#node-input-browse-root").off("click").on("click", function (event) {
955
+ event.preventDefault();
956
+ refreshBrowseRoot();
957
+ });
958
+ $("#node-input-open-browse-modal").off("click").on("click", function (event) {
959
+ event.preventDefault();
960
+ openBrowseModal();
961
+ });
962
+ $("#node-input-close-browse-modal").off("click").on("click", function (event) {
963
+ event.preventDefault();
964
+ closeBrowseModal();
965
+ });
966
+ $("#node-input-browse-modal").off("click").on("click", function (event) {
967
+ hideTreeContextMenu();
968
+ if (event.target === this) closeBrowseModal();
969
+ });
970
+ $("#node-input-browse-search").off("input").on("input", function () {
971
+ browseSearchValue = $(this).val();
972
+ browseSearchTerm = normalizeSearchTerm(browseSearchValue);
973
+ $("#node-input-browse-search-clear").toggle(!!browseSearchTerm);
974
+ renderBrowseTree();
975
+ });
976
+ $("#node-input-browse-search-clear").off("click").on("click", function (event) {
977
+ event.preventDefault();
978
+ browseSearchValue = "";
979
+ browseSearchTerm = "";
980
+ $("#node-input-browse-search").val("");
981
+ $(this).hide();
982
+ renderBrowseTree();
983
+ });
984
+ $("#node-input-connection").off("change.opcuaClientBrowse").on("change.opcuaClientBrowse", function () {
985
+ browseState = null;
986
+ expansionState = {};
987
+ browseSelectedPath = "";
988
+ loadBrowseSession();
989
+ renderBrowseTree();
990
+ });
991
+ $(document).off("keydown.opcuaClientBrowseModal").on("keydown.opcuaClientBrowseModal", function (event) {
992
+ hideTreeContextMenu();
993
+ if (event.key === "Escape") closeBrowseModal();
994
+ });
995
+
996
+ toggleModeFields();
997
+ },
998
+ oneditsave: function () {
999
+ updateSelectedItemsField();
1000
+ saveBrowseSession();
1001
+ closeBrowseModal();
1002
+ $(document).off("keydown.opcuaClientBrowseModal");
1003
+ }
1004
+ ,
1005
+ oneditcancel: function () {
1006
+ closeBrowseModal();
1007
+ $(document).off("keydown.opcuaClientBrowseModal");
1008
+ }
1009
+ });
1010
+
1011
+ $(document).on("change", "#node-input-selectedItems", function () {
1012
+ selectedItemsState = parseSelectedItems($(this).val());
1013
+ renderSelectedTags();
1014
+ renderBrowseTree();
1015
+ });
1016
+
1017
+ $(document).on("click", ".opcua-client-remove-tag", function (event) {
1018
+ event.preventDefault();
1019
+ removeSelectedItemByIndex(Number($(this).attr("data-index")));
1020
+ });
1021
+
1022
+ $(document).on("click", ".opcua-client-toggle-tag", function (event) {
1023
+ event.preventDefault();
1024
+ toggleSelectedNode($(this).attr("data-path"));
1025
+ });
1026
+
1027
+ $(document).on("click", ".opcua-client-toggle-tree", function (event) {
1028
+ event.preventDefault();
1029
+ expandNode($(this).attr("data-path"));
1030
+ });
1031
+ $(document).on("change", ".opcua-client-item-value-type", function () {
1032
+ var index = Number($(this).attr("data-index"));
1033
+ if (!selectedItemsState[index]) return;
1034
+ selectedItemsState[index].valuePropertyType = $(this).val();
1035
+ updateSelectedItemsField();
1036
+ });
1037
+ $(document).on("change input", ".opcua-client-item-value-prop", function () {
1038
+ var index = Number($(this).attr("data-index"));
1039
+ if (!selectedItemsState[index]) return;
1040
+ selectedItemsState[index].valueProperty = $(this).typedInput ? $(this).typedInput("value") : $(this).val();
1041
+ var typeField = $("#opcua-client-item-value-type-" + index);
1042
+ selectedItemsState[index].valuePropertyType = (typeField.val() || "msg");
1043
+ updateSelectedItemsField();
1044
+ });
1045
+
1046
+ $(document).on("click", ".opcua-tree-row", function (event) {
1047
+ if ($(event.target).closest(".opcua-client-toggle-tree, .opcua-client-toggle-tag, .opcua-tree-actions, #node-input-browse-context-menu").length) {
1048
+ return;
1049
+ }
1050
+ setBrowseSelectedPath($(this).attr("data-path"));
1051
+ });
1052
+
1053
+ $(document).on("contextmenu", ".opcua-tree-row", function (event) {
1054
+ var clickedPath = $(this).attr("data-path");
1055
+ if (clickedPath) {
1056
+ setBrowseSelectedPath(clickedPath);
1057
+ }
1058
+ var path = browseSelectedPath || clickedPath;
1059
+ var item = getItemAtPath(path);
1060
+ if (!item) {
1061
+ hideTreeContextMenu();
1062
+ return;
1063
+ }
1064
+
1065
+ event.preventDefault();
1066
+ showTreeContextMenu(event.clientX, event.clientY, path);
1067
+ });
1068
+
1069
+ $(document).on("click", "#node-input-browse-context-refresh", function (event) {
1070
+ event.preventDefault();
1071
+ var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
1072
+ var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
1073
+ hideTreeContextMenu();
1074
+ refreshNode(path);
1075
+ });
1076
+
1077
+ $(document).on("click", "#node-input-browse-context-copy-nodeid", function (event) {
1078
+ event.preventDefault();
1079
+ var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
1080
+ var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
1081
+ hideTreeContextMenu();
1082
+ copyNodeIdFromPath(path);
1083
+ });
1084
+
1085
+ $(document).on("click", function (event) {
1086
+ if (!$(event.target).closest("#node-input-browse-context-menu").length) {
1087
+ hideTreeContextMenu();
1088
+ }
1089
+ });
1090
+ })();
1091
+ </script>
1092
+
1093
+ <script type="text/html" data-template-name="opcua-client">
1094
+ <div class="form-row">
1095
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
1096
+ <input type="text" id="node-input-name" placeholder="opcua-client">
1097
+ </div>
1098
+ <div class="form-row">
1099
+ <label for="node-input-connection"><i class="fa fa-server"></i> Connection</label>
1100
+ <input type="text" id="node-input-connection">
1101
+ </div>
1102
+ <div class="form-row">
1103
+ <label for="node-input-mode"><i class="fa fa-exchange"></i> Mode</label>
1104
+ <select id="node-input-mode">
1105
+ <option value="read">Read</option>
1106
+ <option value="write">Write</option>
1107
+ <option value="browse">Browse</option>
1108
+ <option value="method">Method</option>
1109
+ <option value="getSubscriptionId">getSubscriptionId</option>
1110
+ <option value="subscription">Subscription</option>
1111
+ <option value="events">Events</option>
1112
+ </select>
1113
+ </div>
1114
+ <div class="form-row opcua-client-selection-row">
1115
+ <label for="node-input-selectedItems"><i class="fa fa-code"></i> JSON</label>
1116
+ <input type="text" id="node-input-selectedItems" class="opcua-client-json" style="width: 70%;">
1117
+ </div>
1118
+ <div class="form-row opcua-client-selection-row">
1119
+ <label style="width:auto;"><i class="fa fa-sitemap"></i> Browse Tree</label>
1120
+ <a href="#" id="node-input-open-browse-modal" class="editor-button"><i class="fa fa-expand"></i> Open browse tree</a>
1121
+ </div>
1122
+ <div id="node-input-browse-modal" class="opcua-tree-modal" style="display:none;">
1123
+ <div class="opcua-tree-modal__dialog">
1124
+ <div class="opcua-tree-modal__header">
1125
+ <div class="opcua-tree-modal__title"><i class="fa fa-sitemap"></i> OPC UA Browse Tree</div>
1126
+ <a href="#" id="node-input-close-browse-modal" class="editor-button editor-button-small"><i class="fa fa-times"></i> Close</a>
1127
+ </div>
1128
+ <div class="opcua-tree-modal__toolbar">
1129
+ <a href="#" id="node-input-browse-root" class="editor-button editor-button-small"><i class="fa fa-search"></i> Browse Root</a>
1130
+ <div class="opcua-tree-search">
1131
+ <i class="fa fa-search"></i>
1132
+ <input type="text" id="node-input-browse-search" placeholder="Search for items already explored">
1133
+ <a href="#" id="node-input-browse-search-clear" class="editor-button editor-button-small" style="display:none;"><i class="fa fa-times"></i></a>
1134
+ </div>
1135
+ </div>
1136
+ <div class="opcua-tree-modal__body">
1137
+ <div id="node-input-browse-tree" class="opcua-tree-editor"></div>
1138
+ </div>
1139
+ </div>
1140
+ <div id="node-input-browse-context-menu" class="opcua-tree-context-menu" style="display:none;">
1141
+ <a href="#" id="node-input-browse-context-refresh"><i class="fa fa-refresh"></i> Refresh node</a>
1142
+ <a href="#" id="node-input-browse-context-copy-nodeid"><i class="fa fa-copy"></i> Copy NodeID</a>
1143
+ </div>
1144
+ </div>
1145
+ <div class="form-row opcua-client-subscription-row">
1146
+ <label for="node-input-samplingInterval"><i class="fa fa-clock-o"></i> Sampling</label>
1147
+ <input type="text" id="node-input-samplingInterval" placeholder="250">
1148
+ </div>
1149
+ <div class="form-row opcua-client-subscription-row">
1150
+ <label for="node-input-publishingInterval"><i class="fa fa-clock-o"></i> Publishing</label>
1151
+ <input type="text" id="node-input-publishingInterval" placeholder="250">
1152
+ </div>
1153
+ <div class="form-row opcua-client-selection-row" style="margin-bottom:0;">
1154
+ <label style="width:auto;"><i class="fa fa-tags"></i> Selected items</label>
1155
+ </div>
1156
+ <div class="form-row opcua-client-selection-row">
1157
+ <label style="width:auto;"></label>
1158
+ <div id="node-input-selected-tags" class="opcua-client-tag-box"></div>
1159
+ </div>
1160
+
1161
+
1162
+
1163
+ </script>
1164
+
1165
+ <script type="text/html" data-help-name="opcua-client">
1166
+ <p>Unified OPC UA client node for read, write, browse and subscription using a shared client configuration node.</p>
1167
+
1168
+ <h3>Modes</h3>
1169
+ <p><b>Read</b>: reads one or more variable values.</p>
1170
+ <p><b>Write</b>: writes one or more variable values.</p>
1171
+ <p><b>Browse</b>: browses one or more OPC UA nodes and returns their children.</p>
1172
+ <p><b>Method</b>: calls one or more OPC UA methods.</p>
1173
+ <p><b>Subscription</b>: subscribes to one or more variable values and emits one message per change.</p>
1174
+
1175
+ <h3>Read</h3>
1176
+ <p><b>Input</b> <code>msg.payload</code>:</p>
1177
+ <pre><code>[
1178
+ {
1179
+ "name": "status",
1180
+ "nodeID": "ns=2;s=Factory.Line1.Motor1.status"
1181
+ }
1182
+ ]</code></pre>
1183
+ <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node reads that configured node.</p>
1184
+
1185
+ <h3>Write</h3>
1186
+ <p><b>Input</b> <code>msg.payload</code>:</p>
1187
+ <pre><code>[
1188
+ {
1189
+ "name": "speed",
1190
+ "nodeID": "ns=2;s=Factory.Line1.Motor1.speed",
1191
+ "value": 25.5,
1192
+ "type": "Float"
1193
+ }
1194
+ ]</code></pre>
1195
+
1196
+ <h3>Browse</h3>
1197
+ <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node browses that configured node.</p>
1198
+ <p>If <code>msg.payload = []</code>, the node browses the OPC UA <code>RootFolder</code>.</p>
1199
+
1200
+ <h3>Method</h3>
1201
+ <p><code>methodId</code> and <code>objectId</code> are accepted explicitly. For backward compatibility, <code>nodeID</code> is also accepted as the method id.</p>
1202
+ <p><b>Input</b> <code>msg.payload</code>:</p>
1203
+ <pre><code>[
1204
+ {
1205
+ "name": "method1",
1206
+ "nodeID": "ns=2;s=motor.method1",
1207
+ "inputs": [
1208
+ {
1209
+ "name": "input1",
1210
+ "type": "Int32",
1211
+ "value": 1
1212
+ },
1213
+ {
1214
+ "name": "input2",
1215
+ "type": "Int32",
1216
+ "value": 1
1217
+ }
1218
+ ]
1219
+ },
1220
+
1221
+
1222
+ ]</code></pre>
1223
+ <h3>Subscription</h3>
1224
+ <p>In subscription mode the node emits one message per value change.</p>
1225
+ </script>