loupedeck-commander 1.2.11 → 1.3.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.
@@ -0,0 +1,457 @@
1
+
2
+ import { loadImage } from 'canvas'
3
+ //import { loadImage } from "https://deno.land/x/canvas/mod.ts";
4
+ import { calcDelta, invertColor } from './utils.mjs'
5
+ import format from 'string-template'
6
+
7
+ import { shellinterface, httpinterface, opcuainterface, profileEmitter } from '../interfaces/interfaces.mjs'
8
+
9
+ export const ButtonType = {
10
+ NONE: '',
11
+ TOGGLE: 'TOGGLE',
12
+ PUSH: 'PUSH'
13
+ }
14
+
15
+
16
+
17
+ export class Button {
18
+ #profile
19
+ #params
20
+ //#data
21
+ #index = 0
22
+ #enforcedIndex = -1
23
+ #event
24
+ #keys
25
+ #states
26
+ // Timestamp when button was pressed
27
+ timeStampPressed
28
+ // Timestamp when button was released
29
+ timeStampReleased
30
+ // Time actually hold the button in ms
31
+ timeHold
32
+ #counter = 0
33
+
34
+ constructor (id, width, height, data={},key,profile = {}) {
35
+ this.#states = {}
36
+ this.#keys = []
37
+
38
+ this.id = id
39
+ this.#index = 0
40
+ this.#enforcedIndex = -1
41
+
42
+ this.#profile = profile
43
+ this.#params = {
44
+ "key" : key,
45
+ "state" : '',
46
+ "width" : width,
47
+ "height" : height,
48
+ "min" : 0,
49
+ "max" : 100,
50
+ "x" : 0,
51
+ "y" : 0,
52
+ "color" : '#000000',
53
+ "text" : '',
54
+ "textColor" : '#ffffff',
55
+ "textAlign" : 'center',
56
+ "textBaseline": 'top',
57
+ "font" :'16px Arial',
58
+ "value" : 50,
59
+ "minPressed": 25,
60
+ "moveLeft" : false,
61
+ "moveRight" : false,
62
+ "moveUp" : false,
63
+ "moveDown" : false,
64
+ "nodeid" : '',
65
+ "type" : ButtonType.TOGGLE,
66
+ "group" : key,
67
+ "filter" : "",
68
+ "blink" : false,
69
+ "vibrate" : false,
70
+ "profile" : "",
71
+ "brightness" : undefined,
72
+ "default" : "0"
73
+ }
74
+
75
+ if (profile.parameters){
76
+ let params = Object.keys(profile.parameters)
77
+ for (let i = 0; i < params.length; i++) {
78
+ const k = params[i]
79
+ this.#params[k] = profile.parameters[k]
80
+ }
81
+ }
82
+ // enforce key, so it's not oveerwritten somewhere else
83
+ this.#params.key = key
84
+
85
+ // New approach: Use all data from data.params:
86
+ if (data.params){
87
+ this.#params = {
88
+ ...this.#params,
89
+ ...data.params
90
+ }
91
+
92
+ // Format the params:
93
+ this.#params.group = format(this.#params.group, this.getParams({}))
94
+ this.#params.nodeid = format(this.#params.nodeid, this.getParams({}))
95
+ }
96
+
97
+ if (data.states) {
98
+ this.#states = data.states
99
+ this.#keys = Object.keys(this.#states)
100
+
101
+ var defaultIndex = this.#keys.indexOf(this.#params.default)
102
+ if (defaultIndex >= 0) {
103
+ this.#index = defaultIndex
104
+ } else {
105
+ this.#params.default = this.#keys[0]
106
+ console.info(` button ${id} default set to ${this.#params.default}, available states`, this.#keys)
107
+ }
108
+ this.#params.state = this.#keys[this.#index]
109
+ }
110
+
111
+ // Set the default value for the button:
112
+
113
+ // Legacy approach: Use data directly:
114
+ if (data.minPressed) {
115
+ console.warn("Legacy approach: minPressed is deprecated, use params.minPressed instead")
116
+ this.#params.minPressed = data.minPressed
117
+ }
118
+ if (data.text) {
119
+ console.warn("Legacy approach: text is deprecated, use params.text instead")
120
+ this.#params.text = data.text
121
+ }
122
+ if (data.nodeid){
123
+ this.#params.nodeid = format(data.nodeid, this.getParams({}))
124
+ console.warn("Legacy approach: nodeid is deprecated, use params.nodeid instead",this.#params.nodeid)
125
+ }
126
+ if (data.type){
127
+ console.warn("Legacy approach: type is deprecated, use params.type instead")
128
+ this.#params.type = data.type
129
+ }
130
+
131
+ if (data.default) {
132
+ console.warn("Legacy approach: default is deprecated, use params.default instead")
133
+ this.#index = this.#keys.indexOf(data.default)
134
+ }
135
+
136
+ if (data.group){
137
+ console.warn("Legacy approach: group is deprecated, use params.group instead")
138
+ this.#params.group = format(data.group, this.getParams({}))
139
+ }
140
+ // End Legacy approach
141
+ }
142
+
143
+ setState (index = 0) {
144
+ this.#index = index
145
+ }
146
+
147
+ async drawPhysical (device, id) {
148
+ const elem = this.getCurrentElement()
149
+ if (!elem || !elem.color) { return }
150
+
151
+ const r = parseInt(elem.color.slice(1, 3), 16)
152
+ const g = parseInt(elem.color.slice(3, 5), 16)
153
+ const b = parseInt(elem.color.slice(5, 7), 16)
154
+
155
+ try {
156
+ var idx = parseInt(id, 10);
157
+
158
+ const val = {
159
+ id:idx,
160
+ color: `rgba(${r}, ${g}, ${b})`
161
+ }
162
+ // console.log(' Set Button Color',id, val.id,elem.color, val.color)
163
+ device.setButtonColor(val)
164
+ } catch (error) {
165
+ console.error(' Error', error)
166
+ }
167
+ }
168
+
169
+ async draw (row, column, ctx) {
170
+ const x = column * this.#params.width
171
+ const y = row * this.#params.height
172
+
173
+ const elem = this.getCurrentElement()
174
+ let params = this.getParams(elem)
175
+
176
+ if (elem) {
177
+ if(elem.blink && this.#counter % 2 == 0) {
178
+ // If blink is set, we switch the color to inverted color
179
+ let oldColor = elem.color
180
+ elem.color = params.textColor
181
+ elem.textColor = oldColor
182
+ //console.log("Blinking Button",this.#index,elem.color)
183
+ }
184
+
185
+ ctx.fillStyle = params.color
186
+ ctx.fillRect(x, y, params.width, params.height)
187
+
188
+ if (elem.imgBuffer) {
189
+ ctx.drawImage(elem.imgBuffer, x, y, params.width, params.height)
190
+ }
191
+ }
192
+ if (params.text != undefined && params.text != '') {
193
+ ctx.fillStyle = params.textColor
194
+ ctx.font = params.font
195
+ ctx.textBaseline = params.textBaseline;
196
+ ctx.textAlign = params.textAlign;
197
+ let dynamicText = format(params.text, this.getParams(elem))
198
+ ctx.fillText(dynamicText,x+6,y+6)
199
+ }
200
+
201
+ this.#counter = this.#counter + 1
202
+ }
203
+
204
+ async load (globalConfig) {
205
+ this.#profile = globalConfig
206
+ for (let i = 0; i < this.#keys.length; i++) {
207
+ const key = this.#keys[i]
208
+ const elem = this.#states[key]
209
+ const file = elem.image
210
+ if (file !== undefined && file !== '') {
211
+ try {
212
+ this.#states[key].imgBuffer = await loadImage(file)
213
+ } catch (e) {
214
+ console.error('No such image', file)
215
+ return false
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ getCurrentElement () {
222
+ const state = this.#keys[this.#index]
223
+ return this.#states[state]
224
+ }
225
+
226
+ getLastElement () {
227
+ let i=this.#index-1
228
+ if (i < 0) { i = this.#keys.length - 1 }
229
+
230
+ const lastState = this.#keys[i]
231
+ return this.#states[lastState]
232
+ }
233
+
234
+ pressed () {
235
+ this.timeStampPressed = Date.now()
236
+
237
+ this.#index++
238
+
239
+ if (this.#enforcedIndex>=0){
240
+ console.log("Enforced Index",this.#enforcedIndex )
241
+ this.#index = this.#enforcedIndex
242
+ }
243
+
244
+ this.updateState(this.#index,"pressed",true)
245
+ return true
246
+ }
247
+
248
+ released () {
249
+ let elem = this.getCurrentElement()
250
+ if (!elem) { return false }
251
+ this.timeStampReleased = Date.now()
252
+ this.timeHold = this.timeStampReleased - this.timeStampPressed
253
+
254
+ let bExecute = true
255
+ if (this.timeHold < this.#params.minPressed) {
256
+ // Update the State according to the not correct pressed state
257
+ console.log('Did not hold minimum time of ', this.#params.minPressed, 'only', this.timeHold)
258
+ //if (this.#index < 0) { this.#index = this.#keys.length - 1 }
259
+ bExecute = false
260
+ }
261
+
262
+ // Update the State according to the correctly pressed state
263
+ switch (this.#params.type) {
264
+ case ButtonType.TOGGLE:
265
+ // do nothing
266
+ break
267
+ default:
268
+ this.#index--
269
+ //if (this.#index < 0) { this.#index = this.#keys.length - 1 }
270
+ break
271
+ }
272
+
273
+ if (this.#enforcedIndex>=0){
274
+ console.log("Enforced Index",this.#enforcedIndex )
275
+ this.#index = this.#enforcedIndex
276
+ }
277
+
278
+
279
+ this.updateState(this.#index,"released",bExecute)
280
+
281
+ return true // this.runCommand()
282
+ }
283
+
284
+ updateState(index,eventType,bExecute){
285
+ this.#index = index
286
+ this.#event = eventType
287
+ // Update the State according to the correctly pressed state
288
+ if (this.#index < 0) { this.#index = this.#keys.length - 1 }
289
+ this.#index %= this.#keys.length
290
+ if (bExecute)
291
+ this.runCommand()
292
+ //console.log("TODO: expect newState", newState)
293
+ return true // this.runCommand()
294
+ }
295
+
296
+ async rotated (delta) {
297
+ if (!this.getCurrentElement()) { return false }
298
+
299
+ this.#event = "rotated"
300
+ this.#params.value = calcDelta(this.#params.value, delta, this.#params.min, this.#params.max)
301
+ return this.runCommand()
302
+ }
303
+
304
+ async changed(buttonID,nodeid,val){
305
+ // Only handle updates within the same group identified by nodeid
306
+ if (nodeid !== this.#params.nodeid){
307
+ return
308
+ }
309
+
310
+ this.#index = 0;
311
+ this.#enforcedIndex = -1;
312
+ for (let i = 0; i < this.#keys.length; i++) {
313
+ let key = this.#keys[i]
314
+ // check if the state-name is same as the value we get from outside:
315
+ if (val == key){
316
+ this.#index = i;
317
+ console.info("enforce index", buttonID,nodeid,val,i)
318
+ this.#enforcedIndex = this.#index;
319
+ break;
320
+ }
321
+
322
+ // check if the nodeid is the same and the value is one of the states
323
+ // todo überarbeiten
324
+ let state = this.#states[key]
325
+ if (state.value === undefined)
326
+ continue
327
+
328
+ const params = {
329
+ id: buttonID,
330
+ key: buttonID,
331
+ state : key,
332
+ ...state
333
+ }
334
+ if (state && state.value !== undefined){
335
+ let strVal = state.value.toString()
336
+ let val1 = format(strVal,params)
337
+ if (val1 === val?.toString()){
338
+ this.#index = i;
339
+ break;
340
+ }
341
+ }
342
+ //break;
343
+ }
344
+ }
345
+
346
+ async touchmove (x, y) {
347
+ // if (!this.getCurrentElement()) { return false }
348
+
349
+ let delta = 0
350
+ if (x > this.#params.x) {
351
+ this.#params.moveRight = true
352
+ this.#params.moveLeft = false
353
+ } else if (x < this.#params.x) {
354
+ this.#params.moveRight = false
355
+ this.#params.moveLeft = true
356
+ }
357
+
358
+ if (y > this.#params.y) {
359
+ this.#params.moveDown = true
360
+ this.#params.moveUp = false
361
+ delta = -1
362
+ } else if (y < this.#params.y) {
363
+ this.#params.moveDown = false
364
+ this.#params.moveUp = true
365
+ delta = 1
366
+ }
367
+
368
+ this.#params.x = (x%100)
369
+ this.#params.y = (y%100)
370
+
371
+ // Calculate delta for value no touchmove up/down
372
+ this.#params.value = calcDelta(this.#params.value, delta, this.#params.min, this.#params.max)
373
+
374
+ // console.log(`d: ${this.#params.moveDown} r: ${this.#params.moveRight} `)
375
+ return false
376
+ }
377
+
378
+ getParams(elem){
379
+ // Call an action - include dynamic parameters
380
+ // and also all attributes of elem + global config
381
+ const params = {
382
+ ...this.#profile.parameters,
383
+ ...this.#params,
384
+ ...elem,
385
+ id: this.id,
386
+ //key: this.key,
387
+ event: this.#event,
388
+ pressed : this.#event == "pressed",
389
+ released : this.#event == "released",
390
+ rotated : this.#event == "rotated",
391
+ // state: this.#keys[this.#index]
392
+ }
393
+ return params
394
+ }
395
+
396
+ async runCommand () {
397
+ const elem = this.getCurrentElement()
398
+ // Only continue, if we have an element:
399
+ if (!elem) {
400
+ return
401
+ }
402
+ // Filter for Event Type:
403
+ if (elem.filter && elem.filter != this.#event){
404
+ return
405
+ }
406
+
407
+ if (elem.profile !== undefined) {
408
+ profileEmitter.emit("profileChanged", elem.profile)
409
+ }
410
+
411
+ if (elem.brightness !== undefined) {
412
+ profileEmitter.emit("brightnessChanged", elem.brightness)
413
+ }
414
+
415
+ if (elem.vibrate !== undefined) {
416
+ profileEmitter.emit("vibrate", elem.vibrate)
417
+ }
418
+
419
+ // Only continue, if we have an element, that contains some kind of command:
420
+ if (!elem.cmd && !elem.http && !elem.opcua) {
421
+ return
422
+ }
423
+
424
+ let params = this.getParams(elem)
425
+ let res = ''
426
+ if ('cmd' in elem) {
427
+ if (shellinterface){
428
+ res = await shellinterface.call(elem.cmd, params)
429
+ }else{
430
+ console.warn("shellinterface not started")
431
+ }
432
+ }
433
+ if ('http' in elem) {
434
+ if (httpinterface){
435
+ res = await httpinterface.call(elem.http, params)
436
+ }else{
437
+ console.warn("httpinterface not started")
438
+ }
439
+ }
440
+ if ('opcua' in elem) {
441
+ if (opcuainterface){
442
+ res = await opcuainterface.call(elem.opcua, params)
443
+
444
+ /** TODO: CHECK THIS - don't think it's ever used any more */
445
+ /*if (this.#data.statenodeid){
446
+ let stateParams = params
447
+ params.value = params.state
448
+ res = await opcuainterface.call(this.#data.statenodeid, params)
449
+ }*/
450
+ }else{
451
+ console.warn("opcuainterface not started")
452
+ }
453
+ }
454
+
455
+ return res
456
+ }
457
+ }
@@ -25,7 +25,7 @@ export class HTTPif extends BaseIf {
25
25
  }
26
26
 
27
27
  async stop(){
28
- this.LogInfo("HTTPif: Stopping\n")
28
+ this.LogInfo("HTTPif: Stopped\n")
29
29
 
30
30
  }
31
31
 
@@ -0,0 +1,34 @@
1
+ import * as shellif from '../interfaces/shellif.mjs'
2
+ import * as httpif from '../interfaces/httpif.mjs'
3
+ import * as opcuaif from '../interfaces/opcuaif.mjs'
4
+ import { EventEmitter } from 'node:events'
5
+
6
+ export var opcuainterface = undefined
7
+ export var httpinterface = undefined
8
+ export var shellinterface = undefined
9
+ export var profileEmitter = undefined
10
+
11
+ export async function InitializeInterfaces(appConfig){
12
+ if (opcuainterface === undefined ){
13
+ opcuainterface = new opcuaif.OPCUAIf()
14
+ }
15
+ // the opcua interface needs the profile to register nodes with subscriptions:
16
+ opcuainterface.init(appConfig.parameters,appConfig)
17
+ if (httpinterface === undefined)
18
+ httpinterface = new httpif.HTTPif()
19
+ if (shellinterface === undefined)
20
+ shellinterface = new shellif.SHELLif()
21
+ if (profileEmitter === undefined)
22
+ profileEmitter = new EventEmitter()
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
+
@@ -79,28 +79,24 @@ export class OPCUAIf extends BaseIf {
79
79
  for (let f = 0; f < fieldKeys.length; f++) {
80
80
  let field = fields[f]
81
81
  const keys = Object.keys(field)
82
+
83
+ // Iterate over buttons:
82
84
  for (let i = 0; i < keys.length; i++) {
83
85
  const key = keys[i]
84
86
  const elem = field[key]
87
+ options["key"] = key // we have to know the key of the button
85
88
  // groupnode
86
- if (elem.nodeid) {
87
- options["key"] = key
88
- let formattedNodeId = this.formatString(elem.nodeid, options)
89
+ if (elem.params && elem.params.nodeid) {
90
+ let formattedNodeId = this.formatString(elem.params.nodeid, options)
89
91
  let monitoredItemId = await this.Subscribe(formattedNodeId)
90
- console.log("Subscribe to",formattedNodeId)
91
- this.buttons[monitoredItemId] = i
92
- }
93
- // statenode
94
- if (elem.statenodeid) {
95
- let format = this.formatString(elem.statenodeid, options)
96
- let monitoredItemId = await this.Subscribe(format)
92
+ console.log("Subscribe to",monitoredItemId,formattedNodeId)
97
93
  this.buttons[monitoredItemId] = i
98
94
  }
99
95
  await this.monitorStates(elem, options)
100
96
  }
101
97
  }
102
98
  } catch (error) {
103
- this.LogError(`OPCUAIf: Error $error\n`)
99
+ this.LogError(`OPCUAIf: Error\n`,error)
104
100
  }
105
101
  }
106
102
 
@@ -115,8 +111,9 @@ export class OPCUAIf extends BaseIf {
115
111
  const key2 = stateKeys[i]
116
112
  const state = elem.states[key2]
117
113
  if (state.opcua) {
118
- let format = this.formatString(state.opcua, options)
119
- let monitoredItemId = await this.Subscribe(format)
114
+ let formattedNodeId = this.formatString(state.opcua, options)
115
+ let monitoredItemId = await this.Subscribe(formattedNodeId)
116
+ console.log("Subscribe to",monitoredItemId,formattedNodeId)
120
117
  this.buttons[monitoredItemId] = i
121
118
  }
122
119
  }
@@ -16,7 +16,7 @@ export class SHELLif extends BaseIf {
16
16
  }
17
17
 
18
18
  async stop() {
19
- this.LogInfo("SHELLif: Stopping")
19
+ this.LogInfo("SHELLif: Stopped")
20
20
  }
21
21
 
22
22
  Check(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loupedeck-commander",
3
- "version": "1.2.11",
3
+ "version": "1.3.0",
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": {