Skip to content

Commit 48b6c61

Browse files
authored
Merge pull request #10 from unix-streamdeck/feat/multiple-deck-support
Added support for multiple streamdecks & config file flag
2 parents 575e672 + 9868442 commit 48b6c61

File tree

9 files changed

+447
-199
lines changed

9 files changed

+447
-199
lines changed

README.md

+98-37
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
# Streamdeckd
1+
# Streamdeckd
2+
3+
### Installation
4+
5+
- create the file `/etc/udev/rules.d/50-elgato.rules` with the following config
26

3-
### Installation
4-
5-
- create the file `/etc/udev/rules.d/50-elgato.rules` with the following config
67
```
78
SUBSYSTEM=="input", GROUP="input", MODE="0666"
89
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", MODE:="666", GROUP="plugdev"
910
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0063", MODE:="666", GROUP="plugdev"
1011
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", MODE:="666", GROUP="plugdev"
1112
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006d", MODE:="666", GROUP="plugdev"
1213
```
13-
14-
- run `sudo udevadm control --reload-rules` to reload the udev rules
15-
16-
Then xdotool will be required to simulate keypresses, to install this run:
17-
18-
#### Arch
19-
20-
`sudo pacman -S xdotool`
21-
22-
#### Debian based
23-
24-
`sudo apt install xdotool`
2514

15+
- run `sudo udevadm control --reload-rules` to reload the udev rules
16+
17+
Then xdotool will be required to simulate keypresses, to install this run:
18+
19+
#### Arch
20+
21+
`sudo pacman -S xdotool`
22+
23+
#### Debian based
24+
25+
`sudo apt install xdotool`
2626

2727
### Configuration
2828

@@ -34,18 +34,30 @@ An example config would be something like:
3434

3535
```json
3636
{
37-
"pages": [
38-
[
39-
{
40-
"switch_page": 1,
41-
"icon": "~/icon.png"
42-
}
43-
]
37+
"modules": [
38+
"/home/user/module.so"
39+
],
40+
"decks": [
41+
{
42+
"serial": "AB12C3D45678",
43+
"pages": [
44+
[
45+
{
46+
"switch_page": 1,
47+
"icon": "~/icon.png"
48+
}
49+
]
50+
]
51+
}
4452
]
4553
}
4654
```
4755

48-
The outer array is the list of pages, the inner array is the list of button on that page, with the buttons going in a right to left order.
56+
At the top is the list of custom modules, these are go plugins in the .so format, following that is the list of deck
57+
objects, each represents a different streamdeck device, and contains its serial, and its list of pages
58+
59+
The outer array in a deck is the list of pages, the inner array is the list of button on that page, with the buttons
60+
going in a right to left order.
4961

5062
The actions you can have on a button are:
5163

@@ -55,28 +67,77 @@ The actions you can have on a button are:
5567
- `brightness`: set the brightness of the streamdeck as a percentage
5668
- `switch_page`: change the active page on the streamdeck
5769

58-
5970
### D-Bus
6071

61-
There is a D-Bus interface built into the daemon, the service name and interface for D-Bus are `com.unixstreamdeck.streamdeckd` and `com/unixstreamdeck/streamdeckd` respectively, and is made up of the following methods/signals
72+
There is a D-Bus interface built into the daemon, the service name and interface for D-Bus
73+
are `com.unixstreamdeck.streamdeckd` and `com/unixstreamdeck/streamdeckd` respectively, and is made up of the following
74+
methods/signals
6275

6376
#### Methods
6477

65-
- GetConfig - returns the current running config
66-
- SetConfig - sets the config, without saving to disk, takes in Stringified json, returns an error if anything breaks
67-
- ReloadConfig - reloads the config from disk
68-
- GetDeckInfo - Returns information about the active streamdeck in the format of
78+
- GetConfig - returns the current running config
79+
- SetConfig - sets the config, without saving to disk, takes in Stringified json, returns an error if anything breaks
80+
- ReloadConfig - reloads the config from disk
81+
- GetDeckInfo - Returns information about all the active streamdecks in the format of
82+
6983
```json
70-
{
71-
"icon_size": 72,
72-
"rows": 3,
73-
"cols": 5,
74-
"page": 0
75-
}
84+
[
85+
{
86+
"icon_size": 72,
87+
"rows": 3,
88+
"cols": 5,
89+
"page": 0,
90+
"serial": "AB12C3D45678"
91+
}
92+
]
7693
```
94+
7795
- SetPage - Set the page on the streamdeck to the number passed to it, returns an error if anything breaks
78-
- CommitConfig - Commits the currently active config to disk, returns an error if anything breaks
96+
- CommitConfig - Commits the currently active config to disk, returns an error if anything breaks
97+
- GetModules - Get the list of loaded modules, and the config fields those modules use
98+
- PressButton - Simulates a button press on the streamdeck device, consumes a device serial, and a key index
99+
79100

