The dataflow turned on `enable_debug_inspection` without the README saying why anyone would want it, which made it a line of configuration nobody could act on. Documents the three commands it buys — topic list, topic echo, topic hz — and that they need a coordinator rather than `dora run`. hz is the one that checks the timings this node promises, so it names the measurement to expect.
241 lines
11 KiB
Markdown
241 lines
11 KiB
Markdown
# dora-teleop
|
|
|
|
A dora-rs node that hands a robot arm to a human. Pick up a gamepad and move the arm, or
|
|
open a browser and move it from there — one of the two at a time, decided here.
|
|
|
|
It drives [`dora-ulite6`](https://gitea.liria.cl/liria/dora-ulite6) through that node's
|
|
ordinary `jog` and `request` ports, and talks to the browser through
|
|
[`dora-websocket-transceiver`](https://gitea.liria.cl/liria/dora-websocket-transceiver).
|
|
It contains no web server and no robot driver: it is the piece in the middle that decides
|
|
what the arm should be doing, and that decision is the only thing it owns.
|
|
|
|
## The gamepad
|
|
|
|
Analog and blended — several axes at once, and how fast depends on how far you push. That
|
|
is the whole reason a pad beats the web console's four-sector dial pad, which can only ask
|
|
for one axis at a time at one fixed speed.
|
|
|
|
| Control | Does | Units |
|
|
|-------------|-------------------------------------------|-------|
|
|
| Left stick | Move along X (up/down) and Y (left/right) | mm/s |
|
|
| Triggers | Lift on the right, lower on the left | mm/s |
|
|
| Right stick | Roll (left/right) and pitch (up/down) | deg/s |
|
|
| Bumpers | Yaw, left and right | deg/s |
|
|
| A | Close the tool | — |
|
|
| B | Open the tool | — |
|
|
| X | Clear a controller error | — |
|
|
| Y | Toggle freedrive | — |
|
|
| Back | **E-STOP** | — |
|
|
|
|
Up on the stick sends the arm away from you, along **+X** in the robot's base frame —
|
|
the same convention the web console's dial uses, so the two agree.
|
|
|
|
Indices are SDL's, which is what most Xbox- and PlayStation-style pads report. A pad that
|
|
disagrees is a change to `Layout` in `dora_teleop/mapping.py` rather than a new
|
|
environment variable: an index nobody can verify from outside is worse than a line of code
|
|
somebody can read.
|
|
|
|
`home` is deliberately **not** on the pad. It is a large unattended motion and belongs
|
|
behind a deliberate click, not a thumb resting on a face button.
|
|
|
|
### Checking your pad
|
|
|
|
```bash
|
|
uv run dora-teleop-probe
|
|
```
|
|
|
|
Prints the raw axes, the buttons it sees, and the velocity this node would send — live,
|
|
with no dataflow and no arm. **It cannot move anything.** There is no dora node and no
|
|
connection to the robot; it only reads.
|
|
|
|
```text
|
|
axes (6 reported)
|
|
0 left stick X +0.600 ----------|-----#----
|
|
1 left stick Y -0.800 --#-------|----------
|
|
2 left trigger -1.000 #---------|----------
|
|
...
|
|
buttons (6 reported)
|
|
5:yaw right
|
|
|
|
velocity this would send to the arm
|
|
vx +38.89 mm/s forward / back <--
|
|
vy +23.53 mm/s left / right <--
|
|
vz +20.39 mm/s up / down <--
|
|
wz +30.00 deg/s yaw <--
|
|
|
|
moving: yes
|
|
```
|
|
|
|
Run this before trusting the table above. The indices are SDL's documented layout, not
|
|
observed ones, and a pad that numbers its axes differently is much better found here than
|
|
by watching an arm move the wrong way. Two things to look for:
|
|
|
|
- **Every axis is listed, including ones the layout does not use** — an axis marked
|
|
`unused` that moves when you push a stick means the layout needs changing.
|
|
- **A bar that does not sit centred with the stick released** is a worn pad announcing
|
|
itself, and a reason to raise `DEADZONE`.
|
|
|
|
It goes through the same `Gamepad` and `velocity()` the node runs, so what it prints is
|
|
what the arm would be sent.
|
|
|
|
## The lease
|
|
|
|
Two sources, one arm. Whoever holds the lease drives; the other is ignored.
|
|
|
|
- The **gamepad takes it implicitly**, the first time a stick moves, if nobody holds it.
|
|
Nobody should have to reach for a browser to use the thing in their hands.
|
|
- It **gives it back** after `GAMEPAD_IDLE_RELEASE_S` untouched, so a pad left on the
|
|
bench does not lock the browser out forever.
|
|
- The **browser takes it explicitly**, with the console's TAKE CONTROL button, and gives
|
|
it back with RELEASE. It can preempt a held arm, because an operator who can see the arm
|
|
is better placed than this code to decide they should have it.
|
|
- **E-STOP is never gated.** From either source, held lease or not. Somebody reaching for
|
|
the stop is the one moment when asking who owns the arm is the wrong question.
|
|
|
|
**The lease binds only what goes through this node.** `dora-ulite6` has no idea it exists.
|
|
Another node publishing `request` will preempt an operator mid-motion and nothing here can
|
|
stop it — if that matters for your dataflow, do not wire one.
|
|
|
|
## The deadman
|
|
|
|
An arm under a velocity command keeps moving until told otherwise, so the question is
|
|
never whether to start but whether anything has happened that means it must stop. Three
|
|
layers, outermost last:
|
|
|
|
1. **Re-send every tick** (50 ms) while a source is driving.
|
|
2. **This node's watchdog** (`JOG_TTL_S`, 0.4 s) stops the arm when the owner goes quiet —
|
|
and stops it *immediately*, without waiting out the timer, when the gamepad is unplugged
|
|
or the last browser disconnects. An unplugged pad is not a centred pad.
|
|
3. **The arm's own expiry** (`VELOCITY_COMMAND_TTL_S` in `dora-ulite6`, 0.5 s), enforced by
|
|
the controller whether or not this process is alive.
|
|
|
|
Keep them in that order. If this node's timer is longer than the controller's, the backstop
|
|
fires before the thing it is backing up.
|
|
|
|
On shutdown the node sends one all-zero velocity before exiting. A motion lives in the
|
|
controller, not in this process.
|
|
|
|
The stop is sent **once**, not every tick. A stream of zeroes would keep `dora-ulite6`
|
|
believing a jog is live and hold `robot_state` at `moving` forever.
|
|
|
|
## Ports
|
|
|
|
| Direction | Name | Description |
|
|
|-----------|----------------|------------------------------------------------------|
|
|
| in | `tick` | 50 ms — re-send, deadman, telemetry |
|
|
| in | `ui_command` | One console message, from the bridge's `data_out` |
|
|
| in | `clients` | Connect/disconnect from the bridge — the web deadman |
|
|
| in | `response` | An answer from the arm |
|
|
| in | `motion_state` | How a motion ended |
|
|
| in | `robot_state` | The arm's own state |
|
|
| in | `tcp_pose` | Tool-tip pose, six float32, mm and degrees |
|
|
| in | `joint_pose` | Joint angles, six float32, degrees |
|
|
| out | `jog` | `{velocity: [vx, vy, vz, wx, wy, wz]}`, mm/s + deg/s |
|
|
| out | `request` | A service call, `request_id` in the metadata |
|
|
| out | `ui_state` | The console's telemetry snapshot, as JSON |
|
|
|
|
## Environment Variables
|
|
|
|
| Variable | Default | Description |
|
|
|--------------------------|---------|--------------------------------------------------------|
|
|
| `SPEED_MM_S` | `60.0` | Translation speed at full deflection |
|
|
| `SPEED_DEG_S` | `30.0` | Rotation speed at full deflection |
|
|
| `DEADZONE` | `0.12` | Stick travel near centre to ignore |
|
|
| `EXPO` | `0.4` | How much of the curve is cubic; 0 is linear |
|
|
| `JOYSTICK_INDEX` | `0` | Which pad to open |
|
|
| `JOG_TTL_S` | `0.4` | Owner silence before the arm is stopped |
|
|
| `GAMEPAD_IDLE_RELEASE_S` | `2.0` | Untouched pad before it hands the arm back |
|
|
| `STATE_POLL_S` | `0.5` | How often to ask the arm what telemetry does not carry |
|
|
|
|
A value that cannot be read, a `DEADZONE` outside the stick or an `EXPO` off the curve
|
|
stops the node at startup rather than the first time somebody picks up the pad.
|
|
|
|
## Usage
|
|
|
|
The real stack is three nodes: this one, the WebSocket bridge and the arm.
|
|
|
|
```yaml
|
|
- id: teleop
|
|
git: ssh://git@gitea.liria.cl:5001/liria/dora-teleop.git
|
|
tag: v0.1.0
|
|
build: pip install .
|
|
path: dora-teleop
|
|
inputs:
|
|
tick: dora/timer/millis/50
|
|
ui_command: ws-transceiver/data_out
|
|
clients: ws-transceiver/clients
|
|
response: ulite6/response
|
|
motion_state: ulite6/motion_state
|
|
robot_state: ulite6/robot_state
|
|
tcp_pose: ulite6/tcp_pose
|
|
joint_pose: ulite6/joint_pose
|
|
outputs: [jog, request, ui_state]
|
|
```
|
|
|
|
Point `ws-transceiver`'s `data_in` at `teleop/ui_state` and its `STATIC_DIR` at this
|
|
repo's `web/`, and wire `ulite6`'s `request` and `jog` from this node.
|
|
|
|
**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.
|
|
|
|
### Running it with no arm and no browser
|
|
|
|
`dataflow.yml` in this repo is **not** that stack. It is this node plus a bench stand-in,
|
|
so you can drive the real thing from a real gamepad with no robot in the room:
|
|
|
|
```bash
|
|
dora run dataflow.yml --uv
|
|
```
|
|
|
|
The bench answers every service, publishes plausible telemetry, and prints each velocity
|
|
it is sent:
|
|
|
|
```text
|
|
jog 1 [ +38.89, +23.53, +0.00, +0.00, +0.00, +0.00]
|
|
jog 2 [ +38.89, +23.53, +0.00, +0.00, +0.00, +0.00]
|
|
jog 3 [ +0.00, +0.00, +0.00, +0.00, +0.00, +0.00] <- stop
|
|
```
|
|
|
|
It is **not a simulator**: it never integrates the velocity or moves the pose, and a jog
|
|
is printed and forgotten. It closes the loop so this node's behaviour can be watched —
|
|
the lease changing hands, the deadman firing when you let go — not so the arm can be
|
|
pretended at.
|
|
|
|
### Watching the wire
|
|
|
|
`dataflow.yml` sets `debug: enable_debug_inspection: true`, so dora will let you read what
|
|
is actually on the wire without the bench printing anything. Run it under a coordinator
|
|
rather than `dora run`:
|
|
|
|
```bash
|
|
dora up
|
|
dora start dataflow.yml --uv --name teleop-bench
|
|
```
|
|
|
|
Then, in another terminal:
|
|
|
|
```bash
|
|
dora topic list -d teleop-bench # every edge in the graph
|
|
dora topic echo -d teleop-bench teleop/jog # every velocity, live
|
|
dora topic echo -d teleop-bench teleop/ui_state # what the console would draw
|
|
dora topic hz -d teleop-bench teleop/jog --duration 10 # the re-send cadence
|
|
```
|
|
|
|
`hz` is the one that checks the timings this node promises. The `get_state` poll measures
|
|
at 1.88 Hz against a configured `STATE_POLL_S` of 0.5, and with a pad held over the
|
|
deadzone `teleop/jog` should sit near the 50 ms tick.
|
|
|
|
Tear it down with `dora stop --name teleop-bench && dora down`.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
uv sync --group dev
|
|
uv run pytest # 76 tests, no gamepad, arm, browser or daemon needed
|
|
uv run ruff check .
|
|
uv run ruff format --check .
|
|
```
|
|
|
|
`dora_teleop/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`.
|