`v0.1` is an annotated tag, which dora 1.0.1 cannot check out: it records the tag object's id as the commit hash, then rejects the checkout for sitting on the peeled commit. `v0.1.1` is lightweight and resolves straight to the commit.
182 lines
5.8 KiB
Markdown
182 lines
5.8 KiB
Markdown
# 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:
|
|
|
|
```yaml
|
|
nodes:
|
|
- id: joy
|
|
git: https://gitea.liria.cl/liria/dora-joy.git
|
|
tag: v0.1.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:
|
|
|
|
```yaml
|
|
inputs:
|
|
joy:
|
|
source: joy/joy
|
|
queue_size: 1
|
|
queue_policy: drop_oldest
|
|
```
|
|
|
|
### Runnable dataflow
|
|
|
|
[`dataflow.yml`](dataflow.yml) is the node plus a printer, and is what CI runs:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```yaml
|
|
env:
|
|
JOYSTICK_INDEX: 1
|
|
```
|
|
|
|
## Checking your pad
|
|
|
|
```bash
|
|
uv run dora-joy-probe
|
|
```
|
|
|
|
Prints every axis and every button by index, live, with no dataflow and nothing attached.
|
|
**It cannot move anything.**
|
|
|
|
```text
|
|
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`](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`:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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`.
|