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.
98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
"""Settings: parsed once at startup, and refused there rather than mid-motion."""
|
|
|
|
import pytest
|
|
|
|
from dora_teleop.config import Config
|
|
|
|
|
|
def test_the_defaults_need_no_environment():
|
|
config = Config.from_env()
|
|
|
|
assert config.speed_mm_s == 60.0
|
|
assert config.speed_deg_s == 30.0
|
|
|
|
|
|
def test_the_deadman_is_shorter_than_the_arms_own_expiry():
|
|
# The arm node's VELOCITY_COMMAND_TTL_S defaults to 0.5 s and is the
|
|
# backstop. Ours has to fire first, or the backstop backs up nothing.
|
|
assert Config.from_env().deadman.jog_ttl_s < 0.5
|
|
|
|
|
|
def test_the_layers_fire_innermost_first():
|
|
# A stale pad reading has to be noticed before the jog deadline it feeds.
|
|
deadman = Config.from_env().deadman
|
|
|
|
assert deadman.joy_ttl_s < deadman.jog_ttl_s
|
|
|
|
|
|
def test_a_blank_variable_falls_back_to_the_default(monkeypatch):
|
|
monkeypatch.setenv("SPEED_MM_S", " ")
|
|
|
|
assert Config.from_env().speed_mm_s == 60.0
|
|
|
|
|
|
def test_a_speed_that_is_not_a_number_names_itself(monkeypatch):
|
|
monkeypatch.setenv("SPEED_MM_S", "brisk")
|
|
|
|
with pytest.raises(ValueError, match="SPEED_MM_S"):
|
|
Config.from_env()
|
|
|
|
|
|
def test_a_joy_ttl_at_or_above_the_jog_ttl_is_refused_at_startup(monkeypatch):
|
|
# Layers out of order means a backstop firing before the thing it backs up,
|
|
# which is invisible until the day something actually goes wrong.
|
|
monkeypatch.setenv("JOY_TTL_S", "0.4")
|
|
|
|
with pytest.raises(ValueError, match="JOY_TTL_S"):
|
|
Config.from_env()
|
|
|
|
|
|
def test_a_keepalive_at_or_above_the_jog_ttl_is_refused_at_startup(monkeypatch):
|
|
# The repeat has to outpace the arm's own velocity expiry. It cannot be read
|
|
# from here, but JOG_TTL_S already sits below it, so a repeat slower than
|
|
# JOG_TTL_S is a repeat the arm stops between — an arm that stutters under a
|
|
# held stick, with nothing in the logs to say why.
|
|
monkeypatch.setenv("JOG_KEEPALIVE_S", "0.4")
|
|
|
|
with pytest.raises(ValueError, match="JOG_KEEPALIVE_S"):
|
|
Config.from_env()
|
|
|
|
|
|
def test_a_keepalive_of_zero_is_refused_at_startup(monkeypatch):
|
|
# Zero would mean repeating every tick, which is the behaviour the keepalive
|
|
# exists to replace.
|
|
monkeypatch.setenv("JOG_KEEPALIVE_S", "0")
|
|
|
|
with pytest.raises(ValueError, match="JOG_KEEPALIVE_S"):
|
|
Config.from_env()
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["1.0", "-0.1", "2"])
|
|
def test_a_deadzone_outside_the_stick_is_refused_at_startup(monkeypatch, value):
|
|
# A deadzone of 1 would silently make the pad inert; catching it here beats
|
|
# finding out when somebody picks it up.
|
|
monkeypatch.setenv("DEADZONE", value)
|
|
|
|
with pytest.raises(ValueError, match="DEADZONE"):
|
|
Config.from_env()
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["-0.1", "1.1"])
|
|
def test_an_expo_outside_the_curve_is_refused_at_startup(monkeypatch, value):
|
|
monkeypatch.setenv("EXPO", value)
|
|
|
|
with pytest.raises(ValueError, match="EXPO"):
|
|
Config.from_env()
|
|
|
|
|
|
def test_the_timings_can_be_tuned_without_touching_the_speeds(monkeypatch):
|
|
monkeypatch.setenv("JOG_TTL_S", "0.25")
|
|
monkeypatch.setenv("JOY_TTL_S", "0.1")
|
|
monkeypatch.setenv("GAMEPAD_IDLE_RELEASE_S", "5")
|
|
|
|
config = Config.from_env()
|
|
|
|
assert config.deadman.jog_ttl_s == 0.25
|
|
assert config.deadman.joy_ttl_s == 0.1
|
|
assert config.deadman.gamepad_idle_release_s == 5.0
|