add dora-joy node publishing gamepad axes and buttons
This commit is contained in:
@@ -1,2 +1,181 @@
|
||||
# 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.0
|
||||
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`.
|
||||
|
||||
Reference in New Issue
Block a user