OpenRGB lavender stack: colour script, user units, resume watcher

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jasonwitty
2026-08-25 13:10:50 -07:00
commit f8858eb730
7 changed files with 156 additions and 0 deletions
+36
View File
@@ -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
```
+64
View File
@@ -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
+14
View File
@@ -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
Executable
+8
View File
@@ -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
+11
View File
@@ -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
+12
View File
@@ -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
+11
View File
@@ -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