node-opcua-server 2.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.mocharc.yml +10 -0
  2. package/LICENSE +20 -0
  3. package/dist/base_server.d.ts +110 -0
  4. package/dist/base_server.js +476 -0
  5. package/dist/base_server.js.map +1 -0
  6. package/dist/factory.d.ts +10 -0
  7. package/dist/factory.js +24 -0
  8. package/dist/factory.js.map +1 -0
  9. package/dist/history_server_capabilities.d.ts +35 -0
  10. package/dist/history_server_capabilities.js +44 -0
  11. package/dist/history_server_capabilities.js.map +1 -0
  12. package/dist/i_channel_data.d.ts +13 -0
  13. package/dist/i_channel_data.js +3 -0
  14. package/dist/i_channel_data.js.map +1 -0
  15. package/dist/i_register_server_manager.d.ts +16 -0
  16. package/dist/i_register_server_manager.js +3 -0
  17. package/dist/i_register_server_manager.js.map +1 -0
  18. package/dist/i_server_side_publish_engine.d.ts +36 -0
  19. package/dist/i_server_side_publish_engine.js +50 -0
  20. package/dist/i_server_side_publish_engine.js.map +1 -0
  21. package/dist/i_socket_data.d.ts +11 -0
  22. package/dist/i_socket_data.js +3 -0
  23. package/dist/i_socket_data.js.map +1 -0
  24. package/dist/index.d.ts +14 -0
  25. package/dist/index.js +27 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/monitored_item.d.ts +173 -0
  28. package/dist/monitored_item.js +1006 -0
  29. package/dist/monitored_item.js.map +1 -0
  30. package/dist/node_sampler.d.ts +3 -0
  31. package/dist/node_sampler.js +76 -0
  32. package/dist/node_sampler.js.map +1 -0
  33. package/dist/opcua_server.d.ts +668 -0
  34. package/dist/opcua_server.js +2407 -0
  35. package/dist/opcua_server.js.map +1 -0
  36. package/dist/queue.d.ts +11 -0
  37. package/dist/queue.js +71 -0
  38. package/dist/queue.js.map +1 -0
  39. package/dist/register_server_manager.d.ts +92 -0
  40. package/dist/register_server_manager.js +574 -0
  41. package/dist/register_server_manager.js.map +1 -0
  42. package/dist/register_server_manager_hidden.d.ts +17 -0
  43. package/dist/register_server_manager_hidden.js +28 -0
  44. package/dist/register_server_manager_hidden.js.map +1 -0
  45. package/dist/register_server_manager_mdns_only.d.ts +19 -0
  46. package/dist/register_server_manager_mdns_only.js +58 -0
  47. package/dist/register_server_manager_mdns_only.js.map +1 -0
  48. package/dist/server_capabilities.d.ts +61 -0
  49. package/dist/server_capabilities.js +109 -0
  50. package/dist/server_capabilities.js.map +1 -0
  51. package/dist/server_end_point.d.ts +180 -0
  52. package/dist/server_end_point.js +825 -0
  53. package/dist/server_end_point.js.map +1 -0
  54. package/dist/server_engine.d.ts +311 -0
  55. package/dist/server_engine.js +1659 -0
  56. package/dist/server_engine.js.map +1 -0
  57. package/dist/server_publish_engine.d.ts +109 -0
  58. package/dist/server_publish_engine.js +531 -0
  59. package/dist/server_publish_engine.js.map +1 -0
  60. package/dist/server_publish_engine_for_orphan_subscriptions.d.ts +16 -0
  61. package/dist/server_publish_engine_for_orphan_subscriptions.js +50 -0
  62. package/dist/server_publish_engine_for_orphan_subscriptions.js.map +1 -0
  63. package/dist/server_session.d.ts +176 -0
  64. package/dist/server_session.js +734 -0
  65. package/dist/server_session.js.map +1 -0
  66. package/dist/server_subscription.d.ts +393 -0
  67. package/dist/server_subscription.js +1313 -0
  68. package/dist/server_subscription.js.map +1 -0
  69. package/dist/sessions_compatible_for_transfer.d.ts +2 -0
  70. package/dist/sessions_compatible_for_transfer.js +36 -0
  71. package/dist/sessions_compatible_for_transfer.js.map +1 -0
  72. package/dist/validate_filter.d.ts +5 -0
  73. package/dist/validate_filter.js +64 -0
  74. package/dist/validate_filter.js.map +1 -0
  75. package/package.json +88 -0
  76. package/source/base_server.ts +617 -0
  77. package/source/factory.ts +25 -0
  78. package/source/history_server_capabilities.ts +75 -0
  79. package/source/i_channel_data.ts +17 -0
  80. package/source/i_register_server_manager.ts +24 -0
  81. package/source/i_server_side_publish_engine.ts +77 -0
  82. package/source/i_socket_data.ts +11 -0
  83. package/source/index.ts +14 -0
  84. package/source/monitored_item.ts +1303 -0
  85. package/source/node_sampler.ts +82 -0
  86. package/source/opcua_server.ts +3742 -0
  87. package/source/queue.ts +73 -0
  88. package/source/register_server_manager.ts +744 -0
  89. package/source/register_server_manager_hidden.ts +33 -0
  90. package/source/register_server_manager_mdns_only.ts +69 -0
  91. package/source/server_capabilities.ts +177 -0
  92. package/source/server_end_point.ts +1182 -0
  93. package/source/server_engine.ts +2167 -0
  94. package/source/server_publish_engine.ts +657 -0
  95. package/source/server_publish_engine_for_orphan_subscriptions.ts +52 -0
  96. package/source/server_session.ts +931 -0
  97. package/source/server_subscription.ts +1792 -0
  98. package/source/sessions_compatible_for_transfer.ts +33 -0
  99. package/source/validate_filter.ts +86 -0
  100. package/test_helpers/create_certificates.js +1 -0
