Add Stream Deck+ support with encoders, touch strip, and LCD output
This commit is contained in:
@@ -2,6 +2,7 @@ package streamdeck
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
@@ -13,33 +14,83 @@ import (
|
||||
"rafaelmartins.com/p/usbhid"
|
||||
)
|
||||
|
||||
const (
|
||||
elgatoVendorID = 0x0fd9
|
||||
keyCount = 32
|
||||
keyRows = 4
|
||||
keyCols = 8
|
||||
keySize = 96
|
||||
keyStart = 3
|
||||
)
|
||||
const elgatoVendorID = 0x0fd9
|
||||
|
||||
var xlProductIDs = map[uint16]bool{
|
||||
0x006c: true,
|
||||
0x008f: true,
|
||||
type Model struct {
|
||||
Name string
|
||||
Keys int
|
||||
KeyRows int
|
||||
KeyCols int
|
||||
KeySize int
|
||||
FlipKeys bool
|
||||
Encoders int
|
||||
LCDWidth int
|
||||
LCDHeight int
|
||||
}
|
||||
|
||||
var ModelXL = Model{
|
||||
Name: "XL",
|
||||
Keys: 32,
|
||||
KeyRows: 4,
|
||||
KeyCols: 8,
|
||||
KeySize: 96,
|
||||
FlipKeys: true,
|
||||
}
|
||||
|
||||
var ModelPlus = Model{
|
||||
Name: "Plus",
|
||||
Keys: 8,
|
||||
KeyRows: 2,
|
||||
KeyCols: 4,
|
||||
KeySize: 120,
|
||||
Encoders: 4,
|
||||
LCDWidth: 800,
|
||||
LCDHeight: 100,
|
||||
}
|
||||
|
||||
var productModels = map[uint16]*Model{
|
||||
0x006c: &ModelXL,
|
||||
0x008f: &ModelXL,
|
||||
0x0084: &ModelPlus,
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
dev *usbhid.Device
|
||||
dev *usbhid.Device
|
||||
model *Model
|
||||
}
|
||||
|
||||
func Open() (*Device, error) {
|
||||
devices, err := usbhid.Enumerate(func(dev *usbhid.Device) bool {
|
||||
return dev.VendorId() == elgatoVendorID && xlProductIDs[dev.ProductId()]
|
||||
return dev.VendorId() == elgatoVendorID && productModels[dev.ProductId()] != nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("streamdeck: enumerate: %w", err)
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
return nil, fmt.Errorf("streamdeck: no XL device found")
|
||||
return nil, fmt.Errorf("streamdeck: no device found")
|
||||
}
|
||||
|
||||
dev := devices[0]
|
||||
model := productModels[dev.ProductId()]
|
||||
if err := dev.Open(true); err != nil {
|
||||
return nil, fmt.Errorf("streamdeck: open: %w", err)
|
||||
}
|
||||
|
||||
return &Device{dev: dev, model: model}, nil
|
||||
}
|
||||
|
||||
func OpenModel(m *Model) (*Device, error) {
|
||||
devices, err := usbhid.Enumerate(func(dev *usbhid.Device) bool {
|
||||
if dev.VendorId() != elgatoVendorID {
|
||||
return false
|
||||
}
|
||||
return productModels[dev.ProductId()] == m
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("streamdeck: enumerate: %w", err)
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
return nil, fmt.Errorf("streamdeck: no %s device found", m.Name)
|
||||
}
|
||||
|
||||
dev := devices[0]
|
||||
@@ -47,20 +98,13 @@ func Open() (*Device, error) {
|
||||
return nil, fmt.Errorf("streamdeck: open: %w", err)
|
||||
}
|
||||
|
||||
return &Device{dev: dev}, nil
|
||||
return &Device{dev: dev, model: m}, nil
|
||||
}
|
||||
|
||||
func (d *Device) Close() error {
|
||||
return d.dev.Close()
|
||||
}
|
||||
|
||||
func (d *Device) SerialNumber() string {
|
||||
return d.dev.SerialNumber()
|
||||
}
|
||||
|
||||
func (d *Device) Product() string {
|
||||
return d.dev.Product()
|
||||
}
|
||||
func (d *Device) Model() *Model { return d.model }
|
||||
func (d *Device) Close() error { return d.dev.Close() }
|
||||
func (d *Device) SerialNumber() string { return d.dev.SerialNumber() }
|
||||
func (d *Device) Product() string { return d.dev.Product() }
|
||||
|
||||
func (d *Device) FirmwareVersion() (string, error) {
|
||||
buf, err := d.dev.GetFeatureReport(5)
|
||||
@@ -88,32 +132,38 @@ func (d *Device) Reset() error {
|
||||
}
|
||||
|
||||
func (d *Device) SetKeyColor(key int, c color.Color) error {
|
||||
img := image.NewRGBA(image.Rect(0, 0, keySize, keySize))
|
||||
sz := d.model.KeySize
|
||||
img := image.NewRGBA(image.Rect(0, 0, sz, sz))
|
||||
xdraw.Draw(img, img.Bounds(), &image.Uniform{c}, image.Point{}, xdraw.Src)
|
||||
return d.SetKeyImage(key, img)
|
||||
}
|
||||
|
||||
func (d *Device) SetKeyImage(key int, img image.Image) error {
|
||||
if key < 0 || key >= keyCount {
|
||||
if key < 0 || key >= d.model.Keys {
|
||||
return fmt.Errorf("streamdeck: invalid key %d", key)
|
||||
}
|
||||
|
||||
scaled := image.NewRGBA(image.Rect(0, 0, keySize, keySize))
|
||||
sz := d.model.KeySize
|
||||
scaled := image.NewRGBA(image.Rect(0, 0, sz, sz))
|
||||
xdraw.BiLinear.Scale(scaled, scaled.Bounds(), img, img.Bounds(), xdraw.Over, nil)
|
||||
|
||||
flipped := image.NewRGBA(scaled.Bounds())
|
||||
for y := 0; y < keySize; y++ {
|
||||
for x := 0; x < keySize; x++ {
|
||||
flipped.Set(keySize-1-x, keySize-1-y, scaled.At(x, y))
|
||||
var src image.Image = scaled
|
||||
if d.model.FlipKeys {
|
||||
flipped := image.NewRGBA(scaled.Bounds())
|
||||
for y := 0; y < sz; y++ {
|
||||
for x := 0; x < sz; x++ {
|
||||
flipped.Set(sz-1-x, sz-1-y, scaled.At(x, y))
|
||||
}
|
||||
}
|
||||
src = flipped
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, flipped, &jpeg.Options{Quality: 100}); err != nil {
|
||||
if err := jpeg.Encode(&buf, src, &jpeg.Options{Quality: 100}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.sendImage(byte(key), buf.Bytes())
|
||||
return d.sendKeyImage(byte(key), buf.Bytes())
|
||||
}
|
||||
|
||||
func (d *Device) ClearKey(key int) error {
|
||||
@@ -121,7 +171,7 @@ func (d *Device) ClearKey(key int) error {
|
||||
}
|
||||
|
||||
func (d *Device) ClearAllKeys() error {
|
||||
for i := 0; i < keyCount; i++ {
|
||||
for i := 0; i < d.model.Keys; i++ {
|
||||
if err := d.ClearKey(i); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -129,12 +179,12 @@ func (d *Device) ClearAllKeys() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) sendImage(key byte, imgData []byte) error {
|
||||
func (d *Device) sendKeyImage(key byte, imgData []byte) error {
|
||||
reportLen := d.dev.GetOutputReportLength()
|
||||
hdrLen := uint16(7)
|
||||
hdrLen := uint16(8)
|
||||
payloadLen := reportLen - hdrLen
|
||||
|
||||
var page byte
|
||||
var page uint16
|
||||
for start := uint16(0); start < uint16(len(imgData)); page++ {
|
||||
end := start + payloadLen
|
||||
last := byte(0)
|
||||
@@ -145,13 +195,14 @@ func (d *Device) sendImage(key byte, imgData []byte) error {
|
||||
|
||||
chunk := imgData[start:end]
|
||||
hdr := []byte{
|
||||
0x02,
|
||||
0x07,
|
||||
key,
|
||||
last,
|
||||
byte(len(chunk)),
|
||||
byte(len(chunk) >> 8),
|
||||
page,
|
||||
0,
|
||||
byte(page),
|
||||
byte(page >> 8),
|
||||
}
|
||||
|
||||
payload := append(hdr, chunk...)
|
||||
@@ -166,39 +217,193 @@ func (d *Device) sendImage(key byte, imgData []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) SetLCDImage(x, y, w, h int, img image.Image) error {
|
||||
if d.model.LCDWidth == 0 {
|
||||
return fmt.Errorf("streamdeck: %s has no LCD", d.model.Name)
|
||||
}
|
||||
|
||||
scaled := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
xdraw.BiLinear.Scale(scaled, scaled.Bounds(), img, img.Bounds(), xdraw.Over, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := jpeg.Encode(&buf, scaled, &jpeg.Options{Quality: 100}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.sendLCDImage(uint16(x), uint16(y), uint16(w), uint16(h), buf.Bytes())
|
||||
}
|
||||
|
||||
func (d *Device) SetLCDColor(x, y, w, h int, c color.Color) error {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
xdraw.Draw(img, img.Bounds(), &image.Uniform{c}, image.Point{}, xdraw.Src)
|
||||
return d.SetLCDImage(x, y, w, h, img)
|
||||
}
|
||||
|
||||
func (d *Device) sendLCDImage(x, y, w, h uint16, imgData []byte) error {
|
||||
reportLen := d.dev.GetOutputReportLength()
|
||||
hdrLen := uint16(16)
|
||||
payloadLen := reportLen - hdrLen
|
||||
|
||||
var page uint16
|
||||
for start := uint16(0); start < uint16(len(imgData)); page++ {
|
||||
end := start + payloadLen
|
||||
last := byte(0)
|
||||
if end >= uint16(len(imgData)) {
|
||||
end = uint16(len(imgData))
|
||||
last = 1
|
||||
}
|
||||
|
||||
chunk := imgData[start:end]
|
||||
hdr := make([]byte, 16)
|
||||
hdr[0] = 0x02
|
||||
hdr[1] = 0x0C
|
||||
binary.LittleEndian.PutUint16(hdr[2:], x)
|
||||
binary.LittleEndian.PutUint16(hdr[4:], y)
|
||||
binary.LittleEndian.PutUint16(hdr[6:], w)
|
||||
binary.LittleEndian.PutUint16(hdr[8:], h)
|
||||
hdr[10] = last
|
||||
binary.LittleEndian.PutUint16(hdr[11:], page)
|
||||
binary.LittleEndian.PutUint16(hdr[13:], uint16(len(chunk)))
|
||||
hdr[15] = 0
|
||||
|
||||
payload := append(hdr, chunk...)
|
||||
padding := make([]byte, reportLen-uint16(len(payload)))
|
||||
payload = append(payload, padding...)
|
||||
|
||||
if err := d.dev.SetOutputReport(2, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
start = end
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type KeyEvent struct {
|
||||
Key int
|
||||
Pressed bool
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
func (d *Device) ReadKeys(ch chan<- KeyEvent) error {
|
||||
states := make([]byte, keyCount)
|
||||
type EncoderEvent struct {
|
||||
Encoder int
|
||||
Pressed bool
|
||||
Delta int
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
type TouchEvent struct {
|
||||
X int
|
||||
Y int
|
||||
X2 int
|
||||
Y2 int
|
||||
Type TouchType
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
type TouchType int
|
||||
|
||||
const (
|
||||
TouchShort TouchType = 1
|
||||
TouchLong TouchType = 2
|
||||
TouchSwipe TouchType = 3
|
||||
)
|
||||
|
||||
type InputEvent struct {
|
||||
Key *KeyEvent
|
||||
Encoder *EncoderEvent
|
||||
Touch *TouchEvent
|
||||
}
|
||||
|
||||
func (d *Device) ReadInput(ch chan<- InputEvent) error {
|
||||
keyStates := make([]byte, d.model.Keys)
|
||||
encoderStates := make([]byte, d.model.Encoders)
|
||||
for {
|
||||
_, buf, err := d.dev.GetInputReport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if int(keyStart+keyCount) > len(buf) {
|
||||
if len(buf) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
for i := 0; i < keyCount; i++ {
|
||||
st := buf[keyStart+i]
|
||||
if st != states[i] {
|
||||
ch <- KeyEvent{
|
||||
Key: i,
|
||||
Pressed: st > 0,
|
||||
Time: t,
|
||||
switch buf[0] {
|
||||
case 0x00:
|
||||
keyStart := 3
|
||||
for i := 0; i < d.model.Keys; i++ {
|
||||
if keyStart+i >= len(buf) {
|
||||
break
|
||||
}
|
||||
st := buf[keyStart+i]
|
||||
if st != keyStates[i] {
|
||||
ch <- InputEvent{Key: &KeyEvent{
|
||||
Key: i,
|
||||
Pressed: st > 0,
|
||||
Time: t,
|
||||
}}
|
||||
keyStates[i] = st
|
||||
}
|
||||
states[i] = st
|
||||
}
|
||||
case 0x03:
|
||||
if d.model.Encoders == 0 || len(buf) < 8 {
|
||||
continue
|
||||
}
|
||||
subType := buf[3]
|
||||
switch subType {
|
||||
case 0x00:
|
||||
for i := 0; i < d.model.Encoders; i++ {
|
||||
st := buf[4+i]
|
||||
if st != encoderStates[i] {
|
||||
ch <- InputEvent{Encoder: &EncoderEvent{
|
||||
Encoder: i,
|
||||
Pressed: st > 0,
|
||||
Time: t,
|
||||
}}
|
||||
encoderStates[i] = st
|
||||
}
|
||||
}
|
||||
case 0x01:
|
||||
for i := 0; i < d.model.Encoders; i++ {
|
||||
delta := int(int8(buf[4+i]))
|
||||
if delta != 0 {
|
||||
ch <- InputEvent{Encoder: &EncoderEvent{
|
||||
Encoder: i,
|
||||
Delta: delta,
|
||||
Time: t,
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
case 0x02:
|
||||
if len(buf) < 14 {
|
||||
continue
|
||||
}
|
||||
subType := TouchType(buf[3])
|
||||
x := int(binary.LittleEndian.Uint16(buf[5:7]))
|
||||
y := int(binary.LittleEndian.Uint16(buf[7:9]))
|
||||
ev := TouchEvent{
|
||||
X: x,
|
||||
Y: y,
|
||||
Type: subType,
|
||||
Time: t,
|
||||
}
|
||||
if subType == TouchSwipe {
|
||||
ev.X2 = int(binary.LittleEndian.Uint16(buf[9:11]))
|
||||
ev.Y2 = int(binary.LittleEndian.Uint16(buf[11:13]))
|
||||
}
|
||||
ch <- InputEvent{Touch: &ev}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func KeyCount() int { return keyCount }
|
||||
func KeyRows() int { return keyRows }
|
||||
func KeyCols() int { return keyCols }
|
||||
func (d *Device) ReadKeys(ch chan<- KeyEvent) error {
|
||||
input := make(chan InputEvent, 64)
|
||||
go func() {
|
||||
for ev := range input {
|
||||
if ev.Key != nil {
|
||||
ch <- *ev.Key
|
||||
}
|
||||
}
|
||||
}()
|
||||
return d.ReadInput(input)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user