node-opcua-address-space 2.175.0 → 2.175.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/dist/source/loader/make_xml_extension_object_parser.js +41 -4
- 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 +42 -5
- package/dist/src/nodeid_manager.js.map +1 -1
- package/dist/src/nodeset_tools/dump_to_bsd.js +5 -5
- package/dist/src/nodeset_tools/dump_to_bsd.js.map +1 -1
- package/dist/src/nodeset_tools/nodeset_to_xml.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/package.json +38 -38
- package/source/loader/make_xml_extension_object_parser.ts +47 -5
- package/source/namespace_data_access.ts +9 -1
- package/src/namespace_impl.ts +115 -42
- package/src/nodeid_manager.ts +49 -8
- package/src/nodeset_tools/dump_to_bsd.ts +5 -5
- package/src/nodeset_tools/nodeset_to_xml.ts +1 -1
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.2",
|
|
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,49 +21,49 @@
|
|
|
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.
|
|
25
|
-
"node-opcua-assert": "2.
|
|
26
|
-
"node-opcua-basic-types": "2.
|
|
27
|
-
"node-opcua-binary-stream": "2.
|
|
28
|
-
"node-opcua-client-dynamic-extension-object": "2.
|
|
29
|
-
"node-opcua-constants": "2.
|
|
24
|
+
"node-opcua-address-space-base": "2.175.2",
|
|
25
|
+
"node-opcua-assert": "2.175.1",
|
|
26
|
+
"node-opcua-basic-types": "2.175.1",
|
|
27
|
+
"node-opcua-binary-stream": "2.175.1",
|
|
28
|
+
"node-opcua-client-dynamic-extension-object": "2.175.2",
|
|
29
|
+
"node-opcua-constants": "2.175.1",
|
|
30
30
|
"node-opcua-crypto": "5.3.6",
|
|
31
|
-
"node-opcua-data-access": "2.
|
|
32
|
-
"node-opcua-data-model": "2.
|
|
33
|
-
"node-opcua-data-value": "2.
|
|
34
|
-
"node-opcua-date-time": "2.
|
|
35
|
-
"node-opcua-debug": "2.
|
|
36
|
-
"node-opcua-enum": "2.
|
|
37
|
-
"node-opcua-extension-object": "2.
|
|
38
|
-
"node-opcua-factory": "2.
|
|
39
|
-
"node-opcua-nodeid": "2.
|
|
40
|
-
"node-opcua-nodeset-ua": "2.
|
|
41
|
-
"node-opcua-numeric-range": "2.
|
|
42
|
-
"node-opcua-object-registry": "2.
|
|
43
|
-
"node-opcua-pseudo-session": "2.
|
|
44
|
-
"node-opcua-service-browse": "2.
|
|
45
|
-
"node-opcua-service-call": "2.
|
|
46
|
-
"node-opcua-service-history": "2.
|
|
47
|
-
"node-opcua-service-translate-browse-path": "2.
|
|
48
|
-
"node-opcua-service-write": "2.
|
|
49
|
-
"node-opcua-status-code": "2.
|
|
50
|
-
"node-opcua-types": "2.
|
|
51
|
-
"node-opcua-utils": "2.
|
|
52
|
-
"node-opcua-variant": "2.
|
|
53
|
-
"node-opcua-xml2json": "2.
|
|
31
|
+
"node-opcua-data-access": "2.175.2",
|
|
32
|
+
"node-opcua-data-model": "2.175.1",
|
|
33
|
+
"node-opcua-data-value": "2.175.1",
|
|
34
|
+
"node-opcua-date-time": "2.175.1",
|
|
35
|
+
"node-opcua-debug": "2.175.1",
|
|
36
|
+
"node-opcua-enum": "2.175.1",
|
|
37
|
+
"node-opcua-extension-object": "2.175.1",
|
|
38
|
+
"node-opcua-factory": "2.175.1",
|
|
39
|
+
"node-opcua-nodeid": "2.175.1",
|
|
40
|
+
"node-opcua-nodeset-ua": "2.175.2",
|
|
41
|
+
"node-opcua-numeric-range": "2.175.1",
|
|
42
|
+
"node-opcua-object-registry": "2.175.1",
|
|
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
|
+
"node-opcua-status-code": "2.175.1",
|
|
50
|
+
"node-opcua-types": "2.175.2",
|
|
51
|
+
"node-opcua-utils": "2.175.1",
|
|
52
|
+
"node-opcua-variant": "2.175.1",
|
|
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"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"humanize": "0.0.9",
|
|
60
|
-
"node-opcua-benchmarker": "2.
|
|
61
|
-
"node-opcua-leak-detector": "2.
|
|
62
|
-
"node-opcua-nodesets": "2.
|
|
63
|
-
"node-opcua-packet-analyzer": "2.
|
|
64
|
-
"node-opcua-schemas": "2.
|
|
65
|
-
"node-opcua-service-filter": "2.
|
|
66
|
-
"node-opcua-test-fixtures": "2.
|
|
60
|
+
"node-opcua-benchmarker": "2.175.1",
|
|
61
|
+
"node-opcua-leak-detector": "2.175.1",
|
|
62
|
+
"node-opcua-nodesets": "2.175.1",
|
|
63
|
+
"node-opcua-packet-analyzer": "2.175.1",
|
|
64
|
+
"node-opcua-schemas": "2.175.2",
|
|
65
|
+
"node-opcua-service-filter": "2.175.2",
|
|
66
|
+
"node-opcua-test-fixtures": "2.175.1"
|
|
67
67
|
},
|
|
68
68
|
"author": "Etienne Rossignon",
|
|
69
69
|
"license": "MIT",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"internet of things"
|
|
81
81
|
],
|
|
82
82
|
"homepage": "http://node-opcua.github.io/",
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "4e2a73e5b96bdf3bd3a88709c813289038caf19b",
|
|
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;
|
|
@@ -260,11 +303,11 @@ function _makeTypeReader(
|
|
|
260
303
|
throw new Error(`??? ${field.dataType} ${field.name}`);
|
|
261
304
|
}
|
|
262
305
|
|
|
263
|
-
reader.parser![field.name
|
|
306
|
+
reader.parser![field.name || ""] = {
|
|
264
307
|
parser: fieldParser.parser,
|
|
265
308
|
// endElement: fieldReader.endElement,
|
|
266
309
|
finish(this: any) {
|
|
267
|
-
const elName = lowerFirstLetter(field.name
|
|
310
|
+
const elName = lowerFirstLetter(field.name || "");
|
|
268
311
|
if (fieldParser.finish) {
|
|
269
312
|
fieldParser.finish.call(this);
|
|
270
313
|
} else {
|
|
@@ -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 {
|
|
@@ -169,13 +169,14 @@ export class NodeIdManager {
|
|
|
169
169
|
const [parentNodeId, suffix] = parentInfo;
|
|
170
170
|
fullParentName = buildUpName2(parentNodeId, suffix);
|
|
171
171
|
}
|
|
172
|
-
const fullName = compose(fullParentName, prepareName(options.browseName
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
const fullName = compose(fullParentName, prepareName(options.browseName));
|
|
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
|
-
this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass
|
|
178
|
+
if (nodeId.identifierType === NodeIdType.NUMERIC && !cached) {
|
|
179
|
+
this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass || NodeClass.Unspecified];
|
|
179
180
|
this._cacheSymbolicNameRev.add(nodeId.value as number);
|
|
180
181
|
}
|
|
181
182
|
return nodeId;
|
|
@@ -192,15 +193,55 @@ export class NodeIdManager {
|
|
|
192
193
|
const [parentNodeId, suffix] = parentInfo;
|
|
193
194
|
fullParentName = buildUpName2(parentNodeId, suffix);
|
|
194
195
|
}
|
|
195
|
-
const fullName = compose(fullParentName, prepareName(options.browseName
|
|
196
|
+
const fullName = compose(fullParentName, prepareName(options.browseName));
|
|
196
197
|
if (!this._cacheSymbolicName[fullName]) {
|
|
197
|
-
this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass
|
|
198
|
+
this._cacheSymbolicName[fullName] = [nodeId.value as number, options.nodeClass || NodeClass.Unspecified];
|
|
198
199
|
this._cacheSymbolicNameRev.add(nodeId.value as number);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
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) =>
|
|
@@ -16,7 +16,7 @@ function dumpEnumeratedType(xw: XmlWriter, e: EnumDefinition, name: string): voi
|
|
|
16
16
|
xw.writeAttribute("LengthInBits", "32");
|
|
17
17
|
for (const f of e.fields || []) {
|
|
18
18
|
xw.startElement("opc:EnumeratedValue");
|
|
19
|
-
xw.writeAttribute("Name", f.name
|
|
19
|
+
xw.writeAttribute("Name", f.name|| "");
|
|
20
20
|
assert(f.value[0] === 0, "unsupported 64 bit value !");
|
|
21
21
|
xw.writeAttribute("Value", f.value[1].toString());
|
|
22
22
|
xw.endElement();
|
|
@@ -119,15 +119,15 @@ function dumpDataTypeStructure(
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
xw.startElement("opc:Field");
|
|
122
|
-
xw.writeAttribute("Name", f.name
|
|
122
|
+
xw.writeAttribute("Name", f.name|| "");
|
|
123
123
|
|
|
124
124
|
const typeName = buildXmlName(addressSpace as AddressSpacePrivate, map, f.dataType);
|
|
125
125
|
xw.writeAttribute("TypeName", typeName);
|
|
126
126
|
if (isArray) {
|
|
127
|
-
xw.writeAttribute("LengthField", `NoOf${f.name
|
|
127
|
+
xw.writeAttribute("LengthField", `NoOf${f.name|| ""}`);
|
|
128
128
|
}
|
|
129
129
|
if (f.isOptional) {
|
|
130
|
-
xw.writeAttribute("SwitchField", `${f.name}Specified`);
|
|
130
|
+
xw.writeAttribute("SwitchField", `${f.name|| ""}Specified`);
|
|
131
131
|
}
|
|
132
132
|
xw.endElement();
|
|
133
133
|
}
|
|
@@ -137,7 +137,7 @@ function dumpDataTypeStructure(
|
|
|
137
137
|
function dumpDataTypeToBSD(xw: XmlWriter, dataType: UADataType, map: Map<number, string>) {
|
|
138
138
|
const addressSpace = dataType.addressSpace;
|
|
139
139
|
|
|
140
|
-
const name: string = dataType.browseName.name
|
|
140
|
+
const name: string = dataType.browseName.name || "";
|
|
141
141
|
|
|
142
142
|
const definition = dataType.getDefinition();
|
|
143
143
|
if (definition instanceof StructureDefinition) {
|
|
@@ -898,7 +898,7 @@ function _dumpStructureDefinition(
|
|
|
898
898
|
for (let index = nbFieldsInBase; index < fields.length; index++) {
|
|
899
899
|
const defItem = fields[index];
|
|
900
900
|
xw.startElement("Field");
|
|
901
|
-
xw.writeAttribute("Name", defItem.name
|
|
901
|
+
xw.writeAttribute("Name", defItem.name as string);
|
|
902
902
|
|
|
903
903
|
if (defItem.arrayDimensions) {
|
|
904
904
|
xw.writeAttribute("ArrayDimensions", defItem.arrayDimensions.map((x) => x.toString()).join(","));
|