添加无线模块

This commit is contained in:
minghanchen
2026-08-19 09:59:36 +08:00
parent 344f662970
commit 52c4dd08f9
33 changed files with 4740 additions and 246 deletions
+116 -10
View File
@@ -10,6 +10,7 @@ import urllib.request
from http.cookiejar import CookieJar
from http.server import ThreadingHTTPServer
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from cognitive_agent.auth import AuthConfig, AuthRuntime, hash_password
@@ -177,18 +178,18 @@ operations:
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=dual-seat-82", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-89", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-7", timeout=3
) as response:
gesture_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.js?v=desktop-refactor-36", timeout=3
f"{self.base_url}/console-v2.js?v=desktop-refactor-37", timeout=3
) as response:
console_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/console-v2.css?v=desktop-refactor-107", timeout=3
f"{self.base_url}/console-v2.css?v=desktop-refactor-108", timeout=3
) as response:
console_styles = response.read().decode("utf-8")
with self.opener.open(
@@ -231,12 +232,12 @@ operations:
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
self.assertNotIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=dual-seat-82"', html)
self.assertIn('href="/styles.css?v=dual-seat-9"', html)
self.assertIn('src="/app.js?v=dual-seat-89"', html)
self.assertIn('href="/styles.css?v=dual-seat-10"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-7"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=desktop-refactor-36"', html)
self.assertIn('href="/console-v2.css?v=desktop-refactor-107"', html)
self.assertIn('src="/console-v2.js?v=desktop-refactor-37"', html)
self.assertIn('href="/console-v2.css?v=desktop-refactor-108"', html)
self.assertIn('src="/self-evolution.js?v=seat-weighing-2"', html)
self.assertIn('href="/self-evolution.css?v=seat-weighing-5"', html)
self.assertIn('src="/self-extension.js?v=extension-21"', html)
@@ -399,7 +400,19 @@ operations:
self.assertIn('const dynamicModeOrder = ["rocking_horse", "dynamic_cradle"]', app_script)
self.assertIn('divider.className = "mode-group-divider"', app_script)
self.assertIn(
'mode === "hardware" && !state.runtime?.hardware_execution_allowed',
'mode === "hardware" && !hardwareExecutionReady',
app_script,
)
self.assertIn('data-control-transport="wired_can"', html)
self.assertIn('data-control-transport="wireless_mqtt"', html)
self.assertIn('control_transport: state.controlTransport', app_script)
self.assertIn('const transportField = $("#vehicleTransportField", originalForm)', console_script)
self.assertIn('configStack.append(transportField)', console_script)
self.assertIn('.console-v2 .v2-transport-field .vehicle-transport-options', console_styles)
self.assertIn('button.disabled = false', app_script)
self.assertIn('button.setAttribute("aria-pressed", String(selected))', app_script)
self.assertNotIn(
'availableTransports.find((item) => item.state_configured)?.id',
app_script,
)
self.assertIn("hardwareButton.disabled = !hardwareExecutionReady", app_script)
@@ -484,7 +497,7 @@ operations:
self.assertIn("window.requestAnimationFrame(initSeatTwin)", app_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v120"', service_worker)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v128"', service_worker)
self.assertIn('"/self-evolution.js?v=seat-weighing-2"', service_worker)
self.assertIn('"/self-evolution.css?v=seat-weighing-5"', service_worker)
self.assertIn(
@@ -699,7 +712,7 @@ operations:
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-82", timeout=3
f"{self.base_url}/app.js?v=dual-seat-89", timeout=3
) as response:
app_script = response.read().decode("utf-8")
@@ -1032,10 +1045,103 @@ operations:
synchronize.assert_called_once_with(
"conv-e2e-physical-state",
"denza_n7",
control_transport="wired_can",
hardware_client_id=None,
hardware_access_bypass=True,
)
def test_wireless_passenger_snapshot_uses_mqtt_can0(self) -> None:
physical_state = {
"position": {
"longitudinal": {
"position_pct": 42,
"position_valid": True,
"motion_state": "stopped",
"fault": False,
}
},
"captured_at": "2026-08-18T00:00:00Z",
"complete": True,
"missing": [],
}
executor = SimpleNamespace(read_physical_state=lambda: dict(physical_state))
with (
patch.object(
self.service,
"vehicle_mqtt_settings",
SimpleNamespace(state_configured=True),
),
patch.object(
self.service._vehicle_mqtt_transport,
"bus",
return_value=object(),
) as mqtt_bus,
patch.object(
self.service,
"_new_hardware_executor",
return_value=executor,
) as new_executor,
patch.object(
self.service.hardware_execution,
"reserve",
side_effect=AssertionError(
"wireless cached state must not reserve the physical bench"
),
),
):
payload = self.service.synchronize_hardware_state(
"conv-e2e-wireless-seat-state",
"denza_n7",
control_transport="wireless_mqtt",
hardware_access_bypass=True,
)
bus_factory = new_executor.call_args.args[0]
bus_factory()
mqtt_bus.assert_called_once_with(channel=0, replay_latest=True)
self.assertTrue(new_executor.call_args.kwargs["remote_transport"])
self.assertEqual(payload["source"], "wireless_mqtt_ch0")
self.assertEqual(payload["control_transport"], "wireless_mqtt")
self.assertEqual(
payload["actuator_state"]["passenger"]["position"]["longitudinal"]["position_pct"],
42,
)
def test_wireless_vehicle_snapshot_does_not_reserve_physical_bench(self) -> None:
snapshot = SimpleNamespace(
feedback_states=(),
observed_ids=frozenset(),
captured_at="2026-08-18T00:00:00Z",
capture_sequence=17,
)
with (
patch.object(
self.service,
"vehicle_mqtt_settings",
SimpleNamespace(state_configured=True),
),
patch.object(
self.service._vehicle_mqtt_transport,
"snapshot",
return_value=snapshot,
),
patch.object(
self.service.hardware_execution,
"reserve",
side_effect=AssertionError(
"wireless cached state must not reserve the physical bench"
),
),
):
payload = self.service.synchronize_vehicle_state(
"conv-e2e-wireless-vehicle-state",
"denza_n7",
control_transport="wireless_mqtt",
hardware_access_bypass=True,
)
self.assertEqual(payload["source"], "wireless_mqtt_ch1")
self.assertEqual(payload["capture_sequence"], 17)
def test_gpt_http_failure_falls_back_without_returning_502(self) -> None:
original_api_key = self.service.openai.api_key
self.service.openai.api_key = "test-key"
+6 -2
View File
@@ -1,9 +1,11 @@
from __future__ import annotations
from contextlib import nullcontext
import os
from types import SimpleNamespace
import sys
import unittest
from contextlib import nullcontext
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
@@ -21,6 +23,8 @@ class Ch1VehicleSafetyTests(unittest.TestCase):
"COGNITIVE_AGENT_VEHICLE_CAN_REQUIRED_CHANNEL": "1",
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED": "true",
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN": "approved",
"COGNITIVE_AGENT_ZLGCAN_PYTHON": sys.executable,
"COGNITIVE_AGENT_ZLGCAN_DLL": str(Path(__file__).resolve()),
}
with patch.dict(os.environ, environment, clear=False):
return VehicleCanSettings.from_env(REPOSITORY_ROOT)
@@ -225,9 +225,18 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
"async function synchronizePhysicalSeatState() {",
"\nasync function loadAuthStatus()",
)
self.assertIn("state.runtime?.hardware?.configured", passenger_source)
self.assertIn('requestedTransport === "wireless_mqtt"', passenger_source)
self.assertIn("selectedVehicleTransport()?.state_configured", passenger_source)
self.assertIn("control_transport: requestedTransport", passenger_source)
self.assertIn("requestedTransport !== state.controlTransport", passenger_source)
self.assertIn('payload.source === "wireless_mqtt_ch0"', passenger_source)
self.assertIn("/api/hardware/seat-state", passenger_source)
self.assertIn("selectedVehicleTransport()?.state_configured", driver_source)
self.assertIn("control_transport: requestedTransport", driver_source)
self.assertIn("requestedTransport !== state.controlTransport", driver_source)
self.assertIn("/api/hardware/vehicle-state", driver_source)
self.assertIn('state.controlTransport === "wireless_mqtt"', combined_source)
self.assertIn("state.passengerStateSyncPromise", combined_source)
self.assertLess(
combined_source.index("synchronizePassengerSeatState()"),
combined_source.index("synchronizeDriverSeatState()"),
@@ -239,6 +248,18 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
)
self.assertNotIn("passenger: mergedActuatorState.driver", render_source)
def test_persistent_linux_monitor_uses_backend_selected_fast_polling(self) -> None:
interval_source = function_source(
self.app_source,
"function physicalStatePollIntervalMs() {",
"\nfunction refreshIcons",
)
self.assertIn("selectedVehicleTransport()?.state_sync?.poll_interval_ms", interval_source)
self.assertIn("Math.max(250", interval_source)
self.assertIn("DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000", self.app_source)
self.assertIn("}, physicalStatePollIntervalMs());", self.app_source)
self.assertIn("restartPhysicalStatePolling();", self.app_source)
def test_vehicle_selection_cannot_replay_unfiltered_state_in_ch1_mode(self) -> None:
select_source = function_source(
self.app_source,
+581
View File
@@ -0,0 +1,581 @@
from __future__ import annotations
import json
import time
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from cognitive_agent.infrastructure.can.gateway import CanFrame
from cognitive_agent.infrastructure.can.hardware import (
HardwareSafetyError,
VehicleMqttSettings,
)
from cognitive_agent.infrastructure.can.mqtt_transport import (
MQTT_CAN_SCHEMA,
MqttCanTransport,
parse_mqtt_can_payload,
)
class _PublishResult:
rc = 0
def wait_for_publish(self, timeout: float) -> bool:
return timeout > 0
class _FakeMqttClient:
def __init__(self) -> None:
self.on_connect = None
self.on_disconnect = None
self.on_message = None
self.subscriptions: list[tuple[str, int]] = []
self.published: list[tuple[str, str, int, bool]] = []
def reconnect_delay_set(self, **_kwargs) -> None:
return None
def connect_async(self, host: str, port: int, keepalive: int) -> None:
self.connection = (host, port, keepalive)
def loop_start(self) -> None:
self.on_connect(self, None, {}, 0, None)
def loop_stop(self) -> None:
return None
def disconnect(self) -> None:
return None
def subscribe(self, topic: str, qos: int):
self.subscriptions.append((topic, qos))
return (0, 1)
def publish(self, topic: str, payload: str, qos: int, retain: bool):
self.published.append((topic, payload, qos, retain))
return _PublishResult()
def emit(self, topic: str, payload: object) -> None:
message = SimpleNamespace(
topic=topic,
payload=json.dumps(payload).encode("utf-8"),
)
self.on_message(self, None, message)
def _settings():
values = {
"enabled": True,
"state_configured": True,
"configured": True,
"host": "127.0.0.1",
"port": 1883,
"condition_topic": "n7/n7-headunit/can/state",
"control_topic": "n7/can_control",
"qos": 1,
"keepalive_s": 30,
"username": None,
"password": None,
"tls_enabled": False,
"feedback_timeout_ms": 100,
}
values["public_info"] = lambda: {
"enabled": True,
"configured": True,
"state_configured": True,
"backend": "mqtt",
"condition_topic": "n7/n7-headunit/can/state",
"control_topic": "n7/can_control",
}
return SimpleNamespace(**values)
class MqttCanPayloadTests(unittest.TestCase):
def test_accepts_one_canonical_frame_and_common_receive_aliases(self) -> None:
canonical = parse_mqtt_can_payload(
json.dumps(
{
"schema": MQTT_CAN_SCHEMA,
"arbitration_id": 0x342,
"channel": 1,
"data_hex": "00 00 00 01 00 00 00 00",
"is_fd": True,
"bitrate_switch": True,
}
)
)[0]
alias = parse_mqtt_can_payload(
json.dumps({"canId": "0x4FA", "data": [0] * 8})
)[0]
self.assertEqual(canonical.arbitration_id, 0x342)
self.assertEqual(canonical.channel, 1)
self.assertTrue(canonical.is_fd)
self.assertEqual(alias.arbitration_id, 0x4FA)
def test_accepts_vehicle_n7_can_signals_envelope(self) -> None:
frames = parse_mqtt_can_payload(
json.dumps(
{
"schema": "n7-can-signals/v1",
"sequence": 335,
"timestamp": 1787037724090,
"online": True,
"signals": [
{
"channel": "can1",
"channelIndex": 1,
"canId": "0x388",
"canIdDecimal": 904,
"frameType": "fd",
"brs": True,
"dlc": 8,
"data": "00 00 00 02 01 00 59 00",
"bytes": [0, 0, 0, 2, 1, 0, 89, 0],
"timestamp": 1787037667665,
"direction": "rx",
}
],
"can1Vehicle": {
"parsed": {"speedKph": 0, "gear": 1}
},
}
)
)
self.assertEqual(len(frames), 1)
self.assertEqual(frames[0].arbitration_id, 0x388)
self.assertEqual(frames[0].channel, 1)
self.assertEqual(frames[0].data, bytes.fromhex("00 00 00 02 01 00 59 00"))
self.assertTrue(frames[0].is_fd)
self.assertTrue(frames[0].bitrate_switch)
self.assertEqual(frames[0].timestamp, 1787037667.665)
def test_accepts_nested_can1_vehicle_signals_when_top_level_is_absent(self) -> None:
frame = parse_mqtt_can_payload(
json.dumps(
{
"schema": "n7-can-signals/v1",
"can1Vehicle": {
"signals": [
{
"channel": "can1",
"canId": "0x42E",
"frameType": "fd",
"brs": True,
"bytes": [17, 100, 50, 3, 144, 2, 0, 198],
}
]
},
}
)
)[0]
self.assertEqual(frame.arbitration_id, 0x42E)
self.assertTrue(frame.is_fd)
def test_accepts_nested_can0_seat_signals_with_can0_default(self) -> None:
frame = parse_mqtt_can_payload(
json.dumps(
{
"schema": "n7-can-signals/v1",
"can0Seat": {
"signals": [
{
"canId": "0x302",
"frameType": "classic",
"bytes": [0] * 8,
}
]
},
}
)
)[0]
self.assertEqual(frame.arbitration_id, 0x302)
self.assertEqual(frame.channel, 0)
self.assertFalse(frame.is_fd)
def test_translates_seat_ota_info_responses_into_four_position_states(self) -> None:
values = {
1: (480 << 16) | 407,
2: (373 << 16) | 266,
3: (917 << 16) | 451,
4: (647 << 16) | 575,
5: 0x11,
6: 0x11,
7: 0x11,
8: 0x11,
}
signals = []
for selector, value in values.items():
sequence = 0x20 + selector
signals.extend(
(
{
"channel": "can0",
"canId": "0x6A0",
"frameType": "classic",
"direction": "tx",
"bytes": [0xC7, sequence, 0x60, selector, 0, 0, 0, 0],
},
{
"channel": "can0",
"canId": "0x6A2",
"frameType": "classic",
"direction": "rx",
"bytes": [0x7C, sequence, 0x60, 0, *value.to_bytes(4, "little")],
},
)
)
frames = parse_mqtt_can_payload(json.dumps({"signals": signals}))
positions = [frame for frame in frames if frame.arbitration_id == 0x302]
self.assertEqual([frame.data[0] for frame in positions], [0, 1, 2, 3])
self.assertEqual(
[int.from_bytes(frame.data[5:7], "little") for frame in positions],
[round(407 * 10_000 / 480), round(266 * 10_000 / 373),
round(451 * 10_000 / 917), round(575 * 10_000 / 647)],
)
self.assertTrue(all(frame.data[2] & (1 << 3) for frame in positions))
def test_rejects_can0_frames_outside_seat_firmware_format(self) -> None:
for label, frame in (
(
"fd",
{
"channel": "can0",
"canId": "0x302",
"frameType": "fd",
"bytes": [0] * 8,
},
),
(
"brs",
{
"channel": "can0",
"canId": "0x302",
"frameType": "classic",
"brs": True,
"bytes": [0] * 8,
},
),
(
"dlc",
{
"channel": "can0",
"canId": "0x302",
"frameType": "classic",
"bytes": [0] * 7,
},
),
):
with self.subTest(label=label), self.assertRaisesRegex(
ValueError, "classic CAN, no BRS, and DLC 8"
):
parse_mqtt_can_payload(json.dumps(frame))
def test_top_level_signals_can_mix_can0_and_can1_without_nested_duplicates(self) -> None:
frames = parse_mqtt_can_payload(
json.dumps(
{
"schema": "n7-can-signals/v1",
"signals": [
{"channel": "can0", "canId": "0x302", "bytes": [0] * 8},
{"channel": "can1", "canId": "0x342", "bytes": [0] * 8},
],
"can0Seat": {
"signals": [
{"channel": "can0", "canId": "0x302", "bytes": [0] * 8}
]
},
}
)
)
self.assertEqual(
[(frame.channel, frame.arbitration_id) for frame in frames],
[(0, 0x302), (1, 0x342)],
)
def test_rejects_decoded_state_invalid_channels_and_channel_conflicts(self) -> None:
with self.assertRaisesRegex(ValueError, "requires an ID and data"):
parse_mqtt_can_payload(json.dumps({"gear": "P", "speed_kmh": 0}))
with self.assertRaisesRegex(ValueError, "only CAN0 or CAN1"):
parse_mqtt_can_payload(
json.dumps({"id": 0x342, "channel": 2, "data": [0] * 8})
)
with self.assertRaisesRegex(ValueError, "declarations conflict"):
parse_mqtt_can_payload(
json.dumps(
{
"id": 0x342,
"channel": "can1",
"channelIndex": 0,
"data": [0] * 8,
}
)
)
def test_control_remains_locked_without_existing_hardware_approval(self) -> None:
settings = VehicleMqttSettings(
enabled=True,
approval_required=True,
approval_token="",
host="127.0.0.1",
port=1883,
condition_topic="n7/n7-headunit/can/state",
control_topic="n7/can_control",
qos=1,
keepalive_s=30,
username=None,
password=None,
tls_enabled=False,
feedback_timeout_ms=1_500,
)
with patch(
"cognitive_agent.infrastructure.can.hardware._module_available",
return_value=True,
):
self.assertTrue(settings.state_configured)
self.assertFalse(settings.configured)
with self.assertRaisesRegex(HardwareSafetyError, "not configured"):
settings.authorize(None)
class MqttCanTransportTests(unittest.TestCase):
def setUp(self) -> None:
self.client = _FakeMqttClient()
self.transport = MqttCanTransport(
_settings(),
client_factory=lambda: self.client,
)
self.transport.start()
def tearDown(self) -> None:
self.transport.close()
def test_condition_frames_use_existing_decoder_and_snapshot_contract(self) -> None:
self.client.emit(
"n7/n7-headunit/can/state",
{
"frames": [
{"id": 0x342, "data": [0, 0, 0, 1, 0, 0, 0, 0]},
{"id": 0x4EB, "data": [7, 0, 0, 0, 0, 0, 0, 0]},
{"id": 0x407, "data": [0] * 8},
{"id": 0x494, "data": [0] * 8},
{"id": 0x4FA, "data": [0] * 8},
{"id": 0x555, "data": [255] * 8},
]
},
)
snapshot = self.transport.snapshot()
self.assertEqual(
snapshot.observed_ids,
frozenset({0x342, 0x4EB, 0x407, 0x494, 0x4FA}),
)
self.assertTrue(
all(
state["verification"]["transport"] == "wireless_mqtt"
for state in snapshot.feedback_states
)
)
self.assertEqual(
self.client.subscriptions,
[("n7/n7-headunit/can/state", 1)],
)
def test_first_partial_live_vehicle_snapshot_does_not_wait_for_missing_ids(self) -> None:
self.client.emit(
"n7/n7-headunit/can/state",
{
"signals": [
{
"channel": "can1",
"canId": "0x388",
"frameType": "fd",
"brs": True,
"bytes": [0, 0, 0, 2, 1, 0, 89, 0],
}
]
},
)
started = time.perf_counter()
snapshot = self.transport.snapshot()
self.assertLess(time.perf_counter() - started, 0.05)
self.assertEqual(snapshot.observed_ids, frozenset({0x388}))
def test_stale_safety_signal_cannot_enter_snapshot_or_executor_bus(self) -> None:
bus = self.transport.bus(channel=1)
self.client.emit(
"n7/n7-headunit/can/state",
{
"schema": "n7-can-signals/v1",
"signals": [
{
"channel": "can1",
"channelIndex": 1,
"canId": "0x342",
"frameType": "fd",
"brs": True,
"data": "00 00 00 01 00 00 00 00",
"timestamp": round((time.time() - 60) * 1_000),
}
],
},
)
snapshot = self.transport.snapshot()
info = self.transport.public_info()["state_sync"]
self.assertEqual(info["capture_sequence"], 1)
self.assertEqual(info["stale_frames"], 1)
self.assertEqual(info["observed_frame_ids"], [])
self.assertEqual(snapshot.observed_ids, frozenset())
self.assertIsNone(bus.receive(0, include_fd=True))
def test_stale_non_safety_can1_frames_are_display_only(self) -> None:
bus = self.transport.bus(channel=1)
cached_timestamp = round((time.time() - 60) * 1_000)
self.client.emit(
"n7/n7-headunit/can/state",
{
"schema": "n7-can-signals/v1",
"signals": [
{
"channel": "can1",
"canId": frame_id,
"frameType": "fd",
"brs": True,
"data": data,
"timestamp": cached_timestamp,
}
for frame_id, data in (
("0x388", "00 00 00 02 01 00 59 00"),
("0x41A", "00 5C 85 08 20 42 C1 FD"),
("0x42E", "11 64 32 03 90 02 00 C6"),
("0x3FF", "5B 09 FF 0F 32 61 04 12"),
)
],
},
)
snapshot = self.transport.snapshot()
self.assertEqual(
snapshot.observed_ids,
frozenset({0x388, 0x3FF, 0x41A, 0x42E}),
)
for state in snapshot.feedback_states:
verification = state["verification"]
self.assertFalse(verification["fresh"])
self.assertTrue(verification["display_only"])
self.assertFalse(verification["control_eligible"])
self.assertGreater(verification["cache_age_ms"], 50_000)
self.assertIsNone(bus.receive(0, include_fd=True))
def test_bus_publishes_only_canonical_frame_contract(self) -> None:
bus = self.transport.bus()
sent = bus.send(
0x431,
bytes.fromhex("10 00 00 00 00 00 00 00"),
is_fd=True,
bitrate_switch=True,
)
self.assertEqual(sent.channel, 1)
self.assertEqual(len(self.client.published), 1)
topic, raw_payload, qos, retain = self.client.published[0]
payload = json.loads(raw_payload)
self.assertEqual(topic, "n7/can_control")
self.assertEqual(qos, 1)
self.assertFalse(retain)
self.assertEqual(payload["schema"], MQTT_CAN_SCHEMA)
self.assertEqual(payload["arbitration_id"], 0x431)
self.assertEqual(payload["data_hex"], "10 00 00 00 00 00 00 00")
self.assertTrue(payload["is_fd"])
self.assertTrue(payload["bitrate_switch"])
with self.assertRaisesRegex(Exception, "approved CAN1 control set"):
bus.send(0x555, bytes(8), is_fd=True)
self.assertEqual(len(self.client.published), 1)
def test_can0_bus_publishes_only_classic_passenger_seat_control(self) -> None:
bus = self.transport.bus(channel=0)
sent = bus.send(0x6A0, bytes.fromhex("C7 01 64 00 F4 01 00 00"))
payload = json.loads(self.client.published[-1][1])
self.assertEqual(sent.channel, 0)
self.assertEqual(payload["channel"], 0)
self.assertFalse(payload["is_fd"])
with self.assertRaisesRegex(Exception, "approved CAN0 control set"):
bus.send(0x431, bytes(8))
with self.assertRaisesRegex(Exception, "requires classic CAN"):
bus.send(0x6A0, bytes.fromhex("C7 01 64 00 F4 01 00 00"),
is_fd=True, bitrate_switch=True)
with self.assertRaisesRegex(Exception, "approved CAN0 control set"):
bus.send(0x300, bytes(8))
with self.assertRaisesRegex(Exception, "only STOP, MOVE, HEARTBEAT, or DYNAMIC"):
bus.send(0x6A0, bytes.fromhex("C7 02 61 00 00 00 00 00"))
with self.assertRaisesRegex(Exception, "target 0..1000"):
bus.send(0x6A0, bytes.fromhex("C7 03 64 00 E9 03 00 00"))
with self.assertRaisesRegex(Exception, "approved channel policy"):
self.transport.publish(
CanFrame(arbitration_id=0x431, data=bytes(8), channel=0)
)
def test_bus_receive_views_are_isolated_by_channel(self) -> None:
can0 = self.transport.bus(channel=0)
can1 = self.transport.bus(channel=1)
self.client.emit(
"n7/n7-headunit/can/state",
{
"signals": [
{"channel": "can0", "canId": "0x302", "bytes": [0] * 8},
{
"channel": "can1",
"canId": "0x342",
"frameType": "fd",
"bytes": [0] * 8,
},
]
},
)
self.assertEqual(can0.receive(0).arbitration_id, 0x302)
self.assertEqual(can1.receive(0, include_fd=True).arbitration_id, 0x342)
self.assertIsNone(can0.receive(0))
self.assertIsNone(can1.receive(0, include_fd=True))
def test_can0_status_bus_can_replay_latest_translated_ota_positions(self) -> None:
signals = []
for selector, value in {
1: (480 << 16) | 407,
2: (373 << 16) | 266,
3: (917 << 16) | 451,
4: (647 << 16) | 575,
5: 0x11,
6: 0x11,
7: 0x11,
8: 0x11,
}.items():
sequence = 0x40 + selector
signals.extend(
(
{"channel": "can0", "canId": "0x6A0", "bytes":
[0xC7, sequence, 0x60, selector, 0, 0, 0, 0]},
{"channel": "can0", "canId": "0x6A2", "bytes":
[0x7C, sequence, 0x60, 0, *value.to_bytes(4, "little")]},
)
)
self.client.emit("n7/n7-headunit/can/state", {"signals": signals})
bus = self.transport.bus(channel=0, replay_latest=True)
replayed = [bus.receive(0) for _ in range(4)]
self.assertEqual(
[frame.data[0] for frame in replayed if frame is not None],
[0, 1, 2, 3],
)
self.assertEqual(bus.receive(0).arbitration_id, 0x6A2)
self.assertIsNone(bus.receive(0))
self.assertEqual(
self.transport.public_info()["state_sync"]["observed_frame_ids_by_channel"]["0"],
["0x302", "0x6A2"],
)
if __name__ == "__main__":
unittest.main()
+218
View File
@@ -0,0 +1,218 @@
from __future__ import annotations
import unittest
from collections import deque
from types import SimpleNamespace
from cognitive_agent.application.mqtt_ota_seat_executor import (
MqttOtaSeatExecutor,
)
from cognitive_agent.infrastructure.can.gateway import CanFrame
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.closed = False
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)
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:
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 _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_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(
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_fails_before_publish(self) -> None:
bus = _FakeOtaBus([])
executor = MqttOtaSeatExecutor( # type: ignore[arg-type]
_StatusExecutor(),
lambda: bus,
)
plan = {
"spec": {
"commands": [
{
"operation_ref": "operation-1",
"message": "T2G_Seat_Thermal_Control_29E",
"signals": {},
}
]
}
}
report = executor.execute(
plan,
_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, [])
if __name__ == "__main__":
unittest.main()
+25
View File
@@ -54,6 +54,31 @@ class NativeCanFeedbackTests(unittest.TestCase):
self.assertFalse(feedback_matches("indoor_light:on", feedback))
self.assertTrue(feedback_matches("ambient_power:off", feedback))
def test_can1_matrix_rejects_reserved_ambient_color_values(self) -> None:
decoded = decode_feedback(
self.frame(0x42E, "11 64 32 03 90 02 00 C6")
)["domain_state"]["ambient_light"]
self.assertIsNone(decoded["front_color_index"])
self.assertIsNone(decoded["rear_color_index"])
def test_3ff_matrix_decodes_lighting_and_illuminance(self) -> None:
light = decode_feedback(
self.frame(0x3FF, "5B 09 FF 0F 32 61 04 12")
)["domain_state"]["interior_light"]
self.assertEqual(light["power"], "unknown")
self.assertEqual(light["footwell_power"], "off")
self.assertEqual(light["ambient_illuminance_lux"], 510)
def test_3f3_matrix_decodes_wall_and_ambient_switches(self) -> None:
decoded = decode_feedback(
self.frame(0x3F3, "00 00 00 00 00 00 00 84")
)["domain_state"]
self.assertEqual(decoded["interior_light"]["wall_light_power"], "on")
self.assertEqual(decoded["ambient_light"]["power"], "on")
def test_driver_seat_feedback_uses_0x4fa_not_0x42a(self) -> None:
passive_feedback = self.frame(0x4FA, "0980aa0000810000")
passive_driver = decode_feedback(passive_feedback)["actuator_state"]["driver"]
+209
View File
@@ -0,0 +1,209 @@
from __future__ import annotations
import threading
import time
import unittest
from collections import deque
from pathlib import Path
from types import SimpleNamespace
from cognitive_agent.application.seat_service import SeatDemoService
from cognitive_agent.application.vehicle_state_monitor import (
OBSERVABLE_IDS,
REQUIRED_UI_IDS,
VehicleStateMonitor,
VehicleStateSnapshot,
)
from cognitive_agent.infrastructure.can.gateway import (
CanFrame,
CanGatewayError,
ZlgChannelConfig,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
class _FakeBus:
def __init__(self, frames: list[CanFrame]) -> None:
self._frames = deque(frames)
self._condition = threading.Condition()
def push(self, frame: CanFrame) -> None:
with self._condition:
self._frames.append(frame)
self._condition.notify_all()
def receive(self, timeout_ms: int, *, include_fd: bool = False) -> CanFrame | None:
if not include_fd:
raise AssertionError("vehicle monitor must receive classic CAN and CAN FD")
with self._condition:
if not self._frames:
self._condition.wait(max(0, timeout_ms) / 1000)
return self._frames.popleft() if self._frames else None
class _FakeBridge:
def __init__(self, bus: _FakeBus) -> None:
self.bus = bus
self.closed = False
def channel_bus(self, channel: int) -> _FakeBus:
if channel != 1:
raise AssertionError("vehicle monitor must stay on CH1")
return self.bus
def close(self) -> None:
self.closed = True
class _FakeSettings:
channel = 1
feedback_timeout_ms = 100
def __init__(self, bus: _FakeBus) -> None:
self.bus = bus
self.bridge_instance = _FakeBridge(bus)
self.monitor_only: bool | None = None
def channel_config(self) -> ZlgChannelConfig:
return ZlgChannelConfig(channel=1, termination_enabled=False)
def bridge(
self,
channels: list[ZlgChannelConfig],
*,
monitor_only: bool = False,
) -> _FakeBridge:
if len(channels) != 1:
raise AssertionError("vehicle monitor must own exactly one channel")
self.monitor_only = monitor_only
if channels[0].receive_ids != tuple(sorted(OBSERVABLE_IDS)):
raise AssertionError("vehicle monitor must filter to observable identifiers")
return self.bridge_instance
def _frame(frame_id: int, data: bytes | None = None) -> CanFrame:
return CanFrame(
frame_id,
data or bytes(8),
channel=1,
is_fd=True,
bitrate_switch=True,
)
def _baseline_frames() -> list[CanFrame]:
gear_p = bytearray(8)
gear_p[3] = 1
return [
_frame(0x342, bytes(gear_p)),
_frame(0x4EB, bytes.fromhex("0700000000000000")),
_frame(0x407),
_frame(0x4FA),
_frame(0x494),
]
class VehicleStateMonitorTests(unittest.TestCase):
def test_persistent_monitor_returns_cached_state_without_reopening_adapter(self) -> None:
bus = _FakeBus(_baseline_frames())
settings = _FakeSettings(bus)
monitor = VehicleStateMonitor(settings, reconnect_delay_s=0.01) # type: ignore[arg-type]
self.addCleanup(monitor.close)
first = monitor.snapshot()
started = time.perf_counter()
second = monitor.snapshot()
cached_latency_s = time.perf_counter() - started
self.assertEqual(first.observed_ids, REQUIRED_UI_IDS)
self.assertEqual(second.observed_ids, REQUIRED_UI_IDS)
self.assertLess(cached_latency_s, 0.05)
self.assertTrue(settings.monitor_only)
self.assertEqual(monitor.public_info()["mode"], "persistent_monitor")
def test_monitor_updates_sequence_for_each_new_feedback_frame(self) -> None:
bus = _FakeBus(_baseline_frames())
monitor = VehicleStateMonitor(
_FakeSettings(bus), # type: ignore[arg-type]
reconnect_delay_s=0.01,
)
self.addCleanup(monitor.close)
baseline = monitor.snapshot()
gear_d = bytearray(8)
gear_d[3] = 4
bus.push(_frame(0x342, bytes(gear_d)))
deadline = time.monotonic() + 0.5
updated = baseline
while updated.capture_sequence == baseline.capture_sequence:
if time.monotonic() >= deadline:
self.fail("monitor did not consume the new gear frame")
time.sleep(0.005)
updated = monitor.snapshot()
gear = next(
state["vehicle_safety"]["gear"]
for state in updated.feedback_states
if state["frame_id"] == "0x342"
)
self.assertEqual(gear, "D")
def test_monitor_surfaces_adapter_open_failure(self) -> None:
settings = _FakeSettings(_FakeBus([]))
def fail_bridge(*_args: object, **_kwargs: object) -> _FakeBridge:
raise RuntimeError("adapter open failed")
settings.bridge = fail_bridge # type: ignore[method-assign]
monitor = VehicleStateMonitor(settings, reconnect_delay_s=0.01) # type: ignore[arg-type]
self.addCleanup(monitor.close)
with self.assertRaisesRegex(CanGatewayError, "adapter open failed"):
monitor.snapshot()
def test_service_merges_a_cached_monitor_snapshot(self) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
gear_state = {
"source": "physical_can_ch1",
"channel": 1,
"frame_id": "0x342",
"feedback_frame_id": "0x342",
"valid": True,
"captured_at": "2026-08-18T00:00:00Z",
"vehicle_safety": {"gear": "P"},
"verification": {
"authoritative": True,
"channel": 1,
"simulated": False,
"read_only": True,
"frame_id": "0x342",
"payload_size": 8,
},
}
service.vehicle_can_settings = SimpleNamespace(
state_configured=True,
backend="zlgcan-linux",
)
service._vehicle_state_monitor = SimpleNamespace(
snapshot=lambda: VehicleStateSnapshot(
feedback_states=(gear_state,),
observed_ids=REQUIRED_UI_IDS,
captured_at="2026-08-18T00:00:00Z",
capture_sequence=42,
)
)
result = service._synchronize_vehicle_state_once(
"conv-monitor-test",
"denza_n7",
)
self.assertEqual(result["capture_sequence"], 42)
self.assertEqual(result["domain_state"]["vehicle_status"]["gear"], "P")
self.assertTrue(result["complete"])
self.assertTrue(result["body_state_complete"])
if __name__ == "__main__":
unittest.main()
+143
View File
@@ -0,0 +1,143 @@
from __future__ import annotations
import io
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
from cognitive_agent.infrastructure.can.gateway import (
ZlgCanBridgeProcess,
ZlgChannelConfig,
)
from cognitive_agent.infrastructure.can.hardware import VehicleCanSettings
from cognitive_agent.infrastructure.can.zlgcan_linux_bridge import (
EXPECTED_ABI_SIZES,
Driver,
_acceptance_filters,
_verified_init,
abi_sizes,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
class LinuxZlgBridgeTests(unittest.TestCase):
def test_ctypes_layout_matches_vendor_packed_abi(self) -> None:
self.assertEqual(abi_sizes(), EXPECTED_ABI_SIZES)
def test_verified_60mhz_timing_matches_can_dev_baseline(self) -> None:
init = _verified_init(
{
"arbitration_bitrate": 500_000,
"data_bitrate": 2_000_000,
"can_fd_iso": True,
}
)
self.assertEqual(init.clk, 60_000_000)
self.assertEqual(init.mode, 0)
self.assertEqual(
(init.aset.tseg1, init.aset.tseg2, init.aset.sjw, init.aset.brp),
(14, 3, 2, 5),
)
self.assertEqual(
(init.dset.tseg1, init.dset.tseg2, init.dset.sjw, init.dset.brp),
(10, 2, 2, 1),
)
with self.assertRaisesRegex(ValueError, "locked"):
_verified_init(
{"arbitration_bitrate": 1_000_000, "data_bitrate": 4_000_000}
)
def test_passive_state_filter_uses_exact_standard_identifiers(self) -> None:
frame_ids = frozenset({0x342, 0x407, 0x4EB})
filters = _acceptance_filters({"receive_ids": frame_ids})
self.assertEqual(filters.size, 3 * 12)
self.assertEqual(
[
(
filters.table[index].type,
filters.table[index].sid,
filters.table[index].eid,
)
for index in range(3)
],
[(0, 0x342, 0x342), (0, 0x407, 0x407), (0, 0x4EB, 0x4EB)],
)
def test_channel_config_serializes_receive_filter_only_when_requested(self) -> None:
filtered = ZlgChannelConfig(channel=1, receive_ids=(0x342, 0x4EB))
unfiltered = ZlgChannelConfig(channel=1)
self.assertEqual(filtered.public()["receive_ids"], [0x342, 0x4EB])
self.assertNotIn("receive_ids", unfiltered.public())
def test_monitor_only_rejects_send_before_touching_sdk(self) -> None:
driver = object.__new__(Driver)
driver.monitor_only = True
driver.configs = {1: {"channel": 1, "frame_format": "can_fd"}}
driver.sdk = Mock()
with self.assertRaisesRegex(PermissionError, "monitor-only"):
driver.send(
1,
0x123,
bytes.fromhex("0001020304050607"),
is_fd=True,
bitrate_switch=True,
)
driver.sdk.VCI_TransmitFD.assert_not_called()
driver.sdk.VCI_Transmit.assert_not_called()
def test_linux_state_can_be_ready_while_transmit_stays_locked(self) -> None:
with tempfile.TemporaryDirectory() as directory:
library = Path(directory) / "libusbcanfd.so"
library.write_bytes(b"test")
environment = {
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "true",
"COGNITIVE_AGENT_VEHICLE_CAN_BACKEND": "zlgcan-linux",
"COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL": "1",
"COGNITIVE_AGENT_VEHICLE_CAN_REQUIRED_CHANNEL": "1",
"COGNITIVE_AGENT_ZLGCAN_PYTHON": sys.executable,
"COGNITIVE_AGENT_ZLGCAN_LINUX_LIBRARY": str(library),
}
with patch.dict(os.environ, environment, clear=True):
settings = VehicleCanSettings.from_env(REPOSITORY_ROOT)
self.assertEqual(settings.device_type, 33)
self.assertTrue(settings.state_configured)
self.assertFalse(settings.configured)
self.assertTrue(settings.bridge_script.name.endswith("linux_bridge.py"))
self.assertTrue(settings.public_info()["state_configured"])
def test_process_passes_monitor_and_trace_flags_to_bridge(self) -> None:
process = Mock()
process.stdout = io.StringIO('{"ok":true,"event":"ready"}\n')
process.stderr = io.StringIO()
process.stdin = io.StringIO()
process.poll.return_value = 0
with patch(
"cognitive_agent.infrastructure.can.gateway.subprocess.Popen",
return_value=process,
) as popen:
bridge = ZlgCanBridgeProcess(
python_executable=Path(sys.executable),
bridge_script=Path("bridge.py"),
dll_path=Path("libusbcanfd.so"),
device_type=33,
device_index=0,
channels=[ZlgChannelConfig(channel=1, termination_enabled=False)],
monitor_only=True,
trace_all=True,
)
command = popen.call_args.args[0]
self.assertIn("--monitor-only", command)
self.assertIn("--trace-all", command)
bridge.close()
if __name__ == "__main__":
unittest.main()