@vitormnm/node-red-simple-opcua 1.4.3 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/client/icons/opcua.svg +132 -132
  2. package/client/lib/opcua-client-browser.js +368 -330
  3. package/client/lib/opcua-client-method-service.js +88 -88
  4. package/client/lib/opcua-client-read-service.js +27 -15
  5. package/client/lib/opcua-client-subscription-id-service.js +24 -24
  6. package/client/lib/opcua-client-subscription-service.js +175 -170
  7. package/client/lib/opcua-client-write-service.js +146 -146
  8. package/client/opcua-client-config.html +80 -80
  9. package/client/opcua-client-utils.js +217 -22
  10. package/client/opcua-client.html +140 -140
  11. package/client/view/opcua-client.js +1144 -1140
  12. package/examples/flows_simple_opc.json +1 -2851
  13. package/icons/opcua.svg +132 -132
  14. package/icons/opcua2.svg +132 -132
  15. package/package.json +3 -3
  16. package/server/icons/opcua.svg +132 -132
  17. package/server/lib/opcua-address-space-alarm.js +341 -341
  18. package/server/lib/opcua-address-space-builder.js +1797 -1485
  19. package/server/lib/opcua-config.js +800 -546
  20. package/server/lib/opcua-constants.js +120 -109
  21. package/server/lib/opcua-server-events-child.js +139 -139
  22. package/server/lib/opcua-server-methods.js +2 -0
  23. package/server/lib/opcua-server-runtime-child.js +874 -819
  24. package/server/lib/opcua-server-runtime.js +385 -311
  25. package/server/lib/opcua-server-status-child.js +187 -187
  26. package/server/lib/server-node-utils.js +16 -16
  27. package/server/opcua-server-io.html +346 -346
  28. package/server/opcua-server-io.js +497 -496
  29. package/server/opcua-server-registry.js +270 -270
  30. package/server/opcua-server.css +265 -265
  31. package/server/opcua-server.html +158 -1643
  32. package/server/opcua-server.js +24 -13
  33. package/server/view/opcua-server.css +496 -0
  34. package/server/view/opcua-server.js +1585 -0
