@vitormnm/node-red-simple-opcua 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/client/lib/opcua-client-browser.js +46 -7
- package/client/lib/opcua-client-write-service.js +127 -33
- package/client/opcua-client.html +142 -74
- package/examples/flows_simple_opc.json +2851 -1
- package/package.json +1 -1
- package/server/opcua-server.html +106 -40
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @vitormnm/node-red-simple-opcua
|
|
2
2
|
OPC UA client and server with a simple graphical interface for Node-RED.
|
|
3
|
-
|
|
3
|
+
Fully parameterized in JSON.
|
|
4
4
|
|
|
5
5
|
It supports the following OPC UA items on only 3 nodes.
|
|
6
6
|
|
|
@@ -9,6 +9,8 @@ It supports the following OPC UA items on only 3 nodes.
|
|
|
9
9
|
- events read and write tags in server(See which tags are being written to or read from the client directly on the server in a simple workflow)
|
|
10
10
|
- methods(write methods in node-red flow)
|
|
11
11
|
- variables
|
|
12
|
+
- variables arrays
|
|
13
|
+
- description and displayname nodes
|
|
12
14
|
- objects
|
|
13
15
|
- simple objectsType
|
|
14
16
|
- custom namespace
|
|
@@ -21,8 +23,9 @@ It supports the following OPC UA items on only 3 nodes.
|
|
|
21
23
|
**Client editor**
|
|
22
24
|

