@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.
@@ -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
- return values.map((dataValue, index) => dataValueToItemResult(items[index], dataValue));
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
- monitoredItem.on("changed", (dataValue) => {
53
- const payload = dataValueToItemResult(item, dataValue);
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
- 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),
@@ -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
- name: outputDefinitions[index] && outputDefinitions[index].name
276
- ? String(outputDefinitions[index].name)
277
- : "output" + (index + 1),
278
- type: variantTypeToName(variant),
279
- value: variant ? variant.value : null
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,