@@ -0,0 +1,1303 @@
1
+ /**
2
+ * @module node-opcua-server
3
+ */
4
+ // tslint:disable:no-console
5
+ import * as chalk from "chalk";
6
+ import { EventEmitter } from "events";
7
+ import { assert } from "node-opcua-assert";
8
+
9
+ import {
10
+ BaseNode,
11
+ IEventData,
12
+ extractEventFields,
13
+ makeAttributeEventName,
14
+ SessionContext,
15
+ UAVariable,
16
+ checkWhereClause,
17
+ AddressSpace
18
+ } from "node-opcua-address-space";
19
+ import { DateTime, UInt32 } from "node-opcua-basic-types";
20
+ import { NodeClass, QualifiedNameOptions } from "node-opcua-data-model";
21
+ import { AttributeIds } from "node-opcua-data-model";
22
+ import {
23
+ apply_timestamps,
24
+ DataValue,
25
+ extractRange,
26
+ sameDataValue,
27
+ coerceTimestampsToReturn,
28
+ sameStatusCode
29
+ } from "node-opcua-data-value";
30
+ import { checkDebugFlag, make_debugLog } from "node-opcua-debug";
31
+ import { ExtensionObject } from "node-opcua-extension-object";
32
+ import { NodeId } from "node-opcua-nodeid";
33
+ import { NumericalRange0, NumericRange } from "node-opcua-numeric-range";
34
+ import { ObjectRegistry } from "node-opcua-object-registry";
35
+ import { EventFilter } from "node-opcua-service-filter";
36
+ import { ReadValueId, TimestampsToReturn } from "node-opcua-service-read";
37
+ import {
38
+ MonitoredItemModifyResult,
39
+ MonitoredItemNotification,
40
+ MonitoringMode,
41
+ MonitoringParameters
42
+ } from "node-opcua-service-subscription";
43
+ import {
44
+ DataChangeFilter,
45
+ DataChangeTrigger,
46
+ DeadbandType,
47
+ isOutsideDeadbandAbsolute,
48
+ isOutsideDeadbandNone,
49
+ isOutsideDeadbandPercent,
50
+ PseudoRange
51
+ } from "node-opcua-service-subscription";
52
+ import { StatusCode, StatusCodes } from "node-opcua-status-code";
53
+ import {
54
+ DataChangeNotification,
55
+ EventFieldList,
56
+ EventNotificationList,
57
+ MonitoringFilter,
58
+ ReadValueIdOptions,
59
+ SimpleAttributeOperand,
60
+ SubscriptionDiagnosticsDataType
61
+ } from "node-opcua-types";
62
+ import { sameVariant, Variant } from "node-opcua-variant";
63
+
64
+ import { appendToTimer, removeFromTimer } from "./node_sampler";
65
+ import { validateFilter } from "./validate_filter";
66
+
67
+ export type QueueItem = MonitoredItemNotification | EventFieldList;
68
+
69
+ const defaultItemToMonitor: ReadValueIdOptions = new ReadValueId({
70
+ attributeId: AttributeIds.Value,
71
+ indexRange: undefined
72
+ });
73
+
74
+ const debugLog = make_debugLog(__filename);
75
+ const doDebug = checkDebugFlag(__filename);
76
+
77
+ function _adjust_sampling_interval(samplingInterval: number, node_minimumSamplingInterval: number): number {
78
+ assert(typeof node_minimumSamplingInterval === "number", "expecting a number");
79
+
80
+ if (samplingInterval === 0) {
81
+ return node_minimumSamplingInterval === 0
82
+ ? samplingInterval
83
+ : Math.max(MonitoredItem.minimumSamplingInterval, node_minimumSamplingInterval);
84
+ }
85
+ assert(samplingInterval >= 0, " this case should have been prevented outside");
86
+ samplingInterval = samplingInterval || MonitoredItem.defaultSamplingInterval;
87
+ samplingInterval = Math.max(samplingInterval, MonitoredItem.minimumSamplingInterval);
88
+ samplingInterval = Math.min(samplingInterval, MonitoredItem.maximumSamplingInterval);
89
+ samplingInterval =
90
+ node_minimumSamplingInterval === 0 ? samplingInterval : Math.max(samplingInterval, node_minimumSamplingInterval);
91
+
92
+ return samplingInterval;
93
+ }
94
+
95
+ const maxQueueSize = 5000;
96
+
97
+ function _adjust_queue_size(queueSize: number): number {
98
+ queueSize = Math.min(queueSize, maxQueueSize);
99
+ queueSize = Math.max(1, queueSize);
100
+ return queueSize;
101
+ }
102
+
103
+ function _validate_parameters(monitoringParameters: any) {
104
+ // xx assert(options instanceof MonitoringParameters);
105
+ assert(monitoringParameters.hasOwnProperty("clientHandle"));
106
+ assert(monitoringParameters.hasOwnProperty("samplingInterval"));
107
+ assert(isFinite(monitoringParameters.clientHandle));
108
+ assert(isFinite(monitoringParameters.samplingInterval));
109
+ assert(typeof monitoringParameters.discardOldest === "boolean");
110
+ assert(isFinite(monitoringParameters.queueSize));
111
+ assert(monitoringParameters.queueSize >= 0);
112
+ }
113
+
114
+ function statusCodeHasChanged(newDataValue: DataValue, oldDataValue: DataValue): boolean {
115
+ assert(newDataValue instanceof DataValue);
116
+ assert(oldDataValue instanceof DataValue);
117
+ return newDataValue.statusCode !== oldDataValue.statusCode;
118
+ }
119
+
120
+ function valueHasChanged(
121
+ this: MonitoredItem,
122
+ newDataValue: DataValue,
123
+ oldDataValue: DataValue,
124
+ deadbandType: DeadbandType,
125
+ deadbandValue: number
126
+ ): boolean {
127
+ assert(newDataValue instanceof DataValue);
128
+ assert(oldDataValue instanceof DataValue);
129
+ switch (deadbandType) {
130
+ case DeadbandType.None:
131
+ assert(newDataValue.value instanceof Variant);
132
+ assert(newDataValue.value instanceof Variant);
133
+ // No Deadband calculation should be applied.
134
+ return isOutsideDeadbandNone(oldDataValue.value, newDataValue.value);
135
+ case DeadbandType.Absolute:
136
+ // AbsoluteDeadband
137
+ return isOutsideDeadbandAbsolute(oldDataValue.value, newDataValue.value, deadbandValue);
138
+ default:
139
+ // Percent_2 PercentDeadband (This type is specified in Part 8).
140
+ assert(deadbandType === DeadbandType.Percent);
141
+
142
+ // The range of the deadbandValue is from 0.0 to 100.0 Percent.
143
+ assert(deadbandValue >= 0 && deadbandValue <= 100);
144
+
145
+ // DeadbandType = PercentDeadband
146
+ // For this type of deadband the deadbandValue is defined as the percentage of the EURange. That is,
147
+ // it applies only to AnalogItems with an EURange Property that defines the typical value range for the
148
+ // item. This range shall be multiplied with the deadbandValue and then compared to the actual value change
149
+ // to determine the need for a data change notification. The following pseudo code shows how the deadband
150
+ // is calculated:
151
+ // DataChange if (absolute value of (last cached value - current value) >
152
+ // (deadbandValue/100.0) * ((high-low) of EURange)))
153
+ //
154
+ // Specifying a deadbandValue outside of this range will be rejected and reported with the
155
+ // StatusCode BadDeadbandFilterInvalid (see Table 27).
156
+ // If the Value of the MonitoredItem is an array, then the deadband calculation logic shall be applied to
157
+ // each element of the array. If an element that requires a DataChange is found, then no further
158
+ // deadband checking is necessary and the entire array shall be returned.
159
+ assert(this.node !== null, "expecting a valid address_space object here to get access the the EURange");
160
+
161
+ const euRangeNode = this.node!.getChildByName("EURange") as UAVariable;
162
+ if (euRangeNode && euRangeNode.nodeClass === NodeClass.Variable) {
163
+ // double,double
164
+ const rangeVariant = euRangeNode.readValue().value;
165
+ return isOutsideDeadbandPercent(
166
+ oldDataValue.value,
167
+ newDataValue.value,
168
+ deadbandValue,
169
+ rangeVariant.value as PseudoRange
170
+ );
171
+ } else {
172
+ console.log("EURange is not of type Variable");
173
+ }
174
+ return true;
175
+ }
176
+ }
177
+
178
+ function timestampHasChanged(t1: DateTime, t2: DateTime): boolean {
179
+ if (t1 || !t2 || t2 || !t1) {
180
+ return true;
181
+ }
182
+ if (!t1 || !t2) {
183
+ return false;
184
+ }
185
+ return (t1 as Date).getTime() !== (t2 as Date).getTime();
186
+ }
187
+
188
+ function isGoodish(statusCode: StatusCode): boolean {
189
+ return statusCode.value < 0x10000000;
190
+ }
191
+
192
+ function apply_dataChange_filter(this: MonitoredItem, newDataValue: DataValue, oldDataValue: DataValue): boolean {
193
+ /* istanbul ignore next */
194
+ if (!this.filter || !(this.filter instanceof DataChangeFilter)) {
195
+ throw new Error("Internal Error");
196
+ }
197
+
198
+ const trigger = this.filter.trigger;
199
+ // istanbul ignore next
200
+ if (doDebug) {
201
+ try {
202
+ debugLog("filter pass ?", DataChangeTrigger[trigger], this.oldDataValue?.toString(), newDataValue.toString());
203
+ if (
204
+ trigger === DataChangeTrigger.Status ||
205
+ trigger === DataChangeTrigger.StatusValue ||
206
+ trigger === DataChangeTrigger.StatusValueTimestamp
207
+ ) {
208
+ debugLog("statusCodeHasChanged ", statusCodeHasChanged(newDataValue, oldDataValue));
209
+ }
210
+ if (trigger === DataChangeTrigger.StatusValue || trigger === DataChangeTrigger.StatusValueTimestamp) {
211
+ debugLog(
212
+ "valueHasChanged ",
213
+ valueHasChanged.call(this, newDataValue, oldDataValue, this.filter!.deadbandType, this.filter!.deadbandValue)
214
+ );
215
+ }
216
+ if (trigger === DataChangeTrigger.StatusValueTimestamp) {
217
+ debugLog("timestampHasChanged ", timestampHasChanged(newDataValue.sourceTimestamp, oldDataValue.sourceTimestamp));
218
+ }
219
+ } catch (err) {
220
+ console.log(err);
221
+ }
222
+ }
223
+ switch (trigger) {
224
+ case DataChangeTrigger.Status: {
225
+ //
226
+ // Status
227
+ // Report a notification ONLY if the StatusCode associated with
228
+ // the value changes. See Table 166 for StatusCodes defined in
229
+ // this standard. Part 8 specifies additional StatusCodes that are
230
+ // valid in particular for device data.
231
+ return statusCodeHasChanged(newDataValue, oldDataValue);
232
+ }
233
+ case DataChangeTrigger.StatusValue: {
234
+ // filtering value changes.
235
+ // change. The Deadband filter can be used in addition for
236
+ // Report a notification if either the StatusCode or the value
237
+ // StatusValue
238
+ // This is the default setting if no filter is set.
239
+ return (
240
+ statusCodeHasChanged(newDataValue, oldDataValue) ||
241
+ valueHasChanged.call(this, newDataValue, oldDataValue, this.filter.deadbandType, this.filter.deadbandValue)
242
+ );
243
+ }
244
+ default: {
245
+ // StatusValueTimestamp
246
+ // Report a notification if either StatusCode, value or the
247
+ // SourceTimestamp change.
248
+ //
249
+ // If a Deadband filter is specified,this trigger has the same behavior as STATUS_VALUE_1.
250
+ //
251
+ // If the DataChangeFilter is not applied to the monitored item, STATUS_VALUE_1
252
+ // is the default reporting behavior
253
+ assert(trigger === DataChangeTrigger.StatusValueTimestamp);
254
+ return (
255
+ timestampHasChanged(newDataValue.sourceTimestamp, oldDataValue.sourceTimestamp) ||
256
+ statusCodeHasChanged(newDataValue, oldDataValue) ||
257
+ valueHasChanged.call(this, newDataValue, oldDataValue, this.filter.deadbandType, this.filter.deadbandValue)
258
+ );
259
+ }
260
+ }
261
+ return false;
262
+ }
263
+
264
+ function apply_filter(this: MonitoredItem, newDataValue: DataValue) {
265
+ if (!this.oldDataValue) {
266
+ return true; // keep
267
+ }
268
+ if (this.filter instanceof DataChangeFilter) {
269
+ return apply_dataChange_filter.call(this, newDataValue, this.oldDataValue);
270
+ } else {
271
+ // if filter not set, by default report changes to Status or Value only
272
+ if (newDataValue.statusCode.value !== this.oldDataValue.statusCode.value) {
273
+ return true; // Keep because statusCode has changed ...
274
+ }
275
+ return !sameVariant(newDataValue.value, this.oldDataValue.value);
276
+ }
277
+ return true; // keep
278
+ // else {
279
+ // return !sameDataValue(newDataValue, this.oldDataValue, TimestampsToReturn.Neither);
280
+ // }
281
+ // return true; // keep
282
+ }
283
+
284
+ function setSemanticChangeBit(notification: QueueItem | DataValue): void {
285
+ if (notification instanceof MonitoredItemNotification) {
286
+ notification.value.statusCode = StatusCode.makeStatusCode(
287
+ notification.value.statusCode || StatusCodes.Good,
288
+ "SemanticChanged"
289
+ );
290
+ } else if (notification instanceof DataValue) {
291
+ notification.statusCode = StatusCode.makeStatusCode(notification.statusCode || StatusCodes.Good, "SemanticChanged");
292
+ }
293
+ }
294
+
295
+ const useCommonTimer = true;
296
+
297
+ export interface MonitoredItemOptions extends MonitoringParameters {
298
+ monitoringMode: MonitoringMode;
299
+ /**
300
+ * the monitoredItem Id assigned by the server to this monitoredItem.
301
+ */
302
+ monitoredItemId: number;
303
+ itemToMonitor?: ReadValueIdOptions;
304
+ timestampsToReturn?: TimestampsToReturn;
305
+
306
+ // MonitoringParameters
307
+ filter: ExtensionObject | null;
308
+ /**
309
+ * if discardOldest === true, older items are removed from the queue when queue overflows
310
+ */
311
+ discardOldest: boolean;
312
+ /**
313
+ * the size of the queue.
314
+ */
315
+ queueSize: number;
316
+ /**
317
+ * the monitored item sampling interval ..
318
+ */
319
+ samplingInterval: number;
320
+ /**
321
+ * the client handle
322
+ */
323
+ clientHandle: number;
324
+ }
325
+
326
+ export interface BaseNode2 extends EventEmitter {
327
+ nodeId: NodeId;
328
+ browseName: QualifiedNameOptions;
329
+ nodeClass: NodeClass;
330
+ dataType: NodeId;
331
+ addressSpace: any;
332
+
333
+ readAttribute(context: SessionContext | null, attributeId: AttributeIds): DataValue;
334
+ }
335
+
336
+ type TimerKey = NodeJS.Timer;
337
+
338
+ export interface ISubscription {
339
+ $session?: any;
340
+ subscriptionDiagnostics: SubscriptionDiagnosticsDataType;
341
+ getMonitoredItem(monitoredItemId: number): MonitoredItem | null;
342
+ }
343
+
344
+ function isSourceNewerThan(a: DataValue, b?: DataValue): boolean {
345
+ if (!b) {
346
+ return true;
347
+ }
348
+ const at = a.sourceTimestamp?.getTime() || 0;
349
+ const bt = b.sourceTimestamp?.getTime() || 0;
350
+
351
+ if (at === bt) {
352
+ return a.sourcePicoseconds > b.sourcePicoseconds;
353
+ }
354
+ return at > bt;
355
+ }
356
+
357
+ /**
358
+ * a server side monitored item
359
+ *
360
+ * - Once created, the MonitoredItem will raised an "samplingEvent" event every "samplingInterval" millisecond
361
+ * until {{#crossLink "MonitoredItem/terminate:method"}}{{/crossLink}} is called.
362
+ *
363
+ * - It is up to the event receiver to call {{#crossLink "MonitoredItem/recordValue:method"}}{{/crossLink}}.
364
+ *
365
+ */
366
+ export class MonitoredItem extends EventEmitter {
367
+ public get node(): BaseNode | null {
368
+ return this._node;
369
+ }
370
+
371
+ public set node(someNode: BaseNode | null) {
372
+ throw new Error("Unexpected way to set node");
373
+ }
374
+
375
+ public static registry = new ObjectRegistry();
376
+ public static minimumSamplingInterval = 50; // 50 ms as a minimum sampling interval
377
+ public static defaultSamplingInterval = 1500; // 1500 ms as a default sampling interval
378
+ public static maximumSamplingInterval = 1000 * 60 * 60; // 1 hour !
379
+
380
+ public samplingInterval: number = -1;
381
+ public monitoredItemId: number;
382
+ public overflow: boolean;
383
+ public oldDataValue?: DataValue;
384
+ public monitoringMode: MonitoringMode;
385
+ public timestampsToReturn: TimestampsToReturn;
386
+ public itemToMonitor: any;
387
+ public filter: MonitoringFilter | null;
388
+ public discardOldest: boolean = true;
389
+ public queueSize: number = 0;
390
+ public clientHandle: UInt32;
391
+ public $subscription?: ISubscription;
392
+ public _samplingId?: TimerKey | string;
393
+ public samplingFunc:
394
+ | ((this: MonitoredItem, value: DataValue, callback: (err: Error | null, dataValue?: DataValue) => void) => void)
395
+ | null = null;
396
+
397
+ private _node: BaseNode | null;
398
+ private queue: QueueItem[];
399
+ private _semantic_version: number;
400
+ private _is_sampling: boolean = false;
401
+ private _on_opcua_event_received_callback: any;
402
+ private _attribute_changed_callback: any;
403
+ private _value_changed_callback: any;
404
+ private _semantic_changed_callback: any;
405
+ private _on_node_disposed_listener: any;
406
+ private _linkedItems?: number[];
407
+ private _triggeredNotifications?: QueueItem[];
408
+
409
+ constructor(options: MonitoredItemOptions) {
410
+ super();
411
+
412
+ assert(Object.prototype.hasOwnProperty.call(options,"monitoredItemId"));
413
+ assert(!options.monitoringMode, "use setMonitoring mode explicitly to activate the monitored item");
414
+
415
+ options.itemToMonitor = options.itemToMonitor || defaultItemToMonitor;
416
+
417
+ this._samplingId = undefined;
418
+ this.clientHandle = 0; // invalid
419
+ this.filter = null;
420
+ this._set_parameters(options);
421
+
422
+ this.monitoredItemId = options.monitoredItemId; // ( known as serverHandle)
423
+
424
+ this.queue = [];
425
+ this.overflow = false;
426
+
427
+ this.oldDataValue = new DataValue({ statusCode: StatusCodes.BadDataUnavailable }); // unset initially
428
+
429
+ // user has to call setMonitoringMode
430
+ this.monitoringMode = MonitoringMode.Invalid;
431
+
432
+ this.timestampsToReturn = coerceTimestampsToReturn(options.timestampsToReturn);
433
+
434
+ this.itemToMonitor = options.itemToMonitor;
435
+
436
+ this._node = null;
437
+ this._semantic_version = 0;
438
+
439
+ if (doDebug) {
440
+ debugLog("Monitoring ", options.itemToMonitor.toString());
441
+ }
442
+
443
+ this._on_node_disposed_listener = null;
444
+
445
+ MonitoredItem.registry.register(this);
446
+ }
447
+
448
+ public setNode(node: BaseNode) {
449
+ assert(!this.node || this.node === node, "node already set");
450
+ this._node = node;
451
+ this._semantic_version = (node as any).semantic_version;
452
+ this._on_node_disposed_listener = () => this._on_node_disposed(this._node!);
453
+ this._node.on("dispose", this._on_node_disposed_listener);
454
+ }
455
+
456
+ public setMonitoringMode(monitoringMode: MonitoringMode) {
457
+ assert(monitoringMode !== MonitoringMode.Invalid);
458
+
459
+ if (monitoringMode === this.monitoringMode) {
460
+ // nothing to do
461
+ return;
462
+ }
463
+
464
+ const old_monitoringMode = this.monitoringMode;
465
+
466
+ this.monitoringMode = monitoringMode;
467
+
468
+ if (this.monitoringMode === MonitoringMode.Disabled) {
469
+ this._stop_sampling();
470
+
471
+ // OPCUA 1.03 part 4 : $5.12.4
472
+ // setting the mode to DISABLED causes all queued Notifications to be deleted
473
+ this._empty_queue();
474
+ } else {
475
+ assert(this.monitoringMode === MonitoringMode.Sampling || this.monitoringMode === MonitoringMode.Reporting);
476
+
477
+ // OPCUA 1.03 part 4 : $5.12.1.3
478
+ // When a MonitoredItem is enabled (i.e. when the MonitoringMode is changed from DISABLED to
479
+ // SAMPLING or REPORTING) or it is created in the enabled state, the Server shall report the first
480
+ // sample as soon as possible and the time of this sample becomes the starting point for the next
481
+ // sampling interval.
482
+ const recordInitialValue =
483
+ old_monitoringMode === MonitoringMode.Invalid || old_monitoringMode === MonitoringMode.Disabled;
484
+
485
+ this._start_sampling(recordInitialValue);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Terminate the MonitoredItem.
491
+ * @method terminate
492
+ *
493
+ * This will stop the internal sampling timer.
494
+ */
495
+ public terminate() {
496
+ this._stop_sampling();
497
+ }
498
+
499
+
500
+ public dispose() {
501
+ if (doDebug) {
502
+ debugLog("DISPOSING MONITORED ITEM", this._node!.nodeId.toString());
503
+ }
504
+
505
+ this._stop_sampling();
506
+
507
+ MonitoredItem.registry.unregister(this);
508
+
509
+ if (this._on_node_disposed_listener) {
510
+ this._node!.removeListener("dispose", this._on_node_disposed_listener);
511
+ this._on_node_disposed_listener = null;
512
+ }
513
+
514
+ // x assert(this._samplingId === null,"Sampling Id must be null");
515
+ this.oldDataValue = undefined;
516
+ this.queue = [];
517
+ this.itemToMonitor = null;
518
+ this.filter = null;
519
+ this.monitoredItemId = 0;
520
+ this._node = null;
521
+ this._semantic_version = 0;
522
+
523
+ this.$subscription = undefined;
524
+
525
+ this.removeAllListeners();
526
+
527
+ assert(!this._samplingId);
528
+ assert(!this._value_changed_callback);
529
+ assert(!this._semantic_changed_callback);
530
+ assert(!this._attribute_changed_callback);
531
+ assert(!this._on_opcua_event_received_callback);
532
+ this._on_opcua_event_received_callback = null;
533
+ this._attribute_changed_callback = null;
534
+ this._semantic_changed_callback = null;
535
+ this._on_opcua_event_received_callback = null;
536
+ }
537
+
538
+ public get isSampling(): boolean {
539
+ return (
540
+ !!this._samplingId ||
541
+ typeof this._value_changed_callback === "function" ||
542
+ typeof this._attribute_changed_callback === "function"
543
+ );
544
+ }
545
+
546
+ public toString(): string {
547
+ let str = "";
548
+ str += `monitored item nodeId : ${this.node?.nodeId.toString()} \n`;
549
+ str += ` sampling interval : ${this.samplingInterval} \n`;
550
+ str += ` monitoredItemId : ${this.monitoredItemId} \n`;
551
+ return str;
552
+ }
553
+ /**
554
+ * @param dataValue the whole dataValue
555
+ * @param skipChangeTest indicates whether recordValue should not check that dataValue is really
556
+ * different from previous one, ( by checking timestamps but also variant value)
557
+ * @private
558
+ *
559
+ * Notes:
560
+ * - recordValue can only be called within timer event
561
+ * - for performance reason, dataValue may be a shared value with the underlying node,
562
+ * therefore recordValue must clone the dataValue to make sure it retains a snapshot
563
+ * of the contain at the time recordValue was called.
564
+ *
565
+ */
566
+ public recordValue(dataValue: DataValue, skipChangeTest: boolean, indexRange?: NumericRange) {
567
+ assert(dataValue instanceof DataValue);
568
+ assert(dataValue !== this.oldDataValue, "recordValue expects different dataValue to be provided");
569
+
570
+ assert(
571
+ !dataValue.value || dataValue.value !== this.oldDataValue!.value,
572
+ "recordValue expects different dataValue.value to be provided"
573
+ );
574
+
575
+ assert(!dataValue.value || dataValue.value.isValid(), "expecting a valid variant value");
576
+
577
+ const hasSemanticChanged = this.node && (this.node as any).semantic_version !== this._semantic_version;
578
+
579
+ // xx console.log("`\n----------------------------",skipChangeTest,this.clientHandle,
580
+ // this.node.listenerCount("value_changed"),this.node.nodeId.toString());
581
+ // xx console.log("events ---- ",this.node.eventNames().join("-"));
582
+ // xx console.log("indexRange = ",indexRange ? indexRange.toString() :"");
583
+ // xx console.log("this.itemToMonitor.indexRange = ",this.itemToMonitor.indexRange.toString());
584
+
585
+ if (!hasSemanticChanged && indexRange && this.itemToMonitor.indexRange) {
586
+ // we just ignore changes that do not fall within our range
587
+ // ( unless semantic bit has changed )
588
+ if (!NumericRange.overlap(indexRange as NumericalRange0, this.itemToMonitor.indexRange)) {
589
+ return; // no overlap !
590
+ }
591
+ }
592
+
593
+ assert(this.itemToMonitor, "must have a valid itemToMonitor(have this monitoredItem been disposed already ?");
594
+ // extract the range that we are interested with
595
+ dataValue = extractRange(dataValue, this.itemToMonitor.indexRange);
596
+
597
+ // istanbul ignore next
598
+ if (doDebug) {
599
+ debugLog(
600
+ "MonitoredItem#recordValue",
601
+ this.node!.nodeId.toString(),
602
+ this.node!.browseName.toString(),
603
+ " has Changed = ",
604
+ !sameDataValue(dataValue, this.oldDataValue!),
605
+ "skipChangeTest = ",
606
+ skipChangeTest,
607
+ "hasSemanticChanged = ",
608
+ hasSemanticChanged
609
+ );
610
+ }
611
+
612
+ // if semantic has changed, value need to be enqueued regardless of other assumptions
613
+ if (hasSemanticChanged) {
614
+ debugLog("_enqueue_value => because hasSemanticChanged");
615
+ setSemanticChangeBit(dataValue);
616
+ this._semantic_version = (this.node as UAVariable).semantic_version;
617
+ return this._enqueue_value(dataValue);
618
+ debugLog("_enqueue_value => because hasSemanticChanged 2");
619
+ }
620
+
621
+ const useIndexRange = this.itemToMonitor.indexRange && !this.itemToMonitor.indexRange.isEmpty();
622
+
623
+ if (!skipChangeTest) {
624
+ const hasChanged = !sameDataValue(dataValue, this.oldDataValue!);
625
+ if (!hasChanged) {
626
+ return;
627
+ }
628
+ }
629
+
630
+ if (!apply_filter.call(this, dataValue)) {
631
+ return;
632
+ }
633
+
634
+ if (useIndexRange) {
635
+ // when an indexRange is provided , make sure that no record happens unless
636
+ // extracted variant in the selected range has really changed.
637
+
638
+ // istanbul ignore next
639
+ if (doDebug) {
640
+ debugLog("Current : ", this.oldDataValue!.toString());
641
+ debugLog("New : ", dataValue.toString());
642
+ debugLog("indexRange=", indexRange);
643
+ }
644
+
645
+ if (sameVariant(dataValue.value, this.oldDataValue!.value)) {
646
+ return;
647
+ }
648
+ }
649
+
650
+ // processTriggerItems
651
+ this.triggerLinkedItems();
652
+
653
+ if (doDebug) {
654
+ debugLog("RECORD VALUE ", this.node?.nodeId.toString());
655
+ }
656
+ // store last value
657
+ this._enqueue_value(dataValue);
658
+ }
659
+
660
+ public hasLinkItem(linkedMonitoredItemId: number): boolean {
661
+ if (!this._linkedItems) {
662
+ return false;
663
+ }
664
+ return this._linkedItems.findIndex((x) => x === linkedMonitoredItemId) > 0;
665
+ }
666
+ public addLinkItem(linkedMonitoredItemId: number): StatusCode {
667
+ if (linkedMonitoredItemId === this.monitoredItemId) {
668
+ return StatusCodes.BadMonitoredItemIdInvalid;
669
+ }
670
+ this._linkedItems = this._linkedItems || [];
671
+ if (this.hasLinkItem(linkedMonitoredItemId)) {
672
+ return StatusCodes.BadMonitoredItemIdInvalid; // nothing to do
673
+ }
674
+ this._linkedItems.push(linkedMonitoredItemId);
675
+ return StatusCodes.Good;
676
+ }
677
+ public removeLinkItem(linkedMonitoredItemId: number): StatusCode {
678
+ if (!this._linkedItems || linkedMonitoredItemId === this.monitoredItemId) {
679
+ return StatusCodes.BadMonitoredItemIdInvalid;
680
+ }
681
+ const index = this._linkedItems.findIndex((x) => x === linkedMonitoredItemId);
682
+ if (index === -1) {
683
+ return StatusCodes.BadMonitoredItemIdInvalid;
684
+ }
685
+ this._linkedItems.splice(index, 1);
686
+ return StatusCodes.Good;
687
+ }
688
+ /**
689
+ * @internals
690
+ */
691
+ private triggerLinkedItems() {
692
+ if (!this.$subscription || !this._linkedItems) {
693
+ return;
694
+ }
695
+ // see https://reference.opcfoundation.org/v104/Core/docs/Part4/5.12.1/#5.12.1.6
696
+ for (const linkItem of this._linkedItems) {
697
+ const linkedMonitoredItem = this.$subscription.getMonitoredItem(linkItem);
698
+ if (!linkedMonitoredItem) {
699
+ // monitoredItem may have been deleted
700
+ continue;
701
+ }
702
+ if (linkedMonitoredItem.monitoringMode === MonitoringMode.Disabled) {
703
+ continue;
704
+ }
705
+ if (linkedMonitoredItem.monitoringMode === MonitoringMode.Reporting) {
706
+ continue;
707
+ }
708
+ assert(linkedMonitoredItem.monitoringMode === MonitoringMode.Sampling);
709
+
710
+ // istanbul ignore next
711
+ if (doDebug) {
712
+ debugLog("triggerLinkedItems => ", this.node?.nodeId.toString(), linkedMonitoredItem.node?.nodeId.toString());
713
+ }
714
+ linkedMonitoredItem.trigger();
715
+ }
716
+ }
717
+
718
+ get hasMonitoredItemNotifications(): boolean {
719
+ return this.queue.length > 0 || (this._triggeredNotifications !== undefined && this._triggeredNotifications.length > 0);
720
+ }
721
+
722
+ /**
723
+ * @internals
724
+ */
725
+ private trigger() {
726
+ setImmediate(() => {
727
+ this._triggeredNotifications = this._triggeredNotifications || [];
728
+ const notifications = this.extractMonitoredItemNotifications(true);
729
+ this._triggeredNotifications = ([] as QueueItem[]).concat(
730
+ this._triggeredNotifications!,
731
+ notifications
732
+ );
733
+ });
734
+ }
735
+
736
+ public extractMonitoredItemNotifications(bForce: boolean = false): QueueItem[] {
737
+ if (!bForce && this.monitoringMode === MonitoringMode.Sampling && this._triggeredNotifications) {
738
+ const notifications1 = this._triggeredNotifications;
739
+ this._triggeredNotifications = undefined;
740
+ return notifications1;
741
+ }
742
+ if (!bForce && this.monitoringMode !== MonitoringMode.Reporting) {
743
+ return [];
744
+ }
745
+ const notifications = this.queue;
746
+ this._empty_queue();
747
+
748
+ // apply semantic changed bit if necessary
749
+ if (notifications.length > 0 && this.node && this._semantic_version < (this.node as UAVariable).semantic_version) {
750
+ const dataValue = notifications[notifications.length - 1];
751
+ setSemanticChangeBit(dataValue);
752
+ assert(this.node.nodeClass === NodeClass.Variable);
753
+ this._semantic_version = (this.node as UAVariable).semantic_version;
754
+ }
755
+
756
+ return notifications;
757
+ }
758
+
759
+ public modify(timestampsToReturn: TimestampsToReturn, monitoringParameters: MonitoringParameters): MonitoredItemModifyResult {
760
+ assert(monitoringParameters instanceof MonitoringParameters);
761
+
762
+ const old_samplingInterval = this.samplingInterval;
763
+
764
+ this.timestampsToReturn = timestampsToReturn || this.timestampsToReturn;
765
+
766
+ if (old_samplingInterval !== 0 && monitoringParameters.samplingInterval === 0) {
767
+ monitoringParameters.samplingInterval = MonitoredItem.minimumSamplingInterval; // fastest possible
768
+ }
769
+
770
+ // spec says: Illegal request values for parameters that can be revised do not generate errors. Instead the
771
+ // server will choose default values and indicate them in the corresponding revised parameter
772
+ this._set_parameters(monitoringParameters);
773
+
774
+ this._adjust_queue_to_match_new_queue_size();
775
+
776
+ this._adjust_sampling(old_samplingInterval);
777
+
778
+ if (monitoringParameters.filter) {
779
+ const statusCodeFilter = validateFilter(monitoringParameters.filter, this.itemToMonitor, this.node!);
780
+ if (statusCodeFilter.isNot(StatusCodes.Good)) {
781
+ return new MonitoredItemModifyResult({
782
+ statusCode: statusCodeFilter
783
+ });
784
+ }
785
+ }
786
+
787
+ // validate filter
788
+ // note : The DataChangeFilter does not have an associated result structure.
789
+ const filterResult = null; // new subscription_service.DataChangeFilter
790
+
791
+ return new MonitoredItemModifyResult({
792
+ filterResult,
793
+ revisedQueueSize: this.queueSize,
794
+ revisedSamplingInterval: this.samplingInterval,
795
+ statusCode: StatusCodes.Good
796
+ });
797
+ }
798
+
799
+ public async resendInitialValues(): Promise<void> {
800
+ // tte first Publish response(s) after the TransferSubscriptions call shall contain the current values of all
801
+ // Monitored Items in the Subscription where the Monitoring Mode is set to Reporting.
802
+ // the first Publish response after the TransferSubscriptions call shall contain only the value changes since
803
+ // the last Publish response was sent.
804
+ // This parameter only applies to MonitoredItems used for monitoring Attribute changes.
805
+ this._stop_sampling();
806
+ return this._start_sampling(true);
807
+ }
808
+
809
+ /**
810
+ * @method _on_sampling_timer
811
+ * @private
812
+ * request
813
+ *
814
+ */
815
+ private _on_sampling_timer() {
816
+ // istanbul ignore next
817
+ if (doDebug) {
818
+ debugLog(
819
+ "MonitoredItem#_on_sampling_timer",
820
+ this.node ? this.node.nodeId.toString() : "null",
821
+ " isSampling?=",
822
+ this._is_sampling
823
+ );
824
+ }
825
+
826
+ if (this._samplingId) {
827
+ assert(this.monitoringMode === MonitoringMode.Sampling || this.monitoringMode === MonitoringMode.Reporting);
828
+
829
+ if (this._is_sampling) {
830
+ // previous sampling call is not yet completed..
831
+ // there is nothing we can do about it except waiting until next tick.
832
+ // note : see also issue #156 on github
833
+ return;
834
+ }
835
+ // xx console.log("xxxx ON SAMPLING");
836
+ assert(!this._is_sampling, "sampling func shall not be re-entrant !! fix it");
837
+
838
+ if (!this.samplingFunc) {
839
+ throw new Error("internal error : missing samplingFunc");
840
+ }
841
+
842
+ this._is_sampling = true;
843
+
844
+ this.samplingFunc.call(this, this.oldDataValue!, (err: Error | null, newDataValue?: DataValue) => {
845
+ if (!this._samplingId) {
846
+ // item has been disposed. The monitored item has been disposed while the async sampling func
847
+ // was taking place ... just ignore this
848
+ return;
849
+ }
850
+ if (err) {
851
+ console.log(" SAMPLING ERROR =>", err);
852
+ } else {
853
+ // only record value if source timestamp is newer
854
+ // xx if (newDataValue && isSourceNewerThan(newDataValue, this.oldDataValue)) {
855
+ this._on_value_changed(newDataValue!);
856
+ // xx }
857
+ }
858
+ this._is_sampling = false;
859
+ });
860
+ } else {
861
+ /* istanbul ignore next */
862
+ debugLog("_on_sampling_timer call but MonitoredItem has been terminated !!! ");
863
+ }
864
+ }
865
+
866
+ private _stop_sampling() {
867
+ // debugLog("MonitoredItem#_stop_sampling");
868
+ /* istanbul ignore next */
869
+ if (!this.node) {
870
+ throw new Error("Internal Error");
871
+ }
872
+ if (this._on_opcua_event_received_callback) {
873
+ assert(typeof this._on_opcua_event_received_callback === "function");
874
+ this.node.removeListener("event", this._on_opcua_event_received_callback);
875
+ this._on_opcua_event_received_callback = null;
876
+ }
877
+
878
+ if (this._attribute_changed_callback) {
879
+ assert(typeof this._attribute_changed_callback === "function");
880
+
881
+ const event_name = makeAttributeEventName(this.itemToMonitor.attributeId);
882
+ this.node.removeListener(event_name, this._attribute_changed_callback);
883
+ this._attribute_changed_callback = null;
884
+ }
885
+
886
+ if (this._value_changed_callback) {
887
+ // samplingInterval was 0 for a exception-based data Item
888
+ // we setup a event listener that we need to unwind here
889
+ assert(typeof this._value_changed_callback === "function");
890
+ assert(!this._samplingId);
891
+
892
+ this.node.removeListener("value_changed", this._value_changed_callback);
893
+ this._value_changed_callback = null;
894
+ }
895
+
896
+ if (this._semantic_changed_callback) {
897
+ assert(typeof this._semantic_changed_callback === "function");
898
+ assert(!this._samplingId);
899
+ this.node.removeListener("semantic_changed", this._semantic_changed_callback);
900
+ this._semantic_changed_callback = null;
901
+ }
902
+ if (this._samplingId) {
903
+ this._clear_timer();
904
+ }
905
+
906
+ assert(!this._samplingId);
907
+ assert(!this._value_changed_callback);
908
+ assert(!this._semantic_changed_callback);
909
+ assert(!this._attribute_changed_callback);
910
+ assert(!this._on_opcua_event_received_callback);
911
+ }
912
+
913
+ private _on_value_changed(dataValue: DataValue, indexRange?: NumericRange) {
914
+ assert(dataValue instanceof DataValue);
915
+ this.recordValue(dataValue, false, indexRange);
916
+ }
917
+
918
+ private _on_semantic_changed() {
919
+ const dataValue: DataValue = (this.node! as UAVariable).readValue();
920
+ this._on_value_changed(dataValue);
921
+ }
922
+
923
+ private _on_opcua_event(eventData: IEventData) {
924
+ // TO DO : => Improve Filtering, bearing in mind that ....
925
+ // Release 1.04 8 OPC Unified Architecture, Part 9
926
+ // 4.5 Condition state synchronization
927
+ // To ensure a Client is always informed, the three special EventTypes
928
+ // (RefreshEndEventType, RefreshStartEventType and RefreshRequiredEventType)
929
+ // ignore the Event content filtering associated with a Subscription and will always be
930
+ // delivered to the Client.
931
+
932
+ // istanbul ignore next
933
+ if (!this.filter || !(this.filter instanceof EventFilter)) {
934
+ throw new Error("Internal Error : a EventFilter is requested");
935
+ }
936
+
937
+ const addressSpace: AddressSpace = eventData.$eventDataSource?.addressSpace as AddressSpace;
938
+
939
+ if (!checkWhereClause(addressSpace, SessionContext.defaultContext, this.filter.whereClause, eventData)) {
940
+ return;
941
+ }
942
+
943
+ const selectClauses = this.filter.selectClauses ? this.filter.selectClauses : ([] as SimpleAttributeOperand[]);
944
+
945
+ const eventFields: Variant[] = extractEventFields(SessionContext.defaultContext, selectClauses, eventData);
946
+
947
+ // istanbul ignore next
948
+ if (doDebug) {
949
+ console.log(" RECEIVED INTERNAL EVENT THAT WE ARE MONITORING");
950
+ console.log(this.filter ? this.filter.toString() : "no filter");
951
+ eventFields.forEach((e: any) => {
952
+ console.log(e.toString());
953
+ });
954
+ }
955
+
956
+ this._enqueue_event(eventFields);
957
+ }
958
+
959
+ private _getSession(): any {
960
+ if (!this.$subscription) {
961
+ return null;
962
+ }
963
+ if (!this.$subscription.$session) {
964
+ return null;
965
+ }
966
+ return this.$subscription.$session;
967
+ }
968
+
969
+ private _start_sampling(recordInitialValue?: boolean): void {
970
+ // istanbul ignore next
971
+ if (!this.node) {
972
+ throw new Error("Internal Error");
973
+ }
974
+ setImmediate(() => this.__start_sampling(recordInitialValue));
975
+ }
976
+ private __start_sampling(recordInitialValue?: boolean): void {
977
+ // istanbul ignore next
978
+ if (!this.node) {
979
+ return; // we just want to ignore here ...
980
+ }
981
+
982
+ // make sure oldDataValue is scrapped so first data recording can happen
983
+ this.oldDataValue = new DataValue({ statusCode: StatusCodes.BadDataUnavailable }); // unset initially
984
+
985
+ this._stop_sampling();
986
+
987
+ const context = new SessionContext({
988
+ session: this._getSession()
989
+ });
990
+
991
+ if (this.itemToMonitor.attributeId === AttributeIds.EventNotifier) {
992
+ // istanbul ignore next
993
+ if (doDebug) {
994
+ debugLog("xxxxxx monitoring EventNotifier on", this.node.nodeId.toString(), this.node.browseName.toString());
995
+ }
996
+ // we are monitoring OPCUA Event
997
+ this._on_opcua_event_received_callback = this._on_opcua_event.bind(this);
998
+ this.node.on("event", this._on_opcua_event_received_callback);
999
+
1000
+ return;
1001
+ }
1002
+ if (this.itemToMonitor.attributeId !== AttributeIds.Value) {
1003
+ // sampling interval only applies to Value Attributes.
1004
+ this.samplingInterval = 0; // turned to exception-based regardless of requested sampling interval
1005
+
1006
+ // non value attribute only react on value change
1007
+ this._attribute_changed_callback = this._on_value_changed.bind(this);
1008
+ const event_name = makeAttributeEventName(this.itemToMonitor.attributeId);
1009
+
1010
+ this.node.on(event_name, this._attribute_changed_callback);
1011
+
1012
+ if (recordInitialValue) {
1013
+ // read initial value
1014
+ const dataValue = this.node.readAttribute(context, this.itemToMonitor.attributeId);
1015
+ this.recordValue(dataValue, true);
1016
+ }
1017
+ return;
1018
+ }
1019
+
1020
+ if (this.samplingInterval === 0) {
1021
+ // we have a exception-based dataItem : event based model, so we do not need a timer
1022
+ // rather , we setup the "value_changed_event";
1023
+ this._value_changed_callback = this._on_value_changed.bind(this);
1024
+ this._semantic_changed_callback = this._on_semantic_changed.bind(this);
1025
+
1026
+ this.node.on("value_changed", this._value_changed_callback);
1027
+ this.node.on("semantic_changed", this._semantic_changed_callback);
1028
+
1029
+ // initiate first read
1030
+ if (recordInitialValue) {
1031
+ /* await */ new Promise<void>((resolve: () => void) => {
1032
+ (this.node as UAVariable).readValueAsync(context, (err: Error | null, dataValue?: DataValue) => {
1033
+ if (!err && dataValue) {
1034
+ this.recordValue(dataValue, true);
1035
+ }
1036
+ resolve();
1037
+ });
1038
+ });
1039
+ }
1040
+ } else {
1041
+ this._set_timer();
1042
+ if (recordInitialValue) {
1043
+ setImmediate(() => {
1044
+ this._on_sampling_timer();
1045
+ });
1046
+ }
1047
+ }
1048
+ }
1049
+
1050
+ private _set_parameters(monitoredParameters: MonitoringParameters) {
1051
+ _validate_parameters(monitoredParameters);
1052
+ // only change clientHandle if it is valid (0<X<MAX)
1053
+ if (monitoredParameters.clientHandle !== 0 && monitoredParameters.clientHandle !== 4294967295) {
1054
+ this.clientHandle = monitoredParameters.clientHandle;
1055
+ }
1056
+
1057
+ // The Server may support data that is collected based on a sampling model or generated based on an
1058
+ // exception-based model. The fastest supported sampling interval may be equal to 0, which indicates
1059
+ // that the data item is exception-based rather than being sampled at some period. An exception-based
1060
+ // model means that the underlying system does not require sampling and reports data changes.
1061
+ if (this.node && this.node.nodeClass === NodeClass.Variable) {
1062
+ this.samplingInterval = _adjust_sampling_interval(
1063
+ monitoredParameters.samplingInterval,
1064
+ this.node ? (this.node as UAVariable).minimumSamplingInterval : 0
1065
+ );
1066
+ } else {
1067
+ this.samplingInterval = _adjust_sampling_interval(monitoredParameters.samplingInterval, 0);
1068
+ }
1069
+ this.discardOldest = monitoredParameters.discardOldest;
1070
+ this.queueSize = _adjust_queue_size(monitoredParameters.queueSize);
1071
+
1072
+ // change filter
1073
+ this.filter = (monitoredParameters.filter as MonitoringFilter) || null;
1074
+ }
1075
+
1076
+ private _setOverflowBit(notification: any) {
1077
+ if (notification.hasOwnProperty("value")) {
1078
+ assert(notification.value.statusCode.equals(StatusCodes.Good));
1079
+ notification.value.statusCode = StatusCode.makeStatusCode(
1080
+ notification.value.statusCode,
1081
+ "Overflow | InfoTypeDataValue"
1082
+ );
1083
+ assert(sameStatusCode(notification.value.statusCode, StatusCodes.GoodWithOverflowBit));
1084
+ assert(notification.value.statusCode.hasOverflowBit);
1085
+ }
1086
+ // console.log(chalk.cyan("Setting Over"), !!this.$subscription, !!this.$subscription!.subscriptionDiagnostics);
1087
+ if (this.$subscription && this.$subscription.subscriptionDiagnostics) {
1088
+ this.$subscription.subscriptionDiagnostics.monitoringQueueOverflowCount++;
1089
+ }
1090
+ // to do eventQueueOverFlowCount
1091
+ }
1092
+
1093
+ private _enqueue_notification(notification: QueueItem) {
1094
+ if (this.queueSize === 1) {
1095
+ // https://reference.opcfoundation.org/v104/Core/docs/Part4/5.12.1/#5.12.1.5
1096
+ // If the queue size is one, the queue becomes a buffer that always contains the newest
1097
+ // Notification. In this case, if the sampling interval of the MonitoredItem is faster
1098
+ // than the publishing interval of the Subscription, the MonitoredItem will be over
1099
+ // sampling and the Client will always receive the most up-to-date value.
1100
+ // The discard policy is ignored if the queue size is one.
1101
+ // ensure queue size
1102
+ if (!this.queue || this.queue.length !== 1) {
1103
+ this.queue = [];
1104
+ }
1105
+ this.queue[0] = notification;
1106
+ assert(this.queue.length === 1);
1107
+ } else {
1108
+ if (this.discardOldest) {
1109
+ // push new value to queue
1110
+ this.queue.push(notification);
1111
+
1112
+ if (this.queue.length > this.queueSize) {
1113
+ this.overflow = true;
1114
+
1115
+ this.queue.shift(); // remove front element
1116
+
1117
+ // set overflow bit
1118
+ this._setOverflowBit(this.queue[0]);
1119
+ }
1120
+ } else {
1121
+ if (this.queue.length < this.queueSize) {
1122
+ this.queue.push(notification);
1123
+ } else {
1124
+ this.overflow = true;
1125
+
1126
+ this._setOverflowBit(notification);
1127
+ this.queue[this.queue.length - 1] = notification;
1128
+ }
1129
+ }
1130
+ }
1131
+ assert(this.queue.length >= 1);
1132
+ }
1133
+
1134
+ private _makeDataChangeNotification(dataValue: DataValue): MonitoredItemNotification {
1135
+ if (this.clientHandle === -1 || this.clientHandle === 4294967295) {
1136
+ debugLog("Invalid client handle");
1137
+ }
1138
+ const attributeId = this.itemToMonitor.attributeId;
1139
+ // if dataFilter is specified ....
1140
+ if (this.filter && this.filter instanceof DataChangeFilter) {
1141
+ if (this.filter.trigger === DataChangeTrigger.Status) {
1142
+ /** */
1143
+ }
1144
+ }
1145
+ dataValue = apply_timestamps(dataValue, this.timestampsToReturn, attributeId);
1146
+ return new MonitoredItemNotification({
1147
+ clientHandle: this.clientHandle,
1148
+ value: dataValue
1149
+ });
1150
+ }
1151
+
1152
+ /**
1153
+ * @method _enqueue_value
1154
+ * @param dataValue {DataValue} the dataValue to enqueue
1155
+ * @private
1156
+ */
1157
+ private _enqueue_value(dataValue: DataValue) {
1158
+ // preconditions:
1159
+ if (doDebug) {
1160
+ debugLog("_enqueue_value = ", dataValue.toString());
1161
+ }
1162
+
1163
+ assert(dataValue instanceof DataValue);
1164
+ // lets verify that, if status code is good then we have a valid Variant in the dataValue
1165
+ assert(!isGoodish(dataValue.statusCode) || dataValue.value instanceof Variant);
1166
+ // xx assert(isGoodish(dataValue.statusCode) || util.isNullOrUndefined(dataValue.value) );
1167
+ // let's check that data Value is really a different object
1168
+ // we may end up with corrupted queue if dataValue are recycled and stored as is in notifications
1169
+ assert(dataValue !== this.oldDataValue, "dataValue cannot be the same object twice!");
1170
+
1171
+ // Xx // todo ERN !!!! PLEASE CHECK this !!!
1172
+ // Xx // let make a clone, so we have a snapshot
1173
+ // Xx dataValue = dataValue.clone();
1174
+
1175
+ // let's check that data Value is really a different object
1176
+ // we may end up with corrupted queue if dataValue are recycled and stored as is in notifications
1177
+ assert(
1178
+ !this.oldDataValue || !dataValue.value || dataValue.value !== this.oldDataValue.value,
1179
+ "dataValue cannot be the same object twice!"
1180
+ );
1181
+
1182
+ if (
1183
+ !(
1184
+ !this.oldDataValue ||
1185
+ !this.oldDataValue.value ||
1186
+ !dataValue.value ||
1187
+ !(dataValue.value.value instanceof Object) ||
1188
+ dataValue.value.value !== this.oldDataValue.value.value
1189
+ ) &&
1190
+ !(dataValue.value.value instanceof Date)
1191
+ ) {
1192
+ throw new Error(
1193
+ "dataValue.value.value cannot be the same object twice! " +
1194
+ this.node!.browseName.toString() +
1195
+ " " +
1196
+ dataValue.toString() +
1197
+ " " +
1198
+ chalk.cyan(this.oldDataValue.toString())
1199
+ );
1200
+ }
1201
+
1202
+ // istanbul ignore next
1203
+ if (doDebug) {
1204
+ debugLog("MonitoredItem#_enqueue_value", this.node!.nodeId.toString());
1205
+ }
1206
+ this.oldDataValue = dataValue;
1207
+ const notification = this._makeDataChangeNotification(dataValue);
1208
+ this._enqueue_notification(notification);
1209
+ }
1210
+
1211
+ private _makeEventFieldList(eventFields: any[]): EventFieldList {
1212
+ assert(Array.isArray(eventFields));
1213
+ return new EventFieldList({
1214
+ clientHandle: this.clientHandle,
1215
+ eventFields
1216
+ });
1217
+ }
1218
+
1219
+ private _enqueue_event(eventFields: any[]) {
1220
+ if (doDebug) {
1221
+ debugLog(" MonitoredItem#_enqueue_event");
1222
+ }
1223
+ const notification = this._makeEventFieldList(eventFields);
1224
+ this._enqueue_notification(notification);
1225
+ }
1226
+
1227
+ private _empty_queue() {
1228
+ // empty queue
1229
+ this.queue = [];
1230
+ this.overflow = false;
1231
+ }
1232
+
1233
+ private _clear_timer() {
1234
+ if (this._samplingId) {
1235
+ if (useCommonTimer) {
1236
+ removeFromTimer(this);
1237
+ } else {
1238
+ clearInterval(this._samplingId as NodeJS.Timer);
1239
+ }
1240
+ this._samplingId = undefined;
1241
+ }
1242
+ }
1243
+
1244
+ private _set_timer() {
1245
+ assert(this.samplingInterval >= MonitoredItem.minimumSamplingInterval);
1246
+ assert(!this._samplingId);
1247
+
1248
+ if (useCommonTimer) {
1249
+ this._samplingId = appendToTimer(this);
1250
+ } else {
1251
+ // settle periodic sampling
1252
+ this._samplingId = setInterval(() => {
1253
+ this._on_sampling_timer();
1254
+ }, this.samplingInterval);
1255
+ }
1256
+ // xx console.log("MonitoredItem#_set_timer",this._samplingId);
1257
+ }
1258
+
1259
+ private _adjust_queue_to_match_new_queue_size() {
1260
+ // adjust queue size if necessary
1261
+ if (this.queueSize < this.queue.length) {
1262
+ if (this.discardOldest) {
1263
+ this.queue.splice(0, this.queue.length - this.queueSize);
1264
+ } else {
1265
+ const lastElement = this.queue[this.queue.length - 1];
1266
+ // only keep queueSize first element, discard others
1267
+ this.queue.splice(this.queueSize);
1268
+ this.queue[this.queue.length - 1] = lastElement;
1269
+ }
1270
+ }
1271
+ if (this.queueSize <= 1) {
1272
+ this.overflow = false;
1273
+ // unset OverFlowBit
1274
+ if (this.queue.length === 1) {
1275
+ if (this.queue[0] instanceof MonitoredItemNotification) {
1276
+ const el = this.queue[0] as MonitoredItemNotification;
1277
+ if (el.value.statusCode.hasOverflowBit) {
1278
+ (el.value.statusCode as any).unset("Overflow | InfoTypeDataValue");
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ assert(this.queue.length <= this.queueSize);
1284
+ }
1285
+
1286
+ private _adjust_sampling(old_samplingInterval: number) {
1287
+ if (old_samplingInterval !== this.samplingInterval) {
1288
+ this._start_sampling(false);
1289
+ }
1290
+ }
1291
+
1292
+ private _on_node_disposed(node: BaseNode) {
1293
+ this._on_value_changed(
1294
+ new DataValue({
1295
+ sourceTimestamp: new Date(),
1296
+ statusCode: StatusCodes.BadNodeIdInvalid
1297
+ })
1298
+ );
1299
+ this._stop_sampling();
1300
+ node.removeListener("dispose", this._on_node_disposed_listener);
1301
+ this._on_node_disposed_listener = null;
1302
+ }
1303
+ }