Files
qrun/lib/streamdeck/streamdeck.go
2026-03-05 11:39:13 -08:00

414 lines
8.6 KiB
Go

package streamdeck
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"image/color"
"image/jpeg"
"time"
xdraw "golang.org/x/image/draw"
"rafaelmartins.com/p/usbhid"
)
const elgatoVendorID = 0x0fd9
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
model *Model
}
func Open() (*Device, error) {
devices, err := usbhid.Enumerate(func(dev *usbhid.Device) bool {
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 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]
if err := dev.Open(true); err != nil {
return nil, fmt.Errorf("streamdeck: open: %w", err)
}
return &Device{dev: dev, model: m}, nil
}
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)
if err != nil {
return "", err
}
b, _, _ := bytes.Cut(buf[5:], []byte{0})
return string(b), nil
}
func (d *Device) SetBrightness(perc byte) error {
if perc > 100 {
perc = 100
}
pl := make([]byte, d.dev.GetFeatureReportLength())
pl[0] = 0x08
pl[1] = perc
return d.dev.SetFeatureReport(3, pl)
}
func (d *Device) Reset() error {
pl := make([]byte, d.dev.GetFeatureReportLength())
pl[0] = 0x02
return d.dev.SetFeatureReport(3, pl)
}
func (d *Device) SetKeyColor(key int, c color.Color) error {
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 >= d.model.Keys {
return fmt.Errorf("streamdeck: invalid key %d", key)
}
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)
var src image.Image = scaled
if d.model.FlipKeys {
flipped := image.NewRGBA(scaled.Bounds())
for y := range sz {
for x := range sz {
flipped.Set(sz-1-x, sz-1-y, scaled.At(x, y))
}
}
src = flipped
}
var buf bytes.Buffer
if err := jpeg.Encode(&buf, src, &jpeg.Options{Quality: 100}); err != nil {
return err
}
return d.sendKeyImage(byte(key), buf.Bytes())
}
func (d *Device) ClearKey(key int) error {
return d.SetKeyColor(key, color.Black)
}
func (d *Device) ClearAllKeys() error {
for i := 0; i < d.model.Keys; i++ {
if err := d.ClearKey(i); err != nil {
return err
}
}
return nil
}
func (d *Device) sendKeyImage(key byte, imgData []byte) error {
reportLen := d.dev.GetOutputReportLength()
hdrLen := uint16(7)
payloadLen := reportLen - hdrLen
var page byte
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 := []byte{
0x07,
key,
last,
byte(len(chunk)),
byte(len(chunk) >> 8),
page,
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
}
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(15)
payloadLen := reportLen - hdrLen
var page byte
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, 15)
hdr[0] = 0x0C
hdr[1] = byte(x)
hdr[2] = byte(x >> 8)
hdr[3] = byte(y)
hdr[4] = byte(y >> 8)
hdr[5] = byte(w)
hdr[6] = byte(w >> 8)
hdr[7] = byte(h)
hdr[8] = byte(h >> 8)
hdr[9] = last
hdr[10] = page
hdr[11] = 0
hdr[12] = byte(len(chunk))
hdr[13] = byte(len(chunk) >> 8)
hdr[14] = 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
}
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 len(buf) < 4 {
continue
}
t := time.Now()
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
}
}
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 (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)
}