loupedeck-commander 1.3.1 → 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.
@@ -219,7 +219,7 @@ export class BaseLoupeDeckHandler {
219
219
  const self = this
220
220
 
221
221
  // Register callback on monitored item change:
222
- opcuainterface.on("monitored item changed",(buttonID,nodeid,val) => { self.buttonStateChanged(buttonID,nodeid,val) })
222
+ opcuainterface.on("monitored item changed",(buttonID,attribute,nodeid,val) => { self.buttonStateChanged(buttonID,attribute,nodeid,val) })
223
223
  profileEmitter.on("profileChanged",(profileID) => { self.activateProfile(profileID) })
224
224
  profileEmitter.on("brightnessChanged",(brightness) => { self.changeBrightness(brightness) })
225
225
  profileEmitter.on("vibrate",(pattern) => { self.vibrate(pattern) })
@@ -364,14 +364,14 @@ export class BaseLoupeDeckHandler {
364
364
  /**
365
365
  * Handler for StateChanged through OPC/UA Interface - only connected with touch-buttons on center field (yet)
366
366
  */
367
- async buttonStateChanged(buttonID,nodeid,val) {
367
+ async buttonStateChanged(buttonID,attribute,nodeid,val) {
368
368
  let ok = false
369
369
  this.screenUpdate["center"] = true
370
370
  this.screenUpdate["left"] = true
371
371
  this.screenUpdate["right"] = true
372
372
 
373
- ok = await this.screens.center.changed(buttonID,nodeid,val)
374
- ok = await this.knobs.changed(buttonID,nodeid,val)
373
+ ok = await this.screens.center.buttonStateChanged(buttonID,attribute,nodeid,val)
374
+ ok = await this.knobs.buttonStateChanged(buttonID,attribute,nodeid,val)
375
375
 
376
376
  //await this.updateScreens()
377
377
  return ok
@@ -97,8 +97,13 @@ export class ButtonField {
97
97
  for (let i = 0; i < this.#keys.length; i++) {
98
98
  let key = this.#keys[i]
99
99
  if (!isNaN(key)) { key = parseInt(key, 10) }
100
+ // Ignore for myself (active element of the group)
100
101
  if (id === key) { continue }
101
- if (this.#buttons[key].group === this.#buttons[id].group) {
102
+ let toggleGroup = this.#buttons[id].getGroup()
103
+ // Ignore empty groups
104
+ if (toggleGroup === undefined || toggleGroup == "" ) { continue }
105
+ // Switch all other elements of the same group off:
106
+ if (this.#buttons[key].getGroup() === toggleGroup) {
102
107
  this.#buttons[key].setState(0)
103
108
  }
104
109
  }
@@ -115,10 +120,12 @@ export class ButtonField {
115
120
  * @param {*} nodeid
116
121
  * @param {*} val
117
122
  */
118
- async changed(buttonID,nodeid,val){
123
+ async buttonStateChanged(changedButtonID,attribute,nodeid,val){
124
+ // todo: filter for buttonID
119
125
  for (let i = 0; i < this.#keys.length; i++) {
120
- let bID = this.#keys[i]
121
- await this.#buttons[bID].changed(i,nodeid,val)
126
+ let buttonID = this.#keys[i]
127
+ if (changedButtonID == buttonID)
128
+ await this.#buttons[buttonID].buttonStateChanged(i,attribute,nodeid,val)
122
129
  }
123
130
  }
124
131
 
package/common/button.mjs CHANGED
@@ -20,6 +20,7 @@ export class Button {
20
20
  //#data
21
21
  #index = 0
22
22
  #enforcedIndex = -1
23
+ #enforcedBlink = false
23
24
  #event
24
25
  #keys
25
26
  #states
@@ -38,6 +39,7 @@ export class Button {
38
39
  this.id = id
39
40
  this.#index = 0
40
41
  this.#enforcedIndex = -1
42
+ this.#enforcedBlink = false
41
43
 
42
44
  this.#profile = profile
43
45
  this.#params = {
@@ -144,6 +146,14 @@ export class Button {
144
146
  this.#index = index
145
147
  }
146
148
 
149
+ /**
150
+ *
151
+ * @returns Group attribute of the current button
152
+ */
153
+ getGroup() {
154
+ return this.#params.group
155
+ }
156
+
147
157
  /**
148
158
  * Draw a physical button (color in RGBA format) on a device
149
159
  * @param {*} device
@@ -194,7 +204,7 @@ export class Button {
194
204
  let color = params.color
195
205
  let textColor = params.textColor
196
206
 
197
- if (params.blink && this.#counter % 2 == 0) {
207
+ if ((params.blink || this.#enforcedBlink) && this.#counter % 2 == 0) {
198
208
  // If blink is set, we switch textcolor and background color:
199
209
  color = params.textColor
200
210
  textColor = params.color
@@ -370,28 +380,32 @@ export class Button {
370
380
  * @param {*} val : the current value of the given nodeid
371
381
  * @returns
372
382
  */
373
- async changed(buttonID, nodeid, val) {
374
- // Only handle updates within the same group identified by nodeid
375
- if (nodeid !== this.#params.nodeid) {
376
- return
377
- }
378
-
379
- this.#index = 0;
380
- this.#enforcedIndex = -1;
381
- for (let i = 0; i < this.#keys.length; i++) {
382
- let stateStr = this.#keys[i].toString()
383
- let valStr = val.toString()
384
- // check if the state-name is same as the value we get from outside:
385
- if (valStr == stateStr) {
386
- if (i !== this.#index) {
387
- console.info("Changed index", this.id, nodeid, val, i)
388
- this.#index = i;
389
- console.info("enforce State index", this.id, nodeid, val, i)
390
- this.#enforcedIndex = this.#index;
383
+ async buttonStateChanged(buttonID, attribute, nodeid, val) {
384
+
385
+ //
386
+ if (attribute == "nodeid" && nodeid == this.#params.nodeid) {
387
+ this.#index = 0;
388
+ this.#enforcedIndex = -1;
389
+ for (let i = 0; i < this.#keys.length; i++) {
390
+ let stateStr = this.#keys[i].toString()
391
+ let valStr = val.toString()
392
+ // check if the state-name is same as the value we get from outside:
393
+ if (valStr == stateStr) {
394
+ if (i !== this.#index) {
395
+ console.info("Changed index", this.id, nodeid, val, i)
396
+ this.#index = i;
397
+ console.info("enforce State index", this.id, nodeid, val, i)
398
+ this.#enforcedIndex = this.#index;
399
+ }
400
+ break;
391
401
  }
392
- break;
393
402
  }
394
403
  }
404
+
405
+ if (attribute == "blink") {
406
+ console.info("Changed blink", buttonID, nodeid, val)
407
+ this.#enforcedBlink = val;
408
+ }
395
409
  }
396
410
 
397
411
  /**
@@ -475,23 +489,27 @@ export class Button {
475
489
  profileEmitter.emit("profileChanged", params.profile)
476
490
  }
477
491
 
478
- if (params.brightness !== undefined) {
492
+ if (params.brightness !== undefined && params.brightness !== '') {
493
+ // If brightness is set, we emit a brightnessChanged event with the given params
494
+ // This is used to change the brightness of the device
479
495
  profileEmitter.emit("brightnessChanged", params.brightness)
480
496
  }
481
497
 
482
- if (params.vibrate !== undefined) {
498
+ if (params.vibrate !== undefined && params.vibrate !== '' && params.vibrate !== false) {
499
+ // If vibrate is set, we emit a vibrate event with the given params
500
+ // This is used to trigger a vibration on the device
483
501
  profileEmitter.emit("vibrate", params.vibrate)
484
502
  }
485
503
 
486
504
  let res = ''
487
505
  if ('cmd' in params && shellinterface) {
488
- res = await shellinterface.call(params.cmd, params)
506
+ res = await shellinterface.call(params.cmd, params)
489
507
  }
490
508
  if ('http' in params && httpinterface) {
491
- res = await httpinterface.call(params.http, params)
509
+ res = await httpinterface.call(params.http, params)
492
510
  }
493
511
  if ('opcua' in params && opcuainterface) {
494
- res = await opcuainterface.call(params.opcua, params)
512
+ res = await opcuainterface.call(params.opcua, params)
495
513
  }
496
514
 
497
515
  return res
@@ -22,12 +22,14 @@ export class OPCUAIf extends BaseIf {
22
22
  #endpointurl
23
23
  #publishingInterval
24
24
  #samplingInterval
25
+ // Dictionary to store monitored items : key = monitoredItemId, value = nodeID
25
26
  monitoreditems
26
27
  types
28
+ // Dictionary to store buttons : key = monitoredItemId, value = buttonID
27
29
  buttons
28
30
  constructor() {
29
31
  super()
30
- }
32
+ }
31
33
 
32
34
  /**
33
35
  * Initialize the OPCUA client and subscribe to monitored items.
@@ -44,13 +46,13 @@ export class OPCUAIf extends BaseIf {
44
46
  this.#endpointurl = this.formatString(options.endpointurl, options)
45
47
  if (options.publishingInterval)
46
48
  this.#publishingInterval = options.publishingInterval
47
- else{
49
+ else {
48
50
  this.#publishingInterval = 250;
49
51
  this.LogInfo(`OPCUAIf init using default publishingInterval: ${this.#publishingInterval}ms\n`);
50
52
  }
51
53
  if (options.samplingInterval)
52
54
  this.#samplingInterval = options.samplingInterval
53
- else{
55
+ else {
54
56
  this.#samplingInterval = 100;
55
57
  this.LogInfo(`OPCUAIf init using default samplingInterval: ${this.#samplingInterval}ms\n`);
56
58
  }
@@ -62,55 +64,65 @@ export class OPCUAIf extends BaseIf {
62
64
  await this.Connect(this.#endpointurl);
63
65
 
64
66
  let fields = [config.touch.center, config.knobs, config.buttons]
65
- const fieldKeys = Object.keys(fields)
66
- for (let f = 0; f < fieldKeys.length; f++) {
67
+ for (let f = 0; f < fields.length; f++) {
67
68
  let field = fields[f]
68
- const keys = Object.keys(field)
69
+ const buttonNames = Object.keys(field)
69
70
 
70
71
  // Iterate over buttons:
71
- for (let i = 0; i < keys.length; i++) {
72
- const key = keys[i]
73
- const elem = field[key]
74
- 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
75
76
  // groupnode
76
- if (elem.params && elem.params.nodeid) {
77
- let formattedNodeId = this.formatString(elem.params.nodeid, options)
78
- let monitoredItemId = await this.Subscribe(formattedNodeId)
79
- if (monitoredItemId === undefined) {
80
- this.LogError(`OPCUAIf: Error subscribing to node ${formattedNodeId}\n`)
81
- continue
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
+ }
82
120
  }
83
- console.log("OPCUAIf: Subscribe to",monitoredItemId,formattedNodeId)
84
- this.buttons[monitoredItemId] = i
85
121
  }
86
- await this.monitorStates(elem, options)
87
122
  }
88
123
  }
89
124
  } catch (error) {
90
- this.LogError(`OPCUAIf: Error\n`,error)
91
- }
92
- }
93
-
94
- /**
95
- * Monitor the state OPC/UA node of the given element
96
- * @param {*} elem : Elem Object
97
- * @param {*} options
98
- */
99
- async monitorStates(elem, options) {
100
- const stateKeys = Object.keys(elem.states)
101
- for (let i = 0; i < stateKeys.length; i++) {
102
- const key2 = stateKeys[i]
103
- const state = elem.states[key2]
104
- if (state.opcua) {
105
- let formattedNodeId = this.formatString(state.opcua, options)
106
- let monitoredItemId = await this.Subscribe(formattedNodeId)
107
- if (monitoredItemId === undefined) {
108
- this.LogError(`OPCUAIf: Error subscribing State to node ${formattedNodeId}\n`)
109
- continue
110
- }
111
- console.log("OPCUAIf: Subscribe to",monitoredItemId,formattedNodeId)
112
- this.buttons[monitoredItemId] = i
113
- }
125
+ this.LogError(`OPCUAIf: Error\n`, error)
114
126
  }
115
127
  }
116
128
 
@@ -231,7 +243,7 @@ export class OPCUAIf extends BaseIf {
231
243
  async Disconnect() {
232
244
  if (this.#client) {
233
245
  if (this.#session)
234
- await this.#client.closeSession(this.#session,true)
246
+ await this.#client.closeSession(this.#session, true)
235
247
  this.#session = undefined
236
248
  this.#client = undefined
237
249
  this.LogInfo(`OPCUAIf: Disconnected\n`);
@@ -243,10 +255,10 @@ export class OPCUAIf extends BaseIf {
243
255
  */
244
256
  async Connect(url) {
245
257
  let self = this
246
- if (this.#client){
258
+ if (this.#client) {
247
259
  await this.Disconnect()
248
260
  }
249
-
261
+
250
262
  this.#client = OPCUAClient.create({
251
263
  applicationName: "NodeOPCUA-Client",
252
264
  endpointMustExist: true,
@@ -263,7 +275,7 @@ export class OPCUAIf extends BaseIf {
263
275
  defaultSecureTokenLifetime: 20000,
264
276
  tokenRenewalInterval: 1000
265
277
  });
266
-
278
+
267
279
 
268
280
  this.#client.on("backoff", (retry, delay) => {
269
281
  //if ((retry % 10) == 0)
@@ -321,12 +333,28 @@ export class OPCUAIf extends BaseIf {
321
333
  });
322
334
 
323
335
  this.LogInfo(`OPCUAIf: session created\n`);
324
- // this.LogInfo(`OPCUAIf: client\n`);
325
- // this.LogInfo(`OPCUAIf: subscription\n`);
336
+ // this.LogInfo(`OPCUAIf: client\n`);
337
+ // this.LogInfo(`OPCUAIf: subscription\n`);
326
338
  this.#connected = true
327
339
  this.#endpointurl = url
328
340
  }
329
341
 
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
+ }
357
+
330
358
  /**
331
359
  * Register a subscription for a nodeID.
332
360
  * This method will create a monitored item for the given nodeID and return the monitored item ID.
@@ -334,15 +362,16 @@ export class OPCUAIf extends BaseIf {
334
362
  * @param {*} nodeID
335
363
  * @returns
336
364
  */
337
- async Subscribe(nodeID) {
365
+ async Subscribe(unformattedNodeID, options = {},attribute) {
366
+ // Format the nodeID using the provided options
367
+ let nodeID = this.formatString(unformattedNodeID, options)
368
+
338
369
  // install monitored item
339
- const keys = Object.keys(this.monitoreditems)
340
- for (let i = 0; i < keys.length; i++) {
341
- const key = keys[i]
342
- const elem = this.monitoreditems[key]
343
- if (elem == nodeID) {
370
+ const monitoredNodeIdList = Object.values(this.monitoreditems)
371
+ for (let i = 0; i < monitoredNodeIdList.length; i++) {
372
+ if (monitoredNodeIdList[i] == nodeID) {
344
373
  // already registered => return itemid
345
- return key
374
+ return this.monitoreditems[i]
346
375
  }
347
376
  }
348
377
 
@@ -357,7 +386,7 @@ export class OPCUAIf extends BaseIf {
357
386
  };
358
387
 
359
388
  if (!this.#sub) {
360
- this.LogError(`OPCUAIf: not register monitored items $itemToMonitor\n`);
389
+ this.LogError(`OPCUAIf: no subscription - don't register monitored items $itemToMonitor\n`);
361
390
  return
362
391
  }
363
392
  const monitoredItem = await this.#sub.monitor(itemToMonitor, monitoringParameters, TimestampsToReturn.Both);
@@ -365,14 +394,20 @@ export class OPCUAIf extends BaseIf {
365
394
  var self = this
366
395
  monitoredItem.on("changed", function (dataValue) {
367
396
  var nodeId = self.monitoreditems[this.monitoredItemId]
368
- var buttonID = self.buttons[this.monitoredItemId]
397
+ var buttonInfo = self.buttons[this.monitoredItemId]
369
398
  // store the type of a nodeid in local dictionary
370
399
  self.types[nodeId] = dataValue.value.dataType
371
400
  // publish the value to subscribers:
372
- self.LogInfo(`OPCUAIf: monitored item changed: ${nodeId} => ${dataValue.value.value}\n`);
373
- 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)
374
403
  });
375
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
+
376
411
  return monitoredItem.monitoredItemId;
377
412
  }
378
413
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loupedeck-commander",
3
- "version": "1.3.1",
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": {
package/profile-1.yaml CHANGED
@@ -159,7 +159,8 @@ knobs:
159
159
  states:
160
160
  on:
161
161
  cmd: echo "{id} {state}"
162
- opcua: ns=1;s={simnbr}.Function1
162
+ opcua: ns=2;s=DRV.AAFHR.compOut.FeldTempGetriebeoel
163
+ blink: ns=2;s=DRV.AAFHR.compOut.Feststellbremse
163
164
  knobTR:
164
165
  states:
165
166
  on:
package/profile-2.yaml CHANGED
@@ -31,6 +31,8 @@ touch:
31
31
  cmd: echo "{id} {state}"
32
32
  "1":
33
33
  default: two
34
+ params:
35
+ nodeid: ns=2;s=DRV.AAFHR.compOut.FeldTempGetriebeoel
34
36
  states:
35
37
  off:
36
38
  color: "#000099"
@@ -185,7 +187,8 @@ knobs:
185
187
  states:
186
188
  on:
187
189
  cmd: echo "{id} {state}"
188
- opcua: ns=2;s=Is{simnbr}.Audio.in.VolumeAccustic
190
+ opcua: ns=2;s=DRV.AAFHR.compOut.FeldTempGetriebeoel
191
+ blink: ns=2;s=DRV.AAFHR.compOut.Feststellbremse
189
192
  group: ""
190
193
  knobTR:
191
194
  states: