node-opcua-address-space 2.186.8 → 2.186.10
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/dist/impl/address_space.d.ts.map +1 -1
- package/dist/impl/address_space.js +2 -3
- package/dist/impl/address_space.js.map +1 -1
- package/dist/impl/historical_access/address_space_historical_data_node.d.ts +3 -1
- package/dist/impl/historical_access/address_space_historical_data_node.d.ts.map +1 -1
- package/dist/impl/historical_access/address_space_historical_data_node.js +142 -1
- package/dist/impl/historical_access/address_space_historical_data_node.js.map +1 -1
- package/dist/impl/ua_variable_impl.d.ts.map +1 -1
- package/dist/impl/ua_variable_impl.js +9 -0
- package/dist/impl/ua_variable_impl.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/impl/address_space.ts +4 -3
- package/impl/historical_access/address_space_historical_data_node.ts +188 -2
- package/impl/ua_variable_impl.ts +9 -0
- package/package.json +18 -18
package/impl/address_space.ts
CHANGED
|
@@ -1042,12 +1042,13 @@ export class AddressSpaceImpl implements AddressSpacePrivate {
|
|
|
1042
1042
|
if (!structureDataType) {
|
|
1043
1043
|
throw new Error("getExtensionObjectConstructor: cannot find 'Structure' DataType in standard address Space");
|
|
1044
1044
|
}
|
|
1045
|
-
/* c8 ignore next */
|
|
1046
1045
|
if (!_dataType.isSubtypeOf(structureDataType)) {
|
|
1047
|
-
// c8 ignore next
|
|
1048
1046
|
doDebug && debugLog(_dataType.toString());
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`getExtensionObjectConstructor: ${_dataType.browseName.toString()} (${_dataType.nodeId.toString()}) ` +
|
|
1049
|
+
"is not a Structure, so it has no extension object constructor"
|
|
1050
|
+
);
|
|
1049
1051
|
}
|
|
1050
|
-
assert(_dataType.isSubtypeOf(structureDataType));
|
|
1051
1052
|
if (!_dataType._extensionObjectConstructor) {
|
|
1052
1053
|
if (!this.$$extraDataTypeManager) {
|
|
1053
1054
|
throw new Error(
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
} from "node-opcua-address-space-base";
|
|
15
15
|
import { assert } from "node-opcua-assert";
|
|
16
16
|
import { AccessLevelFlag, NodeClass, type QualifiedNameLike } from "node-opcua-data-model";
|
|
17
|
-
import
|
|
17
|
+
import { DataValue } from "node-opcua-data-value";
|
|
18
18
|
import { isMinDate, minDate } from "node-opcua-date-time";
|
|
19
19
|
import { make_warningLog } from "node-opcua-debug";
|
|
20
20
|
import type { UAHistoricalDataConfiguration } from "node-opcua-nodeset-ua";
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
ReadProcessedDetails,
|
|
28
28
|
ReadRawModifiedDetails
|
|
29
29
|
} from "node-opcua-service-history";
|
|
30
|
-
import { type CallbackT, StatusCodes } from "node-opcua-status-code";
|
|
30
|
+
import { type CallbackT, StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
31
31
|
import { DataType } from "node-opcua-variant";
|
|
32
32
|
import type { AddressSpacePrivate } from "../address_space_private.js";
|
|
33
33
|
import { historizerFactoryHolder } from "../historizer_factory.js";
|
|
@@ -88,6 +88,32 @@ function inInTimeRange2(historyReadDetails: ReadRawModifiedDetails, dataValue: D
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function _findNearestBefore(q: Dequeue<DataValue>, timeMs: number): DataValue | null {
|
|
92
|
+
let result: DataValue | null = null;
|
|
93
|
+
let c = q.head.next;
|
|
94
|
+
while (c.data) {
|
|
95
|
+
const ts = (c.data.sourceTimestamp || minDate).getTime();
|
|
96
|
+
if (ts >= timeMs) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
result = c.data;
|
|
100
|
+
c = c.next;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _findNearestAfter(q: Dequeue<DataValue>, timeMs: number): DataValue | null {
|
|
106
|
+
let c = q.head.next;
|
|
107
|
+
while (c.data) {
|
|
108
|
+
const ts = (c.data.sourceTimestamp || minDate).getTime();
|
|
109
|
+
if (ts > timeMs) {
|
|
110
|
+
return c.data;
|
|
111
|
+
}
|
|
112
|
+
c = c.next;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
91
117
|
function filter_dequeue(
|
|
92
118
|
q: Dequeue<DataValue>,
|
|
93
119
|
historyReadRawModifiedDetails: ReadRawModifiedDetails,
|
|
@@ -201,6 +227,14 @@ export class VariableHistorian implements IVariableHistorian {
|
|
|
201
227
|
}
|
|
202
228
|
callback(null, dataValues);
|
|
203
229
|
}
|
|
230
|
+
|
|
231
|
+
public findBoundBefore(date: Date): DataValue | null {
|
|
232
|
+
return _findNearestBefore(this._timeline, date.getTime());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public findBoundAfter(date: Date): DataValue | null {
|
|
236
|
+
return _findNearestAfter(this._timeline, date.getTime());
|
|
237
|
+
}
|
|
204
238
|
}
|
|
205
239
|
|
|
206
240
|
function _get_startOfOfflineArchive(node: UAVariableImpl) {
|
|
@@ -354,6 +388,139 @@ function _historyReadRawAsync(
|
|
|
354
388
|
this.varHistorian.extractDataValues(historyReadRawModifiedDetails, maxNumberToExtract, isReversed, reverseDataValue, callback);
|
|
355
389
|
}
|
|
356
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Compute a single Bounding Value for the raw-read time domain (OPC 10000-11 6.5.3.2).
|
|
393
|
+
*
|
|
394
|
+
* `before` and `after` are the nearest raw values immediately before and after
|
|
395
|
+
* `boundaryTime` (either may come from outside the requested [startTime, endTime] window).
|
|
396
|
+
* Per 3.1.2 BoundingValues, when neither is missing the Server "determines" a value
|
|
397
|
+
* associated with the boundary time itself, built from that neighbouring data; this
|
|
398
|
+
* function follows the same rule the HistoricalDataConfiguration already uses for the
|
|
399
|
+
* Interpolative Aggregate (Part 13): a sloped linear interpolation, or a held value when
|
|
400
|
+
* the node is configured as Stepped or the value is not numeric.
|
|
401
|
+
*
|
|
402
|
+
* When one side is missing there is nothing to determine the value from, so the result
|
|
403
|
+
* is Bad_BoundNotFound with a null value at the boundary time, as required by 6.5.3.2.
|
|
404
|
+
*/
|
|
405
|
+
function _computeBoundingValue(
|
|
406
|
+
node: UAVariableImpl,
|
|
407
|
+
before: DataValue | null,
|
|
408
|
+
after: DataValue | null,
|
|
409
|
+
boundaryTime: Date
|
|
410
|
+
): DataValue {
|
|
411
|
+
if (!before || !after || !before.sourceTimestamp || !after.sourceTimestamp) {
|
|
412
|
+
return new DataValue({
|
|
413
|
+
sourceTimestamp: boundaryTime,
|
|
414
|
+
statusCode: StatusCodes.BadBoundNotFound,
|
|
415
|
+
value: { dataType: DataType.Null }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const stepped = node.$historicalDataConfiguration?.stepped?.readValue()?.value?.value === true;
|
|
420
|
+
const beforeValue = before.value?.value;
|
|
421
|
+
const afterValue = after.value?.value;
|
|
422
|
+
if (stepped || typeof beforeValue !== "number" || typeof afterValue !== "number") {
|
|
423
|
+
// stepped display, or a value that cannot be placed on a slope: hold the value that
|
|
424
|
+
// was in effect at the boundary time (the sample immediately preceding it).
|
|
425
|
+
return new DataValue({
|
|
426
|
+
sourceTimestamp: boundaryTime,
|
|
427
|
+
statusCode: StatusCode.makeStatusCode(before.statusCode, "HistorianInterpolated"),
|
|
428
|
+
value: before.value
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const t0 = before.sourceTimestamp.getTime();
|
|
433
|
+
const t1 = after.sourceTimestamp.getTime();
|
|
434
|
+
const t = boundaryTime.getTime();
|
|
435
|
+
const coef1 = (t - t0) / (t1 - t0);
|
|
436
|
+
const coef2 = (t1 - t) / (t1 - t0);
|
|
437
|
+
const value = before.value.clone();
|
|
438
|
+
value.value = coef2 * beforeValue + coef1 * afterValue;
|
|
439
|
+
return new DataValue({
|
|
440
|
+
sourceTimestamp: boundaryTime,
|
|
441
|
+
statusCode: StatusCode.makeStatusCode(before.statusCode, "HistorianInterpolated"),
|
|
442
|
+
value
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Add the start and end Bounding Values to a raw-read result when returnBounds is set
|
|
448
|
+
* (OPC 10000-11 6.5.3.2 and 4.6). Only called when both startTime and endTime were
|
|
449
|
+
* specified: the single-sided requests (only startTime, or only endTime) are read to the
|
|
450
|
+
* edge of the online archive rather than a bounded window, so there is no second boundary
|
|
451
|
+
* to compute a bound against, and are left unbounded here.
|
|
452
|
+
*
|
|
453
|
+
* `dataValues` is in the order the client will receive it (already reversed when
|
|
454
|
+
* `reverseDataValue` is set); this function restores chronological order to reason about
|
|
455
|
+
* "first" and "last", then re-applies the same order on the way out.
|
|
456
|
+
*/
|
|
457
|
+
function _addBoundingValues(
|
|
458
|
+
node: UAVariableImpl,
|
|
459
|
+
details: ReadRawModifiedDetails,
|
|
460
|
+
dataValues: DataValue[],
|
|
461
|
+
reverseDataValue: boolean
|
|
462
|
+
): DataValue[] {
|
|
463
|
+
const historian = node.varHistorian;
|
|
464
|
+
const startTime = details.startTime as Date;
|
|
465
|
+
const endTime = details.endTime as Date;
|
|
466
|
+
|
|
467
|
+
const findBefore = (date: Date): DataValue | null => (historian?.findBoundBefore ? historian.findBoundBefore(date) : null);
|
|
468
|
+
const findAfter = (date: Date): DataValue | null => (historian?.findBoundAfter ? historian.findBoundAfter(date) : null);
|
|
469
|
+
const boundNotSupported = (boundaryTime: Date): DataValue =>
|
|
470
|
+
new DataValue({
|
|
471
|
+
sourceTimestamp: boundaryTime,
|
|
472
|
+
statusCode: StatusCodes.BadBoundNotSupported,
|
|
473
|
+
value: { dataType: DataType.Null }
|
|
474
|
+
});
|
|
475
|
+
const supportsBounds = !!(historian?.findBoundBefore && historian.findBoundAfter);
|
|
476
|
+
|
|
477
|
+
const ascending = reverseDataValue ? dataValues.slice().reverse() : dataValues.slice();
|
|
478
|
+
|
|
479
|
+
if (startTime.getTime() === endTime.getTime()) {
|
|
480
|
+
// a request for a single instant: the value at that instant, if any, is already
|
|
481
|
+
// part of dataValues, so there is at most one bound to add.
|
|
482
|
+
if (ascending.length === 0) {
|
|
483
|
+
const bound = supportsBounds
|
|
484
|
+
? _computeBoundingValue(node, findBefore(startTime), findAfter(startTime), startTime)
|
|
485
|
+
: boundNotSupported(startTime);
|
|
486
|
+
ascending.push(bound);
|
|
487
|
+
}
|
|
488
|
+
return reverseDataValue ? ascending.reverse() : ascending;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const result: DataValue[] = [];
|
|
492
|
+
|
|
493
|
+
const hasExactStart = ascending.length > 0 && ascending[0].sourceTimestamp?.getTime() === startTime.getTime();
|
|
494
|
+
if (!hasExactStart) {
|
|
495
|
+
const bound = supportsBounds
|
|
496
|
+
? _computeBoundingValue(
|
|
497
|
+
node,
|
|
498
|
+
findBefore(startTime),
|
|
499
|
+
ascending.length > 0 ? ascending[0] : findAfter(startTime),
|
|
500
|
+
startTime
|
|
501
|
+
)
|
|
502
|
+
: boundNotSupported(startTime);
|
|
503
|
+
result.push(bound);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
result.push(...ascending);
|
|
507
|
+
|
|
508
|
+
const hasExactEnd = ascending.length > 0 && ascending[ascending.length - 1].sourceTimestamp?.getTime() === endTime.getTime();
|
|
509
|
+
if (!hasExactEnd) {
|
|
510
|
+
const bound = supportsBounds
|
|
511
|
+
? _computeBoundingValue(
|
|
512
|
+
node,
|
|
513
|
+
ascending.length > 0 ? ascending[ascending.length - 1] : findBefore(endTime),
|
|
514
|
+
findAfter(endTime),
|
|
515
|
+
endTime
|
|
516
|
+
)
|
|
517
|
+
: boundNotSupported(endTime);
|
|
518
|
+
result.push(bound);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return reverseDataValue ? result.reverse() : result;
|
|
522
|
+
}
|
|
523
|
+
|
|
357
524
|
function _historyReadRaw(
|
|
358
525
|
this: UAVariable,
|
|
359
526
|
context: ISessionContext,
|
|
@@ -534,6 +701,22 @@ function _historyReadRaw(
|
|
|
534
701
|
return callback(err);
|
|
535
702
|
}
|
|
536
703
|
|
|
704
|
+
// Bounding Values (6.4.3.2 / 6.5.3.2): only meaningful when the request has both
|
|
705
|
+
// ends of a time domain to bound. See _addBoundingValues for why the single-sided
|
|
706
|
+
// requests (only startTime, or only endTime) are left unbounded.
|
|
707
|
+
if (
|
|
708
|
+
historyReadRawModifiedDetails.returnBounds &&
|
|
709
|
+
!isMinDate(historyReadRawModifiedDetails.startTime) &&
|
|
710
|
+
!isMinDate(historyReadRawModifiedDetails.endTime)
|
|
711
|
+
) {
|
|
712
|
+
dataValues = _addBoundingValues(
|
|
713
|
+
this as UAVariableImpl,
|
|
714
|
+
historyReadRawModifiedDetails,
|
|
715
|
+
dataValues,
|
|
716
|
+
reverseDataValue
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
537
720
|
const cnt = session.continuationPointManager.registerHistoryReadRaw(
|
|
538
721
|
historyReadRawModifiedDetails.numValuesPerNode,
|
|
539
722
|
dataValues,
|
|
@@ -590,6 +773,7 @@ function _historyRead(
|
|
|
590
773
|
statusCode: StatusCodes.BadUserAccessDenied
|
|
591
774
|
});
|
|
592
775
|
callback(null, result);
|
|
776
|
+
return;
|
|
593
777
|
}
|
|
594
778
|
|
|
595
779
|
assert(typeof callback === "function");
|
|
@@ -846,4 +1030,6 @@ export function AddressSpace_installHistoricalDataNode(
|
|
|
846
1030
|
// update the index of historizing nodes in the addressSpace
|
|
847
1031
|
node.addressSpace.historizingNodes = node.addressSpace.historizingNodes || new Set();
|
|
848
1032
|
node.addressSpace.historizingNodes.add(node);
|
|
1033
|
+
|
|
1034
|
+
node.addressSpace.onHistorizingNodeAdded?.(node);
|
|
849
1035
|
}
|
package/impl/ua_variable_impl.ts
CHANGED
|
@@ -1468,6 +1468,15 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
|
|
|
1468
1468
|
if (dataType.nodeId.namespace === 0 && dataType.nodeId.value === DataType.ExtensionObject) {
|
|
1469
1469
|
return true;
|
|
1470
1470
|
}
|
|
1471
|
+
// A variable declared with a dataType that is not a Structure admits any extension
|
|
1472
|
+
// object - BaseDataType is the usual case (AutoID's LastScanData, or any variable that
|
|
1473
|
+
// holds whatever its source produces) - so there is no constructor to compare the value
|
|
1474
|
+
// with. Asking getExtensionObjectConstructor for one used to fail an assert with no
|
|
1475
|
+
// message, which turned a legitimate write from a method into a failed call.
|
|
1476
|
+
const structureDataType = addressSpace.findDataType("Structure");
|
|
1477
|
+
if (structureDataType && !dataType.isSubtypeOf(structureDataType)) {
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1471
1480
|
const Constructor = addressSpace.getExtensionObjectConstructor(this.dataType);
|
|
1472
1481
|
|
|
1473
1482
|
if (this.valueRank === -1) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-address-space",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.186.
|
|
4
|
+
"version": "2.186.10",
|
|
5
5
|
"description": "pure nodejs OPCUA SDK - module address-space",
|
|
6
6
|
"main": "./dist/api/index.js",
|
|
7
7
|
"types": "./dist/api/index.d.ts",
|
|
@@ -23,35 +23,35 @@
|
|
|
23
23
|
"@types/semver": "7.8.0",
|
|
24
24
|
"chalk": "6.0.1",
|
|
25
25
|
"dequeue": "^1.0.5",
|
|
26
|
-
"node-opcua-address-space-base": "2.186.
|
|
26
|
+
"node-opcua-address-space-base": "2.186.10",
|
|
27
27
|
"node-opcua-assert": "2.186.7",
|
|
28
28
|
"node-opcua-basic-types": "2.186.7",
|
|
29
29
|
"node-opcua-binary-stream": "2.186.7",
|
|
30
|
-
"node-opcua-client-dynamic-extension-object": "2.186.
|
|
30
|
+
"node-opcua-client-dynamic-extension-object": "2.186.10",
|
|
31
31
|
"node-opcua-constants": "2.185.0",
|
|
32
32
|
"node-opcua-crypto": "6.1.2",
|
|
33
|
-
"node-opcua-data-access": "2.186.
|
|
34
|
-
"node-opcua-data-model": "2.186.
|
|
35
|
-
"node-opcua-data-value": "2.186.
|
|
33
|
+
"node-opcua-data-access": "2.186.10",
|
|
34
|
+
"node-opcua-data-model": "2.186.10",
|
|
35
|
+
"node-opcua-data-value": "2.186.10",
|
|
36
36
|
"node-opcua-date-time": "2.186.7",
|
|
37
37
|
"node-opcua-debug": "2.186.7",
|
|
38
38
|
"node-opcua-enum": "2.186.7",
|
|
39
39
|
"node-opcua-extension-object": "2.186.7",
|
|
40
40
|
"node-opcua-factory": "2.186.7",
|
|
41
41
|
"node-opcua-nodeid": "2.186.7",
|
|
42
|
-
"node-opcua-nodeset-ua": "2.186.
|
|
42
|
+
"node-opcua-nodeset-ua": "2.186.10",
|
|
43
43
|
"node-opcua-numeric-range": "2.186.7",
|
|
44
44
|
"node-opcua-object-registry": "2.186.7",
|
|
45
|
-
"node-opcua-pseudo-session": "2.186.
|
|
46
|
-
"node-opcua-service-browse": "2.186.
|
|
47
|
-
"node-opcua-service-call": "2.186.
|
|
48
|
-
"node-opcua-service-history": "2.186.
|
|
49
|
-
"node-opcua-service-translate-browse-path": "2.186.
|
|
50
|
-
"node-opcua-service-write": "2.186.
|
|
45
|
+
"node-opcua-pseudo-session": "2.186.10",
|
|
46
|
+
"node-opcua-service-browse": "2.186.10",
|
|
47
|
+
"node-opcua-service-call": "2.186.10",
|
|
48
|
+
"node-opcua-service-history": "2.186.10",
|
|
49
|
+
"node-opcua-service-translate-browse-path": "2.186.10",
|
|
50
|
+
"node-opcua-service-write": "2.186.10",
|
|
51
51
|
"node-opcua-status-code": "2.186.7",
|
|
52
|
-
"node-opcua-types": "2.186.
|
|
52
|
+
"node-opcua-types": "2.186.10",
|
|
53
53
|
"node-opcua-utils": "2.186.7",
|
|
54
|
-
"node-opcua-variant": "2.186.
|
|
54
|
+
"node-opcua-variant": "2.186.10",
|
|
55
55
|
"node-opcua-xml2json": "2.186.7",
|
|
56
56
|
"semver": "^7.8.5",
|
|
57
57
|
"thenify-ex": "4.4.0",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"node-opcua-leak-detector": "2.186.7",
|
|
64
64
|
"node-opcua-nodesets": "2.185.0",
|
|
65
65
|
"node-opcua-packet-analyzer": "2.186.7",
|
|
66
|
-
"node-opcua-schemas": "2.186.
|
|
67
|
-
"node-opcua-service-filter": "2.186.
|
|
66
|
+
"node-opcua-schemas": "2.186.10",
|
|
67
|
+
"node-opcua-service-filter": "2.186.10",
|
|
68
68
|
"node-opcua-test-fixtures": "2.184.0",
|
|
69
69
|
"node-opcua-test-helpers": "2.184.0"
|
|
70
70
|
},
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"internet of things"
|
|
84
84
|
],
|
|
85
85
|
"homepage": "http://node-opcua.github.io/",
|
|
86
|
-
"gitHead": "
|
|
86
|
+
"gitHead": "f6e25f12e17feabaf6ac2b07621b8bfbf92375c9",
|
|
87
87
|
"files": [
|
|
88
88
|
"dist",
|
|
89
89
|
"distHelpers",
|