Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ All three install the same commands:
|---|---|
| `ragnarosd` | the daemon (run by the `ragnarosd` user unit) |
| `ragnarosctl` | drive the running daemon (see [Control CLI](#control-cli)) |
| `ragnaros` | `install-system`, `install-service`, `paths` |
| `ragnaros` | `install-system`, `install-service`, `paths`, `diagnose` (see [Reporting a new device](#reporting-a-new-device)) |
| `ragnaros-fetch-gifs` | opt-in anime GIF themes for the strip (see [GIF themes](#gif-themes)) |
| `ragnaros-claude-hook` | Claude Code hook (see [Notices that wait for you](#notices-that-wait-for-you)) |
| `ragnaros-strip-gif` | convert any GIF to the 704x124 full-strip format |
Expand Down Expand Up @@ -312,6 +312,47 @@ 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.

```bash
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:

```bash
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:
Expand Down
15 changes: 14 additions & 1 deletion ragnaros/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ragnaros install-system [--print] udev rules + usbhid quirk (sudo, one reboot)
ragnaros install-service [--print] systemd user unit for ragnarosd
ragnaros paths where profiles, assets and GIF themes live
ragnaros diagnose [--device VID:PID] read-only device report to paste in an issue

The .deb and AUR packages already install the udev rules, the quirk and
the user units; there you only run `systemctl --user enable --now ragnarosd`.
Expand All @@ -15,7 +16,7 @@
import subprocess
import sys

from . import __version__, control, paths, theme
from . import __version__, control, diagnose, paths, theme

UDEV_RULES = "70-ragnaros.rules"
QUIRK_CONF = "ragnaros-usbhid.conf"
Expand Down Expand Up @@ -188,6 +189,13 @@ def show_paths(args):
return 0


def device_arg(text):
try:
return diagnose.parse_device(text)
except ValueError as error:
raise argparse.ArgumentTypeError(str(error)) from None


def main(argv=None):
parser = argparse.ArgumentParser(
prog="ragnaros", description="Setup helpers for the Ragnaros deck daemon.",
Expand All @@ -213,6 +221,11 @@ def main(argv=None):
p = sub.add_parser("paths", help="show where profiles, assets and GIF themes live")
p.set_defaults(func=show_paths)

p = sub.add_parser("diagnose", help="read-only device report to paste in an issue")
p.add_argument("--device", type=device_arg, metavar="VID:PID",
help="report this USB device instead of every vendor-0200 one")
p.set_defaults(func=diagnose.run)

args = parser.parse_args(argv)
if not args.command:
parser.print_help()
Expand Down
69 changes: 53 additions & 16 deletions ragnaros/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,12 @@ def run_action(action):


class Deck:
def __init__(self):
pids = (protocol.PID,) # product IDs to open, in order
experimental_id = None # "0200:2000" while driving an untested product ID

def __init__(self, pids=(protocol.PID,)):
self.dev = None
self.pids = tuple(pids)
self._wait_for_device()
self.profile = load_profile(saved_profile_name())
self.last_activity = time.monotonic()
Expand Down Expand Up @@ -199,9 +203,13 @@ def _wait_for_device(self):
announced = False
while True:
try:
self.dev = protocol.Ragnaros()
self.dev = protocol.Ragnaros(self.pids)
if announced:
print("ragnaros: device attached, resuming", file=sys.stderr)
if getattr(self.dev, "experimental", False):
self.experimental_id = protocol.device_id(self.dev.pid)
print(f"ragnaros: EXPERIMENTAL: opened untested device "
f"{self.experimental_id} with the SS552 protocol", file=sys.stderr)
return
except RuntimeError as error:
if "libusb-1.0 is required" in str(error):
Expand Down Expand Up @@ -1017,17 +1025,20 @@ def handle_control(self, argv):
return {"ok": False, "error": "empty command"}
command, args = argv[0], argv[1:]
if command in ("status", "state"):
return {"ok": True, "profile": self.profile.get("_name"),
"profiles": available_profiles(),
"layer": self.layer or "base", "layers": sorted(self.layers()),
"strip": self.strip_state(), "brightness": self.brightness,
"locked": self.locked, "dashboard": self.pinned == "dashboard",
"player": self.player and {k: self.player[k]
for k in ("title", "artist", "length")},
"alert": self.alert and self.alert["summary"],
"watchers": len(self.control.subscribers) if self.control else 0,
"pomodoro": self.pomo and self.pomo["phase"],
"obs": self.obs_state}
reply = {"ok": True, "profile": self.profile.get("_name"),
"profiles": available_profiles(),
"layer": self.layer or "base", "layers": sorted(self.layers()),
"strip": self.strip_state(), "brightness": self.brightness,
"locked": self.locked, "dashboard": self.pinned == "dashboard",
"player": self.player and {k: self.player[k]
for k in ("title", "artist", "length")},
"alert": self.alert and self.alert["summary"],
"watchers": len(self.control.subscribers) if self.control else 0,
"pomodoro": self.pomo and self.pomo["phase"],
"obs": self.obs_state}
if self.experimental_id:
reply["experimental"] = f"{self.experimental_id} (untested, SS552 protocol)"
return reply
if command in ("profile", "profiles"):
if not args or args[0] == "list":
return {"ok": True, "profiles": available_profiles(),
Expand Down Expand Up @@ -1164,23 +1175,49 @@ def run(self):
self.dev.close()


def experimental_arg(argv):
"""The value of --experimental-pid X / --experimental-pid=X, or None."""
for i, arg in enumerate(argv):
if arg == "--experimental-pid":
if i + 1 >= len(argv):
raise ValueError("--experimental-pid needs a product ID, e.g. 0x2000")
return argv[i + 1]
if arg.startswith("--experimental-pid="):
return arg.partition("=")[2]
return None


def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if argv and argv[0] in ("-h", "--help"):
print("usage: ragnarosd\n\n"
print("usage: ragnarosd [--experimental-pid PID]\n\n"
"Run the Ragnaros deck daemon in the foreground (normally started by\n"
"the ragnarosd.service systemd user unit).\n\n"
" --experimental-pid PID also open the untested vendor-0200 deck PID\n"
" (hex, e.g. 0x2000) with the SS552 protocol;\n"
" for testing only, see `ragnaros diagnose`\n\n"
f" profiles {', '.join(paths.profile_dirs())}\n"
f" assets {', '.join(paths.asset_roots())}\n"
f" control {control.socket_path()} (drive it with ragnarosctl)\n\n"
"Environment: RAGNAROS_CONFIG, RAGNAROS_DATA, RAGNAROS_IDLE_SECONDS,\n"
"RAGNAROS_IDLE_FACE_SECONDS, RAGNAROS_USAGE_LOG, RAGNAROS_NO_KEEPALIVE")
"RAGNAROS_IDLE_FACE_SECONDS, RAGNAROS_USAGE_LOG, RAGNAROS_NO_KEEPALIVE,\n"
f"{protocol.EXPERIMENTAL_ENV} (same as --experimental-pid)")
return 0
if argv and argv[0] == "--version":
from . import __version__
print(f"ragnarosd {__version__}")
return 0
Deck().run()
try:
pids = protocol.product_ids(experimental_arg(argv))
except ValueError as error:
print(f"ragnarosd: {error}", file=sys.stderr)
return 2
for pid in pids[1:]:
print(f"ragnaros: EXPERIMENTAL: also accepting {protocol.device_id(pid)}, which is "
"untested, and driving it with the SS552 protocol. Keys, knobs and the strip "
"may not line up. Please report what works in "
"https://github.com/ValentinTorassa/VT-Ragnaros/issues", file=sys.stderr)
Deck(pids).run()
return 0


Expand Down
Loading