diff --git a/cmd/streamdeck/main.go b/cmd/streamdeck/main.go index 8d62e21..c571662 100644 --- a/cmd/streamdeck/main.go +++ b/cmd/streamdeck/main.go @@ -172,6 +172,7 @@ func run(ctx context.Context, sd *device.StreamDeck, cfg *config.Config, reg *mo for keyIdx, keyCfg := range cfg.Keys { // Resolve module-based commands to shell strings before any other logic. + // keyCfg is a copy from the map, so we must write it back after modification. if keyCfg.Module != "" { resolved, err := reg.Resolve(keyCfg.Module, keyCfg.Function, keyCfg.Params) if err != nil { @@ -179,6 +180,7 @@ func run(ctx context.Context, sd *device.StreamDeck, cfg *config.Config, reg *mo continue } keyCfg.Command = resolved + cfg.Keys[keyIdx] = keyCfg } if keyCfg.Poll != nil && keyCfg.Poll.Module != "" { resolved, err := reg.Resolve(keyCfg.Poll.Module, keyCfg.Poll.Function, keyCfg.Poll.Params) @@ -187,6 +189,7 @@ func run(ctx context.Context, sd *device.StreamDeck, cfg *config.Config, reg *mo continue } keyCfg.Poll.Command = resolved + cfg.Keys[keyIdx] = keyCfg } // Toggle/status key: managed by a polling goroutine. @@ -414,7 +417,14 @@ func mustConnect(vendorID, productID uint16) *device.StreamDeck { } // animateKey cycles pre-encoded GIF frames on a key until ctx is cancelled. +// +// Transient HID write errors (USB bus contention, especially with multiple +// concurrent GIFs) are suppressed after the first occurrence to avoid log spam. +// A summary is logged when errors stop or when they become persistent enough +// to indicate a real device problem. func animateKey(ctx context.Context, sd *device.StreamDeck, keyIdx int, frames [][]byte, delays []time.Duration) { + var writeErrors int // consecutive HID write failures + for { for i, frame := range frames { select { @@ -423,7 +433,20 @@ func animateKey(ctx context.Context, sd *device.StreamDeck, keyIdx int, frames [ default: } if err := sd.SetKeyFrame(keyIdx, frame); err != nil { - log.Printf("key %d: animate frame %d: %v", keyIdx, i, err) + writeErrors++ + if writeErrors == 1 { + // Log the first error so the user knows something happened. + log.Printf("key %d: frame write error: %v", keyIdx, err) + } else if writeErrors == 50 { + // Persistent errors — likely a real device issue, not transient. + log.Printf("key %d: %d consecutive frame write errors — device may be unhealthy", keyIdx, writeErrors) + } + } else if writeErrors > 0 { + // Recovered — log a summary if we suppressed errors. + if writeErrors > 1 { + log.Printf("key %d: recovered after %d frame write errors", keyIdx, writeErrors) + } + writeErrors = 0 } select { case <-ctx.Done(): diff --git a/internal/defaults/modules.example.yaml b/internal/defaults/modules.example.yaml index 9d13b65..39df2f8 100644 --- a/internal/defaults/modules.example.yaml +++ b/internal/defaults/modules.example.yaml @@ -50,7 +50,7 @@ modules: curl -s -X POST https://slack.com/api/users.profile.set \ -H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \ -H "Content-Type: application/json" \ - -d '{"profile":{"status_emoji":"","status_text":"","status_expiration":0}}' + -d '{"profile":{"status_emoji":":dumpsterfire:","status_text":"Offline","status_expiration":0}}' end_snooze: exec: | diff --git a/modules.example.yaml b/modules.example.yaml index 9d13b65..39df2f8 100644 --- a/modules.example.yaml +++ b/modules.example.yaml @@ -50,7 +50,7 @@ modules: curl -s -X POST https://slack.com/api/users.profile.set \ -H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \ -H "Content-Type: application/json" \ - -d '{"profile":{"status_emoji":"","status_text":"","status_expiration":0}}' + -d '{"profile":{"status_emoji":":dumpsterfire:","status_text":"Offline","status_expiration":0}}' end_snooze: exec: |