Skip to content

About

Reverse-engineered Linux driver and daemon for the Ragnaros USB deck - native libusb transport, 10 LCD keys, 4 rotary knobs, touch strip, YAML profiles & idle animations

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

VT-Ragnaros

A reverse-engineered, from-scratch Linux driver and daemon for the Ragnaros USB control deck (Redragon SS552, USB 0200:3001) - a Linux-native alternative to the vendor's Windows software.

The daemon renders icons and animated GIFs onto the deck's LCD keys and touch strip, and runs a configurable command on every input event.

Features

  • 10 LCD keys (2x5 grid, 112x112 px each) with static icons or animated GIFs
  • 4 rotary knobs with rotate (CW/CCW) and press actions, with detent debouncing
  • 4-segment LCD touch strip (704x124 combined): taps and swipes are real inputs, and it shows now playing, a system dashboard, desktop notifications or idle animations (generated art by default, opt-in GIF themes)
  • Key layers - a second set of keys over the same profile, on a knob press
  • Hot profile switching from a key, a swipe or the CLI
  • ragnarosctl - a control socket for scripts, shortcuts and CI
  • Desktop aware - toasts on the strip, and the deck goes dark when the session locks
  • YAML keymap profiles mapping any input to a command or a deck verb
  • Runs as a systemd user service

How It Works

The vendor protocol was reverse-engineered from the Windows SDLibrary1.dll - no vendor SDK is used. It turned out to be the Mirabox / Ajazz "Stream Dock" protocol (CRT\0\0 commands, ACK input reports), the 1024-byte "v3" variant also used by the Mirabox N4 / Ajazz AKP05; see mirajazz and opendeck-akp05, which lists the SS552.

