UNPKG

node-opcua-server

Version:

pure nodejs OPCUA SDK - module server

1,053 lines (1,052 loc) 65.3 kB
"use strict"; /** * @module node-opcua-server */ // tslint:disable:no-console var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Subscription = exports.SubscriptionState = void 0; const events_1 = require("events"); const chalk_1 = __importDefault(require("chalk")); const node_opcua_address_space_1 = require("node-opcua-address-space"); const node_opcua_assert_1 = require("node-opcua-assert"); const node_opcua_common_1 = require("node-opcua-common"); const node_opcua_data_model_1 = require("node-opcua-data-model"); const node_opcua_debug_1 = require("node-opcua-debug"); const node_opcua_nodeid_1 = require("node-opcua-nodeid"); const node_opcua_object_registry_1 = require("node-opcua-object-registry"); const node_opcua_secure_channel_1 = require("node-opcua-secure-channel"); const node_opcua_service_filter_1 = require("node-opcua-service-filter"); const node_opcua_service_subscription_1 = require("node-opcua-service-subscription"); const node_opcua_service_subscription_2 = require("node-opcua-service-subscription"); const node_opcua_status_code_1 = require("node-opcua-status-code"); const node_opcua_types_1 = require("node-opcua-types"); const queue_1 = require("./queue"); const monitored_item_1 = require("./monitored_item"); const validate_filter_1 = require("./validate_filter"); const i_server_side_publish_engine_1 = require("./i_server_side_publish_engine"); const debugLog = (0, node_opcua_debug_1.make_debugLog)(__filename); const doDebug = (0, node_opcua_debug_1.checkDebugFlag)(__filename); const warningLog = (0, node_opcua_debug_1.make_warningLog)(__filename); const maxNotificationMessagesInQueue = 100; var SubscriptionState; (function (SubscriptionState) { SubscriptionState[SubscriptionState["CLOSED"] = 1] = "CLOSED"; SubscriptionState[SubscriptionState["CREATING"] = 2] = "CREATING"; SubscriptionState[SubscriptionState["NORMAL"] = 3] = "NORMAL"; // The keep-alive counter is not used in this state. SubscriptionState[SubscriptionState["LATE"] = 4] = "LATE"; // ready to be sent, but there are no Publish requests queued. When in this state, the next Publish // request is processed when it is received. The keep-alive counter is not used in this state. SubscriptionState[SubscriptionState["KEEPALIVE"] = 5] = "KEEPALIVE"; // alive counter to count down to 0 from its maximum. SubscriptionState[SubscriptionState["TERMINATED"] = 6] = "TERMINATED"; })(SubscriptionState || (exports.SubscriptionState = SubscriptionState = {})); function _adjust_publishing_interval(publishingInterval) { publishingInterval = publishingInterval === undefined || Number.isNaN(publishingInterval) ? Subscription.defaultPublishingInterval : publishingInterval; publishingInterval = Math.max(publishingInterval, Subscription.minimumPublishingInterval); publishingInterval = Math.min(publishingInterval, Subscription.maximumPublishingInterval); return publishingInterval; } const minimumMaxKeepAliveCount = 2; const maximumMaxKeepAliveCount = 12000; function _adjust_maxKeepAliveCount(maxKeepAliveCount /*,publishingInterval*/) { maxKeepAliveCount = maxKeepAliveCount || minimumMaxKeepAliveCount; maxKeepAliveCount = Math.max(maxKeepAliveCount, minimumMaxKeepAliveCount); maxKeepAliveCount = Math.min(maxKeepAliveCount, maximumMaxKeepAliveCount); return maxKeepAliveCount; } const MaxUint32 = 0xffffffff; function _adjust_lifeTimeCount(lifeTimeCount, maxKeepAliveCount, publishingInterval) { lifeTimeCount = lifeTimeCount || 1; const minTicks = Math.ceil(Subscription.minimumLifetimeDuration / publishingInterval); const maxTicks = Math.floor(Subscription.maximumLifetimeDuration / publishingInterval); lifeTimeCount = Math.max(minTicks, lifeTimeCount); lifeTimeCount = Math.min(maxTicks, lifeTimeCount); // let's make sure that lifeTimeCount is at least three time maxKeepAliveCount // Note : the specs say ( part 3 - CreateSubscriptionParameter ) // "The lifetime count shall be a minimum of three times the keep keep-alive count." lifeTimeCount = Math.max(lifeTimeCount, Math.min(maxKeepAliveCount * 3, MaxUint32)); return lifeTimeCount; } function _adjust_publishingEnable(publishingEnabled) { return publishingEnabled === null || publishingEnabled === undefined ? true : !!publishingEnabled; } function _adjust_maxNotificationsPerPublish(maxNotificationsPerPublish) { (0, node_opcua_assert_1.assert)(Subscription.maxNotificationPerPublishHighLimit > 0, "Subscription.maxNotificationPerPublishHighLimit must be positive"); maxNotificationsPerPublish = maxNotificationsPerPublish || 0; (0, node_opcua_assert_1.assert)(typeof maxNotificationsPerPublish === "number"); // must be strictly positive maxNotificationsPerPublish = maxNotificationsPerPublish >= 0 ? maxNotificationsPerPublish : 0; if (maxNotificationsPerPublish === 0) { // if zero then => use our HighLimit maxNotificationsPerPublish = Subscription.maxNotificationPerPublishHighLimit; } else { // if not zero then should be capped by maxNotificationPerPublishHighLimit maxNotificationsPerPublish = Math.min(Subscription.maxNotificationPerPublishHighLimit, maxNotificationsPerPublish); } (0, node_opcua_assert_1.assert)(maxNotificationsPerPublish !== 0 && maxNotificationsPerPublish <= Subscription.maxNotificationPerPublishHighLimit); return maxNotificationsPerPublish; } function w(s, length) { return ("000" + s).padStart(length); } function t(d) { return w(d.getHours(), 2) + ":" + w(d.getMinutes(), 2) + ":" + w(d.getSeconds(), 2) + ":" + w(d.getMilliseconds(), 3); } function _getSequenceNumbers(arr) { return arr.map((notificationMessage) => notificationMessage.sequenceNumber); } function analyzeEventFilterResult(node, eventFilter) { /* istanbul ignore next */ if (!(eventFilter instanceof node_opcua_service_filter_1.EventFilter)) { throw new Error("Internal Error"); } const selectClauseResults = (0, node_opcua_service_filter_1.checkSelectClauses)(node, eventFilter.selectClauses || []); const whereClauseResult = new node_opcua_types_1.ContentFilterResult(); return new node_opcua_types_1.EventFilterResult({ selectClauseDiagnosticInfos: [], selectClauseResults, whereClauseResult }); } function analyzeDataChangeFilterResult(node, dataChangeFilter) { (0, node_opcua_assert_1.assert)(dataChangeFilter instanceof node_opcua_service_subscription_2.DataChangeFilter); // the opcua specification doesn't provide dataChangeFilterResult return null; } function analyzeAggregateFilterResult(node, aggregateFilter) { (0, node_opcua_assert_1.assert)(aggregateFilter instanceof node_opcua_service_subscription_1.AggregateFilter); return new node_opcua_types_1.AggregateFilterResult({}); } function _process_filter(node, filter) { if (!filter) { return null; } if (filter instanceof node_opcua_service_filter_1.EventFilter) { return analyzeEventFilterResult(node, filter); } else if (filter instanceof node_opcua_service_subscription_2.DataChangeFilter) { return analyzeDataChangeFilterResult(node, filter); } else if (filter instanceof node_opcua_service_subscription_1.AggregateFilter) { return analyzeAggregateFilterResult(node, filter); } // istanbul ignore next throw new Error("invalid filter"); } /** * @private */ function createSubscriptionDiagnostics(subscription) { (0, node_opcua_assert_1.assert)(subscription instanceof Subscription); const subscriptionDiagnostics = new node_opcua_common_1.SubscriptionDiagnosticsDataType({}); const subscription_subscriptionDiagnostics = subscriptionDiagnostics; subscription_subscriptionDiagnostics.$subscription = subscription; // "sessionId" subscription_subscriptionDiagnostics.__defineGetter__("sessionId", function () { if (!this.$subscription) { return node_opcua_nodeid_1.NodeId.nullNodeId; } return this.$subscription.getSessionId(); }); subscription_subscriptionDiagnostics.__defineGetter__("subscriptionId", function () { if (!this.$subscription) { return 0; } return this.$subscription.id; }); subscription_subscriptionDiagnostics.__defineGetter__("priority", function () { if (!this.$subscription) { return 0; } return this.$subscription.priority; }); subscription_subscriptionDiagnostics.__defineGetter__("publishingInterval", function () { if (!this.$subscription) { return 0; } return this.$subscription.publishingInterval; }); subscription_subscriptionDiagnostics.__defineGetter__("maxLifetimeCount", function () { return this.$subscription.lifeTimeCount; }); subscription_subscriptionDiagnostics.__defineGetter__("maxKeepAliveCount", function () { if (!this.$subscription) { return 0; } return this.$subscription.maxKeepAliveCount; }); subscription_subscriptionDiagnostics.__defineGetter__("maxNotificationsPerPublish", function () { if (!this.$subscription) { return 0; } return this.$subscription.maxNotificationsPerPublish; }); subscription_subscriptionDiagnostics.__defineGetter__("publishingEnabled", function () { if (!this.$subscription) { return false; } return this.$subscription.publishingEnabled; }); subscription_subscriptionDiagnostics.__defineGetter__("monitoredItemCount", function () { if (!this.$subscription) { return 0; } return this.$subscription.monitoredItemCount; }); subscription_subscriptionDiagnostics.__defineGetter__("nextSequenceNumber", function () { if (!this.$subscription) { return 0; } return this.$subscription.futureSequenceNumber; }); subscription_subscriptionDiagnostics.__defineGetter__("disabledMonitoredItemCount", function () { if (!this.$subscription) { return 0; } return this.$subscription.disabledMonitoredItemCount; }); /* those member of self.subscriptionDiagnostics are handled directly modifyCount enableCount, disableCount, republishRequestCount, notificationsCount, publishRequestCount, dataChangeNotificationsCount, eventNotificationsCount, */ /* those members are not updated yet in the code : "republishMessageRequestCount", "republishMessageCount", "transferRequestCount", "transferredToAltClientCount", "transferredToSameClientCount", "latePublishRequestCount", "unacknowledgedMessageCount", "discardedMessageCount", "monitoringQueueOverflowCount", "eventQueueOverflowCount" */ subscription_subscriptionDiagnostics.__defineGetter__("currentKeepAliveCount", function () { if (!this.$subscription) { return 0; } return this.$subscription.currentKeepAliveCount; }); subscription_subscriptionDiagnostics.__defineGetter__("currentLifetimeCount", function () { if (!this.$subscription) { return 0; } return this.$subscription.currentLifetimeCount; }); // add object in Variable SubscriptionDiagnosticArray (i=2290) ( Array of SubscriptionDiagnostics) // add properties in Variable to reflect return subscriptionDiagnostics; } let g_monitoredItemId = Math.ceil(Math.random() * 100000); function getNextMonitoredItemId() { return g_monitoredItemId++; } // function myFilter<T>(t1: any, chunk: any[]): T[] { // return chunk.filter(filter_instanceof.bind(null, t1)); // } // function makeNotificationData(notifications_chunk: QueueItem): NotificationData { // const dataChangedNotificationData = myFilter<MonitoredItemNotification>(MonitoredItemNotification, notifications_chunk); // const eventNotificationListData = myFilter<EventFieldList>(EventFieldList, notifications_chunk); // assert(notifications_chunk.length === dataChangedNotificationData.length + eventNotificationListData.length); // const notifications: (DataChangeNotification | EventNotificationList)[] = []; // // add dataChangeNotification // if (dataChangedNotificationData.length) { // const dataChangeNotification = new DataChangeNotification({ // diagnosticInfos: [], // monitoredItems: dataChangedNotificationData // }); // notifications.push(dataChangeNotification); // } // // add dataChangeNotification // if (eventNotificationListData.length) { // const eventNotificationList = new EventNotificationList({ // events: eventNotificationListData // }); // notifications.push(eventNotificationList); // } // return notifications.length === 0 ? null : notifications; // } const INVALID_ID = -1; /** * The Subscription class used in the OPCUA server side. */ class Subscription extends events_1.EventEmitter { /** * @deprecated use serverCapacity.maxMonitoredItems and serverCapacity.maxMonitoredItemsPerSubscription instead */ static get maxMonitoredItemCount() { return Subscription.defaultMaxMonitoredItemCount; } set state(value) { if (this._state !== value) { this._state = value; this.emit("stateChanged", value); } } get state() { return this._state; } get sessionId() { return this.$session ? this.$session.nodeId : node_opcua_nodeid_1.NodeId.nullNodeId; } get currentLifetimeCount() { return this._life_time_counter; } get currentKeepAliveCount() { return this._keep_alive_counter; } constructor(options) { super(); this._state = -1; this._keep_alive_counter = 0; this._hasUncollectedMonitoredItemNotifications = false; options = options || {}; Subscription.registry.register(this); (0, node_opcua_assert_1.assert)(this.sessionId instanceof node_opcua_nodeid_1.NodeId, "expecting a sessionId NodeId"); this.publishEngine = options.publishEngine; this.id = options.id || INVALID_ID; this.priority = options.priority || 0; this.publishingInterval = _adjust_publishing_interval(options.publishingInterval); this.maxKeepAliveCount = _adjust_maxKeepAliveCount(options.maxKeepAliveCount); // , this.publishingInterval); this.resetKeepAliveCounter(); this.lifeTimeCount = _adjust_lifeTimeCount(options.lifeTimeCount || 0, this.maxKeepAliveCount, this.publishingInterval); this.maxNotificationsPerPublish = _adjust_maxNotificationsPerPublish(options.maxNotificationsPerPublish); this._life_time_counter = 0; this.resetLifeTimeCounter(); // notification message that are ready to be sent to the client this._pending_notifications = new queue_1.Queue(); this._sent_notification_messages = []; this._sequence_number_generator = new node_opcua_secure_channel_1.SequenceNumberGenerator(); // initial state of the subscription this.state = SubscriptionState.CREATING; this.publishIntervalCount = 0; this.monitoredItems = {}; // monitored item map this.monitoredItemIdCounter = 0; this.publishingEnabled = _adjust_publishingEnable(options.publishingEnabled); this.subscriptionDiagnostics = createSubscriptionDiagnostics(this); // A boolean value that is set to TRUE to mean that either a NotificationMessage or a keep-alive // Message has been sent on the Subscription. It is a flag that is used to ensure that either a // NotificationMessage or a keep-alive Message is sent out the first time the publishing // timer expires. this.messageSent = false; this.timerId = null; this._start_timer({ firstTime: true }); debugLog(chalk_1.default.green(`creating subscription ${this.id}`)); this.serverCapabilities = options.serverCapabilities; this.serverCapabilities.maxMonitoredItems = this.serverCapabilities.maxMonitoredItems || Subscription.defaultMaxMonitoredItemCount; this.serverCapabilities.maxMonitoredItemsPerSubscription = this.serverCapabilities.maxMonitoredItemsPerSubscription || Subscription.defaultMaxMonitoredItemCount; this.globalCounter = options.globalCounter; } getSessionId() { return this.sessionId; } toString() { let str = "Subscription:\n"; str += " subscriptionId " + this.id + "\n"; str += " sessionId " + this.getSessionId()?.toString() + "\n"; str += " publishingEnabled " + this.publishingEnabled + "\n"; str += " maxKeepAliveCount " + this.maxKeepAliveCount + "\n"; str += " publishingInterval " + this.publishingInterval + "\n"; str += " lifeTimeCount " + this.lifeTimeCount + "\n"; str += " maxKeepAliveCount " + this.maxKeepAliveCount + "\n"; return str; } /** * modify subscription parameters * @param param */ modify(param) { // update diagnostic counter this.subscriptionDiagnostics.modifyCount += 1; const publishingInterval_old = this.publishingInterval; param.requestedPublishingInterval = param.requestedPublishingInterval || 0; param.requestedMaxKeepAliveCount = param.requestedMaxKeepAliveCount || this.maxKeepAliveCount; param.requestedLifetimeCount = param.requestedLifetimeCount || this.lifeTimeCount; this.publishingInterval = _adjust_publishing_interval(param.requestedPublishingInterval); this.maxKeepAliveCount = _adjust_maxKeepAliveCount(param.requestedMaxKeepAliveCount); this.lifeTimeCount = _adjust_lifeTimeCount(param.requestedLifetimeCount, this.maxKeepAliveCount, this.publishingInterval); this.maxNotificationsPerPublish = _adjust_maxNotificationsPerPublish(param.maxNotificationsPerPublish || 0); this.priority = param.priority || 0; this.resetLifeTimeAndKeepAliveCounters(); if (publishingInterval_old !== this.publishingInterval) { // todo } this._stop_timer(); this._start_timer({ firstTime: false }); } /** * set publishing mode * @param publishingEnabled */ setPublishingMode(publishingEnabled) { this.publishingEnabled = !!publishingEnabled; // update diagnostics if (this.publishingEnabled) { this.subscriptionDiagnostics.enableCount += 1; } else { this.subscriptionDiagnostics.disableCount += 1; } this.resetLifeTimeCounter(); if (!publishingEnabled && this.state !== SubscriptionState.CLOSED) { this.state = SubscriptionState.NORMAL; } return node_opcua_status_code_1.StatusCodes.Good; } /** * @private */ get keepAliveCounterHasExpired() { return this._keep_alive_counter >= this.maxKeepAliveCount || this.state === SubscriptionState.LATE; } /** * Reset the Lifetime Counter Variable to the value specified for the lifetime of a Subscription in * the CreateSubscription Service( 5.13.2). * @private */ resetLifeTimeCounter() { this._life_time_counter = 0; } /** * @private */ increaseLifeTimeCounter() { this._life_time_counter += 1; if (this._life_time_counter >= this.lifeTimeCount) { this.emit("lifeTimeExpired"); } this.emit("lifeTimeCounterChanged", this._life_time_counter); } /** * True if the subscription life time has expired. * */ get lifeTimeHasExpired() { (0, node_opcua_assert_1.assert)(this.lifeTimeCount > 0); return this._life_time_counter >= this.lifeTimeCount; } /** * number of milliseconds before this subscription times out (lifeTimeHasExpired === true); */ get timeToExpiration() { return (this.lifeTimeCount - this._life_time_counter) * this.publishingInterval; } get timeToKeepAlive() { return (this.maxKeepAliveCount - this._keep_alive_counter) * this.publishingInterval; } /** * Terminates the subscription. * Calling this method will also remove any monitored items. * */ terminate() { (0, node_opcua_assert_1.assert)(arguments.length === 0); debugLog("Subscription#terminate status", SubscriptionState[this.state]); if (this.state === SubscriptionState.CLOSED) { // todo verify if asserting is required here return; } // stop timer this._stop_timer(); debugLog("terminating Subscription ", this.id, " with ", this.monitoredItemCount, " monitored items"); // dispose all monitoredItem const keys = Object.keys(this.monitoredItems); for (const key of keys) { const status = this.removeMonitoredItem(parseInt(key, 10)); (0, node_opcua_assert_1.assert)(status === node_opcua_status_code_1.StatusCodes.Good); } (0, node_opcua_assert_1.assert)(this.monitoredItemCount === 0); if (this.$session && this.$session._unexposeSubscriptionDiagnostics) { this.$session._unexposeSubscriptionDiagnostics(this); } this.state = SubscriptionState.CLOSED; /** * notify the subscription owner that the subscription has been terminated. * @event "terminated" */ this.emit("terminated"); if (this.publishEngine) { this.publishEngine.on_close_subscription(this); } } setTriggering(triggeringItemId, linksToAdd, linksToRemove) { /** Bad_NothingToDo, Bad_TooManyOperations,Bad_SubscriptionIdInvalid, Bad_MonitoredItemIdInvalid */ linksToAdd = linksToAdd || []; linksToRemove = linksToRemove || []; if (linksToAdd.length === 0 && linksToRemove.length === 0) { return { statusCode: node_opcua_status_code_1.StatusCodes.BadNothingToDo, addResults: [], removeResults: [] }; } const triggeringItem = this.getMonitoredItem(triggeringItemId); const monitoredItemsToAdd = linksToAdd.map((id) => this.getMonitoredItem(id)); const monitoredItemsToRemove = linksToRemove.map((id) => this.getMonitoredItem(id)); if (!triggeringItem) { const removeResults1 = monitoredItemsToRemove.map((m) => m ? node_opcua_status_code_1.StatusCodes.Good : node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid); const addResults1 = monitoredItemsToAdd.map((m) => m ? node_opcua_status_code_1.StatusCodes.Good : node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid); return { statusCode: node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid, addResults: addResults1, removeResults: removeResults1 }; } // // note: it seems that CTT imposed that we do remove before add const removeResults = monitoredItemsToRemove.map((m) => !m ? node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid : triggeringItem.removeLinkItem(m.monitoredItemId)); const addResults = monitoredItemsToAdd.map((m) => !m ? node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid : triggeringItem.addLinkItem(m.monitoredItemId)); const statusCode = node_opcua_status_code_1.StatusCodes.Good; // do binding return { statusCode, addResults, removeResults }; } dispose() { // istanbul ignore next if (doDebug) { debugLog("Subscription#dispose", this.id, this.monitoredItemCount); } (0, node_opcua_assert_1.assert)(this.monitoredItemCount === 0, "MonitoredItems haven't been deleted first !!!"); (0, node_opcua_assert_1.assert)(this.timerId === null, "Subscription timer haven't been terminated"); if (this.subscriptionDiagnostics) { this.subscriptionDiagnostics.$subscription = null; } this.publishEngine = undefined; this._pending_notifications.clear(); this._sent_notification_messages = []; this.$session = undefined; this.removeAllListeners(); Subscription.registry.unregister(this); } get aborted() { const session = this.$session; if (!session) { return true; } return session.aborted; } /** * number of pending notifications */ get pendingNotificationsCount() { return this._pending_notifications ? this._pending_notifications.size : 0; } /** * is 'true' if there are pending notifications for this subscription. (i.e moreNotifications) */ get hasPendingNotifications() { return this.pendingNotificationsCount > 0; } /** * number of sent notifications */ get sentNotificationMessageCount() { return this._sent_notification_messages.length; } /** * @internal */ _flushSentNotifications() { const tmp = this._sent_notification_messages; this._sent_notification_messages = []; return tmp; } /** * number of monitored items handled by this subscription */ get monitoredItemCount() { return Object.keys(this.monitoredItems).length; } /** * number of disabled monitored items. */ get disabledMonitoredItemCount() { return Object.values(this.monitoredItems).reduce((sum, monitoredItem) => { return sum + (monitoredItem.monitoringMode === node_opcua_service_subscription_2.MonitoringMode.Disabled ? 1 : 0); }, 0); } /** * The number of unacknowledged messages saved in the republish queue. */ get unacknowledgedMessageCount() { return this.subscriptionDiagnostics.unacknowledgedMessageCount; } /** * adjust monitored item sampling interval * - an samplingInterval ===0 means that we use a event-base model ( no sampling) * - otherwise the sampling is adjusted * @private */ adjustSamplingInterval(samplingInterval, node) { if (samplingInterval < 0) { // - The value -1 indicates that the default sampling interval defined by the publishing // interval of the Subscription is requested. // - Any negative number is interpreted as -1. samplingInterval = this.publishingInterval; } else if (samplingInterval === 0) { // istanbul ignore next if (!node) throw new Error("Internal Error"); // OPCUA 1.0.3 Part 4 - 5.12.1.2 // The value 0 indicates that the Server should use the fastest practical rate. // The fastest supported sampling interval may be equal to 0, which indicates // that the data item is exception-based rather than being sampled at some period. // An exception-based model means that the underlying system does not require // sampling and reports data changes. const dataValueSamplingInterval = node.readAttribute(node_opcua_address_space_1.SessionContext.defaultContext, node_opcua_data_model_1.AttributeIds.MinimumSamplingInterval); // TODO if attributeId === AttributeIds.Value : sampling interval required here if (dataValueSamplingInterval.statusCode.isGood()) { // node provides a Minimum sampling interval ... samplingInterval = dataValueSamplingInterval.value.value; (0, node_opcua_assert_1.assert)(samplingInterval >= 0 && samplingInterval <= monitored_item_1.MonitoredItem.maximumSamplingInterval); // note : at this stage, a samplingInterval===0 means that the data item is really exception-based } } else if (samplingInterval < monitored_item_1.MonitoredItem.minimumSamplingInterval) { samplingInterval = monitored_item_1.MonitoredItem.minimumSamplingInterval; } else if (samplingInterval > monitored_item_1.MonitoredItem.maximumSamplingInterval) { // If the requested samplingInterval is higher than the // maximum sampling interval supported by the Server, the maximum sampling // interval is returned. samplingInterval = monitored_item_1.MonitoredItem.maximumSamplingInterval; } const node_minimumSamplingInterval = node && node.minimumSamplingInterval ? node.minimumSamplingInterval : 0; samplingInterval = Math.max(samplingInterval, node_minimumSamplingInterval); return samplingInterval; } /** * create a monitored item * @param addressSpace - address space * @param timestampsToReturn - the timestamp to return * @param monitoredItemCreateRequest - the parameters describing the monitored Item to create */ preCreateMonitoredItem(addressSpace, timestampsToReturn, monitoredItemCreateRequest) { (0, node_opcua_assert_1.assert)(monitoredItemCreateRequest instanceof node_opcua_service_subscription_2.MonitoredItemCreateRequest); function handle_error(statusCode) { return { createResult: new node_opcua_service_subscription_2.MonitoredItemCreateResult({ statusCode }), monitoredItemCreateRequest }; } const itemToMonitor = monitoredItemCreateRequest.itemToMonitor; const node = addressSpace.findNode(itemToMonitor.nodeId); if (!node || (node.nodeClass !== node_opcua_data_model_1.NodeClass.Variable && node.nodeClass !== node_opcua_data_model_1.NodeClass.Object && node.nodeClass !== node_opcua_data_model_1.NodeClass.Method)) { return handle_error(node_opcua_status_code_1.StatusCodes.BadNodeIdUnknown); } if (itemToMonitor.attributeId === node_opcua_data_model_1.AttributeIds.Value && !(node.nodeClass === node_opcua_data_model_1.NodeClass.Variable)) { // AttributeIds.Value is only valid for monitoring value of UAVariables. return handle_error(node_opcua_status_code_1.StatusCodes.BadAttributeIdInvalid); } if (itemToMonitor.attributeId === node_opcua_data_model_1.AttributeIds.INVALID) { return handle_error(node_opcua_status_code_1.StatusCodes.BadAttributeIdInvalid); } if (!itemToMonitor.indexRange.isValid()) { return handle_error(node_opcua_status_code_1.StatusCodes.BadIndexRangeInvalid); } // check dataEncoding applies only on Values if (itemToMonitor.dataEncoding.name && itemToMonitor.attributeId !== node_opcua_data_model_1.AttributeIds.Value) { return handle_error(node_opcua_status_code_1.StatusCodes.BadDataEncodingInvalid); } // check dataEncoding if (!(0, node_opcua_data_model_1.isValidDataEncoding)(itemToMonitor.dataEncoding)) { return handle_error(node_opcua_status_code_1.StatusCodes.BadDataEncodingUnsupported); } // check that item can be read by current user session // filter const requestedParameters = monitoredItemCreateRequest.requestedParameters; const filter = requestedParameters.filter; const statusCodeFilter = (0, validate_filter_1.validateFilter)(filter, itemToMonitor, node); if (statusCodeFilter !== node_opcua_status_code_1.StatusCodes.Good) { return handle_error(statusCodeFilter); } // do we have enough room for new monitored items ? if (this.monitoredItemCount >= this.serverCapabilities.maxMonitoredItemsPerSubscription) { return handle_error(node_opcua_status_code_1.StatusCodes.BadTooManyMonitoredItems); } if (this.globalCounter.totalMonitoredItemCount >= this.serverCapabilities.maxMonitoredItems) { return handle_error(node_opcua_status_code_1.StatusCodes.BadTooManyMonitoredItems); } const createResult = this._createMonitoredItemStep2(timestampsToReturn, monitoredItemCreateRequest, node); (0, node_opcua_assert_1.assert)(createResult.statusCode.isGood()); const monitoredItem = this.getMonitoredItem(createResult.monitoredItemId); // istanbul ignore next if (!monitoredItem) { throw new Error("internal error"); } // TODO: fix old way to set node. !!!! monitoredItem.setNode(node); this.emit("monitoredItem", monitoredItem, itemToMonitor); return { monitoredItem, monitoredItemCreateRequest, createResult }; } async applyOnMonitoredItem(functor) { for (const m of Object.values(this.monitoredItems)) { await functor(m); } } postCreateMonitoredItem(monitoredItem, monitoredItemCreateRequest, createResult) { this._createMonitoredItemStep3(monitoredItem, monitoredItemCreateRequest); } async createMonitoredItem(addressSpace, timestampsToReturn, monitoredItemCreateRequest) { const { monitoredItem, createResult } = this.preCreateMonitoredItem(addressSpace, timestampsToReturn, monitoredItemCreateRequest); this.postCreateMonitoredItem(monitoredItem, monitoredItemCreateRequest, createResult); return createResult; } /** * get a monitoredItem by Id. * @param monitoredItemId : the id of the monitored item to get. * @return the monitored item matching monitoredItemId */ getMonitoredItem(monitoredItemId) { return this.monitoredItems[monitoredItemId] || null; } /** * remove a monitored Item from the subscription. * @param monitoredItemId : the id of the monitored item to get. */ removeMonitoredItem(monitoredItemId) { debugLog("Removing monitoredIem ", monitoredItemId); if (!Object.prototype.hasOwnProperty.call(this.monitoredItems, monitoredItemId.toString())) { return node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid; } const monitoredItem = this.monitoredItems[monitoredItemId]; monitoredItem.terminate(); /** * * notify that a monitored item has been removed from the subscription * @param monitoredItem {MonitoredItem} */ this.emit("removeMonitoredItem", monitoredItem); monitoredItem.dispose(); delete this.monitoredItems[monitoredItemId]; this.globalCounter.totalMonitoredItemCount -= 1; this._removePendingNotificationsFor(monitoredItemId); // flush pending notifications // assert(this._pending_notifications.size === 0); return node_opcua_status_code_1.StatusCodes.Good; } /** * rue if monitored Item have uncollected Notifications */ get hasUncollectedMonitoredItemNotifications() { if (this._hasUncollectedMonitoredItemNotifications) { return true; } const keys = Object.keys(this.monitoredItems); const n = keys.length; for (let i = 0; i < n; i++) { const key = parseInt(keys[i], 10); const monitoredItem = this.monitoredItems[key]; if (monitoredItem.hasMonitoredItemNotifications) { this._hasUncollectedMonitoredItemNotifications = true; return true; } } return false; } get subscriptionId() { return this.id; } getMessageForSequenceNumber(sequenceNumber) { const notification_message = this._sent_notification_messages.find((e) => e.sequenceNumber === sequenceNumber); return notification_message || null; } /** * returns true if the notification has expired * @param notification */ notificationHasExpired(notification) { (0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(notification, "start_tick")); (0, node_opcua_assert_1.assert)(isFinite(notification.start_tick + this.maxKeepAliveCount)); return notification.start_tick + this.maxKeepAliveCount < this.publishIntervalCount; } /** * returns in an array the sequence numbers of the notifications that have been sent * and that haven't been acknowledged yet. */ getAvailableSequenceNumbers() { const availableSequenceNumbers = _getSequenceNumbers(this._sent_notification_messages); return availableSequenceNumbers; } /** * acknowledges a notification identified by its sequence number */ acknowledgeNotification(sequenceNumber) { debugLog("acknowledgeNotification ", sequenceNumber); let foundIndex = -1; this._sent_notification_messages.forEach((e, index) => { if (e.sequenceNumber === sequenceNumber) { foundIndex = index; } }); if (foundIndex === -1) { // istanbul ignore next if (doDebug) { debugLog(chalk_1.default.red("acknowledging sequence FAILED !!! "), chalk_1.default.cyan(sequenceNumber.toString())); } return node_opcua_status_code_1.StatusCodes.BadSequenceNumberUnknown; } else { // istanbul ignore next if (doDebug) { debugLog(chalk_1.default.yellow("acknowledging sequence "), chalk_1.default.cyan(sequenceNumber.toString())); } this._sent_notification_messages.splice(foundIndex, 1); this.subscriptionDiagnostics.unacknowledgedMessageCount--; return node_opcua_status_code_1.StatusCodes.Good; } } /** * getMonitoredItems is used to get information about monitored items of a subscription.Its intended * use is defined in Part 4. This method is the implementation of the Standard OPCUA GetMonitoredItems Method. * from spec: * This method can be used to get the list of monitored items in a subscription if CreateMonitoredItems * failed due to a network interruption and the client does not know if the creation succeeded in the server. * */ getMonitoredItems() { const monitoredItems = Object.keys(this.monitoredItems); const monitoredItemCount = monitoredItems.length; const result = { clientHandles: new Uint32Array(monitoredItemCount), serverHandles: new Uint32Array(monitoredItemCount), statusCode: node_opcua_status_code_1.StatusCodes.Good }; for (let index = 0; index < monitoredItemCount; index++) { const monitoredItemId = monitoredItems[index]; const serverHandle = parseInt(monitoredItemId, 10); const monitoredItem = this.getMonitoredItem(serverHandle); result.clientHandles[index] = monitoredItem.clientHandle; // TODO: serverHandle is defined anywhere in the OPCUA Specification 1.02 // I am not sure what shall be reported for serverHandle... // using monitoredItem.monitoredItemId instead... // May be a clarification in the OPCUA Spec is required. result.serverHandles[index] = serverHandle; } return result; } /** * @private */ async resendInitialValues() { this._keep_alive_counter = 0; try { const promises = []; for (const monitoredItem of Object.values(this.monitoredItems)) { promises.push((async () => { try { monitoredItem.resendInitialValue(); } catch (err) { warningLog("resendInitialValues:", monitoredItem.node?.nodeId.toString(), "error:", err.message); } })()); } await Promise.all(promises); } catch (err) { warningLog("resendInitialValues: error:", err.message); } // make sure data will be sent immediately this._keep_alive_counter = this.maxKeepAliveCount - 1; this.state = SubscriptionState.NORMAL; this._harvestMonitoredItems(); } /** * @private */ notifyTransfer() { // OPCUA UA Spec 1.0.3 : part 3 - page 82 - 5.13.7 TransferSubscriptions: // If the Server transfers the Subscription to the new Session, the Server shall issue // a StatusChangeNotification notificationMessage with the status code // Good_SubscriptionTransferred to the old Session. debugLog(chalk_1.default.red(" Subscription => Notifying Transfer ")); const notificationData = new node_opcua_service_subscription_2.StatusChangeNotification({ status: node_opcua_status_code_1.StatusCodes.GoodSubscriptionTransferred }); if (this.publishEngine.pendingPublishRequestCount) { // the GoodSubscriptionTransferred can be processed immediately this._addNotificationMessage(notificationData); debugLog(chalk_1.default.red("pendingPublishRequestCount"), this.publishEngine?.pendingPublishRequestCount); this._publish_pending_notifications(); } else { debugLog(chalk_1.default.red("Cannot send GoodSubscriptionTransferred => lets create a TransferredSubscription ")); const ts = new i_server_side_publish_engine_1.TransferredSubscription({ generator: this._sequence_number_generator, id: this.id, publishEngine: this.publishEngine }); ts._pending_notification = notificationData; this.publishEngine._closed_subscriptions.push(ts); } } /** * * the server invokes the resetLifeTimeAndKeepAliveCounters method of the subscription * when the server has send a Publish Response, so that the subscription * can reset its life time counter. * * @private */ resetLifeTimeAndKeepAliveCounters() { this.resetLifeTimeCounter(); this.resetKeepAliveCounter(); } _updateCounters(notificationMessage) { for (const notificationData of notificationMessage.notificationData || []) { // update diagnostics if (notificationData instanceof node_opcua_service_subscription_2.DataChangeNotification) { const nbNotifs = notificationData.monitoredItems.length; this.subscriptionDiagnostics.dataChangeNotificationsCount += nbNotifs; this.subscriptionDiagnostics.notificationsCount += nbNotifs; } else if (notificationData instanceof node_opcua_service_subscription_2.EventNotificationList) { const nbNotifs = notificationData.events.length; this.subscriptionDiagnostics.eventNotificationsCount += nbNotifs; this.subscriptionDiagnostics.notificationsCount += nbNotifs; } else { (0, node_opcua_assert_1.assert)(notificationData instanceof node_opcua_service_subscription_2.StatusChangeNotification); // TODO // note: :there is no way to count StatusChangeNotifications in opcua yet. } } } /** * _publish_pending_notifications send a "notification" event: * * @private * * precondition * - pendingPublishRequestCount > 0 */ _publish_pending_notifications() { const publishEngine = this.publishEngine; const subscriptionId = this.id; // preconditions (0, node_opcua_assert_1.assert)(publishEngine.pendingPublishRequestCount > 0); (0, node_opcua_assert_1.assert)(this.hasPendingNotifications); const notificationMessage = this._popNotificationToSend(); if (notificationMessage.notificationData.length === 0) { return; // nothing to do } const moreNotifications = this.hasPendingNotifications; this.emit("notification", notificationMessage); // Update counters .... this._updateCounters(notificationMessage); (0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(notificationMessage, "sequenceNumber")); (0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(notificationMessage, "notificationData")); // update diagnostics this.subscriptionDiagnostics.publishRequestCount += 1; const response = new node_opcua_service_subscription_2.PublishResponse({ moreNotifications, notificationMessage: { notificationData: notificationMessage.notificationData, sequenceNumber: this._get_next_sequence_number() }, subscriptionId }); this._sent_notification_messages.push(response.notificationMessage); // get available sequence number; const availableSequenceNumbers = this.getAvailableSequenceNumbers(); (0, node_opcua_assert_1.assert)(!response.notificationMessage || availableSequenceNumbers[availableSequenceNumbers.length - 1] === response.notificationMessage.sequenceNumber); response.availableSequenceNumbers = availableSequenceNumbers; publishEngine._send_response(this, response); this.messageSent = true; this.subscriptionDiagnostics.unacknowledgedMessageCount++; this.resetLifeTimeAndKeepAliveCounters(); // istanbul ignore next if (doDebug) { debugLog("Subscription sending a notificationMessage subscriptionId=", subscriptionId, "sequenceNumber = ", notificationMessage.sequenceNumber.toString(), notificationMessage.notificationData?.map((x) => x?.constructor.name).join(" ")); // debugLog(notificationMessage.toString()); } if (this.state !== SubscriptionState.CLOSED) { (0, node_opcua_assert_1.assert)(notificationMessage.notificationData.length > 0, "We are not expecting a keep-alive message here"); this.state = SubscriptionState.NORMAL; debugLog("subscription " + this.id + chalk_1.default.bgYellow(" set to NORMAL")); } } process_subscription() { (0, node_opcua_assert_1.assert)(this.publishEngine.pendingPublishRequestCount > 0); if (!this.publishingEnabled) { // no publish to do, except keep alive debugLog(" -> no publish to do, except keep alive"); this._process_keepAlive(); return; } if (!this.hasPendingNotifications && this.hasUncollectedMonitoredItemNotifications) { // collect notification from monitored items this._harvestMonitoredItems(); } // let process them first if (this.hasPendingNotifications) { this._publish_pending_notifications(); if (this.state === SubscriptionState.NORMAL || (this.state === SubscriptionState.LATE && this.hasPendingNotifications)) { // istanbul ignore next if (doDebug) { debugLog(" -> pendingPublishRequestCount > 0 " + "&& normal state => re-trigger tick event immediately "); } // let process an new publish request setImmediate(this._tick.bind(this)); } } else { this._process_keepAlive(); } } _process_keepAlive() { this.increaseKeepAliveCounter(); if (this.keepAliveCounterHasExpired) { debugLog(` -> _process_keepAlive => keepAliveCounterHasExpired`); if (this._sendKeepAliveResponse()) { this.resetLifeTimeAndKeepAliveCounters(); } else { debugLog(" -> subscription.state === LATE , " + "because keepAlive Response cannot be send due to lack of PublishRequest"); if (this.messageSent || this.keepAliveCounterHasExpired) { this.state = SubscriptionState.LATE; } } } } _stop_timer() { if (this.timerId) { debugLog(chalk_1.default.bgWhite.blue("Subscription#_stop_timer subscriptionId="), this.id); clearInterval(this.timerId); this.timerId = null; } } _start_timer({ firstTime }) { debugLog(chalk_1.default.bgWhite.blue("Subscription#_start_timer subscriptionId="), this.id, " publishingInterval = ", this.publishingInterval); (0, node_opcua_assert_1.assert)(this.timerId === null); // from the spec: // When a Subscription is created, the first Message is sent at the end of the first publishing cycle to // inform the Client that the Subscription is operational. A NotificationMessage is sent if there are // Notifications ready to be reported. If there are none, a keep-alive Message is sent instead that // contains a sequence number of 1, indicating that the first NotificationMessage has not yet been sent. // This is the only time a keep-alive Message is sent without waiting for the maximum keep-alive count // to be reached, as specified in (f) above. // make sure that a keep-alive Message will be send at the end of the first publishing cycle // if there are no Notifications ready. this._keep_alive_counter = this.