node-opcua-server
Version:
pure nodejs OPCUA SDK - module server
1,333 lines (1,136 loc) • 73.1 kB
text/typescript
/**
* @module node-opcua-server
*/
// tslint:disable:no-console
import { EventEmitter } from "events";
import chalk from "chalk";
import {
SessionContext,
AddressSpace,
BaseNode,
Duration,
UAObjectType,
ISessionContext,
IAddressSpace,
UAVariable,
UAObject,
UAMethod
} from "node-opcua-address-space";
import { assert } from "node-opcua-assert";
import { Byte, UInt32 } from "node-opcua-basic-types";
import { SubscriptionDiagnosticsDataType } from "node-opcua-common";
import { NodeClass, AttributeIds, isValidDataEncoding, QualifiedNameLike } from "node-opcua-data-model";
import { DataValue, TimestampsToReturn } from "node-opcua-data-value";
import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
import { NodeId } from "node-opcua-nodeid";
import { NumericRange } from "node-opcua-numeric-range";
import { ObjectRegistry } from "node-opcua-object-registry";
import { SequenceNumberGenerator } from "node-opcua-secure-channel";
import { EventFilter, checkSelectClauses } from "node-opcua-service-filter";
import { AggregateFilter } from "node-opcua-service-subscription";
import {
DataChangeNotification,
EventNotificationList,
MonitoringMode,
MonitoredItemCreateResult,
MonitoredItemNotification,
PublishResponse,
NotificationMessage,
StatusChangeNotification,
DataChangeFilter,
MonitoredItemCreateRequest
} from "node-opcua-service-subscription";
import { StatusCode, StatusCodes } from "node-opcua-status-code";
import { AggregateFilterResult, ContentFilterResult, EventFieldList, EventFilterResult, MonitoringFilter, NotificationData } from "node-opcua-types";
import { Queue } from "./queue";
import { MonitoredItem, MonitoredItemOptions, QueueItem } from "./monitored_item";
import { ServerSession } from "./server_session";
import { validateFilter } from "./validate_filter";
import { IServerSidePublishEngine, TransferredSubscription } from "./i_server_side_publish_engine";
const debugLog = make_debugLog(__filename);
const doDebug = checkDebugFlag(__filename);
const warningLog = make_warningLog(__filename);
const maxNotificationMessagesInQueue = 100;
export interface SubscriptionDiagnosticsDataTypePriv extends SubscriptionDiagnosticsDataType {
$subscription: Subscription;
}
export enum SubscriptionState {
CLOSED = 1, // The Subscription has not yet been created or has terminated.
CREATING = 2, // The Subscription is being created
NORMAL = 3, // The Subscription is cyclically checking for Notifications from its MonitoredItems.
// The keep-alive counter is not used in this state.
LATE = 4, // The publishing timer has expired and there are Notifications available or a keep-alive Message is
// 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.
KEEPALIVE = 5, // The Subscription is cyclically checking for Notification
// alive counter to count down to 0 from its maximum.
TERMINATED = 6
}
function _adjust_publishing_interval(publishingInterval?: number): number {
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?: number /*,publishingInterval*/): number {
maxKeepAliveCount = maxKeepAliveCount || minimumMaxKeepAliveCount;
maxKeepAliveCount = Math.max(maxKeepAliveCount, minimumMaxKeepAliveCount);
maxKeepAliveCount = Math.min(maxKeepAliveCount, maximumMaxKeepAliveCount);
return maxKeepAliveCount;
}
const MaxUint32 = 0xffffffff;
function _adjust_lifeTimeCount(lifeTimeCount: number, maxKeepAliveCount: number, publishingInterval: number): number {
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?: boolean | null): boolean {
return publishingEnabled === null || publishingEnabled === undefined ? true : !!publishingEnabled;
}
function _adjust_maxNotificationsPerPublish(maxNotificationsPerPublish?: number): number {
assert(Subscription.maxNotificationPerPublishHighLimit > 0, "Subscription.maxNotificationPerPublishHighLimit must be positive");
maxNotificationsPerPublish = maxNotificationsPerPublish || 0;
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);
}
assert(maxNotificationsPerPublish !== 0 && maxNotificationsPerPublish <= Subscription.maxNotificationPerPublishHighLimit);
return maxNotificationsPerPublish;
}
function w(s: string | number, length: number): string {
return ("000" + s).padStart(length);
}
function t(d: Date): string {
return w(d.getHours(), 2) + ":" + w(d.getMinutes(), 2) + ":" + w(d.getSeconds(), 2) + ":" + w(d.getMilliseconds(), 3);
}
function _getSequenceNumbers(arr: NotificationMessage[]): number[] {
return arr.map((notificationMessage) => notificationMessage.sequenceNumber);
}
function analyzeEventFilterResult(node: BaseNode, eventFilter: EventFilter): EventFilterResult {
/* istanbul ignore next */
if (!(eventFilter instanceof EventFilter)) {
throw new Error("Internal Error");
}
const selectClauseResults = checkSelectClauses(node as UAObjectType, eventFilter.selectClauses || []);
const whereClauseResult = new ContentFilterResult();
return new EventFilterResult({
selectClauseDiagnosticInfos: [],
selectClauseResults,
whereClauseResult
});
}
function analyzeDataChangeFilterResult(node: BaseNode, dataChangeFilter: DataChangeFilter): null {
assert(dataChangeFilter instanceof DataChangeFilter);
// the opcua specification doesn't provide dataChangeFilterResult
return null;
}
function analyzeAggregateFilterResult(node: BaseNode, aggregateFilter: AggregateFilter): AggregateFilterResult {
assert(aggregateFilter instanceof AggregateFilter);
return new AggregateFilterResult({});
}
function _process_filter(node: BaseNode, filter: any): EventFilterResult | AggregateFilterResult | null {
if (!filter) {
return null;
}
if (filter instanceof EventFilter) {
return analyzeEventFilterResult(node, filter);
} else if (filter instanceof DataChangeFilter) {
return analyzeDataChangeFilterResult(node, filter);
} else if (filter instanceof AggregateFilter) {
return analyzeAggregateFilterResult(node, filter);
}
// istanbul ignore next
throw new Error("invalid filter");
}
/**
* @private
*/
function createSubscriptionDiagnostics(subscription: Subscription): SubscriptionDiagnosticsDataTypePriv {
assert(subscription instanceof Subscription);
const subscriptionDiagnostics = new SubscriptionDiagnosticsDataType({});
const subscription_subscriptionDiagnostics = subscriptionDiagnostics as SubscriptionDiagnosticsDataTypePriv as any;
subscription_subscriptionDiagnostics.$subscription = subscription;
// "sessionId"
subscription_subscriptionDiagnostics.__defineGetter__(
"sessionId",
function (this: SubscriptionDiagnosticsDataTypePriv): NodeId {
if (!this.$subscription) {
return NodeId.nullNodeId;
}
return this.$subscription.getSessionId();
}
);
subscription_subscriptionDiagnostics.__defineGetter__(
"subscriptionId",
function (this: SubscriptionDiagnosticsDataTypePriv): number {
if (!this.$subscription) {
return 0;
}
return this.$subscription.id;
}
);
subscription_subscriptionDiagnostics.__defineGetter__("priority", function (this: SubscriptionDiagnosticsDataTypePriv): number {
if (!this.$subscription) {
return 0;
}
return this.$subscription.priority;
});
subscription_subscriptionDiagnostics.__defineGetter__(
"publishingInterval",
function (this: SubscriptionDiagnosticsDataTypePriv): number {
if (!this.$subscription) {
return 0;
}
return this.$subscription.publishingInterval;
}
);
subscription_subscriptionDiagnostics.__defineGetter__("maxLifetimeCount", function (this: SubscriptionDiagnosticsDataTypePriv) {
return this.$subscription.lifeTimeCount;
});
subscription_subscriptionDiagnostics.__defineGetter__(
"maxKeepAliveCount",
function (this: SubscriptionDiagnosticsDataTypePriv): number {
if (!this.$subscription) {
return 0;
}
return this.$subscription.maxKeepAliveCount;
}
);
subscription_subscriptionDiagnostics.__defineGetter__(
"maxNotificationsPerPublish",
function (this: SubscriptionDiagnosticsDataTypePriv): number {
if (!this.$subscription) {
return 0;
}
return this.$subscription.maxNotificationsPerPublish;
}
);
subscription_subscriptionDiagnostics.__defineGetter__(
"publishingEnabled",
function (this: SubscriptionDiagnosticsDataTypePriv): boolean {
if (!this.$subscription) {
return false;
}
return this.$subscription.publishingEnabled;
}
);
subscription_subscriptionDiagnostics.__defineGetter__(
"monitoredItemCount",
function (this: SubscriptionDiagnosticsDataTypePriv): number {
if (!this.$subscription) {
return 0;
}
return this.$subscription.monitoredItemCount;
}
);
subscription_subscriptionDiagnostics.__defineGetter__(
"nextSequenceNumber",
function (this: SubscriptionDiagnosticsDataTypePriv): number {
if (!this.$subscription) {
return 0;
}
return this.$subscription.futureSequenceNumber;
}
);
subscription_subscriptionDiagnostics.__defineGetter__(
"disabledMonitoredItemCount",
function (this: SubscriptionDiagnosticsDataTypePriv): number {
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 (this: SubscriptionDiagnosticsDataTypePriv): number {
if (!this.$subscription) {
return 0;
}
return this.$subscription.currentKeepAliveCount;
}
);
subscription_subscriptionDiagnostics.__defineGetter__(
"currentLifetimeCount",
function (this: SubscriptionDiagnosticsDataTypePriv): number {
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 as SubscriptionDiagnosticsDataTypePriv;
}
interface IGlobalMonitoredItemCounter {
totalMonitoredItemCount: number;
}
export interface SubscriptionOptions {
sessionId?: NodeId;
/**
* (default:1000) the publishing interval.
*/
publishingInterval?: number;
/**
* (default:10) the max Life Time Count
*/
maxKeepAliveCount?: number;
lifeTimeCount?: number;
/**
* (default:true)
*/
publishingEnabled?: boolean;
/**
* (default:0)
*/
maxNotificationsPerPublish?: number;
/**
* subscription priority Byte:(0-255)
*/
priority?: number;
publishEngine?: IServerSidePublishEngine;
/**
* a unique identifier
*/
id?: number;
serverCapabilities: ServerCapabilitiesPartial;
globalCounter: IGlobalMonitoredItemCounter;
}
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;
export type Notification = DataChangeNotification | EventNotificationList | StatusChangeNotification;
export type Counter = number;
export interface ModifySubscriptionParameters {
/**
* requestedPublishingInterval =0 means fastest possible
*/
requestedPublishingInterval?: Duration;
/*
* requestedLifetimeCount=0 means no change
*/
requestedLifetimeCount?: Counter;
/**
* requestedMaxKeepAliveCount ===0 means no change
*/
requestedMaxKeepAliveCount?: Counter;
maxNotificationsPerPublish?: Counter;
priority?: Byte;
}
export interface GetMonitoredItemsResult {
/**
* array of serverHandles for all MonitoredItems of the subscription
* identified by subscriptionId.
*/
serverHandles: Uint32Array;
/**
* array of clientHandles for all MonitoredItems of the subscription
* identified by subscriptionId.
*/
clientHandles: Uint32Array;
statusCode: StatusCode;
}
export interface InternalNotification {
monitoredItemId?: number;
notification: QueueItem | StatusChangeNotification;
publishTime: Date;
start_tick: number;
}
export interface InternalCreateMonitoredItemResult {
monitoredItem?: MonitoredItem;
monitoredItemCreateRequest: MonitoredItemCreateRequest;
createResult: MonitoredItemCreateResult;
}
export interface MonitoredItemBase {
node: UAVariable | UAObject | UAMethod | null;
// from monitoring parameters
filter: MonitoringFilter | null;
monitoringMode: MonitoringMode;
timestampsToReturn: TimestampsToReturn;
discardOldest: boolean;
queueSize: number;
clientHandle: UInt32;
}
export type CreateMonitoredItemHook = (subscription: Subscription, monitoredItem: MonitoredItemBase) => Promise<StatusCode>;
export type DeleteMonitoredItemHook = (subscription: Subscription, monitoredItem: MonitoredItemBase) => Promise<StatusCode>;
export interface ServerCapabilitiesPartial {
maxMonitoredItems: UInt32;
maxMonitoredItemsPerSubscription: UInt32;
}
export interface IReadAttributeCapable {
readAttribute(
context: ISessionContext | null,
attributeId: AttributeIds,
indexRange?: NumericRange,
dataEncoding?: QualifiedNameLike | null
): DataValue;
}
/**
* The Subscription class used in the OPCUA server side.
*/
export class Subscription extends EventEmitter {
public static minimumPublishingInterval = 50; // fastest possible
public static defaultPublishingInterval = 1000; // one second
public static maximumPublishingInterval: number = 1000 * 60; // one minute
public static maxNotificationPerPublishHighLimit = 1000;
public static minimumLifetimeDuration = 5 * 1000; // // we want 2 seconds minimum lifetime for any subscription
public static maximumLifetimeDuration = 60 * 60 * 1000; // 1 hour
/**
* maximum number of monitored item in a subscription to be used
* when serverCapacity.maxMonitoredItems and serverCapacity.maxMonitoredItemsPerSubscription are not set.
*/
public static defaultMaxMonitoredItemCount = 20000;
/**
* @deprecated use serverCapacity.maxMonitoredItems and serverCapacity.maxMonitoredItemsPerSubscription instead
*/
protected static get maxMonitoredItemCount() {
return Subscription.defaultMaxMonitoredItemCount;
}
public static registry = new ObjectRegistry();
public publishEngine?: IServerSidePublishEngine;
public id: number;
public priority: number;
/**
* the Subscription publishing interval
* @default 1000
*/
public publishingInterval: number;
/**
* The keep alive count defines how many times the publish interval need to
* expires without having notifications available before the server send an
* empty message.
* OPCUA Spec says: a value of 0 is invalid.
* @default 10
*
*/
public maxKeepAliveCount: number;
/**
* The life time count defines how many times the publish interval expires without
* having a connection to the client to deliver data.
* If the life time count reaches maxKeepAliveCount, the subscription will
* automatically terminate.
* OPCUA Spec: The life-time count shall be a minimum of three times the keep keep-alive count.
*
* Note: this has to be interpreted as without having a PublishRequest available
* @default 1
*/
public lifeTimeCount: number;
/**
* The maximum number of notifications that the Client wishes to receive in a
* single Publish response. A value of zero indicates that there is no limit.
* The number of notifications per Publish is the sum of monitoredItems in the
* DataChangeNotification and events in the EventNotificationList.
*
* @property maxNotificationsPerPublish
* @default 0
*/
public maxNotificationsPerPublish: number;
public publishingEnabled: boolean;
public subscriptionDiagnostics: SubscriptionDiagnosticsDataTypePriv;
public publishIntervalCount: number;
/**
* number of monitored Item
*/
public monitoredItemIdCounter: number;
private _state: SubscriptionState = -1 as SubscriptionState;
public set state(value: SubscriptionState) {
if (this._state !== value) {
this._state = value;
this.emit("stateChanged", value);
}
}
public get state(): SubscriptionState {
return this._state;
}
public messageSent: boolean;
public $session?: ServerSession;
public get sessionId(): NodeId {
return this.$session ? this.$session.nodeId : NodeId.nullNodeId;
}
public get currentLifetimeCount(): number {
return this._life_time_counter;
}
public get currentKeepAliveCount(): number {
return this._keep_alive_counter;
}
private _life_time_counter: number;
protected _keep_alive_counter = 0;
public _pending_notifications: Queue<InternalNotification>;
private _sent_notification_messages: NotificationMessage[];
private readonly _sequence_number_generator: SequenceNumberGenerator;
private readonly monitoredItems: { [key: number]: MonitoredItem };
private timerId: any;
private _hasUncollectedMonitoredItemNotifications = false;
private globalCounter: IGlobalMonitoredItemCounter;
private serverCapabilities: ServerCapabilitiesPartial;
constructor(options: SubscriptionOptions) {
super();
options = options || {};
Subscription.registry.register(this);
assert(this.sessionId instanceof 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<InternalNotification>();
this._sent_notification_messages = [];
this._sequence_number_generator = new 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.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;
}
public getSessionId(): NodeId {
return this.sessionId;
}
public toString(): string {
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
*/
public modify(param: ModifySubscriptionParameters): void {
// 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
*/
public setPublishingMode(publishingEnabled: boolean): StatusCode {
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 StatusCodes.Good;
}
/**
* @private
*/
public get keepAliveCounterHasExpired(): boolean {
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
*/
public resetLifeTimeCounter(): void {
this._life_time_counter = 0;
}
/**
* @private
*/
public increaseLifeTimeCounter(): void {
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.
*
*/
public get lifeTimeHasExpired(): boolean {
assert(this.lifeTimeCount > 0);
return this._life_time_counter >= this.lifeTimeCount;
}
/**
* number of milliseconds before this subscription times out (lifeTimeHasExpired === true);
*/
public get timeToExpiration(): number {
return (this.lifeTimeCount - this._life_time_counter) * this.publishingInterval;
}
public get timeToKeepAlive(): number {
return (this.maxKeepAliveCount - this._keep_alive_counter) * this.publishingInterval;
}
/**
* Terminates the subscription.
* Calling this method will also remove any monitored items.
*
*/
public terminate(): void {
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));
assert(status === StatusCodes.Good);
}
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);
}
}
public setTriggering(
triggeringItemId: number,
linksToAdd: number[] | null,
linksToRemove: number[] | null
): { statusCode: StatusCode; addResults: StatusCode[]; removeResults: StatusCode[] } {
/** Bad_NothingToDo, Bad_TooManyOperations,Bad_SubscriptionIdInvalid, Bad_MonitoredItemIdInvalid */
linksToAdd = linksToAdd || [];
linksToRemove = linksToRemove || [];
if (linksToAdd.length === 0 && linksToRemove.length === 0) {
return { statusCode: 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: StatusCode[] = monitoredItemsToRemove.map((m) =>
m ? StatusCodes.Good : StatusCodes.BadMonitoredItemIdInvalid
);
const addResults1: StatusCode[] = monitoredItemsToAdd.map((m) =>
m ? StatusCodes.Good : StatusCodes.BadMonitoredItemIdInvalid
);
return {
statusCode: StatusCodes.BadMonitoredItemIdInvalid,
addResults: addResults1,
removeResults: removeResults1
};
}
//
// note: it seems that CTT imposed that we do remove before add
const removeResults = monitoredItemsToRemove.map((m) =>
!m ? StatusCodes.BadMonitoredItemIdInvalid : triggeringItem.removeLinkItem(m.monitoredItemId)
);
const addResults = monitoredItemsToAdd.map((m) =>
!m ? StatusCodes.BadMonitoredItemIdInvalid : triggeringItem.addLinkItem(m.monitoredItemId)
);
const statusCode: StatusCode = StatusCodes.Good;
// do binding
return {
statusCode,
addResults,
removeResults
};
}
public dispose(): void {
// istanbul ignore next
if (doDebug) {
debugLog("Subscription#dispose", this.id, this.monitoredItemCount);
}
assert(this.monitoredItemCount === 0, "MonitoredItems haven't been deleted first !!!");
assert(this.timerId === null, "Subscription timer haven't been terminated");
if (this.subscriptionDiagnostics) {
(this.subscriptionDiagnostics as SubscriptionDiagnosticsDataTypePriv).$subscription = null as any as Subscription;
}
this.publishEngine = undefined;
this._pending_notifications.clear();
this._sent_notification_messages = [];
this.$session = undefined;
this.removeAllListeners();
Subscription.registry.unregister(this);
}
public get aborted(): boolean {
const session = this.$session;
if (!session) {
return true;
}
return session.aborted;
}
/**
* number of pending notifications
*/
public get pendingNotificationsCount(): number {
return this._pending_notifications ? this._pending_notifications.size : 0;
}
/**
* is 'true' if there are pending notifications for this subscription. (i.e moreNotifications)
*/
public get hasPendingNotifications(): boolean {
return this.pendingNotificationsCount > 0;
}
/**
* number of sent notifications
*/
public get sentNotificationMessageCount(): number {
return this._sent_notification_messages.length;
}
/**
* @internal
*/
public _flushSentNotifications(): NotificationMessage[] {
const tmp = this._sent_notification_messages;
this._sent_notification_messages = [];
return tmp;
}
/**
* number of monitored items handled by this subscription
*/
public get monitoredItemCount(): number {
return Object.keys(this.monitoredItems).length;
}
/**
* number of disabled monitored items.
*/
public get disabledMonitoredItemCount(): number {
return Object.values(this.monitoredItems).reduce((sum: number, monitoredItem: MonitoredItem) => {
return sum + (monitoredItem.monitoringMode === MonitoringMode.Disabled ? 1 : 0);
}, 0);
}
/**
* The number of unacknowledged messages saved in the republish queue.
*/
public get unacknowledgedMessageCount(): number {
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
*/
public adjustSamplingInterval(samplingInterval: number, node?: IReadAttributeCapable): number {
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(
SessionContext.defaultContext,
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;
assert(samplingInterval >= 0 && samplingInterval <= MonitoredItem.maximumSamplingInterval);
// note : at this stage, a samplingInterval===0 means that the data item is really exception-based
}
} else if (samplingInterval < MonitoredItem.minimumSamplingInterval) {
samplingInterval = MonitoredItem.minimumSamplingInterval;
} else if (samplingInterval > MonitoredItem.maximumSamplingInterval) {
// If the requested samplingInterval is higher than the
// maximum sampling interval supported by the Server, the maximum sampling
// interval is returned.
samplingInterval = MonitoredItem.maximumSamplingInterval;
}
const node_minimumSamplingInterval =
node && (node as any).minimumSamplingInterval ? (node as any).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
*/
public preCreateMonitoredItem(
addressSpace: IAddressSpace,
timestampsToReturn: TimestampsToReturn,
monitoredItemCreateRequest: MonitoredItemCreateRequest
): InternalCreateMonitoredItemResult {
assert(monitoredItemCreateRequest instanceof MonitoredItemCreateRequest);
function handle_error(statusCode: StatusCode): InternalCreateMonitoredItemResult {
return {
createResult: new MonitoredItemCreateResult({ statusCode }),
monitoredItemCreateRequest
};
}
const itemToMonitor = monitoredItemCreateRequest.itemToMonitor;
const node = addressSpace.findNode(itemToMonitor.nodeId) as UAObject | UAVariable | UAMethod;
if (!node || (node.nodeClass !== NodeClass.Variable && node.nodeClass !== NodeClass.Object && node.nodeClass !== NodeClass.Method)) {
return handle_error(StatusCodes.BadNodeIdUnknown);
}
if (itemToMonitor.attributeId === AttributeIds.Value && !(node.nodeClass === NodeClass.Variable)) {
// AttributeIds.Value is only valid for monitoring value of UAVariables.
return handle_error(StatusCodes.BadAttributeIdInvalid);
}
if (itemToMonitor.attributeId === AttributeIds.INVALID) {
return handle_error(StatusCodes.BadAttributeIdInvalid);
}
if (!itemToMonitor.indexRange.isValid()) {
return handle_error(StatusCodes.BadIndexRangeInvalid);
}
// check dataEncoding applies only on Values
if (itemToMonitor.dataEncoding.name && itemToMonitor.attributeId !== AttributeIds.Value) {
return handle_error(StatusCodes.BadDataEncodingInvalid);
}
// check dataEncoding
if (!isValidDataEncoding(itemToMonitor.dataEncoding)) {
return handle_error(StatusCodes.BadDataEncodingUnsupported);
}
// check that item can be read by current user session
// filter
const requestedParameters = monitoredItemCreateRequest.requestedParameters;
const filter = requestedParameters.filter;
const statusCodeFilter = validateFilter(filter, itemToMonitor, node);
if (statusCodeFilter !== StatusCodes.Good) {
return handle_error(statusCodeFilter);
}
// do we have enough room for new monitored items ?
if (this.monitoredItemCount >= this.serverCapabilities.maxMonitoredItemsPerSubscription) {
return handle_error(StatusCodes.BadTooManyMonitoredItems);
}
if (this.globalCounter.totalMonitoredItemCount >= this.serverCapabilities.maxMonitoredItems) {
return handle_error(StatusCodes.BadTooManyMonitoredItems);
}
const createResult = this._createMonitoredItemStep2(timestampsToReturn, monitoredItemCreateRequest, node);
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 };
}
public async applyOnMonitoredItem(functor: (monitoredItem: MonitoredItem) => Promise<void>): Promise<void> {
for (const m of Object.values(this.monitoredItems)) {
await functor(m);
}
}
public postCreateMonitoredItem(
monitoredItem: MonitoredItem,
monitoredItemCreateRequest: MonitoredItemCreateRequest,
createResult: MonitoredItemCreateResult
): void {
this._createMonitoredItemStep3(monitoredItem, monitoredItemCreateRequest);
}
public async createMonitoredItem(
addressSpace: IAddressSpace,
timestampsToReturn: TimestampsToReturn,
monitoredItemCreateRequest: MonitoredItemCreateRequest
): Promise<MonitoredItemCreateResult> {
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
*/
public getMonitoredItem(monitoredItemId: number): MonitoredItem | null {
return this.monitoredItems[monitoredItemId] || null;
}
/**
* remove a monitored Item from the subscription.
* @param monitoredItemId : the id of the monitored item to get.
*/
public removeMonitoredItem(monitoredItemId: number): StatusCode {
debugLog("Removing monitoredIem ", monitoredItemId);
if (!Object.prototype.hasOwnProperty.call(this.monitoredItems, monitoredItemId.toString())) {
return 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 StatusCodes.Good;
}
/**
* rue if monitored Item have uncollected Notifications
*/
public get hasUncollectedMonitoredItemNotifications(): boolean {
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;
}
public get subscriptionId(): number {
return this.id;
}
public getMessageForSequenceNumber(sequenceNumber: number): NotificationMessage | null {
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
*/
public notificationHasExpired(notification: { start_tick: number }): boolean {
assert(Object.prototype.hasOwnProperty.call(notification, "start_tick"));
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.
*/
public getAvailableSequenceNumbers(): number[] {
const availableSequenceNumbers = _getSequenceNumbers(this._sent_notification_messages);
return availableSequenceNumbers;
}
/**
* acknowledges a notification identified by its sequence number
*/
public acknowledgeNotification(sequenceNumber: number): StatusCode {
debugLog("acknowledgeNotification ", sequenceNumber);
let foundIndex = -1;
this._sent_notification_messages.forEach((e: NotificationMessage, index: number) => {
if (e.sequenceNumber === sequenceNumber) {
foundIndex = index;
}
});
if (foundIndex === -1) {
// istanbul ignore next
if (doDebug) {
debugLog(chalk.red("acknowledging sequence FAILED !!! "), chalk.cyan(sequenceNumber.toString()));
}
return StatusCodes.BadSequenceNumberUnknown;
} else {
// istanbul ignore next
if (doDebug) {
debugLog(chalk.yellow("acknowledging sequence "), chalk.cyan(sequenceNumber.toString()));
}
this._sent_notification_messages.splice(foundIndex, 1);
this.subscriptionDiagnostics.unacknowledgedMessageCount--;
return 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.
*
*/
public getMonitoredItems(): GetMonitoredItemsResult {
const monitoredItems = Object.keys(this.monitoredItems);
const monitoredItemCount = monitoredItems.length;
const result: GetMonitoredItemsResult = {
clientHandles: new Uint32Array(monitoredItemCount),
serverHandles: new Uint32Array(monitoredItemCount),
statusCode: 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
*/
public async resendInitialValues(): Promise<void> {
this._keep_alive_counter = 0;
try {
const promises: Promise<void>[] = [];
for (const monitoredItem of Object.values(this.monitoredItems)) {
promises.push(
(async () => {
try {
monitoredItem.resendInitialValue();
} catch (err) {
warningLog(
"resendInitialValues:",
monitoredItem.node?.nodeId.toString(),
"error:",
(err as any).message
);
}
})()
);
}
await Promise.all(promises);
} catch (err) {
warningLog("resendInitialValues: error:", (err as any).message);
}
// make sure data will be sent immediately
this._keep_alive_counter = this.maxKeepAliveCount - 1 ;
this.state = SubscriptionState.NORMAL;
this._harvestMonitoredItems();
}
/**
* @private
*/
public notifyTransfer(): void {
// 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.red(" Subscription => Notifying Transfer "));
const notificationData = new StatusChangeNotification({
status: StatusCodes.GoodSubscriptionTransferred
});
if (this.publishEngine!.pendingPublishRequestCount) {
// the GoodSubscriptionTransferred can be processed immediately
this._addNotification