5.8 KiB
dora-joy
Dora node that reads a gamepad and publishes its axes and buttons as an Arrow struct. A source for anything driven by hand: it takes a tick and produces readings.
Values go out raw — no deadzone, no response curve, no scaling — and the node never interprets them. What a stick or a button means is a decision that belongs to whatever acts on it, and two places shaping one number is a bug nobody can see. This repository names no consumer: it is a source.
Usage
Add to your dataflow:
nodes:
- id: joy
git: https://gitea.liria.cl/liria/dora-joy.git
tag: v0.1
build: pip install .
path: dora-joy
inputs:
tick: dora/timer/millis/20
outputs:
- joy
Run the dataflow with --uv (dora run dataflow.yml --uv): the build command is
rewritten to uv pip install ., so the node gets its own managed environment and never
installs into an ambient one.
Give the consuming input queue_size: 1 unless it genuinely wants a backlog. This node
usually ticks faster than whatever reads it, and a consumer acting on a reading should
act on the newest one or on none:
inputs:
joy:
source: joy/joy
queue_size: 1
queue_policy: drop_oldest
Runnable dataflow
dataflow.yml is the node plus a printer, and is what CI runs:
dora run dataflow.yml --uv
The printer exists so the dataflow is self-contained and runs with no pad and no consumer. With a pad plugged in it is also the quickest way to see what is on the wire:
joy 41 axes [+0.000 -0.812 -1.000 +0.000 +0.000 -1.000] buttons [-]
joy 42 axes [+0.000 -0.812 -1.000 +0.000 +0.000 -1.000] buttons [0]
joy 43 no gamepad
Output
| Output | Payload | Metadata |
|---|---|---|
joy |
{axes, buttons, present} struct |
none — the struct describes itself |
axes—list<float32>, raw SDL. A stick runs -1 to 1 and centres near 0; a trigger runs -1 to 1 and rests at -1, which is the thing that catches people out. A consumer treating a trigger like a stick will see it half pressed at rest.buttons—list<bool>. Down or not.present—bool, false when no pad is open: unplugged, never plugged in, or taken away by SDL mid-run. An absent pad is not a centred pad. A consumer driving anything that moves must read this as a stop, not as zeroed axes.
Published on every tick, whether or not anything changed, so the stream doubles as a
heartbeat: silence means this node died and nothing else. A consumer that acts on a
reading should stop on silence as well as on present: false.
There is no timestamp in the payload. dora already knows when a message arrived, and a second clock in it would only invite skew questions.
Inputs
| Input | Description |
|---|---|
tick |
Publishes the latest reading. Wire to dora/timer/millis/N; the node stops when it closes. |
20 ms is a good default. Ticking faster than the consumer costs almost nothing and keeps the reading it acts on fresh.
Environment variables
| Variable | Default | Meaning |
|---|---|---|
JOYSTICK_INDEX |
0 |
Which pad to open, when more than one is plugged in. |
A value that cannot be read stops the node at startup rather than the first time somebody picks up the pad.
A dataflow can carry it per node, which keeps the configuration with the wiring it belongs to and out of whatever shell happens to start it:
env:
JOYSTICK_INDEX: 1
Checking your pad
uv run dora-joy-probe
Prints every axis and every button by index, live, with no dataflow and nothing attached. It cannot move anything.
dora-joy probe — Ctrl-C to stop
axes (6 reported)
0 +0.600 ----------|-----#----
1 -0.800 --#-------|----------
2 -1.000 #---------|----------
...
buttons (6 reported)
5
push one control at a time and note the index it moves.
Push one control at a time and read off the index it moves. Whatever consumes this node has to decide which index is which, and that decision is almost always SDL's documented numbering rather than an observed one — a pad that disagrees is much better found here than by watching a machine move the wrong way. A bar that does not sit centred with the stick released is a worn pad announcing itself.
The probe shows the reading and nothing else. It has no idea what any index is for, which is the same thing that makes this node reusable.
Watching the wire
dataflow.yml sets debug: enable_debug_inspection: true, so dora will
let you read the port directly without the printer in the way. Run it under a coordinator
rather than dora run:
dora up
dora start dataflow.yml --uv --name joy-bench
dora topic echo -d joy-bench joy/joy # every reading, live
dora topic hz -d joy-bench joy/joy --duration 10 # should sit near 50 Hz
hz is the one that checks the promise this node makes: a reading every tick, whether or
not the pad moved. Tear it down with dora stop --name joy-bench && dora down.
Build
uv sync
Every dependency ships prebuilt wheels for linux-x86_64, linux-aarch64 and macos-arm64, so
the node installs with no system packages and no compiler. pygame-ce is used rather than
evdev for that reason and because evdev is Linux only.
Requirements: dora 1.0+ (verified with dora-cli 1.0.1), Python 3.11+, uv, and a
gamepad if you want one — the node starts without and says so.
Test
uv run pytest
uv run ruff check .
uv run ruff format --check .
Unit tests over the wire schema, the configuration and the probe's rendering; they need
neither a pad nor a dataflow. dora_joy/gamepad.py is the only file that touches pygame,
and the only one that cannot be tested without hardware. Everything else sees a plain
Sample.