UNPKG

node-opcua-server

Version:

pure nodejs OPCUA SDK - module server

965 lines (964 loc) 56.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MonitoredItem = void 0; /** * @module node-opcua-server */ const events_1 = require("events"); const chalk_1 = __importDefault(require("chalk")); const node_opcua_assert_1 = require("node-opcua-assert"); const node_opcua_address_space_1 = require("node-opcua-address-space"); const node_opcua_service_filter_1 = require("node-opcua-service-filter"); const node_opcua_data_model_1 = require("node-opcua-data-model"); const node_opcua_data_model_2 = require("node-opcua-data-model"); const node_opcua_data_value_1 = require("node-opcua-data-value"); const node_opcua_debug_1 = require("node-opcua-debug"); const node_opcua_numeric_range_1 = require("node-opcua-numeric-range"); const node_opcua_object_registry_1 = require("node-opcua-object-registry"); const node_opcua_service_filter_2 = require("node-opcua-service-filter"); const node_opcua_service_read_1 = require("node-opcua-service-read"); 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 node_opcua_variant_1 = require("node-opcua-variant"); const node_sampler_1 = require("./node_sampler"); const validate_filter_1 = require("./validate_filter"); const check_where_clause_on_address_space_1 = require("./filter/check_where_clause_on_address_space"); const errorLog = (0, node_opcua_debug_1.make_errorLog)(__filename); const defaultItemToMonitor = new node_opcua_service_read_1.ReadValueId({ attributeId: node_opcua_data_model_2.AttributeIds.Value, indexRange: undefined }); const debugLog = (0, node_opcua_debug_1.make_debugLog)(__filename); const doDebug = (0, node_opcua_debug_1.checkDebugFlag)(__filename); const doDebug2 = doDebug && false; const warningLog = (0, node_opcua_debug_1.make_warningLog)(__filename); function _adjust_sampling_interval(samplingInterval, node_minimumSamplingInterval) { (0, node_opcua_assert_1.assert)(typeof node_minimumSamplingInterval === "number", "expecting a number"); if (samplingInterval === 0) { return node_minimumSamplingInterval === 0 ? samplingInterval : Math.max(MonitoredItem.minimumSamplingInterval, node_minimumSamplingInterval); } (0, node_opcua_assert_1.assert)(samplingInterval >= 0, " this case should have been prevented outside"); samplingInterval = samplingInterval || MonitoredItem.defaultSamplingInterval; samplingInterval = Math.max(samplingInterval, MonitoredItem.minimumSamplingInterval); samplingInterval = Math.min(samplingInterval, MonitoredItem.maximumSamplingInterval); samplingInterval = node_minimumSamplingInterval === 0 ? samplingInterval : Math.max(samplingInterval, node_minimumSamplingInterval); return samplingInterval; } const maxQueueSize = 5000; function _adjust_queue_size(queueSize) { queueSize = Math.min(queueSize, maxQueueSize); queueSize = Math.max(1, queueSize); return queueSize; } function _validate_parameters(monitoringParameters) { // xx assert(options instanceof MonitoringParameters); (0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(monitoringParameters, "clientHandle")); (0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(monitoringParameters, "samplingInterval")); (0, node_opcua_assert_1.assert)(isFinite(monitoringParameters.clientHandle)); (0, node_opcua_assert_1.assert)(isFinite(monitoringParameters.samplingInterval)); (0, node_opcua_assert_1.assert)(typeof monitoringParameters.discardOldest === "boolean"); (0, node_opcua_assert_1.assert)(isFinite(monitoringParameters.queueSize)); (0, node_opcua_assert_1.assert)(monitoringParameters.queueSize >= 0); } function statusCodeHasChanged(newDataValue, oldDataValue) { (0, node_opcua_assert_1.assert)(newDataValue instanceof node_opcua_data_value_1.DataValue); (0, node_opcua_assert_1.assert)(oldDataValue instanceof node_opcua_data_value_1.DataValue); return newDataValue.statusCode.value !== oldDataValue.statusCode.value; } function valueHasChanged(newDataValue, oldDataValue, deadbandType, deadbandValue) { (0, node_opcua_assert_1.assert)(newDataValue instanceof node_opcua_data_value_1.DataValue); (0, node_opcua_assert_1.assert)(oldDataValue instanceof node_opcua_data_value_1.DataValue); switch (deadbandType) { case node_opcua_service_subscription_2.DeadbandType.None: (0, node_opcua_assert_1.assert)(newDataValue.value instanceof node_opcua_variant_1.Variant); (0, node_opcua_assert_1.assert)(newDataValue.value instanceof node_opcua_variant_1.Variant); // No Deadband calculation should be applied. return (0, node_opcua_service_subscription_2.isOutsideDeadbandNone)(oldDataValue.value, newDataValue.value); case node_opcua_service_subscription_2.DeadbandType.Absolute: // AbsoluteDeadband return (0, node_opcua_service_subscription_2.isOutsideDeadbandAbsolute)(oldDataValue.value, newDataValue.value, deadbandValue); default: { // Percent_2 PercentDeadband (This type is specified in Part 8). (0, node_opcua_assert_1.assert)(deadbandType === node_opcua_service_subscription_2.DeadbandType.Percent); // The range of the deadbandValue is from 0.0 to 100.0 Percent. (0, node_opcua_assert_1.assert)(deadbandValue >= 0 && deadbandValue <= 100); // DeadbandType = PercentDeadband // For this type of deadband the deadbandValue is defined as the percentage of the EURange. That is, // it applies only to AnalogItems with an EURange Property that defines the typical value range for the // item. This range shall be multiplied with the deadbandValue and then compared to the actual value change // to determine the need for a data change notification. The following pseudo code shows how the deadband // is calculated: // DataChange if (absolute value of (last cached value - current value) > // (deadbandValue/100.0) * ((high-low) of EURange))) // // StatusCode BadDeadbandFilterInvalid (see Table 27). // If the Value of the MonitoredItem is an array, then the deadband calculation logic shall be applied to // each element of the array. If an element that requires a DataChange is found, then no further // deadband checking is necessary and the entire array shall be returned. (0, node_opcua_assert_1.assert)(this.node !== null, "expecting a valid address_space object here to get access the the EURange"); const euRangeNode = this.node.getChildByName("EURange"); if (euRangeNode && euRangeNode.nodeClass === node_opcua_data_model_1.NodeClass.Variable) { // double,double const rangeVariant = euRangeNode.readValue().value; return (0, node_opcua_service_subscription_2.isOutsideDeadbandPercent)(oldDataValue.value, newDataValue.value, deadbandValue, rangeVariant.value); } else { errorLog("EURange is not of type Variable"); } return true; } } } function timestampHasChanged(t1, t2) { if (t1 || !t2 || t2 || !t1) { return true; } if (!t1 || !t2) { return false; } return t1.getTime() !== t2.getTime(); } function apply_dataChange_filter(newDataValue, oldDataValue) { /* istanbul ignore next */ if (!this.filter || !(this.filter instanceof node_opcua_service_subscription_2.DataChangeFilter)) { throw new Error("Internal Error"); } const trigger = this.filter.trigger; // istanbul ignore next if (doDebug) { try { debugLog("filter pass ?", node_opcua_service_subscription_2.DataChangeTrigger[trigger], this.oldDataValue?.toString(), newDataValue.toString()); if (trigger === node_opcua_service_subscription_2.DataChangeTrigger.Status || trigger === node_opcua_service_subscription_2.DataChangeTrigger.StatusValue || trigger === node_opcua_service_subscription_2.DataChangeTrigger.StatusValueTimestamp) { debugLog("statusCodeHasChanged ", statusCodeHasChanged(newDataValue, oldDataValue)); } if (trigger === node_opcua_service_subscription_2.DataChangeTrigger.StatusValue || trigger === node_opcua_service_subscription_2.DataChangeTrigger.StatusValueTimestamp) { debugLog("valueHasChanged ", valueHasChanged.call(this, newDataValue, oldDataValue, this.filter.deadbandType, this.filter.deadbandValue)); } if (trigger === node_opcua_service_subscription_2.DataChangeTrigger.StatusValueTimestamp) { debugLog("timestampHasChanged ", timestampHasChanged(newDataValue.sourceTimestamp, oldDataValue.sourceTimestamp)); } } catch (err) { warningLog(err); } } switch (trigger) { case node_opcua_service_subscription_2.DataChangeTrigger.Status: { // // Status // Report a notification ONLY if the StatusCode associated with // the value changes. See Table 166 for StatusCodes defined in // this standard. Part 8 specifies additional StatusCodes that are // valid in particular for device data. return statusCodeHasChanged(newDataValue, oldDataValue); } case node_opcua_service_subscription_2.DataChangeTrigger.StatusValue: { // filtering value changes. // change. The Deadband filter can be used in addition for // Report a notification if either the StatusCode or the value // StatusValue // This is the default setting if no filter is set. return (statusCodeHasChanged(newDataValue, oldDataValue) || valueHasChanged.call(this, newDataValue, oldDataValue, this.filter.deadbandType, this.filter.deadbandValue)); } default: { // StatusValueTimestamp // Report a notification if either StatusCode, value or the // SourceTimestamp change. // // If a Deadband filter is specified,this trigger has the same behavior as STATUS_VALUE_1. // // If the DataChangeFilter is not applied to the monitored item, STATUS_VALUE_1 // is the default reporting behavior (0, node_opcua_assert_1.assert)(trigger === node_opcua_service_subscription_2.DataChangeTrigger.StatusValueTimestamp); return (timestampHasChanged(newDataValue.sourceTimestamp, oldDataValue.sourceTimestamp) || statusCodeHasChanged(newDataValue, oldDataValue) || valueHasChanged.call(this, newDataValue, oldDataValue, this.filter.deadbandType, this.filter.deadbandValue)); } } } const s = (a) => JSON.stringify(a, null, " "); function safeGuardRegister(monitoredItem) { monitoredItem.oldDataValue._$monitoredItem = monitoredItem.node?.nodeId?.toString(); monitoredItem._$safeGuard = s(monitoredItem.oldDataValue); } function safeGuardVerify(monitoredItem) { if (monitoredItem._$safeGuard) { const verif = s(monitoredItem.oldDataValue || ""); if (verif !== monitoredItem._$safeGuard) { errorLog(verif, monitoredItem._$safeGuard); throw new Error("Internal error: DataValue has been altered !!!"); } } } function apply_filter(newDataValue) { if (this.oldDataValue === badDataUnavailable) { return true; // keep } // istanbul ignore next doDebug && safeGuardVerify(this); if (this.filter instanceof node_opcua_service_subscription_2.DataChangeFilter) { return apply_dataChange_filter.call(this, newDataValue, this.oldDataValue); } else { // if filter not set, by default report changes to Status or Value only if (newDataValue.statusCode.value !== this.oldDataValue.statusCode.value) { return true; // Keep because statusCode has changed ... } return !(0, node_opcua_variant_1.sameVariant)(newDataValue.value, this.oldDataValue.value); } } function setSemanticChangeBit(notification) { if (notification instanceof node_opcua_service_subscription_1.MonitoredItemNotification) { notification.value.statusCode = node_opcua_status_code_1.StatusCode.makeStatusCode(notification.value.statusCode || node_opcua_status_code_1.StatusCodes.Good, "SemanticChanged"); } else if (notification instanceof node_opcua_data_value_1.DataValue) { notification.statusCode = node_opcua_status_code_1.StatusCode.makeStatusCode(notification.statusCode || node_opcua_status_code_1.StatusCodes.Good, "SemanticChanged"); } } const useCommonTimer = true; function isSourceNewerThan(a, b) { if (!b) { return true; } const at = a.sourceTimestamp?.getTime() || 0; const bt = b.sourceTimestamp?.getTime() || 0; if (at === bt) { return a.sourcePicoseconds > b.sourcePicoseconds; } return at > bt; } const badDataUnavailable = new node_opcua_data_value_1.DataValue({ statusCode: node_opcua_status_code_1.StatusCodes.BadDataUnavailable }); // unset initially /** * a server side monitored item * * - Once created, the MonitoredItem will raised an "samplingEvent" event every "samplingInterval" millisecond * until {{#crossLink "MonitoredItem/terminate:method"}}{{/crossLink}} is called. * * - It is up to the event receiver to call {{#crossLink "MonitoredItem/recordValue:method"}}{{/crossLink}}. * */ class MonitoredItem extends events_1.EventEmitter { get node() { return this._node; } set node(someNode) { throw new Error("Unexpected way to set node"); } constructor(options) { super(); this.samplingInterval = -1; this.discardOldest = true; this.queueSize = 0; this.samplingFunc = null; this._is_sampling = false; (0, node_opcua_assert_1.assert)(Object.prototype.hasOwnProperty.call(options, "monitoredItemId")); (0, node_opcua_assert_1.assert)(!options.monitoringMode, "use setMonitoring mode explicitly to activate the monitored item"); options.itemToMonitor = options.itemToMonitor || defaultItemToMonitor; this._samplingId = undefined; this.clientHandle = 0; // invalid this.filter = null; this._set_parameters(options); this.monitoredItemId = options.monitoredItemId; // ( known as serverHandle) this.queue = []; this.overflow = false; this.oldDataValue = badDataUnavailable; // user has to call setMonitoringMode this.monitoringMode = node_opcua_service_subscription_1.MonitoringMode.Invalid; this.timestampsToReturn = (0, node_opcua_data_value_1.coerceTimestampsToReturn)(options.timestampsToReturn); this.itemToMonitor = options.itemToMonitor; this._node = null; this._semantic_version = 0; // istanbul ignore next if (doDebug) { debugLog("Monitoring ", options.itemToMonitor.toString()); } this._on_node_disposed_listener = null; MonitoredItem.registry.register(this); } setNode(node) { (0, node_opcua_assert_1.assert)(!this.node || this.node === node, "node already set"); this._node = node; this._semantic_version = node.semantic_version; this._on_node_disposed_listener = () => this._on_node_disposed(this._node); this._node.on("dispose", this._on_node_disposed_listener); } setMonitoringMode(monitoringMode) { (0, node_opcua_assert_1.assert)(monitoringMode !== node_opcua_service_subscription_1.MonitoringMode.Invalid); if (monitoringMode === this.monitoringMode) { // nothing to do return; } const old_monitoringMode = this.monitoringMode; this.monitoringMode = monitoringMode; if (this.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Disabled) { this._stop_sampling(); // OPCUA 1.03 part 4 : $5.12.4 // setting the mode to DISABLED causes all queued Notifications to be deleted this._empty_queue(); } else { (0, node_opcua_assert_1.assert)(this.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Sampling || this.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Reporting); // OPCUA 1.03 part 4 : $5.12.1.3 // When a MonitoredItem is enabled (i.e. when the MonitoringMode is changed from DISABLED to // SAMPLING or REPORTING) or it is created in the enabled state, the Server shall report the first // sample as soon as possible and the time of this sample becomes the starting point for the next // sampling interval. const recordInitialValue = old_monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Invalid || old_monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Disabled; const installEventHandler = old_monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Invalid; this._start_sampling(recordInitialValue); } } /** * Terminate the MonitoredItem. * This will stop the internal sampling timer. */ terminate() { this._stop_sampling(); } dispose() { // istanbul ignore next if (doDebug) { debugLog("DISPOSING MONITORED ITEM", this._node.nodeId.toString()); } this._stop_sampling(); MonitoredItem.registry.unregister(this); if (this._on_node_disposed_listener) { this._node.removeListener("dispose", this._on_node_disposed_listener); this._on_node_disposed_listener = null; } // x assert(this._samplingId === null,"Sampling Id must be null"); this.oldDataValue = badDataUnavailable; this.queue = []; this.itemToMonitor = null; this.filter = null; this.monitoredItemId = 0; this._node = null; this._semantic_version = 0; this.$subscription = undefined; this.removeAllListeners(); (0, node_opcua_assert_1.assert)(!this._samplingId); (0, node_opcua_assert_1.assert)(!this._value_changed_callback); (0, node_opcua_assert_1.assert)(!this._semantic_changed_callback); (0, node_opcua_assert_1.assert)(!this._attribute_changed_callback); (0, node_opcua_assert_1.assert)(!this._on_opcua_event_received_callback); this._on_opcua_event_received_callback = null; this._attribute_changed_callback = null; this._semantic_changed_callback = null; this._on_opcua_event_received_callback = null; } get isSampling() { return (!!this._samplingId || typeof this._value_changed_callback === "function" || typeof this._attribute_changed_callback === "function"); } toString() { let str = ""; str += `monitored item nodeId : ${this.node?.nodeId.toString()} \n`; str += ` sampling interval : ${this.samplingInterval} \n`; str += ` monitoredItemId : ${this.monitoredItemId} \n`; return str; } /** * @param dataValue the whole dataValue * @param skipChangeTest indicates whether recordValue should not check that dataValue is really * different from previous one, ( by checking timestamps but also variant value) * @private * * Notes: * - recordValue can only be called within timer event * - for performance reason, dataValue may be a shared value with the underlying node, * therefore recordValue must clone the dataValue to make sure it retains a snapshot * of the contain at the time recordValue was called. * * return true if the value has been recorded, false if not. * * Value will not be recorded : * * if the range do not overlap * * is !skipChangeTest and value is equal to previous value * */ // eslint-disable-next-line complexity, max-statements recordValue(dataValue, skipChangeTest, indexRange) { if (!this.itemToMonitor) { // we must have a valid itemToMonitor(have this monitoredItem been disposed already ?) // istanbul ignore next doDebug && debugLog("recordValue => Rejected", this.node?.browseName.toString(), " because no itemToMonitor"); return false; } if (dataValue === this.oldDataValue) { errorLog("recordValue expects different dataValue to be provided"); } doDebug && (0, node_opcua_assert_1.assert)(!dataValue.value || dataValue.value.isValid(), "expecting a valid variant value"); const hasSemanticChanged = this.node && this.node.semantic_version !== this._semantic_version; if (!hasSemanticChanged && indexRange && this.itemToMonitor.indexRange) { // we just ignore changes that do not fall within our range // ( unless semantic bit has changed ) if (!node_opcua_numeric_range_1.NumericRange.overlap(indexRange, this.itemToMonitor.indexRange)) { // istanbul ignore next doDebug && debugLog("recordValue => Rejected", this.node?.browseName.toString(), " because no range not overlap"); return false; // no overlap ! } } // extract the range that we are interested with dataValue = (0, node_opcua_data_value_1.extractRange)(dataValue, this.itemToMonitor.indexRange); // istanbul ignore next if (doDebug2) { debugLog("MonitoredItem#recordValue", this.node.nodeId.toString(), this.node.browseName.toString(), " has Changed = ", !(0, node_opcua_data_value_1.sameDataValue)(dataValue, this.oldDataValue), "skipChangeTest = ", skipChangeTest, "hasSemanticChanged = ", hasSemanticChanged); } // if semantic has changed, value need to be enqueued regardless of other assumptions if (hasSemanticChanged) { debugLog("_enqueue_value => because hasSemanticChanged"); setSemanticChangeBit(dataValue); this._semantic_version = this.node.semantic_version; this._enqueue_value(dataValue); // istanbul ignore next doDebug && debugLog("recordValue => OK ", this.node?.browseName.toString(), " because hasSemanticChanged"); return true; } const useIndexRange = this.itemToMonitor.indexRange && !this.itemToMonitor.indexRange.isEmpty(); if (!skipChangeTest) { const hasChanged = !(0, node_opcua_data_value_1.sameDataValue)(dataValue, this.oldDataValue); if (!hasChanged) { // istanbul ignore next doDebug2 && debugLog("recordValue => Rejected ", this.node?.browseName.toString(), " because !skipChangeTest && sameDataValue"); return false; } } if (!skipChangeTest && !apply_filter.call(this, dataValue)) { // istanbul ignore next if (doDebug) { debugLog("recordValue => Rejected ", this.node?.browseName.toString(), " because apply_filter"); debugLog("current Value =>", this.oldDataValue?.toString()); debugLog("proposed Value =>", dataValue?.toString()); debugLog("proposed Value =>", dataValue == this.oldDataValue, dataValue.value === this.oldDataValue?.value); } return false; } if (useIndexRange) { // when an indexRange is provided , make sure that no record happens unless // extracted variant in the selected range has really changed. // istanbul ignore next if (doDebug) { debugLog("Current : ", this.oldDataValue?.toString()); debugLog("New : ", dataValue.toString()); debugLog("indexRange=", indexRange); } if (this.oldDataValue !== badDataUnavailable && (0, node_opcua_variant_1.sameVariant)(dataValue.value, this.oldDataValue.value)) { // istanbul ignore next doDebug && debugLog("recordValue => Rejected ", this.node?.browseName.toString(), " because useIndexRange && sameVariant"); return false; } } // processTriggerItems this.triggerLinkedItems(); // store last value this._enqueue_value(dataValue); // istanbul ignore next doDebug && debugLog("recordValue => OK ", this.node?.browseName.toString()); return true; } hasLinkItem(linkedMonitoredItemId) { if (!this._linkedItems) { return false; } return this._linkedItems.findIndex((x) => x === linkedMonitoredItemId) > 0; } addLinkItem(linkedMonitoredItemId) { if (linkedMonitoredItemId === this.monitoredItemId) { return node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid; } this._linkedItems = this._linkedItems || []; if (this.hasLinkItem(linkedMonitoredItemId)) { return node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid; // nothing to do } this._linkedItems.push(linkedMonitoredItemId); return node_opcua_status_code_1.StatusCodes.Good; } removeLinkItem(linkedMonitoredItemId) { if (!this._linkedItems || linkedMonitoredItemId === this.monitoredItemId) { return node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid; } const index = this._linkedItems.findIndex((x) => x === linkedMonitoredItemId); if (index === -1) { return node_opcua_status_code_1.StatusCodes.BadMonitoredItemIdInvalid; } this._linkedItems.splice(index, 1); return node_opcua_status_code_1.StatusCodes.Good; } /** * @private */ triggerLinkedItems() { if (!this.$subscription || !this._linkedItems) { return; } // see https://reference.opcfoundation.org/v104/Core/docs/Part4/5.12.1/#5.12.1.6 for (const linkItem of this._linkedItems) { const linkedMonitoredItem = this.$subscription.getMonitoredItem(linkItem); if (!linkedMonitoredItem) { // monitoredItem may have been deleted continue; } if (linkedMonitoredItem.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Disabled) { continue; } if (linkedMonitoredItem.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Reporting) { continue; } (0, node_opcua_assert_1.assert)(linkedMonitoredItem.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Sampling); // istanbul ignore next if (doDebug) { debugLog("triggerLinkedItems => ", this.node?.nodeId.toString(), linkedMonitoredItem.node?.nodeId.toString()); } linkedMonitoredItem.trigger(); } } get hasMonitoredItemNotifications() { return this.queue.length > 0 || (this._triggeredNotifications !== undefined && this._triggeredNotifications.length > 0); } /** * @private */ trigger() { setImmediate(() => { this._triggeredNotifications = this._triggeredNotifications || []; const notifications = this.extractMonitoredItemNotifications(true); this._triggeredNotifications = [].concat(this._triggeredNotifications, notifications); }); } extractMonitoredItemNotifications(bForce = false) { if (!bForce && this.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Sampling && this._triggeredNotifications) { const notifications1 = this._triggeredNotifications; this._triggeredNotifications = undefined; return notifications1; } if (!bForce && this.monitoringMode !== node_opcua_service_subscription_1.MonitoringMode.Reporting) { return []; } const notifications = this.queue; this._empty_queue(); // apply semantic changed bit if necessary if (notifications.length > 0 && this.node && this._semantic_version < this.node.semantic_version) { const dataValue = notifications[notifications.length - 1]; setSemanticChangeBit(dataValue); (0, node_opcua_assert_1.assert)(this.node.nodeClass === node_opcua_data_model_1.NodeClass.Variable); this._semantic_version = this.node.semantic_version; } return notifications; } modify(timestampsToReturn, monitoringParameters) { (0, node_opcua_assert_1.assert)(monitoringParameters instanceof node_opcua_service_subscription_1.MonitoringParameters); const old_samplingInterval = this.samplingInterval; this.timestampsToReturn = timestampsToReturn || this.timestampsToReturn; if (!monitoringParameters) { return new node_opcua_service_subscription_1.MonitoredItemModifyResult({ revisedQueueSize: this.queueSize, revisedSamplingInterval: this.samplingInterval, filterResult: null, statusCode: node_opcua_status_code_1.StatusCodes.Good }); } if (old_samplingInterval !== 0 && monitoringParameters.samplingInterval === 0) { monitoringParameters.samplingInterval = MonitoredItem.minimumSamplingInterval; // fastest possible } // spec says: Illegal request values for parameters that can be revised do not generate errors. Instead the // server will choose default values and indicate them in the corresponding revised parameter this._set_parameters(monitoringParameters); this._adjust_queue_to_match_new_queue_size(); this._adjustSampling(old_samplingInterval); if (monitoringParameters.filter) { const statusCodeFilter = (0, validate_filter_1.validateFilter)(monitoringParameters.filter, this.itemToMonitor, this.node); if (statusCodeFilter.isNot(node_opcua_status_code_1.StatusCodes.Good)) { return new node_opcua_service_subscription_1.MonitoredItemModifyResult({ statusCode: statusCodeFilter }); } } // validate filter // note : The DataChangeFilter does not have an associated result structure. const filterResult = null; // new subscription_service.DataChangeFilter return new node_opcua_service_subscription_1.MonitoredItemModifyResult({ filterResult, revisedQueueSize: this.queueSize, revisedSamplingInterval: this.samplingInterval, statusCode: node_opcua_status_code_1.StatusCodes.Good }); } async resendInitialValue() { // the first Publish response(s) after the TransferSubscriptions call shall contain the current values of all // Monitored Items in the Subscription where the Monitoring Mode is set to Reporting. // the first Publish response after the TransferSubscriptions call shall contain only the value changes since // the last Publish response was sent. // This parameter only applies to MonitoredItems used for monitoring Attribute changes. // istanbul ignore next if (!this.node) return; const sessionContext = this.getSessionContext() || node_opcua_address_space_1.SessionContext.defaultContext; // istanbul ignore next if (!sessionContext) return; // no need to resend if a value is already in the queue if (this.queue.length > 0) return; const theValueToResend = this.oldDataValue !== badDataUnavailable ? this.oldDataValue : this.node.readAttribute(sessionContext, this.itemToMonitor.attributeId); this.oldDataValue = badDataUnavailable; this._enqueue_value(theValueToResend); } getSessionContext() { const session = this._getSession(); if (!session) { return null; } const sessionContext = session.sessionContext; return sessionContext; } /** * @private */ _on_sampling_timer() { if (this.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Disabled) { return; } // Use default context if session is not available const sessionContext = this.getSessionContext() || node_opcua_address_space_1.SessionContext.defaultContext; if (!sessionContext) { warningLog("MonitoredItem#_on_sampling_timer : ", this.node?.nodeId.toString(), "cannot find session"); return; } // istanbul ignore next if (doDebug2) { debugLog("MonitoredItem#_on_sampling_timer", this.node ? this.node.nodeId.toString() : "null", " isSampling?=", this._is_sampling); } if (this._samplingId) { (0, node_opcua_assert_1.assert)(this.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Sampling || this.monitoringMode === node_opcua_service_subscription_1.MonitoringMode.Reporting); if (this._is_sampling) { // previous sampling call is not yet completed.. // there is nothing we can do about it except waiting until next tick. // note : see also issue #156 on github // Note: some server returns GoodOverload here const statusCode = node_opcua_status_code_1.StatusCodes.GoodOverload; return; } (0, node_opcua_assert_1.assert)(!this._is_sampling, "sampling func shall not be re-entrant !! fix it"); // istanbul ignore next if (!this.samplingFunc) { throw new Error("internal error : missing samplingFunc"); } this._is_sampling = true; this.samplingFunc.call(this, sessionContext, this.oldDataValue, (err, newDataValue) => { if (!this._samplingId) { // item has been disposed. The monitored item has been disposed while the async sampling func // was taking place ... just ignore this return; } // istanbul ignore next if (err) { errorLog(" SAMPLING ERROR =>", err); } else { // only record value if source timestamp is newer // xx if (newDataValue && isSourceNewerThan(newDataValue, this.oldDataValue)) { this._on_value_changed(newDataValue); // xx } } this._is_sampling = false; }); } else { /* istanbul ignore next */ debugLog("_on_sampling_timer call but MonitoredItem has been terminated !!! "); } } _stop_sampling() { // debugLog("MonitoredItem#_stop_sampling"); /* istanbul ignore next */ if (!this.node) { throw new Error("Internal Error"); } if (this._on_opcua_event_received_callback) { (0, node_opcua_assert_1.assert)(typeof this._on_opcua_event_received_callback === "function"); this.node.removeListener("event", this._on_opcua_event_received_callback); this._on_opcua_event_received_callback = null; } if (this._attribute_changed_callback) { (0, node_opcua_assert_1.assert)(typeof this._attribute_changed_callback === "function"); const event_name = (0, node_opcua_address_space_1.makeAttributeEventName)(this.itemToMonitor.attributeId); this.node.removeListener(event_name, this._attribute_changed_callback); this._attribute_changed_callback = null; } if (this._value_changed_callback) { // samplingInterval was 0 for a exception-based data Item // we setup a event listener that we need to unwind here (0, node_opcua_assert_1.assert)(typeof this._value_changed_callback === "function"); (0, node_opcua_assert_1.assert)(!this._samplingId); this.node.removeListener("value_changed", this._value_changed_callback); this._value_changed_callback = null; } if (this._semantic_changed_callback) { (0, node_opcua_assert_1.assert)(typeof this._semantic_changed_callback === "function"); (0, node_opcua_assert_1.assert)(!this._samplingId); this.node.removeListener("semantic_changed", this._semantic_changed_callback); this._semantic_changed_callback = null; } if (this._samplingId) { this._clear_timer(); } (0, node_opcua_assert_1.assert)(!this._samplingId); (0, node_opcua_assert_1.assert)(!this._value_changed_callback); (0, node_opcua_assert_1.assert)(!this._semantic_changed_callback); (0, node_opcua_assert_1.assert)(!this._attribute_changed_callback); (0, node_opcua_assert_1.assert)(!this._on_opcua_event_received_callback); } _on_value_changed(dataValue, indexRange) { (0, node_opcua_assert_1.assert)(dataValue instanceof node_opcua_data_value_1.DataValue); this.recordValue(dataValue, false, indexRange); } _on_semantic_changed() { const dataValue = this.node.readValue(); this._on_value_changed(dataValue); } _on_opcua_event(eventData) { // TO DO : => Improve Filtering, bearing in mind that .... // Release 1.04 8 OPC Unified Architecture, Part 9 // 4.5 Condition state synchronization // To ensure a Client is always informed, the three special EventTypes // (RefreshEndEventType, RefreshStartEventType and RefreshRequiredEventType) // ignore the Event content filtering associated with a Subscription and will always be // delivered to the Client. // istanbul ignore next if (!this.filter || !(this.filter instanceof node_opcua_service_filter_2.EventFilter)) { throw new Error("Internal Error : a EventFilter is requested"); } const addressSpace = eventData.$eventDataSource?.addressSpace; if (!(0, check_where_clause_on_address_space_1.checkWhereClauseOnAdressSpace)(addressSpace, node_opcua_address_space_1.SessionContext.defaultContext, this.filter.whereClause, eventData)) { return; } const selectClauses = this.filter.selectClauses ? this.filter.selectClauses : []; const eventFields = (0, node_opcua_service_filter_1.extractEventFields)(node_opcua_address_space_1.SessionContext.defaultContext, selectClauses, eventData); // istanbul ignore next if (doDebug) { debugLog(" RECEIVED INTERNAL EVENT THAT WE ARE MONITORING"); debugLog(this.filter ? this.filter.toString() : "no filter"); eventFields.forEach((e) => { debugLog(e.toString()); }); } this._enqueue_event(eventFields); } _getSession() { if (!this.$subscription) { return null; } if (!this.$subscription.$session) { return null; } return this.$subscription.$session; } _start_sampling(recordInitialValue) { // istanbul ignore next if (!this.node) { return; // we just want to ignore here ... } this.oldDataValue = badDataUnavailable; setImmediate(() => this.__start_sampling(recordInitialValue)); } __acquireInitialValue(sessionContext, callback) { // acquire initial value from the variable/object not itself or from the last known value if we have // one already (0, node_opcua_assert_1.assert)(this.itemToMonitor.attributeId === node_opcua_data_model_2.AttributeIds.Value); (0, node_opcua_assert_1.assert)(this.node); if (this.node?.nodeClass !== node_opcua_data_model_1.NodeClass.Variable) { return callback(new Error("Invalid ")); } const variable = this.node; if (this.oldDataValue == badDataUnavailable) { variable.readValueAsync(sessionContext, (err, dataValue) => { callback(err, dataValue); }); } else { const o = this.oldDataValue; this.oldDataValue = badDataUnavailable; // istanbul ignore next if (doDebug) { safeGuardRegister(this); } callback(null, o); } } __start_sampling(recordInitialValue) { // istanbul ignore next if (!this.node) { return; // we just want to ignore here ... } const sessionContext = this.getSessionContext() || node_opcua_address_space_1.SessionContext.defaultContext; // istanbul ignore next if (!sessionContext) { return; } this._stop_sampling(); if (this.itemToMonitor.attributeId === node_opcua_data_model_2.AttributeIds.EventNotifier) { // istanbul ignore next if (doDebug) { debugLog("xxxxxx monitoring EventNotifier on", this.node.nodeId.toString(), this.node.browseName.toString()); } if (!this._on_opcua_event_received_callback) { // we are monitoring OPCUA Event this._on_opcua_event_received_callback = this._on_opcua_event.bind(this); if (this.node && this.node.nodeClass == node_opcua_data_model_1.NodeClass.Object) { this.node.on("event", this._on_opcua_event_received_callback); } } return; } if (this.itemToMonitor.attributeId !== node_opcua_data_model_2.AttributeIds.Value) { // sampling interval only applies to Value Attributes. this.samplingInterval = 0; // turned to exception-based regardless of requested sampling interval // non value attribute only react on value change if (!this._attribute_changed_callback) { this._attribute_changed_callback = this._on_value_changed.bind(this); const event_name = (0, node_opcua_address_space_1.makeAttributeEventName)(this.itemToMonitor.attributeId); this.node.on(event_name, this._attribute_changed_callback); } if (recordInitialValue) { // read initial value const dataValue = this.node.readAttribute(sessionContext, this.itemToMonitor.attributeId); this.recordValue(dataValue, true); } return; } if (this.samplingInterval === 0) { // we have a exception-based dataItem : event based model, so we do not need a timer // rather , we setup the "value_changed_event"; if (!this._value_changed_callback) { (0, node_opcua_assert_1.assert)(!this._semantic_changed_callback); this._value_changed_callback = this._on_value_changed.bind(this); this._semantic_changed_callback = this._on_semantic_changed.bind(this); if (this.node.nodeClass == node_opcua_data_model_1.NodeClass.Variable) { this.node.on("value_changed", this._value_changed_callback); this.node.on("semantic_changed", this._semantic_changed_callback); } } // initiate first read if (recordInitialValue) { this.__acquireInitialValue(sessionContext, (err, dataValue) => { if (err) { warningLog(err.message); } if (!err && dataValue) { this.recordValue(dataValue.clone(), true); } }); } } else { if (recordInitialValue) { this.__acquireInitialValue(sessionContext, (err, dataValue) => { if (err) { warningLog(err.message); } if (!err && dataValue) { this.recordValue(dataValue, true); } this._set_timer(); }); } else { this._set_timer(); } } } _set_parameters(monitoredParameters) { _validate_parameters(monitoredParameters); // only change clientHandle if it is valid (0<X<MAX) if (monitoredParameters.clientHandle !== 0 && monitoredParameters.clientHandle !== 4294967295) { this.clientHandle = monitoredParameters.clientHandle; } // The Server may support data that is collected based on a sampling model or generated based on an // exception-based model. 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. if (this.node && this.node.nodeClass === node_opcua_data_model_1.NodeClass.Variable) { const variable = this.node; this.samplingInterval = _adjust_sampling_interval(monitoredParameters.samplingInterval, variable.minimumSamplingInterval || 0); } else { this.samplingInterval = _adjust_sampling_interval(monitoredParameters.samplingInterval, 0); } this.discardOldest = monitoredParameters.discardOldest; this.queueSize = _adjust_queue_size(monitoredParameters.queueSize); // change filter this.filter = monitoredParameters.filter || null; } _setOverflowBit(notification) { if (Object.prototype.hasOwnProperty.call(notification, "value")) { (0, node_opcua_assert_1.assert)(notification.value.statusCode.equals(node_opcua_status_code_1.StatusCodes.Good)); notification.value.statusCode = node_opcua_status_code_1.StatusCode.makeStatusCode(notification.value.statusCode, "Overflow | InfoTypeDataValue"); (0, node_opcua_assert_1.assert)((0, node_opcua_data_value_1.sameStatusCode)(notification.value.statusCode, node_opcua_status_code_1.StatusCodes.GoodWithOverflowBit)); (0, node_opcua_assert_1.assert)(notification.value.statusCode.hasOverflowBit); } if (this.$subscription && this.$subscription.subscriptionDiagnostics) { this.$subscription.subscriptionDiagnostics.monitoringQueueOverflowCount++; } // to do: eventQueueOverflowCount } _enqueue_notification(notification) { if (this.queueSize === 1) { // https://reference.opcfoundation.org/v104/Core/docs/Part4/5.12.1/#5.12.1.5 // If the queue size is one, the queue becomes a buffer that always contains the newest // Notification. In this case, if the sampling interval of the MonitoredItem is faster // than the publishing interval of the Subscription, the MonitoredItem will be over // sampling and the Client will always receive the most up-to-date value. // The discard policy is ignored if the queue size is one. // ensure queue size if (!this.queue || this.queue.length !== 1) { this.queue = []; } this.queue[0] = notification; (0, node_opcua_assert_1.assert)(this.queue.length === 1); } else { if (this.discardOldest) { // push new value to queue this.queue.push(notification); if (this.queue.length > this.queueSize) { this.overflow = true; this.queue.shift(); // remove front element // set overflow bit this._setOverflowBit(this.queue[0]); } } else { if (this.queue.length < this.queueSize) { this.queue.push(notification); } else { this.overflow = true; this._setOverflowBit(notification); this.queue[this.queue.length - 1] = notification; } } } (0, node_opcua_assert_1.assert)(this.queue.length >= 1); } _makeDataChangeNotification(dataValue) { if (this.clientHandle === -1 || this.clientHandle === 4294967295) { debugLog("Invalid client handle"); } const attributeId = this.itemToMonitor.attributeId; // if dataFilter is specified ....