ragnaros/daemon.py ──> protocol.py ──(ctypes / libusb-1.0)──> Ragnaros deck
     │                     JPEG frames in 1024-byte chunks to display slots
     ├── renderer.py ── PIL: icons / GIFs -> JPEG
     ├── inputmap.py ── HID input reports -> key / knob / strip events
     └── profiles/*.yaml ── events -> commands
  • Output: images sent as JPEG payloads over the interrupt OUT endpoint 0x03 (1024-byte packets) with a CRT command header; keys map to display slots 5-14, strip segments to 0-3 (content rotated 180°)
  • Input: reports polled from EP 0x82, decoded into key, knob, knob_press, strip_touch, and strip_swipe events

Installation

Requires Linux with libusb-1.0, udev and systemd. Every route needs one reboot after the first install, so usbhid loads with the deck's quirk and lets libusb claim it.

Debian / Ubuntu (.deb)

Every v* tag builds ragnaros_<version>-1_all.deb (ragnaros_0.1.1-1_all.deb for v0.1.1) and attaches it to the releases page (packaging/deb/build-deb.sh builds it locally from a wheel, with nfpm):

sudo apt install ./ragnaros_*_all.deb      # udev rules, usbhid quirk, user units
sudo reboot
systemctl --user enable --now ragnarosd

Debian source package

debian/ is a policy-compliant source package (pybuild, DEP-5 copyright, man pages, opt-in user units) for a Debian upload. It builds from a +ds repack of the release tarball without the app-logo key icons, which aren't DFSG-free; see debian/README.source. The release .deb above is still the nfpm one.

Arch Linux (AUR)

The PKGBUILD lives in packaging/aur/ and builds from GitHub's tarball of the release tag:

cd packaging/aur && makepkg -si            # or, once it is on the AUR: yay -S ragnaros
sudo mkinitcpio -P && sudo reboot          # if your initramfs carries usbhid
systemctl --user enable --now ragnarosd

If usbhid is built into your kernel, add usbhid.quirks=0x0200:0x3001:0x00000004 to the kernel command line instead (ragnaros install-system --print tells you which case you are in).

pipx (any distro)

pipx install git+https://github.com/ValentinTorassa/VT-Ragnaros   # or `pipx install .` in a clone
ragnaros install-system      # udev rules + usbhid quirk into /etc, with sudo (--print to review first)
sudo reboot
ragnaros install-service     # ~/.config/systemd/user/ragnarosd.service for this install
systemctl --user start ragnarosd

All three install the same commands:

command what it does
ragnarosd the daemon (run by the ragnarosd user unit)
ragnarosctl drive the running daemon (see Control CLI)
ragnaros install-system, install-service, paths, diagnose (see Reporting a new device)
ragnaros-fetch-gifs opt-in anime GIF themes for the strip (see GIF themes)
ragnaros-claude-hook Claude Code hook, and review / label / eval for its optional Jev log (see Notices that wait for you)
ragnaros-strip-gif convert any GIF to the 704x124 full-strip format
ragnaros-preview-strip render strip layouts to a PNG without the deck
ragnaros-usage summarize the usage log

From a git checkout

git clone https://github.com/ValentinTorassa/VT-Ragnaros
cd VT-Ragnaros
./install.sh

The installer:

  1. Symlinks daemon/, profiles/, assets/ and tools/ into ~/.config/ragnaros, so edits in the checkout are live
  2. Installs udev rules (uaccess + plugdev group) and a usbhid kernel quirk for the device - requires sudo and one reboot
  3. Installs and enables the ragnarosd.service systemd user unit
  4. Installs Python dependencies (pillow, pyyaml) if missing

Then start it with systemctl --user start ragnarosd, and check it anytime with ./install.sh doctor.

Where things live

Profiles and images are looked up in your config first, then in the data directory, then in the ones shipped with ragnaros; ragnaros paths prints the actual directories.

path
your profiles / icons ~/.config/ragnaros/profiles/, ~/.config/ragnaros/assets/
downloaded GIF themes ~/.local/share/ragnaros/gifs/
state, usage log, album art ~/.local/state/ragnaros/
Claude hook settings (optional) ~/.config/ragnaros/claude-hook.yaml

A profile in ~/.config/ragnaros/profiles/ with the same name as a bundled one replaces it.

Profiles

Profiles live in profiles/ as YAML files. Bundled: streaming (default), work, ai-tools, stream (OBS) and system - a dashboard tab whose strip stays on the metrics and whose first four keys pick which cards it shows.

Each profile maps 10 keys and 4 knobs to actions, plus optional idle strip GIFs:

name: streaming
strip:
  segments:                # optional: one GIF per panel, from any assets directory
    - gifs/segments/mine.gif
    - strip/default_1.gif
keys:
  "0":
    icon: icons/brave.png
    action: gtk-launch com.brave.Browser
  "6":
    icon: icons/mic_mute.png
    action: wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
  "9":
    icon: icons/work.png
    profile: work        # switch to another profile
knobs:
  "0":
    rotate:
      cw: wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
      ccw: wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
    press: wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle

Keys accept action (a command or a deck verb), profile (hot switch), layer (momentary while held) or layer_toggle. The deck verbs are pomodoro, dashboard[:cards], obs:*, profile:<name> and layer:<name>.

Commands run without a shell: the line is split the way a shell splits words (quotes work) and started directly, so pipes, &&, redirections and $VARS are passed through as plain arguments. Wrap those in sh -c '...'. A ~ at the start of an argument, or right after --option=, expands to your home directory.

action: dashboard:cpu,ram,temp,gpu pins that set of cards on the strip; pressing the same key again puts the strip back. strip.pinned: dashboard does it for a whole profile, which is what system.yaml uses.

The strip

Priority, top to bottom: a transient overlay (knob feedback, a toast), then a pinned dashboard, then now playing, then the idle face. After ~10 s of inactivity the idle face alternates between the segment GIFs and the dashboard every 60 s (strip.idle_seconds). A profile with strip.pinned: dashboard never rotates.

The segment GIFs come from the profile's strip.segments, else from your GIF theme, else from the generated idle art that ships with ragnaros (assets/strip/, made by tools/gen_assets.py --strip).

strip:
  segments: [gifs/segments/mine.gif, ...]   # one GIF per 176x124 panel
  gif: gifs/wide.gif                        # or one 704x124 GIF across the strip
  pinned: dashboard                         # this profile owns the strip
  idle: [gifs, dashboard]                   # idle rotation (default: both)
  idle_seconds: 60                          # how long each face holds the strip
  dashboard: [clock, cpu, ram, temp]        # gpu, disk, net, battery, uptime
  players:
    ignore: [chromium, brave]               # keep browser tabs off the strip
  touch:
    "3": dashboard                          # per-panel tap actions
  swipe:
    left: profile:work
    right: profile:streaming

Without a touch mapping, tapping the last panel pins or unpins the dashboard and, while music plays, the first three panels are play/pause, previous and next

  • they sit right under the art, title and artist cards. A swipe cycles profiles.

GIF themes

The repository ships no third-party GIFs. ragnaros-fetch-gifs (in a checkout: python3 tools/fetch_gifs.py) downloads an anime theme from Tenor search results into ~/.local/share/ragnaros/gifs/ on your machine and writes the rotation to gifs/theme.yaml there; every profile without its own strip.segments picks it up within seconds.

ragnaros-fetch-gifs --theme jjk          # or evangelion; fetches 3 new GIFs
ragnaros-fetch-gifs                      # 3 more for the current theme
ragnaros-fetch-gifs --off                # back to the bundled idle art
systemctl --user enable --now ragnaros-gif-refresh.timer   # weekly refresh

theme.yaml can also pin a rotation per profile (profiles: {system: [...]}) and keep two anchors per theme on the end panels. The GIFs belong to their owners; they stay in your data directory.

Layers

A layer lays a second set of keys over the profile; keys it does not define fall through to the base. The bundled profiles put a media layer on knob 2's press.

layers:
  media:
    keys:
      "0": {icon: icons/media_prev.png, action: playerctl previous}
      "9": {icon: icons/layers.png, layer_toggle: media}   # back to base

Notifications

Desktop notifications are read off the session bus with dbus-monitor and shown as a 4 s card. Mute them per profile:

notifications:
  enabled: true
  seconds: 4
  ignore: [Spotify, Thunderbird]

Control CLI

ragnarosctl talks to the running daemon over a unix socket ($XDG_RUNTIME_DIR/ragnaros.sock, mode 0600), so shortcuts, scripts and CI can drive the deck:

ragnarosctl status                       # profile, layer, what the strip shows
ragnarosctl profile work                 # or: next, prev, list
ragnarosctl layer media                  # or: base, list
ragnarosctl dashboard toggle             # or: on, off, "cpu,ram,temp,gpu"
ragnarosctl brightness 40
ragnarosctl reset-display               # firmware sleep/wake and full repaint
ragnarosctl toast "Build passed" "12 tests, 4.2s" --app=ci --seconds=6
ragnarosctl key 6                        # run a key's action without touching it
ragnarosctl alert "Claude terminó" "VT-Ragnaros"   # holds until you press a key
ragnarosctl alert --clear
ragnarosctl watch [--grab]               # stream every input as JSON

Add --json for machine-readable replies.

Notices that wait for you

A toast is gone in four seconds; an alert owns the strip until someone presses a key on the deck, and the last card counts how long it has been waiting. That is the shape of "come back and look at this".

The bundled tools/claude_hook.py is a Claude Code Stop hook: when a session finishes, the deck shows the project and the last thing Claude said for eight seconds, then hands the strip back to the GIFs or the dashboard. Set RAGNAROS_CLAUDE_HOOK_MODE=alert to make it an alert that waits instead, and RAGNAROS_CLAUDE_HOOK_SECONDS to change how long the toast stays.

{ "hooks": { "Stop": [ { "hooks": [ {
  "type": "command",
  "command": "ragnaros-claude-hook"
} ] } ] } }

From a checkout without installing, use python3 ~/path/to/VT-Ragnaros/tools/claude_hook.py.

It exits 0 whatever happens, so a stopped daemon or an unplugged deck never breaks the session. Registered as a Notification hook instead, it raises the notice when Claude is waiting for a permission answer.

Waiting, finished or stuck (optional, off by default)

A stop can mean three things: Claude waits for you (a question, a permission, a choice), it finished, or it got stuck (an error it could not get past, a missing credential, it gave up). Only the text says which, so the hook can ask Jev (TypeSafe AI, through Vercel AI Gateway), a small decision model that answers closed questions with probabilities and never writes text. With Jev in charge, waiting and stuck hold the strip as an alert and a finished session gets the usual toast.

Rules come first and Jev only gets the doubtful case:

stop class Jev asked?
Notification for a permission, an idle prompt or a question dialog waiting no
StopFailure (the turn ended on an API error) stuck no
Stop with background tasks or session crons still in flight as today no
a session under a folder in jev_skip as today never
any other Stop Jev's pick yes

Settings live in ~/.config/ragnaros/claude-hook.yaml (read on every stop: no restart):

jev: shadow          # off (default) | shadow | on
# jev_provider: vercel  # vercel (default, Vercel AI Gateway) | openrouter
jev_skip:            # folders whose sessions never reach Jev, matched against the
  - ~/work/client    # session's cwd and the folder it started in (symlinks resolved)
# jev_question: en-1 # the question set: en-1 (default) or es-1, the same in Spanish
# jev_timeout: 1.5   # seconds, capped at 1.5
# jev_min: 0.5       # in `on`, Jev's pick needs at least this probability
  • off: nothing is called, nothing is logged; the hook behaves exactly as before.
  • shadow: the notice is today's, and a detached worker asks Jev and logs what it would have picked next to what the hook did.
  • on: the worker shows Jev's pick when it reaches jev_min, and today's notice when it does not, or when anything fails (no key, timeout, HTTP error, odd reply).

RAGNAROS_JEV=off|shadow|on overrides the mode, RAGNAROS_JEV_PROVIDER the provider, RAGNAROS_JEV_MODEL the pinned model (typesafe-ai/jev-1.14.0 on the gateway, typesafe/jev-1.14 on OpenRouter), and RAGNAROS_JEV_SKIP (a :-separated list) adds folders to the skip-list. A config file that does not parse turns Jev off, since its skip-list may be all that keeps a folder at home.

Key. A Vercel AI Gateway key in $AI_GATEWAY_API_KEY, or in ~/.config/ragnaros/jev.key ($RAGNAROS_JEV_KEY_FILE), which must be a plain file only you can read: chmod 600. The gateway is asked to route only to TypeSafe. With jev_provider: openrouter the variable is $OPENROUTER_API_KEY and the key file holds that key instead. The key never reaches the logs.

What leaves the machine. Only Claude's last message, cut to its first 300 and last 1,200 characters, with anything shaped like a token, key or password masked; its length; and the stop reason and last tool when the transcript has them. Never the project, the path or the session id. The message is sent as data, and the questions tell Jev to ignore any instructions inside it.

Speed. The hook itself only adds the time to start the worker (about 10 ms); the worker runs in its own session with no shared output, so Claude never waits for it. The whole decision has a hard 1.5 s wall clock. In on mode the notice comes from the worker, so it can show up to that much later.

The log. Every decision is one JSON line in ~/.local/state/ragnaros/claude-jev.jsonl ($RAGNAROS_JEV_LOG), mode 600: time, id, session, project, the model, the question set, the provider, the hook's class and signal, Jev's class, probabilities, signal, latency, cost and input tokens (when the gateway reports them), what was actually shown, any error, and the excerpt Jev read. To turn it into labels:

ragnaros-claude-hook review            # where Jev and the hook disagree, newest first
ragnaros-claude-hook label 8b2119ff11 waiting
ragnaros-claude-hook eval              # the hook vs Jev (and `on` at each jev_min) on the labels

From a checkout, python3 tools/claude_hook.py review works the same. Labels go to claude-jev-labels.jsonl next to the log; nothing in these commands calls Jev.

Scripts can take the deck

ragnarosctl watch keeps its connection open and prints one JSON line per input, so a long-running script can react to the deck instead of launching one-shot commands:

ragnarosctl watch --json | while read -r event; do
  case "$(jq -r '.type + ":" + (.index|tostring)' <<<"$event")" in
    key:4) ./deploy.sh ;;
    strip_swipe:1) ./next-take.sh ;;
  esac
done

--grab additionally suppresses the profile's own actions while the script is connected, so the deck becomes a blank input device; the grab is released the moment the client disconnects. Profile switches, layer changes and alerts are published on the same stream.

Reporting a new device

Ragnaros drives one deck, the SS552 (0200:3001). Other Redragon decks share its USB vendor ID 0200 (which lsusb labels "TP-Link") and may or may not speak the same protocol. If you have one, two things show whether it can be added.

1. A device report. ragnaros diagnose prints a Markdown report on every vendor-0200 device plugged in: IDs and strings, interfaces and endpoints, the HID report descriptors, the hidraw and USB device nodes with their permissions, which udev rules and usbhid quirks cover it, and the ragnaros, kernel and distribution versions. It only reads sysfs and the permissions in /dev: the deck is never opened, so it needs no root and is safe while the daemon runs. It leaves out the serial number, the hostname and user names.

ragnaros diagnose > report.md          # --device VID:PID reports any other device

Paste report.md into an issue, with how many keys, knobs and screens the deck has. What to try next goes to the terminal, not to the file.

2. An experimental test. ragnarosd --experimental-pid 0x2000 (or RAGNAROS_EXPERIMENTAL_PID=0x2000) also opens that product ID and drives it with the SS552 protocol. It is off unless you ask for it, it is marked experimental in the daemon's log and in ragnarosctl status, and it logs every raw input report the deck sends. Nothing about it is known to work.

The udev rules only give your user the SS552, so hand yourself the deck's USB node (the device node line of the report) until you unplug it, instead of widening the rules:

sudo chown "$USER" /dev/bus/usb/005/002   # the device node from the report
systemctl --user stop ragnarosd           # if it runs
ragnarosd --experimental-pid 0x2000       # stays in the foreground; Ctrl+C stops it
ragnarosctl watch --grab                  # in a second terminal: press every key, turn every knob

--grab keeps the profile's actions from running while you press things. Report whether icons show up and on which keys (a photo helps), and paste the output of both terminals. Unplugging the deck undoes the chown. From a git checkout the commands are python3 -m ragnaros.cli diagnose, python3 daemon/ragnarosd.py and tools/ragnarosctl.

Tools

The user-facing tools are installed as commands (table above). The checkout also keeps:

  • tools/gen_assets.py - generates the 112x112 key icons (Font Awesome glyphs + app icons, including animated tiles) and, with --strip, the default strip idle art
  • tools/strip_order.py - paints 1 2 3 4 on the panels to confirm their physical order
  • tools/probe_modes.py - walks the firmware's undocumented MOD command, dumping the feature and input reports at each step. Findings so far: modes 0-9 change no reported state and never break input, and every feature report (0-3) and input report 1 return the same firmware string, V3.SS_552.02.009 - a version identifier, not device state
  • tools/sniff.py, handshake.py, probe*.py, wake.py, listen.py, paint*.py - reverse-engineering and protocol discovery utilities
  • tools/ragnarosctl, tools/claude_hook.py, tools/fetch_gifs.py, daemon/ragnarosd.py - entry points that run the commands straight from the checkout (what install.sh wires up)

Requirements

  • Python 3.9+ with pillow and pyyaml
  • libusb-1.0 (loaded via ctypes, no Python binding needed)
  • DejaVu fonts (fonts-dejavu-core / ttf-dejavu) for the strip text
  • Linux: udev rules, usbhid quirk, systemd user units
  • playerctl, wpctl, brightnessctl for the bundled actions
  • dbus-monitor (Debian: dbus-bin) for toasts and lock dimming - optional

License

MIT - see LICENSE.

Citation

To cite this software, use the metadata in CITATION.cff.

About

Reverse-engineered Linux driver and daemon for the Ragnaros USB deck - native libusb transport, 10 LCD keys, 4 rotary knobs, touch strip, YAML profiles & idle animations

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages