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.
- package/ExampleDeviceHandler.mjs +45 -0
- package/README.md +51 -0
- package/common/ApplicationConfig.mjs +81 -0
- package/common/BaseLoupeDeckHandler.mjs +189 -0
- package/common/cmd-executer.mjs +16 -0
- package/common/index.mjs +5 -0
- package/common/touchbuttons.mjs +473 -0
- package/common/utils.mjs +29 -0
- package/config.json +9 -0
- package/example.mjs +13 -0
- package/icons/A-G_n.png +0 -0
- package/icons/A-G_p.png +0 -0
- package/icons/A-H_n.png +0 -0
- package/icons/A-H_p.png +0 -0
- package/icons/Audio_Plug_n.png +0 -0
- package/icons/Audio_Plug_p.png +0 -0
- package/icons/Comp_A_n.png +0 -0
- package/icons/Comp_A_p.png +0 -0
- package/icons/Comp_B_n.png +0 -0
- package/icons/Comp_B_p.png +0 -0
- package/icons/Comp_C_n.png +0 -0
- package/icons/Comp_C_p.png +0 -0
- package/icons/Comp_D_n.png +0 -0
- package/icons/Comp_D_p.png +0 -0
- package/icons/Comp_E_n.png +0 -0
- package/icons/Comp_E_p.png +0 -0
- package/icons/Comp_F_n.png +0 -0
- package/icons/Comp_F_p.png +0 -0
- package/icons/Comp_G_n.png +0 -0
- package/icons/Comp_G_p.png +0 -0
- package/icons/Comp_H_n.png +0 -0
- package/icons/Comp_H_p.png +0 -0
- package/icons/Mic_n.png +0 -0
- package/icons/Mic_p.png +0 -0
- package/icons/Sound_VolHigh.png +0 -0
- package/icons/Sound_VolLow.png +0 -0
- package/icons/Sound_VolMid.png +0 -0
- package/icons/Sound_Voldown.png +0 -0
- package/icons/Sound_Volup.png +0 -0
- package/icons/Sound_off.png +0 -0
- package/icons/cia.png +0 -0
- package/icons/saph.png +0 -0
- package/index.mjs +3 -0
- package/package.json +31 -0
- 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
|
+
}
|
package/common/index.mjs
ADDED
|
@@ -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
|
+
}
|
package/common/utils.mjs
ADDED
|
@@ -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
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()
|
package/icons/A-G_n.png
ADDED
|
Binary file
|
package/icons/A-G_p.png
ADDED
|
Binary file
|
package/icons/A-H_n.png
ADDED
|
Binary file
|
package/icons/A-H_p.png
ADDED
|
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/Mic_n.png
ADDED
|
Binary file
|
package/icons/Mic_p.png
ADDED
|
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
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
|
+
}
|