feat: integrate cabin CAN and verified dynamic seat modes

This commit is contained in:
vee
2026-08-04 17:51:38 +08:00
parent 155fbb59ba
commit dc7c91800e
99 changed files with 51897 additions and 3618 deletions
+145 -2
View File
@@ -16,6 +16,7 @@ if str(SEAT_SOURCE) not in sys.path:
from cognitive_agent.infrastructure.can.gateway import CanFrame, CanGatewayError # noqa: E402
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
from cognitive_agent.runtime.contracts import ContractValidator # noqa: E402
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
HardwareSafetyError,
SeatHardwareSettings,
@@ -242,6 +243,34 @@ class ReactiveFakeBus:
},
)
)
if opcode == 14:
self.received.append(
self._frame(
"T2G_Seat_Dynamic_Mode_Status_306",
{
"Dynamic_Mode_ID": command["Param_A"],
"Dynamic_State": 2,
"Dynamic_Phase": 2 if command["Param_A"] == 1 else 5,
"Dynamic_Active_Mask": 0x0F,
"Dynamic_Cycle_Index": 0,
"Dynamic_Phase_Elapsed_Ms": 100,
},
)
)
if opcode == 15:
self.received.append(
self._frame(
"T2G_Seat_Dynamic_Mode_Status_306",
{
"Dynamic_Mode_ID": 0,
"Dynamic_State": 0,
"Dynamic_Phase": 0,
"Dynamic_Active_Mask": 0,
"Dynamic_Cycle_Index": 0,
"Dynamic_Phase_Elapsed_Ms": 0,
},
)
)
if opcode == 0x0B and self.complete_motion:
motor = command["Motor_Selector"]
self.position_valid = True
@@ -447,7 +476,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 60")))
self.received.append(CanFrame(0x301, bytes.fromhex("10 00 00 00 00 00 F0 D0")))
for motor in range(4):
self.received.append(
CanFrame(0x302, bytes([motor, 0, 0x08, 0, 0, 0, 0, 0]))
@@ -464,6 +493,7 @@ class ProtocolSignatureFakeBus:
CanFrame(0x304, bytes([motor, 3, 0, 0, 0, 0, 0xF4, 0x01]))
)
self.received.append(CanFrame(0x305, bytes(8)))
self.received.append(CanFrame(0x306, bytes(8)))
self.received.append(CanFrame(0x4FA, bytes.fromhex("09 00 00 00 00 00 00 00")))
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
@@ -485,6 +515,29 @@ class FailingProtocolSignatureBus(ProtocolSignatureFakeBus):
raise RuntimeError("CAN receive failed during passive protocol probe")
class LegacyProtocolSignatureFakeBus(ProtocolSignatureFakeBus):
def __init__(self):
super().__init__(False)
self.received.append(CanFrame(0x301, bytes.fromhex("10 00 00 00 00 00 F0 00")))
for motor in range(4):
self.received.append(
CanFrame(0x302, bytes([motor, 0, 0x08, 0, 0, 0, 0, 0]))
)
for motor, maximum in enumerate((480, 374, 917, 647)):
self.received.append(
CanFrame(
0x303,
bytes([motor, 0, 0, 0, maximum & 0xFF, maximum >> 8, 0, 0]),
)
)
for motor in range(4):
self.received.append(
CanFrame(0x304, bytes([motor, 3, 0, 0, 0, 0, 0xF4, 0x01]))
)
self.received.append(CanFrame(0x305, bytes(8)))
self.received.append(CanFrame(0x4FA, bytes(8)))
class FakeProgrammer:
def __init__(self):
self.calls = 0
@@ -511,6 +564,7 @@ class T2GHardwareTests(unittest.TestCase):
ROOT / "domains" / "seat" / "can" / "signal-mapping.hardware.yaml",
ROOT / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
)
self.contracts = ContractValidator(ROOT)
def test_compiler_emits_verified_heating_payload(self) -> None:
config = function_config(
@@ -569,6 +623,34 @@ class T2GHardwareTests(unittest.TestCase):
self.assertEqual(fallback["signals"]["Param_B"], 900)
self.assertEqual(fallback["completion"]["signal"], "Task_State")
def test_compiler_emits_native_dynamic_mode_command(self) -> None:
config = function_config(
operation("seat.start_dynamic_mode", {"mode_id": 1})
)
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"], "0E 10 00 00 01 00 00 00")
self.assertEqual(command["completion"]["message"], "T2G_Seat_Dynamic_Mode_Status_306")
self.assertEqual(command["completion"]["accepted_states"], [1, 2])
self.assertEqual(self.contracts.validate(plan), [])
def test_compiler_emits_native_dynamic_mode_stop_command(self) -> None:
config = function_config(operation("seat.stop_dynamic_mode", {}))
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"], "0F 10 00 00 00 00 00 00")
self.assertEqual(command["completion"]["signal"], "Dynamic_State")
self.assertEqual(command["completion"]["accepted_states"], [0])
self.assertEqual(self.contracts.validate(plan), [])
def test_compiler_maps_large_relative_backrest_request_to_absolute_target(self) -> None:
config = function_config(
operation(
@@ -696,6 +778,46 @@ class T2GHardwareTests(unittest.TestCase):
self.assertEqual([item.data[0] for item in bus.sent], [9, 1])
self.assertTrue(bus.closed)
def test_executor_waits_for_native_dynamic_mode_feedback(self) -> None:
config = function_config(
operation("seat.start_dynamic_mode", {"mode_id": 2})
)
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"]["status"], "succeeded")
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertEqual(telemetry["dynamic_mode_id"], 2)
self.assertEqual(telemetry["dynamic_state"], 2)
self.assertEqual(telemetry["dynamic_phase"], 5)
self.assertEqual([item.data[0] for item in bus.sent], [9, 14])
self.assertTrue(bus.closed)
def test_executor_stops_dynamic_mode_without_homing_or_pre_stop(self) -> None:
config = function_config(operation("seat.stop_dynamic_mode", {}))
plan = self.compiler.compile(config, {})
bus = ReactiveFakeBus(self.matrix)
report = HardwareSeatExecutor(
self.matrix,
lambda: bus,
ack_timeout_ms=20,
synchronize_position_before_first_motion=True,
position_sync_state={"synchronized": False},
).execute(plan, config)
self.assertEqual(report["spec"]["status"], "succeeded")
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertEqual(telemetry["dynamic_mode_id"], 0)
self.assertEqual(telemetry["dynamic_state"], 0)
self.assertEqual(telemetry["repeat"], "none")
self.assertEqual([item.data[0] for item in bus.sent], [15])
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})
@@ -1453,6 +1575,9 @@ class T2GHardwareTests(unittest.TestCase):
settings.validate_function_config(
function_config(operation("seat.set_cushion_tilt", {"position_pct": 40}))
)
settings.validate_function_config(
function_config(operation("seat.stop_dynamic_mode", {}))
)
with self.assertRaises(HardwareSafetyError):
settings.validate_function_config(
function_config(
@@ -1559,7 +1684,7 @@ class T2GHardwareTests(unittest.TestCase):
self.assertIs(prepared, bus)
self.assertFalse(telemetry["programmed_this_request"])
self.assertEqual(telemetry["firmware_revision"], 6)
self.assertEqual(telemetry["firmware_revision"], 13)
self.assertEqual(bus.sent, [])
self.assertEqual(telemetry["max_hall_counts"], [480, 374, 917, 647])
self.assertFalse(bus.closed)
@@ -1617,6 +1742,24 @@ class T2GHardwareTests(unittest.TestCase):
guard.prepare_bus()
self.assertTrue(bus.closed)
def test_firmware_guard_reports_legacy_revision_and_missing_dynamic_status(self) -> None:
bus = LegacyProtocolSignatureFakeBus()
guard = T2GFirmwareGuard(
lambda: bus,
None,
auto_flash=False,
probe_timeout_ms=20,
)
with self.assertRaises(FirmwareProgrammingError) as raised:
guard.prepare_bus()
message = str(raised.exception)
self.assertIn("firmware_revision=0 (expected 13)", message)
self.assertIn("missing_frame_ids=0x306", message)
self.assertIn("max_hall_counts=480,374,917,647", message)
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 = (