Drive the 3.5" LCD of the ASUS ROG Ryujin III AIO cooler (USB 0b05:1aa2,
firmware AURJ2-S750-0108) from Linux, without Armoury Crate.
The protocol was captured from Armoury Crate and then replayed and checked on the
panel from Linux. Nothing public implemented it before: liquidctl's
set_screen for this cooler raises "not yet reverse engineered" and the kernel's
asus_rog_ryujin hwmon driver leaves the screen to userspace. The full write-up
is docs/protocol.md; the raw material (parsed HID timeline,
bulk payloads, device-only pcapng, capture runbook) is in capture/.
What works, all verified on the hardware:
- brightness, standby settings
- the hardware-monitor page: up to three label/value lines with the device's °C, RPM and V glyphs
- upload of GIFs (tested with 400 KB, 30 frames) and JPEGs into the cooler's own storage (16 slots each, Armoury Crate uses 10), and deleting them
- playing a stored GIF, showing a stored JPEG, optionally with up to six lines of text (color, alpha, left/right alignment)
- the clock page, 12 or 24 hour
- reading back firmware, display status, storage table and the current item
The device keeps playing whatever was last selected, with no host involvement, across reboots.
There is a command-line tool and a web control panel in the style of Armoury Crate (below).
git clone https://github.com/zaclanzon/ryujin-iii-lcd-linux && cd ryujin-iii-lcd-linux
./install.sh # venv + deps, udev rule (sudo), wrappers in ~/.local/bin
ryujin-lcd info
ryujin-lcd-web # http://127.0.0.1:8686/
install.sh installs the distro's Python/venv, libusb, and udev packages,
then installs pyusb and Pillow into ~/.local/lib/ryujin-lcd/venv and adds
commands in ~/.local/bin. Reruns preserve the install directory and configuration.
| Family | Distros |
|---|---|
| Debian / Ubuntu | Includes Mint and Pop!_OS |
| Fedora / RHEL | Compatible releases with required repositories |
| Arch | Includes Manjaro and EndeavourOS; setup includes a full upgrade |
| openSUSE | Tumbleweed and compatible Leap releases |
Use ./install.sh --dry-run for the plan, --no-packages for dependencies managed
elsewhere, and --no-udev for a preconfigured USB rule. Immutable hosts need native
host configuration. The USB rule grants access to the active local session through
logind; other session managers need their own permission setup. No pump/fan duty
is changed by installation. Preview with ryujin-lcd-web --demo without hardware.
Weekly compatibility checks install current packages, verify the libusb backend and image bindings, and run hardware-free tests. Library updates stay with the distro updater; Dependabot proposes Python/CI updates.
The tool coexists with the asus_rog_ryujin hwmon driver on the same HID
interface: it skips the driver's replies and the driver ignores the LCD replies.
Mainline has the Ryujin III but not yet USB PID 0x1aa2; a patch was sent to
linux-hwmon in September 2026.
Nothing here writes pump or fan duty, on purpose. The Ryujin III has a 4-pin pump cable
to a motherboard fan header, and until the first duty command over USB the pump follows
that header's BIOS curve (confirmed with nct6775: the header PWM tracks CPU temperature
and the pump tracks the PWM). The first duty write of any kind, from liquidctl, the hwmon
driver's pwm1/pwm2 or fancontrol, takes the pump off the header, and on this firmware
it then runs at maximum while still reporting the old 40 % duty, until pwm1 (liquidctl
set pump) is written with a value different from the one it reports. Only a power cycle
returns the pump to the header. Tracked in
liquidctl#923 and
asus_rog_ryujin-hwmon#12;
whether the Extreme, EVA and White variants behave the same is untested.
ryujin-lcd info
ryujin-lcd brightness 60
ryujin-lcd hwmon "Coolant=40.9°C" "Pump=1920 RPM" "CPU=1.066V"
ryujin-lcd upload cat.gif gif 0 --show # cropped and resized to 320x240
ryujin-lcd upload photo.jpg jpg 0 --show
ryujin-lcd show gif 0
ryujin-lcd banner jpg 0 "line one" "line two" --color FF0000FF --align 1 --x 312
ryujin-lcd clock --24h
ryujin-lcd delete jpg 0
ryujin-lcd raw DC # any command, prints the reply
ryujin-lcd monitor 10 # dump incoming reports for 10 s
-v prints every report on the wire.
ryujin-lcd-monitor keeps the hardware-monitor page fed from hwmon and re-sends
only when a value changes:
ryujin-lcd-monitor Coolant=rog_ryujin/temp1 Pump=rog_ryujin/fan1 CPU=k10temp/temp1
./install.sh --monitor # same thing as a user service, reading ~/.config/ryujin-lcd/monitor.conf
Lines are LABEL=HWMON/SENSOR where HWMON is the driver name in
/sys/class/hwmon/*/name and SENSOR the attribute without _input
(temp1, fan2, in0, power1, ...). Up to three lines. When several devices
share a name (three NVMe drives are all nvme), HWMON can be NAME:DEVICE
with DEVICE the basename of /sys/class/hwmon/hwmonN/device, e.g.
nvme:nvme1; the web panel lists them that way.
ryujin-lcd-web serves a single-page control panel that follows the layout of the
Armoury Crate device page: a preview of the LCD on the pump head, the display
mode (Hardware Monitor with layout tiles and hwmon sensor pickers, or Customized
Slideshow with Animation / Wallpaper / Time), banner text over a wallpaper, a
media library with upload, crop and delete, the brightness slider and standby
settings, and a tools page with the raw status bytes and a raw command console.
ryujin-lcd-web # http://127.0.0.1:8686/
RYUJIN_LCD_TOKEN='replace-with-a-long-random-token' ryujin-lcd-web --host 0.0.0.0
ryujin-lcd-web --demo # simulated cooler and sensors, no hardware needed
./install.sh --web # run it as a user service (replaces the monitor service)
Loopback access needs no token. A non-loopback bind is refused unless --token
or RYUJIN_LCD_TOKEN is set. Open the remote panel once with a URL whose fragment
is http://HOST:8686/#token= followed by encodeURIComponent(TOKEN) (or paste a
token containing no reserved URL characters directly); the panel decodes the entire
suffix, moves the token into session storage, and removes it from the address bar.
The built-in server is plain HTTP, so use a trusted LAN or put it behind an HTTPS
reverse proxy. The server also refuses requests whose Host header is not the
loopback address it listens on, so a web page cannot reach it through DNS rebinding.
The server is the Python standard library only; Pillow is needed to crop and
resize uploads. Applied settings are saved in ~/.config/ryujin-lcd/web.json,
uploaded media are kept in ~/.local/share/ryujin-lcd/media for the thumbnails
(the cooler cannot send files back). "Live update" on the hardware-monitor page
starts a sensor feed inside the server, equivalent to ryujin-lcd-monitor, and
the web service is declared to conflict with the monitor service so only one of
them drives the page. At start the server re-applies what needs the host: it
restarts the live feed, sets the clock again, and resumes a multi-animation
slideshow (--no-restore skips this); a single stored animation or wallpaper
keeps playing by itself. If the cooler is not accessible yet at login, the server
retries restoration every five seconds. A successful settings change in the web
panel cancels the pending startup restore. Pick several animations in Customized Slideshow to
rotate through them at the chosen duration: the cooler was never seen to cycle a
list on its own, so the server drives it by playing each in turn (display
commands only, no writes to the cooler's storage), which means the rotation runs
only while the server is up. All device access is
serialized, so the page, the feed and an upload never interleave on the shared
HID interface. The API refuses cross-origin requests, so another web site open
in the same browser cannot drive the cooler through it.
A file that is on screen cannot be deleted: the cooler acknowledges the command and keeps the file. The panel reports this; show something else first.
Power-on default (Settings page). The cooler resumes whatever was last selected
when it powers up, so a page driven from the host (Hardware Monitor, Clock, a
rotation) comes back empty at boot until the user service starts at login. Pick a
stored animation or wallpaper as the default and the service switches the LCD to it
when it stops (systemctl --user stop, logout, shutdown), then restores your display
mode when it starts. The service checks that the display switched before reporting
success. This relies on a clean stop with device access; it cannot guarantee the
default after power loss or confirm what firmware retains across power cycles.
Display commands only, nothing is written to the cooler's storage.
In Customized Slideshow → Animation, choose Temperature response and select exactly three stored GIFs in cool, warm, hot order. Choose the CPU and coolant sensors, then Apply. CPU temperature selects the animation; sustained high CPU or coolant temperature temporarily switches to live readings using your saved Hardware Monitor lines. Sensor loss also shows that page and an error in the panel.
Default display triggers are CPU 60°C / 75°C for the second / third animation, and CPU 85°C or coolant 45°C for stats. A condition must last 10 seconds; returning requires a 3°C margin and the same delay. These values are adjustable display preferences, not cooling limits or pump/fan controls.
The service restores this behavior at startup and parks on your power-on default at shutdown. Only display commands are sent during operation; media files are not re-uploaded. Timed rotation remains available for ordinary slideshows. Applying a GIF now waits for its actual slot in device readback, and the applied preview follows the current slot instead of always displaying the first selected GIF.
The cooler never sends a file back (see docs/protocol.md), so a slot
filled by the ASUS software shows as no local copy here. Armoury Crate keeps its own
copy of every upload, already converted to 320x240, plus a profile mapping each file to a
slot (uploadImages[] = id, mediaIndex = slot, category 0 gif / 1 jpg):
C:\Program Files (x86)\ASUS\ArmouryDevice\View\externalFiles\aio\RYUJIN_III\<id>.gif|jpg
C:\ProgramData\ASUS\Framework\aioFan\RYUJIN3\fp_1_config.xml
Those copies are byte-identical to what is on the device. Mount that system partition read-only, stop the web service, and import them:
ryujin-lcd-web --import-crate /mnt # prints each slot with size and frame count
It copies files into ~/.local/share/ryujin-lcd/media for slots the cooler lists as used;
the panel shows them on its next storage refresh.
Check the result against the LCD. Armoury Crate never verifies its folder against the cooler,
so after a device-page update it can hold stale stand-ins (tiny single-frame GIFs) instead of
the real animations. For any slot the importer got wrong or does not know (files uploaded with
the ryujin-lcd CLI), use the Media page: Show now puts the slot on the LCD, Set thumbnail
attaches the matching file as a local copy only, without touching the cooler.
Two USB interfaces. Interface 1 is HID: every command is a 65-byte output report
(EC + 64 bytes) with one 64-byte reply, written straight to hidraw. Interface 0
is a vendor bulk pipe used only for file data, in 4096-byte writes, each
acknowledged by an EE 14 event on HID. Media are addressed by (kind, source,
slot). Three things the capture alone got wrong and only the panel revealed:
brightness lives in byte 7 of the display-status report (Armoury Crate writes
byte 12, which does nothing), the hardware-monitor commit byte is the line count
minus one, and a stored JPEG is selected by the wallpaper background command,
not by the list entry Armoury Crate also sends. Details, byte layouts and the
still-unknown fields: docs/protocol.md.
Tested on one Ryujin III 360 with firmware 0108. Untested: the White / EVA /
Extreme variants (same HAL class in Armoury Crate, so probably identical),
portrait orientation, standby behavior, monitor-page colors. A liquidctl
set_screen implementation can be built directly from ryujin_lcd/device.py.
Issues and captures from other units welcome.
Written with Claude Fable 5.1.
MIT.
