优化了副驾座椅控制
This commit is contained in:
@@ -2,8 +2,12 @@ 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,
|
||||
)
|
||||
@@ -11,6 +15,9 @@ 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(
|
||||
@@ -57,6 +64,8 @@ class _FakeOtaBus:
|
||||
bitrate_switch=bitrate_switch,
|
||||
)
|
||||
self.sent.append(frame)
|
||||
if arbitration_id != 0x6A0:
|
||||
return frame
|
||||
opcode = data[2]
|
||||
job_state = (
|
||||
1 | (4 << 4) | (9 << 8)
|
||||
@@ -87,6 +96,30 @@ class _FakeOtaBus:
|
||||
|
||||
|
||||
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}
|
||||
|
||||
@@ -97,6 +130,37 @@ class _StatusExecutor:
|
||||
) -> 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 {
|
||||
@@ -115,6 +179,65 @@ def _function_config(capability: str, parameters: dict[str, object]) -> dict[str
|
||||
|
||||
|
||||
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%")
|
||||
|
||||
@@ -161,7 +284,7 @@ class MqttOtaSeatExecutorTests(unittest.TestCase):
|
||||
self.assertEqual(int.from_bytes(bus.sent[0].data[4:8], "little"), 500)
|
||||
self.assertEqual(
|
||||
bus.debug_commands,
|
||||
["seat passenger longitudinal position 50"],
|
||||
["seat passenger horizontal position 50"],
|
||||
)
|
||||
self.assertEqual(
|
||||
report["spec"]["results"][0]["telemetry"]["effective_position_pct"],
|
||||
@@ -203,8 +326,10 @@ class MqttOtaSeatExecutorTests(unittest.TestCase):
|
||||
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_fails_before_publish(self) -> None:
|
||||
bus = _FakeOtaBus([])
|
||||
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,
|
||||
@@ -212,10 +337,21 @@ class MqttOtaSeatExecutorTests(unittest.TestCase):
|
||||
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": {},
|
||||
"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,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -226,9 +362,14 @@ class MqttOtaSeatExecutorTests(unittest.TestCase):
|
||||
_function_config("seat.set_heating", {"state": "on", "level": 1}),
|
||||
)
|
||||
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
self.assertIn("does not provide wireless thermal control", report["spec"]["results"][0]["message"])
|
||||
self.assertEqual(bus.sent, [])
|
||||
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 (
|
||||
@@ -267,7 +408,7 @@ class MqttOtaSeatExecutorTests(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
bus.debug_commands,
|
||||
[
|
||||
"seat passenger dynamic start "
|
||||
"seat passenger dynamic "
|
||||
+ ("rocking-horse" if semantic_mode == 1 else "cradle")
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user