node-opcua-address-space-for-conformance-testing 2.180.0 → 2.181.1

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.
Files changed (52) hide show
  1. package/dist/address_space_for_conformance_testing.d.ts +12 -2
  2. package/dist/address_space_for_conformance_testing.js +32 -27
  3. package/dist/address_space_for_conformance_testing.js.map +1 -1
  4. package/dist/conformance_testing/alarm_input_nodes.d.ts +2 -0
  5. package/dist/conformance_testing/alarm_input_nodes.js +102 -0
  6. package/dist/conformance_testing/alarm_input_nodes.js.map +1 -0
  7. package/dist/conformance_testing/ctt_folder.d.ts +2 -0
  8. package/dist/conformance_testing/ctt_folder.js +375 -0
  9. package/dist/conformance_testing/ctt_folder.js.map +1 -0
  10. package/dist/conformance_testing/ctt_tree.d.ts +35 -0
  11. package/dist/conformance_testing/ctt_tree.js +85 -0
  12. package/dist/conformance_testing/ctt_tree.js.map +1 -0
  13. package/dist/conformance_testing/custom_extension_object.d.ts +63 -0
  14. package/dist/conformance_testing/custom_extension_object.js +48 -0
  15. package/dist/conformance_testing/custom_extension_object.js.map +1 -0
  16. package/dist/conformance_testing/discrete_variables.js +6 -4
  17. package/dist/conformance_testing/discrete_variables.js.map +1 -1
  18. package/dist/conformance_testing/helpers.d.ts +2 -1
  19. package/dist/conformance_testing/helpers.js +23 -5
  20. package/dist/conformance_testing/helpers.js.map +1 -1
  21. package/dist/conformance_testing/mass_variables.d.ts +2 -1
  22. package/dist/conformance_testing/mass_variables.js +7 -6
  23. package/dist/conformance_testing/mass_variables.js.map +1 -1
  24. package/dist/conformance_testing/path_and_large_arrays.d.ts +2 -1
  25. package/dist/conformance_testing/path_and_large_arrays.js +6 -5
  26. package/dist/conformance_testing/path_and_large_arrays.js.map +1 -1
  27. package/dist/conformance_testing/simulation_variables.d.ts +2 -1
  28. package/dist/conformance_testing/simulation_variables.js +9 -9
  29. package/dist/conformance_testing/simulation_variables.js.map +1 -1
  30. package/dist/conformance_testing/static_variables.d.ts +2 -1
  31. package/dist/conformance_testing/static_variables.js +19 -12
  32. package/dist/conformance_testing/static_variables.js.map +1 -1
  33. package/dist/conformance_testing/type_defaults.d.ts +15 -1
  34. package/dist/conformance_testing/type_defaults.js +30 -1
  35. package/dist/conformance_testing/type_defaults.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/package.json +19 -14
  40. package/src/address_space_for_conformance_testing.ts +37 -18
  41. package/src/conformance_testing/alarm_input_nodes.ts +151 -0
  42. package/src/conformance_testing/ctt_folder.ts +440 -0
  43. package/src/conformance_testing/ctt_tree.ts +105 -0
  44. package/src/conformance_testing/custom_extension_object.ts +105 -0
  45. package/src/conformance_testing/discrete_variables.ts +6 -4
  46. package/src/conformance_testing/helpers.ts +26 -6
  47. package/src/conformance_testing/mass_variables.ts +5 -4
  48. package/src/conformance_testing/path_and_large_arrays.ts +5 -4
  49. package/src/conformance_testing/simulation_variables.ts +6 -6
  50. package/src/conformance_testing/static_variables.ts +31 -9
  51. package/src/conformance_testing/type_defaults.ts +31 -2
  52. package/src/index.ts +1 -1
