# Labfy RGB A small native **C17 Linux command-line controller** for the RGB keyboard on the **ASUS Vivobook S16 M5606UA**, using its **ITE5570 I2C HID controller (`0B05:5570`)** and the standard **HID LampArray** protocol. Labfy RGB talks to Linux `hidraw` through `open`, `close` and HID Feature ioctls. It is a userspace application, not a kernel module. Static colors need no daemon. It works independently of Sway, Wayland, X11 and QuickShell. - Dynamically discovers the matching device; never assumes `/dev/hidraw1`. - Checks the bus, VID/PID and complete known HID report descriptor before control. - Reads live lamp capabilities before converting RGB and intensity values. - Supports static colors, intensity, off, and explicit firmware control. - Serializes cooperating instances and attempts firmware recovery on update errors. - Keeps command history separate from hardware observations. ## Tested hardware and compatibility | Item | Verified on the tested machine | |---|---| | Laptop | ASUS Vivobook S16 M5606UA | | Controller | ITE5570, I2C HID `0B05:5570` | | Kernel HID driver | `hid-generic` | | Operating system | Arch Linux, x86-64 | | Logical lamps | One, ID 0 | | Programmability | Programmable RGB | | Maximum R/G/B/intensity values | 255 / 255 / 255 / 255 | | Minimum update interval | 3000 microseconds | | Physical tests | Static RGB colors, including the eight examples below, and return to ASUS firmware control | | Unprivileged access | Confirmed with the targeted udev rule and an active local session | Firmware control returned the tested keyboard to white. Other firmware versions may choose a different effect or color. Other Vivobook models, other VID/PID combinations, USB devices and descriptor variants are **not validated or currently accepted**. Even another `0B05:5570` may have a different descriptor. The strict profile is deliberate: a familiar product name or report length is not enough to safely identify field semantics. The protocol may exist on other hardware, but that is not a compatibility claim. ## Build Requirements: Linux headers, a C17 compiler, Meson and Ninja. No Python or HID library is required for the controller implementation. On Arch Linux, the usual build tools are provided by `base-devel`, `meson` and `ninja`. The optional permission helper also needs Bash, systemd's `udevadm`, coreutils and `getfacl` from `acl`. ```sh git clone https://git.labfytools.com/fy59/labfy-rgb.git cd labfy-rgb meson setup build --prefix="$HOME/.local" meson compile -C build meson test -C build --print-errorlogs ``` GitHub mirror: . Meson selects C17 and enables `-Wall -Wextra -Wpedantic -Werror` with GCC/Clang. ## Install Install the executable for your user: ```sh meson install -C build ``` With the prefix above, the executable is `$HOME/.local/bin/labfy-rgb`. Make sure that directory is in your session's `PATH`. A system installation can instead use a suitable prefix and an administrator-managed install step. Installing the executable does **not** install udev rules, configure an autostart, create a systemd service or modify desktop configuration. ## Device permissions The device may initially be root-only. Do not use `chmod 666`, run your desktop shell as root or grant access to all HID devices. The opt-in rule in `packaging/70-labfy-rgb.rules` matches only: ```udev SUBSYSTEM=="hidraw", KERNEL=="hidraw*", SUBSYSTEMS=="hid", KERNELS=="0018:0B05:5570.*", TAG+="uaccess" ``` On a systemd-logind system, `uaccess` grants access to the active local seat user. A remote/headless session is not automatically entitled to access. This grants access to the matching HID node as a whole, not just its lighting reports. First inspect the read-only installation plan: ```sh bash packaging/apply-udev-rule.sh --check ``` Then, only if you want to install and activate that rule: ```sh sudo -- bash packaging/apply-udev-rule.sh --apply ``` The helper validates the narrow rule, dynamically identifies exactly one matching controller, refuses to overwrite a different rule, installs it as root-owned `0644`, reloads udev rules and triggers a `change` event **only for that hidraw node**. It displays the resulting ACL. It sends no HID reports and changes no kernel modules. Reloading rules reparses the system's rule set; other rules for the targeted event may also run. Verify as your normal user: ```sh labfy-rgb status ``` For diagnostics, inspect the discovered node with `getfacl`. A successful rule installation alone does not prove that an ACL was granted. Logind seat membership and the resulting ACL must be checked. Do not broaden permissions as a fallback. Automatic reapplication after reboot or device re-enumeration is expected from udev matching, but was not physically tested by rebooting or reconnecting the internal keyboard during development. ## Commands ```sh labfy-rgb status labfy-rgb status --json labfy-rgb probe labfy-rgb set '#8000FF' labfy-rgb set '#8000FF' 60 labfy-rgb brightness 30 labfy-rgb off labfy-rgb auto ``` | Command | Behavior | |---|---| | `status [--json]` | Read-only HID identity, descriptor and array attributes; local command history is labeled separately | | `probe` | Select lamp 0 with SET `0x42`, then read attributes with GET `0x43`; this **is an HID write**, although it does not request a color or mode change | | `set '#RRGGBB' [P]` | Set a uniform RGB color at intensity P, default **100%**; retain host control after success | | `brightness P` | Reapply the last locally recorded color at absolute intensity 0–100%; refuse if that color is unknown | | `off` | Send RGBI zero under host control; preserve a previously known color for a later brightness command | | `auto` | Explicitly restore autonomous firmware control; do not promise the previous color | | `--dry-run COMMAND [ARG...]` | Validate arguments and print the plan without opening HID or reading state | Color strings must be exactly `#RRGGBB`; quote them in a shell. Intensities are decimal integers from 0 through 100. Wire levels are rounded to the live channel maxima. The HID intensity is distinct from the WMI `asus::kbd_backlight` setting; Labfy RGB does not adjust that setting or assume a synchronized relationship. ### Saturated color examples | Color | Command | |---|---| | Violet | `labfy-rgb set '#8000FF'` | | Blue | `labfy-rgb set '#0040FF'` | | Cyan | `labfy-rgb set '#00FFFF'` | | Green | `labfy-rgb set '#00FF40'` | | Pink | `labfy-rgb set '#FF0080'` | | Red | `labfy-rgb set '#FF0000'` | | Orange | `labfy-rgb set '#FF6000'` | | Yellow | `labfy-rgb set '#FFFF00'` | ## Firmware recovery and limitations A color operation verifies live capabilities, disables autonomous mode, waits the device's minimum interval and sends one range update for lamp 0. On success, it leaves host control enabled so that the color persists. If a host-control/update operation fails, Labfy RGB attempts **one** firmware restoration and reports the original error. It also handles ambiguous/short ioctl returns conservatively, without blindly retrying SET reports. `auto` does not require a successful capability read or a known color cache. SIGKILL, default SIGINT/SIGTERM handling, power loss or disconnection can interrupt recovery. The keyboard can remain in host control. When access is available, use: ```sh labfy-rgb auto ``` An ioctl acknowledgement is not an optical measurement or a guarantee of lasting visual state. Suspend/resume, other tools and firmware may alter the keyboard. Do not run another lighting controller concurrently: advisory locking only serializes cooperating Labfy RGB instances. Suspend/resume restoration and the interaction with WMI brightness have not been comprehensively characterized. ## Runtime state and JSON Normal user state lives at `$XDG_RUNTIME_DIR/labfy-rgb/state.bin`, with a `0700` directory and `0600` file. Without XDG_RUNTIME_DIR, the fallback is `/run/user/UID`. Paths, ownership and permissions are checked; symlinks are rejected at the opened boundaries. Root uses a separate namespace and does not import the old root cache. State is versioned, bounded and atomically published, with no native-struct serialization. A replaced device node invalidates its cached identity. The cache records the **last acknowledged command**, not current physical color. `status --json` schema 1 exposes `commanded_mode`, nullable `last_color` and `last_percent`, while `observed_mode` remains `unknown`. Missing/corrupt state does not become an invented color. A successful firmware restoration can still return a cache error if local state could not be invalidated or saved. | Exit code | Meaning | |---|---| | 0 | Success or valid dry-run | | 2 | Invalid arguments | | 3 | Device absent or identity mismatch | | 4 | Permission denied | | 5 | Unsupported capabilities, descriptor/protocol mismatch or ambiguous discovery | | 6 | I/O failure, including a failed operation whose recovery succeeded | | 7 | Last color unknown/unreadable for brightness; no SET sent | | 8 | Original operation and its single recovery both failed; device state uncertain | | 9 | Runtime state/cache error; read the diagnostic, because firmware may already have been restored | | 10 | Another cooperating instance holds the device lock | ## Tests Seven Meson suites use memory transports, syscall doubles and temporary files. They do not send reports to a real keyboard. Tests cover color parsing, RGBI conversion, exact report bytes, discovery, identity rejection, cache validation, process concurrency, errors, exit codes and single-attempt recovery. ```sh CC=clang meson setup build-clang meson test -C build-clang --print-errorlogs CC=gcc meson setup build-sanitize -Db_sanitize=address,undefined -Db_lundef=false meson test -C build-sanitize --print-errorlogs CC=clang meson setup build-clang-sanitize -Db_sanitize=address,undefined -Db_lundef=false meson test -C build-clang-sanitize --print-errorlogs udevadm verify packaging/70-labfy-rgb.rules bash -n packaging/apply-udev-rule.sh git diff --check ``` Development validation used GCC 16.2.1 and Clang 23.1.4 on Arch Linux. Other Linux distributions/toolchain versions are not claimed as hardware-tested platforms. GNU-compatible linker wrapping is used for Linux transport tests. ## Optional QuickShell integration QuickShell is **not required** for the CLI. An existing Control Center can call it through native `Process` objects using argument arrays, never shell command strings, sudo or pkexec. Serialize writes, handle exit/error states, and refresh with `status --json` after completion. Keep the distinction between command history and physical measurements visible. The tested local integration used QuickShell 0.3.1 on SwayFX 0.6 with saturated presets and a firmware button. It is not automatically installed by this project; no private desktop configuration is bundled. See [docs/quickshell.md](docs/quickshell.md). Startup restoration must remain opt-in; this CLI installs no autostart or service. ## Protocol and acknowledgements See [docs/protocol.md](docs/protocol.md) for the accepted report layout and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for reference implementations and license notices. No third-party implementation source is vendored. ## License MIT, copyright (c) 2026 fy59. See [LICENSE](LICENSE).