mirror of
https://github.com/Luzifer/streamdeck.git
synced 2024-12-20 09:41:19 +00:00
Initial library version
This commit is contained in:
commit
4eb30f3a4a
5 changed files with 320 additions and 0 deletions
163
deck_original_v2.go
Normal file
163
deck_original_v2.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package streamdeck
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sstallion/go-hid"
|
||||
)
|
||||
|
||||
const (
|
||||
deckOriginalV2MaxPacketSize = 1024
|
||||
deckOriginalV2HeaderSize = 8
|
||||
)
|
||||
|
||||
type deckConfigOriginalV2 struct {
|
||||
dev *hid.Device
|
||||
|
||||
keyState []EventType
|
||||
}
|
||||
|
||||
func newDeckConfigOriginalV2() *deckConfigOriginalV2 {
|
||||
d := &deckConfigOriginalV2{}
|
||||
d.keyState = make([]EventType, d.NumKeys())
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *deckConfigOriginalV2) SetDevice(dev *hid.Device) { d.dev = dev }
|
||||
|
||||
func (d deckConfigOriginalV2) NumKeys() int { return 15 }
|
||||
func (d deckConfigOriginalV2) KeyColumns() int { return 5 }
|
||||
func (d deckConfigOriginalV2) KeyRows() int { return 3 }
|
||||
func (d deckConfigOriginalV2) KeyDirection() keyDirection { return keyDirectionLTR }
|
||||
func (d deckConfigOriginalV2) KeyDataOffset() int { return 4 }
|
||||
func (d deckConfigOriginalV2) TransformKeyIndex(keyIdx int) int { return keyIdx }
|
||||
|
||||
func (d deckConfigOriginalV2) IconSize() int { return 72 }
|
||||
func (d deckConfigOriginalV2) IconBytes() int { return d.IconSize() * d.IconSize() * 3 }
|
||||
|
||||
func (d deckConfigOriginalV2) Model() uint16 { return StreamDeckOriginalV2 }
|
||||
|
||||
func (d deckConfigOriginalV2) 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 deckConfigOriginalV2) FillImage(keyIdx int, img image.Image) error {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := jpeg.Encode(buf, img, &jpeg.Options{Quality: 95}); err != nil {
|
||||
return errors.Wrap(err, "Unable to encode jpeg")
|
||||
}
|
||||
|
||||
var partIndex int16
|
||||
for buf.Len() > 0 {
|
||||
chunk := make([]byte, deckOriginalV2MaxPacketSize-deckOriginalV2HeaderSize)
|
||||
n, err := buf.Read(chunk)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to read image chunk")
|
||||
}
|
||||
|
||||
var last uint8
|
||||
if n < deckOriginalV2MaxPacketSize-deckOriginalV2HeaderSize {
|
||||
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 deckConfigOriginalV2) 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 deckConfigOriginalV2) ClearKey(keyIdx int) error {
|
||||
return d.FillColor(keyIdx, color.RGBA{0x0, 0x0, 0x0, 0xff})
|
||||
}
|
||||
|
||||
func (d deckConfigOriginalV2) 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 deckConfigOriginalV2) 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 deckConfigOriginalV2) 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 deckConfigOriginalV2) 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
|
||||
}
|
8
go.mod
Normal file
8
go.mod
Normal file
|
@ -0,0 +1,8 @@
|
|||
module github.com/Luzifer/streamdeck
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/sstallion/go-hid v0.0.0-20190621001400-1cf4630be9f4
|
||||
)
|
4
go.sum
Normal file
4
go.sum
Normal file
|
@ -0,0 +1,4 @@
|
|||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/sstallion/go-hid v0.0.0-20190621001400-1cf4630be9f4 h1:hczfXYN39SDj4FcN5J7sgHBtJm4U7ef2nvlonn6NvVU=
|
||||
github.com/sstallion/go-hid v0.0.0-20190621001400-1cf4630be9f4/go.mod h1:JwBz6izP5UGcbYDU5VGeLkqpRIpSBDPtCB5/XnVXz9Q=
|
49
interface.go
Normal file
49
interface.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package streamdeck
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/sstallion/go-hid"
|
||||
)
|
||||
|
||||
type keyDirection uint
|
||||
|
||||
const (
|
||||
keyDirectionLTR keyDirection = iota
|
||||
keyDirectionRTL
|
||||
)
|
||||
|
||||
type deckConfig interface {
|
||||
SetDevice(dev *hid.Device)
|
||||
|
||||
NumKeys() int
|
||||
KeyColumns() int
|
||||
KeyRows() int
|
||||
KeyDirection() keyDirection
|
||||
KeyDataOffset() int
|
||||
|
||||
TransformKeyIndex(keyIdx int) int
|
||||
|
||||
IconSize() int
|
||||
IconBytes() int
|
||||
|
||||
Model() uint16
|
||||
|
||||
FillColor(keyIdx int, col color.RGBA) error
|
||||
FillImage(keyIdx int, img image.Image) error
|
||||
FillPanel(img image.RGBA) error
|
||||
|
||||
ClearKey(keyIdx int) error
|
||||
ClearAllKeys() error
|
||||
|
||||
SetBrightness(pct int) error
|
||||
|
||||
ResetToLogo() error
|
||||
|
||||
GetFimwareVersion() (string, error)
|
||||
}
|
||||
|
||||
var decks = map[uint16]deckConfig{
|
||||
StreamDeckOriginalV2: &deckConfigOriginalV2{},
|
||||
}
|
96
streamdeck.go
Normal file
96
streamdeck.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package streamdeck
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
hid "github.com/sstallion/go-hid"
|
||||
)
|
||||
|
||||
const vendorElgato = 0x0fd9
|
||||
|
||||
const (
|
||||
StreamDeckOriginalV2 uint16 = 0x006d
|
||||
)
|
||||
|
||||
type EventType uint8
|
||||
|
||||
const (
|
||||
EventTypeUp EventType = iota
|
||||
EventTypeDown
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Key int
|
||||
Type EventType
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
cfg deckConfig
|
||||
dev *hid.Device
|
||||
devType uint16
|
||||
keyStates []EventType
|
||||
|
||||
evts chan Event
|
||||
}
|
||||
|
||||
func New(devicePID uint16) (*Client, error) {
|
||||
dev, err := hid.OpenFirst(vendorElgato, devicePID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to open device")
|
||||
}
|
||||
|
||||
cfg := decks[devicePID]
|
||||
cfg.SetDevice(dev)
|
||||
|
||||
client := &Client{
|
||||
cfg: cfg,
|
||||
dev: dev,
|
||||
devType: devicePID,
|
||||
keyStates: make([]EventType, cfg.NumKeys()),
|
||||
|
||||
evts: make(chan Event, 100),
|
||||
}
|
||||
|
||||
go client.read()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c Client) Close() error { return c.dev.Close() }
|
||||
func (c Client) Serial() (string, error) { return c.dev.GetSerialNbr() }
|
||||
|
||||
func (c Client) FillColor(keyIdx int, col color.RGBA) error { return c.cfg.FillColor(keyIdx, col) }
|
||||
func (c Client) FillImage(keyIdx int, img image.Image) error { return c.cfg.FillImage(keyIdx, img) }
|
||||
func (c Client) FillPanel(img image.RGBA) error { return c.cfg.FillPanel(img) }
|
||||
|
||||
func (c Client) ClearKey(keyIdx int) error { return c.cfg.ClearKey(keyIdx) }
|
||||
func (c Client) ClearAllKeys() error { return c.cfg.ClearAllKeys() }
|
||||
|
||||
func (c Client) SetBrightness(pct int) error { return c.cfg.SetBrightness(pct) }
|
||||
|
||||
func (c Client) ResetToLogo() error { return c.cfg.ResetToLogo() }
|
||||
|
||||
func (c Client) GetFimwareVersion() (string, error) { return c.cfg.GetFimwareVersion() }
|
||||
|
||||
func (c Client) Subscribe() <-chan Event { return c.evts }
|
||||
func (c Client) emit(key int, t EventType) { c.evts <- Event{Key: key, Type: t} }
|
||||
|
||||
func (c *Client) read() {
|
||||
for {
|
||||
buf := make([]byte, 1024)
|
||||
_, err := c.dev.Read(buf)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for k := 0; k < c.cfg.NumKeys(); k++ {
|
||||
newState := EventType(buf[k+c.cfg.KeyDataOffset()])
|
||||
if c.keyStates[k] != newState {
|
||||
c.emit(k, newState)
|
||||
c.keyStates[k] = newState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue