loupedeck-commander 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,8 +72,26 @@ node index.mjs
72
72
 
73
73
  If you end up with errors related to canvas module - please install the dependencies as mentioned below, and run `npm install -s` again
74
74
 
75
+ ## Thanks
76
+
77
+ Big thanks go out to [foxxyz](https://github.com/foxxyz/loupedeck) and his team maintaining a great javascript loopdeck module
78
+
75
79
  ## Hints
76
80
 
81
+ ### Cannot connect to Loupedeck device
82
+
83
+ You may need to add your user to the `dialout` group to allow access to the LoupeDeck device, you could do su using the `usermod` as shown below
84
+
85
+ ```bash
86
+ # ls -la /dev/ttyACM0
87
+ crw-rw---- 1 root dialout 166, 0 Nov 30 12:59 /dev/ttyACM0
88
+
89
+ # add the current user to the dialout group:
90
+ # sudo usermod -a -G dialout $USER
91
+ ```
92
+
93
+ After modifying users group you need to logout and login again to activate this change
94
+
77
95
  ### CANVAS Module - additional effort needed
78
96
 
79
97
  The library is using [canvas](https://www.npmjs.com/package/canvas) to load images and render graphical content on the LoupeDeck devices.
@@ -24,6 +24,7 @@ class Profile {
24
24
  touch = {}
25
25
  knobs = {}
26
26
  buttons = {}
27
+ parameters = {}
27
28
  #file = ''
28
29
  #loaded = false
29
30
  #error = false
@@ -48,6 +49,8 @@ class Profile {
48
49
  this.touch = new TouchConfig(config.touch)
49
50
  // Load the Configurations for Button-Areas
50
51
  this.buttons = config.buttons
52
+ // Load Parameters
53
+ this.parameters = config.parameters
51
54
  }
52
55
  }
53
56
 
@@ -1,7 +1,6 @@
1
- import pkg from 'loupedeck'
1
+ import {discover, HAPTIC,LoupedeckDevice } from 'loupedeck'
2
2
  import { ApplicationConfig } from './ApplicationConfig.mjs'
3
- import { ButtonField } from './touchbuttons.mjs'
4
- const { discover, HAPTIC } = pkg
3
+ import { ButtonField, StopInterfaces, InitializeInterfaces,opcuainterface} from './touchbuttons.mjs'
5
4
 
6
5
  export class BaseLoupeDeckHandler {
7
6
  device = undefined
@@ -11,8 +10,9 @@ export class BaseLoupeDeckHandler {
11
10
  screens = {}
12
11
  buttons = {}
13
12
  screenUpdate = {}
13
+ stopping = false
14
14
 
15
- touchButtons = undefined
15
+ //touchButtons = undefined
16
16
 
17
17
  constructor (config) {
18
18
  console.log(`INIT with config ${config}`)
@@ -21,15 +21,31 @@ export class BaseLoupeDeckHandler {
21
21
 
22
22
  async start () {
23
23
  console.info('Start')
24
- while (!this.device) {
24
+ while (!this.device && !this.stopping) {
25
25
  try {
26
+ var list = await LoupedeckDevice.list()
27
+ if (list.length>0){
28
+ var element = list[0]
29
+ console.log("Connecting to device")
30
+ console.log("Path: ", element.path)
31
+ console.log("vendorId: ", element.vendorId, element.productId)
32
+ console.log("serialNumber: ", element.serialNumber)
33
+ }
34
+
26
35
  this.device = await discover()
36
+
37
+
38
+
27
39
  } catch (e) {
28
40
  console.error(`${e}. Reattempting in 3 seconds...`)
29
- await new Promise(res => setTimeout(res, 3000))
41
+ await new Promise(resolve => setTimeout(resolve, 3000))
42
+ }
43
+
44
+ if (this.stopping){
45
+ return
30
46
  }
31
47
  }
32
- console.info(`✅ Connected to ${this.device.type}`)
48
+ console.info(`✅ Discovered Device ${this.device.type}`)
33
49
 
34
50
  const self = this
35
51
 
@@ -40,13 +56,49 @@ export class BaseLoupeDeckHandler {
40
56
  this.device.on('touchstart', (event) => { self.onTouchStart(event) })
41
57
  this.device.on('touchmove', (event) => { self.onTouchMove(event) })
42
58
  this.device.on('touchend', (event) => { self.onTouchEnd(event) })
59
+ this.device.on('disconnect', (event) => { self.disconnectDevice(event) })
43
60
 
44
- // return await this.device.connect()
61
+ console.info(`✅ Registered callbacks`)
45
62
  }
46
63
 
47
- stop () {
64
+ async disconnectDevice (event) {
65
+ console.info("Device Disconnected",event)
66
+ this.device=undefined
67
+ }
68
+
69
+ async stop () {
48
70
  console.info('Stopping Handler')
49
- this.device.close()
71
+
72
+ try {
73
+ console.info(`Closing Device`)
74
+ this.stopping = true
75
+ if (this.device){
76
+ this.device.vibrate(HAPTIC.DESCEND_MED)
77
+ await new Promise(resolve => setTimeout(resolve, 500))
78
+ }
79
+ if (this.device){
80
+ this.device.setBrightness(0)
81
+ await new Promise(resolve => setTimeout(resolve, 500))
82
+ }
83
+ if (this.device){
84
+ this.device.reconnectInterval = 0
85
+ await this.device.close()
86
+ await new Promise(resolve => setTimeout(resolve, 3000))
87
+ console.error(`Device Closed`)
88
+ }
89
+
90
+ console.info(`Stopping interfaces`)
91
+
92
+ await StopInterfaces()
93
+ await new Promise(resolve => setTimeout(resolve, 1000))
94
+
95
+ this.device = null
96
+ } catch (e) {
97
+ console.error(`${e}. Catched error in 3 seconds...`)
98
+ }
99
+
100
+ process.exit()
101
+
50
102
  }
51
103
 
52
104
  loadConfig (fileName) {
@@ -57,34 +109,28 @@ export class BaseLoupeDeckHandler {
57
109
 
58
110
  async activateProfile (id) {
59
111
  // todo Profile-change implementation
112
+ var oldProfile = this.currentProfile
60
113
  if (this.appConfig.profiles.length >= id) {
61
114
  this.currentProfile = id
115
+ }else{
116
+ return
62
117
  }
63
- await this.buttons.draw(this.device)
64
- }
65
118
 
66
- getCurrentProfile () {
67
- return this.appConfig.profiles[this.currentProfile]
68
- }
69
-
70
- // Events:
71
- async onConnected (address) {
72
- console.info('Connected: ', address)
73
119
  const dLeft = this.device.displays.left
74
120
  const dRight = this.device.displays.right
75
121
  const dCenter = this.device.displays.center
76
122
  const dKnob = this.device.displays.knob
77
-
123
+
78
124
  const profile = this.getCurrentProfile()
79
- this.screens.center = new ButtonField('center', this.device.rows, this.device.columns, dCenter.width, dCenter.height, profile.touch.center)
80
- this.screens.left = new ButtonField('left', 1, 1, dLeft.width, dLeft.height, profile.touch.left)
81
- this.screens.right = new ButtonField('right', 1, 1, dRight.width, dRight.height, profile.touch.right)
125
+ this.screens.center = new ButtonField('center', this.device.rows, this.device.columns, dCenter.width, dCenter.height, profile.touch.center,profile)
126
+ this.screens.left = new ButtonField('left', 1, 1, dLeft.width, dLeft.height, profile.touch.left,profile)
127
+ this.screens.right = new ButtonField('right', 1, 1, dRight.width, dRight.height, profile.touch.right,profile)
82
128
  // knob Display is only available in the CT-version - not with live:
83
129
  if (dKnob) {
84
- this.screens.knob = new ButtonField('knob', 1, 1, dKnob.width, dKnob.height, profile.touch.knob)
130
+ this.screens.knob = new ButtonField('knob', 1, 1, dKnob.width, dKnob.height, profile.touch.knob,profile)
85
131
  }
86
132
 
87
- this.buttons = new ButtonField('buttons', 1, 1, 0, 0, profile.buttons)
133
+ this.buttons = new ButtonField('buttons', 1, 1, 0, 0, profile.buttons,profile)
88
134
 
89
135
  await this.screens.center.load()
90
136
  await this.screens.right.load()
@@ -98,8 +144,29 @@ export class BaseLoupeDeckHandler {
98
144
  if (dKnob) {
99
145
  await this.screens.knob.draw(this.device)
100
146
  }
101
- await this.activateProfile(0)
147
+
148
+ this.buttons.setState(this.currentProfile+1,1)
149
+ this.buttons.setState(oldProfile+1,1)
150
+
102
151
  await this.buttons.draw(this.device)
152
+ }
153
+
154
+ getCurrentProfile () {
155
+ return this.appConfig.profiles[this.currentProfile]
156
+ }
157
+
158
+ // Events:
159
+ async onConnected (address) {
160
+ console.info(`✅ Connected to ${this.device.type}, ${address}`)
161
+
162
+ await this.activateProfile(0)
163
+
164
+ var profile = this.getCurrentProfile()
165
+ await InitializeInterfaces(profile,this.buttons.setState)
166
+
167
+ const self = this
168
+
169
+ opcuainterface.myEmitter.on("monitored item changed",(buttonID,nodeid,val) => { self.buttonStateChanged(buttonID,nodeid,val) })
103
170
 
104
171
  this.device.setBrightness(1)
105
172
  this.device.vibrate(HAPTIC.ASCEND_MED)
@@ -129,6 +196,10 @@ export class BaseLoupeDeckHandler {
129
196
  const id = event.id
130
197
  ok = await this.buttons.released(id)
131
198
  await this.buttons.draw(this.device)
199
+
200
+ /*if (this.currentProfile != id){
201
+ this.activateProfile(id-1)
202
+ }*/
132
203
  return ok
133
204
  }
134
205
 
@@ -186,4 +257,16 @@ export class BaseLoupeDeckHandler {
186
257
  await this.updateScreens()
187
258
  return ok
188
259
  }
260
+
261
+ async buttonStateChanged(buttonID,nodeid,val) {
262
+ let ok = false
263
+ this.screenUpdate["center"] = true
264
+
265
+ ok = await this.screens.center.changed(buttonID,nodeid,val)
266
+
267
+ await this.updateScreens()
268
+ return ok
269
+ }
270
+
271
+
189
272
  }
@@ -1,8 +1,37 @@
1
1
  import { loadImage } from 'canvas'
2
- import { sh } from './cmd-executer.mjs'
2
+
3
+ import * as shellif from '../interfaces/shellif.mjs'
4
+ import * as httpif from '../interfaces/httpif.mjs'
5
+ import * as opcuaif from '../interfaces/opcuaif.mjs'
3
6
  import format from 'string-template'
4
7
  import { calcDelta } from './utils.mjs'
5
8
 
9
+ export var opcuainterface = undefined
10
+ var httpinterface = undefined
11
+ var shellinterface = undefined
12
+
13
+ export async function InitializeInterfaces(appConfig,callbackFunction){
14
+ if (opcuainterface === undefined ){
15
+ opcuainterface = new opcuaif.OPCUAIf()
16
+ opcuainterface.init(appConfig.parameters,appConfig,callbackFunction)
17
+ }
18
+ if (httpinterface === undefined)
19
+ httpinterface = new httpif.HTTPif()
20
+ if (shellinterface === undefined)
21
+ shellinterface = new shellif.SHELLif()
22
+
23
+ }
24
+
25
+ export async function StopInterfaces(){
26
+ if (opcuainterface !== undefined )
27
+ await opcuainterface.stop()
28
+ if (httpinterface !== undefined)
29
+ await httpinterface.stop()
30
+ if (shellinterface !== undefined)
31
+ await shellinterface.stop()
32
+
33
+ }
34
+
6
35
  export const ButtonIndex = {
7
36
  BUTN_0: 0,
8
37
  BUTN_1: 1,
@@ -31,7 +60,9 @@ export class ButtonField {
31
60
  #keys = []
32
61
  #type
33
62
  #name
34
- constructor (name, rows, columns, width, height, data) {
63
+ #config
64
+
65
+ constructor (name, rows, columns, width, height, data, config) {
35
66
  console.info(`ButtonField ${name.padEnd(10, ' ')} Buttons: ${rows} x ${columns} , Pixels ${width} x ${height}`)
36
67
  this.#name = name
37
68
  this.width = width
@@ -45,13 +76,18 @@ export class ButtonField {
45
76
  const keys = Object.keys(data)
46
77
  for (let i = 0; i < keys.length; i++) {
47
78
  const key = keys[i]
48
- const tb = new Button(`${this.#type}-${key}`, width / columns, height / rows, data[key])
79
+ const tb = new Button(`${this.#type}-${key}`, width / columns, height / rows, data[key],key,config.parameters)
49
80
  this.#buttons[key] = tb
50
81
  }
51
82
 
52
83
  this.#keys = keys
84
+ this.#config = config
53
85
  }
54
86
 
87
+ //setProfileConfig (config) {
88
+ // this.#config = config
89
+ // }
90
+
55
91
  async draw (device) {
56
92
  if (!this.#screen) {
57
93
  // physical buttons:
@@ -75,6 +111,10 @@ export class ButtonField {
75
111
  }
76
112
  }
77
113
 
114
+ setState (id, val) {
115
+ this.#buttons[id].setState(val)
116
+ }
117
+
78
118
  setIntState (id, val) {
79
119
  this.#buttons[id].setIntState(val)
80
120
  }
@@ -83,10 +123,10 @@ export class ButtonField {
83
123
  for (let i = 0; i < this.#keys.length; i++) {
84
124
  const key = this.#keys[i]
85
125
  if (isNaN(key)) {
86
- await this.#buttons[key].load()
126
+ await this.#buttons[key].load(this.#config)
87
127
  } else {
88
128
  const iVal = parseInt(key, 10)
89
- await this.#buttons[iVal].load()
129
+ await this.#buttons[iVal].load(this.#config)
90
130
  }
91
131
  }
92
132
  }
@@ -119,6 +159,13 @@ export class ButtonField {
119
159
  return result
120
160
  }
121
161
 
162
+ async changed(buttonID,nodeid,val){
163
+ for (let i = 0; i < this.#keys.length; i++) {
164
+ let bID = this.#keys[i]
165
+ const result = await this.#buttons[bID].changed(i,nodeid,val)
166
+ }
167
+ }
168
+
122
169
  async rotated (id, delta) {
123
170
  this.checkAndCreateButton(id)
124
171
  const result = await this.#buttons[id].rotated(delta)
@@ -142,6 +189,7 @@ export class ButtonField {
142
189
  }
143
190
 
144
191
  export class Button {
192
+ #config
145
193
  width = 0
146
194
  height = 0
147
195
 
@@ -151,6 +199,7 @@ export class Button {
151
199
  #max = 100
152
200
  #value = 50
153
201
  #name = undefined
202
+ #nodeid = ""
154
203
 
155
204
  #index = 0
156
205
  #keys
@@ -176,9 +225,11 @@ export class Button {
176
225
  timeHold
177
226
  // Minimum ammount of time in ms to press a button:
178
227
  minPressed = 25
228
+ key = -1
179
229
 
180
- constructor (id, width, height, data) {
230
+ constructor (id, width, height, data,key,params) {
181
231
  this.id = id
232
+ this.key = key
182
233
  this.width = width
183
234
  this.height = height
184
235
  this.#index = 0
@@ -204,6 +255,10 @@ export class Button {
204
255
  this.#states = {}
205
256
  this.#keys = []
206
257
  }
258
+ if (data.nodeid){
259
+ this.#nodeid = format(data.nodeid, params)
260
+ }
261
+
207
262
  }
208
263
 
209
264
  setState (index = 0) {
@@ -219,13 +274,16 @@ export class Button {
219
274
  const b = parseInt(elem.color.slice(5, 7), 16)
220
275
 
221
276
  try {
277
+ var idx = parseInt(id, 10);
278
+
222
279
  const val = {
223
- id,
280
+ id:idx,
224
281
  color: `rgba(${r}, ${g}, ${b})`
225
282
  }
283
+ //console.log(' Set Button Color',val.id, val.color)
226
284
  device.setButtonColor(val)
227
285
  } catch (error) {
228
- // console.log(' Error', error)
286
+ console.error(' Error', error)
229
287
  }
230
288
  }
231
289
 
@@ -250,7 +308,8 @@ export class Button {
250
308
  // ctx.fillText(this.text, x + 10, y - 10)
251
309
  }
252
310
 
253
- async load () {
311
+ async load (globalConfig) {
312
+ this.#config = globalConfig
254
313
  for (let i = 0; i < this.#keys.length; i++) {
255
314
  const key = this.#keys[i]
256
315
  const elem = this.#states[key]
@@ -263,6 +322,10 @@ export class Button {
263
322
  return false
264
323
  }
265
324
  }
325
+ //const uastate = elem.state
326
+ //if (uastate){
327
+ // await opcuainterface.Subscribe(uastate)
328
+ //}
266
329
  }
267
330
  }
268
331
 
@@ -283,7 +346,7 @@ export class Button {
283
346
  this.timeStampPressed = Date.now()
284
347
 
285
348
  this.#index++
286
- this.#index %= this.#keys.length
349
+ this.updateState(this.#index)
287
350
  return true
288
351
  }
289
352
 
@@ -311,7 +374,19 @@ export class Button {
311
374
 
312
375
  break
313
376
  }
377
+
378
+ this.updateState(this.#index)
379
+
380
+ return true // this.runCommand()
381
+ }
382
+
383
+ updateState(index){
384
+ this.#index = index
385
+ // Update the State according to the correctly pressed state
386
+ if (this.#index < 0) { this.#index = this.#keys.length - 1 }
387
+ this.#index %= this.#keys.length
314
388
  this.runCommand()
389
+ //console.log("TODO: expect newState", newState)
315
390
  return true // this.runCommand()
316
391
  }
317
392
 
@@ -322,6 +397,40 @@ export class Button {
322
397
  return this.runCommand()
323
398
  }
324
399
 
400
+ async changed(buttonID,nodeid,val){
401
+ // Only handle updates within the same group identified by nodeid
402
+ if (nodeid !== this.#nodeid){
403
+ return
404
+ }
405
+
406
+ this.#index = 0;
407
+ for (let i = 0; i < this.#keys.length; i++) {
408
+ let key = this.#keys[i]
409
+ // check if the state-name is same as the value we get from outside:
410
+ if (val == key){
411
+ this.#index = i;
412
+ break;
413
+ }
414
+
415
+ // check if the nodeid is the same and the value is one of the states
416
+ let state = this.#states[key]
417
+ if (!state.value)
418
+ continue
419
+
420
+ const params = {
421
+ id: buttonID,
422
+ key: buttonID,
423
+ ...state
424
+ }
425
+ let val1 = format(state.value,params)
426
+ if (val1 === val){
427
+ this.#index = i;
428
+ break;
429
+ }
430
+ break;
431
+ }
432
+ }
433
+
325
434
  async touchmove (x, y) {
326
435
  // if (!this.getCurrentElement()) { return false }
327
436
 
@@ -347,27 +456,48 @@ export class Button {
347
456
  return false
348
457
  }
349
458
 
350
- runCommand () {
459
+ async runCommand () {
351
460
  const elem = this.getCurrentElement()
352
- if (!elem || !elem.cmd) {
461
+ if (!elem || (!elem.cmd && !elem.http && !elem.opcua)) {
353
462
  return
354
463
  }
355
- // Call an action
464
+ // Call an action - include dynamic parameters
465
+ // and also all attributes of elem + global config
356
466
  const params = {
357
467
  id: this.id,
468
+ key: this.key,
358
469
  state: this.#keys[this.#index],
359
470
  min: this.#min,
360
471
  max: this.#max,
361
472
  value: this.#value,
362
- text: this.getCurrentText()
473
+ text: this.getCurrentText(),
474
+ ...this.#config.parameters,
475
+ ...elem
363
476
  }
364
- /* const keys = Object.keys(this.#data.config)
365
- keys.forEach(key => {
366
- params[key] = this.#data.config[key]
367
- }) */
368
477
 
369
- const cmdFormatted = format(elem.cmd, params)
478
+ let res = ''
479
+ if ('cmd' in elem) {
480
+ if (shellinterface){
481
+ res = await shellinterface.call(elem.cmd, params)
482
+ }else{
483
+ console.warn("shellinterface not started")
484
+ }
485
+ }
486
+ if ('http' in elem) {
487
+ if (httpinterface){
488
+ res = await httpinterface.call(elem.http, params)
489
+ }else{
490
+ console.warn("httpinterface not started")
491
+ }
492
+ }
493
+ if ('opcua' in elem) {
494
+ if (opcuainterface){
495
+ res = await opcuainterface.call(elem.opcua, params)
496
+ }else{
497
+ console.warn("opcuainterface not started")
498
+ }
499
+ }
370
500
 
371
- return sh(cmdFormatted)
501
+ return res
372
502
  }
373
503
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "application": "TEST",
3
+ "profiles": [
4
+ {
5
+ "name": "profile1",
6
+ "file": "profile-2.json"
7
+ }
8
+ ]
9
+ }
@@ -0,0 +1,9 @@
1
+ import globals from "globals";
2
+ import pluginJs from "@eslint/js";
3
+
4
+
5
+ /** @type {import('eslint').Linter.Config[]} */
6
+ export default [
7
+ {languageOptions: { globals: globals.browser }},
8
+ pluginJs.configs.recommended,
9
+ ];
@@ -1,9 +1,8 @@
1
- import pkg from 'loupedeck'
1
+ import { HAPTIC } from 'loupedeck'
2
2
  import { BaseLoupeDeckHandler } from '../common/BaseLoupeDeckHandler.mjs'
3
- const { HAPTIC } = pkg
4
3
 
5
4
  /**
6
- * Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
5
+ * Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
7
6
  */
8
7
  export class ExampleDeviceHandler extends BaseLoupeDeckHandler {
9
8
  /**
@@ -2,12 +2,20 @@ import { ExampleDeviceHandler } from './ExampleDeviceHandler.mjs'
2
2
 
3
3
  const handler = new ExampleDeviceHandler('config.json')
4
4
 
5
- const stopHandler = () => {
6
- console.log('Receiving SIGINT => Stopping processes.')
7
- handler.stop()
5
+
6
+ /**
7
+ * Stop the handlers when a signal like SIGINT or SIGTERM arrive
8
+ * @param {*} signal
9
+ */
10
+ const stopHandler = async(signal) => {
11
+ console.log(`Receiving ${signal} => Stopping processes.`)
12
+ await handler.stop()
8
13
  }
9
14
 
10
- // Initiating a process
11
- process.on('SIGINT', stopHandler)
15
+ // Initiating the signal handlers:
16
+ // see https://www.tutorialspoint.com/unix/unix-signals-traps.htm
17
+ process.on('SIGINT', async (signal) => { stopHandler(signal) })
18
+ process.on('SIGTERM', async (signal) => { stopHandler(signal) })
12
19
 
20
+ // Initiating a process
13
21
  await handler.start()
@@ -0,0 +1,57 @@
1
+ import format from 'string-template'
2
+
3
+ export class BaseIf {
4
+ formattedCommand
5
+ cmd
6
+ options
7
+ call (cmd, options = {}) {
8
+ var res = this.Check(options)
9
+ if (res < 0){
10
+ LogError("Missing essential options in dictionary => Quitting\n",res,options)
11
+ return false
12
+ }
13
+
14
+ this.cmd = cmd
15
+ this.options = options
16
+ this.formattedCommand = this.formatString(cmd, options)
17
+ return this.formattedCommand
18
+ }
19
+
20
+ async stop (){
21
+
22
+ }
23
+
24
+ formatString (cmd, options = {}) {
25
+ return format(cmd, options)
26
+ }
27
+
28
+ Check(options) {
29
+ if (!"id" in options)
30
+ return -1
31
+ if (!"key" in options)
32
+ return -2
33
+ if (!"state" in options)
34
+ return -3
35
+ if (!"min" in options)
36
+ return -4
37
+ if (!"max" in options)
38
+ return -5
39
+ if (!"color" in options)
40
+ return -6
41
+ if (!"image" in options)
42
+ return -7
43
+ return 0
44
+ }
45
+
46
+ LogError(...args){
47
+ console.error(args)
48
+ }
49
+
50
+ LogInfo(...args){
51
+ if (this.options && this.options.verbose)
52
+ console.log(args)
53
+ }
54
+
55
+ }
56
+
57
+