A dora node that reads one gamepad and publishes its axes and buttons as an Arrow struct.
It is a source: 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. The repository names no consumer.
The message is ROS-like in spirit but dora in fact: no header (dora already knows when
a message arrived, and a second clock in the payload only invites skew questions),
booleans rather than ROS's int32, and a present field rather than an absence of
messages — a consumer driving something that moves needs "unplugged" and "centred" to be
different answers within a tick, not within a timeout.
Related Issues
None — this is the repository's initial content.
Changes
dora_joy/gamepad.py — the pad on a thread of its own; the only file that touches
pygame, so everything else is testable with no hardware
dora_joy/message.py — the {axes: list<float32>, buttons: list<bool>, present: bool}
schema, declared rather than inferred: pyarrow reads an empty axes list as list<null>, so an inferred schema would change the type of the port the moment
somebody unplugs the pad
dora_joy/main.py — publishes the latest reading on every tick, changed or not, so the
stream doubles as a heartbeat and silence means the node died and nothing else
dora_joy/config.py — JOYSTICK_INDEX, parsed once at startup so an unusable value
stops the node before it joins the dataflow
dora_joy/probe.py — dora-joy-probe: every axis and button by index, live, with no
dataflow and nothing attached. Push one control at a time and read off the index
dataflow.yml + examples/printer.py — self-contained, so CI validates and runs it
with no gamepad and no sibling clones
dora-node.yml — the typed contract. joy is untyped on purpose: dora 1.0 ships no
standard joystick type, so the struct is documented in the description instead
.gitea/workflows/validate.yml — dora validate, ruff check and format, pytest,
markdownlint
Validation
Tests pass — 17, needing neither a pad nor a dataflow
uv run ruff check . and uv run ruff format --check . clean
dora validate dataflow.yml passes
dora run dataflow.yml --uv runs and publishes at ~50 Hz against a 20 ms tick
Verified against a physical gamepad
The last box is deliberately unticked: there is no joystick device on the machine this
was built on, so present: true has never been exercised on real hardware. Everything
observed so far is the present: false path. Worth running uv run dora-joy-probe with
a pad plugged in before tagging.
# Summary
A dora node that reads one gamepad and publishes its axes and buttons as an Arrow struct.
It is a **source**: 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. The repository names no consumer.
The message is ROS-like in spirit but dora in fact: no `header` (dora already knows when
a message arrived, and a second clock in the payload only invites skew questions),
booleans rather than ROS's `int32`, and a `present` field rather than an absence of
messages — a consumer driving something that moves needs "unplugged" and "centred" to be
different answers within a tick, not within a timeout.
## Related Issues
None — this is the repository's initial content.
## Changes
- `dora_joy/gamepad.py` — the pad on a thread of its own; the only file that touches
pygame, so everything else is testable with no hardware
- `dora_joy/message.py` — the `{axes: list<float32>, buttons: list<bool>, present: bool}`
schema, **declared rather than inferred**: pyarrow reads an empty `axes` list as
`list<null>`, so an inferred schema would change the type of the port the moment
somebody unplugs the pad
- `dora_joy/main.py` — publishes the latest reading on every tick, changed or not, so the
stream doubles as a heartbeat and silence means the node died and nothing else
- `dora_joy/config.py` — `JOYSTICK_INDEX`, parsed once at startup so an unusable value
stops the node before it joins the dataflow
- `dora_joy/probe.py` — `dora-joy-probe`: every axis and button by index, live, with no
dataflow and nothing attached. Push one control at a time and read off the index
- `dataflow.yml` + `examples/printer.py` — self-contained, so CI validates and runs it
with no gamepad and no sibling clones
- `dora-node.yml` — the typed contract. `joy` is untyped on purpose: dora 1.0 ships no
standard joystick type, so the struct is documented in the description instead
- `.gitea/workflows/validate.yml` — `dora validate`, ruff check and format, pytest,
markdownlint
## Validation
- [x] Tests pass — 17, needing neither a pad nor a dataflow
- [x] `uv run ruff check .` and `uv run ruff format --check .` clean
- [x] `dora validate dataflow.yml` passes
- [x] `dora run dataflow.yml --uv` runs and publishes at ~50 Hz against a 20 ms tick
- [x] Verified against a physical gamepad
The last box is deliberately unticked: there is no joystick device on the machine this
was built on, so `present: true` has never been exercised on real hardware. Everything
observed so far is the `present: false` path. Worth running `uv run dora-joy-probe` with
a pad plugged in before tagging.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
A dora node that reads one gamepad and publishes its axes and buttons as an Arrow struct.
It is a source: 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. The repository names no consumer.
The message is ROS-like in spirit but dora in fact: no
header(dora already knows whena message arrived, and a second clock in the payload only invites skew questions),
booleans rather than ROS's
int32, and apresentfield rather than an absence ofmessages — a consumer driving something that moves needs "unplugged" and "centred" to be
different answers within a tick, not within a timeout.
Related Issues
None — this is the repository's initial content.
Changes
dora_joy/gamepad.py— the pad on a thread of its own; the only file that touchespygame, so everything else is testable with no hardware
dora_joy/message.py— the{axes: list<float32>, buttons: list<bool>, present: bool}schema, declared rather than inferred: pyarrow reads an empty
axeslist aslist<null>, so an inferred schema would change the type of the port the momentsomebody unplugs the pad
dora_joy/main.py— publishes the latest reading on every tick, changed or not, so thestream doubles as a heartbeat and silence means the node died and nothing else
dora_joy/config.py—JOYSTICK_INDEX, parsed once at startup so an unusable valuestops the node before it joins the dataflow
dora_joy/probe.py—dora-joy-probe: every axis and button by index, live, with nodataflow and nothing attached. Push one control at a time and read off the index
dataflow.yml+examples/printer.py— self-contained, so CI validates and runs itwith no gamepad and no sibling clones
dora-node.yml— the typed contract.joyis untyped on purpose: dora 1.0 ships nostandard joystick type, so the struct is documented in the description instead
.gitea/workflows/validate.yml—dora validate, ruff check and format, pytest,markdownlint
Validation
uv run ruff check .anduv run ruff format --check .cleandora validate dataflow.ymlpassesdora run dataflow.yml --uvruns and publishes at ~50 Hz against a 20 ms tickThe last box is deliberately unticked: there is no joystick device on the machine this
was built on, so
present: truehas never been exercised on real hardware. Everythingobserved so far is the
present: falsepath. Worth runninguv run dora-joy-probewitha pad plugged in before tagging.