node-opcua-address-space 2.175.1 → 2.175.3
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/source/loader/make_xml_extension_object_parser.js +39 -2
- package/dist/source/loader/make_xml_extension_object_parser.js.map +1 -1
- package/dist/source/namespace_data_access.d.ts +7 -1
- package/dist/src/namespace_impl.js +97 -36
- package/dist/src/namespace_impl.js.map +1 -1
- package/dist/src/nodeid_manager.d.ts +16 -0
- package/dist/src/nodeid_manager.js +40 -3
- package/dist/src/nodeid_manager.js.map +1 -1
- package/dist/src/nodeset_tools/construct_namespace_dependency.js +3 -1
- package/dist/src/nodeset_tools/construct_namespace_dependency.js.map +1 -1
- package/dist/src/ua_variable_impl.js +10 -15
- package/dist/src/ua_variable_impl.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/source/loader/make_xml_extension_object_parser.ts +45 -3
- package/source/namespace_data_access.ts +9 -1
- package/src/namespace_impl.ts +115 -42
- package/src/nodeid_manager.ts +45 -4
- package/src/nodeset_tools/construct_namespace_dependency.ts +3 -1
- package/src/ua_variable_impl.ts +10 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-address-space",
|
|
3
|
-
"version": "2.175.
|
|
3
|
+
"version": "2.175.3",
|
|
4
4
|
"description": "pure nodejs OPCUA SDK - module address-space",
|
|
5
5
|
"main": "./dist/src/index_current.js",
|
|
6
6
|
"types": "./dist/source/index.d.ts",
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
"@types/semver": "7.7.1",
|
|
22
22
|
"chalk": "4.1.2",
|
|
23
23
|
"dequeue": "^1.0.5",
|
|
24
|
-
"node-opcua-address-space-base": "2.175.
|
|
24
|
+
"node-opcua-address-space-base": "2.175.2",
|
|
25
25
|
"node-opcua-assert": "2.175.1",
|
|
26
26
|
"node-opcua-basic-types": "2.175.1",
|
|
27
27
|
"node-opcua-binary-stream": "2.175.1",
|
|
28
|
-
"node-opcua-client-dynamic-extension-object": "2.175.
|
|
28
|
+
"node-opcua-client-dynamic-extension-object": "2.175.2",
|
|
29
29
|
"node-opcua-constants": "2.175.1",
|
|
30
30
|
"node-opcua-crypto": "5.3.6",
|
|
31
|
-
"node-opcua-data-access": "2.175.
|
|
31
|
+
"node-opcua-data-access": "2.175.2",
|
|
32
32
|
"node-opcua-data-model": "2.175.1",
|
|
33
33
|
"node-opcua-data-value": "2.175.1",
|
|
34
34
|
"node-opcua-date-time": "2.175.1",
|
|
@@ -37,20 +37,20 @@
|
|
|
37
37
|
"node-opcua-extension-object": "2.175.1",
|
|
38
38
|
"node-opcua-factory": "2.175.1",
|
|
39
39
|
"node-opcua-nodeid": "2.175.1",
|
|
40
|
-
"node-opcua-nodeset-ua": "2.175.
|
|
40
|
+
"node-opcua-nodeset-ua": "2.175.3",
|
|
41
41
|
"node-opcua-numeric-range": "2.175.1",
|
|
42
42
|
"node-opcua-object-registry": "2.175.1",
|
|
43
|
-
"node-opcua-pseudo-session": "2.175.
|
|
44
|
-
"node-opcua-service-browse": "2.175.
|
|
45
|
-
"node-opcua-service-call": "2.175.
|
|
46
|
-
"node-opcua-service-history": "2.175.
|
|
47
|
-
"node-opcua-service-translate-browse-path": "2.175.
|
|
48
|
-
"node-opcua-service-write": "2.175.
|
|
43
|
+
"node-opcua-pseudo-session": "2.175.2",
|
|
44
|
+
"node-opcua-service-browse": "2.175.2",
|
|
45
|
+
"node-opcua-service-call": "2.175.2",
|
|
46
|
+
"node-opcua-service-history": "2.175.2",
|
|
47
|
+
"node-opcua-service-translate-browse-path": "2.175.2",
|
|
48
|
+
"node-opcua-service-write": "2.175.2",
|
|
49
49
|
"node-opcua-status-code": "2.175.1",
|
|
50
|
-
"node-opcua-types": "2.175.
|
|
50
|
+
"node-opcua-types": "2.175.2",
|
|
51
51
|
"node-opcua-utils": "2.175.1",
|
|
52
52
|
"node-opcua-variant": "2.175.1",
|
|
53
|
-
"node-opcua-xml2json": "2.175.
|
|
53
|
+
"node-opcua-xml2json": "2.175.2",
|
|
54
54
|
"semver": "^7.8.5",
|
|
55
55
|
"thenify-ex": "4.4.0",
|
|
56
56
|
"xml-writer": "^1.7.0"
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"humanize": "0.0.9",
|
|
60
60
|
"node-opcua-benchmarker": "2.175.1",
|
|
61
61
|
"node-opcua-leak-detector": "2.175.1",
|
|
62
|
-
"node-opcua-nodesets": "2.175.
|
|
62
|
+
"node-opcua-nodesets": "2.175.3",
|
|
63
63
|
"node-opcua-packet-analyzer": "2.175.1",
|
|
64
|
-
"node-opcua-schemas": "2.175.
|
|
65
|
-
"node-opcua-service-filter": "2.175.
|
|
64
|
+
"node-opcua-schemas": "2.175.2",
|
|
65
|
+
"node-opcua-service-filter": "2.175.2",
|
|
66
66
|
"node-opcua-test-fixtures": "2.175.1"
|
|
67
67
|
},
|
|
68
68
|
"author": "Etienne Rossignon",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"internet of things"
|
|
81
81
|
],
|
|
82
82
|
"homepage": "http://node-opcua.github.io/",
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "31f407d4ad2712b7bf2882d4dc70085f598b0fcd",
|
|
84
84
|
"files": [
|
|
85
85
|
"dist",
|
|
86
86
|
"distHelpers",
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type UInt64
|
|
13
13
|
} from "node-opcua-basic-types";
|
|
14
14
|
import { make_debugLog, make_warningLog } from "node-opcua-debug";
|
|
15
|
-
import { coerceNodeId, type INodeId, type NodeId, NodeIdType } from "node-opcua-nodeid";
|
|
15
|
+
import { coerceNodeId, ExpandedNodeId, type INodeId, type NodeId, NodeIdType } from "node-opcua-nodeid";
|
|
16
16
|
import { EnumDefinition, StructureDefinition } from "node-opcua-types";
|
|
17
17
|
import { lowerFirstLetter } from "node-opcua-utils";
|
|
18
18
|
import { DataType, Variant, type VariantOptions } from "node-opcua-variant";
|
|
@@ -31,6 +31,30 @@ import { makeVariantReader } from "./parsers/variant_parser";
|
|
|
31
31
|
const warningLog = make_warningLog(__filename);
|
|
32
32
|
const debugLog = make_debugLog(__filename);
|
|
33
33
|
|
|
34
|
+
// textual form of an ExpandedNodeId, as found in <ExpandedNodeId><Identifier>...</Identifier></ExpandedNodeId>:
|
|
35
|
+
// an optional server index, an optional namespace uri, then a plain nodeId. see OPC UA part 6.
|
|
36
|
+
const regexServerIndex = /^svr=([0-9]+);(.*)$/;
|
|
37
|
+
const regexNamespaceUri = /^nsu=(.*?);(.*)$/;
|
|
38
|
+
|
|
39
|
+
function parseExpandedNodeId(text: string): ExpandedNodeId {
|
|
40
|
+
let remaining = text.trim();
|
|
41
|
+
let serverIndex = 0;
|
|
42
|
+
let namespaceUri: string | null = null;
|
|
43
|
+
|
|
44
|
+
const serverIndexMatch = remaining.match(regexServerIndex);
|
|
45
|
+
if (serverIndexMatch) {
|
|
46
|
+
serverIndex = parseInt(serverIndexMatch[1], 10);
|
|
47
|
+
remaining = serverIndexMatch[2];
|
|
48
|
+
}
|
|
49
|
+
const namespaceUriMatch = remaining.match(regexNamespaceUri);
|
|
50
|
+
if (namespaceUriMatch) {
|
|
51
|
+
namespaceUri = namespaceUriMatch[1];
|
|
52
|
+
remaining = namespaceUriMatch[2];
|
|
53
|
+
}
|
|
54
|
+
const nodeId = coerceNodeId(remaining);
|
|
55
|
+
return new ExpandedNodeId(nodeId.identifierType, nodeId.value, nodeId.namespace, namespaceUri, serverIndex);
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
function clamp(value: number, minValue: number, maxValue: number) {
|
|
35
59
|
if (value < minValue) {
|
|
36
60
|
warningLog(`invalid value range : ${value} < ${minValue} but should be [${minValue} , ${maxValue}]`);
|
|
@@ -169,6 +193,13 @@ const partials = {
|
|
|
169
193
|
// to do check Local or GMT
|
|
170
194
|
this.value = coerceNodeId(this.text);
|
|
171
195
|
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// <ExpandedNodeId><Identifier>svr=1;nsu=http://acme.com/UA/;i=42</Identifier></ExpandedNodeId>
|
|
199
|
+
ExpandedNodeId: <Parser<ExpandedNodeId>>{
|
|
200
|
+
finish(this: Parser<ExpandedNodeId>) {
|
|
201
|
+
this.value = parseExpandedNodeId(this.text);
|
|
202
|
+
}
|
|
172
203
|
}
|
|
173
204
|
};
|
|
174
205
|
|
|
@@ -231,10 +262,16 @@ function _makeTypeReader(
|
|
|
231
262
|
let reader: ReaderStateParserLike = readerMap.get(dataTypeName)!;
|
|
232
263
|
|
|
233
264
|
if (reader) {
|
|
234
|
-
return { name, reader
|
|
265
|
+
return { name, reader };
|
|
235
266
|
}
|
|
236
267
|
|
|
237
268
|
reader = {
|
|
269
|
+
init(this: any) {
|
|
270
|
+
// the same reader instance is used for every element of that data type: the value must be
|
|
271
|
+
// reset here, or the element being read would inherit the fields of the previously read one
|
|
272
|
+
// ( or of the enclosing one, when the structure refers to itself ).
|
|
273
|
+
this.value = undefined;
|
|
274
|
+
},
|
|
238
275
|
finish(this: any) {
|
|
239
276
|
/** empty */
|
|
240
277
|
},
|
|
@@ -244,6 +281,12 @@ function _makeTypeReader(
|
|
|
244
281
|
};
|
|
245
282
|
|
|
246
283
|
if (definition instanceof StructureDefinition) {
|
|
284
|
+
// the reader must be registered *before* its fields are explored: a structure may refer to
|
|
285
|
+
// itself, directly or through one of its fields, and the recursion below would never end.
|
|
286
|
+
// `reader` and `reader.parser` keep their identity while being filled, so a nested reference
|
|
287
|
+
// that picks the reader up from the map ends up pointing at the completed reader.
|
|
288
|
+
readerMap.set(dataTypeName, reader);
|
|
289
|
+
|
|
247
290
|
for (const field of definition.fields || []) {
|
|
248
291
|
const typeReader = _makeTypeReader(field.dataType, definitionMap, readerMap, translateNodeId);
|
|
249
292
|
const fieldParser = typeReader.reader;
|
|
@@ -301,7 +344,6 @@ function _makeTypeReader(
|
|
|
301
344
|
throw new Error("Unsupported ValueRank !");
|
|
302
345
|
}
|
|
303
346
|
}
|
|
304
|
-
readerMap.set(dataTypeName, reader);
|
|
305
347
|
return { name, reader };
|
|
306
348
|
} else if (definition instanceof EnumDefinition) {
|
|
307
349
|
const turnToInt = (value: any) => {
|
|
@@ -2,7 +2,8 @@ import type {
|
|
|
2
2
|
AddVariableOptionsWithoutValue,
|
|
3
3
|
AddYArrayItemOptions,
|
|
4
4
|
BindVariableOptions,
|
|
5
|
-
UADataType
|
|
5
|
+
UADataType,
|
|
6
|
+
UAVariableType
|
|
6
7
|
} from "node-opcua-address-space-base";
|
|
7
8
|
import type { NodeIdLike } from "node-opcua-nodeid";
|
|
8
9
|
import type { UAAnalogItem, UADataItem } from "node-opcua-nodeset-ua";
|
|
@@ -45,6 +46,13 @@ export interface AddAnalogDataItemOptions extends AddDataItemOptions {
|
|
|
45
46
|
minimumSamplingInterval?: number;
|
|
46
47
|
dataType?: string | NodeIdLike | UADataType;
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* an optional VariableType, which must be a subtype of AnalogItemType, to use instead of the
|
|
51
|
+
* plain AnalogItemType. Its own mandatory members (if any) will be instantiated as well.
|
|
52
|
+
* @default AnalogItemType
|
|
53
|
+
*/
|
|
54
|
+
typeDefinition?: string | NodeIdLike | UAVariableType;
|
|
55
|
+
|
|
48
56
|
/**
|
|
49
57
|
* the acceptValueOutOfRange property indicates whether the write operation will accept or reject a value which
|
|
50
58
|
* is out of range of the instrumentRange.
|
package/src/namespace_impl.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
type QualifiedNameLike
|
|
46
46
|
} from "node-opcua-data-model";
|
|
47
47
|
import { dumpIf, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
48
|
-
import { coerceNodeId, NodeId, type NodeIdLike, NodeIdType, resolveNodeId } from "node-opcua-nodeid";
|
|
48
|
+
import { coerceNodeId, NodeId, type NodeIdLike, NodeIdType, resolveNodeId, sameNodeId } from "node-opcua-nodeid";
|
|
49
49
|
import type { UAAnalogItem, UADataItem, UAInitialState, UAState } from "node-opcua-nodeset-ua";
|
|
50
50
|
import { StatusCodes } from "node-opcua-status-code";
|
|
51
51
|
import {
|
|
@@ -903,9 +903,52 @@ export class NamespaceImpl implements NamespacePrivate {
|
|
|
903
903
|
throw new Error("expecting AnalogItemType to be defined , check nodeset xml file");
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
906
|
+
// options.typeDefinition may designate a subtype of AnalogItemType (e.g. a companion
|
|
907
|
+
// specification's own AnalogItemType-derived VariableType). In that case we must honor it
|
|
908
|
+
// so that the subtype's identity - and its own mandatory members - are preserved, instead of
|
|
909
|
+
// silently downgrading the variable to a plain AnalogItemType.
|
|
910
|
+
let requestedTypeDefinition = analogItemType;
|
|
911
|
+
if (options.typeDefinition) {
|
|
912
|
+
const t =
|
|
913
|
+
options.typeDefinition instanceof BaseNodeImpl
|
|
914
|
+
? (options.typeDefinition as unknown as UAVariableType)
|
|
915
|
+
: addressSpace.findVariableType(options.typeDefinition as NodeIdLike);
|
|
916
|
+
if (!t) {
|
|
917
|
+
throw new Error(`cannot find requested typeDefinition ${options.typeDefinition.toString()}`);
|
|
918
|
+
}
|
|
919
|
+
if (!t.isSubtypeOf(analogItemType)) {
|
|
920
|
+
throw new Error(`typeDefinition ${t.browseName.toString()} shall be a subtype of AnalogItemType`);
|
|
921
|
+
}
|
|
922
|
+
requestedTypeDefinition = t;
|
|
923
|
+
}
|
|
924
|
+
const isStrictSubtype = !sameNodeId(requestedTypeDefinition.nodeId, analogItemType.nodeId);
|
|
925
|
+
|
|
926
|
+
let variable: UAVariableImpl;
|
|
927
|
+
if (isStrictSubtype) {
|
|
928
|
+
// instantiate the requested subtype so that its own mandatory members
|
|
929
|
+
// (in addition to the AnalogItemType ones) get materialized.
|
|
930
|
+
const optionals: string[] = [];
|
|
931
|
+
if (Object.hasOwn(options, "instrumentRange")) {
|
|
932
|
+
optionals.push("InstrumentRange");
|
|
933
|
+
}
|
|
934
|
+
if (Object.hasOwn(options, "engineeringUnits")) {
|
|
935
|
+
optionals.push("EngineeringUnits");
|
|
936
|
+
}
|
|
937
|
+
variable = requestedTypeDefinition.instantiate({
|
|
938
|
+
browseName: options.browseName,
|
|
939
|
+
componentOf: options.componentOf,
|
|
940
|
+
dataType: addressSpace._coerce_DataType(dataType as NodeIdLike),
|
|
941
|
+
modellingRule: options.modellingRule,
|
|
942
|
+
nodeId: options.nodeId,
|
|
943
|
+
notifierOf: options.notifierOf,
|
|
944
|
+
eventSourceOf: options.eventSourceOf,
|
|
945
|
+
organizedBy: options.organizedBy,
|
|
946
|
+
optionals
|
|
947
|
+
}) as unknown as UAVariableImpl;
|
|
948
|
+
} else {
|
|
949
|
+
const clone_options = { ...options, dataType, typeDefinition: analogItemType.nodeId } as AddVariableOptions;
|
|
950
|
+
variable = this.addVariable(clone_options) as unknown as UAVariableImpl;
|
|
951
|
+
}
|
|
909
952
|
|
|
910
953
|
add_dataItem_stuff(variable, options);
|
|
911
954
|
|
|
@@ -920,38 +963,59 @@ export class NamespaceImpl implements NamespacePrivate {
|
|
|
920
963
|
// prepared to handle this.
|
|
921
964
|
// Example: EURange ::= {-200.0,1400.0}
|
|
922
965
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
dataType: DataType.ExtensionObject,
|
|
932
|
-
value: new Range(options.engineeringUnitsRange)
|
|
933
|
-
})
|
|
934
|
-
}) as unknown as UAVariableImpl;
|
|
935
|
-
|
|
936
|
-
assert(euRange.readValue().value.value instanceof Range);
|
|
937
|
-
|
|
938
|
-
const handler = variable.handle_semantic_changed.bind(variable);
|
|
939
|
-
euRange.on("value_changed", handler);
|
|
940
|
-
|
|
941
|
-
if (Object.hasOwn(options, "instrumentRange")) {
|
|
942
|
-
const instrumentRange = this.addVariable({
|
|
943
|
-
accessLevel: "CurrentRead | CurrentWrite",
|
|
944
|
-
browseName: { name: "InstrumentRange", namespaceIndex: 0 },
|
|
966
|
+
// when `variable` was instantiated from a strict AnalogItemType subtype (isStrictSubtype),
|
|
967
|
+
// EURange/InstrumentRange/EngineeringUnits may already exist as cloned mandatory/optional
|
|
968
|
+
// members of that subtype - reuse them instead of creating duplicate property nodes.
|
|
969
|
+
const euRangeExisting = variable.getPropertyByName("EURange") as UAVariableImpl | null;
|
|
970
|
+
const euRange =
|
|
971
|
+
euRangeExisting ||
|
|
972
|
+
(this.addVariable({
|
|
973
|
+
browseName: { name: "EURange", namespaceIndex: 0 },
|
|
945
974
|
dataType: "Range",
|
|
946
975
|
minimumSamplingInterval: 0,
|
|
947
|
-
modellingRule: options.modellingRule
|
|
976
|
+
modellingRule: options.modellingRule,
|
|
948
977
|
propertyOf: variable,
|
|
949
978
|
typeDefinition: "PropertyType",
|
|
950
979
|
value: new Variant({
|
|
951
980
|
dataType: DataType.ExtensionObject,
|
|
952
|
-
value: new Range(options.
|
|
981
|
+
value: new Range(options.engineeringUnitsRange)
|
|
953
982
|
})
|
|
954
|
-
});
|
|
983
|
+
}) as unknown as UAVariableImpl);
|
|
984
|
+
if (euRangeExisting) {
|
|
985
|
+
euRange.setValueFromSource(
|
|
986
|
+
new Variant({ dataType: DataType.ExtensionObject, value: new Range(options.engineeringUnitsRange) }),
|
|
987
|
+
StatusCodes.Good
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
assert(euRange.readValue().value.value instanceof Range);
|
|
992
|
+
|
|
993
|
+
const handler = variable.handle_semantic_changed.bind(variable);
|
|
994
|
+
euRange.on("value_changed", handler);
|
|
995
|
+
|
|
996
|
+
if (Object.hasOwn(options, "instrumentRange")) {
|
|
997
|
+
const instrumentRangeExisting = variable.getPropertyByName("InstrumentRange") as UAVariableImpl | null;
|
|
998
|
+
const instrumentRange =
|
|
999
|
+
instrumentRangeExisting ||
|
|
1000
|
+
this.addVariable({
|
|
1001
|
+
accessLevel: "CurrentRead | CurrentWrite",
|
|
1002
|
+
browseName: { name: "InstrumentRange", namespaceIndex: 0 },
|
|
1003
|
+
dataType: "Range",
|
|
1004
|
+
minimumSamplingInterval: 0,
|
|
1005
|
+
modellingRule: options.modellingRule ? "Mandatory" : undefined,
|
|
1006
|
+
propertyOf: variable,
|
|
1007
|
+
typeDefinition: "PropertyType",
|
|
1008
|
+
value: new Variant({
|
|
1009
|
+
dataType: DataType.ExtensionObject,
|
|
1010
|
+
value: new Range(options.instrumentRange)
|
|
1011
|
+
})
|
|
1012
|
+
});
|
|
1013
|
+
if (instrumentRangeExisting) {
|
|
1014
|
+
instrumentRange.setValueFromSource(
|
|
1015
|
+
new Variant({ dataType: DataType.ExtensionObject, value: new Range(options.instrumentRange) }),
|
|
1016
|
+
StatusCodes.Good
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
955
1019
|
|
|
956
1020
|
instrumentRange.on("value_changed", handler);
|
|
957
1021
|
}
|
|
@@ -964,19 +1028,28 @@ export class NamespaceImpl implements NamespacePrivate {
|
|
|
964
1028
|
// EngineeringUnits specifies the units for the DataItem‟s value (e.g., degree, hertz, seconds). The
|
|
965
1029
|
// EUInformation type is specified in 5.6.3.
|
|
966
1030
|
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1031
|
+
const euExisting = variable.getPropertyByName("EngineeringUnits") as UAVariableImpl | null;
|
|
1032
|
+
const eu =
|
|
1033
|
+
euExisting ||
|
|
1034
|
+
this.addVariable({
|
|
1035
|
+
accessLevel: "CurrentRead",
|
|
1036
|
+
browseName: { name: "EngineeringUnits", namespaceIndex: 0 },
|
|
1037
|
+
dataType: "EUInformation",
|
|
1038
|
+
minimumSamplingInterval: 0,
|
|
1039
|
+
modellingRule: options.modellingRule ? "Mandatory" : undefined,
|
|
1040
|
+
propertyOf: variable,
|
|
1041
|
+
typeDefinition: "PropertyType",
|
|
1042
|
+
value: new Variant({
|
|
1043
|
+
dataType: DataType.ExtensionObject,
|
|
1044
|
+
value: engineeringUnits
|
|
1045
|
+
})
|
|
1046
|
+
});
|
|
1047
|
+
if (euExisting) {
|
|
1048
|
+
eu.setValueFromSource(
|
|
1049
|
+
new Variant({ dataType: DataType.ExtensionObject, value: engineeringUnits }),
|
|
1050
|
+
StatusCodes.Good
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
980
1053
|
|
|
981
1054
|
eu.on("value_changed", handler);
|
|
982
1055
|
}
|
package/src/nodeid_manager.ts
CHANGED
|
@@ -72,7 +72,7 @@ function _findParentNodeId(addressSpace: AddressSpacePartial, options: Construct
|
|
|
72
72
|
|
|
73
73
|
function prepareName(browseName?: QualifiedName | QualifiedNameOptions): string {
|
|
74
74
|
const m = browseName?.name?.toString().replace(/[ ]/g, "").replace(/(<|>)/g, "");
|
|
75
|
-
return m ||"";
|
|
75
|
+
return m || "";
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export interface AddressSpacePartial {
|
|
@@ -170,11 +170,12 @@ export class NodeIdManager {
|
|
|
170
170
|
fullParentName = buildUpName2(parentNodeId, suffix);
|
|
171
171
|
}
|
|
172
172
|
const fullName = compose(fullParentName, prepareName(options.browseName));
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
const cached = this._cacheSymbolicName[fullName];
|
|
174
|
+
if (cached && this._isCacheHitReusable(cached[0], options, parentInfo)) {
|
|
175
|
+
return makeNodeId(cached[0], this.namespaceIndex);
|
|
175
176
|
}
|
|
176
177
|
const nodeId = this._constructNodeId(options);
|
|
177
|
-
if (nodeId.identifierType === NodeIdType.NUMERIC) {
|
|
178
|
+
if (nodeId.identifierType === NodeIdType.NUMERIC && !cached) {
|
|
178
179
|
this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass || NodeClass.Unspecified];
|
|
179
180
|
this._cacheSymbolicNameRev.add(nodeId.value as number);
|
|
180
181
|
}
|
|
@@ -201,6 +202,46 @@ export class NodeIdManager {
|
|
|
201
202
|
return nodeId;
|
|
202
203
|
}
|
|
203
204
|
|
|
205
|
+
/**
|
|
206
|
+
* A symbol-cache hit is only safe to reuse when it actually refers to
|
|
207
|
+
* the node we are about to construct. Because the cache key is built
|
|
208
|
+
* from a (possibly truncated, e.g. during a clone where parentNodeId
|
|
209
|
+
* isn't linked yet) parent-name chain, two unrelated nodes can end up
|
|
210
|
+
* sharing the same key. When that happens, reusing the cached nodeId
|
|
211
|
+
* would hand a brand-new node the identity of a completely different,
|
|
212
|
+
* already-registered one.
|
|
213
|
+
*
|
|
214
|
+
* We only trust the cache hit when either:
|
|
215
|
+
* - nothing occupies that nodeId yet (first-time reservation), or
|
|
216
|
+
* - the occupant is genuinely the same node (same browseName AND same
|
|
217
|
+
* parent) - in which case we deliberately return the same id so
|
|
218
|
+
* that the caller's duplicate-registration error still fires.
|
|
219
|
+
*/
|
|
220
|
+
private _isCacheHitReusable(value: number, options: ConstructNodeIdOptions, parentInfo: [NodeId, Suffix] | null): boolean {
|
|
221
|
+
const candidate = makeNodeId(value, this.namespaceIndex);
|
|
222
|
+
const occupant = this.addressSpace.findNode(candidate);
|
|
223
|
+
if (!occupant) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
// options.browseName is expected to already carry its real, resolved namespaceIndex by the
|
|
227
|
+
// time it reaches here: internalCreateNode() normalizes plain-string browseNames to
|
|
228
|
+
// `namespaceIndex: this.index` before construction, so `0` here means "explicitly namespace 0"
|
|
229
|
+
// (e.g. the standard "0:EURange" / "0:EngineeringUnits" properties), not "unspecified".
|
|
230
|
+
// Comparing literally keeps genuinely different namespaces (own-namespace vs explicit ns 0)
|
|
231
|
+
// from being treated as the same node.
|
|
232
|
+
const sameBrowseName =
|
|
233
|
+
!!occupant.browseName &&
|
|
234
|
+
occupant.browseName.name === (options.browseName?.name ?? null) &&
|
|
235
|
+
(occupant.browseName.namespaceIndex ?? 0) === (options.browseName?.namespaceIndex ?? 0);
|
|
236
|
+
|
|
237
|
+
const expectedParentNodeId = parentInfo ? parentInfo[0] : null;
|
|
238
|
+
const sameParent = expectedParentNodeId
|
|
239
|
+
? !!occupant.parentNodeId && sameNodeId(occupant.parentNodeId, expectedParentNodeId)
|
|
240
|
+
: !occupant.parentNodeId;
|
|
241
|
+
|
|
242
|
+
return sameBrowseName && sameParent;
|
|
243
|
+
}
|
|
244
|
+
|
|
204
245
|
private _constructNodeId(options: ConstructNodeIdOptions): NodeId {
|
|
205
246
|
|
|
206
247
|
const resolveNodeIdEx = (nodeId: BaseNode | NodeIdLike) =>
|
|
@@ -71,13 +71,15 @@ export function _recomputeRequiredModelsFromTypes(namespace: INamespace, cache?:
|
|
|
71
71
|
|
|
72
72
|
const namespaceIndex = dataType.namespace;
|
|
73
73
|
consider(namespaceIndex);
|
|
74
|
+
// mark the dataType as visited *before* exploring its fields: a structure may refer to
|
|
75
|
+
// itself (directly or through a cycle), and marking it afterwards would recurse forever
|
|
76
|
+
_visitedDataType.add(dataType.toString());
|
|
74
77
|
if (dataTypeNode.isStructure()) {
|
|
75
78
|
const definition = dataTypeNode.getStructureDefinition();
|
|
76
79
|
for (const field of definition.fields || []) {
|
|
77
80
|
exploreDataTypeField(field);
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
|
-
_visitedDataType.add(dataType.toString());
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
function exploreExtensionObject(e: ExtensionObject) {
|
package/src/ua_variable_impl.ts
CHANGED
|
@@ -758,26 +758,23 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T>
|
|
|
758
758
|
dataValue.value = variant1;
|
|
759
759
|
|
|
760
760
|
if (dataValue.value.dataType === DataType.ExtensionObject) {
|
|
761
|
+
// early, detailed diagnostics on a malformed extension object (the actual throw
|
|
762
|
+
// happens below in _internal_set_dataValue).
|
|
761
763
|
const valueIsCorrect = this.checkExtensionObjectIsCorrect(dataValue.value.value);
|
|
762
764
|
if (!valueIsCorrect) {
|
|
763
765
|
errorLog("setValueFromSource Invalid value !");
|
|
764
766
|
errorLog(this.toString());
|
|
765
767
|
errorLog(dataValue.toString());
|
|
766
|
-
this.checkExtensionObjectIsCorrect(dataValue.value.value);
|
|
767
768
|
}
|
|
768
|
-
// ----------------------------------
|
|
769
|
-
if (this.$extensionObject || this.$$extensionObjectArray) {
|
|
770
|
-
// we have an extension object already bound to this node
|
|
771
|
-
// the client is asking us to replace the object entirely by a new one
|
|
772
|
-
// const ext = dataValue.value.value;
|
|
773
|
-
this._internal_set_dataValue(dataValue);
|
|
774
|
-
return;
|
|
775
|
-
} else {
|
|
776
|
-
this.$dataValue = dataValue;
|
|
777
|
-
}
|
|
778
|
-
} else {
|
|
779
|
-
this._internal_set_dataValue(dataValue);
|
|
780
769
|
}
|
|
770
|
+
// Route every value - scalar or ExtensionObject - through _internal_set_dataValue.
|
|
771
|
+
// This guarantees the change is consistently propagated to observers (historian,
|
|
772
|
+
// monitored items) and, when the node is a bound extension object (or a bound
|
|
773
|
+
// sub-structure child), cascaded through the proxy field setters by the overridden
|
|
774
|
+
// _inner_replace_dataValue. Previously ExtensionObject values were special-cased with a
|
|
775
|
+
// silent "this.$dataValue = dataValue" assignment that skipped notification for plain
|
|
776
|
+
// (unbound) extension object variables and threw on bound sub-structure children.
|
|
777
|
+
this._internal_set_dataValue(dataValue);
|
|
781
778
|
} catch (err) {
|
|
782
779
|
errorLog("UAVariable#setValueFromString Error : ", this.browseName.toString(), this.nodeId.toString());
|
|
783
780
|
errorLog((err as Error).message);
|