Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions cmd/demo/demo-cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ func realMain() error {
return err
}

// If this is a Stream Deck Plus, demonstrate the LCD panel
if sd.Config.HasLCD() {
log.Printf("Stream Deck Plus detected - demonstrating LCD panel")

// Fill each LCD segment with a different color and label
segments := []struct {
color color.RGBA
label string
}{
{color.RGBA{255, 0, 0, 255}, "Red"},
{color.RGBA{0, 255, 0, 255}, "Green"},
{color.RGBA{0, 0, 255, 255}, "Blue"},
{color.RGBA{255, 255, 0, 255}, "Yellow"},
}

for i, seg := range segments {
err = sd.WriteTextToLCDSegment(i, seg.color, []streamdeck.LCDTextLine{
{Text: seg.label, PosX: 50, PosY: 40, FontSize: 24, FontColor: color.RGBA{255, 255, 255, 255}},
})
if err != nil {
return err
}
}
}

// capture Streamdeck events
sd.SetBtnEventCb(func(s streamdeck.State, e streamdeck.Event) {
log.Printf("got event: %v state: %v", e, s)
Expand Down
20 changes: 20 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ type Config struct {
ImageFormat string
ImageRotate bool
ConvertKey bool
// LCD touchscreen panel (Stream Deck Plus only)
LCDWidth int // Width of the LCD touchscreen panel (0 if not present)
LCDHeight int // Height of the LCD touchscreen panel
NumEncoders int // Number of rotary encoders (for calculating LCD segment widths)
}

func (c Config) NumButtons() int {
Expand All @@ -29,6 +33,19 @@ func (c *Config) PanelHeight() int {
return c.NumButtonRows*c.ButtonSize + c.Spacer*(c.NumButtonRows-1)
}

// HasLCD returns true if this Stream Deck model has an LCD touchscreen panel.
func (c *Config) HasLCD() bool {
return c.LCDWidth > 0 && c.LCDHeight > 0
}

// LCDSegmentWidth returns the width of each LCD segment (area behind each encoder).
func (c *Config) LCDSegmentWidth() int {
if c.NumEncoders == 0 {
return 0
}
return c.LCDWidth / c.NumEncoders
}

func (c *Config) fixKey(key int) int {
if c.ConvertKey {
keyCol := key % c.NumButtonColumns
Expand Down Expand Up @@ -76,6 +93,9 @@ var Plus = Config{
Spacer: 19,
ButtonSize: 120,
ImageFormat: "jpg",
LCDWidth: 800,
LCDHeight: 100,
NumEncoders: 4,
}

var AllConfigs = []Config{Original, OriginalMk1, Original2, Plus}
Expand Down
252 changes: 251 additions & 1 deletion streamdeck.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func (sd *StreamDeck) read(ctx context.Context) {
continue
}

debug("read data:", data)
debug("read data: %v", data)

events, err := myState.Update(sd.Config, data)
if err != nil {
Expand Down Expand Up @@ -457,6 +457,166 @@ func (sd *StreamDeck) FillPanelFromFile(path string) error {
return sd.FillPanel(img)
}

// FillLCD fills the entire LCD touchscreen panel with an image (Stream Deck Plus only).
// The image will be resized to fit the LCD dimensions (800x100 pixels).
func (sd *StreamDeck) FillLCD(img image.Image) error {
if !sd.Config.HasLCD() {
return fmt.Errorf("this Stream Deck model does not have an LCD panel")
}

return sd.fillLCDRegion(img, 0, sd.Config.LCDWidth, sd.Config.LCDHeight)
}

// FillLCDSegment fills a specific segment of the LCD panel (the area behind each encoder).
// Segment index is 0-3 for Stream Deck Plus (left to right).
func (sd *StreamDeck) FillLCDSegment(segmentIndex int, img image.Image) error {
if !sd.Config.HasLCD() {
return fmt.Errorf("this Stream Deck model does not have an LCD panel")
}

if segmentIndex < 0 || segmentIndex >= sd.Config.NumEncoders {
return fmt.Errorf("invalid segment index %d, must be 0-%d", segmentIndex, sd.Config.NumEncoders-1)
}

segmentWidth := sd.Config.LCDSegmentWidth()
xOffset := segmentIndex * segmentWidth

return sd.fillLCDRegion(img, xOffset, segmentWidth, sd.Config.LCDHeight)
}

// fillLCDRegion writes an image to a specific region of the LCD panel.
func (sd *StreamDeck) fillLCDRegion(img image.Image, xOffset, width, height int) error {
// Resize image to fit the target region
rect := img.Bounds()
if rect.Dx() != width || rect.Dy() != height {
img = resize(img, width, height)
}

// Encode as JPEG
buf := bytes.Buffer{}
err := jpeg.Encode(&buf, img, nil)
if err != nil {
return err
}
imgBuf := buf.Bytes()

sd.lock.Lock()
defer sd.lock.Unlock()

// LCD uses 16-byte header with 1024-byte packets
headerSize := 16
bytesLeft := len(imgBuf)
pos := 0
chunkIndex := uint16(0)

for bytesLeft > 0 {
imgToSend := min(bytesLeft, 1024-headerSize)
bytesLeft -= imgToSend

packet := make([]byte, 1024)
packet[0] = 0x02
packet[1] = 0x0C
binary.LittleEndian.PutUint16(packet[2:], uint16(xOffset)) // X offset
packet[4] = 0x00 // constant
packet[5] = 0x00 // constant
binary.LittleEndian.PutUint16(packet[6:], uint16(width)) // image width
binary.LittleEndian.PutUint16(packet[8:], uint16(height)) // image height
if bytesLeft == 0 {
packet[10] = 0x01 // final chunk
} else {
packet[10] = 0x00
}
binary.LittleEndian.PutUint16(packet[11:], chunkIndex) // chunk index
binary.LittleEndian.PutUint16(packet[13:], uint16(imgToSend)) // payload length
packet[15] = 0x00 // constant

copy(packet[16:], imgBuf[pos:(pos+imgToSend)])

debug("LCD write: xOffset=%d width=%d height=%d chunk=%d bytesLeft=%d imgToSend=%d",
xOffset, width, height, chunkIndex, bytesLeft, imgToSend)

n, err := sd.device.Write(packet)
if err != nil {
return err
}
if n != len(packet) {
return fmt.Errorf("only wrote %d of %d", n, len(packet))
}

chunkIndex++
pos += imgToSend
}

return nil
}

// FillLCDFromFile fills the entire LCD panel with an image from a file.
func (sd *StreamDeck) FillLCDFromFile(path string) error {
reader, err := os.Open(path)
if err != nil {
return err
}
defer reader.Close()

img, _, err := image.Decode(reader)
if err != nil {
return err
}

return sd.FillLCD(img)
}

// FillLCDSegmentFromFile fills a specific LCD segment with an image from a file.
func (sd *StreamDeck) FillLCDSegmentFromFile(segmentIndex int, path string) error {
reader, err := os.Open(path)
if err != nil {
return err
}
defer reader.Close()

img, _, err := image.Decode(reader)
if err != nil {
return err
}

return sd.FillLCDSegment(segmentIndex, img)
}

// ClearLCD fills the entire LCD panel with black.
func (sd *StreamDeck) ClearLCD() error {
if !sd.Config.HasLCD() {
return fmt.Errorf("this Stream Deck model does not have an LCD panel")
}

img := image.NewRGBA(image.Rect(0, 0, sd.Config.LCDWidth, sd.Config.LCDHeight))
draw.Draw(img, img.Bounds(), image.NewUniform(color.Black), image.Point{0, 0}, draw.Src)

return sd.FillLCD(img)
}

// FillLCDColor fills the entire LCD panel with a solid color.
func (sd *StreamDeck) FillLCDColor(r, g, b int) error {
if !sd.Config.HasLCD() {
return fmt.Errorf("this Stream Deck model does not have an LCD panel")
}

if err := checkRGB(r); err != nil {
return err
}
if err := checkRGB(g); err != nil {
return err
}
if err := checkRGB(b); err != nil {
return err
}

img := image.NewRGBA(image.Rect(0, 0, sd.Config.LCDWidth, sd.Config.LCDHeight))
c := color.RGBA{uint8(r), uint8(g), uint8(b), 255}
draw.Draw(img, img.Bounds(), image.NewUniform(c), image.Point{0, 0}, draw.Src)

return sd.FillLCD(img)
}

// WriteText can write several lines of Text to a button. It is up to the
// user to ensure that the lines fit properly on the button.
func (sd *StreamDeck) WriteText(btnIndex int, textBtn TextButton) error {
Expand Down Expand Up @@ -508,6 +668,96 @@ func (sd *StreamDeck) checkValidKeyIndex(keyIndex int) error {
return nil
}

// LCDTextLine holds the content of one text line for LCD display.
type LCDTextLine struct {
Text string
PosX int
PosY int
Font *truetype.Font
FontSize float64
FontColor color.Color
}

// WriteTextToLCD writes text lines to the entire LCD panel with a background color.
func (sd *StreamDeck) WriteTextToLCD(bgColor color.Color, lines []LCDTextLine) error {
if !sd.Config.HasLCD() {
return fmt.Errorf("this Stream Deck model does not have an LCD panel")
}

img := image.NewRGBA(image.Rect(0, 0, sd.Config.LCDWidth, sd.Config.LCDHeight))
draw.Draw(img, img.Bounds(), image.NewUniform(bgColor), image.Point{0, 0}, draw.Src)

return sd.WriteTextToLCDOnImage(img, lines)
}

// WriteTextToLCDOnImage writes text lines to the LCD panel on top of a provided image.
func (sd *StreamDeck) WriteTextToLCDOnImage(imgIn image.Image, lines []LCDTextLine) error {
if !sd.Config.HasLCD() {
return fmt.Errorf("this Stream Deck model does not have an LCD panel")
}

img := resize(imgIn, sd.Config.LCDWidth, sd.Config.LCDHeight)

for _, line := range lines {
font := line.Font
if font == nil {
font = MonoRegular
}
fontColor := image.NewUniform(line.FontColor)
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(font)
c.SetFontSize(line.FontSize)
c.SetClip(img.Bounds())
c.SetDst(img)
c.SetSrc(fontColor)
pt := freetype.Pt(line.PosX, line.PosY+int(c.PointToFixed(line.FontSize)>>6))

if _, err := c.DrawString(line.Text, pt); err != nil {
return err
}
}

return sd.FillLCD(img)
}

// WriteTextToLCDSegment writes text lines to a specific LCD segment with a background color.
func (sd *StreamDeck) WriteTextToLCDSegment(segmentIndex int, bgColor color.Color, lines []LCDTextLine) error {
if !sd.Config.HasLCD() {
return fmt.Errorf("this Stream Deck model does not have an LCD panel")
}

if segmentIndex < 0 || segmentIndex >= sd.Config.NumEncoders {
return fmt.Errorf("invalid segment index %d, must be 0-%d", segmentIndex, sd.Config.NumEncoders-1)
}

segmentWidth := sd.Config.LCDSegmentWidth()
img := image.NewRGBA(image.Rect(0, 0, segmentWidth, sd.Config.LCDHeight))
draw.Draw(img, img.Bounds(), image.NewUniform(bgColor), image.Point{0, 0}, draw.Src)

for _, line := range lines {
font := line.Font
if font == nil {
font = MonoRegular
}
fontColor := image.NewUniform(line.FontColor)
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(font)
c.SetFontSize(line.FontSize)
c.SetClip(img.Bounds())
c.SetDst(img)
c.SetSrc(fontColor)
pt := freetype.Pt(line.PosX, line.PosY+int(c.PointToFixed(line.FontSize)>>6))

if _, err := c.DrawString(line.Text, pt); err != nil {
return err
}
}

return sd.FillLCDSegment(segmentIndex, img)
}

// b 0 -> 100
func (sd *StreamDeck) SetBrightness(b uint16) error {

Expand Down