feat: harden seat intent and expand HIL coverage
This commit is contained in:
@@ -76,6 +76,7 @@ class ReactiveFakeBus:
|
||||
absolute_failure_targets: dict[int, int] | None = None,
|
||||
timed_failure_result: int | None = None,
|
||||
timed_failure_event: int = 4,
|
||||
timed_endpoint: str | None = None,
|
||||
):
|
||||
self.matrix = matrix
|
||||
self.complete_motion = complete_motion
|
||||
@@ -86,6 +87,7 @@ class ReactiveFakeBus:
|
||||
self.absolute_failure_targets = dict(absolute_failure_targets or {})
|
||||
self.timed_failure_result = timed_failure_result
|
||||
self.timed_failure_event = timed_failure_event
|
||||
self.timed_endpoint = timed_endpoint
|
||||
self.hall = [0, 0, 0, 0]
|
||||
self.position = [0, 0, 0, 0]
|
||||
self.max_hall = [480, 374, 917, 647]
|
||||
@@ -268,12 +270,70 @@ class ReactiveFakeBus:
|
||||
},
|
||||
)
|
||||
)
|
||||
# The real controller publishes cyclic status. Keep one more
|
||||
# sample queued for the executor's post-home preflight read.
|
||||
# The real controller publishes all four cyclic motor status
|
||||
# samples after a home completes. Keep a complete cycle queued
|
||||
# for the next axis preflight in a four-axis zero request.
|
||||
for cyclic_motor in range(4):
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": cyclic_motor,
|
||||
"Task_State": 0,
|
||||
"Result": 0,
|
||||
"Position_Valid": 1,
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": self.position[cyclic_motor],
|
||||
"Duty_Percent": 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
for cyclic_motor in range(4):
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Diagnostic_303",
|
||||
{
|
||||
"Motor_Sub_ID": cyclic_motor,
|
||||
"Hall_Count": self.hall[cyclic_motor],
|
||||
"Max_Hall_Count": self.max_hall[cyclic_motor],
|
||||
"Hall_Speed_Half_Pps": 180,
|
||||
},
|
||||
)
|
||||
)
|
||||
if opcode == 3 and self.timed_endpoint is not None:
|
||||
motor = command["Motor_Selector"]
|
||||
at_far_endpoint = self.timed_endpoint == "far"
|
||||
self.hall[motor] = self.max_hall[motor] if at_far_endpoint else 0
|
||||
self.position[motor] = 10_000 if at_far_endpoint else 0
|
||||
self.received.append(
|
||||
self._frame("T2G_Seat_Move_Motor_Status_302", home_status)
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Task_State": 7,
|
||||
"Result": 4,
|
||||
"Direction": command["Direction"],
|
||||
"Position_Valid": 1,
|
||||
"Near_Endpoint": int(not at_far_endpoint),
|
||||
"Far_Endpoint": int(at_far_endpoint),
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": self.position[motor],
|
||||
"Duty_Percent": 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
if opcode == 3 and self.timed_failure_result is not None:
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Diagnostic_303",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Last_Motor_Event": 3 if at_far_endpoint else 2,
|
||||
"Hall_Count": self.hall[motor],
|
||||
"Max_Hall_Count": self.max_hall[motor],
|
||||
},
|
||||
)
|
||||
)
|
||||
elif opcode == 3 and self.timed_failure_result is not None:
|
||||
motor = command["Motor_Selector"]
|
||||
self.received.append(
|
||||
self._frame(
|
||||
@@ -387,7 +447,7 @@ class ProtocolSignatureFakeBus:
|
||||
self.received: deque[CanFrame] = deque()
|
||||
self.closed = False
|
||||
if signature:
|
||||
self.received.append(CanFrame(0x301, bytes.fromhex("10 00 00 00 00 00 F0 30")))
|
||||
self.received.append(CanFrame(0x301, bytes.fromhex("10 00 00 00 00 00 F0 60")))
|
||||
for motor in range(4):
|
||||
self.received.append(
|
||||
CanFrame(0x302, bytes([motor, 0, 0x08, 0, 0, 0, 0, 0]))
|
||||
@@ -480,7 +540,7 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
self.assertEqual(command["signals"]["Param_B"], 30_000)
|
||||
self.assertEqual(command["completion"]["timeout_ms"], 33_000)
|
||||
|
||||
def test_compiler_maps_small_relative_backrest_request_to_timed_duty(self) -> None:
|
||||
def test_compiler_prefers_absolute_relative_target_with_timed_fallback(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
@@ -498,11 +558,16 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
|
||||
command = self.compiler.compile(config, {})["spec"]["commands"][0]
|
||||
|
||||
self.assertEqual(command["signals"]["Opcode"], 3)
|
||||
self.assertEqual(command["signals"]["Direction"], 1)
|
||||
self.assertEqual(command["signals"]["Param_A"], 1320)
|
||||
self.assertEqual(command["signals"]["Param_B"], 900)
|
||||
self.assertEqual(command["completion"]["signal"], "Task_State")
|
||||
self.assertEqual(command["signals"]["Opcode"], 1)
|
||||
self.assertEqual(command["signals"]["Direction"], 0)
|
||||
self.assertEqual(command["signals"]["Param_A"], 3100)
|
||||
self.assertEqual(command["completion"]["signal"], "Value_X100")
|
||||
fallback = command["relative_fallback"]
|
||||
self.assertEqual(fallback["signals"]["Opcode"], 3)
|
||||
self.assertEqual(fallback["signals"]["Direction"], 1)
|
||||
self.assertEqual(fallback["signals"]["Param_A"], 1320)
|
||||
self.assertEqual(fallback["signals"]["Param_B"], 900)
|
||||
self.assertEqual(fallback["completion"]["signal"], "Task_State")
|
||||
|
||||
def test_compiler_maps_large_relative_backrest_request_to_absolute_target(self) -> None:
|
||||
config = function_config(
|
||||
@@ -527,6 +592,34 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
self.assertEqual(command["signals"]["Param_A"], 6400)
|
||||
self.assertEqual(command["completion"]["signal"], "Value_X100")
|
||||
|
||||
def test_compiler_emits_bounded_user_timed_motion(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 100, "angle_deg": 163},
|
||||
motion={
|
||||
"mode": "timed",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 0,
|
||||
"delta_position_pct": 100,
|
||||
"requested_duration_ms": 70_000,
|
||||
"applied_duration_ms": 30_000,
|
||||
"max_duration_ms": 30_000,
|
||||
"min_duration_ms": 300,
|
||||
"duration_clamped": True,
|
||||
"endpoint_expected": "far",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
command = self.compiler.compile(config, {})["spec"]["commands"][0]
|
||||
|
||||
self.assertEqual(command["signals"]["Opcode"], 3)
|
||||
self.assertEqual(command["signals"]["Direction"], 1)
|
||||
self.assertEqual(command["signals"]["Param_A"], 30_000)
|
||||
self.assertEqual(command["signals"]["Param_B"], 900)
|
||||
self.assertEqual(command["completion"]["timeout_ms"], 35_000)
|
||||
|
||||
def test_driver_target_is_rejected_by_hardware_compiler(self) -> None:
|
||||
driver = function_config(
|
||||
operation(
|
||||
@@ -603,6 +696,87 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 1])
|
||||
self.assertTrue(bus.closed)
|
||||
|
||||
def test_firmware_replacement_forces_four_axis_position_memory_sync(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_backrest_recline", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
bus.hall = [220, 180, 600, 310]
|
||||
bus.position = [4500, 4800, 6500, 4700]
|
||||
position_sync_state = {"synchronized": True}
|
||||
|
||||
class ReprogrammedGuard:
|
||||
def prepare_bus(self) -> tuple[ReactiveFakeBus, dict[str, object]]:
|
||||
return bus, {
|
||||
"firmware_verified": True,
|
||||
"programmed_this_request": True,
|
||||
}
|
||||
|
||||
with patch("seat_domain.hardware_executor.time.sleep"):
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix,
|
||||
lambda: bus,
|
||||
ack_timeout_ms=20,
|
||||
motor_max_hall={0: 480, 1: 374, 2: 917, 3: 647},
|
||||
firmware_guard=ReprogrammedGuard(), # type: ignore[arg-type]
|
||||
synchronize_position_before_first_motion=True,
|
||||
position_sync_state=position_sync_state,
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
sent_commands = [
|
||||
self.matrix.decode("T2G_Seat_Move_Command_300", frame.data)
|
||||
for frame in bus.sent
|
||||
if frame.arbitration_id == 0x300
|
||||
]
|
||||
self.assertEqual(
|
||||
[(item["Opcode"], item["Motor_Selector"]) for item in sent_commands],
|
||||
[(9, 0), (0x0B, 0), (0x0B, 1), (0x0B, 2), (0x0B, 3), (1, 2)],
|
||||
)
|
||||
sync = next(
|
||||
item
|
||||
for item in report["spec"]["results"]
|
||||
if item["ref"] == "seat-position-memory-sync"
|
||||
)
|
||||
self.assertEqual(
|
||||
sync["telemetry"]["trigger"], "repository_firmware_reprogrammed"
|
||||
)
|
||||
self.assertEqual(len(sync["telemetry"]["axes"]), 4)
|
||||
self.assertTrue(position_sync_state["synchronized"])
|
||||
|
||||
def test_first_process_motion_syncs_once_and_reuses_the_reference(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_longitudinal_position", {"position_pct": 40})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
state = {"synchronized": False}
|
||||
first_bus = ReactiveFakeBus(self.matrix)
|
||||
with patch("seat_domain.hardware_executor.time.sleep"):
|
||||
first = HardwareSeatExecutor(
|
||||
self.matrix,
|
||||
lambda: first_bus,
|
||||
ack_timeout_ms=20,
|
||||
motor_max_hall={0: 480, 1: 374, 2: 917, 3: 647},
|
||||
synchronize_position_before_first_motion=True,
|
||||
position_sync_state=state,
|
||||
).execute(plan, config)
|
||||
|
||||
second_bus = ReactiveFakeBus(self.matrix)
|
||||
second = HardwareSeatExecutor(
|
||||
self.matrix,
|
||||
lambda: second_bus,
|
||||
ack_timeout_ms=20,
|
||||
motor_max_hall={0: 480, 1: 374, 2: 917, 3: 647},
|
||||
synchronize_position_before_first_motion=True,
|
||||
position_sync_state=state,
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(first["spec"]["status"], "succeeded")
|
||||
self.assertEqual(second["spec"]["status"], "succeeded")
|
||||
self.assertEqual([item.data[0] for item in first_bus.sent], [9, 0x0B, 0x0B, 0x0B, 0x0B, 1])
|
||||
self.assertEqual([item.data[0] for item in second_bus.sent], [9, 1])
|
||||
|
||||
def test_passive_snapshot_reads_all_passenger_axes_and_thermal_state(self) -> None:
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
for motor, value_x100 in enumerate((1200, 3400, 5600, 7800)):
|
||||
@@ -693,12 +867,15 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
command["signals"]["Param_A"] == 0
|
||||
command["signals"]["Opcode"] == 0x0B
|
||||
and command["signals"]["Param_A"] == 0x4B52
|
||||
and command["signals"]["Param_B"] == 120
|
||||
for command in plan["spec"]["commands"]
|
||||
)
|
||||
)
|
||||
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
position_sync_state = {"synchronized": False}
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix,
|
||||
lambda: bus,
|
||||
@@ -706,6 +883,8 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
stop_all_motor_selector=int(
|
||||
self.compiler.protocol["stop_all_motor_selector"]
|
||||
),
|
||||
synchronize_position_before_first_motion=True,
|
||||
position_sync_state=position_sync_state,
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
@@ -716,7 +895,15 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(
|
||||
[(item["Opcode"], item["Motor_Selector"]) for item in sent_commands],
|
||||
[(9, 0), (1, 0), (1, 1), (1, 2), (1, 3)],
|
||||
[(9, 0), (0x0B, 0), (0x0B, 1), (0x0B, 2), (0x0B, 3)],
|
||||
)
|
||||
self.assertTrue(position_sync_state["synchronized"])
|
||||
self.assertEqual(
|
||||
sum(
|
||||
item["ref"] == "seat-position-memory-sync"
|
||||
for item in report["spec"]["results"]
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_zero_recovers_verified_stale_near_endpoint_offset(self) -> None:
|
||||
@@ -741,15 +928,12 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
for frame in bus.sent
|
||||
if frame.arbitration_id == 0x300
|
||||
]
|
||||
self.assertEqual(
|
||||
[item["Opcode"] for item in sent_commands],
|
||||
[9, 1, 3, 0x0B],
|
||||
)
|
||||
self.assertEqual([item["Opcode"] for item in sent_commands], [9, 0x0B])
|
||||
telemetry = report["spec"]["results"][0]["telemetry"]
|
||||
recovery = telemetry["position_recovery"]
|
||||
self.assertEqual(recovery["mode"], "verified_endpoint_drift_rehome")
|
||||
self.assertGreater(recovery["away_probe"]["hall_delta"], 2)
|
||||
self.assertEqual(recovery["home"]["hall_after"], 0)
|
||||
self.assertEqual(recovery["mode"], "explicit_known_range_home")
|
||||
self.assertEqual(recovery["hall_before"], 6)
|
||||
self.assertEqual(recovery["hall_after"], 0)
|
||||
self.assertEqual(telemetry["effective_position_pct"], 0)
|
||||
|
||||
def test_zero_recovers_after_full_travel_reaches_near_endpoint_window(self) -> None:
|
||||
@@ -778,14 +962,10 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
for frame in bus.sent
|
||||
if frame.arbitration_id == 0x300
|
||||
]
|
||||
self.assertEqual(
|
||||
[item["Opcode"] for item in sent_commands],
|
||||
[9, 1, 3, 0x0B],
|
||||
)
|
||||
self.assertEqual([item["Opcode"] for item in sent_commands], [9, 0x0B])
|
||||
recovery = report["spec"]["results"][0]["telemetry"]["position_recovery"]
|
||||
self.assertEqual(recovery["failed_absolute_hall_before"], 374)
|
||||
self.assertEqual(recovery["failed_absolute_hall_after"], 4)
|
||||
self.assertEqual(recovery["home"]["hall_after"], 0)
|
||||
self.assertEqual(recovery["hall_before"], 374)
|
||||
self.assertEqual(recovery["hall_after"], 0)
|
||||
|
||||
def test_zero_recovers_real_m0_stall_at_calibrated_near_endpoint(self) -> None:
|
||||
config = function_config(
|
||||
@@ -813,22 +993,16 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
for frame in bus.sent
|
||||
if frame.arbitration_id == 0x300
|
||||
]
|
||||
self.assertEqual(
|
||||
[item["Opcode"] for item in sent_commands],
|
||||
[9, 1, 3, 0x0B],
|
||||
)
|
||||
self.assertEqual([item["Opcode"] for item in sent_commands], [9, 0x0B])
|
||||
recovery = report["spec"]["results"][0]["telemetry"]["position_recovery"]
|
||||
self.assertEqual(recovery["failed_absolute_hall_before"], 480)
|
||||
self.assertEqual(recovery["failed_absolute_hall_after"], 52)
|
||||
self.assertEqual(recovery["endpoint_recovery_max_near_hall"], 72)
|
||||
self.assertGreater(recovery["away_probe"]["hall_delta"], 2)
|
||||
self.assertEqual(recovery["home"]["hall_after"], 0)
|
||||
self.assertEqual(recovery["hall_before"], 480)
|
||||
self.assertEqual(recovery["hall_after"], 0)
|
||||
self.assertEqual(
|
||||
report["spec"]["results"][0]["telemetry"]["effective_position_pct"],
|
||||
0,
|
||||
)
|
||||
|
||||
def test_zero_does_not_rehome_a_midrange_stall_as_an_endpoint(self) -> None:
|
||||
def test_explicit_zero_homes_even_when_the_stored_position_is_midrange(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_longitudinal_position", {"position_pct": 0})
|
||||
)
|
||||
@@ -844,20 +1018,16 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
motor_max_hall={0: 480, 1: 374, 2: 917, 3: 647},
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
sent_commands = [
|
||||
self.matrix.decode("T2G_Seat_Move_Command_300", frame.data)
|
||||
for frame in bus.sent
|
||||
if frame.arbitration_id == 0x300
|
||||
]
|
||||
self.assertEqual([item["Opcode"] for item in sent_commands], [9, 1, 9])
|
||||
self.assertIn("STALL", report["spec"]["results"][-1]["message"])
|
||||
current = report["spec"]["results"][-1]["telemetry"][
|
||||
"current_diagnostic"
|
||||
]
|
||||
self.assertEqual(current["sample_count"], 1)
|
||||
self.assertEqual(current["max_current_delta_raw"], 700)
|
||||
self.assertTrue(current["stall_overlap_seen"])
|
||||
self.assertEqual([item["Opcode"] for item in sent_commands], [9, 0x0B])
|
||||
recovery = report["spec"]["results"][0]["telemetry"]["position_recovery"]
|
||||
self.assertEqual(recovery["hall_before"], 116)
|
||||
self.assertEqual(recovery["hall_after"], 0)
|
||||
|
||||
def test_zero_recovers_near_endpoint_timeout_after_full_travel(self) -> None:
|
||||
config = function_config(
|
||||
@@ -881,9 +1051,8 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
recovery = report["spec"]["results"][0]["telemetry"]["position_recovery"]
|
||||
self.assertEqual(recovery["failed_absolute_hall_before"], 605)
|
||||
self.assertEqual(recovery["failed_absolute_hall_after"], 3)
|
||||
self.assertEqual(recovery["home"]["hall_after"], 0)
|
||||
self.assertEqual(recovery["hall_before"], 605)
|
||||
self.assertEqual(recovery["hall_after"], 0)
|
||||
|
||||
def test_far_endpoint_timeout_within_position_tolerance_is_accepted(self) -> None:
|
||||
config = function_config(
|
||||
@@ -1036,6 +1205,114 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
self.assertGreater(telemetry["hall_delta"], 0)
|
||||
self.assertTrue(telemetry["hall_direction_verified"])
|
||||
|
||||
def test_executor_accepts_user_timed_motion_that_reaches_endpoint_early(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 100, "angle_deg": 163},
|
||||
motion={
|
||||
"mode": "timed",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 0,
|
||||
"delta_position_pct": 100,
|
||||
"requested_duration_ms": 70_000,
|
||||
"applied_duration_ms": 30_000,
|
||||
"max_duration_ms": 30_000,
|
||||
"min_duration_ms": 300,
|
||||
"duration_clamped": True,
|
||||
"endpoint_expected": "far",
|
||||
},
|
||||
)
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, timed_endpoint="far")
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix,
|
||||
lambda: bus,
|
||||
ack_timeout_ms=20,
|
||||
axis_maximum_x100={"seat.set_backrest_recline": 10_000},
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 3])
|
||||
telemetry = report["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["motion_mode"], "timed_user_request")
|
||||
self.assertEqual(telemetry["endpoint_reached"], "far")
|
||||
self.assertTrue(telemetry["endpoint_terminal_acceptance"])
|
||||
self.assertEqual(telemetry["effective_position_pct"], 100)
|
||||
self.assertIn("最大允许时长为30秒", telemetry["user_notice"])
|
||||
self.assertIn("到达最后方", telemetry["user_notice"])
|
||||
|
||||
def test_executor_does_not_hide_non_endpoint_user_timed_stall(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 100, "angle_deg": 163},
|
||||
motion={
|
||||
"mode": "timed",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 0,
|
||||
"delta_position_pct": 100,
|
||||
"requested_duration_ms": 2_000,
|
||||
"applied_duration_ms": 2_000,
|
||||
"max_duration_ms": 30_000,
|
||||
"min_duration_ms": 300,
|
||||
"duration_clamped": False,
|
||||
},
|
||||
)
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(
|
||||
self.matrix,
|
||||
timed_failure_result=5,
|
||||
timed_failure_event=4,
|
||||
)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix,
|
||||
lambda: bus,
|
||||
ack_timeout_ms=20,
|
||||
axis_maximum_x100={"seat.set_backrest_recline": 10_000},
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual(
|
||||
report["spec"]["results"][-1]["telemetry"]["failure_category"],
|
||||
"stall_no_hall_activity",
|
||||
)
|
||||
|
||||
def test_executor_uses_closed_loop_target_for_calibrated_relative_motion(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 31, "angle_deg": 109},
|
||||
motion={
|
||||
"mode": "relative",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 20,
|
||||
"delta_position_pct": 11,
|
||||
"start_angle_deg": 104,
|
||||
"delta_angle_deg": 5,
|
||||
},
|
||||
)
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, position_valid=True)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix,
|
||||
lambda: bus,
|
||||
ack_timeout_ms=20,
|
||||
axis_maximum_x100={"seat.set_backrest_recline": 10_000},
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 1])
|
||||
telemetry = report["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["motion_mode"], "relative_absolute_target")
|
||||
self.assertEqual(telemetry["effective_position_pct"], 31)
|
||||
|
||||
def test_executor_reports_stall_with_post_failure_hall_diagnostics(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
@@ -1209,6 +1486,7 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
automatic = SeatHardwareSettings.from_env(ROOT)
|
||||
self.assertTrue(automatic.enabled)
|
||||
self.assertTrue(automatic.auto_flash_enabled)
|
||||
self.assertTrue(automatic.position_sync_enabled)
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
@@ -1225,6 +1503,7 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
dry_run = SeatHardwareSettings.from_env(ROOT)
|
||||
self.assertFalse(dry_run.enabled)
|
||||
self.assertFalse(dry_run.auto_flash_enabled)
|
||||
self.assertFalse(dry_run.position_sync_enabled)
|
||||
|
||||
def test_settings_allow_explicit_local_debug_mode_without_token(self) -> None:
|
||||
environment = {
|
||||
@@ -1280,7 +1559,7 @@ class T2GHardwareTests(unittest.TestCase):
|
||||
|
||||
self.assertIs(prepared, bus)
|
||||
self.assertFalse(telemetry["programmed_this_request"])
|
||||
self.assertEqual(telemetry["firmware_revision"], 3)
|
||||
self.assertEqual(telemetry["firmware_revision"], 6)
|
||||
self.assertEqual(bus.sent, [])
|
||||
self.assertEqual(telemetry["max_hall_counts"], [480, 374, 917, 647])
|
||||
self.assertFalse(bus.closed)
|
||||
|
||||
Reference in New Issue
Block a user