fix the node manifest and make the example dataflow self-contained
validate / validate (pull_request) Successful in 58m46s
validate / test (pull_request) Successful in 58m47s
validate / markdownlint (pull_request) Successful in 58m43s

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:
2026-09-15 16:51:30 -05:00
parent 8a7bfb02e0
commit e9fa4bf36b
6 changed files with 204 additions and 45 deletions
+110
View File
@@ -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()
+42
View File
@@ -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.