package main import ( "fmt" "image" "image/color" "image/draw" "log/slog" "sync" "github.com/bearsh/hid" "github.com/dh1tw/streamdeck" "github.com/disintegration/gift" ) type DeviceInfo struct { Serial string Model string PID uint16 } type DeckConfig struct { PID uint16 Name string KeysX int KeysY int KeySize int ImageFmt string Rotate bool Convert bool HasDials bool HasTouch bool } var knownDecks = []DeckConfig{ {PID: 0x60, Name: "Mini", KeysX: 3, KeysY: 2, KeySize: 80, ImageFmt: "bmp", Convert: true}, {PID: 0x6d, Name: "Original", KeysX: 5, KeysY: 3, KeySize: 72, ImageFmt: "jpg", Rotate: true}, {PID: 0x63, Name: "OriginalV2",KeysX: 5, KeysY: 3, KeySize: 72, ImageFmt: "bmp"}, {PID: 0x80, Name: "MK2", KeysX: 5, KeysY: 3, KeySize: 72, ImageFmt: "jpg", Rotate: true}, {PID: 0x6c, Name: "XL", KeysX: 8, KeysY: 4, KeySize: 96, ImageFmt: "jpg"}, } func findConfig(pid uint16) (DeckConfig, bool) { for _, d := range knownDecks { if d.PID == pid { return d, true } } return DeckConfig{}, false } func EnumerateDevices() ([]DeviceInfo, error) { devices := hid.Enumerate(streamdeck.VendorID, 0) if len(devices) == 0 { return nil, nil } var infos []DeviceInfo for _, d := range devices { info := DeviceInfo{Serial: d.Serial, PID: d.ProductID} if cfg, ok := findConfig(d.ProductID); ok { info.Model = cfg.Name } else { info.Model = fmt.Sprintf("Unknown (0x%04x)", d.ProductID) } infos = append(infos, info) } return infos, nil } func toStreamDeckConfig(dc DeckConfig) *streamdeck.Config { return &streamdeck.Config{ ProductID: dc.PID, NumButtonColumns: dc.KeysX, NumButtonRows: dc.KeysY, Spacer: 19, ButtonSize: dc.KeySize, ImageFormat: dc.ImageFmt, ImageRotate: dc.Rotate, ConvertKey: dc.Convert, } } type Event struct { Kind int Index int } const ( EventKeyPressed = 1 EventKeyReleased = 2 ) type Deck struct { sd *streamdeck.StreamDeck cfg DeckConfig serial string mu sync.Mutex events chan Event closed bool brightness int } func OpenDeck(serial string) (*Deck, error) { desiredSerial := serial if serial == "first" || serial == "" { desiredSerial = "" } sd, err := streamdeck.NewStreamDeck(desiredSerial) if err != nil { return nil, fmt.Errorf("opening streamdeck: %w", err) } cfg, ok := findConfig(sd.Config.ProductID) if !ok { sd.Close() return nil, fmt.Errorf("unsupported device PID: 0x%04x", sd.Config.ProductID) } d := &Deck{ sd: sd, cfg: cfg, serial: sd.Serial(), events: make(chan Event, 64), brightness: 75, } sd.SetBtnEventCb(func(s streamdeck.State, e streamdeck.Event) { kind := 0 switch e.Kind { case streamdeck.EventKeyPressed: kind = EventKeyPressed case streamdeck.EventKeyReleased: kind = EventKeyReleased default: return } select { case d.events <- Event{Kind: kind, Index: e.Which}: default: slog.Warn("event channel full, dropping event", "kind", e.Kind, "index", e.Which) } }) slog.Info("deck opened", "serial", d.serial, "model", cfg.Name, "keys", cfg.KeysX*cfg.KeysY) return d, nil } func (d *Deck) Close() { d.mu.Lock() defer d.mu.Unlock() if d.closed { return } d.closed = true close(d.events) d.sd.Close() } func (d *Deck) Serial() string { return d.serial } func (d *Deck) Events() <-chan Event { return d.events } func (d *Deck) NumKeys() int { return d.cfg.KeysX * d.cfg.KeysY } func (d *Deck) KeySize() int { return d.cfg.KeySize } func (d *Deck) Config() DeckConfig { return d.cfg } func (d *Deck) SetBrightness(val int) error { if val < 0 { val = 0 } if val > 100 { val = 100 } d.mu.Lock() defer d.mu.Unlock() if d.closed { return fmt.Errorf("deck closed") } d.brightness = val return d.sd.SetBrightness(uint16(val)) } func (d *Deck) Brightness() int { d.mu.Lock() defer d.mu.Unlock() return d.brightness } func (d *Deck) FillColor(keyIndex int, r, g, b uint8) error { d.mu.Lock() defer d.mu.Unlock() if d.closed { return fmt.Errorf("deck closed") } return d.sd.FillColor(keyIndex, int(r), int(g), int(b)) } func (d *Deck) FillImage(keyIndex int, img image.Image) error { d.mu.Lock() defer d.mu.Unlock() if d.closed { return fmt.Errorf("deck closed") } return d.sd.FillImage(keyIndex, img) } func (d *Deck) FillPanel(img image.Image) error { d.mu.Lock() defer d.mu.Unlock() if d.closed { return fmt.Errorf("deck closed") } return d.sd.FillPanel(img) } func (d *Deck) ClearAll() error { d.mu.Lock() defer d.mu.Unlock() if d.closed { return fmt.Errorf("deck closed") } return d.sd.ClearAllBtns() } func (d *Deck) ClearKey(idx int) error { d.mu.Lock() defer d.mu.Unlock() if d.closed { return fmt.Errorf("deck closed") } return d.sd.ClearBtn(idx) } func (d *Deck) WriteText(keyIndex int, text string, bg color.Color, fontName string, fontSize float64) error { d.mu.Lock() defer d.mu.Unlock() if d.closed { return fmt.Errorf("deck closed") } if fontSize <= 0 { fontSize = 18 } font := streamdeck.MonoMedium if fontName == "regular" { font = streamdeck.MonoRegular } ks := d.cfg.KeySize charW := fontSize * 0.55 textW := int(float64(len(text)) * charW) posX := (ks - textW) / 2 if posX < 2 { posX = 2 } posY := ks/2 - 24 + int(fontSize/3) if posY < 2 { posY = 2 } return d.sd.WriteText(keyIndex, streamdeck.TextButton{ Lines: []streamdeck.TextLine{ { Text: text, PosX: posX, PosY: posY, Font: font, FontSize: fontSize, FontColor: color.White, }, }, BgColor: bg, }) } func (d *Deck) WriteTextOnImage(keyIndex int, img image.Image, text, fontName string, fontSize float64) error { ks := d.cfg.KeySize g := gift.New( gift.Resize(ks, ks, gift.LanczosResampling), ) rgba := image.NewRGBA(image.Rect(0, 0, ks, ks)) g.Draw(rgba, img) lines := []streamdeck.TextLine{} if text != "" { if fontSize <= 0 { fontSize = 16 } font := streamdeck.MonoRegular if fontName == "medium" { font = streamdeck.MonoMedium } barHeight := 20 if ks < 72 { barHeight = 18 } barRect := image.Rect(0, ks-barHeight, ks, ks) draw.Draw(rgba, barRect, &image.Uniform{color.RGBA{0, 0, 0, 180}}, image.Point{}, draw.Over) offsetY := 24 baselineY := ks - 6 posY := baselineY - offsetY if posY < 2 { posY = 2 } charW := fontSize * 0.55 textW := int(float64(len(text)) * charW) posX := (ks - textW) / 2 if posX < 2 { posX = 2 } lines = append(lines, streamdeck.TextLine{ Text: text, PosX: posX, PosY: posY, Font: font, FontSize: fontSize, FontColor: color.White, }) } d.mu.Lock() defer d.mu.Unlock() if d.closed { return fmt.Errorf("deck closed") } return d.sd.WriteTextOnImage(keyIndex, rgba, lines) }