@@ -0,0 +1,440 @@
1
+ /**
2
+ * The `CTT` folder: nodes named after the settings of the OPC Foundation
3
+ * Compliance Test Tool (UACTT) server project.
4
+ *
5
+ * The browse path below Objects/CTT mirrors the setting path below
6
+ * "Server Test/NodeIds", e.g. the setting
7
+ * Server Test/NodeIds/Static/HA Profile/Arrays/Int162D
8
+ * is served by
9
+ * Objects/CTT/Static/HA Profile/Arrays/Int162D (NodeId s=CTT/Static/HA Profile/Arrays/Int162D)
10
+ * so a CTT project generator can fill the setting by browsing, without any
11
+ * hand-maintained map. See ./ctt_tree.ts for how a setting outside that prefix
12
+ * is keyed. Only settings the rest of this address space does not already serve
13
+ * are built here - plus the two Bool settings, see addBooleanScalarAndArray().
14
+ *
15
+ * Not covered, and why:
16
+ * Decimal deprecated in OPC UA 1.05
17
+ * the HA Aggregates nodes need node-opcua-aggregates and a seeded
18
+ * history matching the CTT's StartOfBadData*
19
+ * NodeDoesNotSupportServerTimestamp needs the variable read path to omit it
20
+ * NodeManagement/RootNode the server has no AddNodes service
21
+ * Chattering Alarms needs an alarm that flips state on its own
22
+ * DiscrepancyAlarmType Input Nodes see ./alarm_input_nodes.ts
23
+ */
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import type { AddressSpace, Namespace, UAObject, UAVariable } from "node-opcua-address-space";
27
+ import { standardUnits } from "node-opcua-data-access";
28
+ import { AccessLevelFlag } from "node-opcua-data-model";
29
+ import { buildVariantArray, DataType, Variant, VariantArrayType } from "node-opcua-variant";
30
+
31
+ import { addAlarmInputNodes } from "./alarm_input_nodes.js";
32
+ import { CttFolder, readWrite, TYPE_ALIAS } from "./ctt_tree.js";
33
+
34
+ // ─── Static / All Profiles ──────────────────────────────────────────────
35
+
36
+ function addVariantVariables(ctt: CttFolder): void {
37
+ // "Variant" in CTT terms: a Variable whose DataType attribute is BaseDataType
38
+ const scalar = new Variant({ dataType: DataType.Double, value: 3.14 });
39
+ ctt.variable("Static/All Profiles/Scalar/Variant", "BaseDataType", -1, scalar, null);
40
+ // The array is the one case the CTT actually type-checks: Attribute Read 023.js
41
+ // asserts AssertUaValueOfType(BuiltInType.Variant, ...) against this node, so the
42
+ // *value* has to be Variant-encoded (BuiltInType 24), not merely stored on a
43
+ // BaseDataType-typed Variable. A Float64Array reads back as Double (11) and fails
44
+ // that assertion. Mixed element types are also the point of the type, and exercise
45
+ // the encoder the way the test intends.
46
+ ctt.variable(
47
+ "Static/All Profiles/Arrays/Variant",
48
+ "BaseDataType",
49
+ 1,
50
+ new Variant({
51
+ dataType: DataType.Variant,
52
+ arrayType: VariantArrayType.Array,
53
+ value: [
54
+ new Variant({ dataType: DataType.Double, value: 1 }),
55
+ new Variant({ dataType: DataType.UInt32, value: 2 }),
56
+ new Variant({ dataType: DataType.String, value: "three" }),
57
+ new Variant({ dataType: DataType.Boolean, value: true }),
58
+ new Variant({ dataType: DataType.Float, value: 5.5 })
59
+ ]
60
+ }),
61
+ [5]
62
+ );
63
+ ctt.variable(
64
+ "Static/All Profiles/Multi-Dimensional-Arrays/Variant",
65
+ "BaseDataType",
66
+ 2,
67
+ new Variant({
68
+ dataType: DataType.Double,
69
+ arrayType: VariantArrayType.Matrix,
70
+ dimensions: [2, 3],
71
+ value: new Float64Array([1, 2, 3, 4, 5, 6])
72
+ }),
73
+ [2, 3]
74
+ );
75
+ }
76
+
77
+ /**
78
+ * `Static/All Profiles/Scalar/Bool` and `.../Arrays/Bool`, which the Simulation
79
+ * folder would otherwise serve.
80
+ *
81
+ * A generator resolves a setting by overlay, then convention, then the CTT's own
82
+ * default, then by *browse name*, then by rule. The HA Profile nodes below are
83
+ * browse-named after their setting leaf, so `Static/HA Profile/Scalar/Bool` is
84
+ * literally named `Bool` - and the name step matches it against the All Profiles
85
+ * `Bool` settings, for which there is no convention node, while the Simulation
86
+ * node named `Boolean` matches neither. The name step runs before the rule step,
87
+ * so those two settings would silently resolve to a historizing node. Every other
88
+ * type name is safe: `Byte`, `Double` and the rest match both nodes and the rule
89
+ * scoring then prefers the non-historizing one.
90
+ *
91
+ * Building the two nodes here makes the convention step answer first, which is
92
+ * both deterministic and where the CTT-specific extras belong - hence the two
93
+ * Description properties, which View Minimum Continuation Point 01 012 needs
94
+ * (it browses the Scalar/Bool setting node with a NodeClassMask of Variable and
95
+ * wants at least two such references).
96
+ */
97
+ function addBooleanScalarAndArray(ctt: CttFolder): void {
98
+ const scalar = ctt.typedVariable("Static/All Profiles/Scalar/Bool", "Boolean", -1);
99
+ for (const name of ["Description1", "Description2"]) {
100
+ ctt.namespace.addVariable({
101
+ propertyOf: scalar,
102
+ browseName: name,
103
+ nodeId: ctt.nodeId(`Static/All Profiles/Scalar/Bool/${name}`),
104
+ dataType: "String",
105
+ value: new Variant({ dataType: DataType.String, value: `${name} of the static Boolean` })
106
+ });
107
+ }
108
+ ctt.typedVariable("Static/All Profiles/Arrays/Bool", "Boolean", 1);
109
+ }
110
+
111
+ function addImageVariable(ctt: CttFolder): void {
112
+ // the abstract Image data type (the concrete ImagePNG/GIF/JPG/BMP nodes live under Simulation)
113
+ let png: Buffer;
114
+ try {
115
+ png = fs.readFileSync(path.join(__dirname, "../../data", "tux.png"));
116
+ } catch (_err) {
117
+ png = Buffer.from("89504e470d0a1a0a", "hex");
118
+ }
119
+ ctt.variable("Static/All Profiles/Scalar/Image", "Image", -1, new Variant({ dataType: DataType.ByteString, value: png }), null);
120
+ }
121
+
122
+ function addStructureVariables(ctt: CttFolder): void {
123
+ const addressSpace = ctt.addressSpace;
124
+ const structures: { dataType: string; fields: Record<string, unknown> }[] = [
125
+ { dataType: "Range", fields: { low: -10, high: 10 } },
126
+ {
127
+ dataType: "EUInformation",
128
+ fields: {
129
+ namespaceUri: "http://www.opcfoundation.org/UA/units/un/cefact",
130
+ unitId: 4408652,
131
+ displayName: { text: "°C" },
132
+ description: { text: "degree Celsius" }
133
+ }
134
+ },
135
+ { dataType: "TimeZoneDataType", fields: { offset: 60, daylightSavingInOffset: false } },
136
+ {
137
+ dataType: "EnumValueType",
138
+ fields: { value: [1, 0], displayName: { text: "One" }, description: { text: "the value one" } }
139
+ },
140
+ {
141
+ dataType: "Argument",
142
+ fields: {
143
+ name: "arg",
144
+ dataType: "i=6",
145
+ valueRank: -1,
146
+ arrayDimensions: null,
147
+ description: { text: "an Int32 argument" }
148
+ }
149
+ }
150
+ ];
151
+ structures.forEach((s, i) => {
152
+ const dataTypeNode = addressSpace.findDataType(s.dataType);
153
+ if (!dataTypeNode) throw new Error(`DataType ${s.dataType} not found`);
154
+ const value = addressSpace.constructExtensionObject(dataTypeNode, s.fields);
155
+ ctt.variable(
156
+ `Static/All Profiles/Structures/Structure${String(i + 1).padStart(3, "0")}`,
157
+ s.dataType,
158
+ -1,
159
+ new Variant({ dataType: DataType.ExtensionObject, value }),
160
+ null
161
+ );
162
+ });
163
+ }
164
+
165
+ // ─── Static / DA Profile ────────────────────────────────────────────────
166
+
167
+ function addAnalogItemArrays(ctt: CttFolder): void {
168
+ const folder = ctt.folder("Static/DA Profile/AnalogItemType Arrays");
169
+ for (const dataTypeName of ["Double", "Float", "Int16", "Int32", "UInt16", "UInt32"]) {
170
+ const dataType = DataType[dataTypeName as keyof typeof DataType];
171
+ ctt.namespace.addAnalogDataItem({
172
+ organizedBy: folder,
173
+ browseName: dataTypeName,
174
+ nodeId: ctt.nodeId(`Static/DA Profile/AnalogItemType Arrays/${dataTypeName}`),
175
+ dataType: dataTypeName,
176
+ valueRank: 1,
177
+ arrayDimensions: [5],
178
+ arrayType: VariantArrayType.Array,
179
+ engineeringUnitsRange: { low: -100, high: 100 },
180
+ instrumentRange: { low: -1000, high: 1000 },
181
+ engineeringUnits: standardUnits.degree_celsius,
182
+ value: new Variant({ dataType, arrayType: VariantArrayType.Array, value: buildVariantArray(dataType, 5, 0) }),
183
+ accessLevel: readWrite,
184
+ userAccessLevel: readWrite
185
+ });
186
+ }
187
+ }
188
+
189
+ function axis(addressSpace: AddressSpace, title: string, low: number, high: number) {
190
+ const axisInformation = addressSpace.findDataType("AxisInformation");
191
+ if (!axisInformation) throw new Error("AxisInformation not found");
192
+ return addressSpace.constructExtensionObject(axisInformation, {
193
+ engineeringUnits: standardUnits.second,
194
+ euRange: { low, high },
195
+ title: { text: title },
196
+ axisScaleType: 0, // Linear
197
+ axisSteps: null
198
+ });
199
+ }
200
+
201
+ /**
202
+ * The five ArrayItemType subtypes: plain variables of the right VariableType,
203
+ * given the mandatory ArrayItemType properties by hand. instantiate() refuses
204
+ * a ValueRank that differs from the type's (ImageItemType and CubeItemType
205
+ * need 2 and 3) and namespace.addYArrayItem() ignores nodeId and organizedBy.
206
+ */
207
+ function addArrayItems(ctt: CttFolder): void {
208
+ const addressSpace = ctt.addressSpace;
209
+ const namespace = ctt.namespace;
210
+ const group = "Static/DA Profile/ArrayItemType";
211
+ const folder = ctt.folder(group);
212
+ const xAxis = axis(addressSpace, "x", 0, 10);
213
+ const yAxis = axis(addressSpace, "y", 0, 20);
214
+ const zAxis = axis(addressSpace, "z", 0, 30);
215
+ const rangeType = addressSpace.findDataType("Range");
216
+ const xvType = addressSpace.findDataType("XVType");
217
+ if (!rangeType || !xvType) throw new Error("Range or XVType not found");
218
+
219
+ const extensionObject = (value: unknown) => new Variant({ dataType: DataType.ExtensionObject, value });
220
+ const arrayItem = (
221
+ typeName: string,
222
+ options: { dataType: string; valueRank: number; arrayDimensions: number[]; value: Variant },
223
+ axes: Record<string, Variant>
224
+ ): UAVariable => {
225
+ const v = namespace.addVariable({
226
+ organizedBy: folder,
227
+ browseName: typeName,
228
+ nodeId: ctt.nodeId(`${group}/${typeName}`),
229
+ typeDefinition: typeName,
230
+ dataType: options.dataType,
231
+ valueRank: options.valueRank,
232
+ arrayDimensions: options.arrayDimensions,
233
+ accessLevel: readWrite,
234
+ userAccessLevel: readWrite,
235
+ value: options.value
236
+ });
237
+ const property = (browseName: string, dataType: string, value: Variant, valueRank = -1) =>
238
+ namespace.addVariable({ propertyOf: v, browseName, dataType, valueRank, value, modellingRule: "Mandatory" });
239
+ property("Title", "LocalizedText", new Variant({ dataType: DataType.LocalizedText, value: { text: typeName } }));
240
+ property("AxisScaleType", "AxisScaleEnumeration", new Variant({ dataType: DataType.Int32, value: 0 }));
241
+ property("EURange", "Range", extensionObject(addressSpace.constructExtensionObject(rangeType, { low: 0, high: 100 })));
242
+ property(
243
+ "InstrumentRange",
244
+ "Range",
245
+ extensionObject(addressSpace.constructExtensionObject(rangeType, { low: 0, high: 1000 }))
246
+ );
247
+ property("EngineeringUnits", "EUInformation", extensionObject(standardUnits.degree_celsius));
248
+ for (const [name, value] of Object.entries(axes)) {
249
+ property(name, "AxisInformation", value, value.arrayType === VariantArrayType.Array ? 1 : -1);
250
+ }
251
+ return v;
252
+ };
253
+
254
+ arrayItem(
255
+ "YArrayItemType",
256
+ {
257
+ dataType: "Double",
258
+ valueRank: 1,
259
+ arrayDimensions: [5],
260
+ value: new Variant({
261
+ dataType: DataType.Double,
262
+ arrayType: VariantArrayType.Array,
263
+ value: new Float64Array([1, 2, 3, 4, 5])
264
+ })
265
+ },
266
+ { XAxisDefinition: extensionObject(xAxis) }
267
+ );
268
+ arrayItem(
269
+ "XYArrayItemType",
270
+ {
271
+ dataType: "XVType",
272
+ valueRank: 1,
273
+ arrayDimensions: [3],
274
+ value: new Variant({
275
+ dataType: DataType.ExtensionObject,
276
+ arrayType: VariantArrayType.Array,
277
+ value: [0, 1, 2].map((i) => addressSpace.constructExtensionObject(xvType, { x: i, value: i * 2 }))
278
+ })
279
+ },
280
+ { XAxisDefinition: extensionObject(xAxis) }
281
+ );
282
+ arrayItem(
283
+ "ImageItemType",
284
+ {
285
+ dataType: "UInt16",
286
+ valueRank: 2,
287
+ arrayDimensions: [2, 3],
288
+ value: new Variant({
289
+ dataType: DataType.UInt16,
290
+ arrayType: VariantArrayType.Matrix,
291
+ dimensions: [2, 3],
292
+ value: new Uint16Array([1, 2, 3, 4, 5, 6])
293
+ })
294
+ },
295
+ { XAxisDefinition: extensionObject(xAxis), YAxisDefinition: extensionObject(yAxis) }
296
+ );
297
+ arrayItem(
298
+ "CubeItemType",
299
+ {
300
+ dataType: "Double",
301
+ valueRank: 3,
302
+ arrayDimensions: [2, 2, 2],
303
+ value: new Variant({
304
+ dataType: DataType.Double,
305
+ arrayType: VariantArrayType.Matrix,
306
+ dimensions: [2, 2, 2],
307
+ value: new Float64Array([1, 2, 3, 4, 5, 6, 7, 8])
308
+ })
309
+ },
310
+ {
311
+ XAxisDefinition: extensionObject(xAxis),
312
+ YAxisDefinition: extensionObject(yAxis),
313
+ ZAxisDefinition: extensionObject(zAxis)
314
+ }
315
+ );
316
+ arrayItem(
317
+ "NDimensionArrayItemType",
318
+ {
319
+ dataType: "Double",
320
+ valueRank: 2,
321
+ arrayDimensions: [2, 3],
322
+ value: new Variant({
323
+ dataType: DataType.Double,
324
+ arrayType: VariantArrayType.Matrix,
325
+ dimensions: [2, 3],
326
+ value: new Float64Array([1, 2, 3, 4, 5, 6])
327
+ })
328
+ },
329
+ {
330
+ AxisDefinition: new Variant({
331
+ dataType: DataType.ExtensionObject,
332
+ arrayType: VariantArrayType.Array,
333
+ value: [xAxis, yAxis]
334
+ })
335
+ }
336
+ );
337
+ }
338
+
339
+ // ─── Static / HA Profile ────────────────────────────────────────────────
340
+
341
+ const HA_TYPES = [
342
+ "Bool",
343
+ "Byte",
344
+ "ByteString",
345
+ "DateTime",
346
+ "Double",
347
+ "Float",
348
+ "Int16",
349
+ "Int32",
350
+ "Int64",
351
+ "SByte",
352
+ "String",
353
+ "UInt16",
354
+ "UInt32",
355
+ "UInt64",
356
+ "XmlElement"
357
+ ];
358
+
359
+ /** a variable with in-memory history and a first value, so reads are Good with timestamps */
360
+ function historize(ctt: CttFolder, variable: UAVariable): UAVariable {
361
+ ctt.addressSpace.installHistoricalDataNode(variable);
362
+ const current = variable.readValue().value;
363
+ variable.setValueFromSource(current);
364
+ return variable;
365
+ }
366
+
367
+ function addHistorizingVariables(ctt: CttFolder): void {
368
+ for (const leaf of HA_TYPES) {
369
+ const dataTypeName = TYPE_ALIAS[leaf] ?? leaf;
370
+ historize(ctt, ctt.typedVariable(`Static/HA Profile/Scalar/${leaf}`, dataTypeName, -1));
371
+ historize(ctt, ctt.typedVariable(`Static/HA Profile/Arrays/${leaf}`, dataTypeName, 1));
372
+ historize(ctt, ctt.typedVariable(`Static/HA Profile/Arrays/${leaf}2D`, dataTypeName, 2));
373
+ }
374
+
375
+ const range = ctt.addressSpace.findDataType("Range");
376
+ if (!range) throw new Error("Range not found");
377
+ historize(
378
+ ctt,
379
+ ctt.variable(
380
+ "Static/HA Profile/StructureNodeSupportingHistory",
381
+ "Range",
382
+ -1,
383
+ new Variant({
384
+ dataType: DataType.ExtensionObject,
385
+ value: ctt.addressSpace.constructExtensionObject(range, { low: 0, high: 1 })
386
+ }),
387
+ null
388
+ )
389
+ );
390
+
391
+ // AccessLevel_* : the AccessLevel attribute; UserAccessLevel_* : the UserAccessLevel attribute
392
+ const history = AccessLevelFlag.HistoryRead;
393
+ const flags: Record<string, number> = {
394
+ ReadOnly: AccessLevelFlag.CurrentRead | history,
395
+ WriteOnly: AccessLevelFlag.CurrentWrite | history,
396
+ None: history
397
+ };
398
+ for (const attribute of ["AccessLevel", "UserAccessLevel"]) {
399
+ for (const [kind, flag] of Object.entries(flags)) {
400
+ const relPath = `Static/HA Profile/AccessRights/${attribute}_${kind}`;
401
+ const i = relPath.lastIndexOf("/");
402
+ const full = AccessLevelFlag.CurrentRead | AccessLevelFlag.CurrentWrite | history;
403
+ const v = ctt.namespace.addVariable({
404
+ componentOf: ctt.folder(relPath.slice(0, i)),
405
+ browseName: relPath.slice(i + 1),
406
+ nodeId: ctt.nodeId(relPath),
407
+ description: `CTT setting ${relPath}`,
408
+ dataType: "Int32",
409
+ valueRank: -1,
410
+ accessLevel: full,
411
+ userAccessLevel: full,
412
+ value: new Variant({ dataType: DataType.Int32, value: 36 })
413
+ });
414
+ historize(ctt, v);
415
+ // installHistoricalDataNode() adds CurrentRead to both attributes: set the flags after it
416
+ v.accessLevel = attribute === "AccessLevel" ? flag : full;
417
+ v.userAccessLevel = attribute === "UserAccessLevel" ? flag : full;
418
+ }
419
+ }
420
+ }
421
+
422
+ // ─── entry point ────────────────────────────────────────────────────────
423
+
424
+ export function addCttFolder(namespace: Namespace, objectsFolder: UAObject): UAObject {
425
+ const root = namespace.addFolder(objectsFolder, {
426
+ browseName: "CTT",
427
+ nodeId: "s=CTT",
428
+ description: "Nodes named after the settings of the OPC Foundation Compliance Test Tool server project"
429
+ });
430
+ const ctt = new CttFolder(namespace, root);
431
+ addVariantVariables(ctt);
432
+ addBooleanScalarAndArray(ctt);
433
+ addImageVariable(ctt);
434
+ addStructureVariables(ctt);
435
+ addAnalogItemArrays(ctt);
436
+ addArrayItems(ctt);
437
+ addHistorizingVariables(ctt);
438
+ addAlarmInputNodes(ctt);
439
+ return root;
440
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The node tree below `Objects/CTT`, shared by the builders that populate it.
3
+ *
4
+ * A CTT project generator fills a NodeId setting by browsing: the browse path
5
+ * below `Objects/CTT` mirrors the setting path, with only the
6
+ * "Server Test/NodeIds/" prefix stripped. So
7
+ * Server Test/NodeIds/Static/HA Profile/Arrays/Int162D
8
+ * is served by
9
+ * Objects/CTT/Static/HA Profile/Arrays/Int162D
10
+ * while a setting outside that prefix keeps its whole path, e.g.
11
+ * Server Test/Alarms and Conditions/Supported Condition Types/LimitAlarmType Input Nodes
12
+ * is served by
13
+ * Objects/CTT/Server Test/Alarms and Conditions/Supported Condition Types/LimitAlarmType Input Nodes
14
+ */
15
+ import type { AddressSpace, Namespace, UAObject, UAVariable } from "node-opcua-address-space";
16
+ import { makeAccessLevelFlag } from "node-opcua-data-model";
17
+ import { buildVariantArray, DataType, Variant, VariantArrayType } from "node-opcua-variant";
18
+
19
+ import { typeAndDefaultValue } from "./type_defaults.js";
20
+
21
+ export const readWrite = makeAccessLevelFlag("CurrentRead | CurrentWrite");
22
+
23
+ /** setting leaf -> OPC UA DataType name */
24
+ export const TYPE_ALIAS: Record<string, string> = { Bool: "Boolean" };
25
+
26
+ function defaultValueOf(dataTypeName: string): unknown {
27
+ const e = typeAndDefaultValue.find((t) => t.type === dataTypeName);
28
+ if (!e) throw new Error(`no default value for ${dataTypeName}`);
29
+ return typeof e.defaultValue === "function" ? e.defaultValue() : e.defaultValue;
30
+ }
31
+
32
+ export function variantFor(dataTypeName: string, valueRank: number): { variant: Variant; arrayDimensions: number[] | null } {
33
+ const dataType = DataType[dataTypeName as keyof typeof DataType];
34
+ const defaultValue = defaultValueOf(dataTypeName);
35
+ if (valueRank === -1) {
36
+ return {
37
+ variant: new Variant({ dataType, arrayType: VariantArrayType.Scalar, value: defaultValue }),
38
+ arrayDimensions: null
39
+ };
40
+ }
41
+ const dimensions = valueRank === 1 ? [5] : [2, 3];
42
+ const length = dimensions.reduce((a, b) => a * b, 1);
43
+ return {
44
+ variant: new Variant({
45
+ dataType,
46
+ arrayType: valueRank === 1 ? VariantArrayType.Array : VariantArrayType.Matrix,
47
+ dimensions: valueRank === 1 ? null : dimensions,
48
+ value: buildVariantArray(dataType, length, defaultValue)
49
+ }),
50
+ arrayDimensions: dimensions
51
+ };
52
+ }
53
+
54
+ export class CttFolder {
55
+ private readonly folders = new Map<string, UAObject>();
56
+
57
+ constructor(
58
+ readonly namespace: Namespace,
59
+ root: UAObject
60
+ ) {
61
+ this.folders.set("", root);
62
+ }
63
+
64
+ get addressSpace(): AddressSpace {
65
+ return this.namespace.addressSpace as AddressSpace;
66
+ }
67
+
68
+ /** the folder for a setting group, created on demand, e.g. "Static/HA Profile/Arrays" */
69
+ folder(relPath: string): UAObject {
70
+ const existing = this.folders.get(relPath);
71
+ if (existing) return existing;
72
+ const i = relPath.lastIndexOf("/");
73
+ const parent = this.folder(i < 0 ? "" : relPath.slice(0, i));
74
+ const browseName = i < 0 ? relPath : relPath.slice(i + 1);
75
+ const f = this.namespace.addFolder(parent, { browseName, nodeId: `s=CTT/${relPath}` });
76
+ this.folders.set(relPath, f);
77
+ return f;
78
+ }
79
+
80
+ nodeId(relPath: string): string {
81
+ return `s=CTT/${relPath}`;
82
+ }
83
+
84
+ /** a plain read/write variable at the given setting path */
85
+ variable(relPath: string, dataType: string, valueRank: number, value: Variant, arrayDimensions: number[] | null): UAVariable {
86
+ const i = relPath.lastIndexOf("/");
87
+ return this.namespace.addVariable({
88
+ componentOf: this.folder(relPath.slice(0, i)),
89
+ browseName: relPath.slice(i + 1),
90
+ nodeId: this.nodeId(relPath),
91
+ description: `CTT setting ${relPath}`,
92
+ dataType,
93
+ valueRank,
94
+ arrayDimensions,
95
+ accessLevel: readWrite,
96
+ userAccessLevel: readWrite,
97
+ value
98
+ });
99
+ }
100
+
101
+ typedVariable(relPath: string, dataTypeName: string, valueRank: number): UAVariable {
102
+ const { variant, arrayDimensions } = variantFor(dataTypeName, valueRank);
103
+ return this.variable(relPath, dataTypeName, valueRank, variant, arrayDimensions);
104
+ }
105
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The concrete ExtensionObject the Static, Dynamic and Mass folders hold.
3
+ *
4
+ * This package cannot choose one on its own. Namespace zero has no concrete
5
+ * structure worth exercising here - `Structure` (i=22) is abstract and the
6
+ * rest are service types - and a usable one lives in a companion nodeset that
7
+ * only the caller knows it has loaded. So the caller passes the behaviour in:
8
+ * which DataType to build, and how to move its fields for the Dynamic folder.
9
+ *
10
+ * Without these options every ExtensionObject variable keeps a null
11
+ * ExtensionObject, which is what the package did before and what its own tests
12
+ * (namespace zero only) still exercise.
13
+ *
14
+ * A CTT server that has loaded the AutoID nodeset, for instance:
15
+ *
16
+ * build_address_space_for_conformance_testing(addressSpace, {
17
+ * extensionObject: {
18
+ * dataType: "RfidSighting",
19
+ * randomFields: () => ({
20
+ * antenna: 1 + Math.floor(Math.random() * 4),
21
+ * strength: -80 + Math.floor(Math.random() * 60),
22
+ * currentPowerLevel: 10 + Math.floor(Math.random() * 20),
23
+ * timestamp: new Date()
24
+ * })
25
+ * }
26
+ * });
27
+ */
28
+ import type { AddressSpace } from "node-opcua-address-space";
29
+ import type { NodeIdLike } from "node-opcua-nodeid";
30
+
31
+ export interface CustomExtensionObjectOptions {
32
+ /**
33
+ * DataType of the structure to build: a browse name ("RfidSighting") or a
34
+ * NodeId, in a namespace the caller has already loaded. A DataType this
35
+ * address space cannot find is an error - a silent fallback to null would
36
+ * look like the option had been ignored.
37
+ */
38
+ dataType: NodeIdLike;
39
+ /** field values of the static instances; the DataType's own defaults when omitted */
40
+ initialFields?: Record<string, unknown>;
41
+ /** field values for the Dynamic folder, called on every simulation tick */
42
+ randomFields?: () => Record<string, unknown>;
43
+ }
44
+
45
+ export interface CustomExtensionObject {
46
+ /** browse name of the structure, for descriptions */
47
+ readonly typeName: string;
48
+ /** a fresh instance carrying the initial field values */
49
+ initialValue(): unknown;
50
+ /** a fresh instance carrying new field values, or the initial one when no randomFields was given */
51
+ random(): unknown;
52
+ }
53
+
54
+ /**
55
+ * `findDataType("RfidSighting")` searches namespace zero only, and a companion
56
+ * structure is never there. A bare browse name is therefore looked up in every
57
+ * loaded namespace, so the caller does not have to know the index the nodeset
58
+ * happened to land on. A NodeId, or a "1:RfidSighting" qualified name, still
59
+ * resolves directly.
60
+ */
61
+ function findDataTypeAnywhere(addressSpace: AddressSpace, dataType: NodeIdLike) {
62
+ const direct = addressSpace.findDataType(dataType as string);
63
+ if (direct) return direct;
64
+ if (typeof dataType !== "string" || dataType.includes("=") || dataType.includes(":")) return null;
65
+ for (let index = 1; index < addressSpace.getNamespaceArray().length; index++) {
66
+ const found = addressSpace.findDataType(dataType, index);
67
+ if (found) return found;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /** one row of the typeAndDefaultValue table, as the builders see it */
73
+ interface TypeEntry {
74
+ type: string;
75
+ realType?: string;
76
+ defaultValue: unknown;
77
+ }
78
+
79
+ /**
80
+ * The initial value of a typeAndDefaultValue row, with the caller's structure
81
+ * standing in for the ExtensionObject row when one was supplied.
82
+ */
83
+ export function initialValueFor(entry: TypeEntry, custom?: CustomExtensionObject): unknown {
84
+ if (custom && (entry.realType ?? entry.type) === "ExtensionObject") return custom.initialValue();
85
+ return typeof entry.defaultValue === "function" ? (entry.defaultValue as () => unknown)() : entry.defaultValue;
86
+ }
87
+
88
+ export function resolveCustomExtensionObject(
89
+ addressSpace: AddressSpace,
90
+ options?: CustomExtensionObjectOptions
91
+ ): CustomExtensionObject | undefined {
92
+ if (!options) return undefined;
93
+ const dataType = findDataTypeAnywhere(addressSpace, options.dataType);
94
+ if (!dataType) {
95
+ throw new Error(
96
+ `extensionObject.dataType ${options.dataType} is not in the address space: load the nodeset that defines it first`
97
+ );
98
+ }
99
+ const build = (fields?: Record<string, unknown>) => addressSpace.constructExtensionObject(dataType, fields ?? {});
100
+ return {
101
+ typeName: dataType.browseName.name ?? String(options.dataType),
102
+ initialValue: () => build(options.initialFields),
103
+ random: () => build(options.randomFields ? options.randomFields() : options.initialFields)
104
+ };
105
+ }