1
0

Compare commits

...

3 Commits

Author SHA1 Message Date
Maksim Totmin
f144789210 reconnect: faster retry (1s) and health check (2s) 2026-06-22 10:28:43 +07:00
Maksim Totmin
20c46e18b3 daemon: pause work on device disconnect to save battery 2026-06-22 10:25:10 +07:00
Maksim Totmin
3eaf9dc0c4 text-only keys: transparent bg in web UI, #21252b bg on device 2026-06-22 10:14:06 +07:00
4 changed files with 59 additions and 8 deletions

View File

@ -9,6 +9,7 @@ import (
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"streamdeck-lets-go/internal/config"
@ -357,6 +358,7 @@ type AutoSwitchManager struct {
lastManualPage string
autoPage string
autoStay bool
paused atomic.Bool // disables auto-switch when device is disconnected
}
type compiledRule struct {
@ -410,6 +412,25 @@ func (m *AutoSwitchManager) NotifyManualPage(name string) {
slog.Debug("auto-switch: manual page set", "page", name)
}
// Pause disables auto-switch evaluation — use when the device is disconnected
// to prevent unnecessary page switches on a closed deck.
func (m *AutoSwitchManager) Pause() {
m.paused.Store(true)
slog.Debug("auto-switch: paused")
}
// Resume re-enables auto-switch evaluation after reconnect.
// Stale window events should be drained from the channel before resuming.
func (m *AutoSwitchManager) Resume() {
m.paused.Store(false)
slog.Debug("auto-switch: resumed")
}
// IsPaused returns true when auto-switch evaluation is paused.
func (m *AutoSwitchManager) IsPaused() bool {
return m.paused.Load()
}
func (m *AutoSwitchManager) Evaluate(win Window, currentPage string) (page string, shouldSwitch bool) {
m.mu.Lock()
defer m.mu.Unlock()

View File

@ -125,7 +125,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
}
})
reconnectTicker := time.NewTicker(5 * time.Second)
reconnectTicker := time.NewTicker(2 * time.Second)
defer reconnectTicker.Stop()
ssTicker := time.NewTicker(5 * time.Second)
@ -143,15 +143,27 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
case evt, ok := <-primaryDeck.Events():
if !ok {
slog.Warn("deck event channel closed, attempting reconnect...")
// Stop all periodic scripts to avoid wasting cycles on a closed device.
primaryPM.stopPeriodicKeys()
asm.Pause()
primaryDeck.Close()
newDeck := reconnectDeck(ctx, cfg, &primaryPM, asm)
newDeck := reconnectDeck(ctx, cfg, &primaryPM)
if newDeck == nil {
return ctx.Err()
}
// Update all references to the new device and page manager.
pageMgrs[0] = primaryPM
decks[0] = newDeck
primaryDeck = newDeck
web.SetDecks(decks)
web.SetPageManager(primaryPM)
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
asm.NotifyManualPage(cfg.DefaultPage)
asm.Resume()
continue
}
@ -194,6 +206,13 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
}
case win := <-windowCh:
if asm.IsPaused() {
// Drain buffered window events accumulated while disconnected.
for len(windowCh) > 0 {
<-windowCh
}
continue
}
if page, ok := asm.Evaluate(win, primaryPM.ActivePageName()); ok {
for _, pm := range pageMgrs {
if err := pm.ActivatePage(page); err != nil {
@ -263,15 +282,27 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
case <-reconnectTicker.C:
if err := primaryDeck.SetBrightness(primaryDeck.Brightness()); err != nil {
slog.Warn("deck connection lost, reconnecting...", "error", err)
// Stop all periodic scripts to avoid wasting cycles on a closed device.
primaryPM.stopPeriodicKeys()
asm.Pause()
primaryDeck.Close()
newDeck := reconnectDeck(ctx, cfg, &primaryPM, asm)
newDeck := reconnectDeck(ctx, cfg, &primaryPM)
if newDeck == nil {
return ctx.Err()
}
// Update all references to the new device and page manager.
pageMgrs[0] = primaryPM
decks[0] = newDeck
primaryDeck = newDeck
web.SetDecks(decks)
web.SetPageManager(primaryPM)
newDeck.SetBrightness(deviceBrightness(cfg, newDeck.Serial()))
asm.NotifyManualPage(cfg.DefaultPage)
asm.Resume()
}
case <-ssTicker.C:
@ -322,7 +353,7 @@ func deviceBrightness(cfg *config.Config, serial string) int {
return 75
}
func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager, asm *AutoSwitchManager) *Deck {
func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager) *Deck {
for {
newDeck, err := OpenDeck("")
if err == nil {
@ -332,13 +363,12 @@ func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager, as
(*pm).LoadPages(cfg.Pages)
(*pm).ActivatePage(cfg.DefaultPage)
(*pm).startPeriodicKeys()
asm.NotifyManualPage(cfg.DefaultPage)
return newDeck
}
select {
case <-ctx.Done():
return nil
case <-time.After(3 * time.Second):
case <-time.After(1 * time.Second):
}
}
}

View File

@ -313,7 +313,7 @@ func (pm *PageManager) renderKey(idx int, k *config.KeyConfig) {
}
slog.Warn("invalid background color", "value", k.Background, "error", err)
}
if err := pm.deck.WriteText(idx, k.Label, image.Black, fontName, fontSize); err != nil {
if err := pm.deck.WriteText(idx, k.Label, color.RGBA{0x21, 0x25, 0x2b, 0xff}, fontName, fontSize); err != nil {
slog.Warn("write text", "error", err)
}
} else if k.Background != "" {

View File

@ -123,7 +123,7 @@ func composeImageWithLabel(src image.Image, text string, keySize int, fontSize f
}
func renderTextImage(text string, keySize int, fontSize float64) image.Image {
rgba := blankImage(keySize, color.RGBA{0, 0, 0, 255}).(*image.RGBA)
rgba := blankImage(keySize, color.RGBA{0, 0, 0, 0}).(*image.RGBA)
if text != "" {
face, err := parseDisplayFace(fontSize)