Skip to content

Commit 08bf36e

Browse files
authored
Merge pull request #17 from magicmonkey:devices
Create a structure to support different models of Streamdeck
2 parents eb3c9df + 537e30b commit 08bf36e

File tree

7 files changed

+232
-39
lines changed

7 files changed

+232
-39
lines changed

comms.go

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,56 @@ import (
88
"github.com/karalabe/hid"
99
)
1010

11-
const vendorID = 4057
12-
const productID = 0x6c
11+
const vendorID = 0x0fd9
12+
13+
// deviceType represents one of the various types of StreamDeck (mini/orig/orig2/xl)
14+
type deviceType struct {
15+
name string
16+
imageSize image.Point
17+
usbProductID uint16
18+
resetPacket []byte
19+
numberOfButtons uint
20+
brightnessPacket []byte
21+
buttonReadOffset uint
22+
imageFormat string
23+
imagePayloadPerPage uint
24+
imageHeaderFunc func(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte
25+
}
26+
27+
var deviceTypes []deviceType
28+
29+
// RegisterDevicetype allows the declaration of a new type of device, intended for use by subpackage "devices"
30+
func RegisterDevicetype(
31+
name string,
32+
imageSize image.Point,
33+
usbProductID uint16,
34+
resetPacket []byte,
35+
numberOfButtons uint,
36+
brightnessPacket []byte,
37+
buttonReadOffset uint,
38+
imageFormat string,
39+
imagePayloadPerPage uint,
40+
imageHeaderFunc func(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte,
41+
) {
42+
d := deviceType{
43+
name: name,
44+
imageSize: imageSize,
45+
usbProductID: usbProductID,
46+
resetPacket: resetPacket,
47+
numberOfButtons: numberOfButtons,
48+
brightnessPacket: brightnessPacket,
49+
buttonReadOffset: buttonReadOffset,
50+
imageFormat: imageFormat,
51+
imagePayloadPerPage: imagePayloadPerPage,
52+
imageHeaderFunc: imageHeaderFunc,
53+
}
54+
deviceTypes = append(deviceTypes, d)
55+
}
1356

1457
// Device is a struct which represents an actual Streamdeck device, and holds its reference to the USB HID device
1558
type Device struct {
1659
fd *hid.Device
60+
deviceType deviceType
1761
buttonPressListeners []func(int, *Device, error)
1862
}
1963

@@ -29,16 +73,23 @@ func OpenWithoutReset() (*Device, error) {
2973

3074
// Opens a new StreamdeckXL device, and returns a handle
3175
func rawOpen(reset bool) (*Device, error) {
32-
devices := hid.Enumerate(vendorID, productID)
76+
devices := hid.Enumerate(vendorID, 0)
3377
if len(devices) == 0 {
34-
return nil, errors.New("no stream deck device found")
78+
return nil, errors.New("No elgato devices found")
3579
}
80+
81+
retval := &Device{}
3682
id := 0
83+
// Iterate over the known device types, matching to product ID
84+
for _, devType := range deviceTypes {
85+
if devices[id].ProductID == devType.usbProductID {
86+
retval.deviceType = devType
87+
}
88+
}
3789
dev, err := devices[id].Open()
3890
if err != nil {
3991
return nil, err
4092
}
41-
retval := &Device{}
4293
retval.fd = dev
4394
if reset {
4495
retval.ResetComms()
@@ -47,6 +98,11 @@ func rawOpen(reset bool) (*Device, error) {
4798
return retval, nil
4899
}
49100

101+
// GetName returns the name of the type of Streamdeck
102+
func (d *Device) GetName() string {
103+
return d.deviceType.name
104+
}
105+
50106
// Close the device
51107
func (d *Device) Close() {
52108
d.fd.Close()
@@ -62,21 +118,27 @@ func (d *Device) SetBrightness(pct int) {
62118
pct = 100
63119
}
64120

65-
payload := []byte{'\x03', '\x08', byte(pct)}
121+
preamble := d.deviceType.brightnessPacket
122+
payload := append(preamble, byte(pct))
66123
d.fd.SendFeatureReport(payload)
67124
}
68125

69126
// ClearButtons writes a black square to all buttons
70127
func (d *Device) ClearButtons() {
71-
for i := 0; i < 32; i++ {
128+
numButtons := int(d.deviceType.numberOfButtons)
129+
for i := 0; i < numButtons; i++ {
72130
d.WriteColorToButton(i, color.Black)
73131
}
74132
}
75133

76134
// WriteColorToButton writes a specified color to the given button
77-
func (d *Device) WriteColorToButton(btnIndex int, colour color.Color) {
135+
func (d *Device) WriteColorToButton(btnIndex int, colour color.Color) error {
78136
img := getSolidColourImage(colour)
79-
d.rawWriteToButton(btnIndex, getImageAsJpeg(img))
137+
imgForButton, err := getImageForButton(img, d.deviceType.imageFormat)
138+
if err != nil {
139+
return err
140+
}
141+
return d.rawWriteToButton(btnIndex, imgForButton)
80142
}
81143

82144
// WriteImageToButton writes a specified image file to the given button
@@ -90,18 +152,19 @@ func (d *Device) WriteImageToButton(btnIndex int, filename string) error {
90152
}
91153

92154
func (d *Device) buttonPressListener() {
93-
var buttonMask [32]bool
155+
var buttonMask []bool
156+
buttonMask = make([]bool, d.deviceType.numberOfButtons)
94157
for {
95-
data := make([]byte, 50)
158+
data := make([]byte, d.deviceType.numberOfButtons+d.deviceType.buttonReadOffset)
96159
_, err := d.fd.Read(data)
97160
if err != nil {
98161
d.sendButtonPressEvent(-1, err)
99162
break
100163
}
101-
for i := 0; i < 32; i++ {
102-
if data[4+i] == 1 {
164+
for i := uint(0); i < d.deviceType.numberOfButtons; i++ {
165+
if data[d.deviceType.buttonReadOffset+i] == 1 {
103166
if !buttonMask[i] {
104-
d.sendButtonPressEvent(i, nil)
167+
d.sendButtonPressEvent(int(i), nil)
105168
}
106169
buttonMask[i] = true
107170
} else {
@@ -124,48 +187,43 @@ func (d *Device) ButtonPress(f func(int, *Device, error)) {
124187

125188
// ResetComms will reset the comms protocol to the StreamDeck; useful if things have gotten de-synced, but it will also reboot the StreamDeck
126189
func (d *Device) ResetComms() {
127-
payload := []byte{'\x03', '\x02'}
190+
payload := d.deviceType.resetPacket
128191
d.fd.SendFeatureReport(payload)
129192
}
130193

131194
// WriteRawImageToButton takes an `image.Image` and writes it to the given button, after resizing and rotating the image to fit the button (for some reason the StreamDeck screens are all upside down)
132195
func (d *Device) WriteRawImageToButton(btnIndex int, rawImg image.Image) error {
133-
img := resizeAndRotate(rawImg, 96, 96)
134-
return d.rawWriteToButton(btnIndex, getImageAsJpeg(img))
196+
img := resizeAndRotate(rawImg, d.deviceType.imageSize.X, d.deviceType.imageSize.Y)
197+
imgForButton, err := getImageForButton(img, d.deviceType.imageFormat)
198+
if err != nil {
199+
return err
200+
}
201+
return d.rawWriteToButton(btnIndex, imgForButton)
135202
}
136203

137204
func (d *Device) rawWriteToButton(btnIndex int, rawImage []byte) error {
138205
// Based on set_key_image from https://github.com/abcminiuser/python-elgato-streamdeck/blob/master/src/StreamDeck/Devices/StreamDeckXL.py#L151
139206
pageNumber := 0
140207
bytesRemaining := len(rawImage)
141208

142-
imageReportLength := 1024
143-
imageReportHeaderLength := 8
144-
imageReportPayloadLength := imageReportLength - imageReportHeaderLength
145-
146209
// Surely no image can be more than 20 packets...?
147210
payloads := make([][]byte, 20)
148211

149212
for bytesRemaining > 0 {
213+
214+
header := d.deviceType.imageHeaderFunc(uint(bytesRemaining), uint(btnIndex), uint(pageNumber))
215+
imageReportLength := int(d.deviceType.imagePayloadPerPage)
216+
imageReportHeaderLength := len(header)
217+
imageReportPayloadLength := imageReportLength - imageReportHeaderLength
218+
150219
thisLength := 0
151220
if imageReportPayloadLength < bytesRemaining {
152221
thisLength = imageReportPayloadLength
153222
} else {
154223
thisLength = bytesRemaining
155224
}
156-
bytesSent := pageNumber * imageReportPayloadLength
157-
header := []byte{'\x02', '\x07', byte(btnIndex)}
158-
if thisLength == bytesRemaining {
159-
header = append(header, '\x01')
160-
} else {
161-
header = append(header, '\x00')
162-
}
163-
164-
header = append(header, byte(thisLength&0xff))
165-
header = append(header, byte(thisLength>>8))
166225

167-
header = append(header, byte(pageNumber&0xff))
168-
header = append(header, byte(pageNumber>>8))
226+
bytesSent := pageNumber * imageReportPayloadLength
169227

170228
payload := append(header, rawImage[bytesSent:(bytesSent+thisLength)]...)
171229
padding := make([]byte, imageReportLength-len(payload))
@@ -177,7 +235,7 @@ func (d *Device) rawWriteToButton(btnIndex int, rawImage []byte) error {
177235
bytesRemaining = bytesRemaining - thisLength
178236
pageNumber = pageNumber + 1
179237
if pageNumber >= len(payloads) {
180-
return errors.New("Image too big for button, aborting. You probably need to reset the Streamdeck at this stage, and modify the size of `payloads` on line 142 to be something bigger.")
238+
return errors.New("Image too big for button comms, aborting - you probably need to reset the Streamdeck at this stage, and modify the size of `payloads` on line 142 to be something bigger")
181239
}
182240
}
183241
return nil

devices/origv2.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package devices
2+
3+
import (
4+
"image"
5+
6+
streamdeck "github.com/magicmonkey/go-streamdeck"
7+
)
8+
9+
var (
10+
ov2Name string
11+
ov2ButtonWidth uint
12+
ov2ButtonHeight uint
13+
ov2ImageReportPayloadLength uint
14+
)
15+
16+
// GetImageHeaderOv2 returns the USB comms header for a button image for the XL
17+
func GetImageHeaderOv2(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte {
18+
thisLength := uint(0)
19+
if ov2ImageReportPayloadLength < bytesRemaining {
20+
thisLength = ov2ImageReportPayloadLength
21+
} else {
22+
thisLength = bytesRemaining
23+
}
24+
header := []byte{'\x02', '\x07', byte(btnIndex)}
25+
if thisLength == bytesRemaining {
26+
header = append(header, '\x01')
27+
} else {
28+
header = append(header, '\x00')
29+
}
30+
31+
header = append(header, byte(thisLength&0xff))
32+
header = append(header, byte(thisLength>>8))
33+
34+
header = append(header, byte(pageNumber&0xff))
35+
header = append(header, byte(pageNumber>>8))
36+
37+
return header
38+
}
39+
40+
func init() {
41+
ov2Name = "Streamdeck (original v2)"
42+
ov2ButtonWidth = 72
43+
ov2ButtonHeight = 72
44+
ov2ImageReportPayloadLength = 1024
45+
streamdeck.RegisterDevicetype(
46+
ov2Name, // Name
47+
image.Point{X: int(ov2ButtonWidth), Y: int(ov2ButtonHeight)}, // Width/height of a button
48+
0x6d, // USB productID
49+
[]byte{'\x03', '\x02'}, // Reset packet
50+
15, // Number of buttons
51+
[]byte{'\x03', '\x08'}, // Set brightness packet preamble
52+
4, // Button read offset
53+
"JPEG", // Image format
54+
ov2ImageReportPayloadLength, // Amount of image payload allowed per USB packet
55+
GetImageHeaderOv2, // Function to get the comms image header
56+
)
57+
}

devices/xl.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package devices
2+
3+
import (
4+
"image"
5+
6+
streamdeck "github.com/magicmonkey/go-streamdeck"
7+
)
8+
9+
var (
10+
xlName string
11+
xlButtonWidth uint
12+
xlButtonHeight uint
13+
xlImageReportPayloadLength uint
14+
)
15+
16+
// GetImageHeaderXl returns the USB comms header for a button image for the XL
17+
func GetImageHeaderXl(bytesRemaining uint, btnIndex uint, pageNumber uint) []byte {
18+
thisLength := uint(0)
19+
if xlImageReportPayloadLength < bytesRemaining {
20+
thisLength = xlImageReportPayloadLength
21+
} else {
22+
thisLength = bytesRemaining
23+
}
24+
header := []byte{'\x02', '\x07', byte(btnIndex)}
25+
if thisLength == bytesRemaining {
26+
header = append(header, '\x01')
27+
} else {
28+
header = append(header, '\x00')
29+
}
30+
31+
header = append(header, byte(thisLength&0xff))
32+
header = append(header, byte(thisLength>>8))
33+
34+
header = append(header, byte(pageNumber&0xff))
35+
header = append(header, byte(pageNumber>>8))
36+
37+
return header
38+
}
39+
40+
func init() {
41+
xlName = "Streamdeck XL"
42+
xlButtonWidth = 96
43+
xlButtonHeight = 96
44+
xlImageReportPayloadLength = 1024
45+
streamdeck.RegisterDevicetype(
46+
xlName, // Name
47+
image.Point{X: int(xlButtonWidth), Y: int(xlButtonHeight)}, // Width/height of a button
48+
0x6c, // USB productID
49+
[]byte{'\x03', '\x02'}, // Reset packet
50+
32, // Number of buttons
51+
[]byte{'\x03', '\x08'}, // Set brightness packet preamble
52+
4, // Button read offset
53+
"JPEG", // Image format
54+
xlImageReportPayloadLength, // Amount of image payload allowed per USB packet
55+
GetImageHeaderXl, // Function to get the comms image header
56+
)
57+
}

examples/client/client.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
package main
22

33
import (
4+
"fmt"
45
"image/color"
56
"time"
67

78
streamdeck "github.com/magicmonkey/go-streamdeck"
89
"github.com/magicmonkey/go-streamdeck/actionhandlers"
910
"github.com/magicmonkey/go-streamdeck/buttons"
1011
"github.com/magicmonkey/go-streamdeck/decorators"
12+
_ "github.com/magicmonkey/go-streamdeck/devices"
1113
)
1214

1315
func main() {
1416
sd, err := streamdeck.New()
1517
if err != nil {
1618
panic(err)
1719
}
20+
fmt.Printf("Found device [%s]\n", sd.GetName())
1821

1922
myButton := buttons.NewTextButton("Hi world")
2023
myButton.SetActionHandler(&actionhandlers.TextPrintAction{Label: "You pressed me"})
@@ -28,7 +31,10 @@ func main() {
2831
myNextButton.SetActionHandler(&actionhandlers.TextLabelChangeAction{NewLabel: "8"})
2932
sd.AddButton(7, myNextButton)
3033

31-
anotherButton, _ := buttons.NewImageFileButton("/home/kevin/streamdeck/go-streamdeck/examples/play.jpg")
34+
anotherButton, err := buttons.NewImageFileButton("examples/test/play.jpg")
35+
if err != nil {
36+
panic(err)
37+
}
3238
sd.AddButton(9, anotherButton)
3339

3440
cButton := buttons.NewColourButton(color.RGBA{255, 255, 0, 255})

0 commit comments

Comments
 (0)