|
|
23
25
|
|
|
24
|
-
example json server config
|
|
25
26
|
|
|
27
|
+
|
|
28
|
+
example json server config
|
|
26
29
|
```
|
|
27
30
|
{
|
|
28
31
|
"objects": [],
|
|
@@ -124,5 +127,6 @@ example json server config
|
|
|
124
127
|
]
|
|
125
128
|
}
|
|
126
129
|
```
|
|
127
|
-
|
|
130
|
+
Disclaimer
|
|
131
|
+
This node was only used in simulation and testing environments.
|
|
128
132
|
|
|
@@ -24,17 +24,56 @@ async function browseNode(session, root) {
|
|
|
24
24
|
resultMask: 63
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
const references = browseResult
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const references = browseResult?.references ?? [];
|
|
28
|
+
if (!references.length) return result;
|
|
29
|
+
|
|
30
|
+
// Monta lista de todos os atributos de todos os nós de uma vez
|
|
31
|
+
const nodeIds = references.map(ref => normalizeNodeId(ref.nodeId));
|
|
32
|
+
const attributesToRead = nodeIds.flatMap(nodeId => [
|
|
33
|
+
{ nodeId, attributeId: AttributeIds.Description },
|
|
34
|
+
{ nodeId, attributeId: AttributeIds.DataType },
|
|
35
|
+
{ nodeId, attributeId: AttributeIds.Value },
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// UMA única chamada para todos os nós e atributos
|
|
39
|
+
const dataValues = await session.read(attributesToRead);
|
|
40
|
+
|
|
41
|
+
// Distribui os resultados por nó (3 atributos por nó)
|
|
42
|
+
result.browse = await Promise.all(references.map(async (reference, i) => {
|
|
43
|
+
const childNodeId = nodeIds[i];
|
|
44
|
+
const nodeClass = resolveNodeClassName(reference.nodeClass);
|
|
45
|
+
const browseName = extractBrowseName(reference.browseName, childNodeId);
|
|
46
|
+
const displayName = extractDisplayName(reference.displayName, browseName);
|
|
47
|
+
|
|
48
|
+
const descValue = dataValues[i * 3]?.value?.value;
|
|
49
|
+
const description = typeof descValue === "string"
|
|
50
|
+
? descValue
|
|
51
|
+
: (descValue?.text ?? "");
|
|
52
|
+
|
|
53
|
+
const item = { nodeID: childNodeId, nodeClass, browseName, displayName, description };
|
|
54
|
+
|
|
55
|
+
if (nodeClass === "Variable") {
|
|
56
|
+
const dataTypeValue = dataValues[i * 3 + 1]?.value?.value;
|
|
57
|
+
const rawValue = dataValues[i * 3 + 2]?.value?.value;
|
|
58
|
+
|
|
59
|
+
item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
|
|
60
|
+
? (DataType[dataTypeValue.value] || dataTypeValue.toString())
|
|
61
|
+
: (dataTypeValue?.toString() ?? "");
|
|
62
|
+
|
|
63
|
+
item.value = rawValue ?? "";
|
|
64
|
+
}
|
|
30
65
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
66
|
+
if (nodeClass === "Method") {
|
|
67
|
+
const definition = await readMethodArguments(session, childNodeId);
|
|
68
|
+
item.inputArguments = definition.inputArguments;
|
|
69
|
+
item.outputArguments = definition.outputArguments;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return item;
|
|
73
|
+
}));
|
|
34
74
|
|
|
35
75
|
return result;
|
|
36
76
|
}
|
|
37
|
-
|
|
38
77
|
function normalizeBrowseRoots(payload) {
|
|
39
78
|
if (payload === undefined || payload === null) {
|
|
40
79
|
return [{ name: "RootFolder", nodeID: ROOT_NODE_ID }];
|
|
@@ -1,53 +1,147 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { DataType, coerceNodeId } = require("node-opcua");
|
|
3
|
+
const { AttributeIds, DataType, coerceNodeId } = require("node-opcua");
|
|
4
4
|
const {
|
|
5
5
|
buildVariantFromItem,
|
|
6
|
-
dataValueToItemResult,
|
|
7
6
|
normalizeTypeName,
|
|
8
7
|
resolveNodeId,
|
|
9
8
|
statusCodeToString
|
|
10
9
|
} = require("../opcua-client-utils");
|
|
11
10
|
|
|
11
|
+
// Máximo de tags por chamada session.write (ajuste conforme limite do servidor)
|
|
12
|
+
const WRITE_BATCH_SIZE = 100;
|
|
13
|
+
|
|
14
|
+
// Batches em paralelo simultâneos
|
|
15
|
+
const CONCURRENCY = 5;
|
|
16
|
+
|
|
17
|
+
// Cede o event loop a cada N itens para não travar o Node-RED
|
|
18
|
+
const YIELD_EVERY = 50;
|
|
19
|
+
|
|
12
20
|
class OpcUaClientWriteService {
|
|
13
21
|
async execute(node, msg, session, itemsResolver) {
|
|
14
22
|
const items = itemsResolver.ensureWriteItems(node, msg);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
23
|
+
|
|
24
|
+
// 1. Resolve variantes (tipo + valor) — consulta servidor só para quem não tem tipo explícito
|
|
25
|
+
const variants = await resolveVariants(session, items);
|
|
26
|
+
|
|
27
|
+
// 2. Escreve todos os nós em batches paralelos
|
|
28
|
+
const statusCodes = await writeBatches(session, items, variants);
|
|
29
|
+
|
|
30
|
+
// 3. Monta resultados — statusCode Good já confirma a escrita, sem round-trip extra
|
|
31
|
+
return buildResults(items, variants, statusCodes);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Resolução de tipos ──────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
async function resolveVariants(session, items) {
|
|
38
|
+
// Separa quais itens precisam consultar o tipo no servidor
|
|
39
|
+
const needsLookup = items
|
|
40
|
+
.map((item, index) => ({ item, index }))
|
|
41
|
+
.filter(({ item }) => !normalizeTypeName(item.type));
|
|
42
|
+
|
|
43
|
+
// Busca tipos desconhecidos em paralelo
|
|
44
|
+
const resolvedTypes = new Map();
|
|
45
|
+
|
|
46
|
+
await mapConcurrent(needsLookup, CONCURRENCY * 2, async ({ item, index }) => {
|
|
47
|
+
try {
|
|
48
|
+
const builtInType = await session.getBuiltInDataType(coerceNodeId(resolveNodeId(item)));
|
|
49
|
+
resolvedTypes.set(index, DataType[builtInType]);
|
|
50
|
+
} catch {
|
|
51
|
+
resolvedTypes.set(index, "String");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return items.map((item, index) => {
|
|
56
|
+
const typeName = normalizeTypeName(item.type) || resolvedTypes.get(index) || "String";
|
|
57
|
+
return buildVariantFromItem(item, typeName);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Escrita em batches paralelos ────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
async function writeBatches(session, items, variants) {
|
|
64
|
+
const allStatusCodes = new Array(items.length);
|
|
65
|
+
|
|
66
|
+
// Divide em batches de WRITE_BATCH_SIZE
|
|
67
|
+
const batches = [];
|
|
68
|
+
for (let i = 0; i < items.length; i += WRITE_BATCH_SIZE) {
|
|
69
|
+
batches.push({ start: i, end: Math.min(i + WRITE_BATCH_SIZE, items.length) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let processed = 0;
|
|
73
|
+
|
|
74
|
+
await mapConcurrent(batches, CONCURRENCY, async ({ start, end }) => {
|
|
75
|
+
const nodesToWrite = items.slice(start, end).map((item, i) => ({
|
|
76
|
+
nodeId: coerceNodeId(resolveNodeId(item)),
|
|
77
|
+
attributeId: AttributeIds.Value,
|
|
78
|
+
value: { value: variants[start + i] }
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const statusCodes = await session.write(nodesToWrite);
|
|
83
|
+
statusCodes.forEach((sc, i) => {
|
|
84
|
+
allStatusCodes[start + i] = sc;
|
|
85
|
+
});
|
|
86
|
+
} catch (batchError) {
|
|
87
|
+
// Se o batch falhar por completo, marca todos com erro
|
|
88
|
+
for (let i = start; i < end; i++) {
|
|
89
|
+
allStatusCodes[i] = { name: batchError.message, value: -1 };
|
|
44
90
|
}
|
|
45
91
|
}
|
|
46
92
|
|
|
47
|
-
|
|
93
|
+
// Cede o event loop a cada YIELD_EVERY itens para não travar o Node-RED
|
|
94
|
+
processed += end - start;
|
|
95
|
+
if (processed % YIELD_EVERY === 0) {
|
|
96
|
+
await yieldEventLoop();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return allStatusCodes;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Montagem dos resultados ─────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function buildResults(items, variants, statusCodes) {
|
|
106
|
+
return items.map((item, index) => {
|
|
107
|
+
const nodeId = resolveNodeId(item);
|
|
108
|
+
const sc = statusCodes[index];
|
|
109
|
+
const scName = sc && sc.name ? sc.name : "Good";
|
|
110
|
+
const typeName = DataType[variants[index].dataType] || null;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
name: item.name || nodeId,
|
|
114
|
+
nodeID: nodeId,
|
|
115
|
+
value: variants[index].value,
|
|
116
|
+
type: typeName,
|
|
117
|
+
status: scName,
|
|
118
|
+
sourceTimestamp: null,
|
|
119
|
+
serverTimestamp: null
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Utilitários ─────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async function mapConcurrent(items, concurrency, fn) {
|
|
127
|
+
let index = 0;
|
|
128
|
+
|
|
129
|
+
async function worker() {
|
|
130
|
+
while (index < items.length) {
|
|
131
|
+
const i = index++;
|
|
132
|
+
await fn(items[i], i);
|
|
133
|
+
}
|
|
48
134
|
}
|
|
135
|
+
|
|
136
|
+
await Promise.all(
|
|
137
|
+
Array.from({ length: Math.min(concurrency, items.length) }, worker)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function yieldEventLoop() {
|
|
142
|
+
return new Promise(resolve => setImmediate(resolve));
|
|
49
143
|
}
|
|
50
144
|
|
|
51
145
|
module.exports = {
|
|
52
146
|
OpcUaClientWriteService
|
|
53
|
-
};
|
|
147
|
+
};
|
package/client/opcua-client.html
CHANGED
|
@@ -245,12 +245,30 @@
|
|
|
245
245
|
<script type="text/javascript">
|
|
246
246
|
(function () {
|
|
247
247
|
var selectedItemsState = [];
|
|
248
|
+
var selectedNodeIdSet = {}; // fast O(1) lookup: nodeId → index
|
|
248
249
|
var browseState = null;
|
|
249
250
|
var expansionState = {};
|
|
250
251
|
var browseSearchValue = "";
|
|
251
252
|
var browseSearchTerm = "";
|
|
252
253
|
var contextMenuPath = "";
|
|
253
254
|
var browseSelectedPath = "";
|
|
255
|
+
var renderPending = false; // debounce flag for renderBrowseTree
|
|
256
|
+
|
|
257
|
+
function debounce(fn, delay) {
|
|
258
|
+
var timer;
|
|
259
|
+
return function () {
|
|
260
|
+
var ctx = this, args = arguments;
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
timer = setTimeout(function () { fn.apply(ctx, args); }, delay);
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function rebuildNodeIdIndex() {
|
|
267
|
+
selectedNodeIdSet = {};
|
|
268
|
+
selectedItemsState.forEach(function (item, i) {
|
|
269
|
+
if (item.nodeID) selectedNodeIdSet[item.nodeID] = i;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
254
272
|
|
|
255
273
|
function openBrowseModal() { $("#node-input-browse-modal").show(); $("body").addClass("opcua-tree-modal-open"); }
|
|
256
274
|
function closeBrowseModal() {
|
|
@@ -456,6 +474,7 @@
|
|
|
456
474
|
}
|
|
457
475
|
|
|
458
476
|
function syncSelectedItems() {
|
|
477
|
+
rebuildNodeIdIndex();
|
|
459
478
|
updateSelectedItemsField();
|
|
460
479
|
renderSelectedTags();
|
|
461
480
|
renderBrowseTree();
|
|
@@ -470,9 +489,8 @@
|
|
|
470
489
|
}
|
|
471
490
|
|
|
472
491
|
function selectedIndexByNodeId(nodeId) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
});
|
|
492
|
+
var idx = selectedNodeIdSet[nodeId];
|
|
493
|
+
return (idx !== undefined) ? idx : -1;
|
|
476
494
|
}
|
|
477
495
|
|
|
478
496
|
function canExpand(item) {
|
|
@@ -501,24 +519,47 @@
|
|
|
501
519
|
}
|
|
502
520
|
|
|
503
521
|
function renderBrowseTree() {
|
|
522
|
+
if (renderPending) return;
|
|
523
|
+
renderPending = true;
|
|
524
|
+
setTimeout(function () {
|
|
525
|
+
renderPending = false;
|
|
526
|
+
_doRenderBrowseTree();
|
|
527
|
+
}, 0);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function _doRenderBrowseTree() {
|
|
504
531
|
var container = $("#node-input-browse-tree");
|
|
505
|
-
|
|
532
|
+
var frag = document.createDocumentFragment();
|
|
506
533
|
|
|
507
534
|
if (!browseState) {
|
|
508
|
-
|
|
535
|
+
var empty = document.createElement("div");
|
|
536
|
+
empty.className = "opcua-tree-empty";
|
|
537
|
+
empty.textContent = "Click Browse to load the server tree.";
|
|
538
|
+
frag.appendChild(empty);
|
|
539
|
+
container[0].innerHTML = "";
|
|
540
|
+
container[0].appendChild(frag);
|
|
509
541
|
return;
|
|
510
542
|
}
|
|
511
543
|
|
|
512
544
|
if (browseSearchTerm) {
|
|
513
545
|
if (!branchHasSearchMatch(browseState, browseSearchTerm)) {
|
|
514
|
-
|
|
546
|
+
var noMatch = document.createElement("div");
|
|
547
|
+
noMatch.className = "opcua-tree-empty";
|
|
548
|
+
noMatch.textContent = "No items found in the already explored items.";
|
|
549
|
+
frag.appendChild(noMatch);
|
|
550
|
+
container[0].innerHTML = "";
|
|
551
|
+
container[0].appendChild(frag);
|
|
515
552
|
return;
|
|
516
553
|
}
|
|
517
|
-
renderBrowseRootFiltered(browseState, "root", 0,
|
|
554
|
+
renderBrowseRootFiltered(browseState, "root", 0, frag, browseSearchTerm);
|
|
555
|
+
container[0].innerHTML = "";
|
|
556
|
+
container[0].appendChild(frag);
|
|
518
557
|
return;
|
|
519
558
|
}
|
|
520
559
|
|
|
521
|
-
renderBrowseRoot(browseState, "root", 0,
|
|
560
|
+
renderBrowseRoot(browseState, "root", 0, frag);
|
|
561
|
+
container[0].innerHTML = "";
|
|
562
|
+
container[0].appendChild(frag);
|
|
522
563
|
}
|
|
523
564
|
|
|
524
565
|
function isFolderNode(item) {
|
|
@@ -549,92 +590,111 @@
|
|
|
549
590
|
return "fa-tag";
|
|
550
591
|
}
|
|
551
592
|
|
|
552
|
-
function
|
|
593
|
+
function makeEl(tag, className, html) {
|
|
594
|
+
var el = document.createElement(tag);
|
|
595
|
+
if (className) el.className = className;
|
|
596
|
+
if (html !== undefined) el.innerHTML = html;
|
|
597
|
+
return el;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function makeTreeRow(path, extraClass) {
|
|
601
|
+
var row = document.createElement("div");
|
|
602
|
+
row.className = "opcua-tree-row" + (extraClass ? " " + extraClass : "");
|
|
603
|
+
row.setAttribute("data-path", path);
|
|
604
|
+
return row;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function renderBrowseRoot(root, path, depth, frag) {
|
|
553
608
|
var expanded = isExpanded(path, true);
|
|
554
|
-
var row =
|
|
555
|
-
row.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
609
|
+
var row = makeTreeRow(path);
|
|
610
|
+
row.innerHTML = '<span class="opcua-tree-indent"></span>'
|
|
611
|
+
+ '<span class="opcua-tree-twisty opcua-client-toggle-tree" data-path="' + escapeHtml(path) + '">'
|
|
612
|
+
+ (expanded ? '<i class="fa fa-caret-down"></i>' : '<i class="fa fa-caret-right"></i>') + '</span>'
|
|
613
|
+
+ '<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>'
|
|
614
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>'
|
|
615
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>';
|
|
616
|
+
frag.appendChild(row);
|
|
561
617
|
|
|
562
618
|
if (!expanded) return;
|
|
563
619
|
|
|
564
620
|
if (!Array.isArray(root.browse) || !root.browse.length) {
|
|
565
|
-
|
|
621
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "No items found.."));
|
|
566
622
|
} else {
|
|
567
623
|
root.browse.forEach(function (item, index) {
|
|
568
|
-
renderBrowseItem(item, path + ".browse." + index, depth + 1,
|
|
624
|
+
renderBrowseItem(item, path + ".browse." + index, depth + 1, frag);
|
|
569
625
|
});
|
|
570
626
|
}
|
|
571
627
|
}
|
|
572
628
|
|
|
573
|
-
function renderBrowseItem(item, path, depth,
|
|
629
|
+
function renderBrowseItem(item, path, depth, frag) {
|
|
574
630
|
var expanded = isExpanded(path, false);
|
|
575
631
|
var nodeId = nodeIdOf(item);
|
|
576
632
|
var selectedIndex = selectedIndexByNodeId(nodeId);
|
|
577
633
|
var hasChildren = canExpand(item);
|
|
578
|
-
var row =
|
|
579
|
-
if (selectedIndex >= 0) row.addClass("is-selected");
|
|
634
|
+
var row = makeTreeRow(path, selectedIndex >= 0 ? "is-selected" : "");
|
|
580
635
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
636
|
+
var indents = "";
|
|
637
|
+
for (var i = 0; i < depth; i += 1) indents += '<span class="opcua-tree-indent"></span>';
|
|
638
|
+
|
|
639
|
+
var twisty = '<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + escapeHtml(path) + '">'
|
|
640
|
+
+ (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>';
|
|
641
|
+
|
|
642
|
+
var actions = nodeId
|
|
643
|
+
? '<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>'
|
|
644
|
+
: '';
|
|
645
|
+
|
|
646
|
+
row.innerHTML = indents + twisty
|
|
647
|
+
+ '<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>'
|
|
648
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>'
|
|
649
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>'
|
|
650
|
+
+ '<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>'
|
|
651
|
+
+ actions;
|
|
652
|
+
frag.appendChild(row);
|
|
596
653
|
|
|
597
654
|
if (item.description) {
|
|
598
|
-
|
|
655
|
+
var desc = makeEl("div", "opcua-client-description");
|
|
656
|
+
desc.style.padding = "0 10px 8px " + String((depth + 2) * 14) + "px";
|
|
657
|
+
desc.textContent = item.description;
|
|
658
|
+
frag.appendChild(desc);
|
|
599
659
|
}
|
|
600
660
|
|
|
601
661
|
if (expanded && hasChildren) {
|
|
602
662
|
if (Array.isArray(item.browse)) {
|
|
603
663
|
if (!item.browse.length) {
|
|
604
|
-
|
|
664
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "No children found.."));
|
|
605
665
|
} else {
|
|
606
666
|
item.browse.forEach(function (child, index) {
|
|
607
|
-
renderBrowseItem(child, path + ".browse." + index, depth + 1,
|
|
667
|
+
renderBrowseItem(child, path + ".browse." + index, depth + 1, frag);
|
|
608
668
|
});
|
|
609
669
|
}
|
|
610
670
|
} else {
|
|
611
|
-
|
|
671
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "Searching for items..."));
|
|
612
672
|
}
|
|
613
673
|
}
|
|
614
674
|
}
|
|
615
675
|
|
|
616
|
-
function renderBrowseRootFiltered(root, path, depth,
|
|
617
|
-
var row =
|
|
618
|
-
row.
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
676
|
+
function renderBrowseRootFiltered(root, path, depth, frag, term) {
|
|
677
|
+
var row = makeTreeRow(path);
|
|
678
|
+
row.innerHTML = '<span class="opcua-tree-indent"></span>'
|
|
679
|
+
+ '<span class="opcua-tree-twisty"><i class="fa fa-caret-down"></i></span>'
|
|
680
|
+
+ '<span class="opcua-tree-icon"><i class="fa fa-sitemap"></i></span>'
|
|
681
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(root.name || root.nodeID || "RootFolder") + '</span>'
|
|
682
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(root.nodeID || "") + '</span>';
|
|
683
|
+
frag.appendChild(row);
|
|
624
684
|
|
|
625
685
|
if (!Array.isArray(root.browse) || !root.browse.length) {
|
|
626
|
-
|
|
686
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "Nenhum item encontrado."));
|
|
627
687
|
return;
|
|
628
688
|
}
|
|
629
689
|
|
|
630
690
|
root.browse.forEach(function (item, index) {
|
|
631
691
|
if (branchHasSearchMatch(item, term)) {
|
|
632
|
-
renderBrowseItemFiltered(item, path + ".browse." + index, depth + 1,
|
|
692
|
+
renderBrowseItemFiltered(item, path + ".browse." + index, depth + 1, frag, term, false);
|
|
633
693
|
}
|
|
634
694
|
});
|
|
635
695
|
}
|
|
636
696
|
|
|
637
|
-
function renderBrowseItemFiltered(item, path, depth,
|
|
697
|
+
function renderBrowseItemFiltered(item, path, depth, frag, term, ancestorMatched) {
|
|
638
698
|
if (!branchHasSearchMatch(item, term)) return;
|
|
639
699
|
|
|
640
700
|
var nodeId = nodeIdOf(item);
|
|
@@ -648,40 +708,47 @@
|
|
|
648
708
|
var expanded = hasChildren && (hasExplicitExpansion
|
|
649
709
|
? !!expansionState[path]
|
|
650
710
|
: ((subtreeVisible && Array.isArray(item.browse)) || hasMatchingLoadedChild));
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
711
|
+
|
|
712
|
+
var row = makeTreeRow(path, selectedIndex >= 0 ? "is-selected" : "");
|
|
713
|
+
|
|
714
|
+
var indents = "";
|
|
715
|
+
for (var i = 0; i < depth; i += 1) indents += '<span class="opcua-tree-indent"></span>';
|
|
716
|
+
|
|
717
|
+
var twisty = '<span class="opcua-tree-twisty' + (hasChildren ? ' opcua-client-toggle-tree' : '') + '" data-path="' + escapeHtml(path) + '">'
|
|
718
|
+
+ (hasChildren ? '<i class="fa ' + (expanded ? 'fa-caret-down' : 'fa-caret-right') + '"></i>' : '') + '</span>';
|
|
719
|
+
|
|
720
|
+
var actions = nodeId
|
|
721
|
+
? '<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>'
|
|
722
|
+
: '';
|
|
723
|
+
|
|
724
|
+
row.innerHTML = indents + twisty
|
|
725
|
+
+ '<span class="opcua-tree-icon"><i class="fa ' + browseIconFor(item) + '"></i></span>'
|
|
726
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(item.displayName || item.browseName || item.nodeID) + '</span>'
|
|
727
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(item.nodeClass || "") + (item.dataType ? " | " + escapeHtml(item.dataType) : "") + '</span>'
|
|
728
|
+
+ '<span class="opcua-client-nodeid-label">' + escapeHtml(nodeId) + '</span>'
|
|
729
|
+
+ actions;
|
|
730
|
+
frag.appendChild(row);
|
|
667
731
|
|
|
668
732
|
if (item.description) {
|
|
669
|
-
|
|
733
|
+
var desc = makeEl("div", "opcua-client-description");
|
|
734
|
+
desc.style.padding = "0 10px 8px " + String((depth + 2) * 14) + "px";
|
|
735
|
+
desc.textContent = item.description;
|
|
736
|
+
frag.appendChild(desc);
|
|
670
737
|
}
|
|
671
738
|
|
|
672
739
|
if (expanded && hasChildren) {
|
|
673
740
|
if (Array.isArray(item.browse)) {
|
|
674
741
|
if (!item.browse.length) {
|
|
675
|
-
|
|
742
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "Nenhum filho encontrado."));
|
|
676
743
|
} else {
|
|
677
744
|
item.browse.forEach(function (child, index) {
|
|
678
745
|
if (subtreeVisible || branchHasSearchMatch(child, term)) {
|
|
679
|
-
renderBrowseItemFiltered(child, path + ".browse." + index, depth + 1,
|
|
746
|
+
renderBrowseItemFiltered(child, path + ".browse." + index, depth + 1, frag, term, subtreeVisible);
|
|
680
747
|
}
|
|
681
748
|
});
|
|
682
749
|
}
|
|
683
750
|
} else {
|
|
684
|
-
|
|
751
|
+
frag.appendChild(makeEl("div", "opcua-tree-empty", "Expandindo..."));
|
|
685
752
|
}
|
|
686
753
|
}
|
|
687
754
|
}
|
|
@@ -938,6 +1005,7 @@
|
|
|
938
1005
|
});
|
|
939
1006
|
|
|
940
1007
|
selectedItemsState = parseSelectedItems(this.selectedItems);
|
|
1008
|
+
rebuildNodeIdIndex();
|
|
941
1009
|
|
|
942
1010
|
browseState = null;
|
|
943
1011
|
expansionState = {};
|
|
@@ -967,12 +1035,12 @@
|
|
|
967
1035
|
hideTreeContextMenu();
|
|
968
1036
|
if (event.target === this) closeBrowseModal();
|
|
969
1037
|
});
|
|
970
|
-
$("#node-input-browse-search").off("input").on("input", function () {
|
|
1038
|
+
$("#node-input-browse-search").off("input").on("input", debounce(function () {
|
|
971
1039
|
browseSearchValue = $(this).val();
|
|
972
1040
|
browseSearchTerm = normalizeSearchTerm(browseSearchValue);
|
|
973
1041
|
$("#node-input-browse-search-clear").toggle(!!browseSearchTerm);
|
|
974
1042
|
renderBrowseTree();
|
|
975
|
-
});
|
|
1043
|
+
}, 200));
|
|
976
1044
|
$("#node-input-browse-search-clear").off("click").on("click", function (event) {
|
|
977
1045
|
event.preventDefault();
|
|
978
1046
|
browseSearchValue = "";
|
|
@@ -1222,4 +1290,4 @@
|
|
|
1222
1290
|
]</code></pre>
|
|
1223
1291
|
<h3>Subscription</h3>
|
|
1224
1292
|
<p>In subscription mode the node emits one message per value change.</p>
|
|
1225
|
-
</script>
|
|
1293
|
+
</script>
|