@vitormnm/node-red-simple-opcua 1.3.2 → 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/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>
|
package/package.json
CHANGED
package/server/opcua-server.html
CHANGED
|
@@ -730,6 +730,30 @@
|
|
|
730
730
|
function getNamespaceOptions() {
|
|
731
731
|
return Array.isArray(editorState.nameSpaces) ? editorState.nameSpaces.slice().sort(function (left, right) { return left.id - right.id; }) : [];
|
|
732
732
|
}
|
|
733
|
+
|
|
734
|
+
function getDefinedObjectTypeNames() {
|
|
735
|
+
var names = [];
|
|
736
|
+
(editorState.objectsTypes || []).forEach(function (ot) {
|
|
737
|
+
if (ot && ot.name) names.push(String(ot.name));
|
|
738
|
+
});
|
|
739
|
+
return names;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
function buildObjectTypeSelect(id, currentValue) {
|
|
744
|
+
var names = getDefinedObjectTypeNames();
|
|
745
|
+
var cv = String(currentValue || "");
|
|
746
|
+
var opts = "";
|
|
747
|
+
var noneSelected = (cv === "") ? " selected" : "";
|
|
748
|
+
opts += "<option value=\"\"" + noneSelected + ">\u2014 none \u2014</option>";
|
|
749
|
+
names.forEach(function (n) {
|
|
750
|
+
var esc = n.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
751
|
+
var sel = (n === cv) ? " selected" : "";
|
|
752
|
+
opts += "<option value=\"" + esc + "\"" + sel + ">" + esc + "</option>";
|
|
753
|
+
});
|
|
754
|
+
return "<select id=\"" + id + "\">" + opts + "</select>";
|
|
755
|
+
}
|
|
756
|
+
|
|
733
757
|
function getNamespaceLabel(namespaceId) {
|
|
734
758
|
var match = getNamespaceOptions().find(function (item) { return item.id === normalizeNamespaceId(namespaceId); });
|
|
735
759
|
return match ? String(match.id) + " - " + match.name : String(normalizeNamespaceId(namespaceId));
|
|
@@ -900,43 +924,72 @@
|
|
|
900
924
|
return "fa-tag";
|
|
901
925
|
}
|
|
902
926
|
|
|
903
|
-
|
|
927
|
+
// ── performance helpers ───────────────────────────────────────────
|
|
928
|
+
var _renderTreePending = false;
|
|
929
|
+
|
|
930
|
+
function debounce(fn, delay) {
|
|
931
|
+
var timer;
|
|
932
|
+
return function () { var ctx = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(ctx, args); }, delay); };
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function escapeHtml(v) {
|
|
936
|
+
return String(v || "").replace(/[&<>"']/g, function (c) {
|
|
937
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function appendNodeToFrag(frag, path, depth, ancestorMatched) {
|
|
904
942
|
var item = getAtPath(editorState, path);
|
|
905
|
-
if (!item) return
|
|
943
|
+
if (!item) return;
|
|
906
944
|
var nodeClass = nodeClassFromPath(path);
|
|
907
945
|
var hasChildren = nodeClass !== "Variable" && nodeClass !== "Alarm" && nodeClass !== "Namespace" && getChildrenByPath(path).length > 0;
|
|
908
|
-
var expanded = isExpanded(path, depth < 1 || treeSearchTerm);
|
|
946
|
+
var expanded = isExpanded(path, depth < 1 || !!treeSearchTerm);
|
|
909
947
|
var subtreeVisible = !!ancestorMatched || nodeMatchesSearch(path);
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
for (var i = 0; i < depth; i +=
|
|
913
|
-
|
|
914
|
-
row
|
|
915
|
-
row.
|
|
916
|
-
row.
|
|
917
|
-
row.
|
|
918
|
-
|
|
948
|
+
|
|
949
|
+
var indents = "";
|
|
950
|
+
for (var i = 0; i < depth; i++) indents += '<span class="opcua-tree-indent"></span>';
|
|
951
|
+
|
|
952
|
+
var row = document.createElement("div");
|
|
953
|
+
row.className = "opcua-tree-row" + (path === selectedPath ? " is-selected" : "");
|
|
954
|
+
row.setAttribute("data-path", path);
|
|
955
|
+
row.innerHTML = indents
|
|
956
|
+
+ '<span class="opcua-tree-twisty">' + (hasChildren ? '<i class="fa ' + (expanded ? "fa-caret-down" : "fa-caret-right") + '"></i>' : "") + "</span>"
|
|
957
|
+
+ '<span class="opcua-tree-icon"><i class="fa ' + iconForNodeClass(nodeClass) + '"></i></span>'
|
|
958
|
+
+ '<span class="opcua-tree-label">' + escapeHtml(item.name || "(unnamed)") + "</span>"
|
|
959
|
+
+ '<span class="opcua-tree-type">' + escapeHtml(nodeClass) + "</span>";
|
|
960
|
+
frag.appendChild(row);
|
|
961
|
+
|
|
919
962
|
if (hasChildren && expanded) {
|
|
920
963
|
getChildrenByPath(path).forEach(function (childPath) {
|
|
921
964
|
if (!treeSearchTerm || subtreeVisible || branchHasSearchMatch(childPath)) {
|
|
922
|
-
|
|
965
|
+
appendNodeToFrag(frag, childPath, depth + 1, subtreeVisible);
|
|
923
966
|
}
|
|
924
967
|
});
|
|
925
968
|
}
|
|
926
|
-
return fragment;
|
|
927
969
|
}
|
|
928
970
|
|
|
929
971
|
function renderTree() {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
list.
|
|
935
|
-
return;
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (!
|
|
939
|
-
|
|
972
|
+
if (_renderTreePending) return;
|
|
973
|
+
_renderTreePending = true;
|
|
974
|
+
setTimeout(function () {
|
|
975
|
+
_renderTreePending = false;
|
|
976
|
+
var list = document.getElementById("node-input-object-list");
|
|
977
|
+
if (!list) return;
|
|
978
|
+
var frag = document.createDocumentFragment();
|
|
979
|
+
var roots = getTopLevelPaths();
|
|
980
|
+
if (!roots.length) {
|
|
981
|
+
var empty = document.createElement("div");
|
|
982
|
+
empty.className = "opcua-tree-empty";
|
|
983
|
+
empty.textContent = "No OPC UA items. Use Add folder, Add object, or Add namespace.";
|
|
984
|
+
frag.appendChild(empty);
|
|
985
|
+
} else {
|
|
986
|
+
roots.forEach(function (path) {
|
|
987
|
+
if (!treeSearchTerm || branchHasSearchMatch(path)) appendNodeToFrag(frag, path, 0, false);
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
list.innerHTML = "";
|
|
991
|
+
list.appendChild(frag);
|
|
992
|
+
}, 0);
|
|
940
993
|
}
|
|
941
994
|
|
|
942
995
|
function renderBreadcrumbs() {
|
|
@@ -972,8 +1025,10 @@
|
|
|
972
1025
|
parentPath: path,
|
|
973
1026
|
kind: kind,
|
|
974
1027
|
name: kind === "variable" ? "newVariable" : kind === "folder" ? "newFolder" : kind === "objecttype" ? "newObjectType" : kind === "alarm" ? "newAlarm" : kind === "method" ? "newMethod" : "newObject",
|
|
1028
|
+
displayName: "",
|
|
975
1029
|
dataType: "Int32",
|
|
976
1030
|
value: "",
|
|
1031
|
+
access: "readwrite",
|
|
977
1032
|
objectsType: "",
|
|
978
1033
|
alarmType: "levelAlarm",
|
|
979
1034
|
variableNodeId: "",
|
|
@@ -1012,16 +1067,19 @@
|
|
|
1012
1067
|
if (kind === "variable") {
|
|
1013
1068
|
target.push(normalizeVariable({
|
|
1014
1069
|
name: pendingCreate.name,
|
|
1070
|
+
displayName: pendingCreate.displayName || "",
|
|
1015
1071
|
type: pendingCreate.dataType,
|
|
1016
|
-
value: pendingCreate.value
|
|
1072
|
+
value: pendingCreate.value,
|
|
1073
|
+
access: pendingCreate.access || "readwrite"
|
|
1017
1074
|
}));
|
|
1018
1075
|
} else if (kind === "folder") {
|
|
1019
|
-
target.push(normalizeBranch({ name: pendingCreate.name }));
|
|
1076
|
+
target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "" }));
|
|
1020
1077
|
} else if (kind === "objecttype") {
|
|
1021
|
-
target.push(normalizeBranch({ name: pendingCreate.name, objectsType: pendingCreate.objectsType || "" }));
|
|
1078
|
+
target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", objectsType: pendingCreate.objectsType || "" }));
|
|
1022
1079
|
target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(branchTargetPath + "." + (target.length - 1));
|
|
1023
1080
|
} else if (kind === "alarm") {
|
|
1024
1081
|
target.push(normalizeAlarm({
|
|
1082
|
+
displayName: pendingCreate.displayName || "",
|
|
1025
1083
|
name: pendingCreate.name,
|
|
1026
1084
|
type: pendingCreate.alarmType,
|
|
1027
1085
|
variableNodeId: pendingCreate.variableNodeId,
|
|
@@ -1038,9 +1096,9 @@
|
|
|
1038
1096
|
digitalMessage: pendingCreate.digitalMessage
|
|
1039
1097
|
}));
|
|
1040
1098
|
} else if (kind === "method") {
|
|
1041
|
-
target.push(normalizeMethod({ name: pendingCreate.name }));
|
|
1099
|
+
target.push(normalizeMethod({ name: pendingCreate.name, displayName: pendingCreate.displayName || "" }));
|
|
1042
1100
|
} else {
|
|
1043
|
-
target.push(normalizeBranch({ name: pendingCreate.name }));
|
|
1101
|
+
target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "" }));
|
|
1044
1102
|
}
|
|
1045
1103
|
expansionState[parentPath] = true;
|
|
1046
1104
|
pendingCreate = null;
|
|
@@ -1060,12 +1118,14 @@
|
|
|
1060
1118
|
panel.append('<div class="form-row"><label>Parent</label><input type="text" id="opcua-create-parent" readonly></div>');
|
|
1061
1119
|
panel.append('<div class="form-row"><label>Type</label><input type="text" id="opcua-create-kind" readonly></div>');
|
|
1062
1120
|
panel.append('<div class="form-row"><label>Name</label><input type="text" id="opcua-create-name"></div>');
|
|
1121
|
+
panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-create-displayname" placeholder="Leave blank to use browseName"></div>');
|
|
1063
1122
|
if (pendingCreate.kind === "variable") {
|
|
1064
1123
|
panel.append('<div class="form-row"><label>dataType</label><select id="opcua-create-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
|
|
1065
1124
|
panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-create-value"></div>');
|
|
1125
|
+
panel.append('<div class="form-row"><label>Access</label><select id="opcua-create-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
|
|
1066
1126
|
}
|
|
1067
1127
|
if (pendingCreate.kind === "objecttype") {
|
|
1068
|
-
panel.append('<div class="form-row"><label>objectsType</label
|
|
1128
|
+
panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-create-objectstype", pendingCreate.objectsType || "") + '</div>');
|
|
1069
1129
|
}
|
|
1070
1130
|
if (pendingCreate.kind === "alarm") {
|
|
1071
1131
|
panel.append('<div class="form-row"><label>alarmType</label><select id="opcua-create-alarm-type"><option value="levelAlarm">levelAlarm</option><option value="digitalAlarm">digitalAlarm</option></select></div>');
|
|
@@ -1089,9 +1149,11 @@
|
|
|
1089
1149
|
$("#opcua-create-parent").val(pendingCreate.parentPath);
|
|
1090
1150
|
$("#opcua-create-kind").val(pendingCreate.kind);
|
|
1091
1151
|
$("#opcua-create-name").val(pendingCreate.name);
|
|
1152
|
+
$("#opcua-create-displayname").val(pendingCreate.displayName || "");
|
|
1092
1153
|
$("#opcua-create-type").val(pendingCreate.dataType);
|
|
1093
1154
|
$("#opcua-create-value").val(pendingCreate.value);
|
|
1094
1155
|
$("#opcua-create-objectstype").val(pendingCreate.objectsType);
|
|
1156
|
+
$("#opcua-create-access").val(pendingCreate.access || "readwrite");
|
|
1095
1157
|
$("#opcua-create-alarm-type").val(pendingCreate.alarmType);
|
|
1096
1158
|
$("#opcua-create-variable-nodeid").val(pendingCreate.variableNodeId);
|
|
1097
1159
|
$("#opcua-create-severity").val(pendingCreate.severity);
|
|
@@ -1130,15 +1192,16 @@
|
|
|
1130
1192
|
panel.append('<div class="form-row"><label>namespace</label><select id="opcua-detail-namespace"></select></div>');
|
|
1131
1193
|
panel.append('<div class="form-row"><label>nodeId</label><div class="opcua-nodeid-field"><span class="opcua-nodeid-prefix">' + getNodeIdPrefix(namespaceId) + '</span><input type="text" id="opcua-detail-nodeid"' + (nodeIdLocked ? ' readonly title="Generated automatically for object type models."' : '') + '><a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a></div></div>');
|
|
1132
1194
|
panel.append('<div class="form-row"><label>Description</label><input type="text" id="opcua-detail-description"></div>');
|
|
1195
|
+
panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-detail-displayname" placeholder="Leave blank to use browseName"></div>');
|
|
1133
1196
|
if (nodeClass === "ObjectType") {
|
|
1134
|
-
panel.append('<div class="form-row"><label>objectsType</label
|
|
1197
|
+
panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-detail-objectstype", item.objectsType || "") + '</div>');
|
|
1135
1198
|
}
|
|
1136
1199
|
if (nodeClass === "Variable") {
|
|
1137
1200
|
panel.append('<div class="form-row"><label>dataType</label><select id="opcua-detail-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
|
|
1138
1201
|
panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-detail-value"></div>');
|
|
1202
|
+
panel.append('<div class="form-row"><label>Access</label><select id="opcua-detail-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
|
|
1139
1203
|
}
|
|
1140
1204
|
if (nodeClass === "Method") {
|
|
1141
|
-
panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-detail-displayname"></div>');
|
|
1142
1205
|
panel.append('<hr style="margin:8px 0; border-color:#e3e3e3;">');
|
|
1143
1206
|
panel.append('<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:#666;margin-bottom:4px;">Inputs</div>');
|
|
1144
1207
|
var inputsDiv = $('<div id="opcua-detail-inputs"></div>').appendTo(panel);
|
|
@@ -1203,9 +1266,9 @@
|
|
|
1203
1266
|
$("#opcua-detail-namespace").append($("<option></option>").val(option.id).text(getNamespaceLabel(option.id)));
|
|
1204
1267
|
});
|
|
1205
1268
|
$("#opcua-detail-namespace").val(String(namespaceId));
|
|
1206
|
-
|
|
1207
|
-
if (nodeClass === "ObjectType") $("#opcua-detail-objectstype").val(item.objectsType || "");
|
|
1208
|
-
if (nodeClass === "Variable") { $("#opcua-detail-type").val(item.type || "Int32"); $("#opcua-detail-value").val(item.value !== undefined ? item.value : ""); }
|
|
1269
|
+
$("#opcua-detail-displayname").val(item.displayName || "");
|
|
1270
|
+
if (nodeClass === "ObjectType") { $("#opcua-detail-objectstype").val(item.objectsType || ""); }
|
|
1271
|
+
if (nodeClass === "Variable") { $("#opcua-detail-type").val(item.type || "Int32"); $("#opcua-detail-value").val(item.value !== undefined ? item.value : ""); $("#opcua-detail-access").val(item.access || "readwrite"); }
|
|
1209
1272
|
if (nodeClass === "Alarm") {
|
|
1210
1273
|
$("#opcua-detail-alarm-type").val(item.type || "levelAlarm");
|
|
1211
1274
|
$("#opcua-detail-variable-nodeid").val(item.variableNodeId || "");
|
|
@@ -1398,8 +1461,9 @@
|
|
|
1398
1461
|
}
|
|
1399
1462
|
});
|
|
1400
1463
|
$(document).on("input", "#opcua-detail-description", function () { updateNode(selectedPath, { description: $(this).val() }); });
|
|
1401
|
-
$(document).on("
|
|
1464
|
+
$(document).on("change", "#opcua-detail-objectstype", function () { updateNode(selectedPath, { objectsType: $(this).val() }); });
|
|
1402
1465
|
$(document).on("change", "#opcua-detail-type", function () { updateNode(selectedPath, { type: $(this).val() }); });
|
|
1466
|
+
$(document).on("change", "#opcua-detail-access", function () { updateNode(selectedPath, { access: $(this).val() }); });
|
|
1403
1467
|
$(document).on("input", "#opcua-detail-value", function () { updateNode(selectedPath, { value: $(this).val() }); });
|
|
1404
1468
|
$(document).on("change", "#opcua-detail-alarm-type", function () {
|
|
1405
1469
|
updateNode(selectedPath, { type: $(this).val() });
|
|
@@ -1418,9 +1482,11 @@
|
|
|
1418
1482
|
$(document).on("input", "#opcua-detail-normalstatevalue", function () { updateNode(selectedPath, { normalStateValue: Number($(this).val() || 0) }); });
|
|
1419
1483
|
$(document).on("input", "#opcua-detail-digitalmessage", function () { updateNode(selectedPath, { digitalMessage: $(this).val() }); });
|
|
1420
1484
|
$(document).on("input", "#opcua-create-name", function () { if (pendingCreate) pendingCreate.name = $(this).val(); });
|
|
1485
|
+
$(document).on("input", "#opcua-create-displayname", function () { if (pendingCreate) pendingCreate.displayName = $(this).val(); });
|
|
1421
1486
|
$(document).on("change", "#opcua-create-type", function () { if (pendingCreate) pendingCreate.dataType = $(this).val(); });
|
|
1422
1487
|
$(document).on("input", "#opcua-create-value", function () { if (pendingCreate) pendingCreate.value = $(this).val(); });
|
|
1423
|
-
$(document).on("
|
|
1488
|
+
$(document).on("change", "#opcua-create-objectstype", function () { if (pendingCreate) pendingCreate.objectsType = $(this).val(); });
|
|
1489
|
+
$(document).on("change", "#opcua-create-access", function () { if (pendingCreate) pendingCreate.access = $(this).val(); });
|
|
1424
1490
|
$(document).on("change", "#opcua-create-alarm-type", function () {
|
|
1425
1491
|
if (pendingCreate) {
|
|
1426
1492
|
pendingCreate.alarmType = $(this).val();
|
|
@@ -1487,10 +1553,10 @@
|
|
|
1487
1553
|
$("#node-input-close-tree-modal").off("click").on("click", function (event) { event.preventDefault(); closeTreeModal(); });
|
|
1488
1554
|
$("#node-input-tree-modal").off("click").on("click", function (event) { if (event.target === this) closeTreeModal(); });
|
|
1489
1555
|
|
|
1490
|
-
$("#node-input-tree-search").off("input").on("input", function () {
|
|
1556
|
+
$("#node-input-tree-search").off("input").on("input", debounce(function () {
|
|
1491
1557
|
treeSearchValue = $(this).val(); treeSearchTerm = normalizeSearchTerm(treeSearchValue);
|
|
1492
1558
|
$("#node-input-tree-search-clear").toggle(!!treeSearchTerm); renderTree();
|
|
1493
|
-
});
|
|
1559
|
+
}, 200));
|
|
1494
1560
|
$("#node-input-tree-search-clear").off("click").on("click", function (event) {
|
|
1495
1561
|
event.preventDefault(); treeSearchValue = ""; treeSearchTerm = "";
|
|
1496
1562
|
$("#node-input-tree-search").val(""); $(this).hide(); renderTree();
|
|
@@ -1545,4 +1611,4 @@
|
|
|
1545
1611
|
<p>Supported variable types: <code>Int32</code>, <code>Float</code>, <code>Boolean</code>, <code>String</code>.</p>
|
|
1546
1612
|
<p>Supported access modes: <code>readonly</code> and <code>readwrite</code>.</p>
|
|
1547
1613
|
<p>Authentication can be configured in the editor with a local username list and bcrypt password hashes, and anonymous login can be disabled.</p>
|
|
1548
|
-
</script>
|
|
1614
|
+
</script>
|