loupedeck-commander 1.3.0 → 1.3.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/config.yaml ADDED
@@ -0,0 +1,6 @@
1
+ application: Example
2
+ profiles:
3
+ - name: profile-1
4
+ file: profile-1.yaml
5
+ - name: profile-2
6
+ file: profile-2.yaml
@@ -57,7 +57,7 @@ export class BaseIf extends EventEmitter {
57
57
 
58
58
  LogDebug(...args){
59
59
  if (this.options && this.options.verbose){
60
- let str = new String(args)
60
+ let str = args.join(' ')
61
61
  process.stdout.write(str.toString())
62
62
  }
63
63
  }
@@ -2,27 +2,14 @@ import {
2
2
  OPCUAClient,
3
3
  MessageSecurityMode,
4
4
  SecurityPolicy,
5
- BrowseDirection,
6
5
  AttributeIds,
7
- NodeClassMask,
8
- makeBrowsePath,
9
6
  resolveNodeId,
10
7
  TimestampsToReturn,
11
- coerceInt32,
12
- coerceByteString,
13
8
  DataType
14
9
  } from "node-opcua";
15
10
 
16
11
  import { BaseIf } from './baseif.mjs'
17
12
 
18
- const subscriptionParameters = {
19
- maxNotificationsPerPublish: 1000,
20
- publishingEnabled: true,
21
- requestedLifetimeCount: 100,
22
- requestedMaxKeepAliveCount: 10,
23
- requestedPublishingInterval: 1000
24
- };
25
-
26
13
  /**
27
14
  * Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
28
15
  */
@@ -34,20 +21,16 @@ export class OPCUAIf extends BaseIf {
34
21
  #connected
35
22
  #endpointurl
36
23
  #publishingInterval
24
+ #samplingInterval
25
+ // Dictionary to store monitored items : key = monitoredItemId, value = nodeID
37
26
  monitoreditems
38
27
  types
28
+ // Dictionary to store buttons : key = monitoredItemId, value = buttonID
39
29
  buttons
40
30
  constructor() {
41
31
  super()
42
32
  }
43
33
 
44
- async stop() {
45
- if (!this.#client)
46
- return
47
- await this.Disconnect()
48
- this.LogInfo(`OPCUAIf Stopped\n`)
49
- }
50
-
51
34
  /**
52
35
  * Initialize the OPCUA client and subscribe to monitored items.
53
36
  * @param {*} options
@@ -63,10 +46,16 @@ export class OPCUAIf extends BaseIf {
63
46
  this.#endpointurl = this.formatString(options.endpointurl, options)
64
47
  if (options.publishingInterval)
65
48
  this.#publishingInterval = options.publishingInterval
66
- else{
49
+ else {
67
50
  this.#publishingInterval = 250;
68
51
  this.LogInfo(`OPCUAIf init using default publishingInterval: ${this.#publishingInterval}ms\n`);
69
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
+ }
70
59
  this.monitoreditems = {}
71
60
  this.types = {}
72
61
  this.buttons = {}
@@ -75,47 +64,65 @@ export class OPCUAIf extends BaseIf {
75
64
  await this.Connect(this.#endpointurl);
76
65
 
77
66
  let fields = [config.touch.center, config.knobs, config.buttons]
78
- const fieldKeys = Object.keys(fields)
79
- for (let f = 0; f < fieldKeys.length; f++) {
67
+ for (let f = 0; f < fields.length; f++) {
80
68
  let field = fields[f]
81
- const keys = Object.keys(field)
69
+ const buttonNames = Object.keys(field)
82
70
 
83
71
  // Iterate over buttons:
84
- for (let i = 0; i < keys.length; i++) {
85
- const key = keys[i]
86
- const elem = field[key]
87
- options["key"] = key // we have to know the key of the button
72
+ for (let i = 0; i < buttonNames.length; i++) {
73
+ const buttonID = buttonNames[i]
74
+ const elem = field[buttonID]
75
+ options["key"] = buttonID // we have to know the key of the button
88
76
  // groupnode
89
- if (elem.params && elem.params.nodeid) {
90
- let formattedNodeId = this.formatString(elem.params.nodeid, options)
91
- let monitoredItemId = await this.Subscribe(formattedNodeId)
92
- console.log("Subscribe to",monitoredItemId,formattedNodeId)
93
- this.buttons[monitoredItemId] = i
77
+ if (elem.params) {
78
+ let attributes = ["nodeid", "blink"]
79
+ for (let j = 0; j < attributes.length; j++) {
80
+ const attribute = attributes[j]
81
+ if (attribute in elem.params) {
82
+ if (!this.IsNodeID(elem.params[attribute])) {
83
+ continue
84
+ }
85
+
86
+ let monitoredItemId = await this.Subscribe(elem.params[attribute], options,attribute)
87
+ if (monitoredItemId) {
88
+ this.buttons[monitoredItemId] = {
89
+ buttonID: i,
90
+ buttonName: buttonID,
91
+ attribute: attribute
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ if (elem.states) {
98
+ let attributes = ["opcua", "blink"]
99
+ let states = Object.values(elem.states)
100
+ // Iterate over states:
101
+ for (let j = 0; j < states.length; j++) {
102
+ const state = states[j]
103
+
104
+ for (let k = 0; k < attributes.length; k++) {
105
+ const attribute = attributes[k]
106
+ if (attribute in state) {
107
+ if (!this.IsNodeID(state[attribute])) {
108
+ continue
109
+ }
110
+ let monitoredItemId = await this.Subscribe(state[attribute], options,attribute)
111
+ if (monitoredItemId) {
112
+ this.buttons[monitoredItemId] = {
113
+ buttonID: i,
114
+ buttonName: buttonID,
115
+ attribute: attribute
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
94
121
  }
95
- await this.monitorStates(elem, options)
96
122
  }
97
123
  }
98
124
  } catch (error) {
99
- this.LogError(`OPCUAIf: Error\n`,error)
100
- }
101
- }
102
-
103
- /**
104
- *
105
- * @param {*} elem : Elem Object
106
- * @param {*} options
107
- */
108
- async monitorStates(elem, options) {
109
- const stateKeys = Object.keys(elem.states)
110
- for (let i = 0; i < stateKeys.length; i++) {
111
- const key2 = stateKeys[i]
112
- const state = elem.states[key2]
113
- if (state.opcua) {
114
- let formattedNodeId = this.formatString(state.opcua, options)
115
- let monitoredItemId = await this.Subscribe(formattedNodeId)
116
- console.log("Subscribe to",monitoredItemId,formattedNodeId)
117
- this.buttons[monitoredItemId] = i
118
- }
125
+ this.LogError(`OPCUAIf: Error\n`, error)
119
126
  }
120
127
  }
121
128
 
@@ -164,6 +171,13 @@ export class OPCUAIf extends BaseIf {
164
171
  }
165
172
  }
166
173
 
174
+ /**
175
+ * Call the OPCUA node with the given options.
176
+ * This method will write the value to the node and return the new state.
177
+ * @param {*} opcuaNode
178
+ * @param {*} options
179
+ * @returns
180
+ */
167
181
  async call(opcuaNode, options = {}) {
168
182
  var res = this.Check(options)
169
183
  if (res < 0) {
@@ -171,7 +185,7 @@ export class OPCUAIf extends BaseIf {
171
185
  return false
172
186
  }
173
187
 
174
- var nodeId = super.formatString(opcuaNode, options)
188
+ var nodeId = super.call(opcuaNode, options)
175
189
 
176
190
  var type = this.types[nodeId]
177
191
  var value = options.value
@@ -211,21 +225,40 @@ export class OPCUAIf extends BaseIf {
211
225
  return 0
212
226
  }
213
227
 
228
+ /**
229
+ * This method is called when the interface is stopped.
230
+ * It will disconnect the client and stop the subscription.
231
+ * @returns
232
+ */
233
+ async stop() {
234
+ if (!this.#client)
235
+ return
236
+ await this.Disconnect()
237
+ this.LogInfo(`OPCUAIf Stopped\n`)
238
+ }
239
+
240
+ /**
241
+ * Disconnect the OPCUA client and close the session.
242
+ */
214
243
  async Disconnect() {
215
244
  if (this.#client) {
216
245
  if (this.#session)
217
- await this.#client.closeSession(this.#session,true)
246
+ await this.#client.closeSession(this.#session, true)
218
247
  this.#session = undefined
219
248
  this.#client = undefined
220
249
  this.LogInfo(`OPCUAIf: Disconnected\n`);
221
250
  }
222
251
  }
252
+ /**
253
+ * Connect to the OPCUA server with the given URL.
254
+ * @param {string} url - The URL of the OPCUA server, typical format: opc.tcp://ip-address:4840 .
255
+ */
223
256
  async Connect(url) {
224
257
  let self = this
225
- if (this.#client){
258
+ if (this.#client) {
226
259
  await this.Disconnect()
227
260
  }
228
-
261
+
229
262
  this.#client = OPCUAClient.create({
230
263
  applicationName: "NodeOPCUA-Client",
231
264
  endpointMustExist: true,
@@ -242,7 +275,7 @@ export class OPCUAIf extends BaseIf {
242
275
  defaultSecureTokenLifetime: 20000,
243
276
  tokenRenewalInterval: 1000
244
277
  });
245
-
278
+
246
279
 
247
280
  this.#client.on("backoff", (retry, delay) => {
248
281
  //if ((retry % 10) == 0)
@@ -267,9 +300,9 @@ export class OPCUAIf extends BaseIf {
267
300
  this.#client.on("after_reconnection", (err) => {
268
301
  self.LogInfo(`OPCUAIf: After Reconnection event => ${err}\n`);
269
302
  });
270
- this.#client.on("security_token_renewed", () => {
303
+ /*this.#client.on("security_token_renewed", () => {
271
304
  self.LogDebug(`OPCUAIf: security_token_renewed\n`);
272
- })
305
+ })*/
273
306
  this.#client.on("lifetime_75", (token) => { })
274
307
 
275
308
  this.LogInfo(`OPCUAIf: connecting client to ${url}\n`);//, this.#session.toString());
@@ -299,23 +332,46 @@ export class OPCUAIf extends BaseIf {
299
332
  requestedPublishingInterval: this.#publishingInterval
300
333
  });
301
334
 
302
- this.LogInfo(`OPCUAIf: session created\n`);//, this.#session.toString());
303
- this.LogInfo(`OPCUAIf: client\n`);
304
- this.LogInfo(`OPCUAIf: subscription\n`);
335
+ this.LogInfo(`OPCUAIf: session created\n`);
336
+ // this.LogInfo(`OPCUAIf: client\n`);
337
+ // this.LogInfo(`OPCUAIf: subscription\n`);
305
338
  this.#connected = true
306
339
  this.#endpointurl = url
307
340
  }
308
341
 
309
- async Subscribe(nodeID) {
310
- // install monitored item
342
+ /**
343
+ * Check if the given nodeID is a valid OPCUA nodeID.
344
+ * A valid nodeID has the format: ns=<namespace index>;s=<identifier>
345
+ * where <namespace index> is a number and <identifier> is a string.
346
+ * @param {*} nodeID
347
+ * @returns
348
+ */
349
+ IsNodeID(nodeID) {
350
+ let strNodeID = nodeID.toString()
351
+ // regex: ns=\d;s=.*
352
+ if (strNodeID.match(/^ns=\d+;s=.*$/)) {
353
+ return true
354
+ }
355
+ return false
356
+ }
311
357
 
312
- const keys = Object.keys(this.monitoreditems)
313
- for (let i = 0; i < keys.length; i++) {
314
- const key = keys[i]
315
- const elem = this.monitoreditems[key]
316
- if (elem == nodeID) {
358
+ /**
359
+ * Register a subscription for a nodeID.
360
+ * This method will create a monitored item for the given nodeID and return the monitored item ID.
361
+ * If the nodeID is already registered, it will return the existing monitored item ID.
362
+ * @param {*} nodeID
363
+ * @returns
364
+ */
365
+ async Subscribe(unformattedNodeID, options = {},attribute) {
366
+ // Format the nodeID using the provided options
367
+ let nodeID = this.formatString(unformattedNodeID, options)
368
+
369
+ // install monitored item
370
+ const monitoredNodeIdList = Object.values(this.monitoreditems)
371
+ for (let i = 0; i < monitoredNodeIdList.length; i++) {
372
+ if (monitoredNodeIdList[i] == nodeID) {
317
373
  // already registered => return itemid
318
- return key
374
+ return this.monitoreditems[i]
319
375
  }
320
376
  }
321
377
 
@@ -324,13 +380,13 @@ export class OPCUAIf extends BaseIf {
324
380
  attributeId: AttributeIds.Value
325
381
  };
326
382
  const monitoringParameters = {
327
- samplingInterval: 100,
383
+ samplingInterval: this.#samplingInterval,
328
384
  discardOldest: true,
329
385
  queueSize: 10
330
386
  };
331
387
 
332
388
  if (!this.#sub) {
333
- this.LogError(`OPCUAIf: not register monitored items $itemToMonitor\n`);
389
+ this.LogError(`OPCUAIf: no subscription - don't register monitored items $itemToMonitor\n`);
334
390
  return
335
391
  }
336
392
  const monitoredItem = await this.#sub.monitor(itemToMonitor, monitoringParameters, TimestampsToReturn.Both);
@@ -338,14 +394,20 @@ export class OPCUAIf extends BaseIf {
338
394
  var self = this
339
395
  monitoredItem.on("changed", function (dataValue) {
340
396
  var nodeId = self.monitoreditems[this.monitoredItemId]
341
- var buttonID = self.buttons[this.monitoredItemId]
397
+ var buttonInfo = self.buttons[this.monitoredItemId]
342
398
  // store the type of a nodeid in local dictionary
343
399
  self.types[nodeId] = dataValue.value.dataType
344
400
  // publish the value to subscribers:
345
- self.LogInfo(`OPCUAIf: monitored item changed: ${nodeId} => ${dataValue.value.value}\n`);
346
- self.emit('monitored item changed', buttonID, nodeId, dataValue.value.value)
401
+ self.LogInfo(`OPCUAIf: monitored item changed: ${buttonInfo.buttonID} ${buttonInfo.attribute} ${nodeId} => ${dataValue.value.value}\n`);
402
+ self.emit('monitored item changed', buttonInfo.buttonID, buttonInfo.attribute, nodeId, dataValue.value.value)
347
403
  });
348
404
 
405
+ if (monitoredItem.monitoredItemId === undefined) {
406
+ this.LogError(`OPCUAIf: Error subscribing to node ${nodeID} (attribute ${attribute})\n`)
407
+ } else {
408
+ this.LogDebug(`OPCUAIf: Subscribe to ${monitoredItem.monitoredItemId},${nodeID},(attribute ${attribute})\n`);
409
+ }
410
+
349
411
  return monitoredItem.monitoredItemId;
350
412
  }
351
413
 
@@ -390,12 +452,10 @@ export class OPCUAIf extends BaseIf {
390
452
  }
391
453
  } else {
392
454
  self.LogError("OPCUAIf: write NOT ok", nodeID, value, "\n");
393
- self.LogError(err)
394
455
  }
395
456
  });
396
457
  } catch (err) {
397
458
  self.LogError("OPCUAIf: write NOT ok", nodeID, value, "\n");
398
- self.LogError(err)
399
459
  }
400
460
  }
401
461
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loupedeck-commander",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "A system to ease working with LoupeDeck devices using CMD-line, OPC/UA or HTTP-client interfaces",
5
5
  "main": "index.mjs",
6
6
  "scripts": {
@@ -14,7 +14,8 @@
14
14
  "loupedeck": "^7.0.0",
15
15
  "mkdirp": "^3.0.1",
16
16
  "node-opcua": "^2.153.0",
17
- "string-template": "^1.0.0"
17
+ "string-template": "^1.0.0",
18
+ "yaml": "^2.8.0"
18
19
  },
19
20
  "author": "Thomas Schneider",
20
21
  "license": "Apache-2.0",
package/profile-1.yaml ADDED
@@ -0,0 +1,186 @@
1
+ name: profile-1
2
+ profile: example
3
+ description: ""
4
+ touch:
5
+ center:
6
+ "0":
7
+ states:
8
+ off:
9
+ color: "#000055"
10
+ textColor: "#FFFFFF"
11
+ image: icons/home.png
12
+ cmd: echo "{id} {state}"
13
+ font: 20px Arial
14
+ blink: true
15
+ on:
16
+ color: "#00ff00"
17
+ image: icons/home.png
18
+ cmd: echo "{id} {state}"
19
+ font: 10px Arial
20
+ blink: false
21
+ params:
22
+ text: "{key}"
23
+ "1":
24
+ states:
25
+ off:
26
+ color: "#000099"
27
+ image: icons/home.png
28
+ cmd: echo "{id} {state} {pressed}"
29
+ on:
30
+ color: "#00ff00"
31
+ image: icons/home.png
32
+ cmd: echo "{id} {state} {pressed}"
33
+ blink: true
34
+ params:
35
+ minPressed: 100
36
+ text: "{released}"
37
+ "2":
38
+ states:
39
+ off:
40
+ color: "#000099"
41
+ image: icons/home.png
42
+ cmd: echo "{id} {state}"
43
+ on:
44
+ color: "#00ff00"
45
+ image: icons/home.png
46
+ cmd: echo "{id} {state}"
47
+ "3":
48
+ states:
49
+ off:
50
+ color: "#000099"
51
+ image: icons/home.png
52
+ cmd: echo "{id} {state}"
53
+ on:
54
+ color: "#00ff00"
55
+ image: icons/home.png
56
+ cmd: echo "{id} {state}"
57
+ "4":
58
+ states:
59
+ off:
60
+ color: "#000099"
61
+ image: icons/home.png
62
+ cmd: echo "{id} {state}"
63
+ on:
64
+ color: "#00ff00"
65
+ image: icons/home.png
66
+ cmd: echo "{id} {state}"
67
+ "5":
68
+ states:
69
+ off:
70
+ color: "#000099"
71
+ image: icons/home.png
72
+ cmd: echo "{id} {state}"
73
+ on:
74
+ color: "#00ff00"
75
+ image: icons/home.png
76
+ cmd: echo "{id} {state}"
77
+ "6":
78
+ states:
79
+ off:
80
+ color: "#000099"
81
+ image: icons/home.png
82
+ cmd: echo "{id} {state}"
83
+ on:
84
+ color: "#00ff00"
85
+ image: icons/home.png
86
+ cmd: echo "{id} {state}"
87
+ "7":
88
+ states:
89
+ off:
90
+ color: "#000099"
91
+ image: icons/home.png
92
+ cmd: echo "{id} {state}"
93
+ on:
94
+ color: "#00ff00"
95
+ image: icons/home.png
96
+ cmd: echo "{id} {state}"
97
+ "8":
98
+ states:
99
+ off:
100
+ color: "#000099"
101
+ image: icons/home.png
102
+ cmd: echo "{id} {state}"
103
+ on:
104
+ color: "#00ff00"
105
+ image: icons/home.png
106
+ cmd: echo "{id} {state}"
107
+ "9":
108
+ states:
109
+ off:
110
+ color: "#000099"
111
+ image: icons/home.png
112
+ cmd: echo "{id} {state}"
113
+ on:
114
+ color: "#00ff00"
115
+ image: icons/home.png
116
+ cmd: echo "{id} {state}"
117
+ "10":
118
+ states:
119
+ off:
120
+ color: "#000099"
121
+ image: icons/home.png
122
+ cmd: echo "{id} {state}"
123
+ on:
124
+ color: "#00ff00"
125
+ image: icons/home.png
126
+ cmd: echo "{id} {state}"
127
+ "11":
128
+ states:
129
+ off:
130
+ color: "#000099"
131
+ image: icons/home.png
132
+ cmd: echo "{id} {state}"
133
+ on:
134
+ color: "#00ff00"
135
+ image: icons/home.png
136
+ cmd: echo "{id} {state}"
137
+ left:
138
+ "0":
139
+ states:
140
+ on:
141
+ color: "#000000"
142
+ cmd: echo "{id} {state}"
143
+ right:
144
+ "0":
145
+ states:
146
+ on:
147
+ color: "#000000"
148
+ cmd: echo "{id} {state}"
149
+ knobs:
150
+ knobTL:
151
+ states:
152
+ on:
153
+ cmd: echo "{id} {state}"
154
+ knobCL:
155
+ states:
156
+ on:
157
+ cmd: echo "{id} {state}"
158
+ knobBL:
159
+ states:
160
+ on:
161
+ cmd: echo "{id} {state}"
162
+ opcua: ns=2;s=DRV.AAFHR.compOut.FeldTempGetriebeoel
163
+ blink: ns=2;s=DRV.AAFHR.compOut.Feststellbremse
164
+ knobTR:
165
+ states:
166
+ on:
167
+ cmd: echo "{id} {state}"
168
+ knobCR:
169
+ states:
170
+ on:
171
+ cmd: echo "{id} {state}"
172
+ knobBR:
173
+ states:
174
+ on:
175
+ cmd: echo "{id} {state}"
176
+ parameters:
177
+ hostname: 127.0.0.1
178
+ simnbr: "1"
179
+ endpointurl: opc.tcp://{hostname}:4840
180
+ publishingInterval: 200
181
+ nodeid: ns=0;s=nodeID
182
+ textColor: "#abcdff"
183
+ textBaseLine: top
184
+ textAlign: left
185
+ font: 14px Arial
186
+ verbose: true