Two problems, both found by the validate job once it could finally run. categories named `teleop` and `input`, neither of which dora defines. The set is fixed — sensor, actuator, robot, transform, filter, ml-inference, llm, speech, communication, recorder, visualization, simulator, debug — and an unknown variant makes the whole manifest unparseable, so every type annotation in it was being ignored. dataflow.yml spanned three repositories through sibling paths, which meant dora could not resolve the other nodes' manifests and reported their typed outputs as unannotated. It also could not be validated in CI, which clones one repo. The house pattern is a self-contained dataflow, and this now follows it: teleop plus a bench stand-in for the arm and the browser, both from this checkout. The real three-node wiring moves to README.md, where a consumer would look for it. The bench answers every service, publishes plausible telemetry, and prints the velocities it is sent, so the node can be driven from a real gamepad with no robot in the room. It is not a simulator and must not become one: it never integrates a velocity or moves a pose.
150 lines
7.6 KiB
Markdown
150 lines
7.6 KiB
Markdown
# AGENTS.md
|
|
|
|
This file provides guidance to AI coding assistants when working with code in this repository.
|
|
|
|
## What this is
|
|
|
|
A dora-rs Python node that gives a human control of a robot arm, from a gamepad or from a
|
|
web browser, and decides which of the two is driving.
|
|
|
|
It is **the piece in the middle and nothing else**. It contains no robot driver — the arm
|
|
is [`dora-ulite6`](https://gitea.liria.cl/liria/dora-ulite6), reached through that node's
|
|
ordinary `jog` and `request` ports. It contains no web server — the browser is carried by
|
|
[`dora-websocket-transceiver`](https://gitea.liria.cl/liria/dora-websocket-transceiver).
|
|
Do not add either here. A teleop node that grew its own HTTP server is exactly the
|
|
monolith `dora-ulite6` was rewritten to get out of.
|
|
|
|
The web console it serves is ported from `Littlehand/monolitic-littlehand-python`
|
|
(`littlehand/console/static/`), and **only its Control section**. Perception, calibration,
|
|
tasks, chat, ROI, settings and health belong to other nodes; porting them here would
|
|
rebuild the monolith inside a node.
|
|
|
|
## Build
|
|
|
|
```bash
|
|
uv sync # runtime deps
|
|
uv sync --group dev # plus pytest and ruff
|
|
```
|
|
|
|
## Run
|
|
|
|
```bash
|
|
dora run dataflow.yml --uv
|
|
```
|
|
|
|
`dataflow.yml` runs the whole stack — this node, the bridge and the arm — from checkouts
|
|
expected next to this one.
|
|
|
|
## Test
|
|
|
|
```bash
|
|
uv run pytest # 76 tests, no gamepad, arm, browser or daemon needed
|
|
uv run ruff check .
|
|
uv run ruff format --check .
|
|
```
|
|
|
|
## Architecture
|
|
|
|
Two things carry the design: **what may touch the hardware**, and **what must happen when
|
|
a source goes away**.
|
|
|
|
- **`dora_teleop/main.py`** — The dora event loop, and the one decision this node exists
|
|
to make: whether a velocity reaches the arm this tick, and from whom. **It never
|
|
blocks.** `_drive` and `_wanted_velocity` are the safety-critical pair — every path that
|
|
is not an owner actively asking for motion returns a stop, so a case nobody thought of
|
|
fails by halting rather than by carrying on. Keep it that way: the default has to be
|
|
"still".
|
|
- **`dora_teleop/gamepad.py`** — The pad, on a thread of its own. **Nothing outside this
|
|
file may touch pygame.** Everything else sees a plain immutable `Sample`, which is what
|
|
lets the rest be tested with no hardware and keeps SDL's quirks out of the arm logic.
|
|
`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.
|
|
- **`dora_teleop/mapping.py`** — Stick deflection to base-frame velocity. Pure functions,
|
|
no state, no hardware, because this is the part that has to be right and the cheapest to
|
|
check. Output is always the arm node's units: mm/s and deg/s, base frame.
|
|
- **`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's `app.js` is already built on it — it gates its jog pad on
|
|
`t.owner === "web"` and draws an owner chip. A simpler last-writer-wins scheme would mean
|
|
rewriting the page to match the node rather than the other way round.
|
|
- **`dora_teleop/ui.py`** — Translation both ways. The console's telemetry keys are a
|
|
contract: `app.js` reads `arm_state`, `owner`, `estop`, `vacuum`, `freedrive`,
|
|
`position`, `joints` by name, so renaming one breaks the page silently. `arm_state()` is
|
|
where the arm's vocabulary (`connecting|ready|moving|recovering|unavailable`) is
|
|
reconciled with the console's (`idle|moving|jogging|freedrive|estop|stopped|disconnected`).
|
|
- **`dora_teleop/config.py`** — Every environment variable, parsed once at startup, so an
|
|
unusable setting stops the node before it joins the dataflow instead of surfacing the
|
|
first time somebody picks up the pad. Errors name the variable.
|
|
- **`dora_teleop/log.py`** — Timestamped stderr logging. stderr because dora reads a
|
|
dynamic node's stdout.
|
|
- **`examples/bench.py`** — A stand-in for the arm *and* the browser, so `dataflow.yml`
|
|
is self-contained and CI can validate it with no sibling clones. **It is not a
|
|
simulator** and must not become one: it never integrates a velocity or moves a pose. Its
|
|
job is to close the loop so this node can be watched, not to stand in for
|
|
`dora-ulite6` — anything needing the arm to actually respond needs the arm.
|
|
- **`dora_teleop/probe.py`** — `dora-teleop-probe`: shows the raw axes, the buttons and the
|
|
velocity that would be sent, with no dataflow and no arm. It exists because the indices
|
|
in `mapping.py` are SDL's documented layout rather than an observed one, and a pad that
|
|
disagrees is otherwise found by watching a real arm move the wrong way. It goes through
|
|
the same `Gamepad` and `velocity()` the node runs, so what it prints is what the arm
|
|
would get — keep it that way, or it stops being evidence.
|
|
|
|
## Rules that are not negotiable
|
|
|
|
- **The stop is sent once, not every tick.** A stream of zero velocities keeps
|
|
`dora-ulite6` believing a jog is live and holds `robot_state` at `moving` forever.
|
|
- **E-STOP is never gated on the lease.** From either source, at any time.
|
|
- **`JOG_TTL_S` must stay below the arm node's `VELOCITY_COMMAND_TTL_S`** (0.5 s). The
|
|
controller-side expiry is the backstop; a backstop that fires first backs up nothing.
|
|
- **An unplugged pad or a departed browser stops the arm immediately**, without waiting out
|
|
any timer.
|
|
- **Do not reintroduce a web server or a robot driver here.** See above.
|
|
|
|
## What this node cannot do
|
|
|
|
The lease binds only what goes through this node. `dora-ulite6` has no concept of an owner,
|
|
so another node publishing `request` will preempt an operator mid-motion and nothing here
|
|
can prevent it. Say so rather than implying otherwise.
|
|
|
|
`jog` has no feedback channel at all — a malformed one is logged and dropped by the arm
|
|
node, with no response and no error output. What this node sends, it learns nothing back
|
|
about except through `robot_state` and telemetry.
|
|
|
|
## Two manifest rules worth knowing
|
|
|
|
`dora-node.yml` is the typed contract a consumer wires against, and the only one they get:
|
|
no source package is shared between node repositories. `dora validate` checks a dataflow
|
|
against it without running the node, so CI needs neither a gamepad nor a robot.
|
|
|
|
`jog`, `request`, `response`, `motion_state` and the `ui_` ports are deliberately
|
|
**untyped**. dora treats a port carrying `request_id` as pattern-correlated and skips type
|
|
checks on it, and several Arrow schemas legitimately flow over each. Annotating them would
|
|
claim a contract neither side keeps.
|
|
|
|
## Key APIs
|
|
|
|
- **dora Python API** (`dora`): `Node()`, `node.send_output(id, array, metadata)`, event
|
|
iteration via `for event in node`.
|
|
- **pygame-ce** (`pygame.joystick`): chosen over evdev because Littlehand targets Linux
|
|
*and* macOS and evdev is Linux-only, and because it ships prebuilt wheels for
|
|
linux-x86_64, linux-aarch64 and macos-arm64 — nothing here compiles from source. Only
|
|
the display and joystick subsystems are initialised: `pygame.init()` also opens audio,
|
|
which on a headless robot fills stderr with ALSA errors for a device never used. The
|
|
display is needed despite there being no window, because SDL refreshes joystick state
|
|
during the event pump and the pump needs it.
|
|
|
|
## Tools
|
|
|
|
- **Issue tracker:** Gitea. Use the Gitea MCP tools (`mcp__gitea__*`) to read and manage
|
|
issues, pull requests, and other repository operations.
|
|
|
|
## Code style
|
|
|
|
- Write docstrings in NumPy style (summary line, then `Parameters`, `Returns`, `Raises`
|
|
sections as needed).
|
|
- Run `ruff check .` and `ruff format .` before committing. CI enforces both.
|
|
|
|
## Rules
|
|
|
|
- Do not mention AI tools in commit messages or documentation.
|