From 69d2926a1a961d1c621f25a136cea32a61bbbfef Mon Sep 17 00:00:00 2001 From: Levi Woodard Date: Tue, 29 Sep 2026 18:49:39 -0600 Subject: [PATCH] Fix HID read busy-spin: pass 250ms, not 250ns, to ReadWithTimeout go-hid's ReadWithTimeout takes a time.Duration, so the bare 250 was 250ns, which truncates to a 0 ms hid_read_timeout. On both Linux (poll) and macOS (cond_timedwait) 0 ms means non-blocking, so the button loop spun continuously: ~1.2 cores and ~7.5k context switches/sec at idle, from startup. Pass 250*time.Millisecond and match hid.ErrTimeout with errors.Is ahead of the string fallback. Idle CPU drops to ~0%. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016yH8jHn1SNzNnL58CRwTi9 --- internal/device/streamdeck.go | 9 ++++++++- mac-support.md | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/device/streamdeck.go b/internal/device/streamdeck.go index 0c401c8..2fa8f8d 100644 --- a/internal/device/streamdeck.go +++ b/internal/device/streamdeck.go @@ -3,6 +3,7 @@ package device import ( "bytes" "encoding/binary" + "errors" "fmt" "image" "image/jpeg" @@ -10,6 +11,7 @@ import ( "runtime" "strings" "sync" + "time" "github.com/sstallion/go-hid" "golang.org/x/image/draw" @@ -167,8 +169,13 @@ func (sd *StreamDeck) ClearKey(keyIndex int) error { // Returns (nil, nil) on timeout — callers should check context and retry. func (sd *StreamDeck) ReadButtons() ([]bool, error) { data := make([]byte, readReportSize) - n, err := sd.dev.ReadWithTimeout(data, 250) + // ReadWithTimeout takes a time.Duration: a bare 250 would be 250ns, which + // truncates to a 0 ms (non-blocking) hid_read_timeout and busy-spins the loop. + n, err := sd.dev.ReadWithTimeout(data, 250*time.Millisecond) if err != nil { + if errors.Is(err, hid.ErrTimeout) { + return nil, nil + } // Linux hidraw returns errors (not (0,nil)) for non-fatal conditions: // timeout waiting for data, or EINTR (signal interrupted). // macOS IOHIDManager usually returns (0, nil) on timeout, but may also diff --git a/mac-support.md b/mac-support.md index dde443a..eaaf9d5 100644 --- a/mac-support.md +++ b/mac-support.md @@ -96,3 +96,4 @@ The biggest architectural difference is privileged commands: | Log location | `journalctl --user -u streamdeck-go` | `~/Library/Logs/streamdeck-go.log` | | Config path | `~/.config/streamdeck-go/` (XDG) | `~/.config/streamdeck-go/` (XDG — works fine on macOS for CLI tools) | | Sleep/wake | Handled by reconnect loop | Handled by reconnect loop (same code) | +| HID read timeout | `hid_read_timeout` → `poll()`; 0 ms = non-blocking | `hid_read_timeout` → `pthread_cond_timedwait`; 0 ms = non-blocking. Same fix applies: pass `250*time.Millisecond` |