direct-mode color apply aborts (vector OOB → SIGABRT) on ASUS Aura devices that expose addressable zones with zero LEDs; switch to static mode, which applies the same color across all devices/zones without hitting that path. Also connect to the running OpenRGB SDK server via --client host:port instead of re-detecting all hardware on every toggle. This wires up the previously-unused openrgb.host/port config fields and removes a full USB/HID re-scan per command — that re-scan racing the server is what made it flakiest at login/boot. The existing retry logic now serves its intended purpose (waiting for the server to come up if the daemon starts first) rather than re-running a guaranteed-crashing command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
2.5 KiB
Go
84 lines
2.5 KiB
Go
package openrgb
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os/exec"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
maxRetries = 4
|
|
retryDelay = 3 * time.Second
|
|
)
|
|
|
|
// Client controls RGB devices via the openrgb CLI. It talks to a running
|
|
// OpenRGB SDK server (the --startminimized --server instance) rather than
|
|
// re-detecting hardware on every command — local re-detection races with the
|
|
// server for the USB/HID devices during login/boot and is unreliable.
|
|
type Client struct {
|
|
bin string
|
|
server string // host:port of the OpenRGB SDK server
|
|
}
|
|
|
|
func Connect(host string, port int) (*Client, error) {
|
|
bin, err := exec.LookPath("openrgb")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("openrgb not found in PATH: %w", err)
|
|
}
|
|
|
|
server := net.JoinHostPort(host, strconv.Itoa(port))
|
|
|
|
// Verify the SDK server is reachable (confirms it's up and enumerated).
|
|
cmd := exec.Command(bin, "--client", server, "--list-devices")
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return nil, fmt.Errorf("openrgb client connect to %s failed: %w\n%s", server, err, out)
|
|
}
|
|
|
|
return &Client{bin: bin, server: server}, nil
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// run executes an openrgb command with retries to handle transient failures
|
|
// during early boot when device enumeration may not be complete.
|
|
func (c *Client) run(args ...string) error {
|
|
full := append([]string{"--client", c.server}, args...)
|
|
var lastErr error
|
|
for attempt := range maxRetries {
|
|
cmd := exec.Command(c.bin, full...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
lastErr = fmt.Errorf("openrgb %v: %w\n%s", args, err, out)
|
|
if attempt < maxRetries-1 {
|
|
log.Printf("openrgb command failed (attempt %d/%d), retrying in %v: %v",
|
|
attempt+1, maxRetries, retryDelay, lastErr)
|
|
time.Sleep(retryDelay)
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
// SetAllOff sets all devices to black (off).
|
|
//
|
|
// Uses "static" mode, not "direct": OpenRGB's direct-mode color path crashes
|
|
// (vector out-of-bounds → SIGABRT) on devices that expose addressable zones
|
|
// with zero LEDs, e.g. unused ASUS Aura ARGB headers. Static mode applies the
|
|
// same color across all devices and zones without hitting that path.
|
|
func (c *Client) SetAllOff() error {
|
|
return c.run("--mode", "static", "--color", "000000")
|
|
}
|
|
|
|
// SetAllOn sets all devices to the given color. See SetAllOff for why this uses
|
|
// "static" mode rather than "direct".
|
|
func (c *Client) SetAllOn(r, g, b byte) error {
|
|
color := fmt.Sprintf("%02X%02X%02X", r, g, b)
|
|
return c.run("--mode", "static", "--color", color)
|
|
}
|