feat: integrate cabin CAN and verified dynamic seat modes
This commit is contained in:
@@ -111,7 +111,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
def test_static_console_and_runtime_endpoint(self) -> None:
|
||||
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-9", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-13", timeout=3) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
with self.opener.open(f"{self.base_url}/api/runtime", timeout=3) as response:
|
||||
runtime = json.loads(response.read().decode("utf-8"))
|
||||
@@ -121,7 +121,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
self.assertIn('data-vehicle-model="byd-tang-2024-reference"', html)
|
||||
self.assertIn("整车孪生界面", html)
|
||||
self.assertIn("透明车体 · 实体设备", html)
|
||||
self.assertIn('src="/app.js?v=seat-hil-9"', html)
|
||||
self.assertIn('src="/app.js?v=seat-hil-13"', html)
|
||||
self.assertIn('id="hardwareAccessField"', html)
|
||||
self.assertIn("hardware_access_required", app_script)
|
||||
self.assertIn("HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000", app_script)
|
||||
|
||||
@@ -538,7 +538,7 @@ class MultiDomainServiceTests(unittest.TestCase):
|
||||
vehicle_id="denza_n7",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "execution_failed")
|
||||
self.assertEqual(result["status"], "partially_succeeded")
|
||||
self.assertEqual(result["execution_report"]["spec"]["status"], "failed")
|
||||
self.assertEqual(
|
||||
result["actuator_state"]["passenger"]["position"]["longitudinal"][
|
||||
@@ -553,6 +553,10 @@ class MultiDomainServiceTests(unittest.TestCase):
|
||||
"physical_can",
|
||||
)
|
||||
self.assertEqual(
|
||||
result["domain_state"]["roof"]["transmittance_pct"],
|
||||
20,
|
||||
)
|
||||
self.assertNotEqual(
|
||||
result["domain_state"]["roof"]["transmittance_pct"],
|
||||
initial_roof,
|
||||
)
|
||||
|
||||
@@ -69,6 +69,8 @@ class ModeProfileRegistryTests(unittest.TestCase):
|
||||
"electric_beach_chair",
|
||||
"queen_passenger_driving",
|
||||
"calf_decompression_float",
|
||||
"rocking_horse",
|
||||
"dynamic_cradle",
|
||||
}
|
||||
denza_modes = registry.public_modes(vehicle_id="denza_n7")
|
||||
denza_ids = {mode["id"] for mode in denza_modes}
|
||||
@@ -88,6 +90,104 @@ class ModeProfileRegistryTests(unittest.TestCase):
|
||||
"calf_decompression_float",
|
||||
)
|
||||
|
||||
def test_dynamic_modes_preserve_exact_timelines_and_native_mode_ids(self) -> None:
|
||||
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
|
||||
profiles = {
|
||||
profile.id: profile
|
||||
for profile in registry.profiles()
|
||||
if profile.id in {"rocking_horse", "dynamic_cradle"}
|
||||
}
|
||||
|
||||
horse = profiles["rocking_horse"]
|
||||
self.assertIsNotNone(horse.timeline)
|
||||
assert horse.timeline is not None
|
||||
self.assertEqual(horse.timeline.repeat, "until_stopped_or_timeout")
|
||||
self.assertEqual(horse.timeline.max_runtime_ms, 600_000)
|
||||
self.assertIsNone(horse.timeline.cycle_duration_ms)
|
||||
self.assertEqual(
|
||||
[(phase.id, phase.duration_ms) for phase in horse.timeline.phases],
|
||||
[("rock_forward", None), ("rock_backward", None)],
|
||||
)
|
||||
self.assertEqual(
|
||||
horse.timeline.entry["targets"]["longitudinal"]["hall_pulse"],
|
||||
320,
|
||||
)
|
||||
self.assertEqual(
|
||||
[phase.completion for phase in horse.timeline.phases],
|
||||
["all_targets_reached", "all_targets_reached"],
|
||||
)
|
||||
self.assertEqual([phase.timeout_ms for phase in horse.timeline.phases], [15_000, 15_000])
|
||||
self.assertEqual(horse.timeline.phases[0].motion["final_approach_hall_pulses"], 8)
|
||||
self.assertTrue(horse.timeline.phases[0].motion["continuous_four_axis_cruise"])
|
||||
self.assertEqual(
|
||||
horse.timeline.phases[0].targets["cushion_tilt"]["hall_pulse"],
|
||||
250,
|
||||
)
|
||||
self.assertEqual(
|
||||
horse.timeline.phases[1].targets["cushion_tilt"]["hall_pulse"],
|
||||
130,
|
||||
)
|
||||
self.assertEqual(
|
||||
horse.timeline.phases[0].motion["cruise_duty_percent"]["cushion_tilt"],
|
||||
90,
|
||||
)
|
||||
self.assertEqual(
|
||||
horse.timeline.phases[0].targets["longitudinal"]["hall_pulse"],
|
||||
270,
|
||||
)
|
||||
self.assertEqual(
|
||||
horse.timeline.phases[1].targets["longitudinal"]["hall_pulse"],
|
||||
370,
|
||||
)
|
||||
self.assertEqual(
|
||||
horse.timeline.phases[0].motion["cruise_duty_percent"]["longitudinal"],
|
||||
90,
|
||||
)
|
||||
|
||||
cradle = profiles["dynamic_cradle"]
|
||||
self.assertIsNotNone(cradle.timeline)
|
||||
assert cradle.timeline is not None
|
||||
self.assertEqual(cradle.timeline.repeat, "until_stopped_or_timeout")
|
||||
self.assertEqual(cradle.timeline.max_runtime_ms, 600_000)
|
||||
self.assertIsNone(cradle.timeline.cycle_duration_ms)
|
||||
self.assertEqual(
|
||||
[(phase.id, phase.duration_ms) for phase in cradle.timeline.phases],
|
||||
[("forward", None), ("return", None)],
|
||||
)
|
||||
self.assertEqual(
|
||||
cradle.timeline.entry["targets"]["longitudinal"]["hall_pulse"],
|
||||
391,
|
||||
)
|
||||
self.assertEqual(
|
||||
[phase.completion for phase in cradle.timeline.phases],
|
||||
["all_targets_reached", "all_targets_reached"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[phase.timeout_ms for phase in cradle.timeline.phases],
|
||||
[30_000, 30_000],
|
||||
)
|
||||
self.assertEqual(
|
||||
cradle.timeline.phases[0].motion["final_approach_hall_pulses"],
|
||||
12,
|
||||
)
|
||||
self.assertTrue(
|
||||
cradle.timeline.phases[0].motion["continuous_four_axis_cruise"]
|
||||
)
|
||||
self.assertNotIn("clearance_lead", cradle.timeline.phases[0].motion)
|
||||
|
||||
requests = {
|
||||
profile.id: registry.requests_for_profiles([profile])[0]
|
||||
for profile in (horse, cradle)
|
||||
}
|
||||
self.assertEqual(
|
||||
requests["rocking_horse"]["parameters"],
|
||||
[{"name": "mode_id", "value": 1}],
|
||||
)
|
||||
self.assertEqual(
|
||||
requests["dynamic_cradle"]["parameters"],
|
||||
[{"name": "mode_id", "value": 2}],
|
||||
)
|
||||
|
||||
def test_denza_mode_positions_use_corrected_n7_zero_points(self) -> None:
|
||||
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
|
||||
expected_positions = {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.application.native_can_runtime import (
|
||||
NativeCanExecutor,
|
||||
NativeCanRuntime,
|
||||
)
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
from cognitive_agent.infrastructure.can.gateway import CanFrame
|
||||
from cognitive_agent.infrastructure.can.hardware import VehicleCanSettings
|
||||
|
||||
|
||||
class _FakeBus:
|
||||
def __init__(self, received: list[CanFrame] | None = None) -> None:
|
||||
self.received = list(received or [])
|
||||
self.sent: list[CanFrame] = []
|
||||
self.closed = False
|
||||
|
||||
def send(
|
||||
self,
|
||||
arbitration_id: int,
|
||||
data: bytes,
|
||||
*,
|
||||
is_fd: bool = False,
|
||||
bitrate_switch: bool = False,
|
||||
) -> CanFrame:
|
||||
frame = CanFrame(
|
||||
arbitration_id,
|
||||
data,
|
||||
channel=1,
|
||||
is_fd=is_fd,
|
||||
bitrate_switch=bitrate_switch,
|
||||
)
|
||||
self.sent.append(frame)
|
||||
return frame
|
||||
|
||||
def receive(
|
||||
self, timeout_ms: int, *, include_fd: bool = False
|
||||
) -> CanFrame | None:
|
||||
return self.received.pop(0) if self.received else None
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class NativeCanIntegrationTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.runtime = NativeCanRuntime(REPOSITORY_ROOT)
|
||||
|
||||
def _compile(self, domain: str, actions: list[dict]) -> tuple[dict, dict]:
|
||||
config = self.runtime.build_function_config(
|
||||
domain, actions, "req-native-test", "local_rules"
|
||||
)
|
||||
return config, self.runtime.compile(config)
|
||||
|
||||
def test_climate_frames_match_can_dev_layout_and_repeat_policy(self) -> None:
|
||||
actions = [
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_power",
|
||||
"target": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"state": "on"},
|
||||
},
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target": "driver",
|
||||
"operation": "set",
|
||||
"parameters": {"temperature_c": 24},
|
||||
},
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_fan_level",
|
||||
"target": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"level": 3},
|
||||
},
|
||||
]
|
||||
_, plan = self._compile("climate", actions)
|
||||
commands = plan["spec"]["commands"]
|
||||
|
||||
self.assertEqual([item["signals"]["frame_id"] for item in commands], ["0x1DE"] * 3)
|
||||
self.assertEqual(commands[0]["signals"]["data_hex"], "00 00 00 10 00 00 00 00")
|
||||
self.assertEqual(commands[1]["signals"]["data_hex"], "00 00 00 00 18 00 00 00")
|
||||
self.assertEqual(commands[2]["signals"]["data_hex"], "30 00 00 00 00 00 00 00")
|
||||
self.assertEqual(commands[0]["repeat"], {"count": 3, "period_ms": 100})
|
||||
self.assertFalse(commands[0]["signals"]["is_fd"])
|
||||
|
||||
def test_window_motion_uses_can_fd_with_brs(self) -> None:
|
||||
_, plan = self._compile(
|
||||
"window",
|
||||
[
|
||||
{
|
||||
"domain": "window",
|
||||
"capability": "window.set_motion",
|
||||
"target": "front_left",
|
||||
"operation": "set",
|
||||
"parameters": {"motion": "open"},
|
||||
}
|
||||
],
|
||||
)
|
||||
command = plan["spec"]["commands"][0]
|
||||
self.assertEqual(command["signals"]["frame_id"], "0x431")
|
||||
self.assertEqual(command["signals"]["data_hex"], "00 00 00 00 00 00 01 00")
|
||||
self.assertTrue(command["signals"]["is_fd"])
|
||||
self.assertTrue(command["signals"]["bitrate_switch"])
|
||||
|
||||
def test_executor_requires_configured_feedback_for_vehicle_success(self) -> None:
|
||||
config, plan = self._compile(
|
||||
"climate",
|
||||
[
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_power",
|
||||
"target": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"state": "on"},
|
||||
}
|
||||
],
|
||||
)
|
||||
feedback = CanFrame(0x404, bytes((0, 1, 0, 0, 0, 0, 0, 0)), channel=1)
|
||||
bus = _FakeBus([feedback])
|
||||
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
||||
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
|
||||
self.runtime, plan, config
|
||||
)
|
||||
self.assertEqual(report["spec"]["status"], "succeeded")
|
||||
self.assertEqual(len(bus.sent), 3)
|
||||
self.assertTrue(bus.closed)
|
||||
|
||||
no_feedback_bus = _FakeBus()
|
||||
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
||||
report = NativeCanExecutor(
|
||||
lambda: no_feedback_bus, feedback_timeout_ms=1
|
||||
).execute(self.runtime, plan, config)
|
||||
self.assertEqual(report["spec"]["status"], "failed")
|
||||
|
||||
def test_one_sentence_builds_seat_and_vehicle_channel_plans(self) -> None:
|
||||
service = SeatDemoService(REPOSITORY_ROOT)
|
||||
result = service.process(
|
||||
"副驾座椅通风3档,同时打开空调,温度设为24度,风量3档",
|
||||
provider="local",
|
||||
)
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(set(result["function_configs"]), {"seat", "native_climate"})
|
||||
self.assertEqual(set(result["command_plans"]), {"seat", "native_climate"})
|
||||
body_commands = result["command_plans"]["native_climate"]["spec"]["commands"]
|
||||
self.assertTrue(body_commands)
|
||||
self.assertTrue(all(item["bus"] == "body_can" for item in body_commands))
|
||||
self.assertFalse(result["deferred_requests"])
|
||||
|
||||
def test_vehicle_timing_contract_matches_requested_60mhz_values(self) -> None:
|
||||
settings = VehicleCanSettings.from_env(Path(REPOSITORY_ROOT))
|
||||
self.assertTrue(settings.timing_is_verified)
|
||||
info = settings.public_info()
|
||||
self.assertEqual(info["arbitration"]["bitrate"], 500_000)
|
||||
self.assertEqual(info["arbitration"]["sample_point_percent"], 80)
|
||||
self.assertEqual(info["data"]["bitrate"], 2_000_000)
|
||||
self.assertEqual(info["data"]["sample_point_percent"], 80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user