loupedeck-commander 1.0.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.
Files changed (45) hide show
  1. package/ExampleDeviceHandler.mjs +45 -0
  2. package/README.md +51 -0
  3. package/common/ApplicationConfig.mjs +81 -0
  4. package/common/BaseLoupeDeckHandler.mjs +189 -0
  5. package/common/cmd-executer.mjs +16 -0
  6. package/common/index.mjs +5 -0
  7. package/common/touchbuttons.mjs +473 -0
  8. package/common/utils.mjs +29 -0
  9. package/config.json +9 -0
  10. package/example.mjs +13 -0
  11. package/icons/A-G_n.png +0 -0
  12. package/icons/A-G_p.png +0 -0
  13. package/icons/A-H_n.png +0 -0
  14. package/icons/A-H_p.png +0 -0
  15. package/icons/Audio_Plug_n.png +0 -0
  16. package/icons/Audio_Plug_p.png +0 -0
  17. package/icons/Comp_A_n.png +0 -0
  18. package/icons/Comp_A_p.png +0 -0
  19. package/icons/Comp_B_n.png +0 -0
  20. package/icons/Comp_B_p.png +0 -0
  21. package/icons/Comp_C_n.png +0 -0
  22. package/icons/Comp_C_p.png +0 -0
  23. package/icons/Comp_D_n.png +0 -0
  24. package/icons/Comp_D_p.png +0 -0
  25. package/icons/Comp_E_n.png +0 -0
  26. package/icons/Comp_E_p.png +0 -0
  27. package/icons/Comp_F_n.png +0 -0
  28. package/icons/Comp_F_p.png +0 -0
  29. package/icons/Comp_G_n.png +0 -0
  30. package/icons/Comp_G_p.png +0 -0
  31. package/icons/Comp_H_n.png +0 -0
  32. package/icons/Comp_H_p.png +0 -0
  33. package/icons/Mic_n.png +0 -0
  34. package/icons/Mic_p.png +0 -0
  35. package/icons/Sound_VolHigh.png +0 -0
  36. package/icons/Sound_VolLow.png +0 -0
  37. package/icons/Sound_VolMid.png +0 -0
  38. package/icons/Sound_Voldown.png +0 -0
  39. package/icons/Sound_Volup.png +0 -0
  40. package/icons/Sound_off.png +0 -0
  41. package/icons/cia.png +0 -0
  42. package/icons/saph.png +0 -0
  43. package/index.mjs +3 -0
  44. package/package.json +31 -0
  45. package/profile-1.json +159 -0
