/** * @module node-opcua-address-space */ import chalk from "chalk"; import { type BaseNode, type CloneExtraInfo, type CloneFilter, type CloneOptions, defaultCloneFilter, EventNotifierFlags, type EventTypeLike, type IEventData, type ISessionContext, type ListenerSignature, makeDefaultCloneExtraInfo, type RaiseEventData, type UAEventType, type UAMethod, type UAObject, type UAObjectEvents, type UAObjectType } from "node-opcua-address-space-base"; import { assert } from "node-opcua-assert"; import { isValidByte } from "node-opcua-basic-types"; import { AttributeIds, NodeClass, type QualifiedNameLike, type QualifiedNameOptions } from "node-opcua-data-model"; import { DataValue, type DataValueLike } from "node-opcua-data-value"; import { getCurrentClock } from "node-opcua-date-time"; import { make_errorLog } from "node-opcua-debug"; 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 { apply_condition_refresh, type ConditionRefreshCache } from "./apply_condition_refresh"; import { BaseNodeImpl, type InternalBaseNodeOptions } from "./base_node_impl"; import { _clone, ToStringBuilder, UAObject_toString } from "./base_node_private"; const errorLog = make_errorLog(__filename); export class UAObjectImpl = UAObjectEvents> 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 string, 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: BaseNode[] = []; // walk up the hasNotify / hasEventSource chain const m: Record = {}; // 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(); if (!obj) { continue; } // 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;