80101
#### Signals
81102

82103
- Page - sends the number of the page switched to on the StreamDeck
104+
105+
### Custom Modules
106+
107+
To create custom modules, I suggest looking at the gif, counter, and time modules in the example handlers package in streamdeckd, they should be in a file with the GetModule method as shown below
108+
109+
#### Loading Modules into streamdeckd
110+
111+
Modules require a method on them in the main package called "GetModule" that returns an instance of [handler.Module](https://github.com/unix-streamdeck/streamdeckd/blob/575e672c26f275d35a016be6406ceb8480ccfff5/handlers/handlers.go#L9) e.g
112+
113+
```go
114+
package main
115+
116+
type CustomIconHandler struct {
117+
118+
}
119+
...
120+
121+
type CustomKeyHandler struct {
122+
123+
}
124+
...
125+
126+
func GetModule() handlers.Module {
127+
return handlers.Module{
128+
Name: "CustomModule", // the name that will be used in the icon_handler/key_handler field in the config, and that will be shown in the handler dropdown in streamdeckui
129+
NewIcon: func() api.IconHandler { return &CustomerIconHandler{}}, // Method to create a new instance of the Icon handler, if left empty, streamdeckui will not include it in the icon handler fields
130+
NewKey: func() api.KeyHandler { return &CustomerKeyHandler{}}, // Method to create a new instance of the Key Handler, if left empty, streamdeckui will not include it in the key handler fields
131+
IconFields: []api.Field{ // list of fields to be shown in streamdeckui when the icon handler is selected
132+
{
133+
Title: "Icon", // name of field to show in UI
134+
Name: "icon", // name of field that will be included in the iconHandlerFields map
135+
Type: "File" // type of input to show on streamdeckui, options are Text, File, TextAlignment, and Number
136+
FileTypes: []string{".png", ".jpg"} // Allowed file types if a File input type is used
137+
}
138+
},
139+
KeyFields: []api.Field{}, // Same as IconFields
140+
}
141+
}
142+
143+
```

dbus.go

+25-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
var conn *dbus.Conn
1313

1414
var sDbus *StreamDeckDBus
15-
var sDInfo api.StreamDeckInfo
15+
var sDInfo []api.StreamDeckInfo
1616

1717
type StreamDeckDBus struct {
1818
}
@@ -41,9 +41,15 @@ func (StreamDeckDBus) ReloadConfig() *dbus.Error {
4141
return nil
4242
}
4343

44-
func (StreamDeckDBus) SetPage(page int) *dbus.Error {
45-
SetPage(config, page)
46-
return nil
44+
func (StreamDeckDBus) SetPage(serial string, page int) *dbus.Error {
45+
for s := range devs {
46+
if devs[s].Deck.Serial == serial {
47+
dev := devs[s]
48+
SetPage(dev, page)
49+
return nil
50+
}
51+
}
52+
return dbus.MakeFailedError(errors.New("Device with Serial: " + serial + " could not be found"))
4753
}
4854

4955
func (StreamDeckDBus) SetConfig(configString string) *dbus.Error {
@@ -74,6 +80,15 @@ func (StreamDeckDBus) GetModules() (string, *dbus.Error) {
7480
return string(modulesString), nil
7581
}
7682

83+
func (StreamDeckDBus) PressButton(serial string, keyIndex int) *dbus.Error {
84+
dev, ok := devs[serial]
85+
if !ok || !dev.IsOpen{
86+
return dbus.MakeFailedError(errors.New("Can't find connected device: " + serial))
87+
}
88+
HandleInput(dev, &dev.Config[dev.Page][keyIndex], dev.Page)
89+
return nil
90+
}
91+
7792
func InitDBUS() error {
7893
var err error
7994
conn, err = dbus.SessionBus()
@@ -84,9 +99,6 @@ func InitDBUS() error {
8499
defer conn.Close()
85100

86101
sDbus = &StreamDeckDBus{}
87-
sDInfo = api.StreamDeckInfo{
88-
Page: p,
89-
}
90102
conn.ExportAll(sDbus, "/com/unixstreamdeck/streamdeckd", "com.unixstreamdeck.streamdeckd")
91103
reply, err := conn.RequestName("com.unixstreamdeck.streamdeckd",
92104
dbus.NameFlagDoNotQueue)
@@ -100,11 +112,13 @@ func InitDBUS() error {
100112
select {}
101113
}
102114

103-
func EmitPage(page int) {
115+
func EmitPage(dev *VirtualDev, page int) {
104116
if conn != nil {
105-
conn.Emit("/com/unixstreamdeck/streamdeckd", "com.unixstreamdeck.streamdeckd.Page", page)
117+
conn.Emit("/com/unixstreamdeck/streamdeckd", "com.unixstreamdeck.streamdeckd.Page", dev.Deck.Serial, page)
106118
}
107-
if sDbus != nil {
108-
sDInfo.Page = page
119+
for i := range sDInfo {
120+
if sDInfo[i].Serial == dev.Deck.Serial {
121+
sDInfo[i].Page = page
122+
}
109123
}
110124
}

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ go 1.14
44

55
require (
66
github.com/godbus/dbus/v5 v5.0.4-0.20200513180336-df5ef3eb7cca
7-
github.com/unix-streamdeck/api v0.0.0-20201228201207-ca404527f907
7+
github.com/unix-streamdeck/api v1.0.0
88
github.com/unix-streamdeck/driver v0.0.0-20200817173808-cdaf123c076b
9-
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
9+
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
10+
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
1011
)

go.sum

+6-5
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
9191
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
9292
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
9393
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
94-
github.com/unix-streamdeck/api v0.0.0-20201228201207-ca404527f907 h1:MBWGv1fYvM3upBP2Y9TTMgQxMyE3o+ivfBuQ0vmsZh8=
95-
github.com/unix-streamdeck/api v0.0.0-20201228201207-ca404527f907/go.mod h1:Z8bzDHQnWv/2hx9wQXp0/qw6Fp4ty5pFRsgaBG5WYAI=
94+
github.com/unix-streamdeck/api v1.0.0 h1:GZIslyThiZgcZ/GIR0O/DWnciL6bijSdIA+TSnKzHsI=
95+
github.com/unix-streamdeck/api v1.0.0/go.mod h1:Z8bzDHQnWv/2hx9wQXp0/qw6Fp4ty5pFRsgaBG5WYAI=
9696
github.com/unix-streamdeck/driver v0.0.0-20200817173808-cdaf123c076b h1:27gVti9+OevmBC2BnWlKC0dQ0eiIHh7PvYTWxt4vb6A=
9797
github.com/unix-streamdeck/driver v0.0.0-20200817173808-cdaf123c076b/go.mod h1:i3Eg6kJBslgUk2VIPJ3Cclta2fpV1KJrOnOnR8gnVKY=
9898
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@@ -103,10 +103,10 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
103103
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
104104
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
105105
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
106-
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
107106
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
108-
golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw=
109107
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
108+
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
109+
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
110110
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
111111
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
112112
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -117,8 +117,9 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
117117
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
118118
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
119119
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
120-
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
121120
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
121+
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
122+
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
122123
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
123124
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
124125
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

handlers/examples/gif.go

+19-9
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import (
1616
type GifIconHandler struct {
1717
Running bool
1818
Lock *semaphore.Weighted
19+
Quit chan bool
1920
}
2021

2122
func (s *GifIconHandler) Start(key api.Key, info api.StreamDeckInfo, callback func(image image.Image)) {
23+
if s.Quit == nil {
24+
s.Quit = make(chan bool)
25+
}
2226
if s.Lock == nil {
2327
s.Lock = semaphore.NewWeighted(1)
2428
}
@@ -50,7 +54,7 @@ func (s *GifIconHandler) Start(key api.Key, info api.StreamDeckInfo, callback fu
5054
}
5155
frames[i] = img
5256
}
53-
go loop(frames, timeDelay, callback, s)
57+
go s.loop(frames, timeDelay, callback)
5458
}
5559

5660
func (s *GifIconHandler) IsRunning() bool {
@@ -63,24 +67,30 @@ func (s *GifIconHandler) SetRunning(running bool) {
6367

6468
func (s *GifIconHandler) Stop() {
6569
s.Running = false
70+
s.Quit <- true
6671
}
6772

68-
func loop(frames []image.Image, timeDelay int, callback func(image image.Image), s *GifIconHandler) {
73+
func (s *GifIconHandler) loop(frames []image.Image, timeDelay int, callback func(image image.Image)) {
6974
ctx := context.Background()
7075
err := s.Lock.Acquire(ctx, 1)
7176
if err != nil {
7277
return
7378
}
7479
defer s.Lock.Release(1)
7580
gifIndex := 0
76-
for s.Running {
77-
img := frames[gifIndex]
78-
callback(img)
79-
gifIndex++
80-
if gifIndex >= len(frames) {
81-
gifIndex = 0
81+
for {
82+
select {
83+
case <-s.Quit:
84+
return
85+
default:
86+
img := frames[gifIndex]
87+
callback(img)
88+
gifIndex++
89+
if gifIndex >= len(frames) {
90+
gifIndex = 0
91+
}
92+
time.Sleep(time.Duration(timeDelay * 10000000))
8293
}
83-
time.Sleep(time.Duration(timeDelay * 10000000))
8494
}
8595
}
8696

0 commit comments

Comments
 (0)