fix gamepad layout and refuse a pad whose axes disagree
An Xbox One S pad over Bluetooth numbers its controls differently from the indices this node shipped with: the right stick is on axes 2 and 3, the triggers on 4 and 5, and the buttons carry gaps at 2 and 5. Against the old layout an untouched pad reported a resting trigger as full right-stick deflection, which asked for 30 deg/s of pitch while lying on the desk, held the lease for ever so GAMEPAD_IDLE_RELEASE_S never fired, and moved a real arm into a controller error. X toggled freedrive and LB was the E-STOP, so the resulting latch could not be released from the pad at all. Layout now carries the swept indices. Each pad is also measured the first time it is seen at rest: a stick must rest near centre and a trigger near the bottom of its travel, and a pad that does not fit reads as absent and is named in the log rather than driving an arm from a numbering nobody checked. The measured rest becomes each axis's zero, so a worn stick's tilt is subtracted rather than deadzoned away and a trigger that reads 0 until it first moves stops reading as half pressed. Rest is never re-measured while the pad is in use, because doing that under a held stick would record the deflection as centre and the arm would move when the operator let go. The probe drives from the same calibration, so what it prints is still what the arm would get, and it shows the mismatch. Tests name their controls through axes_for and buttons_for instead of numbering them; the positional lists are why a wrong index passed a green suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+24
-17
@@ -6,7 +6,7 @@ from dora_teleop import main as loop
|
||||
from dora_teleop.arbiter import GAMEPAD, WEB
|
||||
from dora_teleop.config import Config
|
||||
from dora_teleop.joy import Sample
|
||||
from tests.conftest import RecordingNode, input_event, joy_message, stick
|
||||
from tests.conftest import RecordingNode, buttons_for, input_event, joy_message, stick
|
||||
|
||||
STOP = [0.0] * 6
|
||||
PUSHED = [0.0, -1.0]
|
||||
@@ -47,7 +47,14 @@ def ticks(node, state, count=1):
|
||||
|
||||
|
||||
def drive(node, state, sample, count=1):
|
||||
"""Hold one reading for `count` ticks, the way a real pad and joy node would."""
|
||||
"""Hold one reading for `count` ticks, the way a real pad and joy node would.
|
||||
|
||||
A pad this node has not yet seen at rest is sent one resting reading first,
|
||||
because a real one is always at rest before a thumb reaches it — and the
|
||||
node refuses to drive an arm from a pad it has never seen still.
|
||||
"""
|
||||
if not state.joy.calibration.known:
|
||||
send_joy(node, state, stick())
|
||||
for _ in range(count):
|
||||
send_joy(node, state, sample)
|
||||
loop._tick(node, state)
|
||||
@@ -271,7 +278,7 @@ def test_a_latched_estop_keeps_the_arm_still_under_a_pushed_stick(clock):
|
||||
drive(node, state, stick(axes=PUSHED))
|
||||
node.clear()
|
||||
|
||||
drive(node, state, stick(axes=PUSHED, buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(axes=PUSHED, buttons=buttons_for(estop=True)))
|
||||
node.clear()
|
||||
jogs = drive(node, state, stick(axes=PUSHED), count=4)
|
||||
|
||||
@@ -282,11 +289,11 @@ def test_the_pad_can_release_the_latch_it_set(clock):
|
||||
# A pad-only dataflow has no console RESET, so a latch nothing on the pad
|
||||
# can release would strand the arm until the process was restarted.
|
||||
node, state = build()
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
assert state.view.estop is True
|
||||
node.clear()
|
||||
|
||||
drive(node, state, stick(buttons=[False, False, True]))
|
||||
drive(node, state, stick(buttons=buttons_for(clear=True)))
|
||||
|
||||
assert state.view.estop is False
|
||||
assert "clear" in node.services()
|
||||
@@ -296,20 +303,20 @@ def test_the_latch_is_released_without_holding_the_arm(clock):
|
||||
# An idle pad hands the lease back two seconds after the stop it just made,
|
||||
# so gating the release on the lease would be a trap.
|
||||
node, state = build()
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
state.arbiter.release(GAMEPAD)
|
||||
state.arbiter.take(WEB, clock.t)
|
||||
node.clear()
|
||||
|
||||
drive(node, state, stick(buttons=[False, False, True]))
|
||||
drive(node, state, stick(buttons=buttons_for(clear=True)))
|
||||
|
||||
assert state.view.estop is False
|
||||
|
||||
|
||||
def test_the_arm_drives_again_once_the_latch_is_released(clock):
|
||||
node, state = build()
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=[False, False, True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
drive(node, state, stick(buttons=buttons_for(clear=True)))
|
||||
node.clear()
|
||||
|
||||
jogs = drive(node, state, stick(axes=PUSHED))
|
||||
@@ -322,7 +329,7 @@ def test_a_latched_estop_stops_the_browser_too(clock):
|
||||
# through a stop somebody made on the gamepad.
|
||||
node, state = build()
|
||||
state.clients = 1
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
state.arbiter.release(GAMEPAD)
|
||||
state.arbiter.take(WEB, clock.t)
|
||||
node.clear()
|
||||
@@ -337,7 +344,7 @@ def test_the_estop_button_works_without_the_pad_holding_the_arm(clock):
|
||||
node, state = build()
|
||||
state.arbiter.take(WEB, clock.t)
|
||||
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
|
||||
assert "stop" in node.services()
|
||||
|
||||
@@ -402,7 +409,7 @@ def test_a_button_asks_in_verbs_as_well_as_in_services(clock):
|
||||
# what dora-ulite6 reads until it does.
|
||||
node, state = build()
|
||||
|
||||
drive(node, state, stick(buttons=[True]))
|
||||
drive(node, state, stick(buttons=buttons_for(grasp=True)))
|
||||
|
||||
assert node.actions() == ["gripper_close"]
|
||||
assert "grasp" in node.services()
|
||||
@@ -410,10 +417,10 @@ def test_a_button_asks_in_verbs_as_well_as_in_services(clock):
|
||||
|
||||
def test_every_verb_the_pad_can_ask_for_is_robot_agnostic(clock):
|
||||
node, state = build()
|
||||
drive(node, state, stick(buttons=[False, True]))
|
||||
drive(node, state, stick(buttons=[False] * 3 + [True]))
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=[False, False, True]))
|
||||
drive(node, state, stick(buttons=buttons_for(release=True)))
|
||||
drive(node, state, stick(buttons=buttons_for(freedrive=True)))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
drive(node, state, stick(buttons=buttons_for(clear=True)))
|
||||
|
||||
assert node.actions() == ["gripper_open", "freedrive", "stop", "clear"]
|
||||
|
||||
@@ -426,6 +433,6 @@ def test_the_first_press_after_the_pad_has_been_idle_is_not_dropped(clock):
|
||||
node, state = build()
|
||||
assert state.arbiter.owner is None
|
||||
|
||||
drive(node, state, stick(buttons=[True]), count=3)
|
||||
drive(node, state, stick(buttons=buttons_for(grasp=True)), count=3)
|
||||
|
||||
assert node.actions() == ["gripper_close"]
|
||||
|
||||
Reference in New Issue
Block a user