serve the web console from the node and drop the websocket bridge
The browser reached this node through `dora-websocket-transceiver`: a second node, a second port, and a round trip through the dataflow for every button press, to carry a UI only this node can interpret. The node now serves the page and holds the browsers' sockets itself on one port, so the console is one URL and a consumer wires nothing for it. `dora_teleop/server.py` runs aiohttp on a thread of its own and meets the control loop at two non-blocking points: a bounded queue the tick drains, and a snapshot handed back. Both queues are bounded deliberately — a flood is dropped rather than grown, and a slow browser loses stale snapshots rather than the newest — because nothing a browser does may stall the thread that stops the arm. A port already in use stops the node rather than leaving it announcing a console nobody can open. The web deadman moved but did not change: it was a `clients` message from the bridge and is now the open-socket count read each tick. One hop fewer, same meaning. Contract, so 0.2.0 -> 0.3.0: `ui_command` and `clients` are gone. `ui_state` stays published for `dora topic echo` and the bench, with no browser reading it. New WEB_HOST and WEB_PORT (7000; 0 opens no port). The console itself is laid out like the arm's own pendant — two rings quartered into arc sectors you hold, each with its out-of-plane axis as pills above. Directions and signs match xArm Studio on the controller, `Y+` on the left included: two consoles driving one arm that disagree about which way is +Y is a trap, not a preference. Also sends a velocity only when it changes, repeating an unchanged one on JOG_KEEPALIVE_S (0.2 s) purely to beat the consumer's own expiry. Repeating every tick made `dora-ulite6` re-plan the same motion twenty times a second. That turned out not to be the whole cause of the stepping an operator felt — the rest was fixed in dora-ulite6 v0.3.1, pinned here — but sending 18 messages where 83 went before is worth keeping on its own. `dora-joy` moves to v0.1.1: v0.1 is an annotated tag, and dora 1.0.1 records the tag object's id as the commit hash and then rejects its own checkout for sitting on the peeled commit. Neither dataflow could be built before this.
This commit is contained in:
@@ -7,16 +7,22 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
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**, and it **touches no hardware at all**.
|
||||
The arm is [`dora-ulite6`](https://gitea.liria.cl/liria/dora-ulite6), reached through that
|
||||
node's ordinary `jog` and `request` ports. The browser is carried by
|
||||
[`dora-websocket-transceiver`](https://gitea.liria.cl/liria/dora-websocket-transceiver).
|
||||
The gamepad is [`dora-joy`](https://gitea.liria.cl/liria/dora-joy), which publishes raw
|
||||
axes and buttons on `joy` and knows nothing about arms. Do not add any of the three here.
|
||||
A teleop node that grew its own HTTP server is exactly the monolith `dora-ulite6` was
|
||||
rewritten to get out of, and the pad was split out of this repo for the same reason: a
|
||||
node whose job is policy should not carry a hardware dependency, and a pad nobody else can
|
||||
reach is a pad that gets reimplemented.
|
||||
It is **the piece in the middle**, and it **touches no hardware at all**. The arm is
|
||||
[`dora-ulite6`](https://gitea.liria.cl/liria/dora-ulite6), reached through that node's
|
||||
ordinary `jog` and `request` ports. The gamepad is
|
||||
[`dora-joy`](https://gitea.liria.cl/liria/dora-joy), which publishes raw axes and buttons
|
||||
on `joy` and knows nothing about arms. Do not add either here: a node whose job is policy
|
||||
should not carry a hardware dependency, and a pad nobody else can reach is a pad that gets
|
||||
reimplemented.
|
||||
|
||||
**The web console is the exception, and it is deliberate.** This node serves the page and
|
||||
holds the browsers' sockets itself (`dora_teleop/server.py`), rather than going through
|
||||
`dora-websocket-transceiver` as it did up to v0.2.0. A bridge node for one page meant a
|
||||
second node, a second port and a round trip through the dataflow for every button press,
|
||||
to carry a UI that only this node can interpret anyway. That is not the monolith
|
||||
`dora-ulite6` was rewritten to escape — the monolith was one process owning the robot
|
||||
driver, perception and the UI at once. What is forbidden is the hardware, and that has not
|
||||
moved.
|
||||
|
||||
**What stays here is everything about what a reading *means***: the deadzone, the response
|
||||
curve, the layout (which button is grasp), and the mapping to base-frame mm/s and deg/s.
|
||||
@@ -26,7 +32,8 @@ number.
|
||||
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.
|
||||
rebuild the monolith inside a node. That boundary is the one that matters, and the
|
||||
built-in server does not move it: the console can ask for motion and nothing else.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -86,7 +93,7 @@ a source goes away**.
|
||||
for ever and `GAMEPAD_IDLE_RELEASE_S` never fires.
|
||||
- **`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
|
||||
lease because the console is already built on it — it gates its controls 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
|
||||
@@ -100,7 +107,13 @@ a source goes away**.
|
||||
layers that are out of order, which is otherwise invisible until the day it matters.
|
||||
- **`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`
|
||||
- **`dora_teleop/server.py`** — The console's HTTP and WebSocket server, on a thread of
|
||||
its own. Serves `dora_teleop/web/index.html` at `/` and holds the browsers at `/ws`, on
|
||||
one port so the page finds its socket on its own origin. Both queues are bounded: a
|
||||
flood of messages is dropped rather than grown, and a slow browser loses stale snapshots
|
||||
rather than the newest one. **The browser's deadman lives here** — `clients` counts open
|
||||
sockets, and the tick reads it.
|
||||
- **`examples/bench.py`** — A stand-in for the arm, 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
|
||||
@@ -138,6 +151,11 @@ belongs here, and a port nothing wires reads as supported while having never run
|
||||
|
||||
## Rules that are not negotiable
|
||||
|
||||
- **Send the change, not the state.** A velocity is a set-point the arm holds. `_drive`
|
||||
sends when it changes and otherwise repeats on `JOG_KEEPALIVE_S`, which exists only to
|
||||
beat the arm's own expiry. Repeating every tick made a real xArm re-plan the same motion
|
||||
twenty times a second, which reads as stepping. Do not restore the every-tick re-send.
|
||||
A change must never be made to wait for the repeat.
|
||||
- **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, and neither is its release.** From either
|
||||
@@ -156,8 +174,13 @@ belongs here, and a port nothing wires reads as supported while having never run
|
||||
a velocity straight through, which is why the console had to speak mm/s. One conversion
|
||||
point or none.
|
||||
- **Declare every published Arrow schema.** See `message.py`.
|
||||
- **Do not reintroduce a web server, a robot driver or a gamepad driver here.** See above.
|
||||
In particular: no `pygame`, no `evdev`, and no environment variable naming a device.
|
||||
- **Do not add a robot driver or a gamepad driver here.** See above. In particular: no
|
||||
`pygame`, no `evdev`, and no environment variable naming a device. The web server is
|
||||
the one deliberate exception and is scoped to the Control console; anything the console
|
||||
cannot ask the arm to do does not belong on it.
|
||||
- **Nothing on the server thread may block the control loop.** `server.py` meets the loop
|
||||
at two non-blocking points only — the queue the tick drains and the snapshot handed
|
||||
back. A browser that stops reading must never stall the thread that stops the arm.
|
||||
|
||||
## What this node cannot do
|
||||
|
||||
|
||||
Reference in New Issue
Block a user