@@ -102,9 +102,69 @@ function coerceValue(value, typeName) {
102
102
  case "UInt32":
103
103
  return Number.parseInt(value, 10);
104
104
 
105
- case "Int64":
106
- case "UInt64":
107
- return BigInt(value);
105
+ case "Int64": {
106
+ const minVal = -9223372036854775808n;
107
+ const maxVal = 9223372036854775807n;
108
+ let bigintVal;
109
+ if (Array.isArray(value) && value.length === 2) {
110
+ const h = BigInt(value[0]);
111
+ const l = BigInt(value[1]);
112
+ const signMask = 1n << 31n;
113
+ const shiftHigh = 1n << 32n;
114
+ if ((h & signMask) === signMask) {
115
+ bigintVal = (h & ~signMask) * shiftHigh + l - 0x8000000000000000n;
116
+ } else {
117
+ bigintVal = h * shiftHigh + l;
118
+ }
119
+ } else {
120
+ try {
121
+ bigintVal = BigInt(value);
122
+ } catch (e) {
123
+ const parsed = Number(value);
124
+ if (Number.isFinite(parsed)) {
125
+ bigintVal = BigInt(Math.trunc(parsed));
126
+ } else {
127
+ bigintVal = 0n;
128
+ }
129
+ }
130
+ }
131
+ if (bigintVal < minVal) bigintVal = minVal;
132
+ else if (bigintVal > maxVal) bigintVal = maxVal;
133
+
134
+ const mask = 0xFFFFFFFFFFFFFFFFn;
135
+ const unsignedVal = bigintVal & mask;
136
+ const high = Number(unsignedVal >> 32n);
137
+ const low = Number(unsignedVal & 0xFFFFFFFFn);
138
+ return [high, low];
139
+ }
140
+ case "UInt64": {
141
+ const minVal = 0n;
142
+ const maxVal = 18446744073709551615n;
143
+ let bigintVal;
144
+ if (Array.isArray(value) && value.length === 2) {
145
+ const h = BigInt(value[0]);
146
+ const l = BigInt(value[1]);
147
+ const shiftHigh = 1n << 32n;
148
+ bigintVal = h * shiftHigh + l;
149
+ } else {
150
+ try {
151
+ bigintVal = BigInt(value);
152
+ } catch (e) {
153
+ const parsed = Number(value);
154
+ if (Number.isFinite(parsed)) {
155
+ bigintVal = BigInt(Math.trunc(parsed));
156
+ } else {
157
+ bigintVal = 0n;
158
+ }
159
+ }
160
+ }
161
+ if (bigintVal < minVal) bigintVal = minVal;
162
+ else if (bigintVal > maxVal) bigintVal = maxVal;
163
+
164
+ const high = Number(bigintVal >> 32n);
165
+ const low = Number(bigintVal & 0xFFFFFFFFn);
166
+ return [high, low];
167
+ }
108
168
 
109
169
  case "Float":
110
170
  case "Double":
@@ -218,12 +278,49 @@ function variantTypeToName(variant) {
218
278
  return DataType[variant.dataType] || String(variant.dataType);
219
279
  }
220
280
 
281
+ function decode64BitValue(value, isUnsigned) {
282
+ if (Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number") {
283
+ const h = BigInt(value[0]);
284
+ const l = BigInt(value[1]);
285
+ const shiftHigh = 1n << 32n;
286
+ let bigintVal;
287
+ if (!isUnsigned) {
288
+ const signMask = 1n << 31n;
289
+ if ((h & signMask) === signMask) {
290
+ bigintVal = (h & ~signMask) * shiftHigh + l - 0x8000000000000000n;
291
+ } else {
292
+ bigintVal = h * shiftHigh + l;
293
+ }
294
+ } else {
295
+ bigintVal = h * shiftHigh + l;
296
+ }
297
+
298
+ const num = Number(bigintVal);
299
+ return num;
300
+ }
301
+ return value;
302
+ }
303
+
304
+ function resolve64BitValue(value, isUnsigned) {
305
+ if (Array.isArray(value)) {
306
+ if (value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number") {
307
+ return decode64BitValue(value, isUnsigned);
308
+ }
309
+ return value.map((val) => resolve64BitValue(val, isUnsigned));
310
+ }
311
+ return value;
312
+ }
313
+
221
314
  function dataValueToItemResult(item, dataValue) {
222
315
  const variant = dataValue && dataValue.value ? dataValue.value : null;
316
+ let val = variant ? variant.value : null;
317
+ if (variant && (variant.dataType === DataType.Int64 || variant.dataType === DataType.UInt64)) {
318
+ val = resolve64BitValue(val, variant.dataType === DataType.UInt64);
319
+ }
223
320
  return {
224
321
  name: resolveName(item, resolveNodeId(item)),
225
322
  nodeID: resolveNodeId(item),
226
- value: variant ? variant.value : null,
323
+ value: val,
227
324
  type: variantTypeToName(variant),
228
325
  status: statusCodeToString(dataValue && dataValue.statusCode),
229
326
  sourceTimestamp: timestampToIso(dataValue && dataValue.sourceTimestamp),
@@ -242,18 +339,20 @@ async function dataValueToItemResultEvent(item, eventFields, session) {
242
339
 
243
340
  const eventTypeName = dv.value.value.name;
244
341
 
342
+
343
+
245
344
  return {
246
- eventId: eventFields[0].value,
247
- eventType: eventFields[1].value,
248
- eventTypeName: eventTypeName,
249
- sourceNode: eventFields[2].value.toString(),
250
- sourceName: eventFields[3].value,
251
- message: eventFields[4].value?.text,
252
- severity: eventFields[5].value,
253
- active: eventFields[6].value?.text, // Active / Inactive
254
- AckedState: eventFields[7].value?.text, // Active / Inactive
255
- ConfirmedState: eventFields[8].value?.text, // Active / Inactive
256
- time: eventFields[9].value
345
+ eventId: eventFields[0]?.value ?? null,
346
+ eventType: eventFields[1]?.value ?? null,
347
+ sourceNode: eventFields[2]?.value?.toString() ?? null,
348
+ sourceName: eventFields[3]?.value ?? null,
349
+ message: eventFields[4]?.value?.text ?? null,
350
+ severity: eventFields[5]?.value ?? null,
351
+ active: eventFields[6]?.value?.text ?? null, // Active / Inactive
352
+ AckedState: eventFields[7]?.value?.text ?? null, // Active / Inactive
353
+ ConfirmedState: eventFields[8]?.value?.text ?? null, // Active / Inactive
354
+ time: eventFields[9]?.value ?? null,
355
+ conditionId: eventFields[10]?.value?.toString() ?? null
257
356
  };
258
357
  }
259
358
 
@@ -270,13 +369,19 @@ function callResultToItemResult(item, callResult, argumentDefinition) {
270
369
  name: resolveName(item, methodId),
271
370
  nodeID: methodId,
272
371
  status: statusCodeToString(callResult.statusCode),
273
- outputs: outputArguments.map((variant, index) => ({
274
- name: outputDefinitions[index] && outputDefinitions[index].name
275
- ? String(outputDefinitions[index].name)
276
- : "output" + (index + 1),
277
- type: variantTypeToName(variant),
278
- value: variant ? variant.value : null
279
- }))
372
+ outputs: outputArguments.map((variant, index) => {
373
+ let val = variant ? variant.value : null;
374
+ if (variant && (variant.dataType === DataType.Int64 || variant.dataType === DataType.UInt64)) {
375
+ val = resolve64BitValue(val, variant.dataType === DataType.UInt64);
376
+ }
377
+ return {
378
+ name: outputDefinitions[index] && outputDefinitions[index].name
379
+ ? String(outputDefinitions[index].name)
380
+ : "output" + (index + 1),
381
+ type: variantTypeToName(variant),
382
+ value: val
383
+ };
384
+ })
280
385
  };
281
386
  }
282
387
 
@@ -363,6 +468,95 @@ async function getMethodArgumentDefinition(session, methodNodeId, cache) {
363
468
  return definition;
364
469
  }
365
470
 
471
+ async function enrichItemResultWithEnumeration(result, session, cache, nodeId) {
472
+ if (result.type !== "Int32" && result.type !== "Enumeration") {
473
+ return result;
474
+ }
475
+
476
+ if (typeof result.value !== "number" || !Number.isInteger(result.value)) {
477
+ return result;
478
+ }
479
+
480
+ try {
481
+ const cacheKeyType = "dt:" + nodeId;
482
+ let dtNodeId = cache ? cache.get(cacheKeyType) : undefined;
483
+ if (dtNodeId === undefined) {
484
+ const dv = await session.read({
485
+ nodeId: nodeId,
486
+ attributeId: AttributeIds.DataType
487
+ });
488
+ if (dv.statusCode.isGood()) {
489
+ dtNodeId = dv.value.value;
490
+ if (cache) cache.set(cacheKeyType, dtNodeId);
491
+ } else {
492
+ if (cache) cache.set(cacheKeyType, null);
493
+ }
494
+ }
495
+
496
+ if (!dtNodeId) return result;
497
+
498
+ const cacheKeyStrings = "enumStrings:" + dtNodeId.toString();
499
+ let enumStrings = cache ? cache.get(cacheKeyStrings) : undefined;
500
+
501
+ if (enumStrings === undefined) {
502
+ const browseResult = await session.browse({
503
+ nodeId: dtNodeId,
504
+ referenceTypeId: "HasProperty",
505
+ browseDirection: BrowseDirection.Forward,
506
+ includeSubtypes: true,
507
+ resultMask: 63
508
+ });
509
+
510
+ const enumStringsRef = browseResult.references ? browseResult.references.find(r => r.browseName.name === "EnumStrings") : null;
511
+ const enumValuesRef = browseResult.references ? browseResult.references.find(r => r.browseName.name === "EnumValues") : null;
512
+
513
+ if (enumStringsRef) {
514
+ const dataValue = await session.read({
515
+ nodeId: enumStringsRef.nodeId,
516
+ attributeId: AttributeIds.Value
517
+ });
518
+ if (dataValue.statusCode.isGood() && dataValue.value.value) {
519
+ enumStrings = dataValue.value.value.map(lt => lt.text);
520
+ if (cache) cache.set(cacheKeyStrings, enumStrings);
521
+ } else {
522
+ if (cache) cache.set(cacheKeyStrings, null);
523
+ }
524
+ } else if (enumValuesRef) {
525
+ const dataValue = await session.read({
526
+ nodeId: enumValuesRef.nodeId,
527
+ attributeId: AttributeIds.Value
528
+ });
529
+ if (dataValue.statusCode.isGood() && dataValue.value.value) {
530
+ const map = {};
531
+ dataValue.value.value.forEach(ev => {
532
+ let val;
533
+ if (Array.isArray(ev.value) && ev.value.length === 2) {
534
+ val = ev.value[1]; // low part of Int64
535
+ } else {
536
+ val = Number(ev.value);
537
+ }
538
+ map[val] = ev.displayName.text;
539
+ });
540
+ enumStrings = map;
541
+ if (cache) cache.set(cacheKeyStrings, enumStrings);
542
+ } else {
543
+ if (cache) cache.set(cacheKeyStrings, null);
544
+ }
545
+ } else {
546
+ if (cache) cache.set(cacheKeyStrings, null);
547
+ }
548
+ }
549
+
550
+ if (enumStrings && enumStrings[result.value] !== undefined) {
551
+ result.valueEnumeration = enumStrings[result.value];
552
+ }
553
+ } catch (e) {
554
+ console.error("Error in enrichItemResultWithEnumeration:", e);
555
+ }
556
+
557
+ return result;
558
+ }
559
+
366
560
  module.exports = {
367
561
  AttributeIds,
368
562
  coerceNodeId,
@@ -373,6 +567,7 @@ module.exports = {
373
567
  callResultToItemResult,
374
568
  dataValueToItemResult,
375
569
  dataValueToItemResultEvent,
570
+ enrichItemResultWithEnumeration,
376
571
  ensureArrayPayload,
377
572
  getMethodArgumentDefinition,
378
573
  inferTypeName,
@@ -1,141 +1,141 @@
1
- <link rel="stylesheet" type="text/css" href="opcua-client-resource/style.css">
2
-
3
- <script type="text/javascript" src="opcua-client-resource/script.js"></script>
4
-
5
- <script type="text/html" data-template-name="opcua-client">
6
- <div class="form-row">
7
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
8
- <input type="text" id="node-input-name" placeholder="opcua-client">
9
- </div>
10
- <div class="form-row">
11
- <label for="node-input-connection"><i class="fa fa-server"></i> Connection</label>
12
- <input type="text" id="node-input-connection">
13
- </div>
14
- <div class="form-row">
15
- <label for="node-input-mode"><i class="fa fa-exchange"></i> Mode</label>
16
- <select id="node-input-mode">
17
- <option value="read">Read</option>
18
- <option value="write">Write</option>
19
- <option value="browse">Browse</option>
20
- <option value="method">Method</option>
21
- <option value="getSubscriptionId">getSubscriptionId</option>
22
- <option value="subscription">Subscription</option>
23
- <option value="events">Events</option>
24
- </select>
25
- </div>
26
-
27
- <div class="form-row opcua-client-selection-row">
28
- <label for="node-input-selectedItems"><i class="fa fa-code"></i> JSON</label>
29
- <input type="text" id="node-input-selectedItems" class="opcua-client-json" style="width: 70%;">
30
- </div>
31
- <div id="node-input-browse-modal" class="opcua-tree-modal" style="display:none;">
32
- <div class="opcua-tree-modal__dialog">
33
- <div class="opcua-tree-modal__header">
34
- <div class="opcua-tree-modal__title"><i class="fa fa-sitemap"></i> OPC UA Browse Tree</div>
35
- <a href="#" id="node-input-close-browse-modal" class="editor-button editor-button-small"><i class="fa fa-times"></i> Close</a>
36
- </div>
37
- <div class="opcua-tree-modal__toolbar">
38
- <a href="#" id="node-input-browse-root" class="editor-button editor-button-small"><i class="fa fa-search"></i> Browse Root</a>
39
- <div class="opcua-tree-search">
40
- <i class="fa fa-search"></i>
41
- <input type="text" id="node-input-browse-search" placeholder="Search for items already explored">
42
- <a href="#" id="node-input-browse-search-clear" class="editor-button editor-button-small" style="display:none;"><i class="fa fa-times"></i></a>
43
- </div>
44
- </div>
45
- <div class="opcua-tree-modal__body">
46
- <div id="node-input-browse-tree" class="opcua-tree-editor"></div>
47
- </div>
48
- </div>
49
- <div id="node-input-browse-context-menu" class="opcua-tree-context-menu" style="display:none;">
50
- <a href="#" id="node-input-browse-context-refresh"><i class="fa fa-refresh"></i> Refresh node</a>
51
- <a href="#" id="node-input-browse-context-copy-nodeid"><i class="fa fa-copy"></i> Copy NodeID</a>
52
- </div>
53
- </div>
54
-
55
- <div class="form-row opcua-client-selection-row">
56
- <label style="width:auto;"><i class="fa fa-sitemap"></i> Browse Tree</label>
57
- <a href="#" id="node-input-open-browse-modal" class="editor-button">
58
- <i class="fa fa-expand"></i> Open browse tree
59
- </a>
60
- </div>
61
-
62
- <div class="form-row opcua-client-subscription-row">
63
- <label for="node-input-samplingInterval"><i class="fa fa-clock-o"></i> Sampling</label>
64
- <input type="text" id="node-input-samplingInterval" placeholder="250">
65
- </div>
66
- <div class="form-row opcua-client-subscription-row">
67
- <label for="node-input-publishingInterval"><i class="fa fa-clock-o"></i> Publishing</label>
68
- <input type="text" id="node-input-publishingInterval" placeholder="250">
69
- </div>
70
-
71
- <div class="form-row opcua-client-selection-row" style="margin-bottom:0;">
72
- <label style="width:auto;"><i class="fa fa-tags"></i> Selected items</label>
73
- </div>
74
- <div class="form-row opcua-client-selection-row">
75
- <label style="width:auto;"></label>
76
- <div id="node-input-selected-tags" class="opcua-client-tag-box"></div>
77
- </div>
78
-
79
- </script>
80
-
81
- <script type="text/html" data-help-name="opcua-client">
82
- <p>Unified OPC UA client node for read, write, browse and subscription using a shared client configuration node.</p>
83
-
84
- <h3>Modes</h3>
85
- <p><b>Read</b>: reads one or more variable values.</p>
86
- <p><b>Write</b>: writes one or more variable values.</p>
87
- <p><b>Browse</b>: browses one or more OPC UA nodes and returns their children.</p>
88
- <p><b>Method</b>: calls one or more OPC UA methods.</p>
89
- <p><b>Subscription</b>: subscribes to one or more variable values and emits one message per change.</p>
90
-
91
- <h3>Read</h3>
92
- <p><b>Input</b> <code>msg.payload</code>:</p>
93
- <pre><code>[
94
- {
95
- "name": "status",
96
- "nodeID": "ns=2;s=Factory.Line1.Motor1.status"
97
- }
98
- ]</code></pre>
99
- <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node reads that configured node.</p>
100
-
101
- <h3>Write</h3>
102
- <p><b>Input</b> <code>msg.payload</code>:</p>
103
- <pre><code>[
104
- {
105
- "name": "speed",
106
- "nodeID": "ns=2;s=Factory.Line1.Motor1.speed",
107
- "value": 25.5,
108
- "type": "Float"
109
- }
110
- ]</code></pre>
111
-
112
- <h3>Browse</h3>
113
- <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node browses that configured node.</p>
114
- <p>If <code>msg.payload = []</code>, the node browses the OPC UA <code>RootFolder</code>.</p>
115
-
116
- <h3>Method</h3>
117
- <p><code>nodeID</code> and <code>objectId</code> are accepted explicitly. For backward compatibility, <code>nodeID</code> is also accepted as the method id.</p>
118
- <p><b>Input</b> <code>msg.payload</code>:</p>
119
- <pre><code>[
120
- {
121
- "name": "method1",
122
- "nodeID": "ns=2;s=motor.method1",
123
- "inputs": [
124
- {
125
- "name": "input1",
126
- "type": "Int32",
127
- "value": 1
128
- },
129
- {
130
- "name": "input2",
131
- "type": "Int32",
132
- "value": 1
133
- }
134
- ]
135
- },
136
-
137
-
138
- ]</code></pre>
139
- <h3>Subscription</h3>
140
- <p>In subscription mode the node emits one message per value change.</p>
1
+ <link rel="stylesheet" type="text/css" href="opcua-client-resource/style.css">
2
+
3
+ <script type="text/javascript" src="opcua-client-resource/script.js"></script>
4
+
5
+ <script type="text/html" data-template-name="opcua-client">
6
+ <div class="form-row">
7
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
8
+ <input type="text" id="node-input-name" placeholder="opcua-client">
9
+ </div>
10
+ <div class="form-row">
11
+ <label for="node-input-connection"><i class="fa fa-server"></i> Connection</label>
12
+ <input type="text" id="node-input-connection">
13
+ </div>
14
+ <div class="form-row">
15
+ <label for="node-input-mode"><i class="fa fa-exchange"></i> Mode</label>
16
+ <select id="node-input-mode">
17
+ <option value="read">Read</option>
18
+ <option value="write">Write</option>
19
+ <option value="browse">Browse</option>
20
+ <option value="method">Method</option>
21
+ <option value="getSubscriptionId">getSubscriptionId</option>
22
+ <option value="subscription">Subscription</option>
23
+ <option value="events">Events</option>
24
+ </select>
25
+ </div>
26
+
27
+ <div class="form-row opcua-client-selection-row">
28
+ <label for="node-input-selectedItems"><i class="fa fa-code"></i> JSON</label>
29
+ <input type="text" id="node-input-selectedItems" class="opcua-client-json" style="width: 70%;">
30
+ </div>
31
+ <div id="node-input-browse-modal" class="opcua-tree-modal" style="display:none;">
32
+ <div class="opcua-tree-modal__dialog">
33
+ <div class="opcua-tree-modal__header">
34
+ <div class="opcua-tree-modal__title"><i class="fa fa-sitemap"></i> OPC UA Browse Tree</div>
35
+ <a href="#" id="node-input-close-browse-modal" class="editor-button editor-button-small"><i class="fa fa-times"></i> Close</a>
36
+ </div>
37
+ <div class="opcua-tree-modal__toolbar">
38
+ <a href="#" id="node-input-browse-root" class="editor-button editor-button-small"><i class="fa fa-search"></i> Browse Root</a>
39
+ <div class="opcua-tree-search">
40
+ <i class="fa fa-search"></i>
41
+ <input type="text" id="node-input-browse-search" placeholder="Search for items already explored">
42
+ <a href="#" id="node-input-browse-search-clear" class="editor-button editor-button-small" style="display:none;"><i class="fa fa-times"></i></a>
43
+ </div>
44
+ </div>
45
+ <div class="opcua-tree-modal__body">
46
+ <div id="node-input-browse-tree" class="opcua-tree-editor"></div>
47
+ </div>
48
+ </div>
49
+ <div id="node-input-browse-context-menu" class="opcua-tree-context-menu" style="display:none;">
50
+ <a href="#" id="node-input-browse-context-refresh"><i class="fa fa-refresh"></i> Refresh node</a>
51
+ <a href="#" id="node-input-browse-context-copy-nodeid"><i class="fa fa-copy"></i> Copy NodeID</a>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="form-row opcua-client-selection-row">
56
+ <label style="width:auto;"><i class="fa fa-sitemap"></i> Browse Tree</label>
57
+ <a href="#" id="node-input-open-browse-modal" class="editor-button">
58
+ <i class="fa fa-expand"></i> Open browse tree
59
+ </a>
60
+ </div>
61
+
62
+ <div class="form-row opcua-client-subscription-row">
63
+ <label for="node-input-samplingInterval"><i class="fa fa-clock-o"></i> Sampling</label>
64
+ <input type="text" id="node-input-samplingInterval" placeholder="250">
65
+ </div>
66
+ <div class="form-row opcua-client-subscription-row">
67
+ <label for="node-input-publishingInterval"><i class="fa fa-clock-o"></i> Publishing</label>
68
+ <input type="text" id="node-input-publishingInterval" placeholder="250">
69
+ </div>
70
+
71
+ <div class="form-row opcua-client-selection-row" style="margin-bottom:0;">
72
+ <label style="width:auto;"><i class="fa fa-tags"></i> Selected items</label>
73
+ </div>
74
+ <div class="form-row opcua-client-selection-row">
75
+ <label style="width:auto;"></label>
76
+ <div id="node-input-selected-tags" class="opcua-client-tag-box"></div>
77
+ </div>
78
+
79
+ </script>
80
+
81
+ <script type="text/html" data-help-name="opcua-client">
82
+ <p>Unified OPC UA client node for read, write, browse and subscription using a shared client configuration node.</p>
83
+
84
+ <h3>Modes</h3>
85
+ <p><b>Read</b>: reads one or more variable values.</p>
86
+ <p><b>Write</b>: writes one or more variable values.</p>
87
+ <p><b>Browse</b>: browses one or more OPC UA nodes and returns their children.</p>
88
+ <p><b>Method</b>: calls one or more OPC UA methods.</p>
89
+ <p><b>Subscription</b>: subscribes to one or more variable values and emits one message per change.</p>
90
+
91
+ <h3>Read</h3>
92
+ <p><b>Input</b> <code>msg.payload</code>:</p>
93
+ <pre><code>[
94
+ {
95
+ "name": "status",
96
+ "nodeID": "ns=2;s=Factory.Line1.Motor1.status"
97
+ }
98
+ ]</code></pre>
99
+ <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node reads that configured node.</p>
100
+
101
+ <h3>Write</h3>
102
+ <p><b>Input</b> <code>msg.payload</code>:</p>
103
+ <pre><code>[
104
+ {
105
+ "name": "speed",
106
+ "nodeID": "ns=2;s=Factory.Line1.Motor1.speed",
107
+ "value": 25.5,
108
+ "type": "Float"
109
+ }
110
+ ]</code></pre>
111
+
112
+ <h3>Browse</h3>
113
+ <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node browses that configured node.</p>
114
+ <p>If <code>msg.payload = []</code>, the node browses the OPC UA <code>RootFolder</code>.</p>
115
+
116
+ <h3>Method</h3>
117
+ <p><code>nodeID</code> and <code>objectId</code> are accepted explicitly. For backward compatibility, <code>nodeID</code> is also accepted as the method id.</p>
118
+ <p><b>Input</b> <code>msg.payload</code>:</p>
119
+ <pre><code>[
120
+ {
121
+ "name": "method1",
122
+ "nodeID": "ns=2;s=motor.method1",
123
+ "inputs": [
124
+ {
125
+ "name": "input1",
126
+ "type": "Int32",
127
+ "value": 1
128
+ },
129
+ {
130
+ "name": "input2",
131
+ "type": "Int32",
132
+ "value": 1
133
+ }
134
+ ]
135
+ },
136
+
137
+
138
+ ]</code></pre>
139
+ <h3>Subscription</h3>
140
+ <p>In subscription mode the node emits one message per value change.</p>
141
141
  </script>