# 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 the two `ui_` ports 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: re-sending the held velocity, 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, buttons: list, 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. Leave it unwired for a browser-only console. The node says so and starts. ui_command: required: false description: >- One message from the web console, as the JSON string the WebSocket bridge publishes on `data_out`. Wire it from the bridge. A message this node does not recognise is dropped rather than refused, so a page from a newer build cannot take the arm down. clients: required: false description: >- Connect and disconnect events from the WebSocket bridge, as `{connected, event}`. **This is the browser's deadman.** Without it a tab closing mid-jog leaves the arm travelling until the arm node's own expiry catches up, because a socket that went away sends nothing to say so. 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`. Re-sent every tick while a source is driving, and followed by exactly one all-zero Twist when it stops — once, 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 and the sender keeps sending. 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 for the WebSocket bridge to broadcast. Also carries error toasts. Every key in it is read by name in the console's page, so the two change together. # 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. 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. example: | - 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 joy: source: joy/joy queue_size: 1 queue_policy: drop_oldest 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: - twist - command - jog - request - ui_state env: SPEED_MM_S: "60" SPEED_DEG_S: "30"