node-opcua-server
Version:
pure nodejs OPCUA SDK - module server
965 lines (964 loc) • 56.1 kB
JavaScript
"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 ....