"""The pad as it arrives on the wire, and what silence on that wire means.""" import pyarrow as pa import pytest from dora_teleop import mapping from dora_teleop.joy import ABSENT, JoyInput, Sample, decode from dora_teleop.mapping import LAYOUT from tests.conftest import axes_for, input_event, joy_message, stick def joy_event(sample): return input_event("joy", joy_message(sample)) def test_a_reading_survives_the_round_trip(): sample = decode(joy_event(stick(axes=[0.5, -0.5], buttons=[False, True]))) assert sample.axes[:2] == [0.5, -0.5] assert sample.buttons == [False, True] assert sample.present is True def test_an_absent_pad_decodes_as_absent_not_as_centred(): sample = decode(joy_event(ABSENT)) assert sample.present is False assert sample.axes == [] def test_a_pad_with_fewer_axes_than_the_layout_decodes_without_complaint(): # A three-axis flight stick is a strange thing to drive an arm with, but it # must not take the node down; `mapping.axis` treats a missing one as centred. sample = decode(joy_event(Sample(axes=[0.0, 0.0, 0.0], buttons=[False], present=True))) assert len(sample.axes) == 3 def test_a_message_with_no_rows_reads_as_absent(): empty = input_event("joy", pa.array([], type=joy_message(ABSENT).type)) assert decode(empty) is ABSENT def test_a_fresh_reading_is_handed_back(): joy = JoyInput(ttl_s=0.2) joy.accept(stick(), 100.0) joy.accept(stick(axes=[0.5]), 100.0) assert joy.read(100.1).axes[0] == 0.5 def test_a_reading_nobody_is_refreshing_reads_as_absent(): # dora-joy publishes every tick, so silence means it died — and a dead pad # node is indistinguishable from an unplugged pad as far as the arm cares. joy = JoyInput(ttl_s=0.2) joy.accept(stick(), 100.0) joy.accept(stick(axes=[0.5]), 100.0) assert joy.read(100.5) is ABSENT # --- the pad this node is actually holding --- def test_a_pad_is_measured_where_it_rests_and_the_tilt_is_subtracted(): # A stick that no longer centres is the ordinary kind of worn pad. Its rest # becomes zero, so it asks for nothing until it is actually pushed. joy = JoyInput(ttl_s=0.2) tilted = axes_for(left_x=0.2, left_y=-0.15) joy.accept(stick(axes=tilted), 100.0) reading = joy.read(100.1) assert reading.present is True assert reading.axes[LAYOUT.left_x] == pytest.approx(0.0) assert reading.axes[LAYOUT.left_y] == pytest.approx(0.0) assert mapping.is_active(reading) is False def test_the_tilt_is_subtracted_rather_than_deadzoned_so_full_travel_remains(): joy = JoyInput(ttl_s=0.2) joy.accept(stick(axes=axes_for(left_x=0.2)), 100.0) joy.accept(stick(axes=axes_for(left_x=1.0)), 100.0) assert joy.read(100.1).axes[LAYOUT.left_x] == pytest.approx(0.8) def test_a_trigger_that_rests_at_zero_does_not_read_as_half_pressed(): # Some pads report a trigger as 0 until it first moves. Read as SDL's -1..1 # that is half travel, which would lift the arm on its own. joy = JoyInput(ttl_s=0.2) joy.accept(stick(axes=axes_for(right_trigger=0.0)), 100.0) reading = joy.read(100.1) assert mapping.trigger(reading.axes[LAYOUT.right_trigger]) == pytest.approx(0.0) assert mapping.is_active(reading) is False def test_a_pad_that_does_not_fit_the_layout_reads_as_absent(): # The bug this exists for: a trigger on an index the layout calls a stick # rests at -1.0, which is full deflection — an untouched pad on the desk # asking for full speed, and holding the lease so nothing can take it back. joy = JoyInput(ttl_s=0.2) wrong = list(axes_for()) wrong[LAYOUT.right_y] = -1.0 joy.accept(stick(axes=wrong), 100.0) assert joy.read(100.1) is ABSENT assert "right stick Y" in joy.calibration.mismatch def test_a_pad_held_when_it_appears_is_refused_until_it_is_let_go(): # A stick held at startup and a wrongly numbered axis look identical in one # reading, so the safe answer is the same for both: no arm until it rests. joy = JoyInput(ttl_s=0.2) joy.accept(stick(axes=axes_for(left_y=-1.0)), 100.0) assert joy.read(100.1) is ABSENT joy.accept(stick(), 100.2) assert joy.read(100.3).present is True def test_rest_is_not_re_measured_while_the_pad_is_in_use(): # Re-measuring under a held stick would record that deflection as centre, # and the arm would then move the moment the operator let go. joy = JoyInput(ttl_s=0.2) joy.accept(stick(), 100.0) joy.accept(stick(axes=axes_for(left_y=-1.0)), 100.1) joy.accept(stick(axes=axes_for(left_y=-1.0)), 100.2) assert joy.read(100.3).axes[LAYOUT.left_y] == pytest.approx(-1.0) def test_a_pad_that_goes_away_is_measured_again_when_it_comes_back(): # The second pad is not the first one, and may not rest where it did. joy = JoyInput(ttl_s=0.2) joy.accept(stick(axes=axes_for(left_x=0.2)), 100.0) joy.accept(Sample(present=False), 100.1) joy.accept(stick(axes=axes_for(left_x=-0.2)), 100.2) assert joy.read(100.3).axes[LAYOUT.left_x] == pytest.approx(0.0) def test_nothing_ever_arriving_reads_as_absent(): # A dataflow with no `joy` wired at all, which is a browser-only console. assert JoyInput(ttl_s=0.2).read(100.0) is ABSENT