init
This commit is contained in:
Executable
+835
@@ -0,0 +1,835 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from collections import deque
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
SEAT_SOURCE = ROOT / "domains" / "seat" / "src"
|
||||
if str(SEAT_SOURCE) not in sys.path:
|
||||
sys.path.insert(0, str(SEAT_SOURCE))
|
||||
|
||||
from cognitive_agent.infrastructure.can.gateway import CanFrame # noqa: E402
|
||||
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
|
||||
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
|
||||
HardwareSafetyError,
|
||||
SeatHardwareSettings,
|
||||
)
|
||||
from cognitive_agent.infrastructure.firmware import ( # noqa: E402
|
||||
EXPECTED_IDENTITY_PAYLOAD,
|
||||
FirmwareProgrammingError,
|
||||
T2GFirmwareGuard,
|
||||
T2GSeatProgrammer,
|
||||
)
|
||||
from seat_domain import HardwareSeatExecutor, SeatCanMatrix, T2GSeatCompiler # noqa: E402
|
||||
|
||||
|
||||
def function_config(*operations: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "FunctionConfig",
|
||||
"metadata": {
|
||||
"id": "function-test-hardware",
|
||||
"name": "T2G hardware test",
|
||||
"version": "1.0.0",
|
||||
"request_id": "req-hardware-test",
|
||||
},
|
||||
"spec": {"domain": "seat", "operations": list(operations)},
|
||||
}
|
||||
|
||||
|
||||
def operation(
|
||||
capability: str,
|
||||
parameters: dict[str, object],
|
||||
*,
|
||||
operation_id: str = "op-hardware-test",
|
||||
instance: str = "driver",
|
||||
zone: str = "front_left",
|
||||
motion: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
result = {
|
||||
"id": operation_id,
|
||||
"capability": capability,
|
||||
"target": {"instance": instance, "zone": zone},
|
||||
"parameters": parameters,
|
||||
"timeout_ms": 20,
|
||||
}
|
||||
if motion is not None:
|
||||
result["motion"] = motion
|
||||
return result
|
||||
|
||||
|
||||
class ReactiveFakeBus:
|
||||
def __init__(
|
||||
self,
|
||||
matrix: SeatCanMatrix,
|
||||
*,
|
||||
complete_motion: bool = True,
|
||||
position_valid: bool = True,
|
||||
timed_failure_result: int | None = None,
|
||||
timed_failure_event: int = 4,
|
||||
):
|
||||
self.matrix = matrix
|
||||
self.complete_motion = complete_motion
|
||||
self.position_valid = position_valid
|
||||
self.timed_failure_result = timed_failure_result
|
||||
self.timed_failure_event = timed_failure_event
|
||||
self.hall = [0, 0, 0, 0]
|
||||
self.max_hall = [480, 790, 1590, 960]
|
||||
self.sent: list[CanFrame] = []
|
||||
self.received: deque[CanFrame] = deque()
|
||||
self.closed = False
|
||||
|
||||
def _frame(self, message: str, values: dict[str, int]) -> CanFrame:
|
||||
encoded = self.matrix.encode(message, values)
|
||||
return CanFrame(int(encoded["frame_id"], 16), bytes.fromhex(encoded["data_hex"]))
|
||||
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
|
||||
frame = CanFrame(arbitration_id, bytes(data))
|
||||
self.sent.append(frame)
|
||||
if arbitration_id == 0x300:
|
||||
command = self.matrix.decode("T2G_Seat_Move_Command_300", data)
|
||||
sequence = command["Sequence"]
|
||||
opcode = command["Opcode"]
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_System_Status_301",
|
||||
{
|
||||
"Ack": 1,
|
||||
"Reject_Reason": 0,
|
||||
"Last_Opcode": opcode,
|
||||
"Last_Sequence": sequence,
|
||||
"Safety_Inhibit": 0,
|
||||
"Safety_Fault": 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
if opcode == 9:
|
||||
for motor in range(4):
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Task_State": 0,
|
||||
"Result": 0,
|
||||
"Position_Valid": int(self.position_valid),
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": 0,
|
||||
"Duty_Percent": 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
for motor in range(4):
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Diagnostic_303",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Hall_Count": self.hall[motor],
|
||||
"Max_Hall_Count": self.max_hall[motor],
|
||||
"Max_Speed_Pps_X100": 9000,
|
||||
},
|
||||
)
|
||||
)
|
||||
if opcode == 1 and self.complete_motion:
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": command["Motor_Selector"],
|
||||
"Task_State": 8,
|
||||
"Result": 2,
|
||||
"Position_Valid": 1,
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": command["Param_A"],
|
||||
"Duty_Percent": command["Param_B"],
|
||||
},
|
||||
)
|
||||
)
|
||||
if opcode == 3 and self.timed_failure_result is not None:
|
||||
motor = command["Motor_Selector"]
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Task_State": 9,
|
||||
"Result": self.timed_failure_result,
|
||||
"Direction": command["Direction"],
|
||||
"Position_Valid": int(self.position_valid),
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": 0,
|
||||
"Duty_Percent": command["Param_B"],
|
||||
},
|
||||
)
|
||||
)
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Diagnostic_303",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Last_Motor_Event": self.timed_failure_event,
|
||||
"Hall_Count": self.hall[motor],
|
||||
"Max_Hall_Count": self.max_hall[motor],
|
||||
"Max_Speed_Pps_X100": 9000,
|
||||
},
|
||||
)
|
||||
)
|
||||
elif opcode == 3 and self.complete_motion:
|
||||
motor = command["Motor_Selector"]
|
||||
delta = max(1, command["Param_A"] // 35)
|
||||
if command["Direction"] == 1:
|
||||
self.hall[motor] = min(
|
||||
self.max_hall[motor], self.hall[motor] + delta
|
||||
)
|
||||
else:
|
||||
self.hall[motor] = max(0, self.hall[motor] - delta)
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Motor_Status_302",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Task_State": 8,
|
||||
"Result": 2,
|
||||
"Direction": command["Direction"],
|
||||
"Position_Valid": int(self.position_valid),
|
||||
"Motor_Sequence": sequence,
|
||||
"Value_X100": 0,
|
||||
"Duty_Percent": command["Param_B"],
|
||||
},
|
||||
)
|
||||
)
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Move_Diagnostic_303",
|
||||
{
|
||||
"Motor_Sub_ID": motor,
|
||||
"Hall_Count": self.hall[motor],
|
||||
"Max_Hall_Count": self.max_hall[motor],
|
||||
"Max_Speed_Pps_X100": 9000,
|
||||
},
|
||||
)
|
||||
)
|
||||
elif arbitration_id == 0x29E:
|
||||
control = self.matrix.decode("T2G_Seat_Thermal_Control_29E", data)
|
||||
heating = control["Driver_Seat_Heating_On"]
|
||||
ventilation = control["Driver_Seat_Ventilation_On"]
|
||||
self.received.append(
|
||||
self._frame(
|
||||
"T2G_Seat_Thermal_Status_4FA",
|
||||
{
|
||||
"Driver_Seat_Heating_Status": (
|
||||
min(control["Driver_Seat_Heating_Gear"], 3) + 1
|
||||
if heating == 1
|
||||
else 1
|
||||
),
|
||||
"Driver_Seat_Ventilation_Status": (
|
||||
min(control["Driver_Seat_Ventilation_Gear"], 3) + 1
|
||||
if ventilation == 1
|
||||
else 1
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
return frame
|
||||
|
||||
def receive(self, timeout_ms: int) -> CanFrame | None:
|
||||
del timeout_ms
|
||||
return self.received.popleft() if self.received else None
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class IdentityFakeBus:
|
||||
def __init__(self, identity: bytes | None):
|
||||
self.identity = identity
|
||||
self.sent: list[CanFrame] = []
|
||||
self.closed = False
|
||||
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
|
||||
frame = CanFrame(arbitration_id, bytes(data))
|
||||
self.sent.append(frame)
|
||||
return frame
|
||||
|
||||
def receive(self, timeout_ms: int) -> CanFrame | None:
|
||||
del timeout_ms
|
||||
if self.identity is None:
|
||||
return None
|
||||
identity = self.identity
|
||||
self.identity = None
|
||||
return CanFrame(0x305, identity)
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class FailingIdentityBus(IdentityFakeBus):
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
|
||||
del arbitration_id, data
|
||||
raise RuntimeError("CAN target did not ACK the identity probe")
|
||||
|
||||
|
||||
class FakeProgrammer:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
def program(self) -> dict[str, object]:
|
||||
self.calls += 1
|
||||
return {
|
||||
"backend": "fake",
|
||||
"programmed": True,
|
||||
"image": "Seat_Controller_demo.hex",
|
||||
"image_sha256": "0" * 64,
|
||||
}
|
||||
|
||||
|
||||
class T2GHardwareTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.matrix = SeatCanMatrix(
|
||||
[
|
||||
ROOT / "domains" / "seat" / "can" / "seat-thermal-hardware-matrix.yaml",
|
||||
ROOT / "domains" / "seat" / "can" / "seat-motion-hardware-matrix.yaml",
|
||||
]
|
||||
)
|
||||
self.compiler = T2GSeatCompiler(
|
||||
ROOT / "domains" / "seat" / "can" / "signal-mapping.hardware.yaml",
|
||||
ROOT / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
|
||||
)
|
||||
|
||||
def test_compiler_emits_verified_heating_payload(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_heating", {"state": "on", "level": 3})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
frames = [
|
||||
self.matrix.encode(item["message"], item["signals"])
|
||||
for item in plan["spec"]["commands"]
|
||||
]
|
||||
|
||||
self.assertEqual(frames[0]["frame_id"], "0x3CE")
|
||||
self.assertEqual(frames[0]["data_hex"], "10 00 00 00 00 00 00 00")
|
||||
self.assertEqual(frames[1]["frame_id"], "0x29E")
|
||||
self.assertEqual(frames[1]["data_hex"], "16 00 30 00 00 00 00 00")
|
||||
|
||||
def test_compiler_emits_verified_backrest_payload(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_backrest_recline", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
command = plan["spec"]["commands"][0]
|
||||
frame = self.matrix.encode(command["message"], command["signals"])
|
||||
|
||||
self.assertEqual(frame["frame_id"], "0x300")
|
||||
self.assertEqual(frame["data_hex"], "01 12 00 00 94 11 32 00")
|
||||
|
||||
def test_compiler_maps_small_relative_backrest_request_to_timed_duty(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,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
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"], 5500)
|
||||
self.assertEqual(command["signals"]["Param_B"], 50)
|
||||
self.assertEqual(command["completion"]["signal"], "Task_State")
|
||||
|
||||
def test_compiler_maps_large_relative_backrest_request_to_absolute_target(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 64, "angle_deg": 124},
|
||||
motion={
|
||||
"mode": "relative",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 20,
|
||||
"delta_position_pct": 44,
|
||||
"start_angle_deg": 104,
|
||||
"delta_angle_deg": 20,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
command = self.compiler.compile(config, {})["spec"]["commands"][0]
|
||||
|
||||
self.assertEqual(command["signals"]["Opcode"], 1)
|
||||
self.assertEqual(command["signals"]["Direction"], 0)
|
||||
self.assertEqual(command["signals"]["Param_A"], 5760)
|
||||
self.assertEqual(command["completion"]["signal"], "Value_X100")
|
||||
|
||||
def test_passenger_alias_emits_the_same_physical_payload(self) -> None:
|
||||
driver = function_config(
|
||||
operation("seat.set_heating", {"state": "on", "level": 2})
|
||||
)
|
||||
passenger = function_config(
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 2},
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
)
|
||||
)
|
||||
|
||||
driver_plan = self.compiler.compile(driver, {})
|
||||
passenger_plan = self.compiler.compile(passenger, {})
|
||||
self.assertEqual(
|
||||
driver_plan["spec"]["commands"], passenger_plan["spec"]["commands"]
|
||||
)
|
||||
|
||||
def test_driver_and_passenger_identical_motion_is_sent_once(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 60},
|
||||
operation_id="op-driver",
|
||||
),
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 60},
|
||||
operation_id="op-passenger",
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
),
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
self.assertEqual(len(plan["spec"]["commands"]), 1)
|
||||
self.assertEqual(
|
||||
plan["spec"]["commands"][0]["operation_ref"],
|
||||
"op-driver,op-passenger",
|
||||
)
|
||||
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 1])
|
||||
operation_results = [
|
||||
item for item in report["spec"]["results"] if item["ref"].startswith("op-")
|
||||
]
|
||||
self.assertEqual({item["ref"] for item in operation_results}, {"op-driver", "op-passenger"})
|
||||
|
||||
def test_executor_waits_for_ack_and_physical_motion_feedback(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_backrest_recline", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["mode"], "hil")
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
self.assertFalse(report["spec"]["results"][0]["telemetry"]["simulated"])
|
||||
self.assertTrue(report["spec"]["results"][0]["telemetry"]["hardware_feedback"])
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 1])
|
||||
self.assertTrue(bus.closed)
|
||||
|
||||
def test_executor_blocks_absolute_motion_when_board_position_is_invalid(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_backrest_recline", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, position_valid=False)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 9])
|
||||
failure = report["spec"]["results"][-1]
|
||||
self.assertIn("Position_Valid=0", failure["message"])
|
||||
self.assertTrue(failure["telemetry"]["calibration_required"])
|
||||
|
||||
def test_large_relative_request_is_blocked_without_sending_absolute_opcode(self) -> None:
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_backrest_recline",
|
||||
{"position_pct": 64, "angle_deg": 124},
|
||||
motion={
|
||||
"mode": "relative",
|
||||
"direction": "increase",
|
||||
"start_position_pct": 20,
|
||||
"delta_position_pct": 44,
|
||||
"start_angle_deg": 104,
|
||||
"delta_angle_deg": 20,
|
||||
},
|
||||
)
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, position_valid=False)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 9])
|
||||
self.assertNotIn(1, [item.data[0] for item in bus.sent])
|
||||
self.assertTrue(
|
||||
report["spec"]["results"][-1]["telemetry"]["calibration_required"]
|
||||
)
|
||||
|
||||
def test_executor_accepts_hall_confirmed_timed_motion_without_position(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=False)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).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"], "relative_timed_duty")
|
||||
self.assertFalse(telemetry["position_valid"])
|
||||
self.assertGreater(telemetry["hall_delta"], 0)
|
||||
self.assertTrue(telemetry["hall_direction_verified"])
|
||||
|
||||
def test_executor_reports_stall_with_post_failure_hall_diagnostics(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=False,
|
||||
timed_failure_result=4,
|
||||
timed_failure_event=4,
|
||||
)
|
||||
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 3, 9])
|
||||
failure = report["spec"]["results"][-1]
|
||||
self.assertIn("STALL", failure["message"])
|
||||
self.assertEqual(failure["telemetry"]["motor_result_name"], "STALL")
|
||||
self.assertEqual(failure["telemetry"]["last_motor_event_name"], "STALL")
|
||||
self.assertEqual(
|
||||
failure["telemetry"]["failure_category"], "stall_no_hall_activity"
|
||||
)
|
||||
self.assertEqual(failure["telemetry"]["hall_before"], 0)
|
||||
self.assertEqual(failure["telemetry"]["hall_after"], 0)
|
||||
self.assertEqual(
|
||||
failure["telemetry"]["diagnostic_frame"]["frame_id"], "0x303"
|
||||
)
|
||||
|
||||
def test_executor_sends_stop_all_after_motion_feedback_timeout(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_legrest_extension", {"position_pct": 50})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix, complete_motion=False)
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, ack_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertEqual([item.data[0] for item in bus.sent], [9, 1, 9])
|
||||
self.assertTrue(
|
||||
report["spec"]["results"][-1]["telemetry"]["stop_all_attempted"]
|
||||
)
|
||||
|
||||
def test_executor_waits_for_real_thermal_status(self) -> None:
|
||||
config = function_config(
|
||||
operation("seat.set_ventilation", {"state": "on", "level": 2})
|
||||
)
|
||||
plan = self.compiler.compile(config, {})
|
||||
bus = ReactiveFakeBus(self.matrix)
|
||||
report = HardwareSeatExecutor(
|
||||
self.matrix, lambda: bus, thermal_timeout_ms=20
|
||||
).execute(plan, config)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
telemetry = report["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["feedback_frame"]["frame_id"], "0x4FA")
|
||||
self.assertEqual(telemetry["feedback_value"], 3)
|
||||
|
||||
def test_settings_allow_shared_target_aliases_but_fail_closed_for_other_targets(self) -> None:
|
||||
environment = {
|
||||
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "true",
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN": "bench-secret",
|
||||
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROFILES": "combined",
|
||||
"COGNITIVE_AGENT_ENABLE_UNVERIFIED_LONGITUDINAL": "false",
|
||||
}
|
||||
with patch.dict(os.environ, environment, clear=False):
|
||||
settings = SeatHardwareSettings.from_env(ROOT)
|
||||
settings = replace(
|
||||
settings, firmware_images={"combined": ROOT / "pyproject.toml"}
|
||||
)
|
||||
|
||||
settings.authorize("bench-secret")
|
||||
settings.validate_function_config(
|
||||
function_config(
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 1},
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
)
|
||||
)
|
||||
)
|
||||
with self.assertRaises(HardwareSafetyError):
|
||||
settings.validate_function_config(
|
||||
function_config(
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 1},
|
||||
instance="rear_left",
|
||||
zone="rear_left",
|
||||
)
|
||||
)
|
||||
)
|
||||
with self.assertRaises(HardwareSafetyError):
|
||||
settings.validate_function_config(
|
||||
function_config(
|
||||
operation(
|
||||
"seat.set_longitudinal_position", {"position_pct": 40}
|
||||
)
|
||||
)
|
||||
)
|
||||
with self.assertRaises(HardwareSafetyError):
|
||||
settings.authorize("wrong-secret")
|
||||
|
||||
def test_settings_reject_conflicting_shared_alias_requests(self) -> None:
|
||||
environment = {
|
||||
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "true",
|
||||
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN": "bench-secret",
|
||||
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROFILES": "combined",
|
||||
}
|
||||
with patch.dict(os.environ, environment, clear=False):
|
||||
settings = SeatHardwareSettings.from_env(ROOT)
|
||||
with self.assertRaises(HardwareSafetyError):
|
||||
settings.validate_function_config(
|
||||
function_config(
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 1},
|
||||
operation_id="op-driver",
|
||||
),
|
||||
operation(
|
||||
"seat.set_heating",
|
||||
{"state": "on", "level": 3},
|
||||
operation_id="op-passenger",
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def test_firmware_guard_reuses_bus_when_identity_matches(self) -> None:
|
||||
bus = IdentityFakeBus(EXPECTED_IDENTITY_PAYLOAD)
|
||||
guard = T2GFirmwareGuard(
|
||||
lambda: bus,
|
||||
None,
|
||||
auto_flash=False,
|
||||
probe_timeout_ms=20,
|
||||
)
|
||||
|
||||
prepared, telemetry = guard.prepare_bus()
|
||||
|
||||
self.assertIs(prepared, bus)
|
||||
self.assertFalse(telemetry["programmed_this_request"])
|
||||
self.assertEqual(bus.sent[0].arbitration_id, 0x304)
|
||||
self.assertFalse(bus.closed)
|
||||
|
||||
def test_firmware_guard_programs_then_reverifies(self) -> None:
|
||||
before = IdentityFakeBus(None)
|
||||
after = IdentityFakeBus(EXPECTED_IDENTITY_PAYLOAD)
|
||||
buses = deque([before, after])
|
||||
programmer = FakeProgrammer()
|
||||
guard = T2GFirmwareGuard(
|
||||
buses.popleft,
|
||||
programmer, # type: ignore[arg-type]
|
||||
auto_flash=True,
|
||||
probe_timeout_ms=20,
|
||||
boot_wait_ms=0,
|
||||
)
|
||||
|
||||
prepared, telemetry = guard.prepare_bus()
|
||||
|
||||
self.assertIs(prepared, after)
|
||||
self.assertEqual(programmer.calls, 1)
|
||||
self.assertTrue(before.closed)
|
||||
self.assertTrue(telemetry["programmed_this_request"])
|
||||
|
||||
def test_firmware_guard_programs_when_unflashed_target_does_not_ack(self) -> None:
|
||||
before = FailingIdentityBus(None)
|
||||
after = IdentityFakeBus(EXPECTED_IDENTITY_PAYLOAD)
|
||||
buses = deque([before, after])
|
||||
programmer = FakeProgrammer()
|
||||
guard = T2GFirmwareGuard(
|
||||
buses.popleft,
|
||||
programmer, # type: ignore[arg-type]
|
||||
auto_flash=True,
|
||||
probe_timeout_ms=20,
|
||||
boot_wait_ms=0,
|
||||
)
|
||||
|
||||
prepared, telemetry = guard.prepare_bus()
|
||||
|
||||
self.assertIs(prepared, after)
|
||||
self.assertEqual(programmer.calls, 1)
|
||||
self.assertTrue(before.closed)
|
||||
self.assertTrue(telemetry["programmed_this_request"])
|
||||
|
||||
def test_firmware_guard_fails_closed_when_auto_flash_is_disabled(self) -> None:
|
||||
bus = IdentityFakeBus(None)
|
||||
guard = T2GFirmwareGuard(
|
||||
lambda: bus,
|
||||
None,
|
||||
auto_flash=False,
|
||||
probe_timeout_ms=20,
|
||||
)
|
||||
|
||||
with self.assertRaises(FirmwareProgrammingError):
|
||||
guard.prepare_bus()
|
||||
self.assertTrue(bus.closed)
|
||||
|
||||
def test_programmer_command_downloads_and_hands_off_to_running_target(self) -> None:
|
||||
firmware_root = ROOT / "firmware" / "t2g" / "Seat_Controller_demo"
|
||||
output_image = (
|
||||
firmware_root / "IAR" / "rev_d" / "Exe" / "Seat_Controller_demo.out"
|
||||
)
|
||||
program_macro = firmware_root / "IAR" / "Seat_Controller_program.mac"
|
||||
programmer = T2GSeatProgrammer(
|
||||
cspybat=ROOT / "fake-iar" / "common" / "bin" / "cspybat.exe",
|
||||
debug_file=output_image,
|
||||
hex_image=(
|
||||
firmware_root / "IAR" / "rev_d" / "Exe" / "Seat_Controller_demo.hex"
|
||||
),
|
||||
macro_file=program_macro,
|
||||
)
|
||||
|
||||
# Dependency discovery is covered by settings/configuration tests. This
|
||||
# test only validates the command and never starts C-SPY or a probe.
|
||||
with patch.object(T2GSeatProgrammer, "required_files", return_value=()):
|
||||
command = programmer.command()
|
||||
|
||||
self.assertEqual(command[3], str(output_image))
|
||||
self.assertIn(f"--macro={program_macro}", command)
|
||||
self.assertNotIn("--download_only", command)
|
||||
self.assertIn("--leave_target_running", command)
|
||||
timeout_index = command.index("--timeout")
|
||||
self.assertEqual(command[timeout_index + 1], "3000")
|
||||
self.assertIn("--jet_probe=cmsisdap", command)
|
||||
self.assertNotIn("Seat_Controller_demo.mac", " ".join(command))
|
||||
|
||||
def test_hardware_validation_replaces_reference_dry_run_warnings(self) -> None:
|
||||
validation = [
|
||||
{"severity": "warning", "code": code, "message": "dry-run"}
|
||||
for code in (
|
||||
"LEVEL_COMMAND_SIMULATED",
|
||||
"SYNTHETIC_MOTION_CAN",
|
||||
"DRIVER_MOTION_INTERLOCK_NOT_IMPLEMENTED",
|
||||
"COMBINED_THERMAL_SIMULATED",
|
||||
"VIRTUAL_EXECUTION_ONLY",
|
||||
)
|
||||
]
|
||||
|
||||
SeatDemoService._adapt_validation_for_hardware(validation)
|
||||
|
||||
codes = {issue["code"] for issue in validation}
|
||||
self.assertEqual(
|
||||
codes,
|
||||
{
|
||||
"LEVEL_COMMAND_T2G_MAPPED",
|
||||
"T2G_MOTION_PROTOCOL_MAPPED",
|
||||
"BENCH_ONLY_NO_VEHICLE_INTERLOCKS",
|
||||
"COMBINED_THERMAL_T2G_MAPPED",
|
||||
"REFERENCE_MATRIX_OVERRIDDEN_BY_T2G_BENCH",
|
||||
},
|
||||
)
|
||||
messages = " ".join(issue["message"] for issue in validation)
|
||||
self.assertNotIn("禁止实车下发", messages)
|
||||
self.assertNotIn("只能虚拟执行", messages)
|
||||
|
||||
def test_physical_feedback_is_mirrored_to_both_logical_seats(self) -> None:
|
||||
actions = [
|
||||
{
|
||||
"target": "passenger",
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": 2,
|
||||
}
|
||||
]
|
||||
config = function_config(
|
||||
operation(
|
||||
"seat.set_ventilation",
|
||||
{"state": "on", "level": 2},
|
||||
instance="passenger",
|
||||
zone="front_right",
|
||||
)
|
||||
)
|
||||
report = {
|
||||
"spec": {
|
||||
"results": [
|
||||
{
|
||||
"ref": "op-hardware-test",
|
||||
"status": "succeeded",
|
||||
"telemetry": {"feedback_value": 3},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
projected = SeatDemoService._hardware_feedback_actions(
|
||||
actions, config, report
|
||||
)
|
||||
|
||||
self.assertEqual({item["target"] for item in projected}, {"driver", "passenger"})
|
||||
self.assertTrue(
|
||||
all(item["requested_level"] == 2 for item in projected)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user