commit f8858eb73061fe1fa4a4a1ad1d371dc95adea184 Author: jasonwitty Date: Tue Aug 25 13:10:50 2026 -0700 OpenRGB lavender stack: colour script, user units, resume watcher Co-Authored-By: Claude Fable 5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b6f9aa --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# rgb-lavender + +Catppuccin Latte lavender (`#7287FD`) on every RGB controller in the cachyos-gaming desktop +(ASUS Z790 Hero build), driven by OpenRGB and kept that way across suspend/resume. + +## Pieces + +| File | Role | +|---|---| +| `bin/rgb-lavender` | Applies the colour to every device, resolving them by name. Tunables at the top (`RGB_COLOR`, sizes, headers). | +| `bin/rgb-resume-watch` | Listens on the system bus for logind `PrepareForSleep(false)` and restarts the OpenRGB server, which re-runs `rgb-lavender`. | +| `systemd/openrgb-server.service` | `openrgb --server --noautoconnect` as a user service. | +| `systemd/rgb-lavender.service` | Oneshot, `PartOf=openrgb-server.service` → re-applies whenever the server (re)starts. | +| `systemd/rgb-resume-watch.service` | Runs the watcher. Pure user-level, no root sleep hook. | + +## Why the resume watcher + +Corsair Commander Core XT, Lighting Node Pro and the Aura headers run in *direct* mode, which only +lives in controller RAM. After s2ram the USB controllers re-enumerate and lose it: XT fans go dark, +the rest fall back to firmware defaults, and the ENE DRAM drops off I2C until re-detected. The +login-time oneshot never re-runs, so the watcher does it for you (~10 s after wake). + +## Gotchas (learned the hard way) + +- **Always write the whole device.** A zone-only direct write starves the other zones and they go dark. +- Lighting Node Pro channels default to 14 LEDs — size to 204; `--size` must travel with mode+colour. +- Aura addressable headers start at 0 LEDs; same rule. Commander Core XT auto-sizes. +- ENE DRAM is firmware-static and survives boots; Glorious Model O detection is flaky and is skipped if absent. +- The `recv_select failed receiving magic` spam in the server log is just the detection poll in `rgb-lavender`. + +## Install + +```sh +./install.sh +RGB_COLOR=ca9ee6 rgb-lavender # try another colour ad hoc +``` diff --git a/bin/rgb-lavender b/bin/rgb-lavender new file mode 100755 index 0000000..2c94cdd --- /dev/null +++ b/bin/rgb-lavender @@ -0,0 +1,64 @@ +#!/bin/bash +# Apply a single colour to every RGB controller in this machine (ASUS Z790 Hero build). +# See ~/Nextcloud/Notes/Linux_RGB_OpenRGB_Catppuccin.md for the MSI/Fedora sibling and the why. +# +# Tunables (env overrides): +# RGB_COLOR hex, no '#'. Default: Catppuccin Latte lavender. +# ASUS_SIZE LED count for each Aura Addressable header (Aura max 120; oversizing is harmless in direct). +# NODEPRO_SIZE LED count per Lighting Node Pro channel (max 204). +# ASUS_ZONES which addressable headers to size (space list). Empty headers are still written by the +# device-wide pass, sizing them just lengthens the buffer. +set -u +RGB_COLOR=${RGB_COLOR:-7287FD} +ASUS_SIZE=${ASUS_SIZE:-120} +ASUS_ZONES=${ASUS_ZONES:-"1 2 3 4"} +NODEPRO_SIZE=${NODEPRO_SIZE:-204} +SERVER=${SERVER:-127.0.0.1:6742} + +o() { openrgb --client "$SERVER" "$@" 2>&1 | grep -viE '^(client|connected|network)|^$'; } + +# Wait until the server has actually finished detection (it accepts connections early). +# Count DISTINCT device names — the client double-connects, so raw line counts are doubled +# and a half-detected list passes a numeric threshold. Require the controllers we care about. +REQUIRED=("ENE DRAM" "Corsair Commander Core XT" "Corsair Lighting Node Pro" "ASUS ROG MAXIMUS Z790 HERO") +for _ in $(seq 1 90); do + l=$(openrgb --client "$SERVER" --list-devices 2>/dev/null | grep -E '^[0-9]+: ') + ok=1; for r in "${REQUIRED[@]}"; do echo "$l" | grep -qF "$r" || ok=0; done + [ "$ok" = 1 ] && break + sleep 1 +done +sleep 2 # let the last-detected device settle before writing + +# Device indices are resolved by name so re-detection order changes don't matter. +# Client double-connects → every device is listed twice; take the lowest index per name. +list=$(openrgb --client "$SERVER" --list-devices 2>/dev/null | grep -E '^[0-9]+: ') +idx() { echo "$list" | grep -F "$1" | head -1 | cut -d: -f1; } +idxs() { echo "$list" | grep -F "$1" | cut -d: -f1 | sort -n | head -n "$2"; } + +# ENE DRAM x2 — firmware static, persists across boots. +for d in $(idxs 'ENE DRAM' 2); do o --device "$d" --mode static --color "$RGB_COLOR"; done + +# Corsair Commander Core XT — ports 1-4 = fans, auto-sized by the controller (34/34/34/16). Direct only. +# Always write the WHOLE device: a zone-only write starves the other ports and they go dark. +d=$(idx 'Corsair Commander Core XT'); [ -n "$d" ] && o --device "$d" --mode direct --color "$RGB_COLOR" + +# Corsair Lighting Node Pro — ch1 = ARGB strip, ch2 = 3 top fans + rear fan via LED hub. +# Channels default to 14 LEDs, which lights nothing useful; size both to the 204 max (surplus falls off). +# --size must travel with mode+colour. Then a device-wide write so neither channel starves in direct. +d=$(idx 'Corsair Lighting Node Pro') +if [ -n "$d" ]; then + for z in 0 1; do o --device "$d" --zone "$z" --size "$NODEPRO_SIZE" --mode direct --color "$RGB_COLOR"; done + o --device "$d" --mode direct --color "$RGB_COLOR" +fi + +# ASUS Aura — addressable headers start at 0 LEDs; size them (must travel with mode+colour), +# then a device-wide write so the mainboard zone is covered too. +d=$(idx 'ASUS ROG MAXIMUS Z790 HERO') +if [ -n "$d" ]; then + for z in $ASUS_ZONES; do o --device "$d" --zone "$z" --size "$ASUS_SIZE" --mode direct --color "$RGB_COLOR"; done + o --device "$d" --mode direct --color "$RGB_COLOR" +fi + +# Glorious Model O — stored in mouse firmware. Detection is flaky; skip silently if absent. +d=$(idx 'Glorious Model O'); [ -n "$d" ] && o --device "$d" --mode custom --color "$RGB_COLOR" +exit 0 diff --git a/bin/rgb-resume-watch b/bin/rgb-resume-watch new file mode 100755 index 0000000..5ff0418 --- /dev/null +++ b/bin/rgb-resume-watch @@ -0,0 +1,14 @@ +#!/bin/bash +# Re-apply RGB after suspend/resume. Direct-mode Corsair/Aura state is volatile and the USB +# controllers re-enumerate on wake, so restart the OpenRGB server (rgb-lavender.service is +# PartOf it and re-runs automatically). Pure user-level: watches logind's PrepareForSleep. +set -u +exec dbus-monitor --system "type='signal',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'" | +while read -r line; do + case "$line" in + *"boolean false"*) + sleep 3 # let USB finish re-enumerating before re-detecting + systemctl --user restart openrgb-server.service + ;; + esac +done diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3a174d4 --- /dev/null +++ b/install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Install scripts + user units and start the stack. +set -eu +cd "$(dirname "$0")" +install -Dm755 bin/rgb-lavender bin/rgb-resume-watch -t ~/.local/bin +install -Dm644 systemd/*.service -t ~/.config/systemd/user +systemctl --user daemon-reload +systemctl --user enable --now openrgb-server.service rgb-resume-watch.service diff --git a/systemd/openrgb-server.service b/systemd/openrgb-server.service new file mode 100644 index 0000000..91a0cd6 --- /dev/null +++ b/systemd/openrgb-server.service @@ -0,0 +1,11 @@ +[Unit] +Description=OpenRGB SDK server +After=graphical-session.target + +[Service] +ExecStart=/usr/bin/openrgb --server --noautoconnect +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=default.target diff --git a/systemd/rgb-lavender.service b/systemd/rgb-lavender.service new file mode 100644 index 0000000..9372df8 --- /dev/null +++ b/systemd/rgb-lavender.service @@ -0,0 +1,12 @@ +[Unit] +Description=Apply Catppuccin lavender to all RGB devices +Requires=openrgb-server.service +After=openrgb-server.service +PartOf=openrgb-server.service + +[Service] +Type=oneshot +ExecStart=%h/.local/bin/rgb-lavender + +[Install] +WantedBy=openrgb-server.service diff --git a/systemd/rgb-resume-watch.service b/systemd/rgb-resume-watch.service new file mode 100644 index 0000000..38d10a7 --- /dev/null +++ b/systemd/rgb-resume-watch.service @@ -0,0 +1,11 @@ +[Unit] +Description=Re-apply RGB colour after resume from suspend +After=openrgb-server.service + +[Service] +ExecStart=%h/.local/bin/rgb-resume-watch +Restart=always +RestartSec=5 + +[Install] +WantedBy=default.target