from __future__ import annotations import unittest from collections import deque from pathlib import Path import tempfile from types import SimpleNamespace from unittest.mock import patch from cognitive_agent.application.seat_service import SeatDemoService from cognitive_agent.application.mqtt_ota_seat_executor import ( MqttOtaSeatExecutor, ) from cognitive_agent.infrastructure.can.gateway import CanFrame from seat_domain import parse_local_intent REPOSITORY_ROOT = Path(__file__).resolve().parents[2] def _axis_frame(axis: int, value_x100: int, *, valid: bool = True) -> CanFrame: flags = (1 << 3) if valid else 0 return CanFrame( 0x302, bytes( ( axis, 0, flags, 0, 0, value_x100 & 0xFF, (value_x100 >> 8) & 0xFF, 0, ) ), channel=0, ) class _FakeOtaBus: def __init__(self, initial_frames: list[CanFrame]) -> None: self.frames = deque(initial_frames) self.sent: list[CanFrame] = [] self.debug_commands: list[str] = [] self.closed = False def set_debug_command(self, command: str) -> None: self.debug_commands.append(command) def send( self, arbitration_id: int, data: bytes, *, is_fd: bool = False, bitrate_switch: bool = False, ) -> CanFrame: frame = CanFrame( arbitration_id, bytes(data), channel=0, is_fd=is_fd, bitrate_switch=bitrate_switch, ) self.sent.append(frame) if arbitration_id != 0x6A0: return frame opcode = data[2] job_state = ( 1 | (4 << 4) | (9 << 8) if opcode == 0x66 else (1 | (3 << 4) | (7 << 8)) if opcode in {0x64, 0x65} else (1 << 1) | (1 << 2) ) self.frames.append( CanFrame( 0x6A2, bytes((0x7C, data[1], opcode, 0)) + job_state.to_bytes(4, "little"), channel=0, ) ) if opcode == 0x64: target_x100 = int.from_bytes(data[4:8], "little") * 10 self.frames.append(_axis_frame(data[3], target_x100)) return frame def receive(self, _timeout_ms: int, *, include_fd: bool = False) -> CanFrame | None: del include_fd return self.frames.popleft() if self.frames else None def close(self) -> None: self.closed = True class _StatusExecutor: class _Matrix: @staticmethod def encode(message: str, signals: dict[str, int]) -> dict[str, str]: if message == "T2G_Seat_Thermal_Mode_3CE": return { "frame_id": "0x3CE", "data_hex": f"{int(signals['Driver_Seat_Thermal_Mode']) << 4:02X} 00 00 00 00 00 00 00", } byte0 = ( int(signals["Driver_Seat_Ventilation_On"]) | (int(signals["Driver_Seat_Ventilation_Mode"]) << 2) | (int(signals["Driver_Seat_Heating_On"]) << 4) ) byte2 = ( int(signals["Driver_Seat_Ventilation_Gear"]) | (int(signals["Driver_Seat_Heating_Gear"]) << 4) ) return { "frame_id": "0x29E", "data_hex": f"{byte0:02X} 00 {byte2:02X} 00 00 00 00 00", } matrix = _Matrix() def read_physical_state(self) -> dict[str, object]: return {"position": {}, "complete": False} def _capture_physical_state( self, _bus: _FakeOtaBus, _results: list[dict[str, object]], ) -> dict[str, object]: return {"position": {}, "complete": True, "missing": []} def _execute_thermal( self, bus: _FakeOtaBus, command: dict[str, object], operations: list[dict[str, object]], ) -> list[dict[str, object]]: encoded = self.matrix.encode( str(command["message"]), command["signals"], # type: ignore[arg-type] ) sent = bus.send( int(encoded["frame_id"], 16), bytes.fromhex(encoded["data_hex"]), ) feedback = bus.receive(100) if feedback is None or feedback.arbitration_id != 0x4FA: raise RuntimeError("missing thermal feedback") return [ { "ref": str(operation["id"]), "domain": "seat", "status": "succeeded", "message": "thermal feedback confirmed", "telemetry": { "command_frame": sent.public(), "feedback_frame": feedback.public(), }, } for operation in operations ] def _function_config(capability: str, parameters: dict[str, object]) -> dict[str, object]: return { "metadata": {"request_id": "req-mqtt-ota-seat"}, "spec": { "operations": [ { "id": "operation-1", "capability": capability, "target": {"zone": "front_right", "instance": "passenger"}, "parameters": parameters, } ] }, } class MqttOtaSeatExecutorTests(unittest.TestCase): def test_wireless_hardware_service_skips_local_t2g_profile_and_bench(self) -> None: bus = _FakeOtaBus([_axis_frame(0, 4_000)]) executor = MqttOtaSeatExecutor( # type: ignore[arg-type] _StatusExecutor(), lambda: bus, ) service = SeatDemoService(REPOSITORY_ROOT) service.vehicle_mqtt_settings = SimpleNamespace( enabled=True, configured=True, authorize=lambda _token: None, ) validated_events: list[dict[str, object]] = [] with tempfile.TemporaryDirectory() as temporary_directory: service.artifact_root = Path(temporary_directory) with ( patch.object( type(service.hardware_settings), "validate_function_config", side_effect=AssertionError( "wireless CAN0 must not check the local combined profile" ), ), patch.object( service.hardware_execution, "reserve", side_effect=AssertionError( "wireless CAN0 must not reserve the local physical bench" ), ), patch.object( service, "_new_hardware_executor", return_value=executor, ) as executor_factory, ): result = service.process( "把副驾座椅水平位置调到35%", provider="local", conversation_id="conv-wireless-can0-no-local-hardware", execution_mode="hardware", control_transport="wireless_mqtt", _on_commands_validated=validated_events.append, ) self.assertEqual(result["status"], "succeeded") self.assertEqual(list(result["command_plans"]), ["seat"]) self.assertEqual(len(validated_events), 1) self.assertEqual(list(validated_events[0]["command_plans"]), ["seat"]) self.assertEqual( result["command_plans"]["seat"]["spec"]["commands"][0]["message"], "T2G_Seat_Move_Command_300", ) self.assertEqual(bus.sent[0].arbitration_id, 0x6A0) self.assertEqual(bus.sent[0].data[2], 0x64) self.assertEqual(int.from_bytes(bus.sent[0].data[4:8], "little"), 350) self.assertTrue(executor_factory.call_args.kwargs["remote_transport"]) def test_seat_height_language_maps_to_internal_axis_one_capability(self) -> None: parsed = parse_local_intent("副驾座椅高度调到60%") self.assertEqual(parsed["ambiguities"], []) self.assertEqual(parsed["actions"][0]["axis"], "cushion_tilt") self.assertEqual(parsed["actions"][0]["position_pct"], 60) self.assertIn("目标:副驾座椅高度位置60%。", parsed["decision_summary"]) def test_absolute_move_uses_runtime_move_and_fresh_position_confirmation(self) -> None: bus = _FakeOtaBus([_axis_frame(0, 4_000)]) executor = MqttOtaSeatExecutor( # type: ignore[arg-type] _StatusExecutor(), lambda: bus, ) plan = { "spec": { "commands": [ { "operation_ref": "operation-1", "message": "T2G_Seat_Move_Command_300", "signals": {"Opcode": 1, "Motor_Selector": 0}, "completion": { "value": 5_000, "tolerance": 100, "timeout_ms": 1_000, }, } ] } } report = executor.execute( plan, _function_config( "seat.set_longitudinal_position", {"position_pct": 50}, ), ) self.assertEqual(report["spec"]["status"], "succeeded") self.assertEqual(bus.sent[0].arbitration_id, 0x6A0) self.assertEqual(bus.sent[0].data[2], 0x64) self.assertEqual(bus.sent[0].data[3], 0) self.assertEqual(int.from_bytes(bus.sent[0].data[4:8], "little"), 500) self.assertEqual( bus.debug_commands, ["seat passenger horizontal position 50"], ) self.assertEqual( report["spec"]["results"][0]["telemetry"]["effective_position_pct"], 50, ) self.assertTrue(bus.closed) def test_legacy_zero_target_maps_to_move_not_homing(self) -> None: bus = _FakeOtaBus([_axis_frame(3, 2_000)]) executor = MqttOtaSeatExecutor( # type: ignore[arg-type] _StatusExecutor(), lambda: bus, ) plan = { "spec": { "commands": [ { "operation_ref": "operation-1", "message": "T2G_Seat_Move_Command_300", "signals": {"Opcode": 0x0B, "Motor_Selector": 3}, "completion": { "value": 0, "tolerance": 100, "timeout_ms": 1_000, }, } ] } } report = executor.execute( plan, _function_config("seat.set_legrest_height", {"position_pct": 0}), ) self.assertEqual(report["spec"]["status"], "succeeded") self.assertEqual(bus.sent[0].data[2], 0x64) self.assertEqual(int.from_bytes(bus.sent[0].data[4:8], "little"), 0) self.assertNotIn(0x61, [frame.data[2] for frame in bus.sent]) self.assertNotIn(0x62, [frame.data[2] for frame in bus.sent]) def test_wireless_thermal_command_uses_native_firmware_frames(self) -> None: bus = _FakeOtaBus( [CanFrame(0x4FA, bytes((0x11, 0, 0, 0, 0, 0, 0, 0)), channel=0)] ) executor = MqttOtaSeatExecutor( # type: ignore[arg-type] _StatusExecutor(), lambda: bus, ) plan = { "spec": { "commands": [ { "operation_ref": "operation-1", "message": "T2G_Seat_Thermal_Mode_3CE", "signals": {"Driver_Seat_Thermal_Mode": 1}, }, { "operation_ref": "operation-1", "message": "T2G_Seat_Thermal_Control_29E", "signals": { "Driver_Seat_Ventilation_On": 2, "Driver_Seat_Ventilation_Mode": 1, "Driver_Seat_Heating_On": 1, "Driver_Seat_Ventilation_Gear": 0, "Driver_Seat_Heating_Gear": 1, }, } ] } } report = executor.execute( plan, _function_config("seat.set_heating", {"state": "on", "level": 1}), ) self.assertEqual(report["spec"]["status"], "succeeded") self.assertEqual([frame.arbitration_id for frame in bus.sent], [0x3CE, 0x29E]) self.assertEqual(bus.sent[0].data, bytes.fromhex("10 00 00 00 00 00 00 00")) self.assertEqual(bus.sent[1].data, bytes.fromhex("16 00 10 00 00 00 00 00")) self.assertEqual( bus.debug_commands, ["seat passenger heat low", "seat passenger heat low"], ) def test_dynamic_semantic_modes_use_seat_ota_firmware_selectors(self) -> None: for semantic_mode, firmware_selector, mode_name in ( (1, 2, "木马模式"), (2, 1, "动态摇篮模式"), ): with self.subTest(semantic_mode=semantic_mode): bus = _FakeOtaBus([]) executor = MqttOtaSeatExecutor( # type: ignore[arg-type] _StatusExecutor(), lambda: bus, ) plan = { "spec": { "commands": [ { "operation_ref": "operation-1", "message": "T2G_Seat_Move_Command_300", "signals": {"Opcode": 0x0E, "Motor_Selector": 0}, } ] } } report = executor.execute( plan, _function_config( "seat.start_dynamic_mode", {"mode_id": semantic_mode}, ), ) self.assertEqual(report["spec"]["status"], "succeeded") self.assertEqual(bus.sent[0].data[2], 0x66) self.assertEqual(bus.sent[0].data[3], firmware_selector) self.assertEqual( bus.debug_commands, [ "seat passenger dynamic " + ("rocking-horse" if semantic_mode == 1 else "cradle") ], ) self.assertEqual( int.from_bytes(bus.sent[0].data[4:8], "little"), 300, ) telemetry = report["spec"]["results"][0]["telemetry"] self.assertEqual(telemetry["dynamic_mode_id"], semantic_mode) self.assertEqual(telemetry["ota_dynamic_selector"], firmware_selector) self.assertEqual(telemetry["dynamic_mode_name"], mode_name) if __name__ == "__main__": unittest.main()