diff --git a/README.md b/README.md
index 0b48076..91163c1 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
[](https://goreportcard.com/report/muesli/streamdeck)
[](https://pkg.go.dev/github.com/muesli/streamdeck)
-A CLI application and Go library to control your Elgato Stream Deck on Linux.
+A Go library and CLI to control your Elgato Stream Deck on Linux.
If you're looking for a complete Linux service to control your StreamDeck, check
out [Deckmaster](https://github.com/muesli/deckmaster), which is based on this
@@ -33,6 +33,7 @@ SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006c", MODE:="666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="006d", MODE:="666", GROUP="plugdev"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0080", MODE:="666", GROUP="plugdev"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0090", MODE:="666", GROUP="plugdev"
+SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0084", MODE:="666", GROUP="plugdev"
```
Make sure your user is part of the `plugdev` group and reload the rules with
@@ -65,6 +66,95 @@ Reset the device:
streamdeck-cli reset
```
+## Hardware Support
+
+
+Stream Deck Mini
+
+#### Regular Buttons
+
+The 6 regular buttons are mapped to key index 0 - 5 (top left to bottom right).
+
+
+
+Stream Deck (including MK.2)
+
+#### Regular Buttons
+
+The 15 regular buttons are mapped to key index 0 - 14 (top left to bottom right).
+
+
+
+Stream Deck XL
+
+#### Regular Buttons
+
+The 32 regular buttons are mapped to key index 0 - 31 (top left to bottom right).
+
+
+
+Stream Deck Plus
+
+Knobs and touchscreen usage are mapped to regular button key indexes.
+
+#### Regular Buttons
+
+The 8 regular buttons are mapped to key index 0 - 7 (top left to bottom right).
+
+#### Touchscreen
+
+The touchscreen is divided into four horizontal segments (matching the
+number of buttons and knobs per row) with index 0 - 3 (from left to right).
+
+The key indexes for touchscreen usage are:
+
+Segment 0
+- Short touch: 20
+- Long touch: 24
+
+Segment 1
+- Short touch: 21
+- Long touch: 25
+
+Segment 2
+- Short touch: 22
+- Long touch: 26
+
+Segment 3
+- Short touch: 23
+- Long touch: 27
+
+All touchscreen presses are not "holdable" like normal buttons.
+
+Swiping is mapped to:
+- From left to right: 28
+- From right to left: 29
+
+#### Knobs
+
+The knobs usages will be mapped to key indexes as following (left to right):
+
+Knob 1
+- Press: 8 (holdable)
+- Left turn: 12
+- Right turn: 16
+
+Knob 2
+- Press: 9 (holdable)
+- Left turn: 13
+- Right turn: 17
+
+Knob 3
+- Press: 10 (holdable)
+- Left turn: 14
+- Right turn: 18
+
+Knob 4
+- Press: 11 (holdable)
+- Left turn: 15
+- Right turn: 19
+
+
## Feedback
Got some feedback or suggestions? Please open an issue or drop me a note!
diff --git a/streamdeck.go b/streamdeck.go
index 8618d54..83f42e8 100644
--- a/streamdeck.go
+++ b/streamdeck.go
@@ -3,6 +3,7 @@ package streamdeck
import (
"bytes"
"context"
+ "encoding/binary"
"fmt"
"image"
"image/color"
@@ -31,6 +32,27 @@ const (
PID_STREAMDECK_MINI = 0x0063
PID_STREAMDECK_MINI_MK2 = 0x0090
PID_STREAMDECK_XL = 0x006c
+ PID_STREAMDECK_PLUS = 0x0084
+
+ INPUT_TYPE_ID_BUTTON = uint8(0)
+ INPUT_TYPE_ID_TOUCH = uint8(2)
+ INPUT_TYPE_ID_KNOB = uint8(3)
+
+ INPUT_KNOB_USAGE_PRESS = uint8(0)
+ INPUT_KNOB_USAGE_DIAL = uint8(1)
+ INPUT_KNOB_STATE_OFFSET = uint8(5)
+
+ INPUT_TOUCH_USAGE_SHORT = uint8(1)
+ INPUT_TOUCH_USAGE_LONG = uint8(2)
+ INPUT_TOUCH_USAGE_SWIPE = uint8(3)
+
+ INPUT_POSITION_TYPE_ID = uint8(1)
+ INPUT_POSITION_KNOB_USAGE_ID = uint8(4)
+ INPUT_POSITION_TOUCH_USAGE_ID = uint8(4)
+ INPUT_POSITION_TOUCH_X_ID = uint8(6)
+ INPUT_POSITION_TOUCH_Y_ID = uint8(8)
+ INPUT_POSITION_TOUCH_X2_ID = uint8(10)
+ INPUT_POSITION_TOUCH_Y2_ID = uint8(12)
)
// Firmware command IDs.
@@ -58,15 +80,26 @@ type Device struct {
DPI uint
Padding uint
- featureReportSize int
- firmwareOffset int
- keyStateOffset int
- translateKeyIndex func(index, columns uint8) uint8
- imagePageSize int
- imagePageHeaderSize int
- flipImage func(image.Image) image.Image
- toImageFormat func(image.Image) ([]byte, error)
- imagePageHeader func(pageIndex int, keyIndex uint8, payloadLength int, lastPage bool) []byte
+ ScreenWidth uint
+ ScreenHeight uint
+ ScreenDPI uint
+ ScreenSegments uint8
+
+ Knobs uint8
+
+ featureReportSize int
+ firmwareOffset int
+ keyStateOffset int
+ translateKeyIndex func(index, columns uint8) uint8
+ readKeys func(*Device) (chan Key, error)
+ imagePageSize int
+ imagePageHeaderSize int
+ flipImage func(image.Image) image.Image
+ toImageFormat func(image.Image) ([]byte, error)
+ imagePageHeader func(pageIndex int, keyIndex uint8, payloadLength int, lastPage bool) []byte
+ screenPageSize int
+ screenPageHeaderSize int
+ screenPageHeader func(page int, position image.Point, width uint, height uint, payloadLength int, lastPage bool) []byte
getFirmwareCommand []byte
resetCommand []byte
@@ -89,8 +122,9 @@ type Device struct {
// Key holds the current status of a key on the device.
type Key struct {
- Index uint8
- Pressed bool
+ Index uint8
+ Pressed bool
+ Holdable bool
}
// Devices returns all attached Stream Decks.
@@ -116,6 +150,7 @@ func Devices() ([]Device, error) {
firmwareOffset: 5,
keyStateOffset: 1,
translateKeyIndex: translateRightToLeft,
+ readKeys: readKeysForButtonsOnlyInput,
imagePageSize: 7819,
imagePageHeaderSize: 16,
imagePageHeader: rev1ImagePageHeader,
@@ -139,6 +174,7 @@ func Devices() ([]Device, error) {
firmwareOffset: 5,
keyStateOffset: 1,
translateKeyIndex: identity,
+ readKeys: readKeysForButtonsOnlyInput,
imagePageSize: 1024,
imagePageHeaderSize: 16,
imagePageHeader: miniImagePageHeader,
@@ -162,6 +198,7 @@ func Devices() ([]Device, error) {
firmwareOffset: 6,
keyStateOffset: 4,
translateKeyIndex: identity,
+ readKeys: readKeysForButtonsOnlyInput,
imagePageSize: 1024,
imagePageHeaderSize: 8,
imagePageHeader: rev2ImagePageHeader,
@@ -185,6 +222,7 @@ func Devices() ([]Device, error) {
firmwareOffset: 6,
keyStateOffset: 4,
translateKeyIndex: identity,
+ readKeys: readKeysForButtonsOnlyInput,
imagePageSize: 1024,
imagePageHeaderSize: 8,
imagePageHeader: rev2ImagePageHeader,
@@ -194,10 +232,41 @@ func Devices() ([]Device, error) {
resetCommand: c_REV2_RESET,
setBrightnessCommand: c_REV2_BRIGHTNESS,
}
+ case d.VendorID == VID_ELGATO && d.ProductID == PID_STREAMDECK_PLUS:
+ dev = Device{
+ ID: d.Path,
+ Serial: d.Serial,
+ Columns: 4,
+ Rows: 2,
+ Keys: 30,
+ Pixels: 120,
+ DPI: 180,
+ Padding: 16,
+ ScreenWidth: 800,
+ ScreenHeight: 100,
+ ScreenDPI: 184, //108x14mm at 800x100px
+ ScreenSegments: 4,
+ Knobs: 4,
+ featureReportSize: 32,
+ firmwareOffset: 6,
+ keyStateOffset: 4,
+ translateKeyIndex: identity,
+ readKeys: readKeysForMultipleInputTypes,
+ imagePageSize: 1024,
+ imagePageHeaderSize: 8,
+ imagePageHeader: rev2ImagePageHeader,
+ toImageFormat: toJPEG,
+ screenPageSize: 1024,
+ screenPageHeaderSize: 16,
+ screenPageHeader: touchScreenImagePageHeader,
+ getFirmwareCommand: c_REV2_FIRMWARE,
+ resetCommand: c_REV2_RESET,
+ setBrightnessCommand: c_REV2_BRIGHTNESS,
+ }
}
if dev.ID != "" {
- dev.keyState = make([]byte, dev.Columns*dev.Rows)
+ dev.keyState = make([]byte, dev.Keys)
dev.info = d
dd = append(dd, dev)
}
@@ -253,6 +322,10 @@ func (d Device) Clear() error {
// ReadKeys returns a channel, which it will use to emit key presses/releases.
func (d *Device) ReadKeys() (chan Key, error) {
+ return d.readKeys(d)
+}
+
+func readKeysForButtonsOnlyInput(d *Device) (chan Key, error) {
kch := make(chan Key)
keyBuffer := make([]byte, d.keyStateOffset+len(d.keyState))
go func() {
@@ -264,29 +337,49 @@ func (d *Device) ReadKeys() (chan Key, error) {
return
}
- // don't trigger a key event if the device is asleep, but wake it
- if d.asleep {
+ if d.Asleep() {
_ = d.Wake()
+ resetKeysStates(d, keyBuffer)
+ // Dont trigger a key event, because the key awoke the device
+ continue
+ }
- // reset state so no spurious key events get triggered
- for i := d.keyStateOffset; i < len(keyBuffer); i++ {
- keyBuffer[i] = 0
- }
+ d.updateLastActionTimeToNow()
+
+ d.sendButtonKeyEventsToChannel(keyBuffer, kch)
+ }
+ }()
+
+ return kch, nil
+}
+
+func readKeysForMultipleInputTypes(device *Device) (chan Key, error) {
+ kch := make(chan Key)
+ inputBuffer := make([]byte, 13)
+ go func() {
+ for {
+ if _, err := device.device.Read(inputBuffer); err != nil {
+ close(kch)
+ return
+ }
+
+ if device.Asleep() {
+ _ = device.Wake()
+ resetKeysStates(device, inputBuffer)
+ // Dont trigger a key event, because the key awoke the device
continue
}
- d.sleepMutex.Lock()
- d.lastActionTime = time.Now()
- d.sleepMutex.Unlock()
-
- for i := d.keyStateOffset; i < len(keyBuffer); i++ {
- keyIndex := uint8(i - d.keyStateOffset)
- if keyBuffer[i] != d.keyState[keyIndex] {
- kch <- Key{
- Index: d.translateKeyIndex(keyIndex, d.Columns),
- Pressed: keyBuffer[i] == 1,
- }
- }
+ device.updateLastActionTimeToNow()
+
+ inputType := inputBuffer[INPUT_POSITION_TYPE_ID]
+
+ if inputType == INPUT_TYPE_ID_BUTTON {
+ device.sendButtonKeyEventsToChannel(inputBuffer, kch)
+ } else if inputType == INPUT_TYPE_ID_KNOB {
+ device.sendKnobEventsToChannel(inputBuffer, kch)
+ } else if inputType == INPUT_TYPE_ID_TOUCH {
+ device.sendTouchEventsToChannel(inputBuffer, kch)
}
}
}()
@@ -294,6 +387,112 @@ func (d *Device) ReadKeys() (chan Key, error) {
return kch, nil
}
+func (d *Device) sendTouchEventsToChannel(inputBuffer []byte, kch chan Key) {
+ touchUsage := inputBuffer[INPUT_POSITION_TOUCH_USAGE_ID]
+
+ x := binary.LittleEndian.Uint16(inputBuffer[INPUT_POSITION_TOUCH_X_ID:])
+
+ segmentWidth := d.ScreenSegmentWidth()
+ segment := uint8(math.Floor(float64(x) / float64(segmentWidth)))
+
+ var keyIndex uint8
+
+ if touchUsage == INPUT_TOUCH_USAGE_SHORT {
+ keyIndex = d.Columns*d.Rows + 3*d.Knobs + segment
+ } else if touchUsage == INPUT_TOUCH_USAGE_LONG {
+ keyIndex = d.Columns*d.Rows + 3*d.Knobs + d.ScreenSegments + segment
+ } else if touchUsage == INPUT_TOUCH_USAGE_SWIPE {
+ x2 := binary.LittleEndian.Uint16(inputBuffer[INPUT_POSITION_TOUCH_X2_ID:])
+ startSegment := uint8(math.Floor(float64(x) / 40.0))
+ stopSegment := uint8(math.Floor(float64(x2) / 40.0))
+
+ if startSegment < stopSegment { //left to right
+ keyIndex = d.Columns*d.Rows + 3*d.Knobs + 2*d.ScreenSegments
+ } else if startSegment > stopSegment { //right to left
+ keyIndex = d.Columns*d.Rows + 3*d.Knobs + 2*d.ScreenSegments + 1
+ } else {
+ return
+ }
+ }
+ kch <- Key{
+ Index: keyIndex,
+ Pressed: true,
+ Holdable: false,
+ }
+}
+
+func (d *Device) sendKnobEventsToChannel(inputBuffer []byte, kch chan Key) {
+ knobUsage := inputBuffer[INPUT_POSITION_KNOB_USAGE_ID]
+
+ for i := INPUT_KNOB_STATE_OFFSET; i < INPUT_KNOB_STATE_OFFSET+d.Knobs; i++ {
+ keyValue := inputBuffer[i]
+
+ if knobUsage == INPUT_KNOB_USAGE_PRESS {
+ keyIndex := i - INPUT_KNOB_STATE_OFFSET + d.Columns*d.Rows
+
+ if keyValue != d.keyState[keyIndex] {
+ d.keyState[keyIndex] = keyValue
+
+ kch <- Key{
+ Index: keyIndex,
+ Pressed: keyValue == 1,
+ Holdable: true,
+ }
+ }
+ } else if knobUsage == INPUT_KNOB_USAGE_DIAL && inputBuffer[i] > 0 {
+ var keyIndex uint8
+
+ if int(keyValue)-128 > 0 { //left turn
+ keyIndex = i - INPUT_KNOB_STATE_OFFSET + d.Columns*d.Rows + d.Knobs
+ } else { //right turn
+ keyIndex = i - INPUT_KNOB_STATE_OFFSET + d.Columns*d.Rows + 2*d.Knobs
+ }
+
+ kch <- Key{
+ Index: keyIndex,
+ Pressed: true,
+ Holdable: false,
+ }
+ }
+ }
+}
+
+func (d *Device) sendButtonKeyEventsToChannel(inputBuffer []byte, kch chan Key) {
+ for i := d.keyStateOffset; i < len(inputBuffer); i++ {
+ keyIndex := uint8(i - d.keyStateOffset)
+ if inputBuffer[i] != d.keyState[keyIndex] {
+ d.keyState[keyIndex] = inputBuffer[i]
+ kch <- Key{
+ Index: keyIndex,
+ Pressed: inputBuffer[i] == 1,
+ Holdable: true,
+ }
+ }
+ }
+}
+
+func (d *Device) updateLastActionTimeToNow() {
+ d.sleepMutex.Lock()
+ d.lastActionTime = time.Now()
+ d.sleepMutex.Unlock()
+}
+
+// ScreenSegmentWidth returns the width of a screen segment. Returns 0 if there are no segments.
+func (d *Device) ScreenSegmentWidth() uint {
+ if d.ScreenSegments == 0 {
+ return 0
+ }
+ return d.ScreenWidth / uint(d.ScreenSegments)
+}
+
+// ScreenSegmentHeight returns the width of a screen segment. Returns 0 if there are no segments.
+func (d *Device) ScreenSegmentHeight() uint {
+ if d.ScreenSegments == 0 {
+ return 0
+ }
+ return d.ScreenHeight
+}
+
// Sleep puts the device asleep, waiting for a key event to wake it up.
func (d *Device) Sleep() error {
d.sleepMutex.Lock()
@@ -425,7 +624,7 @@ func (d Device) SetImage(index uint8, img image.Image) error {
return fmt.Errorf("supplied image has wrong dimensions, expected %[1]dx%[1]d pixels", d.Pixels)
}
- imageBytes, err := d.toImageFormat(d.flipImage(img))
+ imageBytes, err := d.transformImage(img)
if err != nil {
return fmt.Errorf("cannot convert image data: %v", err)
}
@@ -458,6 +657,56 @@ func (d Device) SetImage(index uint8, img image.Image) error {
return nil
}
+// SetTouchScreenSegmentImage sets the image of a segment of the Stream Deck Plus touch screen. The provided image
+// needs to be in the correct resolution for the device. The index starts with
+// 0 to Device.ScreenSegments-1.
+func (d Device) SetTouchScreenSegmentImage(segmentIndex uint8, img image.Image) error {
+ position := image.Point{
+ X: int(uint(segmentIndex) * d.ScreenSegmentWidth()),
+ Y: 0,
+ }
+
+ return d.SetTouchScreenImage(position, d.ScreenSegmentWidth(), d.ScreenSegmentHeight(), img)
+}
+
+// SetTouchScreenImage sets the image of the Stream Deck Plus touch screen at the given point. The provided image
+// needs to be in the correct resolution for the device.
+func (d Device) SetTouchScreenImage(position image.Point, width uint, height uint, img image.Image) error {
+ imageBytes, err := d.transformImage(img)
+
+ if err != nil {
+ return fmt.Errorf("cannot convert image data: %v", err)
+ }
+
+ imageData := imageData{
+ image: imageBytes,
+ pageSize: d.screenPageSize - d.screenPageHeaderSize,
+ }
+
+ data := make([]byte, d.screenPageSize)
+
+ var page int
+ var lastPage bool
+ for !lastPage {
+ var payload []byte
+ payload, lastPage = imageData.Page(page)
+ header := d.screenPageHeader(page, position, width, height, len(payload), lastPage)
+
+ copy(data, header)
+ copy(data[len(header):], payload)
+
+ _, err := d.device.Write(data)
+ if err != nil {
+ return fmt.Errorf("cannot write image page %d of %d (%d image bytes) %d bytes: %v",
+ page, imageData.PageCount(), imageData.Length(), len(data), err)
+ }
+
+ page++
+ }
+
+ return nil
+}
+
// getFeatureReport from the device without worries about the correct payload
// size.
func (d Device) getFeatureReport(payload []byte) ([]byte, error) {
@@ -502,6 +751,22 @@ func toRGBA(img image.Image) *image.RGBA {
return out
}
+// transformImage transforms the image for sending it to the device.
+func (d *Device) transformImage(img image.Image) ([]byte, error) {
+ if d.flipImage != nil {
+ img = d.flipImage(img)
+ }
+
+ return d.toImageFormat(img)
+}
+
+func resetKeysStates(device *Device, inputBuffer []byte) {
+ // reset state so no spurious key events get triggered
+ for i := device.keyStateOffset; i < len(inputBuffer); i++ {
+ inputBuffer[i] = 0
+ }
+}
+
// flipHorizontally returns the given image horizontally flipped.
func flipHorizontally(img image.Image) image.Image {
flipped := image.NewRGBA(img.Bounds())
@@ -647,6 +912,34 @@ func rev2ImagePageHeader(pageIndex int, keyIndex uint8, payloadLength int, lastP
}
}
+// touchScreenImagePageHeader returns the image page header sequence used by Stream
+// Deck Plus for the touch screen.
+func touchScreenImagePageHeader(page int, position image.Point, width uint, height uint, payloadLength int, lastPage bool) []byte {
+ var lastPageByte byte
+ if lastPage {
+ lastPageByte = 1
+ }
+
+ return []byte{
+ 0x02, // 0 Elgato secret flag value #1
+ 0x0c, // 1 Elgato secret flag value #2
+ byte(position.X), // 2 x low byte
+ byte(position.X >> 8), // 3 x high byte
+ byte(position.Y), // 4 y low byte
+ byte(position.Y >> 8), // 5 y high byte
+ byte(width), // 6 width low byte
+ byte(width >> 8), // 7 width high byte
+ byte(height), // 8 height low byte
+ byte(height >> 8), // 9 height high byte
+ lastPageByte, // 10 last page
+ byte(page), // 12 page low byte
+ byte(page >> 8), // 11 page high byte
+ byte(payloadLength), // 14 payload length high byte
+ byte(payloadLength >> 8), // 13 payload length high byte
+ 0x00, // 15 padding
+ }
+}
+
// imageData allows to access raw image data in a byte array through pages of a
// given size.
type imageData struct {