The browser reached this node through `dora-websocket-transceiver`: a second node, a second port, and a round trip through the dataflow for every button press, to carry a UI only this node can interpret. The node now serves the page and holds the browsers' sockets itself on one port, so the console is one URL and a consumer wires nothing for it. `dora_teleop/server.py` runs aiohttp on a thread of its own and meets the control loop at two non-blocking points: a bounded queue the tick drains, and a snapshot handed back. Both queues are bounded deliberately — a flood is dropped rather than grown, and a slow browser loses stale snapshots rather than the newest — because nothing a browser does may stall the thread that stops the arm. A port already in use stops the node rather than leaving it announcing a console nobody can open. The web deadman moved but did not change: it was a `clients` message from the bridge and is now the open-socket count read each tick. One hop fewer, same meaning. Contract, so 0.2.0 -> 0.3.0: `ui_command` and `clients` are gone. `ui_state` stays published for `dora topic echo` and the bench, with no browser reading it. New WEB_HOST and WEB_PORT (7000; 0 opens no port). The console itself is laid out like the arm's own pendant — two rings quartered into arc sectors you hold, each with its out-of-plane axis as pills above. Directions and signs match xArm Studio on the controller, `Y+` on the left included: two consoles driving one arm that disagree about which way is +Y is a trap, not a preference. Also sends a velocity only when it changes, repeating an unchanged one on JOG_KEEPALIVE_S (0.2 s) purely to beat the consumer's own expiry. Repeating every tick made `dora-ulite6` re-plan the same motion twenty times a second. That turned out not to be the whole cause of the stepping an operator felt — the rest was fixed in dora-ulite6 v0.3.1, pinned here — but sending 18 messages where 83 went before is worth keeping on its own. `dora-joy` moves to v0.1.1: v0.1 is an annotated tag, and dora 1.0.1 records the tag object's id as the commit hash and then rejects its own checkout for sitting on the peeled commit. Neither dataflow could be built before this.
108 lines
4.0 KiB
Python
108 lines
4.0 KiB
Python
"""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"})
|
|
|
|
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()
|