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.
@@ -1,576 +0,0 @@
1
- import { loadImage } from 'canvas'
2
- //import { loadImage } from "https://deno.land/x/canvas/mod.ts";
3
-
4
- import * as shellif from '../interfaces/shellif.mjs'
5
- import * as httpif from '../interfaces/httpif.mjs'
6
- import * as opcuaif from '../interfaces/opcuaif.mjs'
7
- import format from 'string-template'
8
- import { calcDelta, invertColor } from './utils.mjs'
9
- import { EventEmitter } from 'node:events'
10
-
11
-
12
- export var opcuainterface = undefined
13
- var httpinterface = undefined
14
- var shellinterface = undefined
15
- export var profileEmitter = undefined
16
- export async function InitializeInterfaces(appConfig){
17
- if (opcuainterface === undefined ){
18
- opcuainterface = new opcuaif.OPCUAIf()
19
- }
20
- // the opcua interface needs the profile to register nodes with subscriptions:
21
- opcuainterface.init(appConfig.parameters,appConfig)
22
- if (httpinterface === undefined)
23
- httpinterface = new httpif.HTTPif()
24
- if (shellinterface === undefined)
25
- shellinterface = new shellif.SHELLif()
26
- if (profileEmitter === undefined)
27
- profileEmitter = new EventEmitter()
28
- }
29
-
30
- export async function StopInterfaces(){
31
- if (opcuainterface !== undefined )
32
- await opcuainterface.stop()
33
- if (httpinterface !== undefined)
34
- await httpinterface.stop()
35
- if (shellinterface !== undefined)
36
- await shellinterface.stop()
37
-
38
- }
39
-
40
- export const ButtonIndex = {
41
- BUTN_0: 0,
42
- BUTN_1: 1,
43
- BUTN_2: 2,
44
- BUTN_3: 3,
45
- BUTN_4: 3,
46
- BUTN_5: 5,
47
- BUTN_6: 6,
48
- BUTN_7: 7,
49
- home: 'home'
50
- }
51
-
52
- const ButtonType = {
53
- NONE: '',
54
- TOGGLE: 'TOGGLE',
55
- PUSH: 'PUSH'
56
- }
57
-
58
- export class ButtonField {
59
- #buttons = {}
60
- #screen
61
- width = 0
62
- height = 0
63
- #rows = 0
64
- #columns = 0
65
- #keys = []
66
- #type
67
- #name
68
- #profile
69
-
70
- constructor (name, rows, columns, width, height, data, profile) {
71
- console.info(`ButtonField ${name.padEnd(10, ' ')} Buttons: ${rows} x ${columns} , Pixels ${width} x ${height}`)
72
- this.#name = name
73
- this.width = width
74
- this.height = height
75
- this.#rows = rows
76
- this.#columns = columns
77
- this.#screen = this.width > 0 && this.height > 0
78
- this.#type = 'button'
79
- this.#profile = profile
80
- if (this.#screen) { this.#type = 'touch' }
81
-
82
- const keys = Object.keys(data)
83
- for (let i = 0; i < keys.length; i++) {
84
- const key = keys[i]
85
- const tb = new Button(`${this.#type}-${key}`, width / columns, height / rows, data[key],key,this.#profile)
86
- this.#buttons[key] = tb
87
- }
88
-
89
- this.#keys = keys
90
- }
91
-
92
-
93
- async draw (device) {
94
- if (!this.#screen) {
95
- // physical buttons:
96
- for (let i = 0; i < this.#keys.length; i++) {
97
- const key = this.#keys[i]
98
- this.#buttons[key].drawPhysical(device, key)
99
- }
100
- } else {
101
- // screen:
102
- device.drawScreen(this.#name, ctx => {
103
- ctx.globalCompositeOperation = 'source-atop'
104
- for (let i = 0; i < this.#keys.length; i++) {
105
- const key = this.#keys[i]
106
- const iValue = parseInt(key, 10)
107
- const row = Math.floor(iValue / device.columns)
108
- const column = iValue % device.columns
109
-
110
- this.#buttons[key].draw(row, column, ctx)
111
- }
112
- })
113
- }
114
- }
115
-
116
- setState (id, val) {
117
- this.#buttons[id].setState(val)
118
- }
119
-
120
- setIntState (id, val) {
121
- this.#buttons[id].setIntState(val)
122
- }
123
-
124
- async load () {
125
- for (let i = 0; i < this.#keys.length; i++) {
126
- const key = this.#keys[i]
127
- if (isNaN(key)) {
128
- await this.#buttons[key].load(this.#profile)
129
- } else {
130
- const iVal = parseInt(key, 10)
131
- await this.#buttons[iVal].load(this.#profile)
132
- }
133
- }
134
- }
135
-
136
- async pressed (id) {
137
- this.checkAndCreateButton(id)
138
- const result = await this.#buttons[id].pressed()
139
- if (!result) {
140
- console.info(`pressed ${this.#type} ${id}`)
141
- }
142
- return result
143
- }
144
-
145
- async released (id) {
146
- const result = await this.#buttons[id].released()
147
- if (result) {
148
- // disable all other buttons of the group, if this one had been activated:
149
- for (let i = 0; i < this.#keys.length; i++) {
150
- let key = this.#keys[i]
151
- if (!isNaN(key)) { key = parseInt(key, 10) }
152
- if (id === key) { continue }
153
- if (this.#buttons[key].group === this.#buttons[id].group) {
154
- this.#buttons[key].setState(0)
155
- }
156
- }
157
- }
158
- if (!result) {
159
- console.info(`released ${this.#type} ${id}`)
160
- }
161
- return result
162
- }
163
-
164
- async changed(buttonID,nodeid,val){
165
- for (let i = 0; i < this.#keys.length; i++) {
166
- let bID = this.#keys[i]
167
- const result = await this.#buttons[bID].changed(i,nodeid,val)
168
- }
169
- }
170
-
171
- async rotated (id, delta) {
172
- this.checkAndCreateButton(id)
173
- const result = await this.#buttons[id].rotated(delta)
174
- if (!result) { console.info(`rotated ${this.#type} ${id} ${delta}`) }
175
- return result
176
- }
177
-
178
- async touchmove (id, x, y) {
179
- this.checkAndCreateButton(id)
180
- const result = await this.#buttons[id].touchmove(x, y)
181
- if (!result) { console.info(`touchmove ${id} ${x} ${y}`) }
182
- return result
183
- }
184
-
185
- checkAndCreateButton (id) {
186
- if (!(id in this.#buttons)) {
187
- const tb = new Button(id, 1, 1, id)
188
- this.#buttons[id] = tb
189
- }
190
- }
191
- }
192
-
193
- export class Button {
194
- #profile
195
- #params
196
- #data
197
- width = 0
198
- height = 0
199
-
200
- #type = ButtonType.TOGGLE
201
-
202
- #min = 0
203
- #max = 100
204
- #value = 50
205
- #name = undefined
206
- #nodeid = ""
207
-
208
- #index = 0
209
- #event
210
- #keys
211
- #states
212
-
213
- group = ''
214
-
215
- text = ''
216
- font = '16px Arial'
217
-
218
- #x = 0
219
- #y = 0
220
- #moveLeft
221
- #moveRight
222
- #moveUp
223
- #moveDown
224
-
225
- // Timestamp when button was pressed
226
- timeStampPressed
227
- // Timestamp when button was released
228
- timeStampReleased
229
- // Time actually hold the button in ms
230
- timeHold
231
- // Minimum ammount of time in ms to press a button:
232
- minPressed = 25
233
- key = -1
234
-
235
- constructor (id, width, height, data,key,profile) {
236
- this.id = id
237
- this.key = key
238
- this.width = width
239
- this.height = height
240
- this.#index = 0
241
-
242
- if (data && data.states) {
243
- this.#data = data
244
-
245
- this.group = data.group
246
-
247
- this.#states = data.states
248
- this.#keys = Object.keys(this.#states)
249
- if (data.type) {
250
- this.#type = data.type.toUpperCase()
251
- }
252
-
253
- if (data.minPressed) {
254
- this.minPressed = data.minPressed
255
- }
256
-
257
- if (data.text) {
258
- this.text = data.text
259
- }
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
- }
272
- if (this.#states === undefined) {
273
- this.#states = {}
274
- this.#keys = []
275
- }
276
- if (data.nodeid){
277
- this.#nodeid = format(data.nodeid, this.#params)
278
- }
279
- if (data.default) {
280
- this.#index = this.#keys.indexOf(data.default)
281
- }
282
-
283
- }
284
-
285
- setState (index = 0) {
286
- this.#index = index
287
- }
288
-
289
- async drawPhysical (device, id) {
290
- const elem = this.getCurrentElement()
291
- if (!elem || !elem.color) { return }
292
-
293
- const r = parseInt(elem.color.slice(1, 3), 16)
294
- const g = parseInt(elem.color.slice(3, 5), 16)
295
- const b = parseInt(elem.color.slice(5, 7), 16)
296
-
297
- try {
298
- var idx = parseInt(id, 10);
299
-
300
- const val = {
301
- id:idx,
302
- color: `rgba(${r}, ${g}, ${b})`
303
- }
304
- // console.log(' Set Button Color',id, val.id,elem.color, val.color)
305
- device.setButtonColor(val)
306
- } catch (error) {
307
- console.error(' Error', error)
308
- }
309
- }
310
-
311
- async draw (row, column, ctx) {
312
- const x = column * this.width
313
- const y = row * this.height
314
-
315
- const elem = this.getCurrentElement()
316
-
317
- if (elem) {
318
- if (elem.color) {
319
- ctx.fillStyle = elem.color
320
- ctx.fillRect(x, y, this.width, this.height)
321
- }
322
- if (elem.imgBuffer) {
323
- ctx.drawImage(elem.imgBuffer, x, y, this.width, this.height)
324
- }
325
- }
326
- if (this.text){
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)
331
- ctx.font = '20px Verdana'
332
- ctx.textBaseline = 'top';
333
- ctx.textAlign = 'left';
334
- let dynamicText = format(this.text, this.getParams(elem))
335
- ctx.fillText(dynamicText,x+6,y+6)
336
- }
337
- }
338
-
339
- async load (globalConfig) {
340
- this.#profile = globalConfig
341
- for (let i = 0; i < this.#keys.length; i++) {
342
- const key = this.#keys[i]
343
- const elem = this.#states[key]
344
- const file = elem.image
345
- if (file !== undefined && file !== '') {
346
- try {
347
- this.#states[key].imgBuffer = await loadImage(file)
348
- } catch (e) {
349
- console.error('No such image', file)
350
- return false
351
- }
352
- }
353
- }
354
- }
355
-
356
- getCurrentElement () {
357
- const key = this.#keys[this.#index]
358
- return this.#states[key]
359
- }
360
-
361
- getLastElement () {
362
- let i=this.#index-1
363
- if (i < 0) { i = this.#keys.length - 1 }
364
-
365
- const key = this.#keys[i]
366
- return this.#states[key]
367
- }
368
-
369
- getCurrentText () {
370
- return this.text
371
- }
372
-
373
- setIntState (val) {
374
- this.#index = val
375
- }
376
-
377
- pressed () {
378
- this.timeStampPressed = Date.now()
379
-
380
- this.#index++
381
- this.updateState(this.#index,"pressed")
382
- return true
383
- }
384
-
385
- released () {
386
- let elem = this.getCurrentElement()
387
- if (!elem) { return false }
388
- this.timeStampReleased = Date.now()
389
- this.timeHold = this.timeStampReleased - this.timeStampPressed
390
-
391
- if (this.timeHold < this.minPressed) {
392
- // Update the State according to the not correct pressed state
393
- console.log('Did not hold minimum time of ', this.minPressed, 'only', this.timeHold)
394
- this.#index--
395
- if (this.#index < 0) { this.#index = this.#keys.length - 1 }
396
- return false
397
- }
398
-
399
- // Update the State according to the correctly pressed state
400
- switch (this.#type) {
401
- case ButtonType.TOGGLE:
402
- // do nothing
403
- break
404
- default:
405
- this.#index--
406
- if (this.#index < 0) { this.#index = this.#keys.length - 1 }
407
-
408
- break
409
- }
410
-
411
- this.updateState(this.#index,"released")
412
-
413
- return true // this.runCommand()
414
- }
415
-
416
- updateState(index,eventType){
417
- this.#index = index
418
- this.#event = eventType
419
- // Update the State according to the correctly pressed state
420
- if (this.#index < 0) { this.#index = this.#keys.length - 1 }
421
- this.#index %= this.#keys.length
422
- this.runCommand()
423
- //console.log("TODO: expect newState", newState)
424
- return true // this.runCommand()
425
- }
426
-
427
- async rotated (delta) {
428
- if (!this.getCurrentElement()) { return false }
429
-
430
- this.#event = "rotated"
431
- this.#value = calcDelta(this.#value, delta, this.#min, this.#max)
432
- return this.runCommand()
433
- }
434
-
435
- async changed(buttonID,nodeid,val){
436
- // Only handle updates within the same group identified by nodeid
437
- if (nodeid !== this.#nodeid){
438
- return
439
- }
440
-
441
- this.#index = 0;
442
- for (let i = 0; i < this.#keys.length; i++) {
443
- let key = this.#keys[i]
444
- // check if the state-name is same as the value we get from outside:
445
- if (val == key){
446
- this.#index = i;
447
- break;
448
- }
449
-
450
- // check if the nodeid is the same and the value is one of the states
451
- let state = this.#states[key]
452
- if (state.value === undefined)
453
- continue
454
-
455
- const params = {
456
- id: buttonID,
457
- key: buttonID,
458
- state : key,
459
- ...state
460
- }
461
- if (state && state.value !== undefined){
462
- let strVal = state.value.toString()
463
- let val1 = format(strVal,params)
464
- if (val1 === val?.toString()){
465
- this.#index = i;
466
- break;
467
- }
468
- }
469
- //break;
470
- }
471
- }
472
-
473
- async touchmove (x, y) {
474
- // if (!this.getCurrentElement()) { return false }
475
-
476
- if (x > this.#x) {
477
- this.#moveRight = true
478
- this.#moveLeft = false
479
- } else if (x < this.#x) {
480
- this.#moveRight = false
481
- this.#moveLeft = true
482
- }
483
-
484
- if (y > this.#y) {
485
- this.#moveDown = true
486
- this.#moveUp = false
487
- } else if (y < this.#y) {
488
- this.#moveDown = false
489
- this.#moveUp = true
490
- }
491
-
492
- this.#x = x
493
- this.#y = y
494
- // console.log(`d: ${this.#moveDown} r: ${this.#moveRight} `)
495
- return false
496
- }
497
-
498
- getParams(elem){
499
- // Call an action - include dynamic parameters
500
- // and also all attributes of elem + global config
501
- const params = {
502
- text: this.getCurrentText(),
503
- ...this.#profile.parameters,
504
- ...elem,
505
- id: this.id,
506
- key: this.key,
507
- event: this.#event,
508
- pressed : this.#event == "pressed",
509
- released : this.#event == "released",
510
- rotated : this.#event == "rotated",
511
- state: this.#keys[this.#index],
512
- min: this.#min,
513
- max: this.#max,
514
- x: (this.#x %100),
515
- y: (this.#y %100)
516
- }
517
-
518
- if (params.value === undefined)
519
- params.value = this.#value
520
-
521
- return params
522
- }
523
-
524
- async runCommand () {
525
- const elem = this.getCurrentElement()
526
- // Only continue, if we have an element:
527
- if (!elem) {
528
- return
529
- }
530
- // Filter for Event Type:
531
- if (elem.filter && elem.filter != this.#event){
532
- return
533
- }
534
-
535
- if (elem.profile !== undefined) {
536
- profileEmitter.emit("profileChanged", elem.profile)
537
- }
538
-
539
- // Only continue, if we have an element, that contains some kind of command:
540
- if (!elem.cmd && !elem.http && !elem.opcua) {
541
- return
542
- }
543
-
544
- let params = this.getParams(elem)
545
- let res = ''
546
- if ('cmd' in elem) {
547
- if (shellinterface){
548
- res = await shellinterface.call(elem.cmd, params)
549
- }else{
550
- console.warn("shellinterface not started")
551
- }
552
- }
553
- if ('http' in elem) {
554
- if (httpinterface){
555
- res = await httpinterface.call(elem.http, params)
556
- }else{
557
- console.warn("httpinterface not started")
558
- }
559
- }
560
- if ('opcua' in elem) {
561
- if (opcuainterface){
562
- res = await opcuainterface.call(elem.opcua, params)
563
-
564
- if (this.#data.statenodeid){
565
- let stateParams = params
566
- params.value = params.state
567
- res = await opcuainterface.call(this.#data.statenodeid, params)
568
- }
569
- }else{
570
- console.warn("opcuainterface not started")
571
- }
572
- }
573
-
574
- return res
575
- }
576
- }