loupedeck-commander 1.4.1 → 1.4.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.
package/common/button.mjs CHANGED
@@ -1,520 +1,533 @@
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
- #enforcedBlink = false
24
- #event
25
- #keys
26
- #states
27
- // Timestamp when button was pressed
28
- timeStampPressed
29
- // Timestamp when button was released
30
- timeStampReleased
31
- // Time actually hold the button in ms
32
- timeHold
33
- #counter = 0
34
-
35
- constructor(id, width, height, data = {}, key, profile = {}) {
36
- this.#states = {}
37
- this.#keys = []
38
-
39
- this.id = id
40
- this.#index = 0
41
- this.#enforcedIndex = -1
42
- this.#enforcedBlink = false
43
-
44
- this.#profile = profile
45
- this.#params = {
46
- "key": key,
47
- "state": '',
48
- "width": width,
49
- "height": height,
50
- "min": 0,
51
- "max": 100,
52
- "x": 0,
53
- "y": 0,
54
- "color": '#000000',
55
- "text": '',
56
- "textColor": '#ffffff',
57
- "textAlign": 'center',
58
- "textBaseline": 'top',
59
- "font": '16px Arial',
60
- "value": 50,
61
- "minPressed": 25,
62
- "moveLeft": false,
63
- "moveRight": false,
64
- "moveUp": false,
65
- "moveDown": false,
66
- "nodeid": '',
67
- "type": ButtonType.TOGGLE,
68
- "group": key,
69
- "filter": "",
70
- "blink": false,
71
- "vibrate": false,
72
- "profile": "",
73
- "brightness": undefined,
74
- "default": "0"
75
- }
76
-
77
- if (profile.parameters) {
78
- let params = Object.keys(profile.parameters)
79
- for (let i = 0; i < params.length; i++) {
80
- const k = params[i]
81
- this.#params[k] = profile.parameters[k]
82
- }
83
- }
84
- // enforce key, so it's not oveerwritten somewhere else
85
- this.#params.key = key
86
-
87
- // New approach: Use all data from data.params:
88
- if (data.params) {
89
- this.#params = {
90
- ...this.#params,
91
- ...data.params
92
- }
93
-
94
- // Format the params:
95
- this.#params.group = format(this.#params.group, this.getParams({}))
96
- this.#params.nodeid = format(this.#params.nodeid, this.getParams({}))
97
- }
98
-
99
- if (data.states) {
100
- this.#states = data.states
101
- this.#keys = Object.keys(this.#states)
102
-
103
- var defaultIndex = this.#keys.indexOf(this.#params.default)
104
- if (defaultIndex >= 0) {
105
- this.#index = defaultIndex
106
- } else {
107
- this.#params.default = this.#keys[0]
108
- // console.info(` button ${id} default set to ${this.#params.default}, available states`, this.#keys)
109
- }
110
- this.#params.state = this.#keys[this.#index]
111
- }
112
-
113
- // Set the default value for the button:
114
-
115
- // Legacy approach: Use data directly:
116
- if (data.minPressed) {
117
- console.warn("Legacy approach: minPressed is deprecated, use params.minPressed instead")
118
- this.#params.minPressed = data.minPressed
119
- }
120
- if (data.text) {
121
- console.warn("Legacy approach: text is deprecated, use params.text instead")
122
- this.#params.text = data.text
123
- }
124
- if (data.nodeid) {
125
- this.#params.nodeid = format(data.nodeid, this.getParams({}))
126
- console.warn("Legacy approach: nodeid is deprecated, use params.nodeid instead", this.#params.nodeid)
127
- }
128
- if (data.type) {
129
- console.warn("Legacy approach: type is deprecated, use params.type instead")
130
- this.#params.type = data.type
131
- }
132
-
133
- if (data.default) {
134
- console.warn("Legacy approach: default is deprecated, use params.default instead")
135
- this.#index = this.#keys.indexOf(data.default)
136
- }
137
-
138
- if (data.group) {
139
- console.warn("Legacy approach: group is deprecated, use params.group instead")
140
- this.#params.group = format(data.group, this.getParams({}))
141
- }
142
- // End Legacy approach
143
- }
144
-
145
- setState(index = 0) {
146
- this.#index = index
147
- }
148
-
149
- /**
150
- *
151
- * @returns Group attribute of the current button
152
- */
153
- getGroup() {
154
- return this.#params.group
155
- }
156
-
157
- /**
158
- * Draw a physical button (color in RGBA format) on a device
159
- * @param {*} device
160
- * @param {*} id - id of the physical button
161
- * @returns
162
- */
163
- async drawPhysical(device, id) {
164
- const elem = this.getCurrentElement()
165
- if (!elem || !elem.color) { return }
166
-
167
- const r = parseInt(elem.color.slice(1, 3), 16)
168
- const g = parseInt(elem.color.slice(3, 5), 16)
169
- const b = parseInt(elem.color.slice(5, 7), 16)
170
-
171
- try {
172
- var idx = parseInt(id, 10);
173
-
174
- const val = {
175
- id: idx,
176
- color: `rgba(${r}, ${g}, ${b})`
177
- }
178
- // console.log(' Set Button Color',id, val.id,elem.color, val.color)
179
- device.setButtonColor(val)
180
- } catch (error) {
181
- console.error(' Error', error)
182
- }
183
- }
184
-
185
- /**
186
- * Draw the Touch-Screen Elementat the given row and column
187
- * @param {*} row
188
- * @param {*} column
189
- * @param {*} ctx
190
- */
191
- async draw(row, column, ctx) {
192
- // Calculate the x/y position based on the row and column and the width/height properties of the button
193
- const x = column * this.#params.width
194
- const y = row * this.#params.height
195
-
196
- // Modifications in draw:
197
- // - Support color and textColor settings also in params section which
198
- // can be overwriten by button state property. This way color and textColor
199
- // can be defined overall for all states without repeating them in every state.
200
- // - Same for blink property.
201
-
202
- let params = this.getParams(this.getCurrentElement())
203
-
204
- let color = params.color
205
- let textColor = params.textColor
206
-
207
- if (((params.blink === true) || this.#enforcedBlink) && this.#counter % 2 == 0) {
208
- // If blink is set, we switch textcolor and background color:
209
- color = params.textColor
210
- textColor = params.color
211
- }
212
-
213
- // Apply the color as fillStyle:
214
- ctx.fillStyle = color
215
- // Draw the background of the button::
216
- ctx.fillRect(x, y, params.width, params.height)
217
-
218
- // if we have an icon/image draw it on top of the button
219
- if (params.imgBuffer) {
220
- ctx.drawImage(params.imgBuffer, x, y, params.width, params.height)
221
- }
222
- // if we have a text defined, draw it on top:
223
- if (params.text != undefined && params.text != '') {
224
- ctx.fillStyle = textColor
225
- ctx.font = params.font
226
- ctx.textBaseline = params.textBaseline;
227
- ctx.textAlign = params.textAlign;
228
- let dynamicText = format(params.text, params)
229
- let tx = x
230
- let ty = y
231
- // Handle Text Horizontal Alignment
232
- switch (params.textAlign) {
233
- case "center":
234
- tx += this.#params.width / 2;
235
- break;
236
- case "left":
237
- tx += 6;
238
- break;
239
- case "right":
240
- tx += this.#params.width - 6;
241
- break;
242
- }
243
- // Handle Text Vertical Alignment
244
- switch (params.textBaseline) {
245
- case "middle":
246
- ty += this.#params.height / 2;
247
- break;
248
- case "top":
249
- ty += 6;
250
- ctx.textBaseline = "top";
251
- break;
252
- case "bottom":
253
- ty += this.#params.height - 6;
254
- ctx.textBaseline = "bottom";
255
- break;
256
- }
257
- ctx.fillText(dynamicText, x + 6, y + 6)
258
- }
259
-
260
- this.#counter = this.#counter + 1
261
- }
262
-
263
- async load(globalConfig) {
264
- this.#profile = globalConfig
265
- for (let i = 0; i < this.#keys.length; i++) {
266
- const key = this.#keys[i]
267
- const elem = this.#states[key]
268
- const file = elem.image
269
- if (file !== undefined && file !== '') {
270
- try {
271
- this.#states[key].imgBuffer = await loadImage(file)
272
- } catch (e) {
273
- console.error('No such image', file)
274
- return false
275
- }
276
- }
277
- }
278
- }
279
-
280
- getCurrentElement() {
281
- const state = this.#keys[this.#index]
282
- return this.#states[state]
283
- }
284
-
285
- /**
286
- * Triggered when either a physical button, touchscreen button or Knob Button was pressed
287
- * Updates the timeStampPressed and timeHold, and checks if the button was hold long enough (minPressed attribute)
288
- * If the button type was not TOGGLE, it will decrease the index to the previous state.
289
- * @returns
290
- */
291
- pressed() {
292
- this.timeStampPressed = Date.now()
293
- this.updateState(this.#index, "pressed", true)
294
- this.#index++
295
-
296
- if (this.#enforcedIndex >= 0) {
297
- console.log("Enforced Index", this.#enforcedIndex)
298
- this.#index = this.#enforcedIndex
299
- }
300
-
301
- // Update the State according to the correctly pressed state
302
- if (this.#index < 0) { this.#index = this.#keys.length - 1 }
303
- this.#index %= this.#keys.length
304
-
305
- return true
306
- }
307
-
308
- /**
309
- * Triggered when either a physical button, touchscreen button or Knob Button was released
310
- * Updates the timeStampReleased and timeHold, and checks if the button was hold long enough (minPressed attribute)
311
- * If the button type was not TOGGLE, it will decrease the index to the previous state.
312
- * @returns
313
- */
314
- released() {
315
- let elem = this.getCurrentElement()
316
- if (!elem) { return false }
317
- this.timeStampReleased = Date.now()
318
- this.timeHold = this.timeStampReleased - this.timeStampPressed
319
-
320
- let bExecute = true
321
- if (this.timeHold < this.#params.minPressed) {
322
- // Update the State according to the not correct pressed state
323
- console.log('Did not hold minimum time of ', this.#params.minPressed, 'only', this.timeHold)
324
- bExecute = false
325
- }
326
-
327
- // Update the State according to the correctly pressed state
328
- switch (this.#params.type) {
329
- case ButtonType.TOGGLE:
330
- // do nothing
331
- break
332
- default:
333
- this.#index--
334
- break
335
- }
336
-
337
- if (this.#enforcedIndex >= 0 && this.#enforcedIndex != this.#index) {
338
- // If we have an enforced index, we set it to the enforced index:
339
- console.log("Enforcing Index", this.#enforcedIndex, "instead of", this.#index)
340
- this.#index = this.#enforcedIndex
341
- }
342
-
343
- // Update the State according to the correctly pressed state
344
- if (this.#index < 0) { this.#index = this.#keys.length - 1 }
345
- this.#index %= this.#keys.length
346
-
347
-
348
- this.updateState(this.#index, "released", bExecute)
349
-
350
- return true
351
- }
352
-
353
-
354
- updateState(index, eventType, bExecute) {
355
- this.#index = index
356
- this.#event = eventType
357
- if (bExecute)
358
- this.runCommand()
359
- return true
360
- }
361
-
362
- /**
363
- * Triggered when one of the physical KNOBS is rotated
364
- * Calculates the new value based on the delta, and store it in the params.value
365
- * @param {*} delta
366
- * @returns
367
- */
368
- async rotated(delta) {
369
- if (!this.getCurrentElement()) { return false }
370
-
371
- this.#event = "rotated"
372
- this.#params.value = calcDelta(this.#params.value, delta, this.#params.min, this.#params.max)
373
- return this.runCommand()
374
- }
375
-
376
- /**
377
- * This function is called when the value of a button is changed from outside (e.g. coming from an opcua subscription)
378
- * @param {*} buttonID: the ID of the button that caused the change
379
- * @param {*} nodeid: the nodeid that changed
380
- * @param {*} val : the current value of the given nodeid
381
- * @returns
382
- */
383
- async buttonStateChanged(buttonID, attribute, nodeid, val) {
384
-
385
- //
386
- if ((attribute == "nodeid" && nodeid == this.#params.nodeid)) {
387
- this.#index = 0;
388
- this.#enforcedIndex = 0;
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("enforce State index", this.id, nodeid, val, i)
396
- this.#enforcedIndex = i;
397
- //}
398
- break;
399
- }
400
- }
401
- //console.info("Changed index", this.id, nodeid, val, this.#enforcedIndex)
402
- this.#index = this.#enforcedIndex;
403
- //console.info("enforce State index", this.id, nodeid, val, i)
404
- //this.#enforcedIndex = this.#index;
405
- }
406
-
407
-
408
- if (attribute == "blink") {
409
- console.info("Changed blink", buttonID, nodeid, val)
410
- this.#enforcedBlink = val;
411
- }
412
- }
413
-
414
- /**
415
- * Handle a touchmove event on a touchscreen button
416
- * This function calculates the new value based on the x and y coordinates of the touchmove event.
417
- * It updates the moveLeft, moveRight, moveUp and moveDown parameters based on the x and y coordinates.
418
- * It also calculates the delta for the value based on the y coordinate and updates the value parameter.
419
- * @param {*} x
420
- * @param {*} y
421
- * @returns
422
- */
423
- async touchmove(x, y) {
424
-
425
- let delta = 0
426
- if (x > this.#params.x) {
427
- this.#params.moveRight = true
428
- this.#params.moveLeft = false
429
- } else if (x < this.#params.x) {
430
- this.#params.moveRight = false
431
- this.#params.moveLeft = true
432
- }
433
-
434
- if (y > this.#params.y) {
435
- this.#params.moveDown = true
436
- this.#params.moveUp = false
437
- delta = -1
438
- } else if (y < this.#params.y) {
439
- this.#params.moveDown = false
440
- this.#params.moveUp = true
441
- delta = 1
442
- }
443
-
444
- this.#params.x = (x % 100)
445
- this.#params.y = (y % 100)
446
-
447
- // Calculate delta for value no touchmove up/down
448
- this.#params.value = calcDelta(this.#params.value, delta, this.#params.min, this.#params.max)
449
-
450
- // console.log(`d: ${this.#params.moveDown} r: ${this.#params.moveRight} `)
451
- return false
452
- }
453
-
454
- getParams(elem) {
455
- // Call an action - include dynamic parameters
456
- // and also all attributes of elem + global config
457
- const params = {
458
- ...this.#profile.parameters,
459
- ...this.#params,
460
- ...elem,
461
- id: this.id,
462
- //key: this.key,
463
- event: this.#event,
464
- pressed: this.#event == "pressed",
465
- released: this.#event == "released",
466
- rotated: this.#event == "rotated",
467
- }
468
- return params
469
- }
470
-
471
- /**
472
- * Run a command based on the current element's parameters.
473
- * This function checks if the current element has a command, http request or opcua request defined.
474
- * If so, it will execute the command using the respective interface (shell, http or opcua).
475
- * It will also emit profile and brightness changes if defined in the parameters.
476
- * @returns
477
- */
478
- async runCommand() {
479
- //const elem = this.getCurrentElement()
480
- let params = this.getParams(this.getCurrentElement())
481
-
482
- // Only continue, if we have an element:
483
- if (!params) {
484
- return
485
- }
486
- // Filter for Event Type:
487
- if (params.filter && params.filter != this.#event) {
488
- return
489
- }
490
-
491
- if (params.profile !== undefined && params.profile !== '') {
492
- profileEmitter.emit("profileChanged", params.profile)
493
- }
494
-
495
- if (params.brightness !== undefined && params.brightness !== '') {
496
- // If brightness is set, we emit a brightnessChanged event with the given params
497
- // This is used to change the brightness of the device
498
- profileEmitter.emit("brightnessChanged", params.brightness)
499
- }
500
-
501
- if (params.vibrate !== undefined && params.vibrate !== '' && params.vibrate !== false) {
502
- // If vibrate is set, we emit a vibrate event with the given params
503
- // This is used to trigger a vibration on the device
504
- profileEmitter.emit("vibrate", params.vibrate)
505
- }
506
-
507
- let res = ''
508
- if ('cmd' in params && shellinterface) {
509
- res = await shellinterface.call(params.cmd, params)
510
- }
511
- if ('http' in params && httpinterface) {
512
- res = await httpinterface.call(params.http, params)
513
- }
514
- if ('opcua' in params && opcuainterface) {
515
- res = await opcuainterface.call(params.opcua, params)
516
- }
517
-
518
- return res
519
- }
520
- }
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
+ #enforcedBlink = false
24
+ #event
25
+ #keys
26
+ #states
27
+ // Timestamp when button was pressed
28
+ timeStampPressed
29
+ // Timestamp when button was released
30
+ timeStampReleased
31
+ // Time actually hold the button in ms
32
+ timeHold
33
+ #counter = 0
34
+
35
+ constructor(id, width, height, data = {}, key, profile = {}) {
36
+ this.#states = {}
37
+ this.#keys = []
38
+
39
+ this.id = id
40
+ this.#index = 0
41
+ this.#enforcedIndex = -1
42
+ this.#enforcedBlink = false
43
+
44
+ // console.info(` Button ${id.padEnd(10, ' ')} Size: ${width} x ${height}`)
45
+
46
+ this.#profile = profile
47
+ this.#params = {
48
+ "key": key,
49
+ "state": '',
50
+ "width": width,
51
+ "height": height,
52
+ "min": 0,
53
+ "max": 100,
54
+ "x": 0,
55
+ "y": 0,
56
+ "color": '#000000',
57
+ "text": '',
58
+ "textColor": '#ffffff',
59
+ "textAlign": 'center',
60
+ "textBaseline": 'top',
61
+ "font": '16px Arial',
62
+ "value": 50,
63
+ "minPressed": 25,
64
+ "moveLeft": false,
65
+ "moveRight": false,
66
+ "moveUp": false,
67
+ "moveDown": false,
68
+ "nodeid": '',
69
+ "type": ButtonType.TOGGLE,
70
+ "group": key,
71
+ "filter": "",
72
+ "blink": false,
73
+ "vibrate": false,
74
+ "profile": "",
75
+ "brightness": undefined,
76
+ "default": "0"
77
+ }
78
+
79
+ if (profile.parameters) {
80
+ let params = Object.keys(profile.parameters)
81
+ for (let i = 0; i < params.length; i++) {
82
+ const k = params[i]
83
+ this.#params[k] = profile.parameters[k]
84
+ }
85
+ }
86
+ // enforce key, so it's not oveerwritten somewhere else
87
+ this.#params.key = key
88
+
89
+ // New approach: Use all data from data.params:
90
+ if (data.params) {
91
+ this.#params = {
92
+ ...this.#params,
93
+ ...data.params
94
+ }
95
+
96
+ // Format the params:
97
+ this.#params.group = format(this.#params.group, this.getParams({}))
98
+ this.#params.nodeid = format(this.#params.nodeid, this.getParams({}))
99
+ }
100
+
101
+ if (data.states) {
102
+ this.#states = data.states
103
+ this.#keys = Object.keys(this.#states)
104
+
105
+ var defaultIndex = this.#keys.indexOf(this.#params.default)
106
+ if (defaultIndex >= 0) {
107
+ this.#index = defaultIndex
108
+ } else {
109
+ this.#params.default = this.#keys[0]
110
+ // console.info(` button ${id} default set to ${this.#params.default}, available states`, this.#keys)
111
+ }
112
+ this.#params.state = this.#keys[this.#index]
113
+ }
114
+
115
+ // Set the default value for the button:
116
+
117
+ // Legacy approach: Use data directly:
118
+ if (data.minPressed) {
119
+ console.warn("Legacy approach: minPressed is deprecated, use params.minPressed instead")
120
+ this.#params.minPressed = data.minPressed
121
+ }
122
+ if (data.text) {
123
+ console.warn("Legacy approach: text is deprecated, use params.text instead")
124
+ this.#params.text = data.text
125
+ }
126
+ if (data.nodeid) {
127
+ this.#params.nodeid = format(data.nodeid, this.getParams({}))
128
+ console.warn("Legacy approach: nodeid is deprecated, use params.nodeid instead", this.#params.nodeid)
129
+ }
130
+ if (data.type) {
131
+ console.warn("Legacy approach: type is deprecated, use params.type instead")
132
+ this.#params.type = data.type
133
+ }
134
+
135
+ if (data.default) {
136
+ console.warn("Legacy approach: default is deprecated, use params.default instead")
137
+ this.#index = this.#keys.indexOf(data.default)
138
+ }
139
+
140
+ if (data.group) {
141
+ console.warn("Legacy approach: group is deprecated, use params.group instead")
142
+ this.#params.group = format(data.group, this.getParams({}))
143
+ }
144
+ // End Legacy approach
145
+ }
146
+
147
+ setState(index = 0) {
148
+ this.#index = index
149
+ }
150
+
151
+ /**
152
+ *
153
+ * @returns Group attribute of the current button
154
+ */
155
+ getGroup() {
156
+ return this.#params.group
157
+ }
158
+
159
+ /**
160
+ * Draw a physical button (color in RGBA format) on a device
161
+ * @param {*} device
162
+ * @param {*} id - id of the physical button
163
+ * @returns
164
+ */
165
+ async drawPhysical(device, id) {
166
+ const elem = this.getCurrentElement()
167
+ if (!elem || !elem.color) { return }
168
+
169
+ const r = parseInt(elem.color.slice(1, 3), 16)
170
+ const g = parseInt(elem.color.slice(3, 5), 16)
171
+ const b = parseInt(elem.color.slice(5, 7), 16)
172
+
173
+ try {
174
+ var idx = parseInt(id, 10);
175
+
176
+ const val = {
177
+ id: idx,
178
+ color: `rgba(${r}, ${g}, ${b})`
179
+ }
180
+ // console.log(' Set Button Color',id, val.id,elem.color, val.color)
181
+ device.setButtonColor(val)
182
+ } catch (error) {
183
+ console.error(' Error', error)
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Draw the Touch-Screen Elementat the given row and column
189
+ * @param {*} row
190
+ * @param {*} column
191
+ * @param {*} ctx
192
+ */
193
+ async draw(row, column, ctx) {
194
+ let params = this.getParams(this.getCurrentElement())
195
+ let x = 0
196
+ let y = 0
197
+
198
+ // Adjust for non-square buttons:
199
+ if (params.width > params.height) {
200
+ x += (2+column) * (params.width - params.height) / 2
201
+ params.width = params.height
202
+ }
203
+
204
+ // Calculate the x/y position based on the row and column and the width/height properties of the button
205
+ x += column * params.width
206
+ y += row * params.height
207
+
208
+
209
+ // Modifications in draw:
210
+ // - Support color and textColor settings also in params section which
211
+ // can be overwriten by button state property. This way color and textColor
212
+ // can be defined overall for all states without repeating them in every state.
213
+ // - Same for blink property.
214
+
215
+
216
+
217
+ let color = params.color
218
+ let textColor = params.textColor
219
+
220
+ if (((params.blink === true) || this.#enforcedBlink) && this.#counter % 2 == 0) {
221
+ // If blink is set, we switch textcolor and background color:
222
+ color = params.textColor
223
+ textColor = params.color
224
+ }
225
+
226
+ // Apply the color as fillStyle:
227
+ ctx.fillStyle = color
228
+ // Draw the background of the button::
229
+ ctx.fillRect(x, y, params.width, params.height)
230
+
231
+ // if we have an icon/image draw it on top of the button
232
+ if (params.imgBuffer) {
233
+ ctx.drawImage(params.imgBuffer, x, y, params.width, params.height)
234
+ }
235
+ // if we have a text defined, draw it on top:
236
+ if (params.text != undefined && params.text != '') {
237
+ ctx.fillStyle = textColor
238
+ ctx.font = params.font
239
+ ctx.textBaseline = params.textBaseline;
240
+ ctx.textAlign = params.textAlign;
241
+ let dynamicText = format(params.text, params)
242
+ let tx = x
243
+ let ty = y
244
+ // Handle Text Horizontal Alignment
245
+ switch (params.textAlign) {
246
+ case "center":
247
+ tx += this.#params.width / 2;
248
+ break;
249
+ case "left":
250
+ tx += 12;
251
+ break;
252
+ case "right":
253
+ tx += this.#params.width - 12;
254
+ break;
255
+ }
256
+ // Handle Text Vertical Alignment
257
+ switch (params.textBaseline) {
258
+ case "middle":
259
+ ty += this.#params.height / 2;
260
+ break;
261
+ case "top":
262
+ ty += 6;
263
+ ctx.textBaseline = "top";
264
+ break;
265
+ case "bottom":
266
+ ty += this.#params.height - 12;
267
+ ctx.textBaseline = "bottom";
268
+ break;
269
+ }
270
+ ctx.fillText(dynamicText, tx, ty)
271
+ }
272
+
273
+ this.#counter = this.#counter + 1
274
+ }
275
+
276
+ async load(globalConfig) {
277
+ this.#profile = globalConfig
278
+ for (let i = 0; i < this.#keys.length; i++) {
279
+ const key = this.#keys[i]
280
+ const elem = this.#states[key]
281
+ const file = elem.image
282
+ if (file !== undefined && file !== '') {
283
+ try {
284
+ this.#states[key].imgBuffer = await loadImage(file)
285
+ } catch (e) {
286
+ console.error('No such image', file)
287
+ return false
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ getCurrentElement() {
294
+ const state = this.#keys[this.#index]
295
+ return this.#states[state]
296
+ }
297
+
298
+ /**
299
+ * Triggered when either a physical button, touchscreen button or Knob Button was pressed
300
+ * Updates the timeStampPressed and timeHold, and checks if the button was hold long enough (minPressed attribute)
301
+ * If the button type was not TOGGLE, it will decrease the index to the previous state.
302
+ * @returns
303
+ */
304
+ pressed() {
305
+ this.timeStampPressed = Date.now()
306
+ this.updateState(this.#index, "pressed", true)
307
+ this.#index++
308
+
309
+ if (this.#enforcedIndex >= 0) {
310
+ console.log("Enforced Index", this.#enforcedIndex)
311
+ this.#index = this.#enforcedIndex
312
+ }
313
+
314
+ // Update the State according to the correctly pressed state
315
+ if (this.#index < 0) { this.#index = this.#keys.length - 1 }
316
+ this.#index %= this.#keys.length
317
+
318
+ return true
319
+ }
320
+
321
+ /**
322
+ * Triggered when either a physical button, touchscreen button or Knob Button was released
323
+ * Updates the timeStampReleased and timeHold, and checks if the button was hold long enough (minPressed attribute)
324
+ * If the button type was not TOGGLE, it will decrease the index to the previous state.
325
+ * @returns
326
+ */
327
+ released() {
328
+ let elem = this.getCurrentElement()
329
+ if (!elem) { return false }
330
+ this.timeStampReleased = Date.now()
331
+ this.timeHold = this.timeStampReleased - this.timeStampPressed
332
+
333
+ let bExecute = true
334
+ if (this.timeHold < this.#params.minPressed) {
335
+ // Update the State according to the not correct pressed state
336
+ console.log('Did not hold minimum time of ', this.#params.minPressed, 'only', this.timeHold)
337
+ bExecute = false
338
+ }
339
+
340
+ // Update the State according to the correctly pressed state
341
+ switch (this.#params.type) {
342
+ case ButtonType.TOGGLE:
343
+ // do nothing
344
+ break
345
+ default:
346
+ this.#index--
347
+ break
348
+ }
349
+
350
+ if (this.#enforcedIndex >= 0 && this.#enforcedIndex != this.#index) {
351
+ // If we have an enforced index, we set it to the enforced index:
352
+ console.log("Enforcing Index", this.#enforcedIndex, "instead of", this.#index)
353
+ this.#index = this.#enforcedIndex
354
+ }
355
+
356
+ // Update the State according to the correctly pressed state
357
+ if (this.#index < 0) { this.#index = this.#keys.length - 1 }
358
+ this.#index %= this.#keys.length
359
+
360
+
361
+ this.updateState(this.#index, "released", bExecute)
362
+
363
+ return true
364
+ }
365
+
366
+
367
+ updateState(index, eventType, bExecute) {
368
+ this.#index = index
369
+ this.#event = eventType
370
+ if (bExecute)
371
+ this.runCommand()
372
+ return true
373
+ }
374
+
375
+ /**
376
+ * Triggered when one of the physical KNOBS is rotated
377
+ * Calculates the new value based on the delta, and store it in the params.value
378
+ * @param {*} delta
379
+ * @returns
380
+ */
381
+ async rotated(delta) {
382
+ if (!this.getCurrentElement()) { return false }
383
+
384
+ this.#event = "rotated"
385
+ this.#params.value = calcDelta(this.#params.value, delta, this.#params.min, this.#params.max)
386
+ return this.runCommand()
387
+ }
388
+
389
+ /**
390
+ * This function is called when the value of a button is changed from outside (e.g. coming from an opcua subscription)
391
+ * @param {*} buttonID: the ID of the button that caused the change
392
+ * @param {*} nodeid: the nodeid that changed
393
+ * @param {*} val : the current value of the given nodeid
394
+ * @returns
395
+ */
396
+ async buttonStateChanged(buttonID, attribute, nodeid, val) {
397
+
398
+ //
399
+ if ((attribute == "nodeid" && nodeid == this.#params.nodeid)) {
400
+ this.#index = 0;
401
+ this.#enforcedIndex = 0;
402
+ for (let i = 0; i < this.#keys.length; i++) {
403
+ let stateStr = this.#keys[i].toString()
404
+ let valStr = val.toString()
405
+ // check if the state-name is same as the value we get from outside:
406
+ if (valStr == stateStr) {
407
+ //if (i !== this.#index) {
408
+ //console.info("enforce State index", this.id, nodeid, val, i)
409
+ this.#enforcedIndex = i;
410
+ //}
411
+ break;
412
+ }
413
+ }
414
+ //console.info("Changed index", this.id, nodeid, val, this.#enforcedIndex)
415
+ this.#index = this.#enforcedIndex;
416
+ //console.info("enforce State index", this.id, nodeid, val, i)
417
+ //this.#enforcedIndex = this.#index;
418
+ }
419
+
420
+
421
+ if (attribute == "blink") {
422
+ console.info("Changed blink", buttonID, nodeid, val)
423
+ this.#enforcedBlink = val;
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Handle a touchmove event on a touchscreen button
429
+ * This function calculates the new value based on the x and y coordinates of the touchmove event.
430
+ * It updates the moveLeft, moveRight, moveUp and moveDown parameters based on the x and y coordinates.
431
+ * It also calculates the delta for the value based on the y coordinate and updates the value parameter.
432
+ * @param {*} x
433
+ * @param {*} y
434
+ * @returns
435
+ */
436
+ async touchmove(x, y) {
437
+
438
+ let delta = 0
439
+ if (x > this.#params.x) {
440
+ this.#params.moveRight = true
441
+ this.#params.moveLeft = false
442
+ } else if (x < this.#params.x) {
443
+ this.#params.moveRight = false
444
+ this.#params.moveLeft = true
445
+ }
446
+
447
+ if (y > this.#params.y) {
448
+ this.#params.moveDown = true
449
+ this.#params.moveUp = false
450
+ delta = -1
451
+ } else if (y < this.#params.y) {
452
+ this.#params.moveDown = false
453
+ this.#params.moveUp = true
454
+ delta = 1
455
+ }
456
+
457
+ this.#params.x = (x % 100)
458
+ this.#params.y = (y % 100)
459
+
460
+ // Calculate delta for value no touchmove up/down
461
+ this.#params.value = calcDelta(this.#params.value, delta, this.#params.min, this.#params.max)
462
+
463
+ // console.log(`d: ${this.#params.moveDown} r: ${this.#params.moveRight} `)
464
+ return false
465
+ }
466
+
467
+ getParams(elem) {
468
+ // Call an action - include dynamic parameters
469
+ // and also all attributes of elem + global config
470
+ const params = {
471
+ ...this.#profile.parameters,
472
+ ...this.#params,
473
+ ...elem,
474
+ id: this.id,
475
+ //key: this.key,
476
+ event: this.#event,
477
+ pressed: this.#event == "pressed",
478
+ released: this.#event == "released",
479
+ rotated: this.#event == "rotated",
480
+ }
481
+ return params
482
+ }
483
+
484
+ /**
485
+ * Run a command based on the current element's parameters.
486
+ * This function checks if the current element has a command, http request or opcua request defined.
487
+ * If so, it will execute the command using the respective interface (shell, http or opcua).
488
+ * It will also emit profile and brightness changes if defined in the parameters.
489
+ * @returns
490
+ */
491
+ async runCommand() {
492
+ //const elem = this.getCurrentElement()
493
+ let params = this.getParams(this.getCurrentElement())
494
+
495
+ // Only continue, if we have an element:
496
+ if (!params) {
497
+ return
498
+ }
499
+ // Filter for Event Type:
500
+ if (params.filter && params.filter != this.#event) {
501
+ return
502
+ }
503
+
504
+ if (params.profile !== undefined && params.profile !== '') {
505
+ profileEmitter.emit("profileChanged", params.profile)
506
+ }
507
+
508
+ if (params.brightness !== undefined && params.brightness !== '') {
509
+ // If brightness is set, we emit a brightnessChanged event with the given params
510
+ // This is used to change the brightness of the device
511
+ profileEmitter.emit("brightnessChanged", params.brightness)
512
+ }
513
+
514
+ if (params.vibrate !== undefined && params.vibrate !== '' && params.vibrate !== false) {
515
+ // If vibrate is set, we emit a vibrate event with the given params
516
+ // This is used to trigger a vibration on the device
517
+ profileEmitter.emit("vibrate", params.vibrate)
518
+ }
519
+
520
+ let res = ''
521
+ if ('cmd' in params && shellinterface) {
522
+ res = await shellinterface.call(params.cmd, params)
523
+ }
524
+ if ('http' in params && httpinterface) {
525
+ res = await httpinterface.call(params.http, params)
526
+ }
527
+ if ('opcua' in params && opcuainterface) {
528
+ res = await opcuainterface.call(params.opcua, params)
529
+ }
530
+
531
+ return res
532
+ }
533
+ }