fix the node manifest and make the example dataflow self-contained
Two problems, both found by the validate job once it could finally run. categories named `teleop` and `input`, neither of which dora defines. The set is fixed — sensor, actuator, robot, transform, filter, ml-inference, llm, speech, communication, recorder, visualization, simulator, debug — and an unknown variant makes the whole manifest unparseable, so every type annotation in it was being ignored. dataflow.yml spanned three repositories through sibling paths, which meant dora could not resolve the other nodes' manifests and reported their typed outputs as unannotated. It also could not be validated in CI, which clones one repo. The house pattern is a self-contained dataflow, and this now follows it: teleop plus a bench stand-in for the arm and the browser, both from this checkout. The real three-node wiring moves to README.md, where a consumer would look for it. The bench answers every service, publishes plausible telemetry, and prints the velocities it is sent, so the node can be driven from a real gamepad with no robot in the room. It is not a simulator and must not become one: it never integrates a velocity or moves a pose.
This commit is contained in:
@@ -77,6 +77,11 @@ a source goes away**.
|
|||||||
first time somebody picks up the pad. Errors name the variable.
|
first time somebody picks up the pad. Errors name the variable.
|
||||||
- **`dora_teleop/log.py`** — Timestamped stderr logging. stderr because dora reads a
|
- **`dora_teleop/log.py`** — Timestamped stderr logging. stderr because dora reads a
|
||||||
dynamic node's stdout.
|
dynamic node's stdout.
|
||||||
|
- **`examples/bench.py`** — A stand-in for the arm *and* the browser, 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
|
||||||
|
`dora-ulite6` — anything needing the arm to actually respond needs the arm.
|
||||||
- **`dora_teleop/probe.py`** — `dora-teleop-probe`: shows the raw axes, the buttons and the
|
- **`dora_teleop/probe.py`** — `dora-teleop-probe`: shows the raw axes, the buttons and the
|
||||||
velocity that would be sent, with no dataflow and no arm. It exists because the indices
|
velocity that would be sent, with no dataflow and no arm. It exists because the indices
|
||||||
in `mapping.py` are SDL's documented layout rather than an observed one, and a pad that
|
in `mapping.py` are SDL's documented layout rather than an observed one, and a pad that
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ stops the node at startup rather than the first time somebody picks up the pad.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
See `dataflow.yml` for the whole stack wired together. The short version:
|
The real stack is three nodes: this one, the WebSocket bridge and the arm.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- id: teleop
|
- id: teleop
|
||||||
@@ -172,12 +172,34 @@ See `dataflow.yml` for the whole stack wired together. The short version:
|
|||||||
outputs: [jog, request, ui_state]
|
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
|
```bash
|
||||||
dora run dataflow.yml --uv
|
dora run dataflow.yml --uv
|
||||||
```
|
```
|
||||||
|
|
||||||
**Requirements:** dora 1.0+ (verified with `dora-cli 1.0.1`), Python 3.11+, `uv`, and a
|
The bench answers every service, publishes plausible telemetry, and prints each velocity
|
||||||
gamepad if you want one — the node starts without and says so.
|
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.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+21
-41
@@ -1,12 +1,13 @@
|
|||||||
# The whole operator stack, and what CI validates.
|
# The repo's own example, and what CI validates.
|
||||||
#
|
#
|
||||||
# Built from checkouts rather than tags, so `dora validate` resolves each node's
|
# Self-contained on purpose: teleop plus a bench stand-in for the arm and the
|
||||||
# manifest from its `path:` and checks the wiring against the versions in front
|
# browser, both built from this checkout, so `dora validate` resolves every
|
||||||
# of you. The sibling repos are expected next to this one, which is how they are
|
# manifest from `path:` and CI needs no gamepad, no robot and no sibling clones.
|
||||||
# cloned in practice.
|
|
||||||
#
|
#
|
||||||
# Three nodes, one loop: the bridge carries the browser, this node decides who
|
# This is **not** the real stack. The bench answers services and publishes
|
||||||
# drives, and the arm node moves the arm and reports back.
|
# telemetry but never moves anything — it exists so the node can be driven from
|
||||||
|
# a real gamepad with no arm in the room. README.md carries the three-node
|
||||||
|
# wiring a consumer actually uses.
|
||||||
|
|
||||||
strict_types: true
|
strict_types: true
|
||||||
|
|
||||||
@@ -16,13 +17,11 @@ nodes:
|
|||||||
build: pip install .
|
build: pip install .
|
||||||
inputs:
|
inputs:
|
||||||
tick: dora/timer/millis/50
|
tick: dora/timer/millis/50
|
||||||
ui_command: ws-transceiver/data_out
|
clients: bench/clients
|
||||||
clients: ws-transceiver/clients
|
response: bench/response
|
||||||
response: ulite6/response
|
robot_state: bench/robot_state
|
||||||
motion_state: ulite6/motion_state
|
tcp_pose: bench/tcp_pose
|
||||||
robot_state: ulite6/robot_state
|
joint_pose: bench/joint_pose
|
||||||
tcp_pose: ulite6/tcp_pose
|
|
||||||
joint_pose: ulite6/joint_pose
|
|
||||||
outputs:
|
outputs:
|
||||||
- jog
|
- jog
|
||||||
- request
|
- request
|
||||||
@@ -31,35 +30,16 @@ nodes:
|
|||||||
SPEED_MM_S: "60"
|
SPEED_MM_S: "60"
|
||||||
SPEED_DEG_S: "30"
|
SPEED_DEG_S: "30"
|
||||||
|
|
||||||
- id: ws-transceiver
|
- id: bench
|
||||||
path: ../dora-websocket-transceiver
|
path: examples/bench.py
|
||||||
build: pip install .
|
|
||||||
inputs:
|
inputs:
|
||||||
data_in: teleop/ui_state
|
tick: dora/timer/millis/200
|
||||||
outputs:
|
|
||||||
- data_out
|
|
||||||
- clients
|
|
||||||
env:
|
|
||||||
WS_PORT: "9000"
|
|
||||||
STATIC_DIR: "../dora-teleop/web"
|
|
||||||
HTTP_PORT: "9010"
|
|
||||||
|
|
||||||
- id: ulite6
|
|
||||||
path: ../dora-ulite6
|
|
||||||
build: pip install .
|
|
||||||
inputs:
|
|
||||||
tick: dora/timer/millis/100
|
|
||||||
request: teleop/request
|
|
||||||
jog: teleop/jog
|
jog: teleop/jog
|
||||||
|
request: teleop/request
|
||||||
|
ui_state: teleop/ui_state
|
||||||
outputs:
|
outputs:
|
||||||
- response
|
- response
|
||||||
- motion_state
|
|
||||||
- joint_pose
|
|
||||||
- tcp_pose
|
|
||||||
- robot_state
|
- robot_state
|
||||||
env:
|
- tcp_pose
|
||||||
ROBOT_IP: "192.168.1.192"
|
- joint_pose
|
||||||
TOOL_TYPE: "vacuum"
|
- clients
|
||||||
# Nothing moves because the node started. An operator asks for the first
|
|
||||||
# motion, which is the whole point of a teleop stack.
|
|
||||||
INIT_MODE: "none"
|
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ apiVersion: 1
|
|||||||
name: dora-teleop
|
name: dora-teleop
|
||||||
namespace: liria
|
namespace: liria
|
||||||
description: Hands a robot arm to a human, from a gamepad or a browser
|
description: Hands a robot arm to a human, from a gamepad or a browser
|
||||||
categories: [robot, teleop, input]
|
categories: [robot]
|
||||||
keywords: [teleop, gamepad, joystick, jog, arm, manipulator, web-console, operator]
|
keywords: [teleop, gamepad, joystick, jog, arm, manipulator, web-console, operator]
|
||||||
|
|
||||||
runtime: python
|
runtime: python
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""A bench stand-in for the arm and the browser, so teleop can run with neither.
|
||||||
|
|
||||||
|
Run by `dataflow.yml`. It answers every service, publishes plausible telemetry,
|
||||||
|
and prints the velocities it is sent — which is enough to drive the node from a
|
||||||
|
real gamepad and watch what a real arm would have been told to do.
|
||||||
|
|
||||||
|
**It is not a simulator.** It does not integrate the velocity, move a pose, or
|
||||||
|
model the arm in any way; a jog is printed and forgotten. Its job is to close
|
||||||
|
the loop so the node's own behaviour can be watched, not to stand in for
|
||||||
|
`dora-ulite6`. Anything that needs the arm to actually respond needs the arm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pyarrow as pa
|
||||||
|
from dora import Node
|
||||||
|
|
||||||
|
REQUEST_ID = "request_id"
|
||||||
|
|
||||||
|
# Somewhere in front of the robot, tool pointing down. Fixed, because a moving
|
||||||
|
# pose here would look like the arm was responding, which it is not.
|
||||||
|
POSE = [250.0, 0.0, 200.0, 180.0, 0.0, 0.0]
|
||||||
|
JOINTS = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Answer whatever teleop asks and report what it would have moved."""
|
||||||
|
node = Node("bench")
|
||||||
|
announced = False
|
||||||
|
tool_engaged = False
|
||||||
|
freedrive = False
|
||||||
|
jogs = 0
|
||||||
|
shown = None
|
||||||
|
|
||||||
|
for event in node:
|
||||||
|
if event["type"] != "INPUT":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if event["id"] == "tick":
|
||||||
|
if not announced:
|
||||||
|
announced = True
|
||||||
|
node.send_output("robot_state", pa.array(["ready"]))
|
||||||
|
node.send_output("tcp_pose", pa.array(POSE, type=pa.float32()), {"units_position": "mm"})
|
||||||
|
node.send_output("joint_pose", pa.array(JOINTS, type=pa.float32()), {"units": "deg"})
|
||||||
|
# No browser is attached to this dataflow, and saying so is what
|
||||||
|
# keeps teleop from believing one went away mid-jog.
|
||||||
|
node.send_output("clients", pa.array([{"connected": 0, "event": "close"}]))
|
||||||
|
|
||||||
|
elif event["id"] == "jog":
|
||||||
|
velocity = event["value"][0].as_py()["velocity"]
|
||||||
|
jogs += 1
|
||||||
|
moving = any(velocity)
|
||||||
|
shown = ", ".join(f"{v:+7.2f}" for v in velocity)
|
||||||
|
print(f"jog {jogs:5d} [{shown}] {'' if moving else '<- stop'}", flush=True)
|
||||||
|
|
||||||
|
elif event["id"] == "request":
|
||||||
|
row = event["value"][0].as_py()
|
||||||
|
service = row.get("service", "")
|
||||||
|
request_id = event["metadata"].get(REQUEST_ID, "")
|
||||||
|
if service == "grasp":
|
||||||
|
tool_engaged = True
|
||||||
|
elif service == "release":
|
||||||
|
tool_engaged = False
|
||||||
|
elif service == "freedrive":
|
||||||
|
freedrive = bool(row.get("enabled", True))
|
||||||
|
if service != "get_state":
|
||||||
|
print(f"service {service} ({request_id})", flush=True)
|
||||||
|
node.send_output(
|
||||||
|
"response",
|
||||||
|
pa.array([_answer(service, tool_engaged, freedrive)]),
|
||||||
|
{REQUEST_ID: request_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
elif event["id"] == "ui_state":
|
||||||
|
# No browser is attached, so this is where the console's picture of
|
||||||
|
# the arm would go. Printing it on change rather than on every tick
|
||||||
|
# makes the things worth watching — the lease moving, the arm
|
||||||
|
# starting and stopping — visible without 20 lines a second.
|
||||||
|
snapshot = json.loads(event["value"][0].as_py())
|
||||||
|
if snapshot.get("type") != "telemetry":
|
||||||
|
continue
|
||||||
|
current = (snapshot["arm_state"], snapshot["owner"], snapshot["estop"])
|
||||||
|
if current != shown:
|
||||||
|
shown = current
|
||||||
|
state, owner, estop = current
|
||||||
|
print(
|
||||||
|
f"console would show: {state}, owner {owner}" + (", E-STOPPED" if estop else ""),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _answer(service: str, tool_engaged: bool, freedrive: bool) -> dict:
|
||||||
|
"""Build the response row the arm node would have sent."""
|
||||||
|
if service == "get_state":
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"service": service,
|
||||||
|
"message": "ready",
|
||||||
|
"connected": True,
|
||||||
|
"ready": True,
|
||||||
|
"tool_engaged": tool_engaged,
|
||||||
|
"freedrive": freedrive,
|
||||||
|
"pose": POSE,
|
||||||
|
"joints": JOINTS,
|
||||||
|
}
|
||||||
|
return {"ok": True, "service": service, "message": f"{service} accepted by the bench"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Manifest for the bench stand-in.
|
||||||
|
#
|
||||||
|
# dora resolves a node's manifest from its `path:`, so without this file the
|
||||||
|
# bench would be validated against the teleop node's own contract and every port
|
||||||
|
# would look wrong.
|
||||||
|
|
||||||
|
apiVersion: 1
|
||||||
|
name: bench
|
||||||
|
namespace: liria
|
||||||
|
description: Bench stand-in for the arm and the browser, so teleop can run with neither
|
||||||
|
|
||||||
|
runtime: python
|
||||||
|
entrypoint: bench.py
|
||||||
|
dora: ">=1.0"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
tick:
|
||||||
|
description: Drives the telemetry the arm node would publish.
|
||||||
|
type: std/core/v1/UInt64
|
||||||
|
jog:
|
||||||
|
description: A velocity teleop wants the arm to move at. Printed, then forgotten.
|
||||||
|
request:
|
||||||
|
description: A service call. Every one is answered ok.
|
||||||
|
ui_state:
|
||||||
|
description: >-
|
||||||
|
The console snapshot teleop publishes. Accepted and dropped: no browser is
|
||||||
|
attached to this dataflow.
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
response:
|
||||||
|
description: One answer per request, under the id it arrived with.
|
||||||
|
robot_state:
|
||||||
|
description: Reports `ready` once, so teleop stops drawing the arm as disconnected.
|
||||||
|
type: std/core/v1/String
|
||||||
|
tcp_pose:
|
||||||
|
description: A fixed tool-tip pose. Fixed on purpose — nothing here integrates a velocity.
|
||||||
|
type: std/core/v1/Float32
|
||||||
|
joint_pose:
|
||||||
|
description: Fixed joint angles, for the same reason.
|
||||||
|
type: std/core/v1/Float32
|
||||||
|
clients:
|
||||||
|
description: Always zero connected, since the WebSocket bridge is not in this dataflow.
|
||||||
Reference in New Issue
Block a user