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.
- package/LICENSE +201 -201
- package/README.md +131 -131
- package/common/ApplicationConfig.mjs +300 -281
- package/common/BaseLoupeDeckHandler.mjs +393 -381
- package/common/ButtonField.mjs +168 -166
- package/common/button.mjs +533 -520
- package/common/cmd-executer.mjs +16 -16
- package/common/index.mjs +5 -5
- package/common/utils.mjs +106 -106
- package/config.yaml +8 -6
- package/index.mjs +3 -3
- package/interfaces/baseif.mjs +71 -71
- package/interfaces/httpif.mjs +90 -90
- package/interfaces/interfaces.mjs +34 -34
- package/interfaces/opcuaif.mjs +484 -481
- package/interfaces/shellif.mjs +58 -58
- package/package.json +32 -32
- package/profile-1.yaml +186 -186
- package/profile-2.yaml +213 -213
- package/test.mjs +40 -40
package/interfaces/opcuaif.mjs
CHANGED
|
@@ -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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
this.#
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
this.#client.on("
|
|
312
|
-
self.LogInfo(`OPCUAIf:
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
self.
|
|
320
|
-
})
|
|
321
|
-
this.#client.on("
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
this
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.#session.on("
|
|
332
|
-
//self.LogInfo(`OPCUAIf: Session has been
|
|
333
|
-
})
|
|
334
|
-
this.#session.on("
|
|
335
|
-
self.LogInfo(`OPCUAIf:
|
|
336
|
-
});
|
|
337
|
-
this.#session.on("
|
|
338
|
-
self.LogInfo(`OPCUAIf: KeepAlive
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
this
|
|
354
|
-
this
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
+
}
|