loupedeck-commander 1.2.6 → 1.2.8

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,4 +1,4 @@
1
- import { readJSONFile, writeJSONFile } from './utils.mjs'
1
+ import { readJSONFile, writeJSONFile,syncParams } from './utils.mjs'
2
2
 
3
3
  export class ApplicationConfig {
4
4
  application = 'undefined'
@@ -19,7 +19,7 @@ export class ApplicationConfig {
19
19
  else{
20
20
  this.application = config.application
21
21
  for (let i = 0; i < config.profiles.length; i++) {
22
- const profile = new Profile(config.profiles[i])
22
+ const profile = new Profile(config.profiles[i],config.profiles.length,i)
23
23
  if (profile.loaded){
24
24
  this.profiles.push(profile)
25
25
  }else{
@@ -42,24 +42,30 @@ class Profile {
42
42
  knobs = {}
43
43
  buttons = {}
44
44
  parameters = {}
45
+ default = {}
45
46
  #file = ''
46
47
  loaded = false
47
48
  #error = true
49
+ #profileCount = 0
50
+ #index = 0
48
51
 
49
- constructor (data) {
52
+ constructor (data,profileCount,index) {
50
53
  this.name = data.name
51
54
  this.#file = data.file
55
+ this.#profileCount = profileCount
56
+ this.#index = index
52
57
  this.touch = new TouchConfig({})
53
58
  this.buttons = new ButtonConfig().buttons
54
59
  this.knobs = new KnobsConfig().knobs
55
60
  this.parameters = new ParametersConfig().parameters
61
+ this.default = new DefaultConfig().default
56
62
 
57
63
  this.loadFromFile(this.#file)
58
64
  if (this.#error) { this.saveToFile(`profile-${this.name}-sav.json`) }
59
65
  }
60
66
 
61
67
  loadFromFile (fileName) {
62
- console.info(`ProfileConfig: Loading Profile File ${fileName}`)
68
+ console.info(`ProfileConfig: Loading Profile File ${fileName}, Index ${this.#index}`)
63
69
  const config = readJSONFile(fileName)
64
70
  if (config === undefined) {
65
71
  console.warn(`ProfileConfig: Cannot parse/load Profile File ${fileName}`)
@@ -70,24 +76,25 @@ class Profile {
70
76
  }else if (!config.touch){
71
77
  console.warn(`ProfileConfig: File ${fileName} is missing a touch attribute`)
72
78
  return false
73
- }else if (!config.buttons){
74
- console.warn(`ProfileConfig: File ${fileName} is missing a buttons attribute`)
75
- return false
76
79
  }else if (!config.parameters){
77
80
  console.warn(`ProfileConfig: File ${fileName} is missing a parameters attribute`)
78
81
  return false
82
+ }else if (!config.buttons){
83
+ console.info(`ProfileConfig: File ${fileName} is missing a buttons attribute - will be used to switch profiles by default`)
84
+ // return false
79
85
  }
80
86
  this.profile = config.profile
81
87
  this.description = config.description
88
+ // Load Parameters.parameters = config.parameters
89
+ this.parameters = new ParametersConfig(config.parameters).parameters
90
+ this.default = new DefaultConfig(config.default).default
82
91
 
83
92
  // Load the Configurations for Touch-Displays
84
- this.touch = new TouchConfig(config.touch)
93
+ this.touch = new TouchConfig(config.touch,this.default)
85
94
  // Load the Configurations for Button-Areas
86
95
 
87
- this.buttons = new ButtonConfig(config.buttons).buttons
96
+ this.buttons = new ButtonConfig(config.buttons,this.#profileCount,this.#index).buttons
88
97
  this.knobs = new KnobsConfig(config.knobs).knobs
89
- // Load Parameters.parameters = config.parameters
90
- this.parameters = new ParametersConfig(config.parameters).parameters
91
98
 
92
99
  this.#error = false
93
100
  this.loaded = true
@@ -128,11 +135,11 @@ class TouchConfig {
128
135
  } // RIGHT Display Config - Available in CT & LIVE
129
136
  //knob = {} // KNOB Display Config - Available only in CT
130
137
 
131
- constructor (data) {
132
- this.loadFromJSON(data)
138
+ constructor (data,defaultState) {
139
+ this.loadFromJSON(data,defaultState)
133
140
  }
134
141
 
135
- loadFromJSON (data) {
142
+ loadFromJSON (data,defaultState) {
136
143
  if (!data)
137
144
  return
138
145
  if (!data.center)
@@ -146,7 +153,21 @@ class TouchConfig {
146
153
  this.center = data.center
147
154
  this.left = data.left
148
155
  this.right = data.right
149
- //this.knob = data.knob
156
+
157
+ /**
158
+ * Sychronize the states of the buttons with the default state - add missing options from default state
159
+ */
160
+ var buttons = Object.keys(this.center)
161
+ for (var buttonID=0;buttonID<buttons.length;buttonID++){
162
+ if (!this.center[buttonID])
163
+ continue
164
+ var states = Object.keys(this.center[buttonID].states)
165
+ for (var stateID=0;stateID<states.length;stateID++){
166
+ var stateKey = states[stateID]
167
+ var stateOld = this.center[buttonID].states[stateKey]
168
+ this.center[buttonID].states[stateKey] = syncParams(stateOld,defaultState)
169
+ }
170
+ }
150
171
  }
151
172
  }
152
173
 
@@ -154,19 +175,32 @@ class TouchConfig {
154
175
  * Class that handles the different configs for the touch displays of the device covering Loupedeck Live (without knob) and also CT (with knob)
155
176
  */
156
177
  class ButtonConfig {
157
- buttons = {
158
- "0" : { "states" : { "off" : { "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "color": "#550000", "cmd": "echo \"{id} {state}\"" } }, group : "" },
159
- "1" : { "states" : { "off" : { "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "color": "#ff0000", "cmd": "echo \"{id} {state}\"" } }, group : "" },
160
- "2" : { "states" : { "off" : { "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "color": "#005500", "cmd": "echo \"{id} {state}\"" } }, group : "" },
161
- "3" : { "states" : { "off" : { "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "color": "#00ff00", "cmd": "echo \"{id} {state}\"" } }, group : "" },
162
- "4" : { "states" : { "off" : { "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "color": "#000055", "cmd": "echo \"{id} {state}\"" } }, group : "" },
163
- "5" : { "states" : { "off" : { "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "color": "#0000ff", "cmd": "echo \"{id} {state}\"" } }, group : "" },
164
- "6" : { "states" : { "off" : { "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "color": "#555500", "cmd": "echo \"{id} {state}\"" } }, group : "" },
165
- "7" : { "states" : { "off" : { "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "color": "#ffff00", "cmd": "echo \"{id} {state}\"" } }, group : "" },
178
+ buttons = {}
179
+ defaultButtons = {
180
+ "0" : {"states" : { "off" : { "filter": "pressed", "color": "#000000", "cmd": "echo \"{id} {state}\"" }, "on" : { "filter": "pressed","color": "#ff0000", "cmd": "echo \"{id} {state}\"" } }, group : "onoff" },
181
+ "1" : {"states" : { "off" : { "filter": "pressed", "color": "#000055", "cmd": "echo \"{id} {state}\"" }, "on" : { "filter": "pressed", "profile": 0, "color": "#00ff00", "cmd": "echo \"{id} {state}\"" } }, group : "profiles" },
182
+ "2" : {"states" : { "off" : { "filter": "pressed", "color": "#000055", "cmd": "echo \"{id} {state}\"" }, "on" : { "filter": "pressed", "profile": 1, "color": "#00ff00", "cmd": "echo \"{id} {state}\"" } }, group : "profiles" },
183
+ "3" : {"states" : { "off" : { "filter": "pressed", "color": "#000055", "cmd": "echo \"{id} {state}\"" }, "on" : { "filter": "pressed", "profile": 2, "color": "#00ff00", "cmd": "echo \"{id} {state}\"" } }, group : "profiles" },
184
+ "4" : {"states" : { "off" : { "filter": "pressed", "color": "#000055", "cmd": "echo \"{id} {state}\"" }, "on" : { "filter": "pressed", "profile": 3, "color": "#00ff00", "cmd": "echo \"{id} {state}\"" } }, group : "profiles" },
185
+ "5" : {"states" : { "off" : { "filter": "pressed", "color": "#000055", "cmd": "echo \"{id} {state}\"" }, "on" : { "filter": "pressed", "profile": 4, "color": "#00ff00", "cmd": "echo \"{id} {state}\"" } }, group : "profiles" },
186
+ "6" : {"states" : { "off" : { "filter": "pressed", "color": "#000055", "cmd": "echo \"{id} {state}\"" }, "on" : { "filter": "pressed", "profile": 5, "color": "#00ff00", "cmd": "echo \"{id} {state}\"" } }, group : "profiles" },
187
+ "7" : {"states" : { "off" : { "filter": "pressed", "color": "#000055", "cmd": "echo \"{id} {state}\"" }, "on" : { "filter": "pressed", "profile": 6, "color": "#00ff00", "cmd": "echo \"{id} {state}\"" } }, group : "profiles" },
166
188
  }
167
189
 
168
- constructor (data) {
169
- this.loadFromJSON(data)
190
+ constructor (data,profileCount,index) {
191
+ if (data)
192
+ return this.loadFromJSON(data)
193
+ // Default to profile switching buttons
194
+ var keys = Object.keys(this.defaultButtons)
195
+ for (let i = 0; i <= profileCount; i++) {
196
+ var key = keys[i]
197
+ this.buttons[key] = this.defaultButtons[key]
198
+ if (i==(index+1)){
199
+ this.buttons[key].default = "on"
200
+ }
201
+
202
+ }
203
+
170
204
  }
171
205
 
172
206
  loadFromJSON (data) {
@@ -204,8 +238,10 @@ class ParametersConfig {
204
238
  parameters = {
205
239
  "hostname": "127.0.0.1",
206
240
  "endpointurl": "opc.tcp://{hostname}:4840",
207
- "nodeid" : "ns=0;s=nodeID"
208
- } // KNOB Config - Available only in CT
241
+ "nodeid" : "ns=0;s=nodeID",
242
+ "min" : 0,
243
+ "max" : 100
244
+ }
209
245
 
210
246
  constructor (data) {
211
247
  this.loadFromJSON(data)
@@ -217,3 +253,20 @@ class ParametersConfig {
217
253
  this.parameters = data
218
254
  }
219
255
  }
256
+
257
+ class DefaultConfig {
258
+ default = {
259
+ "filter": "released",
260
+ "color": "#111111"
261
+ }
262
+
263
+ constructor (data) {
264
+ this.loadFromJSON(data)
265
+ }
266
+
267
+ loadFromJSON (data) {
268
+ if (!data)
269
+ return
270
+ this.default = data
271
+ }
272
+ }
@@ -134,6 +134,13 @@ export class BaseLoupeDeckHandler {
134
134
  const dLeft = this.device.displays.left
135
135
  const dRight = this.device.displays.right
136
136
  const dCenter = this.device.displays.center
137
+
138
+ this.screens.center = undefined
139
+ this.screens.left = undefined
140
+ this.screens.right = undefined
141
+ this.buttons = undefined
142
+ this.knobs = undefined
143
+
137
144
 
138
145
  this.screens.center = new ButtonField('center', this.device.rows, this.device.columns, dCenter.width, dCenter.height, profile.touch.center,profile)
139
146
  this.screens.left = new ButtonField('left', 1, 1, dLeft.width, dLeft.height, profile.touch.left,profile)
@@ -159,6 +166,8 @@ export class BaseLoupeDeckHandler {
159
166
  await this.updateScreens()
160
167
 
161
168
  await this.buttons.draw(this.device)
169
+ // Initialize the Interfaces
170
+ await InitializeInterfaces(profile)
162
171
 
163
172
  }
164
173
 
@@ -179,8 +188,9 @@ export class BaseLoupeDeckHandler {
179
188
 
180
189
  await this.activateProfile(0)
181
190
 
182
- var profile = this.getCurrentProfile()
183
- await InitializeInterfaces(profile,this.buttons.setState)
191
+ // move into activate profile function to call init with every profile change
192
+ // var profile = this.getCurrentProfile()
193
+ //await InitializeInterfaces(profile)
184
194
 
185
195
  const self = this
186
196
 
@@ -222,7 +232,7 @@ export class BaseLoupeDeckHandler {
222
232
  const id = event.id
223
233
  if (Number.isInteger(id)){
224
234
  ok = await this.buttons.pressed(id)
225
- await this.buttons.draw(this.device)
235
+ //await this.buttons.draw(this.device)
226
236
  }else{
227
237
  ok = await this.knobs.pressed(id)
228
238
  }
@@ -237,7 +247,7 @@ export class BaseLoupeDeckHandler {
237
247
  const id = event.id
238
248
  if (Number.isInteger(id) && this.buttons){
239
249
  ok = await this.buttons.released(id)
240
- await this.buttons.draw(this.device)
250
+ //await this.buttons.draw(this.device)
241
251
  }else{
242
252
  if(this.knobs)
243
253
  ok = await this.knobs.released(id)
@@ -326,6 +336,7 @@ export class BaseLoupeDeckHandler {
326
336
  this.screenUpdate["right"] = true
327
337
 
328
338
  ok = await this.screens.center.changed(buttonID,nodeid,val)
339
+ ok = await this.knobs.changed(buttonID,nodeid,val)
329
340
 
330
341
  await this.updateScreens()
331
342
  return ok
@@ -5,7 +5,7 @@ import * as shellif from '../interfaces/shellif.mjs'
5
5
  import * as httpif from '../interfaces/httpif.mjs'
6
6
  import * as opcuaif from '../interfaces/opcuaif.mjs'
7
7
  import format from 'string-template'
8
- import { calcDelta } from './utils.mjs'
8
+ import { calcDelta, invertColor } from './utils.mjs'
9
9
  import { EventEmitter } from 'node:events'
10
10
 
11
11
 
@@ -13,17 +13,18 @@ export var opcuainterface = undefined
13
13
  var httpinterface = undefined
14
14
  var shellinterface = undefined
15
15
  export var profileEmitter = undefined
16
- export async function InitializeInterfaces(appConfig,callbackFunction){
16
+ export async function InitializeInterfaces(appConfig){
17
17
  if (opcuainterface === undefined ){
18
18
  opcuainterface = new opcuaif.OPCUAIf()
19
- opcuainterface.init(appConfig.parameters,appConfig,callbackFunction)
20
19
  }
20
+ // the opcua interface needs the profile to register nodes with subscriptions:
21
+ opcuainterface.init(appConfig.parameters,appConfig)
21
22
  if (httpinterface === undefined)
22
23
  httpinterface = new httpif.HTTPif()
23
24
  if (shellinterface === undefined)
24
25
  shellinterface = new shellif.SHELLif()
25
-
26
- profileEmitter = new EventEmitter()
26
+ if (profileEmitter === undefined)
27
+ profileEmitter = new EventEmitter()
27
28
  }
28
29
 
29
30
  export async function StopInterfaces(){
@@ -64,9 +65,9 @@ export class ButtonField {
64
65
  #keys = []
65
66
  #type
66
67
  #name
67
- #config
68
+ #profile
68
69
 
69
- constructor (name, rows, columns, width, height, data, config) {
70
+ constructor (name, rows, columns, width, height, data, profile) {
70
71
  console.info(`ButtonField ${name.padEnd(10, ' ')} Buttons: ${rows} x ${columns} , Pixels ${width} x ${height}`)
71
72
  this.#name = name
72
73
  this.width = width
@@ -75,17 +76,17 @@ export class ButtonField {
75
76
  this.#columns = columns
76
77
  this.#screen = this.width > 0 && this.height > 0
77
78
  this.#type = 'button'
79
+ this.#profile = profile
78
80
  if (this.#screen) { this.#type = 'touch' }
79
81
 
80
82
  const keys = Object.keys(data)
81
83
  for (let i = 0; i < keys.length; i++) {
82
84
  const key = keys[i]
83
- const tb = new Button(`${this.#type}-${key}`, width / columns, height / rows, data[key],key,config.parameters)
85
+ const tb = new Button(`${this.#type}-${key}`, width / columns, height / rows, data[key],key,this.#profile)
84
86
  this.#buttons[key] = tb
85
87
  }
86
88
 
87
89
  this.#keys = keys
88
- this.#config = config
89
90
  }
90
91
 
91
92
 
@@ -124,10 +125,10 @@ export class ButtonField {
124
125
  for (let i = 0; i < this.#keys.length; i++) {
125
126
  const key = this.#keys[i]
126
127
  if (isNaN(key)) {
127
- await this.#buttons[key].load(this.#config)
128
+ await this.#buttons[key].load(this.#profile)
128
129
  } else {
129
130
  const iVal = parseInt(key, 10)
130
- await this.#buttons[iVal].load(this.#config)
131
+ await this.#buttons[iVal].load(this.#profile)
131
132
  }
132
133
  }
133
134
  }
@@ -190,7 +191,9 @@ export class ButtonField {
190
191
  }
191
192
 
192
193
  export class Button {
193
- #config
194
+ #profile
195
+ #params
196
+ #data
194
197
  width = 0
195
198
  height = 0
196
199
 
@@ -229,7 +232,7 @@ export class Button {
229
232
  minPressed = 25
230
233
  key = -1
231
234
 
232
- constructor (id, width, height, data,key,params) {
235
+ constructor (id, width, height, data,key,profile) {
233
236
  this.id = id
234
237
  this.key = key
235
238
  this.width = width
@@ -237,8 +240,10 @@ export class Button {
237
240
  this.#index = 0
238
241
 
239
242
  if (data && data.states) {
240
- this.group = data.group
243
+ this.#data = data
241
244
 
245
+ this.group = data.group
246
+
242
247
  this.#states = data.states
243
248
  this.#keys = Object.keys(this.#states)
244
249
  if (data.type) {
@@ -253,14 +258,28 @@ export class Button {
253
258
  this.text = data.text
254
259
  }
255
260
  }
261
+ if (profile === undefined){
262
+ this.#profile = {}
263
+ this.#params = {}
264
+ }else{
265
+ this.#profile = profile
266
+ this.#params = profile.parameters
267
+ if (profile.parameters.min !== undefined)
268
+ this.#min = profile.parameters.min
269
+ if (profile.parameters.max !== undefined)
270
+ this.#max = profile.parameters.max
271
+ }
256
272
  if (this.#states === undefined) {
257
273
  this.#states = {}
258
274
  this.#keys = []
259
275
  }
260
276
  if (data.nodeid){
261
- this.#nodeid = format(data.nodeid, params)
277
+ this.#nodeid = format(data.nodeid, this.#params)
262
278
  }
263
-
279
+ if (data.default) {
280
+ this.#index = this.#keys.indexOf(data.default)
281
+ }
282
+
264
283
  }
265
284
 
266
285
  setState (index = 0) {
@@ -282,7 +301,7 @@ export class Button {
282
301
  id:idx,
283
302
  color: `rgba(${r}, ${g}, ${b})`
284
303
  }
285
- // console.log(' Set Button Color',val.id,elem.color, val.color)
304
+ // console.log(' Set Button Color',id, val.id,elem.color, val.color)
286
305
  device.setButtonColor(val)
287
306
  } catch (error) {
288
307
  console.error(' Error', error)
@@ -305,8 +324,10 @@ export class Button {
305
324
  }
306
325
  }
307
326
  if (this.text){
308
- const lastElem = this.getLastElement()
309
- ctx.fillStyle = lastElem.color
327
+ //const lastElem = this.getLastElement()
328
+ // Only change the text color, if it differnce from the currently set color
329
+
330
+ ctx.fillStyle = invertColor(elem.color)
310
331
  ctx.font = '20px Verdana'
311
332
  ctx.textBaseline = 'top';
312
333
  ctx.textAlign = 'left';
@@ -315,7 +336,7 @@ export class Button {
315
336
  }
316
337
 
317
338
  async load (globalConfig) {
318
- this.#config = globalConfig
339
+ this.#profile = globalConfig
319
340
  for (let i = 0; i < this.#keys.length; i++) {
320
341
  const key = this.#keys[i]
321
342
  const elem = this.#states[key]
@@ -392,7 +413,7 @@ export class Button {
392
413
 
393
414
  updateState(index,eventType){
394
415
  this.#index = index
395
- this.#event = eventType
416
+ this.#event = eventType
396
417
  // Update the State according to the correctly pressed state
397
418
  if (this.#index < 0) { this.#index = this.#keys.length - 1 }
398
419
  this.#index %= this.#keys.length
@@ -405,7 +426,7 @@ export class Button {
405
426
  if (!this.getCurrentElement()) { return false }
406
427
 
407
428
  this.#event = "rotated"
408
- this.#value = calcDelta(this.#value, delta, this.#max)
429
+ this.#value = calcDelta(this.#value, delta, this.#min, this.#max)
409
430
  return this.runCommand()
410
431
  }
411
432
 
@@ -426,20 +447,21 @@ export class Button {
426
447
 
427
448
  // check if the nodeid is the same and the value is one of the states
428
449
  let state = this.#states[key]
429
- if (!state.value)
450
+ if (state.value === undefined)
430
451
  continue
431
452
 
432
453
  const params = {
433
454
  id: buttonID,
434
455
  key: buttonID,
456
+ state : key,
435
457
  ...state
436
458
  }
437
459
  let val1 = format(state.value,params)
438
- if (val1 === val){
460
+ if (val1 === val.toString()){
439
461
  this.#index = i;
440
462
  break;
441
463
  }
442
- break;
464
+ //break;
443
465
  }
444
466
  }
445
467
 
@@ -479,7 +501,7 @@ export class Button {
479
501
  return
480
502
  }
481
503
 
482
- if (elem.profile){
504
+ if (elem.profile !== undefined) {
483
505
  profileEmitter.emit("profileChanged", elem.profile)
484
506
  }
485
507
 
@@ -492,7 +514,7 @@ export class Button {
492
514
  // and also all attributes of elem + global config
493
515
  const params = {
494
516
  text: this.getCurrentText(),
495
- ...this.#config.parameters,
517
+ ...this.#profile.parameters,
496
518
  ...elem,
497
519
  id: this.id,
498
520
  key: this.key,
@@ -504,7 +526,7 @@ export class Button {
504
526
  y: (this.#y %100)
505
527
  }
506
528
 
507
- if (!params.value)
529
+ if (params.value === undefined)
508
530
  params.value = this.#value
509
531
 
510
532
 
@@ -526,6 +548,12 @@ export class Button {
526
548
  if ('opcua' in elem) {
527
549
  if (opcuainterface){
528
550
  res = await opcuainterface.call(elem.opcua, params)
551
+
552
+ if (this.#data.statenodeid){
553
+ let stateParams = params
554
+ params.value = params.state
555
+ res = await opcuainterface.call(this.#data.statenodeid, params)
556
+ }
529
557
  }else{
530
558
  console.warn("opcuainterface not started")
531
559
  }
package/common/utils.mjs CHANGED
@@ -22,9 +22,64 @@ export function writeJSONFile (fileName, jsonObj) {
22
22
  writeFileSync(fileName, data)
23
23
  }
24
24
 
25
- export function calcDelta (data, delta, max = 100) {
25
+ /**
26
+ * Calculate the delta of a value within a range between min and max with overflow
27
+ * @param {*} data
28
+ * @param {*} delta
29
+ * @param {*} min
30
+ * @param {*} max
31
+ * @returns
32
+ */
33
+ export function calcDelta (data, delta, min = 0, max = 100) {
26
34
  data = data + delta
27
- if (data > max) { data = max }
28
- if (data < 0) { data = 0 }
35
+ if (data > max) { data = min }
36
+ else if (data < min) { data = max }
29
37
  return data
30
38
  }
39
+
40
+ /**
41
+ * Invert a color in hex format (#RRGGBB) to its inverted color
42
+ * @param {*} colorAsHex
43
+ * @returns
44
+ */
45
+ export function invertColor(colorAsHex){
46
+ let rgb = colorToRGB(colorAsHex)
47
+ for (var i = 0; i < rgb.length; i++) {
48
+ rgb[i] = (i === 3 ? 1 : 255) - rgb[i];
49
+ }
50
+ let invertedColor = `#${rgb[0].toString(16)}${rgb[1].toString(16)}${rgb[2].toString(16)})`
51
+ //let invertedColor = `#${rgb[0].toString(16).padStart(2, '0')}${rgb[1].toString(16).padStart(2, '0')}${rgb[2].toString(16).padStart(2, '0')})`
52
+ //console.log("invert", colorAsHex,"=>",invertedColor)
53
+ return invertedColor
54
+ }
55
+
56
+ /**
57
+ * Convert a color in hex format (#RRGGBB) to RGB array
58
+ * @param {*} colorAsHex
59
+ * @returns
60
+ */
61
+ export function colorToRGB(colorAsHex){
62
+ const r = parseInt(colorAsHex.slice(1, 3), 16)
63
+ const g = parseInt(colorAsHex.slice(3, 5), 16)
64
+ const b = parseInt(colorAsHex.slice(5, 7), 16)
65
+ return [r,g,b]
66
+ }
67
+
68
+ /**
69
+ * Synchronize the parameters of a node with the default node
70
+ * @param {*} node
71
+ * @param {*} defaultnode
72
+ * @returns node
73
+ */
74
+ export function syncParams(node, defaultnode){
75
+ if (node === undefined || defaultnode === undefined)
76
+ return node
77
+ let keys = Object.keys(defaultnode)
78
+ for (var i=0;i<keys.length;i++){
79
+ let key = keys[i]
80
+ if (!(key in node))
81
+ node[key] = defaultnode[key]
82
+ }
83
+ return node
84
+ }
85
+
package/config.json CHANGED
@@ -4,6 +4,10 @@
4
4
  {
5
5
  "name": "profile-1",
6
6
  "file": "profile-1.json"
7
+ },
8
+ {
9
+ "name": "profile-2",
10
+ "file": "profile-2.json"
7
11
  }
8
12
  ]
9
13
  }
@@ -14,7 +14,9 @@ export class BaseIf extends EventEmitter {
14
14
 
15
15
  this.cmd = cmd
16
16
  this.options = options
17
- this.formattedCommand = this.formatString(cmd, options)
17
+ this.formattedCommand = this.formatString(cmd, options)
18
+ this.LogDebug("Formatted command: ", this.formattedCommand,"\n")
19
+ this.LogDebug("Options",JSON.stringify(options),"\n" )
18
20
  return this.formattedCommand
19
21
  }
20
22
 
@@ -36,7 +36,6 @@ export class OPCUAIf extends BaseIf {
36
36
  monitoreditems
37
37
  types
38
38
  buttons
39
- #callback
40
39
  constructor() {
41
40
  super()
42
41
 
@@ -55,14 +54,19 @@ export class OPCUAIf extends BaseIf {
55
54
  this.LogInfo(`OPCUAIf Stopped\n`)
56
55
  }
57
56
 
58
- async init( options = {},config = {},callbackFunction){
57
+ /**
58
+ * Initialize the OPCUA client and subscribe to monitored items.
59
+ * @param {*} options
60
+ * @param {*} config
61
+ * @param {*} callbackFunction
62
+ */
63
+ async init( options = {},config = {}){
59
64
  var res = this.Check(options)
60
65
  if (res<0){
61
66
  this.LogError(`OPCUAIf: Missing essential options in dictionary => Quitting $res $options\n`)
62
67
  }
63
68
  try{
64
69
  this.#endpointurl = this.formatString(options.endpointurl,options)
65
- this.#callback = callbackFunction
66
70
  this.monitoreditems = {}
67
71
  this.types = {}
68
72
  this.buttons = {}
@@ -70,7 +74,7 @@ export class OPCUAIf extends BaseIf {
70
74
 
71
75
  await this.Connect(this.#endpointurl);
72
76
 
73
- let fields = [config.touch.center, config.knobs]
77
+ let fields = [config.touch.center, config.knobs, config.buttons]
74
78
  const fieldKeys = Object.keys(fields)
75
79
  for (let f = 0; f < fieldKeys.length; f++) {
76
80
  let field=fields[f]
@@ -78,11 +82,18 @@ export class OPCUAIf extends BaseIf {
78
82
  for (let i = 0; i < keys.length; i++) {
79
83
  const key = keys[i]
80
84
  const elem = field[key]
85
+ // groupnode
81
86
  if (elem.nodeid){
82
87
  let format = this.formatString(elem.nodeid,options)
83
88
  let monitoredItemId = await this.Subscribe(format)
84
89
  this.buttons[monitoredItemId] = i
85
90
  }
91
+ // statenode
92
+ if (elem.statenodeid){
93
+ let format = this.formatString(elem.statenodeid,options)
94
+ let monitoredItemId = await this.Subscribe(format)
95
+ this.buttons[monitoredItemId] = i
96
+ }
86
97
  await this.monitorStates(elem,options)
87
98
  }
88
99
  }
@@ -91,6 +102,11 @@ export class OPCUAIf extends BaseIf {
91
102
  }
92
103
  }
93
104
 
105
+ /**
106
+ *
107
+ * @param {*} elem : Elem Object
108
+ * @param {*} options
109
+ */
94
110
  async monitorStates(elem,options){
95
111
  const stateKeys = Object.keys(elem.states)
96
112
  for (let i = 0; i < stateKeys.length; i++) {
@@ -104,6 +120,16 @@ export class OPCUAIf extends BaseIf {
104
120
  }
105
121
  }
106
122
 
123
+ /**
124
+ * Convert the given value to the specified type.
125
+ * @param {*} value : The value to convert.
126
+ * @param {*} type : The type to convert to. Can be one of the following:
127
+ * - DataType.Int16
128
+ * - DataType.Int32
129
+ * - DataType.Float
130
+ * - DataType.String
131
+ * @returns the converted value.
132
+ */
107
133
  convert(value,type){
108
134
  switch(type){
109
135
  case DataType.Int16:
@@ -121,6 +147,19 @@ export class OPCUAIf extends BaseIf {
121
147
  return value
122
148
  return parseFloat(value)
123
149
  break;
150
+ case DataType.String:
151
+ if (typeof value == "number")
152
+ return value.toString();
153
+ return value
154
+ case DataType.Boolean:
155
+ if (typeof value == "number" && value === 1)
156
+ return true
157
+ if (typeof value == "string"){
158
+ if (["true","on"].includes(value)){
159
+ return true
160
+ }
161
+ }
162
+ return false
124
163
  default:
125
164
  return value
126
165
  }
@@ -135,13 +174,13 @@ export class OPCUAIf extends BaseIf {
135
174
 
136
175
  var nodeId = super.formatString(opcuaNode, options)
137
176
 
138
- var type = this.types[opcuaNode]
177
+ var type = this.types[nodeId]
139
178
  var value = options.value
140
179
  if (typeof value == "string")
141
180
  value = super.formatString(options.value, options)
142
181
 
143
182
  var convertedValue = this.convert(value,type)
144
- //this.LogInfo(`OPCUAIf: write ${nodeId} => ${value}\n`)
183
+ this.LogInfo(`OPCUAIf: write ${nodeId} => ${value}\n`)
145
184
  await this.Write(nodeId,convertedValue,type)
146
185
 
147
186
  var NewState = "waiting"
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "loupedeck-commander",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
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": {
7
- "test": "echo \"INFO: no test specified\" && exit 0",
7
+ "test": "node test.mjs",
8
8
  "lint": "echo \"INFO: no linter specified\" && exit 0",
9
9
  "format": "echo \"INFO: no format rule specified\" && exit 0",
10
10
  "start": "node index.mjs"
11
11
  },
12
12
  "dependencies": {
13
- "canvas": "^2.11.2",
14
- "loupedeck": "^6.0.1",
13
+ "canvas": "^3.1.0",
14
+ "loupedeck": "^7.0.0",
15
15
  "mkdirp": "^3.0.1",
16
- "node-opcua": "^2.138.1",
16
+ "node-opcua": "^2.153.0",
17
17
  "string-template": "^1.0.0"
18
18
  },
19
19
  "author": "Thomas Schneider",
package/profile-1.json CHANGED
@@ -228,7 +228,8 @@
228
228
  "knobBL": {
229
229
  "states": {
230
230
  "on": {
231
- "cmd": "echo \"{id} {state}\""
231
+ "cmd": "echo \"{id} {state}\"",
232
+ "opcua": "ns=2;s=Is{simnbr}.Audio.in.VolumeAccustic"
232
233
  }
233
234
  },
234
235
  "group": ""
@@ -258,117 +259,11 @@
258
259
  "group": ""
259
260
  }
260
261
  },
261
- "buttons": {
262
- "0": {
263
- "states": {
264
- "off": {
265
- "color": "#000000",
266
- "cmd": "echo \"{id} {state}\""
267
- },
268
- "on": {
269
- "color": "#550000",
270
- "cmd": "echo \"{id} {state}\""
271
- }
272
- },
273
- "group": ""
274
- },
275
- "1": {
276
- "states": {
277
- "off": {
278
- "color": "#000000",
279
- "cmd": "echo \"{id} {state}\""
280
- },
281
- "on": {
282
- "color": "#ff0000",
283
- "cmd": "echo \"{id} {state}\""
284
- }
285
- },
286
- "group": ""
287
- },
288
- "2": {
289
- "states": {
290
- "off": {
291
- "color": "#000000",
292
- "cmd": "echo \"{id} {state}\""
293
- },
294
- "on": {
295
- "color": "#005500",
296
- "cmd": "echo \"{id} {state}\""
297
- }
298
- },
299
- "group": ""
300
- },
301
- "3": {
302
- "states": {
303
- "off": {
304
- "color": "#000000",
305
- "cmd": "echo \"{id} {state}\""
306
- },
307
- "on": {
308
- "color": "#00ff00",
309
- "cmd": "echo \"{id} {state}\""
310
- }
311
- },
312
- "group": ""
313
- },
314
- "4": {
315
- "states": {
316
- "off": {
317
- "color": "#000000",
318
- "cmd": "echo \"{id} {state}\""
319
- },
320
- "on": {
321
- "color": "#000055",
322
- "cmd": "echo \"{id} {state}\""
323
- }
324
- },
325
- "group": ""
326
- },
327
- "5": {
328
- "states": {
329
- "off": {
330
- "color": "#000000",
331
- "cmd": "echo \"{id} {state}\""
332
- },
333
- "on": {
334
- "color": "#0000ff",
335
- "cmd": "echo \"{id} {state}\""
336
- }
337
- },
338
- "group": ""
339
- },
340
- "6": {
341
- "states": {
342
- "off": {
343
- "color": "#000000",
344
- "cmd": "echo \"{id} {state}\""
345
- },
346
- "on": {
347
- "color": "#555500",
348
- "cmd": "echo \"{id} {state}\""
349
- }
350
- },
351
- "group": ""
352
- },
353
- "7": {
354
- "states": {
355
- "off": {
356
- "color": "#000000",
357
- "cmd": "echo \"{id} {state}\""
358
- },
359
- "on": {
360
- "color": "#ffff00",
361
- "cmd": "echo \"{id} {state}\"",
362
- "http": "http://{hostname}:7778/control/connections",
363
- "opcua": "ns=4;s=Is{simnbr}.Kvm.out.Source"
364
- }
365
- },
366
- "group": ""
367
- }
368
- },
369
262
  "parameters": {
370
263
  "hostname": "127.0.0.1",
264
+ "simnbr": "1",
371
265
  "endpointurl": "opc.tcp://{hostname}:4840",
372
- "nodeid" : "ns=0;s=nodeID"
266
+ "nodeid" : "ns=0;s=nodeID",
267
+ "verbose": true
373
268
  }
374
269
  }
package/profile-2.json ADDED
@@ -0,0 +1,290 @@
1
+ {
2
+ "name": "profile-1",
3
+ "profile": "example",
4
+ "description": "",
5
+ "touch": {
6
+ "center": {
7
+ "0": {
8
+ "default": "one",
9
+ "states": {
10
+ "off": {
11
+ "color": "#000099",
12
+ "cmd": "echo \"{id} {state}\""
13
+ },
14
+ "one": {
15
+ "color": "#00ff00",
16
+ "image": "numbers/one_9278045.png",
17
+ "cmd": "echo \"{id} {state}\""
18
+ },
19
+ "two": {
20
+ "color": "#00ff00",
21
+ "image": "numbers/two_9278103.png",
22
+ "cmd": "echo \"{id} {state}\""
23
+ },
24
+ "three": {
25
+ "color": "#00ff00",
26
+ "image": "numbers/three_9278150.png",
27
+ "cmd": "echo \"{id} {state}\""
28
+ },
29
+ "four": {
30
+ "color": "#00ff00",
31
+ "image": "numbers/four_9278183.png",
32
+ "cmd": "echo \"{id} {state}\""
33
+ },
34
+ "five": {
35
+ "color": "#00ff00",
36
+ "image": "numbers/five_9278222.png",
37
+ "cmd": "echo \"{id} {state}\""
38
+ }
39
+ }
40
+ },
41
+ "1": {
42
+ "default": "two",
43
+ "states": {
44
+ "off": {
45
+ "color": "#000099",
46
+ "cmd": "echo \"{id} {state}\""
47
+ },
48
+ "one": {
49
+ "color": "#00ff00",
50
+ "image": "numbers/one_9278045.png",
51
+ "cmd": "echo \"{id} {state}\""
52
+ },
53
+ "two": {
54
+ "color": "#00ff00",
55
+ "image": "numbers/two_9278103.png",
56
+ "cmd": "echo \"{id} {state}\""
57
+ },
58
+ "three": {
59
+ "color": "#00ff00",
60
+ "image": "numbers/three_9278150.png",
61
+ "cmd": "echo \"{id} {state}\""
62
+ },
63
+ "four": {
64
+ "color": "#00ff00",
65
+ "image": "numbers/four_9278183.png",
66
+ "cmd": "echo \"{id} {state}\""
67
+ },
68
+ "five": {
69
+ "color": "#00ff00",
70
+ "image": "numbers/five_9278222.png",
71
+ "cmd": "echo \"{id} {state}\""
72
+ }
73
+ }
74
+ },
75
+ "2": {
76
+ "default": "three",
77
+ "states": {
78
+ "off": {
79
+ "color": "#000099",
80
+ "cmd": "echo \"{id} {state}\""
81
+ },
82
+ "one": {
83
+ "color": "#00ff00",
84
+ "image": "numbers/one_9278045.png",
85
+ "cmd": "echo \"{id} {state}\""
86
+ },
87
+ "two": {
88
+ "color": "#00ff00",
89
+ "image": "numbers/two_9278103.png",
90
+ "cmd": "echo \"{id} {state}\""
91
+ },
92
+ "three": {
93
+ "color": "#00ff00",
94
+ "image": "numbers/three_9278150.png",
95
+ "cmd": "echo \"{id} {state}\""
96
+ },
97
+ "four": {
98
+ "color": "#00ff00",
99
+ "image": "numbers/four_9278183.png",
100
+ "cmd": "echo \"{id} {state}\""
101
+ },
102
+ "five": {
103
+ "color": "#00ff00",
104
+ "image": "numbers/five_9278222.png",
105
+ "cmd": "echo \"{id} {state}\""
106
+ }
107
+ }
108
+ },
109
+ "3": {
110
+ "default": "four",
111
+ "states": {
112
+ "off": {
113
+ "color": "#000099",
114
+ "cmd": "echo \"{id} {state}\""
115
+ },
116
+ "one": {
117
+ "color": "#00ff00",
118
+ "image": "numbers/one_9278045.png",
119
+ "cmd": "echo \"{id} {state}\""
120
+ },
121
+ "two": {
122
+ "color": "#00ff00",
123
+ "image": "numbers/two_9278103.png",
124
+ "cmd": "echo \"{id} {state}\""
125
+ },
126
+ "three": {
127
+ "color": "#00ff00",
128
+ "image": "numbers/three_9278150.png",
129
+ "cmd": "echo \"{id} {state}\""
130
+ },
131
+ "four": {
132
+ "color": "#00ff00",
133
+ "image": "numbers/four_9278183.png",
134
+ "cmd": "echo \"{id} {state}\""
135
+ },
136
+ "five": {
137
+ "color": "#00ff00",
138
+ "image": "numbers/five_9278222.png",
139
+ "cmd": "echo \"{id} {state}\""
140
+ }
141
+ }
142
+ },
143
+ "4": {
144
+ "default": "five",
145
+ "states": {
146
+ "off": {
147
+ "color": "#000099",
148
+ "cmd": "echo \"{id} {state}\""
149
+ },
150
+ "one": {
151
+ "color": "#00ff00",
152
+ "image": "numbers/one_9278045.png",
153
+ "cmd": "echo \"{id} {state}\""
154
+ },
155
+ "two": {
156
+ "color": "#00ff00",
157
+ "image": "numbers/two_9278103.png",
158
+ "cmd": "echo \"{id} {state}\""
159
+ },
160
+ "three": {
161
+ "color": "#00ff00",
162
+ "image": "numbers/three_9278150.png",
163
+ "cmd": "echo \"{id} {state}\""
164
+ },
165
+ "four": {
166
+ "color": "#00ff00",
167
+ "image": "numbers/four_9278183.png",
168
+ "cmd": "echo \"{id} {state}\""
169
+ },
170
+ "five": {
171
+ "color": "#00ff00",
172
+ "image": "numbers/five_9278222.png",
173
+ "cmd": "echo \"{id} {state}\""
174
+ }
175
+ }
176
+ },
177
+ "5": {
178
+ "states": {
179
+ "off": {
180
+ "color": "#000099",
181
+ "cmd": "echo \"{id} {state}\""
182
+ },
183
+ "one": {
184
+ "color": "#00ff00",
185
+ "image": "numbers/one_9278045.png",
186
+ "cmd": "echo \"{id} {state}\""
187
+ },
188
+ "two": {
189
+ "color": "#00ff00",
190
+ "image": "numbers/two_9278103.png",
191
+ "cmd": "echo \"{id} {state}\""
192
+ },
193
+ "three": {
194
+ "color": "#00ff00",
195
+ "image": "numbers/three_9278150.png",
196
+ "cmd": "echo \"{id} {state}\""
197
+ },
198
+ "four": {
199
+ "color": "#00ff00",
200
+ "image": "numbers/four_9278183.png",
201
+ "cmd": "echo \"{id} {state}\""
202
+ },
203
+ "five": {
204
+ "color": "#00ff00",
205
+ "image": "numbers/five_9278222.png",
206
+ "cmd": "echo \"{id} {state}\""
207
+ }
208
+ }
209
+ }
210
+ },
211
+ "left": {
212
+ "0": {
213
+ "states": {
214
+ "on": {
215
+ "color": "#000000",
216
+ "cmd": "echo \"{id} {state}\""
217
+ }
218
+ }
219
+ }
220
+ },
221
+ "right": {
222
+ "0": {
223
+ "states": {
224
+ "on": {
225
+ "color": "#000000",
226
+ "cmd": "echo \"{id} {state}\""
227
+ }
228
+ }
229
+ }
230
+ }
231
+ },
232
+ "knobs": {
233
+ "knobTL": {
234
+ "states": {
235
+ "on": {
236
+ "cmd": "echo \"{id} {state}\""
237
+ }
238
+ },
239
+ "group": ""
240
+ },
241
+ "knobCL": {
242
+ "states": {
243
+ "on": {
244
+ "cmd": "echo \"{id} {state}\""
245
+ }
246
+ },
247
+ "group": ""
248
+ },
249
+ "knobBL": {
250
+ "states": {
251
+ "on": {
252
+ "cmd": "echo \"{id} {state}\"",
253
+ "opcua": "ns=2;s=Is{simnbr}.Audio.in.VolumeAccustic"
254
+ }
255
+ },
256
+ "group": ""
257
+ },
258
+ "knobTR": {
259
+ "states": {
260
+ "on": {
261
+ "cmd": "echo \"{id} {state}\""
262
+ }
263
+ },
264
+ "group": ""
265
+ },
266
+ "knobCR": {
267
+ "states": {
268
+ "on": {
269
+ "cmd": "echo \"{id} {state}\""
270
+ }
271
+ },
272
+ "group": ""
273
+ },
274
+ "knobBR": {
275
+ "states": {
276
+ "on": {
277
+ "cmd": "echo \"{id} {state}\""
278
+ }
279
+ },
280
+ "group": ""
281
+ }
282
+ },
283
+ "parameters": {
284
+ "hostname": "127.0.0.1",
285
+ "simnbr": "1",
286
+ "endpointurl": "opc.tcp://{hostname}:4840",
287
+ "nodeid" : "ns=0;s=nodeID",
288
+ "verbose": true
289
+ }
290
+ }