docs: simplify the README and record the pad layout
validate / validate (pull_request) Successful in 58m41s
validate / test (pull_request) Successful in 58m44s
validate / markdownlint (pull_request) Successful in 58m40s

The README carried a lot of rationale that AGENTS.md already holds; what is
left is the tables a consumer wires against. It gains the swept axis and
button indices, and says plainly that the axes check themselves at runtime
and the buttons cannot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 18:06:12 -03:00
co-authored by Claude Opus 5
parent f7257ab903
commit 5ce83ae945
3 changed files with 231 additions and 331 deletions
+27 -4
View File
@@ -46,16 +46,19 @@ uv sync --group dev # plus pytest and ruff
```bash
dora run dataflow.yml --uv # this node plus a bench stand-in, no pad
dora run dataflow-gamepad.yml --uv # the same, with dora-joy pulled in by git:
dora run dataflow-robot.yml # the pad and a real arm, both pulled in by git:
dora run dataflow-gamepad.yml # the same, with the probe declared and no console
```
`dataflow.yml` is self-contained on purpose — it resolves from this checkout alone, which
is what lets CI validate it. Keep it that way; the pad belongs in `dataflow-gamepad.yml`.
is what lets CI validate it. Keep it that way; the pad belongs in the other two. Neither
of those takes `--uv`: dora 1.0.1 installs a `git:` node into a venv inside its own clone
and then looks for its entrypoint in the dataflow's venv. README.md has the detail.
## Test
```bash
uv run pytest # 112 tests, no gamepad, arm, browser or daemon needed
uv run pytest # 149 tests, no gamepad, arm, browser or daemon needed
uv run ruff check .
uv run ruff format --check .
```
@@ -74,6 +77,14 @@ a source goes away**.
- **`dora_teleop/joy.py`** — The pad as it arrives from `dora-joy`. `Sample.present` is
the load-bearing field: an unplugged pad reads as *absent*, not as centred, so "the
operator let go" and "the pad fell off the desk" stay distinguishable.
**`Calibration` is the other half of that seam.** It measures a pad the first time it is
seen at rest and hands the rest of the node a reading as the layout's ideal pad would
have sent it, so a worn stick's tilt is subtracted rather than deadzoned and a trigger
that rests at 0 stops being a special case. A pad whose resting axes do not fit `Layout`
reads as *absent* and is never corrected: one reading cannot tell a trigger at -1.0 from
a stick held hard over, and guessing wrong drives a real arm. It is not re-measured
while the pad is in use — doing that under a held stick would record the deflection as
centre, and the arm would move when the operator let go.
**`JoyInput.read` is the other safety-critical function in this repo.** Moving the pad
into another process added a way for it to go away that did not exist before — that
process dying — and from here that looks like silence rather than like an unplugged pad.
@@ -90,7 +101,12 @@ a source goes away**.
units: mm/s and deg/s, base frame. `is_active` lives here rather than on `Sample`
because it needs the layout: **a trigger rests at -1.0**, so the pad-is-in-use test has
to normalise triggers exactly as `velocity` does, or every connected pad reads as in use
for ever and `GAMEPAD_IDLE_RELEASE_S` never fires.
for ever and `GAMEPAD_IDLE_RELEASE_S` never fires. `rest_zeros` and `canonical` are the
same concern one layer out: they check a resting pad against `Layout` and map it onto
the ideal one. They are the only defence against a wrong index, which is not a
theoretical failure — an Xbox One S pad reporting its triggers on 4 and 5 against a
layout expecting them on 2 and 5 read as a full-speed pitch command from an untouched
pad, held the lease for ever, and moved a real arm into a controller error.
- **`dora_teleop/arbiter.py`** — The lease. One place that answers "why did it not move",
rather than one answer per input path. Ported from the monolithic repo's `RobotService`
lease because the console is already built on it — it gates its controls on
@@ -170,6 +186,13 @@ belongs here, and a port nothing wires reads as supported while having never run
pair; nothing can enforce the second from here.
- **An unplugged pad, a dead `dora-joy` or a departed browser stops the arm immediately**,
without waiting out any timer beyond the one that detects it.
- **A pad is driven from only after it has been seen at rest**, and a pad whose rest does
not fit `Layout` is not driven from at all. This covers the axes and **cannot cover the
buttons**: an unpressed button reads the same on every pad, so a wrong button index is
silent until somebody presses it and gets another verb. Sweeping a new pad with the
probe is the only check, and it is worth doing before the arm is powered. The cost is that a pad held when it appears
is ignored until it is let go; the alternative is guessing a numbering from one reading,
against a real arm.
- **Both control sources go through `mapping`.** The web path used to bypass it and pass
a velocity straight through, which is why the console had to speak mm/s. One conversion
point or none.