Compare commits

..

1 Commits

Author SHA1 Message Date
Levi Woodard
69d2926a1a 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yH8jHn1SNzNnL58CRwTi9
2026-09-29 18:49:39 -06:00
2 changed files with 9 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ package device
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"image" "image"
"image/jpeg" "image/jpeg"
@@ -10,6 +11,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"time"
"github.com/sstallion/go-hid" "github.com/sstallion/go-hid"
"golang.org/x/image/draw" "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. // Returns (nil, nil) on timeout — callers should check context and retry.
func (sd *StreamDeck) ReadButtons() ([]bool, error) { func (sd *StreamDeck) ReadButtons() ([]bool, error) {
data := make([]byte, readReportSize) 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 err != nil {
if errors.Is(err, hid.ErrTimeout) {
return nil, nil
}
// Linux hidraw returns errors (not (0,nil)) for non-fatal conditions: // Linux hidraw returns errors (not (0,nil)) for non-fatal conditions:
// timeout waiting for data, or EINTR (signal interrupted). // timeout waiting for data, or EINTR (signal interrupted).
// macOS IOHIDManager usually returns (0, nil) on timeout, but may also // macOS IOHIDManager usually returns (0, nil) on timeout, but may also

View File

@@ -96,3 +96,4 @@ The biggest architectural difference is privileged commands:
| Log location | `journalctl --user -u streamdeck-go` | `~/Library/Logs/streamdeck-go.log` | | 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) | | 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) | | 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` |