loupedeck-commander 1.4.0 → 1.4.2

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.
@@ -1,481 +1,484 @@
1
- import {
2
- OPCUAClient,
3
- MessageSecurityMode,
4
- SecurityPolicy,
5
- AttributeIds,
6
- resolveNodeId,
7
- TimestampsToReturn,
8
- DataType
9
- } from "node-opcua";
10
-
11
- import { BaseIf } from './baseif.mjs'
12
-
13
- /**
14
- * Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
15
- */
16
- export class OPCUAIf extends BaseIf {
17
-
18
- #client
19
- #session
20
- #sub
21
- #connected
22
- #endpointurl
23
- #publishingInterval
24
- #samplingInterval
25
- // Dictionary to store monitored items : key = monitoredItemId, value = nodeID
26
- monitoreditems
27
- types
28
- // Dictionary to store buttons : key = monitoredItemId, value = buttonID
29
- buttons
30
- constructor() {
31
- super()
32
- }
33
-
34
- /**
35
- * Initialize the OPCUA client and subscribe to monitored items.
36
- * @param {*} options
37
- * @param {*} config
38
- * @param {*} callbackFunction
39
- */
40
- async init(options = {}, config = {}) {
41
- var res = this.Check(options)
42
- if (res < 0) {
43
- this.LogError(`OPCUAIf: Missing essential options in dictionary => Quitting $res $options\n`)
44
- }
45
- try {
46
- this.#endpointurl = this.formatString(options.endpointurl, options)
47
- if (options.publishingInterval)
48
- this.#publishingInterval = options.publishingInterval
49
- else {
50
- this.#publishingInterval = 250;
51
- this.LogInfo(`OPCUAIf init using default publishingInterval: ${this.#publishingInterval}ms\n`);
52
- }
53
- if (options.samplingInterval)
54
- this.#samplingInterval = options.samplingInterval
55
- else {
56
- this.#samplingInterval = 100;
57
- this.LogInfo(`OPCUAIf init using default samplingInterval: ${this.#samplingInterval}ms\n`);
58
- }
59
- this.options = options
60
- this.monitoreditems = {}
61
- this.types = {}
62
- this.buttons = {}
63
- this.LogInfo(`OPCUAIf init ${this.#endpointurl}\n`);
64
-
65
- await this.Connect(this.#endpointurl);
66
-
67
- let fields = [config.touch.center, config.knobs, config.buttons]
68
- for (let f = 0; f < fields.length; f++) {
69
- let field = fields[f]
70
- const buttonNames = Object.keys(field)
71
-
72
- // Iterate over buttons:
73
- for (let i = 0; i < buttonNames.length; i++) {
74
- const buttonID = buttonNames[i]
75
- const elem = field[buttonID]
76
- options["key"] = buttonID // we have to know the key of the button
77
- // groupnode
78
- if (elem.params) {
79
- let attributes = ["nodeid", "blink"]
80
- for (let j = 0; j < attributes.length; j++) {
81
- const attribute = attributes[j]
82
- let nodeID = super.formatString(elem.params[attribute], options)
83
- if (attribute in elem.params) {
84
- if (!this.IsNodeID(nodeID)) {
85
- continue
86
- }
87
-
88
- let monitoredItemId = await this.Subscribe(nodeID, options,attribute)
89
- if (monitoredItemId) {
90
- console.log("Register nodeid",nodeID, monitoredItemId, buttonID, attribute,)
91
- if (this.buttons[monitoredItemId] == undefined)
92
- this.buttons[monitoredItemId] = []
93
- this.buttons[monitoredItemId].push({
94
- buttonID: i,
95
- buttonName: buttonID,
96
- attribute: attribute
97
- })
98
- }
99
- }
100
- }
101
- }
102
- if (elem.states) {
103
- let attributes = ["opcua", "blink"]
104
- let states = Object.values(elem.states)
105
- // Iterate over states:
106
- for (let j = 0; j < states.length; j++) {
107
- const state = states[j]
108
-
109
- for (let k = 0; k < attributes.length; k++) {
110
- const attribute = attributes[k]
111
- let nodeID = super.formatString(state[attribute], options)
112
- if (attribute in state) {
113
- if (!this.IsNodeID(nodeID)) {
114
- continue
115
- }
116
- let monitoredItemId = await this.Subscribe(nodeID, options,attribute)
117
- if (monitoredItemId) {
118
- console.log("Register state",monitoredItemId, buttonID, attribute,nodeID)
119
- if (this.buttons[monitoredItemId] == undefined)
120
- this.buttons[monitoredItemId] = []
121
- this.buttons[monitoredItemId].push({
122
- buttonID: i,
123
- buttonName: buttonID,
124
- attribute: attribute
125
- })
126
- }
127
- }
128
- }
129
- }
130
- }
131
- }
132
- }
133
- } catch (error) {
134
- this.LogError(`OPCUAIf: Error\n`, error)
135
- }
136
- }
137
-
138
- /**
139
- * Convert the given value to the specified type.
140
- * @param {*} value : The value to convert.
141
- * @param {*} type : The type to convert to. Can be one of the following:
142
- * - DataType.Int16
143
- * - DataType.Int32
144
- * - DataType.Float
145
- * - DataType.String
146
- * @returns the converted value.
147
- */
148
- convert(value, type) {
149
- switch (type) {
150
- case DataType.Int16:
151
- case DataType.Int32:
152
- if (typeof value == "number") {
153
- if (Number.isInteger(value))
154
- return value
155
- else
156
- return Math.trunc(value)
157
- }
158
- return parseInt(value, 10)
159
- break
160
- case DataType.Float:
161
- if (typeof value == "number")
162
- return value
163
- return parseFloat(value)
164
- break;
165
- case DataType.String:
166
- if (typeof value == "number")
167
- return value.toString();
168
- return value
169
- case DataType.Boolean:
170
- if (typeof value == "number" && value === 1)
171
- return true
172
- if (typeof value == "string") {
173
- if (["true", "on"].includes(value)) {
174
- return true
175
- }
176
- }
177
- return false
178
- default:
179
- return value
180
- }
181
- }
182
-
183
- /**
184
- * Call the OPCUA node with the given options.
185
- * This method will write the value to the node and return the new state.
186
- * @param {*} opcuaNode
187
- * @param {*} options
188
- * @returns
189
- */
190
- async call(opcuaNode, options = {}) {
191
- var res = this.Check(options)
192
- if (res < 0) {
193
- // this.LogError(`OPCUAIf call: Missing essential options in dictionary => Quitting $res\n`)
194
- return false
195
- }
196
-
197
- var nodeId = super.call(opcuaNode, options)
198
-
199
- var type = this.types[nodeId]
200
-
201
- if (type === undefined){
202
- let dataValue = await this.Read(nodeId)
203
- this.types[nodeId] = dataValue.value.dataType
204
- type = this.types[nodeId]
205
- }
206
- var value = options.value
207
- if (typeof value == "string")
208
- value = super.formatString(options.value, options)
209
-
210
- var convertedValue = this.convert(value, type)
211
- this.LogInfo(`OPCUAIf: write ${nodeId} => ${value}\n`)
212
- await this.Write(nodeId, convertedValue, type)
213
-
214
- var NewState = "waiting"
215
- return NewState
216
- }
217
-
218
- Check(options) {
219
- var res = super.Check(options)
220
- if (res < 0) {
221
- this.LogError(`OPCUAIf: mandatory parameter missing\n`)
222
- return res
223
- }
224
- if (!"endpointurl" in options) {
225
- this.LogError(`OPCUAIf: mandatory parameter endpointurl missing\n`)
226
- return -11
227
- }
228
- if (!"publishingInterval" in options) {
229
- this.LogError(`OPCUAIf: mandatory parameter publishingInterval missing\n`)
230
- return -11
231
- }
232
- if (!"nodeid" in options) {
233
- this.LogError(`OPCUAIf: mandatory parameter nodeid missing\n`)
234
- return -12
235
- }
236
- if (!"value" in options) {
237
- this.LogError(`OPCUAIf: mandatory parameter value missing\n`)
238
- return -13
239
- }
240
- return 0
241
- }
242
-
243
- /**
244
- * This method is called when the interface is stopped.
245
- * It will disconnect the client and stop the subscription.
246
- * @returns
247
- */
248
- async stop() {
249
- if (!this.#client)
250
- return
251
- await this.Disconnect()
252
- this.LogInfo(`OPCUAIf Stopped\n`)
253
- }
254
-
255
- /**
256
- * Disconnect the OPCUA client and close the session.
257
- */
258
- async Disconnect() {
259
- if (this.#client) {
260
- if (this.#session)
261
- await this.#client.closeSession(this.#session, true)
262
- this.#session = undefined
263
- this.#client = undefined
264
- this.LogInfo(`OPCUAIf: Disconnected\n`);
265
- }
266
- }
267
- /**
268
- * Connect to the OPCUA server with the given URL.
269
- * @param {string} url - The URL of the OPCUA server, typical format: opc.tcp://ip-address:4840 .
270
- */
271
- async Connect(url) {
272
- let self = this
273
- if (this.#client) {
274
- await this.Disconnect()
275
- }
276
-
277
- this.#client = OPCUAClient.create({
278
- applicationName: "NodeOPCUA-Client",
279
- endpointMustExist: true,
280
- // keepSessionAlive: true,
281
- requestedSessionTimeout: 60 * 1000,
282
- securityMode: MessageSecurityMode.None,
283
- securityPolicy: SecurityPolicy.None,
284
- connectionStrategy: {
285
- maxRetry: -1,
286
- maxDelay: 5000,
287
- initialDelay: 2500
288
- },
289
-
290
- defaultSecureTokenLifetime: 20000,
291
- tokenRenewalInterval: 1000
292
- });
293
-
294
-
295
- this.#client.on("backoff", (retry, delay) => {
296
- //if ((retry % 10) == 0)
297
- // self.LogInfo(`OPCUAIf Try Reconnection ${retry} next attempt in ${delay}ms ${self.#endpointurl}\n`);
298
- });
299
-
300
- this.#client.on("connection_lost", () => {
301
- self.LogInfo(`OPCUAIf: Connection lost\n`);
302
- });
303
-
304
- this.#client.on("connection_reestablished", () => {
305
- self.LogInfo(`OPCUAIf: Connection re-established\n`);
306
- });
307
-
308
- this.#client.on("connection_failed", () => {
309
- self.LogInfo(`OPCUAIf: Connection failed\n`);
310
- });
311
- this.#client.on("start_reconnection", () => {
312
- self.LogInfo(`OPCUAIf: Starting reconnection\n`);
313
- });
314
-
315
- this.#client.on("after_reconnection", (err) => {
316
- self.LogInfo(`OPCUAIf: After Reconnection event => ${err}\n`);
317
- });
318
- /*this.#client.on("security_token_renewed", () => {
319
- self.LogDebug(`OPCUAIf: security_token_renewed\n`);
320
- })*/
321
- this.#client.on("lifetime_75", (token) => { })
322
-
323
- this.LogInfo(`OPCUAIf: connecting client to ${url}\n`);//, this.#session.toString());
324
- await this.#client.connect(url);
325
-
326
- this.#session = await this.#client.createSession();
327
-
328
- this.#session.on("session_closed", (statusCode) => {
329
- //self.LogInfo(`OPCUAIf: Session has been closed\n`);
330
- })
331
- this.#session.on("session_restored", () => {
332
- //self.LogInfo(`OPCUAIf: Session has been restored\n`);
333
- });
334
- this.#session.on("keepalive", (lastKnownServerState) => {
335
- self.LogInfo(`OPCUAIf: KeepAlive lastKnownServerState ${lastKnownServerState}\n`);
336
- });
337
- this.#session.on("keepalive_failure", () => {
338
- self.LogInfo(`OPCUAIf: KeepAlive failure\n`);
339
- });
340
-
341
- // create subscription with a custom publishing interval:
342
- this.#sub = await this.#session.createSubscription2({
343
- maxNotificationsPerPublish: 9000,
344
- publishingEnabled: true,
345
- requestedLifetimeCount: 10,
346
- requestedMaxKeepAliveCount: 10,
347
- requestedPublishingInterval: this.#publishingInterval
348
- });
349
-
350
- this.LogInfo(`OPCUAIf: session created\n`);
351
- // this.LogInfo(`OPCUAIf: client\n`);
352
- // this.LogInfo(`OPCUAIf: subscription\n`);
353
- this.#connected = true
354
- this.#endpointurl = url
355
- }
356
-
357
- /**
358
- * Check if the given nodeID is a valid OPCUA nodeID.
359
- * A valid nodeID has the format: ns=<namespace index>;s=<identifier>
360
- * where <namespace index> is a number and <identifier> is a string.
361
- * @param {*} nodeID
362
- * @returns
363
- */
364
- IsNodeID(nodeID) {
365
- let strNodeID = nodeID.toString()
366
- // regex: ns=\d;s=.*
367
- if (strNodeID.match(/^ns=\d+;s=.*$/)) {
368
- return true
369
- }
370
- return false
371
- }
372
-
373
- /**
374
- * Register a subscription for a nodeID.
375
- * This method will create a monitored item for the given nodeID and return the monitored item ID.
376
- * If the nodeID is already registered, it will return the existing monitored item ID.
377
- * @param {*} nodeID
378
- * @returns
379
- */
380
- async Subscribe(unformattedNodeID, options = {},attribute) {
381
- // Format the nodeID using the provided options
382
- let nodeID = this.formatString(unformattedNodeID, options)
383
-
384
- // install monitored item
385
- const monitoredNodeIdList = Object.keys(this.monitoreditems)
386
- let monitorItemId = -1
387
- for (let i = 0; i < monitoredNodeIdList.length; i++) {
388
- let monitorItemId = monitoredNodeIdList[i]
389
- if (this.monitoreditems[monitorItemId] == nodeID) {
390
- return monitorItemId // already registered => return itemid
391
- }
392
- }
393
-
394
- const itemToMonitor = {
395
- nodeId: resolveNodeId(nodeID),
396
- attributeId: AttributeIds.Value
397
- };
398
- const monitoringParameters = {
399
- samplingInterval: this.#samplingInterval,
400
- discardOldest: true,
401
- queueSize: 10
402
- };
403
-
404
- if (!this.#sub) {
405
- this.LogError(`OPCUAIf: no subscription - don't register monitored items $itemToMonitor\n`);
406
- return
407
- }
408
- const monitoredItem = await this.#sub.monitor(itemToMonitor, monitoringParameters, TimestampsToReturn.Both);
409
- this.monitoreditems[monitoredItem.monitoredItemId] = nodeID
410
- if (monitoredItem.monitoredItemId === undefined) {
411
- this.LogError(`OPCUAIf: Error subscribing to node ${nodeID} (attribute ${attribute})\n`)
412
- } else {
413
- this.LogDebug(`OPCUAIf: Subscribe to ${monitoredItem.monitoredItemId},${nodeID},(attribute ${attribute})\n`);
414
- }
415
-
416
- var self = this
417
- monitoredItem.on("changed", function (dataValue) {
418
- var nodeId = self.monitoreditems[this.monitoredItemId]
419
-
420
- // store the type of a nodeid in local dictionary
421
- self.types[nodeId] = dataValue.value.dataType
422
- // publish the value to subscribers:
423
- let buttons = self.buttons[this.monitoredItemId]
424
- for (var i=0;i<buttons.length;i++){
425
- var buttonInfo = buttons[i]
426
- self.LogInfo(`OPCUAIf: monitored item changed: ${buttonInfo.buttonID} ${buttonInfo.attribute} ${nodeId} => ${dataValue.value.value}\n`);
427
- self.emit('monitored item changed', buttonInfo.buttonID, buttonInfo.attribute, nodeId, dataValue.value.value)
428
- }
429
- });
430
-
431
- return monitoredItem.monitoredItemId;
432
- }
433
-
434
- async Read(nodeID) {
435
- const nodeToRead = {
436
- nodeId: nodeID,
437
- attributeId: AttributeIds.Value
438
- };
439
- if (!this.#connected) {
440
- this.LogError(`OPCUAIf: not connected, cannot read ${nodeID}\n`);
441
- return
442
- }
443
- const dataValue2 = await this.#session.read(nodeToRead, 0);
444
- this.LogError("OPCUAIf: read nodeID ", nodeID, dataValue2.toString(), "\n");
445
- return dataValue2
446
- }
447
-
448
- async Write(nodeID, value, datatype = DataType.String) {
449
- let self = this
450
- if (!this.#connected) {
451
- self.LogError("OPCUAIf: not connected, cannot write", nodeID, value, "\n");
452
- return
453
- }
454
- var nodesToWrite = [{
455
- nodeId: nodeID,
456
- attributeId: AttributeIds.Value,
457
- indexRange: null,
458
- value: {
459
- value: {
460
- dataType: datatype,
461
- value: value
462
- }
463
- }
464
- }];
465
- try {
466
- await this.#session.write(nodesToWrite, function (err, statusCodes) {
467
- if (!err) {
468
- if (statusCodes && statusCodes[0].value != 0) {
469
- self.LogInfo(`OPCUAIf: error with Node: "${nodeID}", status ${statusCodes[0]}\n`);
470
- } else {
471
- self.LogInfo(`OPCUAIf: wrote ${nodeID} => ${value}\n`);
472
- }
473
- } else {
474
- self.LogError(`OPCUAIf: writing not OK ${nodeID} => ${value}, ${err}\n`);
475
- }
476
- });
477
- } catch (err) {
478
- self.LogError(`OPCUAIf: writing not OK ${nodeID} => ${value}, ${err}\n`);
479
- }
480
- }
481
- }
1
+ import {
2
+ OPCUAClient,
3
+ MessageSecurityMode,
4
+ SecurityPolicy,
5
+ AttributeIds,
6
+ resolveNodeId,
7
+ TimestampsToReturn,
8
+ DataType
9
+ } from "node-opcua";
10
+
11
+ import { BaseIf } from './baseif.mjs'
12
+
13
+ /**
14
+ * Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
15
+ */
16
+ export class OPCUAIf extends BaseIf {
17
+
18
+ #client
19
+ #session
20
+ #sub
21
+ #connected
22
+ #endpointurl
23
+ #publishingInterval
24
+ #samplingInterval
25
+ // Dictionary to store monitored items : key = monitoredItemId, value = nodeID
26
+ monitoreditems
27
+ types
28
+ // Dictionary to store buttons : key = monitoredItemId, value = buttonID
29
+ buttons
30
+ constructor() {
31
+ super()
32
+ }
33
+
34
+ /**
35
+ * Initialize the OPCUA client and subscribe to monitored items.
36
+ * @param {*} options
37
+ * @param {*} config
38
+ * @param {*} callbackFunction
39
+ */
40
+ async init(options = {}, config = {}) {
41
+ var res = this.Check(options)
42
+ if (res < 0) {
43
+ this.LogError(`OPCUAIf: Missing essential options in dictionary => Quitting $res $options\n`)
44
+ }
45
+ try {
46
+ this.#endpointurl = this.formatString(options.endpointurl, options)
47
+ if (options.publishingInterval)
48
+ this.#publishingInterval = options.publishingInterval
49
+ else {
50
+ this.#publishingInterval = 250;
51
+ this.LogInfo(`OPCUAIf init using default publishingInterval: ${this.#publishingInterval}ms\n`);
52
+ }
53
+ if (options.samplingInterval)
54
+ this.#samplingInterval = options.samplingInterval
55
+ else {
56
+ this.#samplingInterval = 100;
57
+ this.LogInfo(`OPCUAIf init using default samplingInterval: ${this.#samplingInterval}ms\n`);
58
+ }
59
+ this.options = options
60
+ this.monitoreditems = {}
61
+ this.types = {}
62
+ this.buttons = {}
63
+ this.LogInfo(`OPCUAIf init ${this.#endpointurl}\n`);
64
+
65
+ await this.Connect(this.#endpointurl);
66
+
67
+ let fields = [config.touch.center, config.knobs, config.buttons]
68
+ for (let f = 0; f < fields.length; f++) {
69
+ let field = fields[f]
70
+ const buttonNames = Object.keys(field)
71
+
72
+ // Iterate over buttons:
73
+ for (let i = 0; i < buttonNames.length; i++) {
74
+ const buttonID = buttonNames[i]
75
+ const elem = field[buttonID]
76
+ options["key"] = buttonID // we have to know the key of the button
77
+ // groupnode
78
+ if (elem.params) {
79
+ let attributes = ["nodeid", "blink"]
80
+ for (let j = 0; j < attributes.length; j++) {
81
+ const attribute = attributes[j]
82
+ let nodeID = super.formatString(elem.params[attribute], options)
83
+ if (attribute in elem.params) {
84
+ if (!this.IsNodeID(nodeID)) {
85
+ continue
86
+ }
87
+
88
+ let monitoredItemId = await this.Subscribe(nodeID, options, attribute)
89
+ if (monitoredItemId) {
90
+ console.log("Register nodeid", nodeID, monitoredItemId, buttonID, attribute,)
91
+ if (this.buttons[monitoredItemId] == undefined)
92
+ this.buttons[monitoredItemId] = []
93
+ this.buttons[monitoredItemId].push({
94
+ buttonID: i,
95
+ buttonName: buttonID,
96
+ attribute: attribute
97
+ })
98
+ }
99
+ }
100
+ }
101
+ }
102
+ if (elem.states) {
103
+ let attributes = ["opcua", "blink"]
104
+ let states = Object.values(elem.states)
105
+ // Iterate over states:
106
+ for (let j = 0; j < states.length; j++) {
107
+ const state = states[j]
108
+
109
+ for (let k = 0; k < attributes.length; k++) {
110
+ const attribute = attributes[k]
111
+ let nodeID = super.formatString(state[attribute], options)
112
+ if (attribute in state) {
113
+ if (!this.IsNodeID(nodeID)) {
114
+ continue
115
+ }
116
+ let monitoredItemId = await this.Subscribe(nodeID, options, attribute)
117
+ if (monitoredItemId) {
118
+ console.log("Register state", monitoredItemId, buttonID, attribute, nodeID)
119
+ if (this.buttons[monitoredItemId] == undefined)
120
+ this.buttons[monitoredItemId] = []
121
+ this.buttons[monitoredItemId].push({
122
+ buttonID: i,
123
+ buttonName: buttonID,
124
+ attribute: attribute
125
+ })
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ } catch (error) {
134
+ this.LogError(`OPCUAIf: Error\n`, error)
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Convert the given value to the specified type.
140
+ * @param {*} value : The value to convert.
141
+ * @param {*} type : The type to convert to. Can be one of the following:
142
+ * - DataType.Int16
143
+ * - DataType.Int32
144
+ * - DataType.Float
145
+ * - DataType.String
146
+ * @returns the converted value.
147
+ */
148
+ convert(value, type) {
149
+ switch (type) {
150
+ case DataType.Int16:
151
+ case DataType.Int32:
152
+ if (typeof value == "number") {
153
+ if (Number.isInteger(value))
154
+ return value
155
+ else
156
+ return Math.trunc(value)
157
+ }
158
+ return parseInt(value, 10)
159
+ break
160
+ case DataType.Float:
161
+ if (typeof value == "number")
162
+ return value
163
+ return parseFloat(value)
164
+ break;
165
+ case DataType.String:
166
+ if (typeof value == "number")
167
+ return value.toString();
168
+ return value
169
+ case DataType.Boolean:
170
+ if (typeof value == "number" && value === 1)
171
+ return true
172
+ if (typeof value == "string") {
173
+ if (["true", "on"].includes(value)) {
174
+ return true
175
+ }
176
+ }
177
+ return false
178
+ default:
179
+ return value
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Call the OPCUA node with the given options.
185
+ * This method will write the value to the node and return the new state.
186
+ * @param {*} opcuaNode
187
+ * @param {*} options
188
+ * @returns
189
+ */
190
+ async call(opcuaNode, options = {}) {
191
+ var res = this.Check(options)
192
+ if (res < 0) {
193
+ // this.LogError(`OPCUAIf call: Missing essential options in dictionary => Quitting $res\n`)
194
+ return false
195
+ }
196
+
197
+ var nodeId = super.call(opcuaNode, options)
198
+
199
+ var type = this.types[nodeId]
200
+
201
+ if (type === undefined) {
202
+ let dataValue = await this.Read(nodeId)
203
+ if (dataValue) {
204
+ this.types[nodeId] = dataValue.value.dataType
205
+ type = this.types[nodeId]
206
+ }
207
+
208
+ }
209
+ var value = options.value
210
+ if (typeof value == "string")
211
+ value = super.formatString(options.value, options)
212
+
213
+ var convertedValue = this.convert(value, type)
214
+ this.LogInfo(`OPCUAIf: write ${nodeId} => ${value}\n`)
215
+ await this.Write(nodeId, convertedValue, type)
216
+
217
+ var NewState = "waiting"
218
+ return NewState
219
+ }
220
+
221
+ Check(options) {
222
+ var res = super.Check(options)
223
+ if (res < 0) {
224
+ this.LogError(`OPCUAIf: mandatory parameter missing\n`)
225
+ return res
226
+ }
227
+ if (!"endpointurl" in options) {
228
+ this.LogError(`OPCUAIf: mandatory parameter endpointurl missing\n`)
229
+ return -11
230
+ }
231
+ if (!"publishingInterval" in options) {
232
+ this.LogError(`OPCUAIf: mandatory parameter publishingInterval missing\n`)
233
+ return -11
234
+ }
235
+ if (!"nodeid" in options) {
236
+ this.LogError(`OPCUAIf: mandatory parameter nodeid missing\n`)
237
+ return -12
238
+ }
239
+ if (!"value" in options) {
240
+ this.LogError(`OPCUAIf: mandatory parameter value missing\n`)
241
+ return -13
242
+ }
243
+ return 0
244
+ }
245
+
246
+ /**
247
+ * This method is called when the interface is stopped.
248
+ * It will disconnect the client and stop the subscription.
249
+ * @returns
250
+ */
251
+ async stop() {
252
+ if (!this.#client)
253
+ return
254
+ await this.Disconnect()
255
+ this.LogInfo(`OPCUAIf Stopped\n`)
256
+ }
257
+
258
+ /**
259
+ * Disconnect the OPCUA client and close the session.
260
+ */
261
+ async Disconnect() {
262
+ if (this.#client) {
263
+ if (this.#session)
264
+ await this.#client.closeSession(this.#session, true)
265
+ this.#session = undefined
266
+ this.#client = undefined
267
+ this.LogInfo(`OPCUAIf: Disconnected\n`);
268
+ }
269
+ }
270
+ /**
271
+ * Connect to the OPCUA server with the given URL.
272
+ * @param {string} url - The URL of the OPCUA server, typical format: opc.tcp://ip-address:4840 .
273
+ */
274
+ async Connect(url) {
275
+ let self = this
276
+ if (this.#client) {
277
+ await this.Disconnect()
278
+ }
279
+
280
+ this.#client = OPCUAClient.create({
281
+ applicationName: "NodeOPCUA-Client",
282
+ endpointMustExist: true,
283
+ // keepSessionAlive: true,
284
+ requestedSessionTimeout: 60 * 1000,
285
+ securityMode: MessageSecurityMode.None,
286
+ securityPolicy: SecurityPolicy.None,
287
+ connectionStrategy: {
288
+ maxRetry: -1,
289
+ maxDelay: 5000,
290
+ initialDelay: 2500
291
+ },
292
+
293
+ defaultSecureTokenLifetime: 20000,
294
+ tokenRenewalInterval: 1000
295
+ });
296
+
297
+
298
+ this.#client.on("backoff", (retry, delay) => {
299
+ //if ((retry % 10) == 0)
300
+ // self.LogInfo(`OPCUAIf Try Reconnection ${retry} next attempt in ${delay}ms ${self.#endpointurl}\n`);
301
+ });
302
+
303
+ this.#client.on("connection_lost", () => {
304
+ self.LogInfo(`OPCUAIf: Connection lost\n`);
305
+ });
306
+
307
+ this.#client.on("connection_reestablished", () => {
308
+ self.LogInfo(`OPCUAIf: Connection re-established\n`);
309
+ });
310
+
311
+ this.#client.on("connection_failed", () => {
312
+ self.LogInfo(`OPCUAIf: Connection failed\n`);
313
+ });
314
+ this.#client.on("start_reconnection", () => {
315
+ self.LogInfo(`OPCUAIf: Starting reconnection\n`);
316
+ });
317
+
318
+ this.#client.on("after_reconnection", (err) => {
319
+ self.LogInfo(`OPCUAIf: After Reconnection event => ${err}\n`);
320
+ });
321
+ /*this.#client.on("security_token_renewed", () => {
322
+ self.LogDebug(`OPCUAIf: security_token_renewed\n`);
323
+ })*/
324
+ this.#client.on("lifetime_75", (token) => { })
325
+
326
+ this.LogInfo(`OPCUAIf: connecting client to ${url}\n`);//, this.#session.toString());
327
+ await this.#client.connect(url);
328
+
329
+ this.#session = await this.#client.createSession();
330
+
331
+ this.#session.on("session_closed", (statusCode) => {
332
+ //self.LogInfo(`OPCUAIf: Session has been closed\n`);
333
+ })
334
+ this.#session.on("session_restored", () => {
335
+ //self.LogInfo(`OPCUAIf: Session has been restored\n`);
336
+ });
337
+ this.#session.on("keepalive", (lastKnownServerState) => {
338
+ self.LogInfo(`OPCUAIf: KeepAlive lastKnownServerState ${lastKnownServerState}\n`);
339
+ });
340
+ this.#session.on("keepalive_failure", () => {
341
+ self.LogInfo(`OPCUAIf: KeepAlive failure\n`);
342
+ });
343
+
344
+ // create subscription with a custom publishing interval:
345
+ this.#sub = await this.#session.createSubscription2({
346
+ maxNotificationsPerPublish: 9000,
347
+ publishingEnabled: true,
348
+ requestedLifetimeCount: 10,
349
+ requestedMaxKeepAliveCount: 10,
350
+ requestedPublishingInterval: this.#publishingInterval
351
+ });
352
+
353
+ this.LogInfo(`OPCUAIf: session created\n`);
354
+ // this.LogInfo(`OPCUAIf: client\n`);
355
+ // this.LogInfo(`OPCUAIf: subscription\n`);
356
+ this.#connected = true
357
+ this.#endpointurl = url
358
+ }
359
+
360
+ /**
361
+ * Check if the given nodeID is a valid OPCUA nodeID.
362
+ * A valid nodeID has the format: ns=<namespace index>;s=<identifier>
363
+ * where <namespace index> is a number and <identifier> is a string.
364
+ * @param {*} nodeID
365
+ * @returns
366
+ */
367
+ IsNodeID(nodeID) {
368
+ let strNodeID = nodeID.toString()
369
+ // regex: ns=\d;s=.*
370
+ if (strNodeID.match(/^ns=\d+;s=.*$/)) {
371
+ return true
372
+ }
373
+ return false
374
+ }
375
+
376
+ /**
377
+ * Register a subscription for a nodeID.
378
+ * This method will create a monitored item for the given nodeID and return the monitored item ID.
379
+ * If the nodeID is already registered, it will return the existing monitored item ID.
380
+ * @param {*} nodeID
381
+ * @returns
382
+ */
383
+ async Subscribe(unformattedNodeID, options = {}, attribute) {
384
+ // Format the nodeID using the provided options
385
+ let nodeID = this.formatString(unformattedNodeID, options)
386
+
387
+ // install monitored item
388
+ const monitoredNodeIdList = Object.keys(this.monitoreditems)
389
+ let monitorItemId = -1
390
+ for (let i = 0; i < monitoredNodeIdList.length; i++) {
391
+ let monitorItemId = monitoredNodeIdList[i]
392
+ if (this.monitoreditems[monitorItemId] == nodeID) {
393
+ return monitorItemId // already registered => return itemid
394
+ }
395
+ }
396
+
397
+ const itemToMonitor = {
398
+ nodeId: resolveNodeId(nodeID),
399
+ attributeId: AttributeIds.Value
400
+ };
401
+ const monitoringParameters = {
402
+ samplingInterval: this.#samplingInterval,
403
+ discardOldest: true,
404
+ queueSize: 10
405
+ };
406
+
407
+ if (!this.#sub) {
408
+ this.LogError(`OPCUAIf: no subscription - don't register monitored items $itemToMonitor\n`);
409
+ return
410
+ }
411
+ const monitoredItem = await this.#sub.monitor(itemToMonitor, monitoringParameters, TimestampsToReturn.Both);
412
+ this.monitoreditems[monitoredItem.monitoredItemId] = nodeID
413
+ if (monitoredItem.monitoredItemId === undefined) {
414
+ this.LogError(`OPCUAIf: Error subscribing to node ${nodeID} (attribute ${attribute})\n`)
415
+ } else {
416
+ this.LogDebug(`OPCUAIf: Subscribe to ${monitoredItem.monitoredItemId},${nodeID},(attribute ${attribute})\n`);
417
+ }
418
+
419
+ var self = this
420
+ monitoredItem.on("changed", function (dataValue) {
421
+ var nodeId = self.monitoreditems[this.monitoredItemId]
422
+
423
+ // store the type of a nodeid in local dictionary
424
+ self.types[nodeId] = dataValue.value.dataType
425
+ // publish the value to subscribers:
426
+ let buttons = self.buttons[this.monitoredItemId]
427
+ for (var i = 0; i < buttons.length; i++) {
428
+ var buttonInfo = buttons[i]
429
+ self.LogInfo(`OPCUAIf: monitored item changed: ${buttonInfo.buttonID} ${buttonInfo.attribute} ${nodeId} => ${dataValue.value.value}\n`);
430
+ self.emit('monitored item changed', buttonInfo.buttonID, buttonInfo.attribute, nodeId, dataValue.value.value)
431
+ }
432
+ });
433
+
434
+ return monitoredItem.monitoredItemId;
435
+ }
436
+
437
+ async Read(nodeID) {
438
+ const nodeToRead = {
439
+ nodeId: nodeID,
440
+ attributeId: AttributeIds.Value
441
+ };
442
+ if (!this.#connected) {
443
+ this.LogError(`OPCUAIf: not connected, cannot read ${nodeID}\n`);
444
+ return
445
+ }
446
+ const dataValue2 = await this.#session.read(nodeToRead, 0);
447
+ this.LogError("OPCUAIf: read nodeID ", nodeID, dataValue2.toString(), "\n");
448
+ return dataValue2
449
+ }
450
+
451
+ async Write(nodeID, value, datatype = DataType.String) {
452
+ let self = this
453
+ if (!this.#connected) {
454
+ self.LogError("OPCUAIf: not connected, cannot write", nodeID, value, "\n");
455
+ return
456
+ }
457
+ var nodesToWrite = [{
458
+ nodeId: nodeID,
459
+ attributeId: AttributeIds.Value,
460
+ indexRange: null,
461
+ value: {
462
+ value: {
463
+ dataType: datatype,
464
+ value: value
465
+ }
466
+ }
467
+ }];
468
+ try {
469
+ await this.#session.write(nodesToWrite, function (err, statusCodes) {
470
+ if (!err) {
471
+ if (statusCodes && statusCodes[0].value != 0) {
472
+ self.LogInfo(`OPCUAIf: error with Node: "${nodeID}", status ${statusCodes[0]}\n`);
473
+ } else {
474
+ self.LogInfo(`OPCUAIf: wrote ${nodeID} => ${value}\n`);
475
+ }
476
+ } else {
477
+ self.LogError(`OPCUAIf: writing not OK ${nodeID} => ${value}, ${err}\n`);
478
+ }
479
+ });
480
+ } catch (err) {
481
+ self.LogError(`OPCUAIf: writing not OK ${nodeID} => ${value}, ${err}\n`);
482
+ }
483
+ }
484
+ }