Files
dora-teleop/dora-node.yml
cristhian e3fc538e5d
validate / validate (pull_request) Successful in 58m44s
validate / test (pull_request) Successful in 58m45s
validate / markdownlint (pull_request) Successful in 58m40s
docs: simplify the README and record the pad layout
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.
2026-09-16 18:06:12 -03:00

264 lines
11 KiB
YAML

# Node manifest: the typed contract a consumer wires against.
#
# No source package is shared between node repositories, so this file and its
# type URNs are the only contract a consumer has. `dora validate` checks a
# dataflow against it without running the node, so CI needs neither a gamepad
# nor a robot.
#
# `joy`, `jog`, `request`, `response`, `motion_state` and `ui_state` are
# deliberately **untyped**. dora treats a port carrying `request_id` as
# pattern-correlated and skips type checks on it, several Arrow schemas
# legitimately flow over each — one per service, one per console message — and
# dora 1.0 ships no standard joystick type. Annotating them would claim a
# contract neither side keeps.
apiVersion: 1
name: dora-teleop
namespace: liria
description: Hands a robot arm to a human, from a gamepad or a browser
categories: [robot]
keywords: [teleop, gamepad, joystick, jog, arm, manipulator, web-console, operator]
runtime: python
entrypoint: dora-teleop
# `dora build --uv` / `dora run --uv` rewrites this to `uv pip install .` and
# gives the node its own managed venv under `.dora/python-envs/`, so the
# dependencies it sees at runtime are the ones installed here.
build: pip install .
dora: ">=1.0"
inputs:
tick:
description: >-
Drives everything time-based: noticing a changed velocity, repeating the
held one, expiring a
source that has gone quiet, and publishing the console's telemetry. Use 50
ms. Slower than about 100 ms and the arm node's own velocity expiry starts
firing between ticks, which an operator feels as the arm stuttering.
type: std/core/v1/UInt64
joy:
required: false
description: >-
One gamepad reading from `dora-joy`, as the Arrow struct
`{axes: list<float32>, buttons: list<bool>, present: bool}`. Raw SDL — the
deadzone, the response curve and the base-frame mapping all happen here.
Wire it with `queue_size: 1`: `dora-joy` ticks faster than this node does,
and a backlog of old readings is the last thing an arm should be driven
from.
**`present: false` stops the arm at once**, without waiting out any timer,
because an unplugged pad is not a centred pad. So does the stream going
quiet for longer than `JOY_TTL_S` — `dora-joy` publishes on every tick
whether or not the pad moved, so silence means that node died, and a dead
pad node is indistinguishable from an unplugged pad as far as the arm is
concerned.
Each pad is measured the first time it is seen sitting still, so a stick
that no longer centres has its tilt subtracted rather than deadzoned away.
A pad whose resting axes do not fit the layout this node compiles in — a
trigger where a stick should be, say — reads the same as no pad at all
and is named in the log, rather than being guessed at against a real arm.
Leave it unwired for a browser-only console. The node says so and starts.
response:
required: false
description: >-
An answer from the arm node, correlated by `request_id` in the metadata. A
failure is passed to the console as a toast; a `get_state` answer fills in
what telemetry does not carry.
motion_state:
required: false
description: >-
How a motion ended. Needed because this node sends motions with
`wait: false`, and a non-blocking motion reports its outcome here and
nowhere else.
robot_state:
required: false
description: >-
The arm's own state, one of `connecting`, `ready`, `moving`, `recovering`
or `unavailable`. Translated into the vocabulary the console draws.
type: std/core/v1/String
tcp_pose:
required: false
description: >-
Tool-tip pose, six float32 as `[x, y, z, roll, pitch, yaw]` in mm and
degrees, base frame. Shown in the console's pose readout.
type: std/core/v1/Float32
joint_pose:
required: false
description: >-
Joint angles, six float32 in degrees. Shown in the console's joint readout.
type: std/core/v1/Float32
outputs:
twist:
description: >-
**The standard motion port.** A velocity, as
`{linear: [x, y, z], angular: [x, y, z], frame}` in **SI** — metres per
second and radians per second — in the frame named by `frame`, currently
always `base`.
Sent when the velocity **changes**, and otherwise repeated every
`JOG_KEEPALIVE_S` so the consumer's own expiry never fires mid-motion — a
velocity is a set-point a consumer holds, not a frame in a sequence, and a
consumer that re-plans on every message moves in steps if it is fed one
every tick. A stop is a change: exactly one all-zero Twist, not repeatedly,
because a stream of zeroes keeps a consumer believing a jog is live. There
is no answer to a Twist, ever: it expires on the consumer's own TTL.
Both control sources are normalised to a fraction of full speed before
anything is scaled, so a gamepad and the web console asking for the same
deflection produce byte-identical Twists.
command:
description: >-
**The standard discrete port.** One thing to do, as `{action, value}`.
`action` is robot-agnostic — `gripper_open`, `gripper_close`, `stop`,
`home`, `clear`, `freedrive` — so a consumer that swaps a vacuum for a
two-finger gripper needs no change here. `value` carries the argument
where a verb has one (`freedrive` uses 1.0 and 0.0) and is null otherwise.
jog:
description: >-
**Deprecated; use `twist`.** The same motion in `dora-ulite6`'s units:
`{velocity: [vx, vy, vz, wx, wy, wz]}` in mm/s and deg/s, base frame.
Published alongside `twist` while that node learns the standard one.
Wire `twist` **or** `jog`, never both — they carry the same intent, and a
consumer reading both would be driven twice.
request:
description: >-
**Deprecated; use `command`.** The same verbs under the service names
`dora-ulite6` uses — `stop`, `grasp`, `release`, `clear`, `freedrive`,
`home` — with `request_id` in the metadata. `move_to` and `get_state` have
no standard equivalent yet and go only here.
ui_state:
description: >-
The console's telemetry snapshot, as a JSON string. Also carries error
toasts. Every key in it is read by name in the console's page, so the two
change together.
The browsers get this straight from the node's own server and do not read
this port; it is published for anyone else watching — `dora topic echo`,
a recorder, a second screen — and costs nothing when nobody is.
# Reserved, not implemented. A joint-space robot — a lerobot SO-101, say — takes
# joint targets rather than a Cartesian velocity, and no amount of scaling turns
# one into the other: it needs inverse kinematics, which is another node's job.
# When such a consumer exists, this is the port it wires, mirroring ROS's
# control_msgs/JointJog beside geometry_msgs/Twist:
#
# joint_jog:
# description: >-
# A joint-space velocity, as `{names: [str], velocities: [float]}`, the
# joints named rather than positional so a consumer cannot mistake the
# order.
#
# It is written down rather than built because a port nothing wires is worse
# than no port: it reads as supported and has never once run.
env:
SPEED_MM_S:
type: float
default: 60.0
description: >-
Translation speed at full stick deflection, in mm/s. A stick is analog, so
this is a maximum and anything less moves proportionally slower. Defaults
to 60.0.
SPEED_DEG_S:
type: float
default: 30.0
description: Rotation speed at full deflection, in deg/s. Defaults to 30.0.
DEADZONE:
type: float
default: 0.12
description: >-
Stick travel near centre to ignore, as a fraction. A worn stick that does
not centre would otherwise drive the arm on its own. What is left is
rescaled, so motion still starts from zero just outside the band.
Defaults to 0.12.
EXPO:
type: float
default: 0.4
description: >-
How much of the response curve is cubic, from 0 (linear) to 1. Fine
positioning happens near centre and a linear stick gives it almost no
resolution there. Defaults to 0.4.
JOG_TTL_S:
type: float
default: 0.4
description: >-
How long the owning source may go quiet before the arm is stopped. Keep it
**below** the arm node's VELOCITY_COMMAND_TTL_S (0.5 s by default), which
is the controller-side backstop behind this one. Defaults to 0.4.
JOG_KEEPALIVE_S:
type: float
default: 0.2
description: >-
How often to repeat a velocity that has not changed. Must be **above 0 and
below JOG_TTL_S**, which in turn sits below the arm's own
VELOCITY_COMMAND_TTL_S — so the repeat outpaces the expiry it exists to
beat. Raising it sends fewer messages and asks the controller to re-plan
less; too high and the arm stops between repeats. A change is never
delayed by it. Defaults to 0.2.
JOY_TTL_S:
type: float
default: 0.2
description: >-
How long the `joy` stream may go quiet before the pad reads as gone and the
arm is stopped. Must be **below** JOG_TTL_S. `dora-joy` publishes on every
tick, so this is really "how many missed readings to tolerate" — at a 20 ms
joy tick, the default is ten. Defaults to 0.2.
GAMEPAD_IDLE_RELEASE_S:
type: float
default: 2.0
description: >-
How long a pad may sit untouched before it hands the arm back, so a
browser can take over without anyone pressing anything. Defaults to 2.0.
STATE_POLL_S:
type: float
default: 0.5
description: >-
How often to call `get_state` for what telemetry does not carry: whether
the tool is engaged and whether freedrive is on. Defaults to 0.5.
WEB_HOST:
type: string
default: "0.0.0.0"
description: >-
The address the built-in web console binds to. Defaults to `0.0.0.0`,
because the operator is rarely sitting at the machine wired to the arm.
Set `127.0.0.1` where they are, or where the network is not yours.
WEB_PORT:
type: int
default: 7000
description: >-
The port the console is served on — the page and the browsers' WebSocket
both, so there is one URL and nothing to keep in step. **Set it to 0 to
open no port at all**, which is what a pad-only dataflow wants. Defaults
to 7000.
example: |
- id: teleop
git: ssh://git@gitea.liria.cl:5001/liria/dora-teleop.git
tag: v0.3.0
build: pip install .
path: dora-teleop
inputs:
tick: dora/timer/millis/50
joy:
source: joy/joy
queue_size: 1
queue_policy: drop_oldest
response: ulite6/response
motion_state: ulite6/motion_state
robot_state: ulite6/robot_state
tcp_pose: ulite6/tcp_pose
joint_pose: ulite6/joint_pose
outputs:
- twist
- command
- jog
- request
- ui_state
env:
SPEED_MM_S: "60"
SPEED_DEG_S: "30"