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.
- package/.mocharc.yml +10 -0
- package/LICENSE +20 -0
- package/dist/base_server.d.ts +110 -0
- package/dist/base_server.js +476 -0
- package/dist/base_server.js.map +1 -0
- package/dist/factory.d.ts +10 -0
- package/dist/factory.js +24 -0
- package/dist/factory.js.map +1 -0
- package/dist/history_server_capabilities.d.ts +35 -0
- package/dist/history_server_capabilities.js +44 -0
- package/dist/history_server_capabilities.js.map +1 -0
- package/dist/i_channel_data.d.ts +13 -0
- package/dist/i_channel_data.js +3 -0
- package/dist/i_channel_data.js.map +1 -0
- package/dist/i_register_server_manager.d.ts +16 -0
- package/dist/i_register_server_manager.js +3 -0
- package/dist/i_register_server_manager.js.map +1 -0
- package/dist/i_server_side_publish_engine.d.ts +36 -0
- package/dist/i_server_side_publish_engine.js +50 -0
- package/dist/i_server_side_publish_engine.js.map +1 -0
- package/dist/i_socket_data.d.ts +11 -0
- package/dist/i_socket_data.js +3 -0
- package/dist/i_socket_data.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/monitored_item.d.ts +173 -0
- package/dist/monitored_item.js +1006 -0
- package/dist/monitored_item.js.map +1 -0
- package/dist/node_sampler.d.ts +3 -0
- package/dist/node_sampler.js +76 -0
- package/dist/node_sampler.js.map +1 -0
- package/dist/opcua_server.d.ts +668 -0
- package/dist/opcua_server.js +2407 -0
- package/dist/opcua_server.js.map +1 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +71 -0
- package/dist/queue.js.map +1 -0
- package/dist/register_server_manager.d.ts +92 -0
- package/dist/register_server_manager.js +574 -0
- package/dist/register_server_manager.js.map +1 -0
- package/dist/register_server_manager_hidden.d.ts +17 -0
- package/dist/register_server_manager_hidden.js +28 -0
- package/dist/register_server_manager_hidden.js.map +1 -0
- package/dist/register_server_manager_mdns_only.d.ts +19 -0
- package/dist/register_server_manager_mdns_only.js +58 -0
- package/dist/register_server_manager_mdns_only.js.map +1 -0
- package/dist/server_capabilities.d.ts +61 -0
- package/dist/server_capabilities.js +109 -0
- package/dist/server_capabilities.js.map +1 -0
- package/dist/server_end_point.d.ts +180 -0
- package/dist/server_end_point.js +825 -0
- package/dist/server_end_point.js.map +1 -0
- package/dist/server_engine.d.ts +311 -0
- package/dist/server_engine.js +1659 -0
- package/dist/server_engine.js.map +1 -0
- package/dist/server_publish_engine.d.ts +109 -0
- package/dist/server_publish_engine.js +531 -0
- package/dist/server_publish_engine.js.map +1 -0
- package/dist/server_publish_engine_for_orphan_subscriptions.d.ts +16 -0
- package/dist/server_publish_engine_for_orphan_subscriptions.js +50 -0
- package/dist/server_publish_engine_for_orphan_subscriptions.js.map +1 -0
- package/dist/server_session.d.ts +176 -0
- package/dist/server_session.js +734 -0
- package/dist/server_session.js.map +1 -0
- package/dist/server_subscription.d.ts +393 -0
- package/dist/server_subscription.js +1313 -0
- package/dist/server_subscription.js.map +1 -0
- package/dist/sessions_compatible_for_transfer.d.ts +2 -0
- package/dist/sessions_compatible_for_transfer.js +36 -0
- package/dist/sessions_compatible_for_transfer.js.map +1 -0
- package/dist/validate_filter.d.ts +5 -0
- package/dist/validate_filter.js +64 -0
- package/dist/validate_filter.js.map +1 -0
- package/package.json +88 -0
- package/source/base_server.ts +617 -0
- package/source/factory.ts +25 -0
- package/source/history_server_capabilities.ts +75 -0
- package/source/i_channel_data.ts +17 -0
- package/source/i_register_server_manager.ts +24 -0
- package/source/i_server_side_publish_engine.ts +77 -0
- package/source/i_socket_data.ts +11 -0
- package/source/index.ts +14 -0
- package/source/monitored_item.ts +1303 -0
- package/source/node_sampler.ts +82 -0
- package/source/opcua_server.ts +3742 -0
- package/source/queue.ts +73 -0
- package/source/register_server_manager.ts +744 -0
- package/source/register_server_manager_hidden.ts +33 -0
- package/source/register_server_manager_mdns_only.ts +69 -0
- package/source/server_capabilities.ts +177 -0
- package/source/server_end_point.ts +1182 -0
- package/source/server_engine.ts +2167 -0
- package/source/server_publish_engine.ts +657 -0
- package/source/server_publish_engine_for_orphan_subscriptions.ts +52 -0
- package/source/server_session.ts +931 -0
- package/source/server_subscription.ts +1792 -0
- package/source/sessions_compatible_for_transfer.ts +33 -0
- package/source/validate_filter.ts +86 -0
- 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
|
+
}
|