mirror of
https://github.com/Luzifer/streamdeck.git
synced 2024-12-29 22:21:24 +00:00
Add support for SDXL and multiple StreamDecks
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
74bd0d7fb4
commit
41a34dd76c
7 changed files with 256 additions and 4 deletions
|
@ -11,6 +11,7 @@
|
|||
## Supported devices:
|
||||
|
||||
- Elgato StreamDeck Original V2 (15 keys, ID `0fd9:006d`)
|
||||
- Elgato StreamDeck XL (32 keys, ID `0fd9:006c`)
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
60
cmd/streamdeck/enumerate.go
Normal file
60
cmd/streamdeck/enumerate.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/streamdeck"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sstallion/go-hid"
|
||||
)
|
||||
|
||||
func getAvailableDecks() ([]uint16, error) {
|
||||
var out []uint16
|
||||
return out, hid.Enumerate(streamdeck.VendorElgato, hid.ProductIDAny, func(info *hid.DeviceInfo) error {
|
||||
if _, ok := streamdeck.DeckToName[info.ProductID]; !ok {
|
||||
// Is from Elgato but not a supported StreamDeck
|
||||
return nil
|
||||
}
|
||||
|
||||
out = append(out, info.ProductID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func listAndQuit() {
|
||||
av, err := getAvailableDecks()
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Unable to get available decks")
|
||||
}
|
||||
|
||||
for _, id := range av {
|
||||
fmt.Printf("0x%x - %s\n", id, streamdeck.DeckToName[id])
|
||||
}
|
||||
|
||||
// Quit now as listing is done
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func selectDeckToUse() (uint16, error) {
|
||||
if cfg.ProductID != "" {
|
||||
// User selected a specific deck to use
|
||||
id, err := strconv.ParseUint(strings.TrimPrefix(cfg.ProductID, "0x"), 16, 16)
|
||||
return uint16(id), errors.Wrap(err, "Unable to parse given product ID")
|
||||
}
|
||||
|
||||
av, err := getAvailableDecks()
|
||||
if err != nil {
|
||||
return 0x0, errors.Wrap(err, "Unable to get available decks")
|
||||
}
|
||||
|
||||
if len(av) == 0 {
|
||||
return 0x0, errors.Wrap(err, "Found no supported decks")
|
||||
}
|
||||
|
||||
// There is at least one supported deck, use the first one
|
||||
return av[0], nil
|
||||
}
|
|
@ -15,6 +15,7 @@ require (
|
|||
github.com/sashko/go-uinput v0.0.0-20180923134002-15fcac7aa54a
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/sstallion/go-hid v0.0.0-20190621001400-1cf4630be9f4
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e // indirect
|
||||
gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21 // indirect
|
||||
|
|
|
@ -20,7 +20,9 @@ import (
|
|||
var (
|
||||
cfg = struct {
|
||||
Config string `flag:"config,c" vardefault:"config" description:"Configuration with page / key definitions"`
|
||||
List bool `flag:"list,l" default:"false" description:"List all available StreamDecks"`
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||
ProductID string `flag:"product-id,p" default:"" description:"Specify StreamDeck to use (use list to find ID), default first found"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
}{}
|
||||
|
||||
|
@ -80,7 +82,14 @@ func loadConfig() error {
|
|||
}
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
if cfg.List {
|
||||
listAndQuit()
|
||||
}
|
||||
|
||||
deck, err := selectDeckToUse()
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Unable to select StreamDeck to use")
|
||||
}
|
||||
|
||||
// Initalize control devices
|
||||
kbd, err = uinput.CreateKeyboard()
|
||||
|
@ -90,7 +99,7 @@ func main() {
|
|||
defer kbd.Close()
|
||||
|
||||
// Initialize device
|
||||
sd, err = streamdeck.New(streamdeck.StreamDeckOriginalV2)
|
||||
sd, err = streamdeck.New(deck)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Unable to open StreamDeck connection")
|
||||
}
|
||||
|
|
173
deck_xl.go
Normal file
173
deck_xl.go
Normal file
|
@ -0,0 +1,173 @@
|
|||
package streamdeck
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sstallion/go-hid"
|
||||
)
|
||||
|
||||
const (
|
||||
deckXLMaxPacketSize = 1024
|
||||
deckXLHeaderSize = 8
|
||||
)
|
||||
|
||||
type deckConfigXL struct {
|
||||
dev *hid.Device
|
||||
writeLock sync.Mutex
|
||||
|
||||
keyState []EventType
|
||||
}
|
||||
|
||||
func newDeckConfigXL() *deckConfigXL {
|
||||
d := &deckConfigXL{}
|
||||
d.keyState = make([]EventType, d.NumKeys())
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *deckConfigXL) SetDevice(dev *hid.Device) { d.dev = dev }
|
||||
|
||||
func (d *deckConfigXL) NumKeys() int { return 32 }
|
||||
func (d *deckConfigXL) KeyColumns() int { return 8 }
|
||||
func (d *deckConfigXL) KeyRows() int { return 4 }
|
||||
func (d *deckConfigXL) KeyDirection() keyDirection { return keyDirectionLTR }
|
||||
func (d *deckConfigXL) KeyDataOffset() int { return 4 }
|
||||
func (d *deckConfigXL) TransformKeyIndex(keyIdx int) int { return keyIdx }
|
||||
|
||||
func (d *deckConfigXL) IconSize() int { return 96 }
|
||||
func (d *deckConfigXL) IconBytes() int { return d.IconSize() * d.IconSize() * 3 }
|
||||
|
||||
func (d *deckConfigXL) Model() uint16 { return StreamDeckXL }
|
||||
|
||||
func (d *deckConfigXL) FillColor(keyIdx int, col color.RGBA) error {
|
||||
img := image.NewRGBA(image.Rect(0, 0, d.IconSize(), d.IconSize()))
|
||||
|
||||
for x := 0; x < d.IconSize(); x++ {
|
||||
for y := 0; y < d.IconSize(); y++ {
|
||||
img.Set(x, y, col)
|
||||
}
|
||||
}
|
||||
|
||||
return d.FillImage(keyIdx, img)
|
||||
}
|
||||
|
||||
func (d *deckConfigXL) FillImage(keyIdx int, img image.Image) error {
|
||||
d.writeLock.Lock()
|
||||
defer d.writeLock.Unlock()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// We need to rotate the image or it will be presented upside down
|
||||
rimg := imaging.Rotate180(img)
|
||||
|
||||
if err := jpeg.Encode(buf, rimg, &jpeg.Options{Quality: 95}); err != nil {
|
||||
return errors.Wrap(err, "Unable to encode jpeg")
|
||||
}
|
||||
|
||||
var partIndex int16
|
||||
for buf.Len() > 0 {
|
||||
chunk := make([]byte, deckXLMaxPacketSize-deckXLHeaderSize)
|
||||
n, err := buf.Read(chunk)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to read image chunk")
|
||||
}
|
||||
|
||||
var last uint8
|
||||
if n < deckXLMaxPacketSize-deckXLHeaderSize {
|
||||
last = 1
|
||||
}
|
||||
|
||||
tbuf := new(bytes.Buffer)
|
||||
tbuf.Write([]byte{0x02, 0x07, byte(keyIdx), last})
|
||||
binary.Write(tbuf, binary.LittleEndian, int16(n))
|
||||
binary.Write(tbuf, binary.LittleEndian, partIndex)
|
||||
tbuf.Write(chunk)
|
||||
|
||||
if _, err = d.dev.Write(tbuf.Bytes()); err != nil {
|
||||
return errors.Wrap(err, "Unable to send image chunk")
|
||||
}
|
||||
|
||||
partIndex++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deckConfigXL) FillPanel(img image.RGBA) error {
|
||||
if img.Bounds().Size().X < d.KeyColumns()*d.IconSize() || img.Bounds().Size().Y < d.KeyRows()*d.IconSize() {
|
||||
return errors.New("Image is too small")
|
||||
}
|
||||
|
||||
for k := 0; k < d.NumKeys(); k++ {
|
||||
var (
|
||||
ky = k / d.KeyColumns()
|
||||
kx = k % d.KeyColumns()
|
||||
)
|
||||
|
||||
if err := d.FillImage(k, img.SubImage(image.Rect(kx*d.IconSize(), ky*d.IconSize(), (kx+1)*d.IconSize(), (ky+1)*d.IconSize()))); err != nil {
|
||||
return errors.Wrap(err, "Unable to set key image")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deckConfigXL) ClearKey(keyIdx int) error {
|
||||
return d.FillColor(keyIdx, color.RGBA{0x0, 0x0, 0x0, 0xff})
|
||||
}
|
||||
|
||||
func (d *deckConfigXL) ClearAllKeys() error {
|
||||
for i := 0; i < d.NumKeys(); i++ {
|
||||
if err := d.ClearKey(i); err != nil {
|
||||
return errors.Wrap(err, "Unable to clear key")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deckConfigXL) SetBrightness(pct int) error {
|
||||
if pct < 0 || pct > 100 {
|
||||
return errors.New("Percentage out of bounds")
|
||||
}
|
||||
|
||||
_, err := d.dev.SendFeatureReport([]byte{
|
||||
0x03, 0x08, byte(pct), 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "Unable to send feature report")
|
||||
}
|
||||
|
||||
func (d *deckConfigXL) ResetToLogo() error {
|
||||
_, err := d.dev.SendFeatureReport([]byte{
|
||||
0x03,
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "Unable to send feature report")
|
||||
}
|
||||
|
||||
func (d *deckConfigXL) GetFimwareVersion() (string, error) {
|
||||
fw := make([]byte, 32)
|
||||
fw[0] = 5
|
||||
|
||||
_, err := d.dev.GetFeatureReport(fw)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Unable to get feature report")
|
||||
}
|
||||
|
||||
return strings.TrimRight(string(fw[6:]), "\x00"), nil
|
||||
}
|
|
@ -46,4 +46,5 @@ type deckConfig interface {
|
|||
|
||||
var decks = map[uint16]deckConfig{
|
||||
StreamDeckOriginalV2: &deckConfigOriginalV2{},
|
||||
StreamDeckXL: &deckConfigXL{},
|
||||
}
|
||||
|
|
|
@ -8,13 +8,20 @@ import (
|
|||
hid "github.com/sstallion/go-hid"
|
||||
)
|
||||
|
||||
const vendorElgato = 0x0fd9
|
||||
const VendorElgato = 0x0fd9
|
||||
|
||||
const (
|
||||
// Streamdeck Original V2 (0fd9:006d) 15 keys
|
||||
StreamDeckOriginalV2 uint16 = 0x006d
|
||||
// Stremdeck XL (0fd9:006c) 32 keys
|
||||
StreamDeckXL uint16 = 0x006c
|
||||
)
|
||||
|
||||
var DeckToName = map[uint16]string{
|
||||
StreamDeckOriginalV2: "StreamDeck Original V2",
|
||||
StreamDeckXL: "StreamDeck XL",
|
||||
}
|
||||
|
||||
// EventType represents the state of a button (Up / Down)
|
||||
type EventType uint8
|
||||
|
||||
|
@ -41,7 +48,7 @@ type Client struct {
|
|||
|
||||
// New creates a new Client for the given device (see constants for supported types)
|
||||
func New(devicePID uint16) (*Client, error) {
|
||||
dev, err := hid.OpenFirst(vendorElgato, devicePID)
|
||||
dev, err := hid.OpenFirst(VendorElgato, devicePID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to open device")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue