UNPKG

node-opcua-client

Version:

pure nodejs OPCUA SDK - module client

408 lines (366 loc) 16.5 kB
/** * @module node-opcua-client-private */ // tslint:disable:unified-signatures // tslint:disable:no-empty import { EventEmitter } from "events"; import { assert } from "node-opcua-assert"; import { AttributeIds } from "node-opcua-data-model"; import { coerceTimestampsToReturn, type DataValue } from "node-opcua-data-value"; import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug"; import type { ExtensionObject } from "node-opcua-extension-object"; import { EventFilter } from "node-opcua-service-filter"; import { ReadValueId, type ReadValueIdOptions, type TimestampsToReturn } from "node-opcua-service-read"; import { type MonitoredItemCreateResult, type MonitoredItemModifyResult, MonitoringMode, MonitoringParameters, type MonitoringParametersOptions } from "node-opcua-service-subscription"; import { type Callback, type ErrorCallback, type StatusCode, StatusCodes } from "node-opcua-status-code"; import type { Variant } from "node-opcua-variant"; import { ClientMonitoredItem } from "../client_monitored_item"; import { type ClientMonitoredItemBaseEx, ClientMonitoredItemToolbox } from "../client_monitored_item_toolbox"; import type { ClientSubscription } from "../client_subscription"; import { ClientMonitoredItem_create, type ClientSubscriptionImpl } from "./client_subscription_impl"; const debugLog = make_debugLog(__filename); const warningLog = make_warningLog(__filename); const doDebug = checkDebugFlag(__filename); export type PrepareForMonitoringResult = | { error: string } | { error?: null; itemToMonitor: ReadValueIdOptions; monitoringMode: MonitoringMode; requestedParameters: MonitoringParameters; }; /** * ClientMonitoredItem * @class ClientMonitoredItem * @extends ClientMonitoredItemBase * * - event: * - "initialized" * - "err" * - "changed" * * note: this.monitoringMode = subscription_service.MonitoringMode.Reporting; */ export class ClientMonitoredItemImpl extends EventEmitter implements ClientMonitoredItem, ClientMonitoredItemBaseEx { public itemToMonitor: ReadValueId; public monitoringParameters: MonitoringParameters; public subscription: ClientSubscriptionImpl; public monitoringMode: MonitoringMode; public statusCode: StatusCode; public monitoredItemId?: any; public result?: MonitoredItemCreateResult; public filterResult?: ExtensionObject; public timestampsToReturn: TimestampsToReturn; #pendingDataValue?: DataValue[]; #pendingEvents?: Variant[][]; public internalSetMonitoringMode(monitoringMode: MonitoringMode): void { this.monitoringMode = monitoringMode; } constructor( subscription: ClientSubscription, itemToMonitor: ReadValueIdOptions, monitoringParameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn, monitoringMode: MonitoringMode = MonitoringMode.Reporting ) { super(); this.statusCode = StatusCodes.BadDataUnavailable; this.subscription = subscription as ClientSubscriptionImpl; this.itemToMonitor = new ReadValueId(itemToMonitor); this.monitoringParameters = new MonitoringParameters(monitoringParameters); this.monitoringMode = monitoringMode; assert(this.monitoringParameters.clientHandle === 0xffffffff, "should not have a client handle yet"); assert(subscription.session, "expecting session"); timestampsToReturn = coerceTimestampsToReturn(timestampsToReturn); this.timestampsToReturn = timestampsToReturn; } public toString(): string { let ret = ""; ret += "itemToMonitor: " + this.itemToMonitor.toString() + "\n"; ret += "monitoringParameters: " + this.monitoringParameters.toString() + "\n"; ret += "timestampsToReturn: " + this.timestampsToReturn.toString() + "\n"; ret += "itemToMonitor " + this.itemToMonitor.nodeId + "\n"; ret += "statusCode " + this.statusCode?.toString() + "\n"; ret += "result =" + this.result?.toString() + "\n"; return ret; } /** * terminate the monitored item by removing the MonitoredItem from its subscription */ public async terminate(): Promise<void>; public terminate(done: ErrorCallback): void; public terminate(...args: any[]): any { const done = args[0]; assert(typeof done === "function"); const subscription = this.subscription as ClientSubscriptionImpl; subscription._delete_monitored_items([this], (err?: Error) => { if (done) { done(err); } }); } public async modify(parameters: MonitoringParametersOptions): Promise<StatusCode>; public async modify(parameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn): Promise<StatusCode>; public modify(parameters: MonitoringParametersOptions, callback: (err: Error | null, statusCode?: StatusCode) => void): void; public modify( parameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn | null, callback: (err: Error | null, statusCode?: StatusCode) => void ): void; public modify(...args: any[]): any { if (args.length === 2) { return this.modify(args[0], null, args[1]); } const parameters = args[0] as MonitoringParametersOptions; const timestampsToReturn = args[1] as TimestampsToReturn; const callback = args[2]; this.timestampsToReturn = timestampsToReturn || this.timestampsToReturn; ClientMonitoredItemToolbox._toolbox_modify( this.subscription, [this], parameters, this.timestampsToReturn, (err: Error | null, results?: MonitoredItemModifyResult[]) => { if (err) { return callback(err); } if (!results) { return callback(new Error("internal error")); } assert(results!.length === 1); callback(null, results![0]); } ); } public async setMonitoringMode(monitoringMode: MonitoringMode): Promise<StatusCode>; public setMonitoringMode(monitoringMode: MonitoringMode, callback: Callback<StatusCode>): void; public setMonitoringMode(...args: any[]): any { const monitoringMode = args[0] as MonitoringMode; const callback = args[1] as Callback<StatusCode>; ClientMonitoredItemToolbox._toolbox_setMonitoringMode( this.subscription, [this], monitoringMode, (err?: Error | null, statusCodes?: StatusCode[]) => { callback(err ? err : null, statusCodes ? statusCodes[0] : undefined); } ); } /** * @internal * @param value * @private */ public _notify_value_change(value: DataValue): void { // it is possible that the first notification arrives before the CreateMonitoredItemsRequest is fully proceed // in this case we need to put the dataValue aside so we can send the notification changed after // the node-opcua client had time to fully install the on("changed") event handler if (this.statusCode?.value === StatusCodes.BadDataUnavailable.value) { this.#pendingDataValue = this.#pendingDataValue || []; this.#pendingDataValue.push(value); return; } /** * Notify the observers that the MonitoredItem value has changed on the server side. * @event changed * @param value */ try { this.emit("changed", value); } catch (err) { warningLog( "[NODE-OPCUA-W28] Exception raised inside the event handler called by ClientMonitoredItem.on('change')", err ); warningLog(" Please verify the application using this node-opcua client"); } } /** * @internal * @param eventFields * @private */ public _notify_event(eventFields: Variant[]): void { if (this.statusCode?.value === StatusCodes.BadDataUnavailable.value) { this.#pendingEvents = this.#pendingEvents || []; this.#pendingEvents.push(eventFields); return; } /** * Notify the observers that the MonitoredItem value has changed on the server side. * @event changed * @param value */ try { this.emit("changed", eventFields); } catch (err) { warningLog( "[NODE-OPCUA-W29] Exception raised inside the event handler called by ClientMonitoredItem.on('change')", err ); warningLog(" Please verify the application using this node-opcua client"); } } /** * @internal * @private */ public _prepare_for_monitoring(): PrepareForMonitoringResult { assert(this.monitoringParameters.clientHandle === 4294967295, "should not have a client handle yet"); const subscription = this.subscription as ClientSubscriptionImpl; this.monitoringParameters.clientHandle = subscription._nextClientHandle(); assert(this.monitoringParameters.clientHandle > 0 && this.monitoringParameters.clientHandle !== 4294967295); // If attributeId is EventNotifier then monitoring parameters need a filter. // The filter must then either be DataChangeFilter, EventFilter or AggregateFilter. // todo can be done in another way? // todo implement AggregateFilter // todo support DataChangeFilter // todo support whereClause if (this.itemToMonitor.attributeId === AttributeIds.EventNotifier) { // // see OPCUA Spec 1.02 part 4 page 65 : 5.12.1.4 Filter // see part 4 page 130: 7.16.3 EventFilter // part 3 page 11 : 4.6 Event Model // To monitor for Events, the attributeId element of the ReadValueId structure is the // the id of the EventNotifierAttribute // OPC Unified Architecture 1.02, Part 4 5.12.1.2 Sampling interval page 64: // "A Client shall define a sampling interval of 0 if it subscribes for Events." // toDO // note : the EventFilter is used when monitoring Events. this.monitoringParameters.filter = this.monitoringParameters.filter! || new EventFilter({}); const filter = this.monitoringParameters.filter; // c8 ignore next if (!filter) { return { error: "Internal Error" }; } if (filter.schema.name !== "EventFilter") { return { error: "Mismatch between attributeId and filter in monitoring parameters : " + "Got a " + filter.schema.name + " but a EventFilter object is required " + "when itemToMonitor.attributeId== AttributeIds.EventNotifier" }; } } else if (this.itemToMonitor.attributeId === AttributeIds.Value) { // the DataChangeFilter and the AggregateFilter are used when monitoring Variable Values // The Value Attribute is used when monitoring Variables. Variable values are monitored for a change // in value or a change in their status. The filters defined in this standard (see 7.16.2) and in Part 8 are // used to determine if the value change is large enough to cause a Notification to be generated for the // to do : check 'DataChangeFilter' && 'AggregateFilter' } else { if (this.monitoringParameters.filter) { return { error: "Mismatch between attributeId and filter in monitoring parameters : " + "no filter expected when attributeId is not Value or EventNotifier" }; } } return { itemToMonitor: this.itemToMonitor, monitoringMode: this.monitoringMode, requestedParameters: this.monitoringParameters }; } /** * @internal * @param monitoredItemResult * @private */ public _applyResult(monitoredItemResult: MonitoredItemCreateResult): void { this.statusCode = monitoredItemResult.statusCode; if (monitoredItemResult.statusCode.isGood()) { this.result = monitoredItemResult; this.monitoredItemId = monitoredItemResult.monitoredItemId; this.monitoringParameters.samplingInterval = monitoredItemResult.revisedSamplingInterval; this.monitoringParameters.queueSize = monitoredItemResult.revisedQueueSize; this.filterResult = monitoredItemResult.filterResult || undefined; } // some PublishRequest with DataNotificationChange might have been sent by the server, before the monitored // item has been fully initialized it is time to process now any pending notification that were put on hold. if (this.#pendingDataValue) { const dataValues = this.#pendingDataValue; this.#pendingDataValue = undefined; setImmediate(() => { dataValues.map((dataValue) => this._notify_value_change(dataValue)); }); } if (this.#pendingEvents) { const events = this.#pendingEvents; this.#pendingEvents = undefined; setImmediate(() => { events.map((event) => this._notify_event(event)); }); } } public _before_create(): void { const subscription = this.subscription as ClientSubscriptionImpl; subscription._add_monitored_item(this.monitoringParameters.clientHandle, this); } /** * @internal * @param monitoredItemResult * @private */ public _after_create(monitoredItemResult: MonitoredItemCreateResult): void { this._applyResult(monitoredItemResult); if (this.statusCode.isGood()) { /** * Notify the observers that the monitored item is now fully initialized. * @event initialized */ this.emit("initialized"); } else { /** * Notify the observers that the monitored item has failed to initialize. * @event err * @param statusCode {StatusCode} */ const err = new Error(monitoredItemResult.statusCode.toString()); this._terminate_and_emit(err); } } public _terminate_and_emit(err?: Error): void { if ((this as any)._terminated) { return; // already terminated } if (err) { this.emit("err", err.message); } assert(!(this as any)._terminated); (this as any)._terminated = true; /** * Notify the observer that this monitored item has been terminated. * @event terminated */ this.emit("terminated", err); this.removeAllListeners(); // also remove from subscription const clientHandle = this.monitoringParameters.clientHandle; this.subscription._removeMonitoredItem(clientHandle); } } // tslint:disable:no-var-requires // tslint:disable:max-line-length import { withCallback } from "thenify-ex"; const opts = { multiArgs: false }; ClientMonitoredItemImpl.prototype.terminate = withCallback(ClientMonitoredItemImpl.prototype.terminate); ClientMonitoredItemImpl.prototype.setMonitoringMode = withCallback(ClientMonitoredItemImpl.prototype.setMonitoringMode); ClientMonitoredItemImpl.prototype.modify = withCallback(ClientMonitoredItemImpl.prototype.modify); ClientMonitoredItem.create = ( subscription: ClientSubscription, itemToMonitor: ReadValueIdOptions, monitoringParameters: MonitoringParametersOptions, timestampsToReturn: TimestampsToReturn, monitoringMode: MonitoringMode = MonitoringMode.Reporting ): ClientMonitoredItem => { return ClientMonitoredItem_create(subscription, itemToMonitor, monitoringParameters, timestampsToReturn, monitoringMode); };