/** * @module node-opcua-address-space */ // tslint:disable:no-console import chalk from "chalk"; import type { AddBaseNodeOptions, AddEnumerationTypeOptions, AddMethodOptions, AddObjectOptions, AddObjectTypeOptions, AddReferenceOpts, AddReferenceTypeOptions, AddVariableOptions, AddVariableTypeOptions, AddViewOptions, AddYArrayItemOptions, BaseNode, CreateDataTypeOptions, CreateNodeOptions, EnumerationItem, RequiredModel, UADataType, UAEventType, UAMethod, UAObject, UAObjectType, UAReference, UAReferenceType, UAVariable, UAVariableType, UAView } from "node-opcua-address-space-base"; import { assert } from "node-opcua-assert"; import { coerceInt64 } from "node-opcua-basic-types"; import { AxisScaleEnumeration } from "node-opcua-data-access"; import { AccessRestrictionsFlag, BrowseDirection, coerceLocalizedText, coerceQualifiedName, NodeClass, QualifiedName, type QualifiedNameLike } from "node-opcua-data-model"; import { dumpIf, make_errorLog, make_warningLog } from "node-opcua-debug"; import { coerceNodeId, NodeId, type NodeIdLike, NodeIdType, resolveNodeId } from "node-opcua-nodeid"; import type { UAAnalogItem, UADataItem, UAInitialState, UAState } from "node-opcua-nodeset-ua"; import { StatusCodes } from "node-opcua-status-code"; import { Argument, type ArgumentOptions, AxisInformation, EnumDefinition, EnumField, EnumValueType, EUInformation, Range, type RolePermissionType, type RolePermissionTypeOptions } from "node-opcua-types"; import { isNullOrUndefined } from "node-opcua-utils"; import { DataType, Variant, VariantArrayType, type VariantOptions, verifyRankAndDimensions } from "node-opcua-variant"; import type { InstantiateAlarmConditionOptions, InstantiateLimitAlarmOptions, InstantiateOffNormalAlarmOptions, UATwoStateDiscreteEx, UAYArrayItemEx } from "../source"; import type { AddMultiStateDiscreteOptions, AddMultiStateValueDiscreteOptions, AddTwoStateDiscreteOptions, AddTwoStateVariableOptions } from "../source/address_space_ts"; import type { InstantiateExclusiveDeviationAlarmOptions } from "../source/interfaces/alarms_and_conditions/instantiate_exclusive_deviation_alarm_options"; import type { InstantiateNonExclusiveDeviationAlarmOptions } from "../source/interfaces/alarms_and_conditions/instantiate_non_exclusive_deviation_alarm_options"; import type { InstantiateNonExclusiveLimitAlarmOptions } from "../source/interfaces/alarms_and_conditions/instantiate_non_exclusive_limit_alarm_options"; import type { UAAlarmConditionEx } from "../source/interfaces/alarms_and_conditions/ua_alarm_condition_ex"; import type { UAConditionEx } from "../source/interfaces/alarms_and_conditions/ua_condition_ex"; import type { UADiscreteAlarmEx } from "../source/interfaces/alarms_and_conditions/ua_discrete_alarm_ex"; import type { UAExclusiveDeviationAlarmEx } from "../source/interfaces/alarms_and_conditions/ua_exclusive_deviation_alarm_ex"; import type { UAExclusiveLimitAlarmEx } from "../source/interfaces/alarms_and_conditions/ua_exclusive_limit_alarm_ex"; import type { UALimitAlarmEx } from "../source/interfaces/alarms_and_conditions/ua_limit_alarm_ex"; import type { UANonExclusiveDeviationAlarmEx } from "../source/interfaces/alarms_and_conditions/ua_non_exclusive_deviation_alarm_ex"; import type { UANonExclusiveLimitAlarmEx } from "../source/interfaces/alarms_and_conditions/ua_non_exclusive_limit_alarm_ex"; import type { UAMultiStateValueDiscreteEx } from "../source/interfaces/data_access/ua_multistate_value_discrete_ex"; import type { UAStateMachineEx } from "../source/interfaces/state_machine/ua_state_machine_type"; import type { UATransitionEx } from "../source/interfaces/state_machine/ua_transition_ex"; import type { AddAnalogDataItemOptions, AddDataItemOptions } from "../source/namespace_data_access"; import type { UATwoStateVariableEx } from "../source/ua_two_state_variable_ex"; import { _handle_delete_node_model_change_event, _handle_model_change_event } from "./address_space_change_event_tools"; import type { AddressSpacePrivate } from "./address_space_private"; import { UAAcknowledgeableConditionImpl, UAAlarmConditionImpl } from "./alarms_and_conditions"; import { UAConditionImpl } from "./alarms_and_conditions/ua_condition_impl"; import { UADiscreteAlarmImpl } from "./alarms_and_conditions/ua_discrete_alarm_impl"; import { UAExclusiveDeviationAlarmImpl } from "./alarms_and_conditions/ua_exclusive_deviation_alarm_impl"; import { UAExclusiveLimitAlarmImpl } from "./alarms_and_conditions/ua_exclusive_limit_alarm_impl"; import { UALimitAlarmImpl } from "./alarms_and_conditions/ua_limit_alarm_impl"; import { UANonExclusiveDeviationAlarmImpl } from "./alarms_and_conditions/ua_non_exclusive_deviation_alarm_impl"; import { UANonExclusiveLimitAlarmImpl } from "./alarms_and_conditions/ua_non_exclusive_limit_alarm_impl"; import { type UAOffNormalAlarmEx, UAOffNormalAlarmImpl } from "./alarms_and_conditions/ua_off_normal_alarm_impl"; import { BaseNodeImpl } from "./base_node_impl"; import { add_dataItem_stuff } from "./data_access/add_dataItem_stuff"; import { _addMultiStateDiscrete, type UAMultiStateDiscreteImpl } from "./data_access/ua_multistate_discrete_impl"; import { _addMultiStateValueDiscrete } from "./data_access/ua_multistate_value_discrete_impl"; import { _addTwoStateDiscrete } from "./data_access/ua_two_state_discrete_impl"; // import { type NamespacePrivate, UANamespace_process_modelling_rule } from "./namespace_private"; import { type ConstructNodeIdOptions, NodeIdManager } from "./nodeid_manager"; import { coerceRolePermissions } from "./role_permissions"; import type { UAStateMachineImpl, UATransitionImpl } from "./state_machine/finite_state_machine"; // state machine import { _addTwoStateVariable } from "./state_machine/ua_two_state_variable"; import { UADataTypeImpl } from "./ua_data_type_impl"; import { UAMethodImpl } from "./ua_method_impl"; import { UAObjectImpl } from "./ua_object_impl"; import { UAObjectTypeImpl } from "./ua_object_type_impl"; import { UAReferenceTypeImpl } from "./ua_reference_type_impl"; import { UAVariableImpl } from "./ua_variable_impl"; import { UAVariableTypeImpl } from "./ua_variable_type_impl"; import { UAViewImpl } from "./ua_view_impl"; function _makeHashKey(nodeId: NodeId): string | number { switch (nodeId.identifierType) { case NodeIdType.STRING: case NodeIdType.GUID: return nodeId.value as string; case NodeIdType.NUMERIC: return nodeId.value as number; default: // c8 ignore next if (nodeId.identifierType !== NodeIdType.BYTESTRING) { throw new Error("invalid nodeIdType"); } return nodeId.value ? nodeId.value.toString() : "OPAQUE:0"; } } const doDebug = false; const errorLog = make_errorLog("AddressSpace"); const warningLog = make_warningLog("AddressSpace"); const regExp1 = /^(s|i|b|g)=/; const regExpNamespaceDotBrowseName = /^[0-9]+:(.*)/; export interface AddFolderOptions { browseName: QualifiedNameLike; } interface AddVariableOptions2 extends AddVariableOptions { nodeClass?: NodeClass.Variable; } function detachNode(node: BaseNode) { const addressSpace = node.addressSpace; const nonHierarchicalReferences = node.findReferencesEx("NonHierarchicalReferences", BrowseDirection.Inverse); for (const ref of nonHierarchicalReferences) { assert(!ref.isForward); ref.node?.removeReference({ isForward: !ref.isForward, nodeId: node.nodeId, referenceType: ref.referenceType }); } const nonHierarchicalReferencesF = node.findReferencesEx("NonHierarchicalReferences", BrowseDirection.Forward); for (const ref of nonHierarchicalReferencesF) { if (!ref.node) { // could be a special case of a frequently use target node such as ModellingRule_Mandatory that do not back trace // their reference continue; } assert(ref.isForward); ref.node?.removeReference({ isForward: !ref.isForward, nodeId: node.nodeId, referenceType: ref.referenceType }); } // remove reversed Hierarchical references const hierarchicalReferences = node.findReferencesEx("HierarchicalReferences", BrowseDirection.Inverse); for (const ref of hierarchicalReferences) { assert(!ref.isForward); const parent = addressSpace.findNode(ref.nodeId) as BaseNode; parent.removeReference({ isForward: !ref.isForward, nodeId: node.nodeId, referenceType: ref.referenceType }); } (node).unpropagate_back_references(); } interface NamespaceConstructorOptions { addressSpace: AddressSpacePrivate; index: number; namespaceUri: string; publicationDate: Date; version: string; } function toNodeId(nodeId: NodeId | { nodeId: NodeId } | string | number | undefined): NodeId | undefined { if (!nodeId) { return undefined; } if (nodeId instanceof NodeId) { return nodeId; } if (typeof nodeId === "string") { return coerceNodeId(nodeId); } if (typeof nodeId === "number") { return coerceNodeId(nodeId); } return nodeId.nodeId; } /** * * @constructor * @params options {Object} * @params options.namespaceUri {string} * @params options.addressSpace {IAddressSpace} * @params options.index {number} * @params options.version="" {string} * @params options.publicationDate="" {Date} * */ export class NamespaceImpl implements NamespacePrivate { public static _handle_hierarchy_parent = _handle_hierarchy_parent; public static isNonEmptyQualifiedName = isNonEmptyQualifiedName; public readonly namespaceUri: string; public addressSpace: AddressSpacePrivate; public readonly index: number; public emulateVersion103 = false; public version = "0.0.0"; public publicationDate: Date = new Date(Date.UTC(1900, 0, 1)); public registerSymbolicNames = false; private _requiredModels?: RequiredModel[]; private _objectTypeMap: Map; private _variableTypeMap: Map; private _referenceTypeMap: Map; private _dataTypeMap: Map; private _referenceTypeMapInv: Map; private _nodeIdManager: NodeIdManager; private _nodeid_index: Map; private _aliases: Map; private defaultAccessRestrictions?: AccessRestrictionsFlag; private defaultRolePermissions?: RolePermissionType[]; constructor(options: NamespaceConstructorOptions) { // c8 ignore next if (!(typeof options.namespaceUri === "string")) { throw new Error(`NamespaceImpl constructor: namespaceUri must exists and be a string : got ${options.namespaceUri}`); } // c8 ignore next if (typeof options.index !== "number") { throw new Error("NamespaceImpl constructor: index must be a number"); } // c8 ignore next if (!options.addressSpace) { throw new Error("NamespaceImpl constructor: Must specify a valid address space"); } this.namespaceUri = options.namespaceUri; this.addressSpace = options.addressSpace; this.index = options.index; this._nodeid_index = new Map(); this._aliases = new Map(); this._objectTypeMap = new Map(); this._variableTypeMap = new Map(); this._referenceTypeMap = new Map(); this._referenceTypeMapInv = new Map(); this._dataTypeMap = new Map(); this._nodeIdManager = new NodeIdManager(this.index, this.addressSpace); } public getDefaultNamespace(): NamespacePrivate { return this.index === 0 ? this : this.addressSpace.getDefaultNamespace(); } public toJSON(): Record { return { index: this.index, namespaceUri: this.namespaceUri, version: this.version }; } public toString(): string { return `Namespace({ index: ${this.index}, namespaceUri: "${this.namespaceUri}" })`; } public [Symbol.for("nodejs.util.inspect.custom")](): string { return this.toString(); } public dispose(): void { for (const node of this.nodeIterator()) { (node).dispose(); } this._nodeid_index = new Map(); this._aliases = new Map(); this.addressSpace = {} as AddressSpacePrivate; this._objectTypeMap = new Map(); this._variableTypeMap = new Map(); this._referenceTypeMap = new Map(); this._referenceTypeMapInv = new Map(); this._dataTypeMap = new Map(); } public nodeIterator(): IterableIterator { return this._nodeid_index.values(); } public _objectTypeIterator(): IterableIterator { return this._objectTypeMap.values(); } public _objectTypeCount(): number { return this._objectTypeMap.size; } public _variableTypeIterator(): IterableIterator { return this._variableTypeMap.values(); } public _variableTypeCount(): number { return this._variableTypeMap.size; } public _dataTypeIterator(): IterableIterator { return this._dataTypeMap.values(); } public _dataTypeCount(): number { return this._dataTypeMap.size; } public _referenceTypeIterator(): IterableIterator { return this._referenceTypeMap.values(); } public _referenceTypeCount(): number { return this._referenceTypeMap.size; } public _aliasCount(): number { return this._aliases.size; } public findNode2(nodeId: NodeId): BaseNode | null { // this one is faster assuming you have a nodeId assert(nodeId.namespace === this.index); return this._nodeid_index.get(_makeHashKey(nodeId)) || null; } public findNode(nodeId: string | NodeId): BaseNode | null { if (typeof nodeId === "string") { if (nodeId.match(regExp1)) { nodeId = `ns=${this.index};${nodeId}`; } } nodeId = resolveNodeId(nodeId); return this.findNode2(nodeId); } /** * * @param objectTypeName {String} * @return {UAObjectType|null} */ public findObjectType(objectTypeName: string): UAObjectType | null { return this._objectTypeMap.get(objectTypeName) || null; } /** * * @param variableTypeName {String} * @returns {UAVariableType|null} */ public findVariableType(variableTypeName: string): UAVariableType | null { return this._variableTypeMap.get(variableTypeName) || null; } /** * * @param dataTypeName {String} * @returns {UADataType|null} */ public findDataType(dataTypeName: string): UADataType | null { return this._dataTypeMap.get(dataTypeName) || null; } /** * * @param referenceTypeName {String} * @returns {ReferenceType|null} */ public findReferenceType(referenceTypeName: string): UAReferenceType | null { return this._referenceTypeMap.get(referenceTypeName) || null; } /** * find a ReferenceType by its inverse name. * @param inverseName {String} the inverse name of the ReferenceType to find * @return {ReferenceType} */ public findReferenceTypeFromInverseName(inverseName: string): UAReferenceType | null { assert(typeof inverseName === "string"); const node = this._referenceTypeMapInv.get(inverseName); assert(!node || (node.nodeClass === NodeClass.ReferenceType && node.inverseName.text === inverseName)); return node ? node : null; } /** * * @param alias_name {String} the alias name * @param nodeId {NodeId} NodeId must belong to this namespace */ public addAlias(alias_name: string, nodeId: NodeId): void { assert(typeof alias_name === "string"); assert(nodeId instanceof NodeId); assert(nodeId.namespace === this.index); this._aliases.set(alias_name, nodeId); } public resolveAlias(name: string): NodeId | null { return this._aliases.get(name) || null; } /** * add a new Object type to the address space * @param options * @param options.browseName {String} the object type name * @param [options.displayName] {String|LocalizedText} the display name * @param [options.subtypeOf="BaseObjectType"] {String|NodeId|BaseNode} the base class * @param [options.nodeId] {String|NodeId} an optional nodeId for this objectType, * if not provided a new nodeId will be created * @param [options.isAbstract = false] {Boolean} * @param [options.eventNotifier = 0] {Integer} * @param [options.postInstantiateFunc = null] {Function} * */ public addObjectType(options: AddObjectTypeOptions): UAObjectType { assert(!Object.hasOwn(options, "dataType"), "an objectType should not have a dataType"); assert(!Object.hasOwn(options, "valueRank"), "an objectType should not have a valueRank"); assert(!Object.hasOwn(options, "arrayDimensions"), "an objectType should not have a arrayDimensions"); return this._addObjectOrVariableType(options, "BaseObjectType", NodeClass.ObjectType) as UAObjectType; } /** * add a new Variable type to the address space * @param options * @param options.browseName {String} the object type name * @param [options.displayName] {String|LocalizedText} the display name * @param [options.subtypeOf="BaseVariableType"] {String|NodeId|BaseNode} the base class * @param [options.nodeId] {String|NodeId} an optional nodeId for this objectType, * if not provided a new nodeId will be created * @param [options.isAbstract = false] {Boolean} * @param options.dataType {String|NodeId} the variable DataType * @param [options.valueRank = -1] * @param [options.arrayDimensions = null] { Array> * */ public addVariableType(options: AddVariableTypeOptions): UAVariableType { assert(!Object.hasOwn(options, "arrayDimension"), "Do you mean ArrayDimensions ?"); // dataType options.dataType = options.dataType || "Int32"; options.dataType = this.addressSpace._coerce_DataType(options.dataType); // valueRank/ arrayDimensions verifyRankAndDimensions(options); // arrayDimensions const variableType = this._addObjectOrVariableType(options, "BaseVariableType", NodeClass.VariableType) as UAVariableType; variableType.dataType = options.dataType; variableType.valueRank = options.valueRank || 0; variableType.arrayDimensions = options.arrayDimensions || []; return variableType as UAVariableType; } /** * add a variable as a component of the parent node */ public addVariable(options: AddVariableOptions): UAVariable { assert(arguments.length === 1, "Invalid arguments IAddressSpace#addVariable now takes only one argument."); if (Object.hasOwn(options, "propertyOf") && options.propertyOf) { assert(!options.typeDefinition || options.typeDefinition === "PropertyType"); options.typeDefinition = options.typeDefinition || "PropertyType"; } else { assert(!options.typeDefinition || options.typeDefinition !== "PropertyType"); } return this._addVariable(options as AddVariableOptions2); } public addView(options: AddViewOptions): UAView { assert(Object.hasOwn(options, "browseName")); assert(Object.hasOwn(options, "organizedBy")); const browseName = options.browseName; assert(typeof browseName === "string"); const addressSpace = this.addressSpace; const baseDataVariableTypeId = addressSpace.findVariableType("BaseDataVariableType")?.nodeId; // ------------------------------------------ TypeDefinition const _typeDefinition = options.typeDefinition || baseDataVariableTypeId; options.references = options.references || []; options.references.push({ isForward: true, nodeId: toNodeId(_typeDefinition) || NodeId.nullNodeId, referenceType: "HasTypeDefinition" }); const createOptions = options as CreateNodeOptions; assert(!createOptions.nodeClass); createOptions.nodeClass = NodeClass.View; const view = this.createNode(createOptions) as UAView; assert(view.nodeId instanceof NodeId); assert(view.nodeClass === NodeClass.View); return view; } public addObject(options1: AddObjectOptions): UAObject { const options: CreateNodeOptions = options1 as CreateNodeOptions; assert(!options.nodeClass || options.nodeClass === NodeClass.Object); options.nodeClass = NodeClass.Object; const typeDefinition = options.typeDefinition || "BaseObjectType"; options.references = options.references || []; options.references.push({ referenceType: "HasTypeDefinition", isForward: true, nodeId: typeDefinition }); options.eventNotifier = +(options.eventNotifier || 0); const obj = this.createNode(options) as UAObject; assert(obj instanceof UAObjectImpl); assert(obj.nodeClass === NodeClass.Object); return obj; } /** * * @param parentFolder * @param options {String|Object} * @param options.browseName {String} the name of the folder * @param [options.nodeId] {NodeId}. An optional nodeId for this object * * @return {BaseNode} */ public addFolder(parentFolder: UAObject, options: AddFolderOptions | string): UAObject { if (typeof options === "string") { options = { browseName: options }; } const addressSpace = this.addressSpace; assert(!(options as any).typeDefinition, "addFolder does not expect typeDefinition to be defined "); const typeDefinition = addressSpace._coerceTypeDefinition("FolderType"); parentFolder = addressSpace._coerceFolder(parentFolder) as UAObject; (options as any).nodeClass = NodeClass.Object; (options as any).references = [ { referenceType: "HasTypeDefinition", isForward: true, nodeId: typeDefinition }, { referenceType: "Organizes", isForward: false, nodeId: parentFolder.nodeId } ]; const node = this.createNode(options as CreateNodeOptions) as UAObject; return node; } /** * @param options * @param options.isAbstract * @param options.browseName * @param options.inverseName */ public addReferenceType(options: AddReferenceTypeOptions): UAReferenceType { const addressSpace = this.addressSpace; const options1 = options as CreateNodeOptions; options1.nodeClass = NodeClass.ReferenceType; options1.references = options.references || []; options1.nodeId = options.nodeId; if (options.subtypeOf) { const subtypeOfNodeId = addressSpace._coerceType(options.subtypeOf, "References", NodeClass.ReferenceType); assert(subtypeOfNodeId); options1.references.push({ isForward: false, nodeId: subtypeOfNodeId, referenceType: "HasSubtype" }); } const node = this.internalCreateNode(options1) as UAReferenceType; node.propagate_back_references(); return node; } /** */ public addMultiStateDiscrete(options: AddMultiStateDiscreteOptions): UAMultiStateDiscreteImpl { return _addMultiStateDiscrete(this, options); } /** */ public createDataType(options: CreateDataTypeOptions): UADataType { assert(Object.hasOwn(options, "isAbstract"), "must provide isAbstract"); assert(!Object.hasOwn(options, "nodeClass")); assert(Object.hasOwn(options, "browseName"), "must provide a browseName"); const options1 = options as unknown as { nodeClass: NodeClass; references: AddReferenceOpts[]; subtypeOf: UADataType }; options1.nodeClass = NodeClass.DataType; options1.references = options.references || []; if (options1.references.length === 0) { if (!options1.subtypeOf) { throw new Error("must provide a subtypeOf"); } } if (options1.subtypeOf) { if (!(options1.subtypeOf instanceof UADataTypeImpl)) { options1.subtypeOf = this.addressSpace.findDataType(options1.subtypeOf) as UADataType; } if (!options1.subtypeOf) { throw new Error("cannot find subtypeOf "); } options1.references.push({ isForward: false, nodeId: options1.subtypeOf.nodeId, referenceType: "HasSubtype" }); } const node = this.internalCreateNode(options) as UADataType; node.propagate_back_references(); return node; } /** * @param options * @param options.nodeClass * @param [options.nodeVersion {String} = "0" ] install nodeVersion * @param [options.modellingRule {String} = null] * @internal */ public createNode(options: CreateNodeOptions): BaseNode { let node: BaseNode = null as any as BaseNode; const addressSpace = this.addressSpace; addressSpace.modelChangeTransaction(() => { assert(isNonEmptyQualifiedName(options.browseName)); // xx assert(Object.prototype.hasOwnProperty.call(options,"browseName") && options.browseName.length > 0); assert(Object.hasOwn(options, "nodeClass")); options.references = addressSpace.normalizeReferenceTypes(options.references); const references = _copy_references(options.references); _handle_hierarchy_parent(addressSpace, references, options); _handle_event_hierarchy_parent(addressSpace, references, options); UANamespace_process_modelling_rule(references, options.modellingRule); options.references = references; node = this.internalCreateNode(options); assert(node.nodeId instanceof NodeId); node.propagate_back_references(); node.install_extra_properties(); _create_node_version_if_needed(node, options); _handle_model_change_event(node as BaseNodeImpl); }); return node; } /** * remove the specified Node from the address space * * @param nodeOrNodeId * * */ public deleteNode(nodeOrNodeId: NodeId | BaseNode): void { let node: BaseNode | null = null; let nodeId: NodeId = new NodeId(); if (nodeOrNodeId instanceof NodeId) { nodeId = nodeOrNodeId; node = this.findNode(nodeId); // c8 ignore next if (!node) { throw new Error(` deleteNode : cannot find node with nodeId${nodeId.toString()}`); } } else if (nodeOrNodeId instanceof BaseNodeImpl) { node = nodeOrNodeId; nodeId = node.nodeId; } // c8 ignore next if (nodeId.namespace !== this.index) { throw new Error("this node doesn't belong to this namespace"); } const addressSpace = this.addressSpace; addressSpace.modelChangeTransaction(() => { /* c8 ignore next */ if (!node) { throw new Error("this node doesn't belong to this namespace"); } // notify parent that node is being removed const hierarchicalReferences = node.findReferencesEx("HierarchicalReferences", BrowseDirection.Inverse); for (const ref of hierarchicalReferences) { assert(!ref.isForward); const parent = addressSpace.findNode(ref.nodeId) as BaseNodeImpl; assert(parent); parent._on_child_removed(node); } function deleteNodePointedByReference(ref: { nodeId: NodeId }) { const o = addressSpace.findNode(ref.nodeId) as BaseNode; addressSpace.deleteNode(o.nodeId); } // recursively delete all nodes below in the hierarchy of nodes // TODO : a better idea would be to extract any references of type "HasChild" const components = node.findReferencesEx("HasComponent", BrowseDirection.Forward); const properties = node.findReferencesEx("HasProperty", BrowseDirection.Forward); // TODO: shall we delete nodes pointed by "Organizes" links here ? const subFolders = node.findReferencesEx("Organizes", BrowseDirection.Forward); for (const r of components) { deleteNodePointedByReference(r); } for (const r of properties) { deleteNodePointedByReference(r); } for (const r of subFolders) { deleteNodePointedByReference(r); } _handle_delete_node_model_change_event(node); detachNode(node); // delete nodes from global index const namespace = addressSpace.getNamespace(node.nodeId.namespace); namespace._deleteNode(node); }); } /** * @internal */ public getStandardsNodeIds(): { referenceTypeIds: { [key: string]: string }; objectTypeIds: { [key: string]: string } } { const standardNodeIds = { objectTypeIds: {} as { [key: string]: string }, referenceTypeIds: {} as { [key: string]: string } }; for (const referenceType of this._referenceTypeMap.values()) { standardNodeIds.referenceTypeIds[referenceType?.browseName?.name as string] = referenceType.nodeId.toString(); } for (const objectType of this._objectTypeMap.values()) { standardNodeIds.objectTypeIds[objectType?.browseName?.name as string] = objectType.nodeId.toString(); } return standardNodeIds; } // - Events -------------------------------------------------------------------------------------- /** * add a new event type to the address space * @param options * @param options.browseName {String} the eventType name * @param [options.subtypeOf ="BaseEventType"] * @param [options.isAbstract = true] * @return {UAObjectType} : the object type * * @example * * var evtType = namespace.addEventType({ * browseName: "MyAuditEventType", * subtypeOf: "AuditEventType" * }); * var myConditionType = namespace.addEventType({ * browseName: "MyConditionType", * subtypeOf: "ConditionType", * isAbstract: false * }); * */ public addEventType(options: any): UAObjectType { options.subtypeOf = options.subtypeOf || "BaseEventType"; // are eventType always abstract ?? No => Condition can be instantiated! // but, by default is abstract is true options.isAbstract = Object.hasOwn(options, "isAbstract") ? !!options.isAbstract : true; return this.addObjectType(options); } // --------------------------------------------------------------------------------------------------- /** * */ public addDataItem(options: AddDataItemOptions): UADataItem { const addressSpace = this.addressSpace; const dataType = options.dataType || "Number"; const dataItemType = addressSpace.findVariableType("DataItemType"); if (!dataItemType) { throw new Error("Cannot find DataItemType"); } const variable = this.addVariable({ ...options, dataType, typeDefinition: dataItemType.nodeId }); add_dataItem_stuff(variable, options); variable.install_extra_properties(); return variable as UADataItem; } /** * * * AnalogDataItem DataItems that represent continuously-variable physical quantities ( e.g., length, temperature), * in contrast to the digital representation of data in discrete items * NOTE Typical examples are the values provided by temperature sensors or pressure sensors. OPC UA defines a * specific UAVariableType to identify an AnalogItem. Properties describe the possible ranges of AnalogItems. * * * @example: * * * namespace.add_analog_dataItem({ * componentOf: parentObject, * browseName: "TemperatureSensor", * * definition: "(tempA -25) + tempB", * valuePrecision: 0.5, * //- * instrumentRange: { low: 100 , high: 200}, // optional * engineeringUnitsRange: { low: 100 , high: 200}, // mandatory * engineeringUnits: standardUnits.degree_celsius,, // optional * * // access level * accessLevel: 1 * minimumSamplingInterval: 10, * * }); * * @return {UAVariable} */ public addAnalogDataItem(options: AddAnalogDataItemOptions): UAAnalogItem { const addressSpace = this.addressSpace; assert(Object.hasOwn(options, "engineeringUnitsRange"), "expecting engineeringUnitsRange"); const dataType = options.dataType || "Number"; const analogItemType = addressSpace.findVariableType("AnalogItemType"); if (!analogItemType) { throw new Error("expecting AnalogItemType to be defined , check nodeset xml file"); } const clone_options = { ...options, dataType, typeDefinition: analogItemType.nodeId } as AddVariableOptions; const variable = this.addVariable(clone_options) as UAVariableImpl; add_dataItem_stuff(variable, options); // mandatory (EURange in the specs) // OPC Unified Architecture, Part 8 6 Release 1.02 // EURange defines the value range likely to be obtained in normal operation. It is intended for such // use as automatically scaling a bar graph display // Sensor or instrument failure or deactivation can result in a return ed item value which is actually // outside of this range. Client software must be prepared to deal with this possibility. Similarly a client // may attempt to write a value that is outside of this range back to the server. The exact behavior // (accept, reject, clamp, etc.) in this case is server - dependent. However , in general servers shall be // prepared to handle this. // Example: EURange ::= {-200.0,1400.0} const euRange = this.addVariable({ browseName: { name: "EURange", namespaceIndex: 0 }, dataType: "Range", minimumSamplingInterval: 0, modellingRule: options.modellingRule, propertyOf: variable, typeDefinition: "PropertyType", value: new Variant({ dataType: DataType.ExtensionObject, value: new Range(options.engineeringUnitsRange) }) }) as UAVariableImpl; assert(euRange.readValue().value.value instanceof Range); const handler = variable.handle_semantic_changed.bind(variable); euRange.on("value_changed", handler); if (Object.hasOwn(options, "instrumentRange")) { const instrumentRange = this.addVariable({ accessLevel: "CurrentRead | CurrentWrite", browseName: { name: "InstrumentRange", namespaceIndex: 0 }, dataType: "Range", minimumSamplingInterval: 0, modellingRule: options.modellingRule ? "Mandatory" : undefined, propertyOf: variable, typeDefinition: "PropertyType", value: new Variant({ dataType: DataType.ExtensionObject, value: new Range(options.instrumentRange) }) }); instrumentRange.on("value_changed", handler); } (variable as any).acceptValueOutOfRange = options.acceptValueOutOfRange; if (Object.hasOwn(options, "engineeringUnits")) { const engineeringUnits = new EUInformation(options.engineeringUnits); assert(engineeringUnits instanceof EUInformation, "expecting engineering units"); // EngineeringUnits specifies the units for the DataItem‟s value (e.g., degree, hertz, seconds). The // EUInformation type is specified in 5.6.3. const eu = this.addVariable({ accessLevel: "CurrentRead", browseName: { name: "EngineeringUnits", namespaceIndex: 0 }, dataType: "EUInformation", minimumSamplingInterval: 0, modellingRule: options.modellingRule ? "Mandatory" : undefined, propertyOf: variable, typeDefinition: "PropertyType", value: new Variant({ dataType: DataType.ExtensionObject, value: engineeringUnits }) }); eu.on("value_changed", handler); } variable.install_extra_properties(); return variable as unknown as UAAnalogItem; } /** * * @param options {Object} * @param options.browseName {String} * @param [options.nodeId {NodeId}] * @param [options.value {UInt32} = 0 } * @param options.enumValues { EnumValueType[]| {Key,Value} } * @return {Object|UAVariable} * * @example * * * namespace.addMultiStateValueDiscrete({ * componentOf:parentObj, * browseName: "myVar", * enumValues: { * "Red": 0xFF0000, * "Green": 0x00FF00, * "Blue": 0x0000FF * } * }); * addMultiStateValueDiscrete(parentObj,{ * browseName: "myVar", * enumValues: [ * { * value: 0xFF0000, * displayName: "Red", * description: " The color Red" * }, * { * value: 0x00FF000, * displayName: "Green", * description: " The color Green" * }, * { * value: 0x0000FF, * displayName: "Blue", * description: " The color Blue" * } * * ] * }); */ public addMultiStateValueDiscrete( options: AddMultiStateValueDiscreteOptions ): UAMultiStateValueDiscreteEx { return _addMultiStateValueDiscrete(this, options); } // - /** * * @param options * @param options.componentOf {NodeId} * @param options.browseName {String} * @param options.title {String} * @param [options.instrumentRange] * @param [options.instrumentRange.low] {Double} * @param [options.instrumentRange.high] {Double} * @param options.engineeringUnitsRange.low {Double} * @param options.engineeringUnitsRange.high {Double} * @param options.engineeringUnits {String} * @param [options.nodeId = {NodeId}] * @param options.accessLevel * @param options.userAccessLevel * @param options.title {String} * @param options.axisScaleType {AxisScaleEnumeration} * * @param options.xAxisDefinition {AxisInformation} * @param options.xAxisDefinition.engineeringUnits EURange * @param options.xAxisDefinition.range * @param options.xAxisDefinition.range.low * @param options.xAxisDefinition.range.high * @param options.xAxisDefinition.title {LocalizedText} * @param options.xAxisDefinition.axisScaleType {AxisScaleEnumeration} * @param options.xAxisDefinition.axisSteps = {Array} * @param options.value */ public addYArrayItem
(options: AddYArrayItemOptions): UAYArrayItemEx
{ assert(Object.hasOwn(options, "engineeringUnitsRange"), "expecting engineeringUnitsRange"); assert(Object.hasOwn(options, "axisScaleType"), "expecting axisScaleType"); assert(options.xAxisDefinition !== null && typeof options.xAxisDefinition === "object", "expecting a xAxisDefinition"); const addressSpace = this.addressSpace; const YArrayItemType = addressSpace.findVariableType("YArrayItemType"); if (!YArrayItemType) { throw new Error("expecting YArrayItemType to be defined , check nodeset xml file"); } const dataType = toNodeId(options.dataType as unknown as NodeId) || toNodeId((options.value as Variant)?.dataType); const optionals = []; if (Object.hasOwn(options, "instrumentRange")) { optionals.push("InstrumentRange"); } const variable = YArrayItemType.instantiate({ browseName: options.browseName, componentOf: options.componentOf, dataType, optionals }) as UAYArrayItemEx; function coerceAxisScale(value: any) { const ret = AxisScaleEnumeration[value]; assert(!isNullOrUndefined(ret)); return ret; } variable.setValueFromSource(options.value as Variant, StatusCodes.Good); variable.euRange.setValueFromSource( new Variant({ dataType: DataType.ExtensionObject, value: new Range(options.engineeringUnitsRange) }) ); if (Object.hasOwn(options, "instrumentRange") && variable.instrumentRange) { variable.instrumentRange.setValueFromSource( new Variant({ dataType: DataType.ExtensionObject, value: new Range(options.instrumentRange) }) ); } variable.title.setValueFromSource( new Variant({ dataType: DataType.LocalizedText, value: coerceLocalizedText(options.title || "") }) ); // Linear/Log/Ln variable.axisScaleType.setValueFromSource( new Variant({ dataType: DataType.Int32, value: coerceAxisScale(options.axisScaleType) }) ); variable.xAxisDefinition.setValueFromSource( new Variant({ dataType: DataType.ExtensionObject, value: new AxisInformation(options.xAxisDefinition) }) ); return variable as any; } // - Methods ---------------------------------------------------------------------------------------------------- /** * @param parentObject {Object} * @param options {Object} * @param [options.nodeId=null] {NodeId} the object nodeid. * @param [options.browseName=""] {String} the object browse name. * @param [options.description=""] {String} the object description. * @param options.inputArguments {Array} * @param options.outputArguments {Array} * @return {Object} */ public addMethod(parentObject: UAObject, options: AddMethodOptions): UAMethod { const addressSpace = this.addressSpace; assert( parentObject !== null && typeof parentObject === "object" && parentObject instanceof BaseNodeImpl, "expecting a valid parent object" ); assert(Object.hasOwn(options, "browseName")); options.componentOf = parentObject; const method = this._addMethod(options); const propertyTypeId = addressSpace._coerce_VariableTypeIds("PropertyType"); const nodeId_ArgumentDataType = "Argument"; // makeNodeId(DataTypeIds.Argument); if (options.inputArguments) { const _inputArgs = new Variant({ arrayType: VariantArrayType.Array, dataType: DataType.ExtensionObject, value: options.inputArguments.map((opt: ArgumentOptions) => new Argument(opt)) }); const inputArguments = this.addVariable({ accessLevel: "CurrentRead", arrayDimensions: [_inputArgs.value.length], browseName: { name: "InputArguments", namespaceIndex: 0 }, dataType: nodeId_ArgumentDataType, description: "the definition of the input argument of method " + parentObject.browseName.toString() + "." + method.browseName.toString(), minimumSamplingInterval: -1, modellingRule: "Mandatory", propertyOf: method, typeDefinition: "PropertyType", value: _inputArgs, valueRank: 1 }); inputArguments.setValueFromSource(_inputArgs); assert(inputArguments.typeDefinition.toString() === propertyTypeId.toString()); assert(Array.isArray(inputArguments.arrayDimensions)); } if (options.outputArguments) { const _outputArgs = new Variant({ arrayType: VariantArrayType.Array, dataType: DataType.ExtensionObject, value: options.outputArguments.map((opts) => new Argument(opts)) }); const outputArguments = this.addVariable({ accessLevel: "CurrentRead", arrayDimensions: [_outputArgs.value.length], browseName: { name: "OutputArguments", namespaceIndex: 0 }, dataType: nodeId_ArgumentDataType, description: "the definition of the output arguments of method " + parentObject.browseName.toString() + "." + method.browseName.toString(), minimumSamplingInterval: -1, modellingRule: "Mandatory", propertyOf: method, typeDefinition: "PropertyType", value: _outputArgs, valueRank: 1 }); outputArguments.setValueFromSource(_outputArgs); assert(outputArguments.typeDefinition.toString() === propertyTypeId.toString()); assert(Array.isArray(outputArguments.arrayDimensions)); } // verifying post-conditions parentObject.install_extra_properties(); return method; } // - Enumeration ------------------------------------------------------------------------------------------------ /** * * @param options * @param options.browseName {String} * @param options.enumeration {Array} * @param options.enumeration[].displayName {String|LocalizedText} * @param options.enumeration[].value {Number} * @param options.enumeration[].description {String|LocalizedText|null} */ public addEnumerationType(options: AddEnumerationTypeOptions): UADataType { // Release 1.03 OPC Unified Architecture, Part 3 - page 34 // Enumeration DataTypes are DataTypes that represent discrete sets of named values. // Enumerations are always encoded as Int32 on the wire as defined in Part 6. Enumeration // DataTypes inherit directly or indirectly from the DataType Enumeration defined in 8.14. // Enumerations have no encodings exposed in the IAddressSpace. To expose the human readable // representation of an enumerated value the DataType Node may have the EnumString // Property that contains an array of LocalizedText. The Integer representation of the enumeration // value points to a position within that array. EnumValues Property can be used instead of the // EnumStrings to support integer representation of enumerations that are not zero-based or have // gaps. It contains an array of a Structured DataType containing the integer representation as // well as the human-readable representation. An example of an enumeration DataType containing // a sparse list of Integers is NodeClass which is defined in 8.30. // OPC Unified Architecture, Part 3 Release 1.03 page 35 // Table 11 – DataType NodeClass // EnumStrings O LocalizedText[] The EnumStrings Property only applies for Enumeration DataTypes. // It shall not be applied for other DataTypes. If the EnumValues // Property is provided, the EnumStrings Property shall not be provided. // Each entry of the array of LocalizedText in this Property represents // the human-readable representation of an enumerated value. The // Integer representation of the enumeration value points to a position // of the array. // EnumValues O EnumValueType[] The EnumValues Property only applies for Enumeration DataTypes. // It shall not be applied for other DataTypes. If the EnumStrings // Property is provided, the EnumValues Property shall not be provided. // Using the EnumValues Property it is possible to represent. // Enumerations with integers that are not zero-based or have gaps // (e.g. 1, 2, 4, 8, 16). // Each entry of the array of EnumValueType in this Property // represents one enumeration value with its integer notation, human readable // representation and help information. // The Property EnumStrings contains human-readable representations of enumeration values and is // only applied to Enumeration DataTypes. Instead of the EnumStrings Property an Enumeration // DataType can also use the EnumValues Property to represent Enumerations with integer values that are not // zero-based or containing gaps. There are no additional Properties defined for DataTypes in this standard. // Additional parts of this series of standards may define additional Properties for DataTypes. // 8.40 EnumValueType // This Structured DataType is used to represent a human-readable representation of an // Enumeration. Its elements are described inTable 27. When this type is used in an array representing // human-readable representations of an enumeration, each Value shall be unique in that array. // Table 27 – EnumValueType Definition // Name Type Description // EnumValueType structure // Value Int64 The Integer representation of an Enumeration. // DisplayName LocalizedText A human-readable representation of the Value of the Enumeration. // Description LocalizedText A localized description of the enumeration value. This field can // contain an empty string if no description is available. // Note that the EnumValueType has been defined with a Int64 Value to meet a variety of usages. // When it is used to define the string representation of an Enumeration DataType, the value range // is limited to Int32, because the Enumeration DataType is a subtype of Int32. Part 8 specifies // other usages where the actual value might be between 8 and 64 Bit. assert(typeof options.browseName === "string"); assert(Array.isArray(options.enumeration)); const addressSpace = this.addressSpace; const enumerationType = addressSpace.findDataType("Enumeration") as UADataType; assert(enumerationType.nodeId instanceof NodeId); assert(enumerationType instanceof UADataTypeImpl); const references = [{ referenceType: "HasSubtype", isForward: false, nodeId: enumerationType.nodeId }]; const opts = { browseName: options.browseName, definition: undefined, description: coerceLocalizedText(options.description) || null, displayName: options.displayName || null, isAbstract: false, nodeClass: NodeClass.DataType, references }; const enumType = this.internalCreateNode(opts) as UADataType; // as UAEnumeration; enumType.propagate_back_references(); if (typeof options.enumeration[0] === "string") { const enumeration = options.enumeration as string[]; // enumeration is a array of string const definition = enumeration.map((str: string, _index: number) => coerceLocalizedText(str)); const value = new Variant({ arrayType: VariantArrayType.Array, dataType: DataType.LocalizedText, value: definition }); const enumStrings = this.addVariable({ browseName: { name: "EnumStrings", namespaceIndex: 0 }, dataType: "LocalizedText", description: "", modellingRule: "Mandatory", propertyOf: enumType, value, valueRank: 1 }); assert(enumStrings.browseName.toString() === "EnumStrings"); // set $definition // EnumDefinition // This Structured DataType is used to provide the metadata for a custom Enumeration or // OptionSet DataType. It is derived from the DataType DataTypeDefinition. // Enum Field: // This Structured DataType is used to provide the metadata for a field of a custom Enumeration // or OptionSet DataType. It is derived from the DataType EnumValueType. If used for an // OptionSet, the corresponding Value in the base type contains the number of the bit associated // with the field. The EnumField is formally defined in Table 37. (enumType as any).$fullDefinition = new EnumDefinition({ fields: enumeration.map( (x: string, index: number) => new EnumField({ name: x, description: coerceLocalizedText(x), value: coerceInt64(index) }) ) }); } else { const enumeration = options.enumeration as EnumerationItem[]; // construct the definition object const definition = enumeration.map((enumItem: EnumerationItem) => { return new EnumValueType({ description: coerceLocalizedText(enumItem.description), displayName: coerceLocalizedText(enumItem.displayName), value: coerceInt64(enumItem.value) }); }); const value = new Variant({ arrayType: VariantArrayType.Array, dataType: DataType.ExtensionObject, value: definition }); const enumValues = this.addVariable({ browseName: { name: "EnumValues", namespaceIndex: 0 }, dataType: "EnumValueType", description: undefined, modellingRule: "Mandatory", propertyOf: enumType, value, valueRank: 1 }); assert(enumValues.browseName.toString() === "EnumValues"); (enumType as any).$fullDefinition = new EnumDefinition({ fields: enumeration.map( (x: EnumerationItem, _index: number) => new EnumField({ name: x.displayName.toString(), description: x.description || "", value: coerceInt64(x.value) }) ) }); } // now create the string value property // // EnumStrings // // i=68 // i=78 // i=852 // // // // Running // Failed // // // return enumType; } // ------------------------------------------------------------------------- // State and Transition // ------------------------------------------------------------------------- public toNodeset2XML(): string { return "has not be installed!"; } public setRequiredModels(requiredModels: RequiredModel[]): void { this._requiredModels = requiredModels; } public getRequiredModels(): RequiredModel[] | undefined { return this._requiredModels; } // ------------------------------------------------------------------------- // State and Transition // ------------------------------------------------------------------------- /** */ public addState( component: UAStateMachineEx, stateName: QualifiedNameLike, stateNumber: number, isInitialState: boolean ): UAState | UAInitialState { const addressSpace = this.addressSpace; isInitialState = !!isInitialState; const _component = component as UAStateMachineImpl; assert(_component.nodeClass === NodeClass.Object || _component.nodeClass === NodeClass.ObjectType); const initialStateType = addressSpace.findObjectType("InitialStateType")!; const stateType = addressSpace.findObjectType("StateType")!; let state: UAState | UAInitialState; if (isInitialState) { state = initialStateType.instantiate({ browseName: stateName, componentOf: _component }) as UAInitialState; } else { state = stateType.instantiate({ browseName: stateName, componentOf: _component }) as UAState; } // ensure state number is unique state.stateNumber.setValueFromSource({ dataType: DataType.UInt32, value: stateNumber }); return state; } /** */ public addTransition( component: UAStateMachineEx, fromState: string, toState: string, transitionNumber: number, browseName?: QualifiedNameLike ): UATransitionEx { const addressSpace = this.addressSpace; const _component = component as UAStateMachineImpl; assert(_component.nodeClass === NodeClass.Object || _component.nodeClass === NodeClass.ObjectType); assert(typeof fromState === "string"); assert(typeof toState === "string"); assert(Number.isFinite(transitionNumber)); const fromStateNode = _component.getComponentByName(fromState); // c8 ignore next if (!fromStateNode) { throw new Error(`Cannot find state with name ${fromState}`); } assert(fromStateNode.browseName.name?.toString() === fromState); const toStateNode = _component.getComponentByName(toState); // c8 ignore next if (!toStateNode) { throw new Error(`Cannot find state with name ${toState}`); } assert(toStateNode.browseName.name?.toString() === toState); const transitionType = addressSpace.findObjectType("TransitionType"); if (!transitionType) { throw new Error("Cannot find TransitionType"); } browseName = browseName || `${fromState}To${toState}`; // "Transition"; const transition = transitionType.instantiate({ browseName, componentOf: _component }) as UATransitionImpl; transition.addReference({ isForward: true, nodeId: toStateNode.nodeId, referenceType: "ToState" }); transition.addReference({ isForward: true, nodeId: fromStateNode.nodeId, referenceType: "FromState" }); transition.transitionNumber.setValueFromSource({ dataType: DataType.UInt32, value: transitionNumber }); return transition; } /** * * @return {UATwoStateVariable} */ public addTwoStateVariable(options: AddTwoStateVariableOptions): UATwoStateVariableEx { return _addTwoStateVariable(this, options); } /** * * Add a TwoStateDiscrete Variable * @return {UATwoStateDiscrete} */ public addTwoStateDiscrete(options: AddTwoStateDiscreteOptions): UATwoStateDiscreteEx { return _addTwoStateDiscrete(this, options); } // --- Alarms & Conditions ------------------------------------------------- public instantiateCondition( conditionTypeId: UAEventType | NodeId | string, options: any, data: Record ): UAConditionEx { return UAConditionImpl.instantiate(this, conditionTypeId, options, data); } public instantiateAcknowledgeableCondition( conditionTypeId: UAEventType | NodeId | string, options: InstantiateAlarmConditionOptions, data?: Record ): UAAcknowledgeableConditionImpl { return UAAcknowledgeableConditionImpl.instantiate(this, conditionTypeId, options, data); } public instantiateAlarmCondition( alarmConditionTypeId: UAEventType | NodeId | string, options: InstantiateAlarmConditionOptions, data?: Record ): UAAlarmConditionEx { return UAAlarmConditionImpl.instantiate(this, alarmConditionTypeId, options, data); } public instantiateLimitAlarm( limitAlarmTypeId: UAEventType | NodeId | string, options: InstantiateLimitAlarmOptions, data?: Record ): UALimitAlarmEx { return UALimitAlarmImpl.instantiate(this, limitAlarmTypeId, options, data); } public instantiateExclusiveLimitAlarm( exclusiveLimitAlarmTypeId: UAEventType | NodeId | string, options: InstantiateLimitAlarmOptions, data?: Record ): UAExclusiveLimitAlarmEx { return UAExclusiveLimitAlarmImpl.instantiate(this, exclusiveLimitAlarmTypeId, options, data); } public instantiateExclusiveDeviationAlarm( options: InstantiateExclusiveDeviationAlarmOptions, data?: Record ): UAExclusiveDeviationAlarmEx { return UAExclusiveDeviationAlarmImpl.instantiate(this, "ExclusiveDeviationAlarmType", options, data); } public instantiateNonExclusiveLimitAlarm( nonExclusiveLimitAlarmTypeId: UAEventType | NodeId | string, options: InstantiateNonExclusiveLimitAlarmOptions, data?: Record ): UANonExclusiveLimitAlarmEx { return UANonExclusiveLimitAlarmImpl.instantiate(this, nonExclusiveLimitAlarmTypeId, options, data); } public instantiateNonExclusiveDeviationAlarm( options: InstantiateNonExclusiveDeviationAlarmOptions, data?: Record ): UANonExclusiveDeviationAlarmEx { return UANonExclusiveDeviationAlarmImpl.instantiate(this, "NonExclusiveDeviationAlarmType", options, data); } public instantiateDiscreteAlarm( discreteAlarmType: UAEventType | NodeId | string, options: InstantiateAlarmConditionOptions, data?: Record ): UADiscreteAlarmEx { return UADiscreteAlarmImpl.instantiate(this, discreteAlarmType, options, data); } public instantiateOffNormalAlarm( options: InstantiateOffNormalAlarmOptions, data?: Record ): UAOffNormalAlarmEx { return UAOffNormalAlarmImpl.instantiate(this, "OffNormalAlarmType", options, data); } // default roles and permissions public setDefaultRolePermissions(rolePermissions: RolePermissionTypeOptions[] | null): void { this.defaultRolePermissions = rolePermissions ? coerceRolePermissions(rolePermissions) : undefined; } public getDefaultRolePermissions(): RolePermissionType[] | null { return this.defaultRolePermissions || null; } public setDefaultAccessRestrictions(accessRestrictions: AccessRestrictionsFlag): void { this.defaultAccessRestrictions = accessRestrictions; } public getDefaultAccessRestrictions(): AccessRestrictionsFlag { return this.defaultAccessRestrictions || AccessRestrictionsFlag.None; } // --- internal stuff public constructNodeId(options: ConstructNodeIdOptions): NodeId { return this._nodeIdManager.constructNodeId({ registerSymbolicNames: this.registerSymbolicNames, ...options }); } public _register(node: BaseNode): void { assert(node instanceof BaseNodeImpl, "Expecting a instance of BaseNode in _register"); assert(node.nodeId instanceof NodeId, "Expecting a NodeId"); // c8 ignore next if (node.nodeId.namespace !== this.index) { throw new Error( "node must belong to this namespace : " + node.nodeId.toString() + " " + " node.browseName = " + node.browseName.toString() + " this.index = " + this.index ); } assert(node.nodeId.namespace === this.index, "node must belongs to this namespace"); assert(Object.hasOwn(node, "browseName"), "Node must have a browseName"); const hashKey = _makeHashKey(node.nodeId); // c8 ignore next if (this._nodeid_index.has(hashKey)) { const existingNode = this.findNode(node.nodeId)!; throw new Error( "node " + node.browseName.toString() + " nodeId = " + node.nodeId.displayText() + " already registered " + node.nodeId.toString() + "\n" + " in namespace " + this.namespaceUri + " index = " + this.index + "\n" + "existing node = " + existingNode.toString() + "this parent : " + node.parentNodeId?.toString() ); } this._nodeid_index.set(hashKey, node); switch (node.nodeClass) { case NodeClass.ObjectType: this._registerObjectType(node as UAObjectType); break; case NodeClass.VariableType: this._registerVariableType(node as UAVariableType); break; case NodeClass.ReferenceType: this._registerReferenceType(node as UAReferenceType); break; case NodeClass.DataType: this._registerDataType(node as UADataType); break; case NodeClass.Object: case NodeClass.Variable: case NodeClass.Method: case NodeClass.View: break; default: // tslint:disable-next-line:no-console errorLog("Invalid class Name", node.nodeClass); throw new Error("Invalid class name specified"); } } /** * @internal */ public internalCreateNode(options: CreateNodeOptions): BaseNode { assert(options.nodeClass !== undefined, " options.nodeClass must be specified"); assert(options.browseName, "options.browseName must be specified"); // xx assert(options.browseName instanceof QualifiedName // ? (options.browseName.namespaceIndex === this.index): true, // "Expecting browseName to have the same namespaceIndex as the namespace"); options.description = coerceLocalizedText(options.description) || ""; // browseName adjustment if (typeof options.browseName === "string") { const match = options.browseName.match(regExpNamespaceDotBrowseName); if (match) { const correctedName = match[1]; // the application is using an old scheme warningLog( chalk.green( "Warning : since node-opcua 0.4.2 " + "namespace index should not be prepended to the browse name anymore" ) ); warningLog(" ", options.browseName, " will be replaced with ", correctedName); warningLog(" Please update your code"); const indexVerify = parseInt(match[0], 10); if (indexVerify !== this.index) { errorLog( chalk.red.bold( "Error: namespace index used at the front of the browseName " + indexVerify + " do not match the index of the current namespace (" + this.index + ")" ) ); errorLog( " Please fix your code so that the created node is inserted in the correct namespace," + " please refer to the NodeOPCUA documentation" ); } } options.browseName = new QualifiedName({ name: options.browseName, namespaceIndex: this.index }); } else if (!(options.browseName instanceof QualifiedName)) { options.browseName = new QualifiedName(options.browseName); } assert(options.browseName instanceof QualifiedName, "Expecting options.browseName to be instanceof QualifiedName "); // ------------- set display name if (!options.displayName) { assert(typeof options.browseName.name === "string"); options.displayName = coerceLocalizedText(options.browseName.name || ""); } if (!options.nodeClass || options.nodeClass === undefined) { throw new Error("nodeclass must be specified"); } // --- nodeId adjustment options.nodeId = this.constructNodeId(options as ConstructNodeIdOptions); dumpIf(!options.nodeId, options); // missing node Id assert(options.nodeId instanceof NodeId); // assert(options.browseName.namespaceIndex === this.index,"Expecting browseName to have // the same namespaceIndex as the namespace"); const Constructor = _constructors_map[NodeClass[options.nodeClass] as keyof typeof _constructors_map]; if (!Constructor) { throw new Error(` missing constructor for NodeClass ${NodeClass[options.nodeClass]}`); } options.addressSpace = this.addressSpace as AddressSpacePrivate; // biome-ignore lint/suspicious/noExplicitAny: to fix later const node = new Constructor(options as any); this._register(node); // object shall now be registered // c8 ignore next if (doDebug) { assert(this.findNode(node.nodeId) !== null && typeof this.findNode(node.nodeId) === "object"); } return node; } public _deleteNode(node: BaseNode): void { assert(node instanceof BaseNodeImpl); const hashKey = _makeHashKey(node.nodeId); // c8 ignore next if (!this._nodeid_index.has(hashKey)) { throw new Error(`deleteNode : nodeId ${node.nodeId.displayText()} is not registered ${node.nodeId.toString()}`); } switch (node.nodeClass) { case NodeClass.ObjectType: this._unregisterObjectType(node as UAObjectType); break; case NodeClass.VariableType: this._unregisterVariableType(node as UAVariableType); break; case NodeClass.Object: case NodeClass.Variable: case NodeClass.Method: case NodeClass.View: break; default: // tslint:disable:no-console warningLog("Invalid class Name", node.nodeClass); throw new Error("Invalid class name specified"); } const deleted = this._nodeid_index.delete(hashKey); assert(deleted); (node).dispose(); } // --- Private stuff private _addObjectOrVariableType<_T>( options1: AddBaseNodeOptions, topMostBaseType: string, nodeClass: NodeClass.ObjectType | NodeClass.VariableType ) { const addressSpace = this.addressSpace; assert(typeof topMostBaseType === "string"); assert(nodeClass === NodeClass.ObjectType || nodeClass === NodeClass.VariableType); const options = options1 as CreateNodeOptions; assert(!options.nodeClass); assert(options.browseName); assert(typeof options.browseName === "string"); if (Object.hasOwn(options, "references")) { throw new Error("options.references should not be provided, use options.subtypeOf instead"); } const references: UAReference[] = []; function process_subtypeOf_options(this: NamespaceImpl, options2: any, references1: AddReferenceOpts[]) { // check common misspelling mistake assert(!options2.subTypeOf, "misspell error : it should be 'subtypeOf' instead"); if (Object.hasOwn(options2, "hasTypeDefinition")) { throw new Error("hasTypeDefinition option is invalid. Do you mean typeDefinition instead ?"); } assert(!options2.typeDefinition, " do you mean subtypeOf ?"); const subtypeOfNodeId = addressSpace._coerceType(options2.subtypeOf, topMostBaseType, nodeClass); assert(subtypeOfNodeId); references1.push({ isForward: false, nodeId: subtypeOfNodeId, referenceType: "HasSubtype" }); } process_subtypeOf_options.call(this, options, references); const objectType = this.internalCreateNode({ browseName: options.browseName, displayName: options.displayName, description: options.description, eventNotifier: +(options.eventNotifier || 0), isAbstract: !!options.isAbstract, nodeClass, nodeId: options.nodeId, references }); objectType.propagate_back_references(); objectType.install_extra_properties(); if (options.postInstantiateFunc) { (objectType).installPostInstallFunc(options.postInstantiateFunc); } return objectType; } // private _adjust_options(options: any) { // const ns = this.addressSpace.getNamespaceIndex(this.namespaceUri); // if (!options.nodeId) { // const id = this._getNextAvailableId(); // options.nodeId = new NodeId(NodeId.NodeIdType.NUMERIC, id, ns); // } // options.nodeId = NodeId.coerce(options.nodeId); // if (typeof options.browseName === "string") { // options.browseName = new QualifiedName({ // name: options.browseName, // namespaceIndex: ns // }); // } // return options; // } private _registerObjectType(node: UAObjectType) { assert(this.index === node.nodeId.namespace); const key = node.browseName.name || ""; if (this._objectTypeMap.has(key)) { throw new Error(` UAObjectType already declared ${node.browseName.toString()} ${node.nodeId.toString()}`); } this._objectTypeMap.set(key, node); } private _registerVariableType(node: UAVariableType) { assert(this.index === node.nodeId.namespace); const key = node.browseName.name || ""; assert(!this._variableTypeMap.has(key), " UAVariableType already declared"); this._variableTypeMap.set(key, node); } private _registerReferenceType(node: UAReferenceType) { assert(this.index === node.nodeId.namespace); assert(node.browseName instanceof QualifiedName); const key: string = node.browseName.name || ""; this._referenceTypeMap.set(key, node); this._referenceTypeMapInv.set(node.inverseName.text || "", node); } private _registerDataType(node: UADataType) { assert(this.index === node.nodeId.namespace); const key = node.browseName.name || ""; assert(node.browseName instanceof QualifiedName); assert(!this._dataTypeMap.has(key), " DataType already declared"); this._dataTypeMap.set(key, node); } private _unregisterObjectType(node: UAObjectType): void { const key = node.browseName.name || ""; this._objectTypeMap.delete(key); } private _unregisterVariableType(node: UAVariableType): void { const key = node.browseName.name || ""; this._variableTypeMap.delete(key); } /** * @private */ private _addVariable(options: AddVariableOptions2): UAVariable { const addressSpace = this.addressSpace; const baseDataVariableType = addressSpace.findVariableType("BaseDataVariableType"); if (!baseDataVariableType) { throw new Error("cannot find BaseDataVariableType"); } const baseDataVariableTypeId = baseDataVariableType.nodeId; assert(Object.hasOwn(options, "browseName"), "options.browseName must be provided"); assert(Object.hasOwn(options, "dataType"), "options.dataType must be provided"); options.historizing = !!options.historizing; // c8 ignore next if (Object.hasOwn(options, "hasTypeDefinition")) { throw new Error("hasTypeDefinition option is invalid. Do you mean typeDefinition instead ?"); } // ------------------------------------------ TypeDefinition let typeDefinition = options.typeDefinition || baseDataVariableTypeId; if (typeDefinition instanceof BaseNodeImpl) { // c8 ignore next if (typeDefinition.nodeClass !== NodeClass.VariableType) { const message = `invalid typeDefinition expecting a VariableType got ${NodeClass[typeDefinition.nodeClass]}`; errorLog(message); throw new Error(message); } } typeDefinition = addressSpace._coerce_VariableTypeIds(typeDefinition); assert(typeDefinition instanceof NodeId); // ------------------------------------------ DataType options.dataType = addressSpace._coerce_DataType(options.dataType || DataType.Null); options.valueRank = isNullOrUndefined(options.valueRank) ? options.arrayDimensions ? options.arrayDimensions.length : -1 : options.valueRank; assert(typeof options.valueRank === "number" && Number.isFinite(options.valueRank)); options.arrayDimensions = options.arrayDimensions || null; assert(options.arrayDimensions === null || Array.isArray(options.arrayDimensions)); // ----------------------------------------------------- const hasGetter = (options: AddVariableOptions2) => { return typeof options.value?.get === "function" || typeof options.value?.timestamped_get === "function"; }; // c8 ignore next if (options.minimumSamplingInterval === undefined && hasGetter(options)) { // a getter has been specified and no options.minimumSamplingInterval has been specified warningLog( "[NODE-OPCUA-W30", "namespace#addVariable a getter has been specified and minimumSamplingInterval is missing.\nMinimumSamplingInterval has been adjusted to 1000 ms\nvariable = " + options?.browseName?.toString() ); options.minimumSamplingInterval = 1000; } options.minimumSamplingInterval = options.minimumSamplingInterval !== undefined ? +options.minimumSamplingInterval : 0; // c8 ignore next if (options.minimumSamplingInterval === 0 && hasGetter(options)) { warningLog( "[NODE-OPCUA-W31", "namespace#addVariable a getter has been specified and minimumSamplingInterval is 0.\nThis may conduct to an unpredictable behavior.\nPlease specify a non zero minimum sampling interval" ); } let references = options.references || ([] as AddReferenceOpts[]); references = ([] as AddReferenceOpts[]).concat(references, [ { isForward: true, nodeId: typeDefinition, referenceType: "HasTypeDefinition" } ]); assert(!options.nodeClass || options.nodeClass === NodeClass.Variable); options.nodeClass = NodeClass.Variable; options.references = references; const variable = this.createNode(options as CreateNodeOptions) as UAVariable; return variable; } /** * @private */ private _addMethod(options: any): UAMethod { const addressSpace = this.addressSpace; assert(isNonEmptyQualifiedName(options.browseName)); const references: UAReference[] = []; assert(isNonEmptyQualifiedName(options.browseName)); _handle_hierarchy_parent(addressSpace, references, options); UANamespace_process_modelling_rule(references, options.modellingRule); const method = this.internalCreateNode({ browseName: options.browseName, description: options.description || "", displayName: options.displayName, eventNotifier: +options.eventNotifier, isAbstract: false, nodeClass: NodeClass.Method, nodeId: options.nodeId, references, rolePermissions: options.rolePermissions }) as UAMethod; assert(method.nodeId !== null); method.propagate_back_references(); assert(!method.typeDefinition); return method; } } const _constructors_map: Record, typeof BaseNodeImpl> = { DataType: UADataTypeImpl, Method: UAMethodImpl, Object: UAObjectImpl, ObjectType: UAObjectTypeImpl, ReferenceType: UAReferenceTypeImpl, Variable: UAVariableImpl, VariableType: UAVariableTypeImpl, View: UAViewImpl }; /** * convert a 'string' , NodeId or Object into a valid and existing object * @param addressSpace {IAddressSpace} * @param value * @param coerceFunc * @private */ function _coerce_parent( addressSpace: AddressSpacePrivate, value: null | string | BaseNode | undefined | NodeIdLike, coerceFunc: (data: string | NodeId | BaseNode) => BaseNode | null ): BaseNode | null { assert(typeof coerceFunc === "function"); if (value) { if (typeof value === "string") { value = coerceFunc.call(addressSpace, value); } if (value instanceof NodeId) { value = addressSpace.findNode(value) as BaseNode; } } assert(!value || value instanceof BaseNodeImpl); return value as BaseNode; } function _handle_event_hierarchy_parent( addressSpace: AddressSpacePrivate, references: AddReferenceOpts[], options: CreateNodeOptions ) { options.eventSourceOf = _coerce_parent(addressSpace, options.eventSourceOf, addressSpace._coerceNode); options.notifierOf = _coerce_parent(addressSpace, options.notifierOf, addressSpace._coerceNode); if (options.eventSourceOf) { assert(!options.notifierOf, "notifierOf shall not be provided with eventSourceOf "); references.push({ isForward: false, nodeId: options.eventSourceOf.nodeId, referenceType: "HasEventSource" }); options.eventNotifier = options.eventNotifier || 1; } else if (options.notifierOf) { assert(!options.eventSourceOf, "eventSourceOf shall not be provided with notifierOf "); references.push({ isForward: false, nodeId: options.notifierOf.nodeId, referenceType: "HasNotifier" }); } } interface HandleHierarchyParentOptions { addInOf?: NodeIdLike | BaseNode | null | undefined; componentOf?: NodeIdLike | BaseNode | null | undefined; propertyOf?: NodeIdLike | BaseNode | null | undefined; organizedBy?: NodeIdLike | BaseNode | null | undefined; encodingOf?: NodeIdLike | BaseNode | null | undefined; } export function _handle_hierarchy_parent( addressSpace: AddressSpacePrivate, references: AddReferenceOpts[], options: HandleHierarchyParentOptions ): void { options.addInOf = _coerce_parent(addressSpace, options.addInOf, addressSpace._coerceNode); options.componentOf = _coerce_parent(addressSpace, options.componentOf, addressSpace._coerceNode); options.propertyOf = _coerce_parent(addressSpace, options.propertyOf, addressSpace._coerceNode); options.organizedBy = _coerce_parent(addressSpace, options.organizedBy, addressSpace._coerceFolder); options.encodingOf = _coerce_parent(addressSpace, options.encodingOf, addressSpace._coerceNode); if (options.addInOf) { assert(!options.componentOf); assert(!options.propertyOf); assert(!options.organizedBy); assert( options.addInOf.nodeClass === NodeClass.Object || options.addInOf.nodeClass === NodeClass.ObjectType, "addInOf must be of nodeClass Object or ObjectType" ); references.push({ isForward: false, nodeId: options.addInOf.nodeId, referenceType: "HasAddIn" }); } if (options.componentOf) { assert(!options.addInOf); assert(!options.propertyOf); assert(!options.organizedBy); assert(addressSpace.rootFolder.objects, "addressSpace must have a rootFolder.objects folder"); assert( options.componentOf.nodeId !== addressSpace.rootFolder.objects.nodeId, "Only Organizes References are used to relate Objects to the 'Objects' standard Object." ); references.push({ isForward: false, nodeId: options.componentOf.nodeId, referenceType: "HasComponent" }); } if (options.propertyOf) { assert(!options.addInOf); assert(!options.componentOf); assert(!options.organizedBy); assert( options.propertyOf.nodeId !== addressSpace.rootFolder.objects.nodeId, "Only Organizes References are used to relate Objects to the 'Objects' standard Object." ); references.push({ isForward: false, nodeId: options.propertyOf.nodeId, referenceType: "HasProperty" }); } if (options.organizedBy) { assert(!options.addInOf); assert(!options.propertyOf); assert(!options.componentOf); references.push({ isForward: false, nodeId: options.organizedBy.nodeId, referenceType: "Organizes" }); } if (options.encodingOf) { // parent must be a DataType assert(options.encodingOf.nodeClass === NodeClass.DataType, "encodingOf must be toward a DataType"); references.push({ isForward: false, nodeId: options.encodingOf.nodeId, referenceType: "HasEncoding" }); } } function _copy_reference(reference: UAReference | AddReferenceOpts): AddReferenceOpts { assert(Object.hasOwn(reference, "referenceType")); assert(Object.hasOwn(reference, "isForward")); assert(Object.hasOwn(reference, "nodeId")); assert(reference.nodeId instanceof NodeId); return { isForward: reference.isForward, nodeId: reference.nodeId, referenceType: reference.referenceType }; } function _copy_references(references?: UAReference[] | AddReferenceOpts[] | null): AddReferenceOpts[] { references = references || []; return references.map(_copy_reference); } export function isNonEmptyQualifiedName(browseName: QualifiedNameLike): boolean { if (!browseName) { return false; } if (typeof browseName === "string") { return browseName.length >= 0; } if (!(browseName instanceof QualifiedName)) { browseName = new QualifiedName(browseName); } assert(browseName instanceof QualifiedName); return (browseName.name?.length || 0) > 0; } function _create_node_version_if_needed(node: BaseNode, options: { nodeVersion?: string }) { assert(options); if (typeof options.nodeVersion === "string") { assert(node.nodeClass === NodeClass.Variable || node.nodeClass === NodeClass.Object); // c8 ignore next if (node.getNodeVersion()) { return; // already exists } const namespace = node.namespace; const nodeVersion = namespace.addVariable({ browseName: coerceQualifiedName({ name: "NodeVersion", namespaceIndex: 0 }), dataType: DataType.String, propertyOf: node }); const initialValue = typeof options.nodeVersion === "string" ? options.nodeVersion : "0"; nodeVersion.setValueFromSource({ dataType: "String", value: initialValue }); } }