/** * @module node-opcua-address-space */ import chalk from "chalk"; import { assert } from "node-opcua-assert"; import { isValidByte } from "node-opcua-basic-types"; import { NodeClass, type QualifiedNameLike, type QualifiedNameOptions } from "node-opcua-data-model"; import { AttributeIds } from "node-opcua-data-model"; import { DataValue, type DataValueLike } from "node-opcua-data-value"; import { getCurrentClock } from "node-opcua-date-time"; import { NodeId } from "node-opcua-nodeid"; import type { NumericRange } from "node-opcua-numeric-range"; import { StatusCodes } from "node-opcua-status-code"; import { DataType } from "node-opcua-variant"; import { type EventTypeLike, type RaiseEventData, type ISessionContext, type UAMethod, type UAObject, type UAObjectType, type CloneOptions, type CloneFilter, type CloneExtraInfo, type BaseNode, type UAEventType, type IEventData, defaultCloneFilter, makeDefaultCloneExtraInfo, EventNotifierFlags } from "node-opcua-address-space-base"; import { make_errorLog } from "node-opcua-debug"; import { BaseNodeImpl, type InternalBaseNodeOptions } from "./base_node_impl"; import { _clone, ToStringBuilder, UAObject_toString } from "./base_node_private"; import { apply_condition_refresh, type ConditionRefreshCache } from "./apply_condition_refresh"; const errorLog = make_errorLog(__filename); export class UAObjectImpl extends BaseNodeImpl implements UAObject { private _eventNotifier: EventNotifierFlags; public readonly nodeClass = NodeClass.Object; public get eventNotifier(): EventNotifierFlags { // ensure eventNotifier is set if the node has some event if (!this._eventNotifier) { // const s = this.getEventSources(); const s = this.getEventSources(); const n = this.getNotifiers(); if (s.length > 0 || n.length > 0) { this._eventNotifier = this._eventNotifier | EventNotifierFlags.SubscribeToEvents; } } return this._eventNotifier; } public readonly symbolicName: string | null; get typeDefinitionObj(): UAObjectType { // c8 ignore next if (super.typeDefinitionObj.nodeClass !== NodeClass.ObjectType) { const msg = `Invalid type definition node class , expecting a ObjectType got ${ NodeClass[super.typeDefinitionObj.nodeClass] }\n${this.browseName.toString()} ${this.nodeId.toString()}`; errorLog(msg); throw new Error(msg); } return super.typeDefinitionObj as UAObjectType; } constructor(options: InternalBaseNodeOptions & { eventNotifier?: number; symbolicName?: string | null }) { super(options); this._eventNotifier = options.eventNotifier || EventNotifierFlags.None; assert(typeof this.eventNotifier === "number" && isValidByte(this.eventNotifier)); this.symbolicName = options.symbolicName || null; } public readAttribute( context: ISessionContext | null, attributeId: AttributeIds, indexRange?: NumericRange, dataEncoding?: QualifiedNameLike | null ): DataValue { indexRange; dataEncoding; const now = getCurrentClock(); const options: DataValueLike = {}; switch (attributeId) { case AttributeIds.EventNotifier: assert(isValidByte(this.eventNotifier)); options.value = { dataType: DataType.Byte, value: this.eventNotifier }; options.serverTimestamp = now.timestamp; options.serverPicoseconds = now.picoseconds; options.statusCode = StatusCodes.Good; break; default: return BaseNodeImpl.prototype.readAttribute.call(this, context, attributeId, indexRange, dataEncoding); } return new DataValue(options); } public clone(options: CloneOptions, optionalFilter?: CloneFilter, extraInfo?: CloneExtraInfo): UAObject { options = { eventNotifier: this.eventNotifier, symbolicName: this.symbolicName || undefined, ...options, }; const cloneObject = _clone( this, UAObjectImpl, options, optionalFilter || defaultCloneFilter, extraInfo || makeDefaultCloneExtraInfo(this) ) as UAObject; // xx newObject.propagate_back_references(); // xx newObject.install_extra_properties(); return cloneObject; } /** * returns true if the object has some opcua methods */ public get hasMethods(): boolean { return this.getMethods().length > 0; } public getMethodByName(methodName: QualifiedNameOptions): UAMethod | null; public getMethodByName(methodName: string, namespaceIndex?: number): UAMethod | null; public getMethodByName(methodName: QualifiedNameLike, namespaceIndex?: number): UAMethod | null { return super.getMethodByName(methodName as any, namespaceIndex); } public getMethods(): UAMethod[] { return super.getMethods(); } public setEventNotifier(eventNotifierFlags: EventNotifierFlags): void { this._eventNotifier = eventNotifierFlags; } /** * Raise a transient Event */ public raiseEvent(eventType: EventTypeLike | BaseNode, data: RaiseEventData): void { const addressSpace = this.addressSpace; if (typeof eventType === "string") { const eventTypeFound = addressSpace.findEventType(eventType); if (!eventTypeFound) { throw new Error("raiseEvent: eventType cannot find event Type " + eventType.toString()); } eventType = eventTypeFound; if (!eventType || eventType.nodeClass !== NodeClass.ObjectType) { throw new Error("eventType must exist and be an UAObjectType"); } } else if (eventType instanceof NodeId) { const eventTypeFound = addressSpace.findNode(eventType) as BaseNode; if (!eventTypeFound) { throw new Error(`raiseEvent: eventType cannot find event Type ${eventType.toString()}`); } eventType = eventTypeFound!; if (!eventType || eventType.nodeClass !== NodeClass.ObjectType) { throw new Error(`eventType must exist and be an UAObjectType: ${eventType.toString()}`); } } eventType = eventType as UAEventType; let eventTypeNode: UAEventType = eventType as UAEventType; // c8 ignore next if (!eventTypeNode) { throw new Error(`UAObject#raiseEventType : Cannot find event type : ${eventType.toString()}`); } // coerce EventType eventTypeNode = addressSpace.findEventType(eventType as UAEventType) as UAEventType; const _baseEventType = addressSpace.findEventType("BaseEventType"); data.$eventDataSource =eventTypeNode; data.sourceNode = data.sourceNode || { dataType: DataType.NodeId, value: this.nodeId }; const eventData1 = addressSpace.constructEventData(eventTypeNode, data); this._bubble_up_event(eventData1); } public _bubble_up_event(eventData: IEventData): void { const addressSpace = this.addressSpace; const queue: any[] = []; // walk up the hasNotify / hasEventSource chain const m: any = {}; // all events are notified to the server object // emit on server object const server = addressSpace.findNode("Server") as UAObject; if (server) { assert(server.eventNotifier > 0x00, "Server must be an event notifier"); server.emit("event", eventData); m[server.nodeId.toString()] = server; } else { errorLog( chalk.yellow("Warning. ") + chalk.cyan("UAObject#raiseEvent") + chalk.red(" cannot find Server object on addressSpace") ); } addInQueue(this); function addInQueue(obj: BaseNode) { const key: string = obj.nodeId.toString(); if (!m[key]) { m[key] = obj; queue.push(obj); } } while (queue.length) { const obj = queue.pop(); // emit on object obj.emit("event", eventData); const elements1 = obj.findReferencesAsObject("HasNotifier", false); elements1.forEach(addInQueue); const elements2 = obj.findReferencesAsObject("HasEventSource", false); elements2.forEach(addInQueue); } } public _conditionRefresh(_cache?: ConditionRefreshCache): void { apply_condition_refresh.call(this, _cache); } public toString(): string { const options = new ToStringBuilder(); UAObject_toString.call(this, options); return options.toString(); } public toJSON(): Record { return { ...super.toJSON(), typeDefinition: this.typeDefinitionObj ? this.typeDefinitionObj.browseName.toString() : undefined, eventNotifier: this._eventNotifier }; } public [Symbol.for("nodejs.util.inspect.custom")](depth: number | null, inspectOptions: { colors?: boolean }): string { const c = inspectOptions?.colors === false ? plainChalk : chalk; const typeName = this.typeDefinitionObj?.browseName.toString() ?? "?"; const displayName = this.displayName.length ? this.displayName.map((d) => d.text).join(" | ") : ""; if (depth !== null && depth <= 0) { return `${c.cyan("UAObject<")}${c.green(this.browseName.toString())}${c.grey(` ${this.nodeId.toString()}`)}${c.cyan(">")}`; } const lines = [ `${c.cyan("UAObject ")}${c.green(this.browseName.toString())}${c.grey(` ${this.nodeId.toString()}`)}`, c.yellow(" typeDefinition : ") + typeName, c.yellow(" displayName : ") + displayName, c.yellow(" eventNotifier : ") + this._eventNotifier ]; if (this.description?.text) { lines.push(c.yellow(" description : ") + this.description.text); } return lines.join("\n"); } } const plainChalk = new Proxy(chalk, { get: () => (s: string) => s }) as typeof chalk;