@vitormnm/node-red-simple-opcua 1.5.0 → 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.
- package/client/lib/opcua-client-read-service.js +14 -2
- package/client/lib/opcua-client-subscription-service.js +8 -3
- package/client/opcua-client-utils.js +206 -12
- package/examples/flows_simple_opc.json +1 -2851
- package/package.json +1 -1
- package/server/lib/opcua-address-space-builder.js +203 -9
- package/server/lib/opcua-config.js +131 -8
- package/server/lib/opcua-constants.js +1 -0
- package/server/lib/opcua-server-methods.js +2 -0
- package/server/lib/opcua-server-runtime-child.js +3 -2
- package/server/lib/opcua-server-runtime.js +14 -5
- package/server/opcua-server.html +4 -1
- package/server/view/opcua-server.css +4 -0
- package/server/view/opcua-server.js +178 -28
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { dataValueToItemResult, resolveNodeId } = require("../opcua-client-utils");
|
|
3
|
+
const { dataValueToItemResult, resolveNodeId, enrichItemResultWithEnumeration } = require("../opcua-client-utils");
|
|
4
4
|
|
|
5
5
|
class OpcUaClientReadService {
|
|
6
6
|
async execute(node, msg, session, itemsResolver) {
|
|
7
7
|
const items = itemsResolver.ensureClientItems(node, msg, "OPC UA read");
|
|
8
8
|
const nodeIds = items.map((item) => resolveNodeId(item));
|
|
9
9
|
const values = await session.readVariableValue(nodeIds);
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
const results = [];
|
|
13
|
+
|
|
14
|
+
for (let index = 0; index < values.length; index++) {
|
|
15
|
+
const dataValue = values[index];
|
|
16
|
+
const item = items[index];
|
|
17
|
+
let result = dataValueToItemResult(item, dataValue);
|
|
18
|
+
result = await enrichItemResultWithEnumeration(result, session, cache, nodeIds[index]);
|
|
19
|
+
results.push(result);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return results;
|
|
11
23
|
}
|
|
12
24
|
}
|
|
13
25
|
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
const {
|
|
12
12
|
dataValueToItemResult,
|
|
13
13
|
dataValueToItemResultEvent,
|
|
14
|
+
enrichItemResultWithEnumeration,
|
|
14
15
|
resolveName,
|
|
15
16
|
resolveNodeId,
|
|
16
17
|
statusCodeToString
|
|
@@ -49,8 +50,11 @@ class OpcUaClientSubscriptionService {
|
|
|
49
50
|
TimestampsToReturn.Both
|
|
50
51
|
);
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const cache = new Map();
|
|
54
|
+
monitoredItem.on("changed", async (dataValue) => {
|
|
55
|
+
let payload = dataValueToItemResult(item, dataValue);
|
|
56
|
+
payload = await enrichItemResultWithEnumeration(payload, session, cache, resolveNodeId(item));
|
|
57
|
+
|
|
54
58
|
node.status({
|
|
55
59
|
fill: "blue",
|
|
56
60
|
shape: "dot",
|
|
@@ -108,7 +112,8 @@ class OpcUaClientSubscriptionService {
|
|
|
108
112
|
"ActiveState",
|
|
109
113
|
"AckedState",
|
|
110
114
|
"ConfirmedState",
|
|
111
|
-
"Time"
|
|
115
|
+
"Time",
|
|
116
|
+
"ConditionId"
|
|
112
117
|
]);
|
|
113
118
|
|
|
114
119
|
node.monitoredItems = items.map((item) => {
|
|
@@ -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
|
-
|
|
107
|
-
|
|
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:
|
|
323
|
+
value: val,
|
|
227
324
|
type: variantTypeToName(variant),
|
|
228
325
|
status: statusCodeToString(dataValue && dataValue.statusCode),
|
|
229
326
|
sourceTimestamp: timestampToIso(dataValue && dataValue.sourceTimestamp),
|
|
@@ -254,7 +351,8 @@ async function dataValueToItemResultEvent(item, eventFields, session) {
|
|
|
254
351
|
active: eventFields[6]?.value?.text ?? null, // Active / Inactive
|
|
255
352
|
AckedState: eventFields[7]?.value?.text ?? null, // Active / Inactive
|
|
256
353
|
ConfirmedState: eventFields[8]?.value?.text ?? null, // Active / Inactive
|
|
257
|
-
time: eventFields[9]?.value ?? null
|
|
354
|
+
time: eventFields[9]?.value ?? null,
|
|
355
|
+
conditionId: eventFields[10]?.value?.toString() ?? null
|
|
258
356
|
};
|
|
259
357
|
}
|
|
260
358
|
|
|
@@ -271,13 +369,19 @@ function callResultToItemResult(item, callResult, argumentDefinition) {
|
|
|
271
369
|
name: resolveName(item, methodId),
|
|
272
370
|
nodeID: methodId,
|
|
273
371
|
status: statusCodeToString(callResult.statusCode),
|
|
274
|
-
outputs: outputArguments.map((variant, index) =>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
})
|
|
281
385
|
};
|
|
282
386
|
}
|
|
283
387
|
|
|
@@ -364,6 +468,95 @@ async function getMethodArgumentDefinition(session, methodNodeId, cache) {
|
|
|
364
468
|
return definition;
|
|
365
469
|
}
|
|
366
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
|
+
|
|
367
560
|
module.exports = {
|
|
368
561
|
AttributeIds,
|
|
369
562
|
coerceNodeId,
|
|
@@ -374,6 +567,7 @@ module.exports = {
|
|
|
374
567
|
callResultToItemResult,
|
|
375
568
|
dataValueToItemResult,
|
|
376
569
|
dataValueToItemResultEvent,
|
|
570
|
+
enrichItemResultWithEnumeration,
|
|
377
571
|
ensureArrayPayload,
|
|
378
572
|
getMethodArgumentDefinition,
|
|
379
573
|
inferTypeName,
|