add the console's jog pad, gripper and freedrive controls
The page was a status readout: it sent three of the nine commands the node serves, so the browser could take the lease and stop the arm but not move it. It now holds a direction to move and releases to stop, with pads for translation and rotation, gripper, freedrive, home and a speed slider. It sends a fraction of full deflection rather than a speed, so it never learns the robot's units — the same vector a stick produces. Plain HTML and no build step: the bridge serves the directory as it stands, and a console that cannot be fixed without a toolchain is the wrong thing to have between a person and a robot. Details that matter on a machine that moves: the pointer is captured so sliding off a button still releases it; releasing sends jog_stop rather than relying on the deadman; blur, tab-hide and a closed socket release everything; and the pad is disabled unless the console holds the lease, so a button that would be refused looks refused. test_console.py checks the page against ui.py both ways — every message type it sends is one the node serves, every telemetry key it reads is one the node sends. Neither language catches a rename.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""The page and the node are one contract, checked here rather than by eye.
|
||||
|
||||
`ui.py` translates for a console it cannot import, and the page reads telemetry
|
||||
by key. Nothing in either language catches a rename, so a typo would surface as
|
||||
a dead button on a machine that can move — which is the wrong place to find it.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from dora_teleop import ui
|
||||
|
||||
PAGE = Path(__file__).resolve().parent.parent / "web" / "index.html"
|
||||
|
||||
|
||||
def page():
|
||||
return PAGE.read_text()
|
||||
|
||||
|
||||
def sent_types():
|
||||
"""Return every message type the page can send."""
|
||||
return set(re.findall(r'type:\s*"([a-z_]+)"', page()))
|
||||
|
||||
|
||||
def read_keys():
|
||||
"""Return the telemetry keys the page renders by name."""
|
||||
listed = re.search(r"const KEYS = \[(.*?)\]", page(), re.DOTALL).group(1)
|
||||
return set(re.findall(r'"([a-z_]+)"', listed))
|
||||
|
||||
|
||||
def snapshot_keys():
|
||||
view = ui.ArmView()
|
||||
view.pose = [1.0] * 6
|
||||
return set(ui.snapshot(view, "web", False, 0.0))
|
||||
|
||||
|
||||
def test_every_message_the_page_sends_is_one_the_node_serves():
|
||||
# `data` is the bridge's own envelope, not a console command.
|
||||
for kind in sent_types() - {"data"}:
|
||||
assert ui.command_of({"type": kind}) is not None or kind == "jog", kind
|
||||
|
||||
|
||||
def test_every_telemetry_key_the_page_reads_is_one_the_node_sends():
|
||||
missing = read_keys() - snapshot_keys()
|
||||
|
||||
assert not missing, f"the page reads keys the node does not send: {sorted(missing)}"
|
||||
|
||||
|
||||
def test_the_page_reads_the_pose_units_from_the_node():
|
||||
# Rather than assuming mm, which is only true of one robot.
|
||||
assert "units.position" in page().replace("telemetry.units && telemetry.units.position", "units.position")
|
||||
assert "units" in snapshot_keys()
|
||||
|
||||
|
||||
def test_the_jog_the_page_sends_is_normalised_and_accepted():
|
||||
# Six axes, each a fraction of full deflection. If the page ever sent a
|
||||
# velocity instead, it would have to know the robot's units.
|
||||
command = ui.command_of(json.loads('{"type": "jog", "axes": [0, -0.6, 0, 0, 0, 0]}'))
|
||||
|
||||
assert command is not None
|
||||
assert command.axes == [0.0, -0.6, 0.0, 0.0, 0.0, 0.0]
|
||||
|
||||
|
||||
def test_the_page_can_ask_for_everything_the_node_serves():
|
||||
# `move` is the one command with no button: it is a pose in the robot's own
|
||||
# units, which is exactly what this page is built not to know.
|
||||
served = {"jog", "jog_stop", "estop", "reset", "home", "control", "gripper", "freedrive"}
|
||||
|
||||
assert served <= sent_types()
|
||||
|
||||
|
||||
def test_the_page_gates_its_jog_pad_on_holding_the_lease():
|
||||
# The node refuses a jog from a non-owner and answers with a toast; a pad
|
||||
# that looks live but is refused teaches an operator to distrust the screen.
|
||||
assert 'owner === "web"' in page()
|
||||
|
||||
|
||||
def test_the_published_schemas_do_not_depend_on_the_value_sent():
|
||||
# pyarrow reads a null `value` as its own type, so an inferred schema would
|
||||
# change the type of the `command` port between one verb and the next.
|
||||
from dora_teleop import message
|
||||
|
||||
assert message.pack_command("stop", None).type == message.COMMAND
|
||||
assert message.pack_command("freedrive", 1.0).type == message.COMMAND
|
||||
assert (
|
||||
message.pack_twist({"linear": [0.0] * 3, "angular": [0.0] * 3, "frame": "base"}).type == message.TWIST
|
||||
)
|
||||
assert message.pack_jog([0.0] * 6).type == message.JOG
|
||||
Reference in New Issue
Block a user