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>
264 lines
11 KiB
YAML
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"
|