serve the web console from the node and drop the websocket bridge
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.
This commit is contained in:
+40
-16
@@ -1,8 +1,5 @@
|
||||
"""The deadman: every way a source can go away, and the stop that follows it."""
|
||||
|
||||
import json
|
||||
|
||||
import pyarrow as pa
|
||||
import pytest
|
||||
|
||||
from dora_teleop import main as loop
|
||||
@@ -59,8 +56,7 @@ def drive(node, state, sample, count=1):
|
||||
|
||||
def web_jog(node, state, axes):
|
||||
"""Deliver one console jog message, in the normalised units the page speaks."""
|
||||
payload = json.dumps({"type": "jog", "axes": axes})
|
||||
loop._accept_ui_command(node, state, input_event("ui_command", pa.array([payload])))
|
||||
loop._accept_ui_command(node, state, {"type": "jog", "axes": axes})
|
||||
|
||||
|
||||
# --- the gamepad ---
|
||||
@@ -112,7 +108,7 @@ def test_a_joy_node_that_keeps_publishing_keeps_the_arm_moving(clock):
|
||||
clock.advance(0.05)
|
||||
sent = drive(node, state, stick(axes=PUSHED))
|
||||
|
||||
assert len(sent) == 4
|
||||
assert sent
|
||||
assert all(jog[0] > 0 for jog in sent)
|
||||
|
||||
|
||||
@@ -138,15 +134,45 @@ def test_a_stick_at_rest_sends_one_zero_not_a_stream(clock):
|
||||
assert jogs == [STOP]
|
||||
|
||||
|
||||
def test_a_held_stick_is_resent_every_tick(clock):
|
||||
# The arm's controller expires a velocity on its own; the re-send is what
|
||||
# keeps that backstop from firing mid-motion.
|
||||
def test_a_held_stick_is_not_resent_every_tick(clock):
|
||||
# A velocity is a set-point the arm holds, not a frame in a sequence.
|
||||
# Repeating it every tick made the controller re-plan the same motion twenty
|
||||
# times a second, which an operator feels as the arm stepping rather than
|
||||
# travelling.
|
||||
node, state = build()
|
||||
|
||||
jogs = drive(node, state, stick(axes=PUSHED), count=4)
|
||||
|
||||
assert len(jogs) == 4
|
||||
assert all(jog[0] > 0 for jog in jogs)
|
||||
assert len(jogs) == 1
|
||||
assert jogs[0][0] > 0
|
||||
|
||||
|
||||
def test_a_held_stick_is_repeated_before_the_arm_would_expire_it(clock):
|
||||
# The repeat exists for one reason: the arm's controller drops a velocity on
|
||||
# its own, and the repeat is what keeps that backstop from firing mid-motion.
|
||||
node, state = build()
|
||||
drive(node, state, stick(axes=PUSHED))
|
||||
node.clear()
|
||||
|
||||
clock.advance(state.config.deadman.jog_keepalive_s)
|
||||
jogs = drive(node, state, stick(axes=PUSHED))
|
||||
|
||||
assert len(jogs) == 1
|
||||
assert jogs[0][0] > 0
|
||||
|
||||
|
||||
def test_a_change_of_direction_never_waits_for_the_repeat(clock):
|
||||
# Sending only on change is worth nothing if the change itself is delayed:
|
||||
# a press or a release has to be acted on the tick it happens, not up to a
|
||||
# keepalive later.
|
||||
node, state = build()
|
||||
drive(node, state, stick(axes=PUSHED))
|
||||
node.clear()
|
||||
|
||||
jogs = drive(node, state, stick(axes=[0.0, 1.0]))
|
||||
|
||||
assert len(jogs) == 1
|
||||
assert jogs[0][0] < 0
|
||||
|
||||
|
||||
def test_a_pad_that_was_never_wired_up_leaves_the_arm_alone(clock):
|
||||
@@ -172,7 +198,7 @@ def test_the_last_browser_leaving_stops_the_arm(clock):
|
||||
ticks(node, state)
|
||||
node.clear()
|
||||
|
||||
loop._accept_clients(state, input_event("clients", pa.array([{"connected": 0, "event": "close"}])))
|
||||
loop._set_clients(state, 0)
|
||||
jogs = ticks(node, state)
|
||||
|
||||
assert jogs == [STOP]
|
||||
@@ -231,8 +257,7 @@ def test_estop_is_never_gated_by_the_lease(clock):
|
||||
state.clients = 1
|
||||
state.arbiter.take(GAMEPAD, clock.t)
|
||||
|
||||
payload = json.dumps({"type": "estop"})
|
||||
loop._accept_ui_command(node, state, input_event("ui_command", pa.array([payload])))
|
||||
loop._accept_ui_command(node, state, {"type": "estop"})
|
||||
|
||||
assert node.jogs() == [STOP]
|
||||
assert "stop" in node.services()
|
||||
@@ -321,8 +346,7 @@ def test_a_latched_estop_is_cleared_by_reset(clock):
|
||||
node, state = build()
|
||||
state.view.estop = True
|
||||
|
||||
payload = json.dumps({"type": "reset"})
|
||||
loop._accept_ui_command(node, state, input_event("ui_command", pa.array([payload])))
|
||||
loop._accept_ui_command(node, state, {"type": "reset"})
|
||||
|
||||
assert state.view.estop is False
|
||||
assert "clear" in node.services()
|
||||
|
||||
Reference in New Issue
Block a user