@@ -0,0 +1,45 @@
1
+ import pkg from 'loupedeck'
2
+ import { BaseLoupeDeckHandler } from './common/BaseLoupeDeckHandler.mjs'
3
+ const { HAPTIC } = pkg
4
+
5
+ /**
6
+ * Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
7
+ */
8
+ export class ExampleDeviceHandler extends BaseLoupeDeckHandler {
9
+ /**
10
+ * Handle different Vibration-Feedback on OK (true), and NOK (false)
11
+ * @param {*} ok
12
+ */
13
+ async vibrateHandler (ok) {
14
+ if (ok) { await this.device.vibrate(HAPTIC.REV_FASTEST) } else {
15
+ this.device.vibrate(HAPTIC.RUMBLE2)
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Handle Button-Up Events with Vibration Handler
21
+ * @param {*} event
22
+ */
23
+ async onButtonUp (event) {
24
+ const res = await super.onButtonUp(event)
25
+ await this.vibrateHandler(res)
26
+ }
27
+
28
+ /**
29
+ * Handle Knob-Rotation Events with Vibration Handler
30
+ * @param {*} event
31
+ */
32
+ async onRotate (event) {
33
+ const res = await super.onRotate(event)
34
+ await this.vibrateHandler(res)
35
+ }
36
+
37
+ /**
38
+ * Handle Touch-End Events with Vibration Handler
39
+ * @param {*} event
40
+ */
41
+ async onTouchEnd (event) {
42
+ const res = await super.onTouchEnd(event)
43
+ await this.vibrateHandler(res)
44
+ }
45
+ }
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # loupedeck-commander
2
+
3
+ ## Getting started
4
+
5
+ The loupedeck-commander is helping you using your Loupedeck device more easily, and connect it with custom commands you define with script files.
6
+
7
+ Features:
8
+ - Reference image files for every state of your button in the touch-display fields
9
+ - Connect commands to your state transitions
10
+
11
+ ## Basic Usage
12
+
13
+ Do the following steps:
14
+
15
+ ```
16
+ # init your new node package
17
+ npm init
18
+
19
+ # install the loupedeck-commander dependency
20
+ npm install -s loupedeck-commander
21
+ ```
22
+
23
+ Create a new configuration file with at least one profile or copy from here,
24
+ and replace the image references with your own icons (90x90px size):
25
+
26
+ - [config.json](https://gitlab.com/keckxde/loupedeck-commander/-/blob/main/config.json)
27
+ - [profile-1.json](https://gitlab.com/keckxde/loupedeck-commander/-/blob/main/profile-1.json)
28
+
29
+ Create a `index.mjs` file to open up your loupedeck connection:
30
+ ```javascript
31
+ # save as index.mjs
32
+ import { BaseLoupeDeckHandler } from 'loupedeck-commander'
33
+
34
+ const handler = new BaseLoupeDeckHandler('config.json')
35
+
36
+ const stopHandler = () => {
37
+ console.log('Receiving SIGINT => Stopping processes.')
38
+ handler.stop()
39
+ }
40
+
41
+ // Initiating a process
42
+ process.on('SIGINT', stopHandler)
43
+
44
+ await handler.start()
45
+ ```
46
+
47
+ Run the script using the following command:
48
+
49
+ ```bash
50
+ node index.mjs
51
+ ```
@@ -0,0 +1,81 @@
1
+ import { readJSONFile, writeJSONFile } from './utils.mjs'
2
+
3
+ export class ApplicationConfig {
4
+ application = 'undefined'
5
+ profiles = []
6
+
7
+ loadFromFile (fileName) {
8
+ console.info(`ApplicationConfig: Loading Config File ${fileName}`)
9
+ const config = readJSONFile(fileName)
10
+
11
+ this.application = config.application
12
+ for (let i = 0; i < config.profiles.length; i++) {
13
+ const profile = new Profile(config.profiles[i])
14
+ this.profiles.push(profile)
15
+ }
16
+ }
17
+ }
18
+
19
+ class Profile {
20
+ name = ''
21
+ file = ''
22
+ description = ''
23
+
24
+ touch = {}
25
+ knobs = {}
26
+ buttons = {}
27
+ #file = ''
28
+ #loaded = false
29
+ #error = false
30
+
31
+ constructor (data) {
32
+ this.name = data.name
33
+ this.#file = data.file
34
+ this.loadFromFile(this.#file)
35
+ if (this.#error) { this.saveToFile(`profile-${this.name}-sav.json`) }
36
+ }
37
+
38
+ loadFromFile (fileName) {
39
+ console.info(`ProfileConfig: Loading Config File ${fileName}`)
40
+ const config = readJSONFile(fileName)
41
+ this.#error = (config === undefined)
42
+ this.#loaded = !this.#error
43
+ if (!this.#error && this.#loaded) {
44
+ this.profile = config.profile
45
+ this.description = config.description
46
+
47
+ // Load the Configurations for Touch-Displays
48
+ this.touch = new TouchConfig(config.touch)
49
+ // Load the Configurations for Button-Areas
50
+ this.buttons = config.buttons
51
+ }
52
+ }
53
+
54
+ saveToFile (fileName) {
55
+ fileName = fileName.toLowerCase()
56
+
57
+ console.info(`ProfileConfig: Save To Config File ${fileName}`)
58
+ writeJSONFile(fileName, this)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Class that handles the different configs for the touch displays of the device covering Loupedeck Live (without knob) and also CT (with knob)
64
+ */
65
+ class TouchConfig {
66
+ center = {} // CENTER Display Config - Available in CT & LIVE
67
+ left = {} // LEFT Display Config - Available in CT & LIVE
68
+ right = {} // RIGHT Display Config - Available in CT & LIVE
69
+ knob = {} // KNOB Display Config - Available only in CT
70
+
71
+ constructor (data) {
72
+ this.loadFromJSON(data)
73
+ }
74
+
75
+ loadFromJSON (data) {
76
+ this.center = data.center
77
+ this.left = data.left
78
+ this.right = data.right
79
+ this.knob = data.knob
80
+ }
81
+ }
@@ -0,0 +1,189 @@
1
+ import pkg from 'loupedeck'
2
+ import { ApplicationConfig } from './ApplicationConfig.mjs'
3
+ import { TouchButtonField, PhysicalButtonField } from './touchbuttons.mjs'
4
+ const { discover, HAPTIC } = pkg
5
+
6
+ export class BaseLoupeDeckHandler {
7
+ device = undefined
8
+ appConfig = undefined
9
+ profileConfigs = []
10
+ currentProfile = 0
11
+ screens = {}
12
+ buttons = {}
13
+ screenUpdate = {}
14
+
15
+ touchButtons = undefined
16
+
17
+ constructor (config) {
18
+ console.log(`INIT with config ${config}`)
19
+ this.loadConfig(config)
20
+ }
21
+
22
+ async start () {
23
+ console.info('Start')
24
+ while (!this.device) {
25
+ try {
26
+ this.device = await discover()
27
+ } catch (e) {
28
+ console.error(`${e}. Reattempting in 3 seconds...`)
29
+ await new Promise(res => setTimeout(res, 3000))
30
+ }
31
+ }
32
+ console.info(`✅ Connected to ${this.device.type}`)
33
+
34
+ const self = this
35
+
36
+ this.device.on('connect', (address) => { self.onConnected(address) })
37
+ this.device.on('down', (event) => { self.onButtonDown(event) })
38
+ this.device.on('up', (event) => { self.onButtonUp(event) })
39
+ this.device.on('rotate', (event) => { self.onRotate(event) })
40
+ this.device.on('touchstart', (event) => { self.onTouchStart(event) })
41
+ this.device.on('touchmove', (event) => { self.onTouchMove(event) })
42
+ this.device.on('touchend', (event) => { self.onTouchEnd(event) })
43
+
44
+ // return await this.device.connect()
45
+ }
46
+
47
+ stop () {
48
+ console.info('Stopping Handler')
49
+ this.device.close()
50
+ }
51
+
52
+ loadConfig (fileName) {
53
+ console.info(`Loading Config File ${fileName}`)
54
+ this.appConfig = new ApplicationConfig()
55
+ this.appConfig.loadFromFile(fileName)
56
+ }
57
+
58
+ async activateProfile (id) {
59
+ // todo Profile-change implementation
60
+ if (this.appConfig.profiles.length >= id) {
61
+ this.currentProfile = id
62
+ }
63
+ await this.buttons.draw(this.device)
64
+ }
65
+
66
+ getCurrentProfile () {
67
+ return this.appConfig.profiles[this.currentProfile]
68
+ }
69
+
70
+ // Events:
71
+ async onConnected (address) {
72
+ console.info('Connected: ', address)
73
+ const dLeft = this.device.displays.left
74
+ const dRight = this.device.displays.right
75
+ const dCenter = this.device.displays.center
76
+ const dKnob = this.device.displays.knob
77
+
78
+ const profile = this.getCurrentProfile()
79
+ this.screens.center = new TouchButtonField('center', this.device.rows, this.device.columns, dCenter.width, dCenter.height, profile.touch.center)
80
+ this.screens.left = new TouchButtonField('left', 1, 1, dLeft.width, dLeft.height, profile.touch.left)
81
+ this.screens.right = new TouchButtonField('right', 1, 1, dRight.width, dRight.height, profile.touch.right)
82
+ // knob Display is only available in the CT-version - not with live:
83
+ if (dKnob) {
84
+ this.screens.knob = new TouchButtonField('knob', 1, 1, dKnob.width, dKnob.height, profile.touch.knob)
85
+ }
86
+
87
+ this.buttons = new PhysicalButtonField('bottom', profile.buttons)
88
+
89
+ await this.screens.center.load()
90
+ await this.screens.right.load()
91
+ await this.screens.left.load()
92
+ if (dKnob) {
93
+ await this.screens.knob.load()
94
+ }
95
+ await this.buttons.load()
96
+
97
+ await this.screens.center.draw(this.device)
98
+ if (dKnob) {
99
+ await this.screens.knob.draw(this.device)
100
+ }
101
+ await this.activateProfile(0)
102
+ await this.buttons.draw(this.device)
103
+
104
+ this.device.setBrightness(1)
105
+ this.device.vibrate(HAPTIC.ASCEND_MED)
106
+
107
+ console.info('Done initializing')
108
+ }
109
+
110
+ async updateScreens () {
111
+ const keys = Object.keys(this.screenUpdate)
112
+ for (let i = 0; i < keys.length; i++) {
113
+ const screen = keys[i]
114
+ await this.screens[screen].draw(this.device)
115
+ }
116
+ this.screenUpdate = {}
117
+ }
118
+
119
+ async onButtonDown (event) {
120
+ let ok = false
121
+ const id = event.id
122
+ ok = await this.buttons.pressed(id)
123
+ await this.buttons.draw(this.device)
124
+ return ok
125
+ }
126
+
127
+ async onButtonUp (event) {
128
+ let ok = false
129
+ const id = event.id
130
+ ok = await this.buttons.released(id)
131
+ await this.buttons.draw(this.device)
132
+ return ok
133
+ }
134
+
135
+ async onRotate (event) {
136
+ const id = event.id
137
+ const delta = event.delta
138
+ return await this.buttons.rotated(id, delta)
139
+ }
140
+
141
+ async onTouchStart (event) {
142
+ let ok = false
143
+ const changedTouches = event.changedTouches
144
+ for (let i = 0; i < changedTouches.length; i++) {
145
+ const screen = changedTouches[i].target.screen
146
+ let id = changedTouches[i].target.key
147
+ this.screenUpdate[screen] = true
148
+ if (id === undefined) {
149
+ id = 0
150
+ }
151
+ ok = await this.screens[screen].pressed(id)
152
+ }
153
+ await this.updateScreens()
154
+ return ok
155
+ }
156
+
157
+ async onTouchMove (event) {
158
+ const ok = false
159
+ const changedTouches = event.changedTouches
160
+ for (let i = 0; i < changedTouches.length; i++) {
161
+ const x = changedTouches[i].x
162
+ const y = changedTouches[i].y
163
+ const screen = changedTouches[i].target.screen
164
+ let id = changedTouches[i].target.key
165
+ if (id === undefined) {
166
+ id = 0 // for left/right
167
+ }
168
+ console.info('TouchMove : ', screen, id, x, y)
169
+ }
170
+ return ok
171
+ }
172
+
173
+ async onTouchEnd (event) {
174
+ let ok = false
175
+ const changedTouches = event.changedTouches
176
+ for (let i = 0; i < changedTouches.length; i++) {
177
+ const elem = changedTouches[i].target
178
+ let id = changedTouches[i].target.key
179
+ if (id === undefined) {
180
+ id = 0
181
+ }
182
+ this.screenUpdate[elem.screen] = true
183
+
184
+ ok = await this.screens[elem.screen].released(id)
185
+ }
186
+ await this.updateScreens()
187
+ return ok
188
+ }
189
+ }
@@ -0,0 +1,16 @@
1
+ import { exec } from 'child_process'
2
+
3
+ export async function sh (cmd) {
4
+ // console.log('TRIGGER -> ', cmd)
5
+
6
+ return new Promise(function (resolve, reject) {
7
+ exec(cmd, (err, stdout, stderr) => {
8
+ console.log('executed cmd, stdout:', stdout)
9
+ if (err) {
10
+ reject(err)
11
+ } else {
12
+ resolve({ stdout, stderr })
13
+ }
14
+ })
15
+ })
16
+ }
@@ -0,0 +1,5 @@
1
+ import { BaseLoupeDeckHandler } from './BaseLoupeDeckHandler.mjs'
2
+
3
+ module.exports = {
4
+ BaseLoupeDeckHandler
5
+ }
@@ -0,0 +1,473 @@
1
+ import { loadImage } from 'canvas'
2
+ import { sh } from './cmd-executer.mjs'
3
+ import format from 'string-template'
4
+ import { calcDelta } from './utils.mjs'
5
+
6
+ const ButtonStyle = {
7
+ NONE: 0,
8
+ IMAGE: 1,
9
+ COLOR: 2,
10
+ TEXT: 3,
11
+ PHYSICAL: 3
12
+ }
13
+
14
+ export const ButtonIndex = {
15
+ BUTN_0: 0,
16
+ BUTN_1: 1,
17
+ BUTN_2: 2,
18
+ BUTN_3: 3,
19
+ BUTN_4: 3,
20
+ BUTN_5: 5,
21
+ BUTN_6: 6,
22
+ BUTN_7: 7,
23
+ home: 'home'
24
+ }
25
+
26
+ const ButtonType = {
27
+ NONE: '',
28
+ TOGGLE: 'TOGGLE',
29
+ PUSH: 'PUSH'
30
+ }
31
+
32
+ export class TouchButtonField {
33
+ #buttons = []
34
+ width = 0
35
+ height = 0
36
+ #rows = 0
37
+ #columns = 0
38
+ #name
39
+ constructor (name, rows, columns, width, height, data) {
40
+ console.info(`TouchButtonField ${name.padEnd(10, ' ')} Buttons: ${rows} x ${columns} , Pixels ${width} x ${height}`)
41
+ this.#name = name
42
+ this.width = width
43
+ this.height = height
44
+ this.#rows = rows
45
+ this.#columns = columns
46
+ for (let i = 0; i < rows * columns; i++) {
47
+ const tb = new TouchButton(`touch-${i}`, width / columns, height / rows, data[i])
48
+ this.#buttons.push(tb)
49
+ }
50
+ }
51
+
52
+ async draw (device) {
53
+ device.drawScreen(this.#name, ctx => {
54
+ for (let i = 0; i < this.#rows * this.#columns; i++) {
55
+ const row = Math.floor(i / device.columns)
56
+ const column = i % device.columns
57
+
58
+ this.#buttons[i].draw(row, column, ctx)
59
+ }
60
+ })
61
+ }
62
+
63
+ setIntState (id, val) {
64
+ this.#buttons[id].setIntState(val)
65
+ }
66
+
67
+ async load () {
68
+ for (let i = 0; i < this.#rows * this.#columns; i++) {
69
+ await this.#buttons[i].load()
70
+ }
71
+ }
72
+
73
+ async pressed (id) {
74
+ // console.info(`pressed ${id}`)
75
+ await this.#buttons[id].pressed()
76
+ }
77
+
78
+ async released (id) {
79
+ // console.info(`released ${id}`)
80
+ const result = await this.#buttons[id].released()
81
+ if (result) {
82
+ // disable all other buttons of the group, if this one had been activated:
83
+ for (let i = 0; i < this.#rows * this.#columns; i++) {
84
+ if (id === i) { continue }
85
+ if (this.#buttons[i].group === this.#buttons[id].group) {
86
+ this.#buttons[i].setState(0)
87
+ }
88
+ }
89
+ }
90
+ return result
91
+ }
92
+ }
93
+
94
+ export class PhysicalButtonField {
95
+ #buttons = {}
96
+ #name
97
+ constructor (name, data) {
98
+ console.info(`PhysicalButtonField ${name.padEnd(10, ' ')} `)
99
+ this.#name = name
100
+ const keys = Object.keys(data)
101
+ for (let i = 0; i < keys.length; i++) {
102
+ const key = keys[i]
103
+ const tb = new TouchButton(key, 1, 1, data[key])
104
+ this.#buttons[key] = tb
105
+ }
106
+ }
107
+
108
+ async draw (device) {
109
+ const keys = Object.keys(this.#buttons)
110
+ for (let i = 0; i < keys.length; i++) {
111
+ const key = keys[i]
112
+ this.#buttons[key].drawPhysical(device, key)
113
+ }
114
+ }
115
+
116
+ async load () {
117
+ const keys = Object.keys(this.#buttons)
118
+ for (let i = 0; i < keys.length; i++) {
119
+ const key = keys[i]
120
+ await this.#buttons[key].load()
121
+ }
122
+ }
123
+
124
+ checkAndCreateButton (id) {
125
+ if (!(id in this.#buttons)) {
126
+ const tb = new TouchButton(id, 1, 1, id)
127
+ this.#buttons[id] = tb
128
+ }
129
+ }
130
+
131
+ setIntState (id, val) {
132
+ this.checkAndCreateButton(id)
133
+ this.#buttons[id].setIntState(val)
134
+ }
135
+
136
+ async pressed (id) {
137
+ // console.info(`pressed ${id}`)
138
+ this.checkAndCreateButton(id)
139
+ return await this.#buttons[id].pressed()
140
+ }
141
+
142
+ async released (id) {
143
+ // console.info(`released ${id}`)
144
+ this.checkAndCreateButton(id)
145
+ return await this.#buttons[id].released()
146
+ }
147
+
148
+ async rotated (id, delta) {
149
+ // console.info(`rotated ${id} ${delta}`)
150
+ this.checkAndCreateButton(id)
151
+ return await this.#buttons[id].rotated(delta)
152
+ }
153
+ }
154
+
155
+ export class TouchButton {
156
+ width = 0
157
+ height = 0
158
+ #style
159
+ #type
160
+ #index = 0
161
+ #states = []
162
+ #min = 0
163
+ #max = 100
164
+ #value = 50
165
+ #data
166
+ #name = undefined
167
+
168
+ #images = {}
169
+ #colors = {
170
+ off: '#000000'
171
+ // on: '#110011'
172
+ }
173
+
174
+ #rgbs = {
175
+ off: [0, 0, 0]
176
+ // on: [0, 255, 0]
177
+ }
178
+
179
+ #commands = {
180
+ off: '',
181
+ on: ''
182
+ }
183
+
184
+ group = ''
185
+
186
+ text = ''
187
+ font = '16px Arial'
188
+
189
+ // Timestamp when button was pressed
190
+ timeStampPressed
191
+ // Timestamp when button was released
192
+ timeStampReleased
193
+ // Time actually hold the button in ms
194
+ timeHold
195
+ // Minimum ammount of time in ms to press a button:
196
+ minPressed = 25
197
+
198
+ constructor (id, width, height, data) {
199
+ this.id = id
200
+ this.width = width
201
+ this.height = height
202
+ this.#index = 0
203
+ this.#style = ButtonStyle.NONE
204
+ this.#type = ButtonType.TOGGLE
205
+ this.#states = Object.keys(this.#rgbs)
206
+
207
+ if (data) {
208
+ this.group = data.group
209
+ this.#style = ButtonStyle.PHYSICAL
210
+ if (data.images && Object.keys(data.images).length > 0) {
211
+ this.#style = ButtonStyle.IMAGE
212
+ this.#states = Object.keys(data.images)
213
+ } else if (data.colors && Object.keys(data.colors).length > 0) {
214
+ this.#style = ButtonStyle.COLOR
215
+ this.#states = Object.keys(data.colors)
216
+ } else if (data.rgb && Object.keys(data.rgb).length > 0) {
217
+ this.#states = Object.keys(data.rgb)
218
+ }
219
+ if (data.commands) {
220
+ this.#commands = data.commands
221
+ }
222
+
223
+ if (data.type) {
224
+ this.#type = data.type.toUpperCase()
225
+ }
226
+ if (data.minPressed) {
227
+ this.minPressed = data.minPressed
228
+ }
229
+
230
+ if (data.text) {
231
+ this.text = data.text
232
+ }
233
+ this.#data = data
234
+ }
235
+ }
236
+
237
+ setButtonStyle (type) {
238
+ this.#style = type
239
+ }
240
+
241
+ setState (index = 0) {
242
+ this.#index = index
243
+ }
244
+
245
+ async drawPhysical (device, id) {
246
+ try {
247
+ const val = {
248
+ id,
249
+ color: this.getCurrentRGB()
250
+ }
251
+ device.setButtonColor(val)
252
+ } catch (error) {
253
+ // console.log(' Error', error)
254
+ }
255
+ }
256
+
257
+ async draw (row, column, ctx) {
258
+ const x = column * this.width
259
+ const y = row * this.height
260
+ switch (this.#style) {
261
+ case ButtonStyle.IMAGE:
262
+ ctx.drawImage(this.getCurrentStateImage(), x, y, this.width, this.height)
263
+ break
264
+ case ButtonStyle.COLOR:
265
+ // Draw a Colored Rectangle
266
+ ctx.fillStyle = this.getCurrentStateFill()
267
+ ctx.fillRect(x, y, this.width, this.height)
268
+ break
269
+ case ButtonStyle.NONE:
270
+ // Draw a Colored Rectangle
271
+ ctx.fillStyle = this.getCurrentStateFill(0)
272
+ ctx.fillRect(x, y, this.width, this.height)
273
+ break
274
+ }
275
+
276
+ ctx.fillStyle = '#000000'
277
+ ctx.font = this.font
278
+ ctx.fillText(this.text, x + 10, y - 10)
279
+ }
280
+
281
+ async load () {
282
+ let bLoaded = false
283
+ while (!bLoaded) {
284
+ switch (this.#style) {
285
+ case ButtonStyle.IMAGE:
286
+ bLoaded = await this.loadAsImage()
287
+ break
288
+ case ButtonStyle.COLOR:
289
+ bLoaded = await this.loadAsColor()
290
+ break
291
+ case ButtonStyle.PHYSICAL:
292
+ bLoaded = await this.loadAsRGB()
293
+ break
294
+ default:
295
+ bLoaded = true
296
+ }
297
+ }
298
+ }
299
+
300
+ async loadAsImage () {
301
+ this.#states = Object.keys(this.#data.images)
302
+ if (this.#states.length <= 0) {
303
+ this.#style = ButtonStyle.COLOR
304
+ return false
305
+ }
306
+
307
+ for (let i = 0; i < this.#states.length; i++) {
308
+ const key = this.#states[i]
309
+ const file = this.#data.images[key]
310
+ if (file !== undefined && file !== '') {
311
+ try {
312
+ this.#images[key] = await loadImage(file)
313
+ } catch (e) {
314
+ console.error('No such image', file)
315
+ this.#style = ButtonStyle.COLOR
316
+ delete this.#images[key]
317
+ return false
318
+ }
319
+ } else {
320
+ // this is not an image type
321
+ }
322
+ }
323
+ return true
324
+ }
325
+
326
+ async loadAsColor () {
327
+ if (this.#states.length <= 0) {
328
+ // use default colors
329
+ this.#states = Object.keys(this.#colors)
330
+ return true
331
+ }
332
+
333
+ for (let i = 0; i < this.#states.length; i++) {
334
+ const key = this.#states[i]
335
+ if (this.#data.colors && key in this.#data.colors) {
336
+ const color = this.#data.colors[key]
337
+ if (color !== undefined && color !== '') {
338
+ this.#colors[key] = color
339
+ }
340
+ }
341
+ }
342
+ return true
343
+ }
344
+
345
+ async loadAsRGB () {
346
+ if (this.#states.length <= 0) {
347
+ // use default colors
348
+ this.#states = Object.keys(this.#colors)
349
+ return true
350
+ }
351
+
352
+ if (this.#data.rgb) {
353
+ const keys = Object.keys(this.#data.rgb)
354
+ for (let i = 0; i < keys.length; i++) {
355
+ const key = this.#states[i]
356
+ const rgb = this.#data.rgb[key]
357
+ if (rgb !== undefined && rgb !== '') {
358
+ this.#rgbs[key] = rgb
359
+ }
360
+ }
361
+ }
362
+
363
+ return true
364
+ }
365
+
366
+ getCurrentStateImage () {
367
+ const key = this.#states[this.#index]
368
+ return this.#images[key]
369
+ }
370
+
371
+ getCurrentStateFill (forceIndex = -1) {
372
+ let key
373
+ if (forceIndex >= 0) {
374
+ key = this.#states[forceIndex]
375
+ } else {
376
+ key = this.#states[this.#index]
377
+ }
378
+ return this.#colors[key]
379
+ }
380
+
381
+ getCurrentRGB () {
382
+ const key = this.#states[this.#index]
383
+ const rgb = this.#rgbs[key]
384
+
385
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`
386
+ }
387
+
388
+ getCurrentText () {
389
+ return this.text
390
+ }
391
+
392
+ getCurrentCommand () {
393
+ const key = this.#states[this.#index]
394
+ const cmd = this.#commands[key]
395
+
396
+ if (!cmd) { return '' }
397
+ // Call an action
398
+ const params = {
399
+ id: this.id,
400
+ state: key,
401
+ min: this.#min,
402
+ max: this.#max,
403
+ value: this.#value,
404
+ text: this.getCurrentText()
405
+ }
406
+ /* const keys = Object.keys(this.#data.config)
407
+ keys.forEach(key => {
408
+ params[key] = this.#data.config[key]
409
+ }) */
410
+
411
+ return format(cmd, params)
412
+ }
413
+
414
+ setIntState (val) {
415
+ this.#index = val
416
+ }
417
+
418
+ pressed () {
419
+ this.timeStampPressed = Date.now()
420
+
421
+ switch (this.#style) {
422
+ case ButtonStyle.NONE:
423
+ break
424
+ default:
425
+ this.#index++
426
+ this.#index %= this.#states.length
427
+ }
428
+ return true
429
+ }
430
+
431
+ released () {
432
+ this.timeStampReleased = Date.now()
433
+ this.timeHold = this.timeStampReleased - this.timeStampPressed
434
+
435
+ if (this.#style === ButtonStyle.NONE) { return }
436
+
437
+ if (this.timeHold < this.minPressed) {
438
+ // Update the State according to the not correct pressed state
439
+ console.log('Did not hold minimum time of ', this.minPressed, 'only', this.timeHold)
440
+ this.#index--
441
+ if (this.#index < 0) { this.#index = this.#states.length - 1 }
442
+ return false
443
+ }
444
+
445
+ // Update the State according to the correctly pressed state
446
+ switch (this.#type) {
447
+ case ButtonType.TOGGLE:
448
+ // do nothing
449
+ break
450
+ default:
451
+ this.#index--
452
+ if (this.#index < 0) { this.#index = this.#states.length - 1 }
453
+
454
+ break
455
+ }
456
+ this.runCommand()
457
+ return true // this.runCommand()
458
+ }
459
+
460
+ async rotated (delta) {
461
+ this.#value = calcDelta(this.#value, delta, this.#max)
462
+ return this.runCommand()
463
+ }
464
+
465
+ runCommand () {
466
+ const cmd = this.getCurrentCommand()
467
+ if (cmd && cmd !== '') {
468
+ sh(cmd)
469
+ return true
470
+ }
471
+ return false
472
+ }
473
+ }
@@ -0,0 +1,29 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+
3
+ /**
4
+ * Read a JSON File
5
+ */
6
+ export function readJSONFile (fileName) {
7
+ let data
8
+ try {
9
+ data = readFileSync(fileName, 'utf8')
10
+ return JSON.parse(data)
11
+ } catch (error) {
12
+ return data
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Write a JSON File
18
+ */
19
+ export function writeJSONFile (fileName, jsonObj) {
20
+ const data = JSON.stringify(jsonObj, null, 4)
21
+ writeFileSync(fileName, data)
22
+ }
23
+
24
+ export function calcDelta (data, delta, max = 100) {
25
+ data = data + delta
26
+ if (data > max) { data = max }
27
+ if (data < 0) { data = 0 }
28
+ return data
29
+ }
package/config.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "application": "Example",
3
+ "profiles": [
4
+ {
5
+ "name": "profile1",
6
+ "file": "profile-1.json"
7
+ }
8
+ ]
9
+ }
package/example.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import { ExampleDeviceHandler } from './ExampleDeviceHandler.mjs'
2
+
3
+ const handler = new ExampleDeviceHandler('config.json')
4
+
5
+ const stopHandler = () => {
6
+ console.log('Receiving SIGINT => Stopping processes.')
7
+ handler.stop()
8
+ }
9
+
10
+ // Initiating a process
11
+ process.on('SIGINT', stopHandler)
12
+
13
+ await handler.start()
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/icons/cia.png ADDED
Binary file
package/icons/saph.png ADDED
Binary file
package/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { BaseLoupeDeckHandler } from './common/BaseLoupeDeckHandler.mjs'
2
+
3
+ export { BaseLoupeDeckHandler }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "loupedeck-commander",
3
+ "version": "1.0.0",
4
+ "description": "A system to ease working with LoupeDeck devices using CMD-line interfaces",
5
+ "main": "index.mjs",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "dependencies": {
10
+ "canvas": "^2.11.2",
11
+ "loupedeck": "^4.3.0",
12
+ "mkdirp": "^3.0.1",
13
+ "string-template": "^1.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "eslint": "^8.53.0",
17
+ "eslint-config-standard": "^17.1.0",
18
+ "eslint-plugin-import": "^2.29.0",
19
+ "eslint-plugin-n": "^16.3.1",
20
+ "eslint-plugin-promise": "^6.1.1"
21
+ },
22
+ "author": "Thomas Schneider",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git@gitlab.com:keckxde/loupedeck-commander.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://gitlab.com/keckxde/loupedeck-commander/-/issues"
30
+ }
31
+ }
package/profile-1.json ADDED
@@ -0,0 +1,159 @@
1
+ {
2
+ "profile": "KVM",
3
+ "description": "",
4
+ "config": {},
5
+ "touch": {
6
+ "left": {},
7
+ "right": {},
8
+ "center": {
9
+ "0": {
10
+ "images": {
11
+ "off": "icons/Comp_E_n.png",
12
+ "on": "icons/Comp_E_p.png"
13
+ },
14
+ "commands": {
15
+ "off": "echo \"{id} {state}\"",
16
+ "on": "echo \"{id} {state}\""
17
+ },
18
+ "group": "kvm1"
19
+ },
20
+ "1": {
21
+ "images": {
22
+ "off": "icons/Comp_D_n.png",
23
+ "on": "icons/Comp_D_p.png"
24
+ },
25
+ "group": "kvm1"
26
+ },
27
+ "2": {
28
+ "images": {
29
+ "off": "icons/Comp_C_n.png",
30
+ "on": "icons/Comp_C_p.png"
31
+ },
32
+ "group": "kvm1"
33
+ },
34
+ "3": {
35
+ "images": {
36
+ "off": "icons/Comp_B_n.png",
37
+ "on": "icons/Comp_B_p.png"
38
+ },
39
+ "group": "kvm1"
40
+ },
41
+ "4": {
42
+ "images": {
43
+ "off": "icons/Comp_F_n.png",
44
+ "on": "icons/Comp_F_p.png"
45
+ },
46
+ "group": "kvm1"
47
+ },
48
+ "5": {
49
+ "minPressed": 1000,
50
+ "type": "push",
51
+ "colors": {
52
+ "off": "#000000",
53
+ "on": "#ff0000"
54
+ }
55
+ },
56
+ "7": {
57
+ "images": {
58
+ "off": "icons/Comp_A_n.png",
59
+ "on": "icons/Comp_A_p.png"
60
+ },
61
+ "group": "kvm1"
62
+ }
63
+ },
64
+ "knob": {
65
+ "0": {
66
+ "images": {
67
+ "off": "icons/cia.png",
68
+ "on": "icons/saph.png"
69
+ },
70
+ "commands": {
71
+ "off": "echo \"CIA {state}\"",
72
+ "on": "echo \"CIA {state}\""
73
+ }
74
+ }
75
+ }
76
+ },
77
+ "knobs": {
78
+ "left": {},
79
+ "right": {}
80
+ },
81
+ "buttons": {
82
+ "knobTL": {
83
+ "commands": {
84
+ "off": "echo \"{id} {state} {value}\"",
85
+ "on": "echo \"{id} {state} {value}\""
86
+ }
87
+ },
88
+ "knobTR": {
89
+ "commands": {
90
+ "off": "echo \"{id} {state} {value}\"",
91
+ "on": "echo \"{id} {state} {value}\""
92
+ }
93
+ },
94
+ "knobCL": {
95
+ "commands": {
96
+ "off": "echo \"{id} {state} {value}\"",
97
+ "on": "echo \"{id} {state} {value}\""
98
+ }
99
+ },
100
+ "0": {
101
+ "commands": {
102
+ "off": "echo \"{id} {state}\"",
103
+ "on": "echo \"{id} {state}\""
104
+ },
105
+ "rgb": {
106
+ "off": [
107
+ 0,
108
+ 0,
109
+ 0
110
+ ],
111
+ "on": [
112
+ 0,
113
+ 255,
114
+ 0
115
+ ],
116
+ "intermediate": [
117
+ 0,
118
+ 255,
119
+ 255
120
+ ]
121
+ }
122
+ },
123
+ "1": {
124
+ "text": "PA",
125
+ "type": "toggle",
126
+ "commands": {
127
+ "off": "echo \"{id} {state}\"",
128
+ "on": "echo \"{id} {state}\"",
129
+ "intermediate": "echo \"{id} {state}\""
130
+ },
131
+ "rgb": {
132
+ "off": [
133
+ 0,
134
+ 0,
135
+ 0
136
+ ],
137
+ "on": [
138
+ 0,
139
+ 255,
140
+ 0
141
+ ],
142
+ "intermediate": [
143
+ 0,
144
+ 255,
145
+ 255
146
+ ]
147
+ }
148
+ },
149
+ "home": {
150
+ "rgb": {
151
+ "off": [
152
+ 0,
153
+ 0,
154
+ 0
155
+ ]
156
+ }
157
+ }
158
+ }
159
+ }