From 11a24019f90f16c0a3b5903f47eb7ee2ce7522d9 Mon Sep 17 00:00:00 2001 From: Vijay Vuyyuru Date: Wed, 14 Jan 2026 10:09:28 -0500 Subject: [PATCH 1/2] Fix log spamming on disconnection/USB read error (#1) * fix massive logs * massively reduce logs * make this classy, reconnect w/ redraw --- streamdeck.go | 106 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 18 deletions(-) diff --git a/streamdeck.go b/streamdeck.go index 7edfdde..5e02df2 100644 --- a/streamdeck.go +++ b/streamdeck.go @@ -10,6 +10,7 @@ import ( "log" "os" "sync" + "time" "github.com/disintegration/gift" "github.com/golang/freetype" @@ -42,10 +43,13 @@ type BtnEvent func(s State, e Event) // StreamDeck is the object representing the Elgato Stream Deck. type StreamDeck struct { - lock sync.Mutex - device *hid.Device - btnEventCb BtnEvent - Config *Config + lock sync.Mutex + device *hid.Device + btnEventCb BtnEvent + Config *Config + serial string + buttonImages []image.Image + brightness *uint16 waitGroup sync.WaitGroup cancel context.CancelFunc @@ -83,17 +87,8 @@ func NewStreamDeck(serial ...string) (*StreamDeck, error) { return NewStreamDeckWithConfig(nil, s) } -// NewStreamDeckWithConfig is the constructor for a custom config. -func NewStreamDeckWithConfig(c *Config, serial string) (*StreamDeck, error) { - - if c == nil { - cc, found := FindConnectedConfig() - if !found { - return nil, fmt.Errorf("no streamdeck device found with any config") - } - c = &cc - } - +// getDevice enumerates and opens a StreamDeck device based on the provided config and serial number. +func getDevice(c *Config, serial string) (*hid.Device, error) { devices := hid.Enumerate(VendorID, c.ProductID) if len(devices) == 0 { @@ -125,9 +120,31 @@ func NewStreamDeckWithConfig(c *Config, serial string) (*StreamDeck, error) { log.Printf("Connected to StreamDeck: %v", devices[id]) + return device, nil +} + +// NewStreamDeckWithConfig is the constructor for a custom config. +func NewStreamDeckWithConfig(c *Config, serial string) (*StreamDeck, error) { + + if c == nil { + cc, found := FindConnectedConfig() + if !found { + return nil, fmt.Errorf("no streamdeck device found with any config") + } + c = &cc + } + + device, err := getDevice(c, serial) + if err != nil { + return nil, err + } + sd := &StreamDeck{ - device: device, - Config: c, + device: device, + Config: c, + serial: serial, + buttonImages: make([]image.Image, c.NumButtons()), + brightness: nil, } sd.ClearAllBtns() @@ -155,11 +172,49 @@ func (sd *StreamDeck) read(ctx context.Context) { defer sd.waitGroup.Done() myState := State{} + var lastErr error + var lastErrTime time.Time + + var lastReconnectionErr error + var lastReconnectionErrTime time.Time + for ctx.Err() == nil { data := make([]byte, 24) _, err := sd.device.Read(data) if err != nil { - fmt.Println(err) + lastErr, lastErrTime = logErrorIfNew(err, lastErr, lastErrTime) + + device, err := getDevice(sd.Config, sd.serial) + if err != nil { + lastReconnectionErr, lastReconnectionErrTime = logErrorIfNew(err, lastReconnectionErr, lastReconnectionErrTime) + time.Sleep(200 * time.Millisecond) + } else { + sd.lock.Lock() + sd.device = device + // Save button images before clearing + savedImages := make([]image.Image, len(sd.buttonImages)) + copy(savedImages, sd.buttonImages) + sd.lock.Unlock() + + sd.ClearAllBtns() + // Restore and redraw all buttons that had images stored + for i := range savedImages { + if savedImages[i] != nil { + err = sd.FillImage(i, savedImages[i]) + if err != nil { + fmt.Printf("Failed to redraw button %d after reconnection: %v\n", i, err) + } + } + } + if sd.brightness != nil { + err = sd.SetBrightness(*sd.brightness) + if err != nil { + fmt.Printf("Failed to set brightness after reconnection: %v\n", err) + } + } + lastReconnectionErr = nil + lastReconnectionErrTime = time.Time{} + } continue } @@ -310,6 +365,8 @@ func (sd *StreamDeck) FillImage(btnIndex int, img image.Image) error { sd.lock.Lock() defer sd.lock.Unlock() + sd.buttonImages[btnIndex] = img + if sd.Config.ImageFormat == "bmp" { splitPoint := 7803 err := sd.sendOriginalSingleMsgInLock(btnIndex, 1, imgBuf[0:splitPoint]) @@ -510,6 +567,9 @@ func (sd *StreamDeck) checkValidKeyIndex(keyIndex int) error { // b 0 -> 100 func (sd *StreamDeck) SetBrightness(b uint16) error { + sd.lock.Lock() + sd.brightness = &b + sd.lock.Unlock() buf := []byte{0x03, 0x08, 0xFF, 0xFF} binary.LittleEndian.PutUint16(buf[2:], b) @@ -547,3 +607,13 @@ func checkRGB(value int) error { } return nil } + +// logErrorIfNew logs an error if it's a new error or if it's been at least a minute since the last identical error. +// It returns the updated lastErr and lastErrTime when the error is logged, otherwise returns the original values. +func logErrorIfNew(err error, lastErr error, lastErrTime time.Time) (error, time.Time) { + if lastErr == nil || err.Error() != lastErr.Error() || time.Since(lastErrTime) >= time.Minute { + fmt.Println(err) + return err, time.Now() + } + return lastErr, lastErrTime +} From fe1c72dc6b5a2f46aadd283df13f591ae21f24e4 Mon Sep 17 00:00:00 2001 From: Vijay Vuyyuru Date: Thu, 19 Mar 2026 17:39:53 -0400 Subject: [PATCH 2/2] fix --- streamdeck.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/streamdeck.go b/streamdeck.go index 5e02df2..5c68894 100644 --- a/streamdeck.go +++ b/streamdeck.go @@ -180,10 +180,14 @@ func (sd *StreamDeck) read(ctx context.Context) { for ctx.Err() == nil { data := make([]byte, 24) + // note: when device has been closed, err will be "hid: device closed" _, err := sd.device.Read(data) if err != nil { lastErr, lastErrTime = logErrorIfNew(err, lastErr, lastErrTime) + if ctx.Err() != nil { + break + } device, err := getDevice(sd.Config, sd.serial) if err != nil { lastReconnectionErr, lastReconnectionErrTime = logErrorIfNew(err, lastReconnectionErr, lastReconnectionErrTime)