feat(n7web): enhance control workflows and status feedback

This commit is contained in:
Miayui
2026-09-16 12:52:07 +08:00
parent 84a3cd73bb
commit 0e4328f548
45 changed files with 7317 additions and 1001 deletions
@@ -55,6 +55,7 @@ from cognitive_agent.application.mode_scene_catalog import ( # noqa: E402
DEFAULT_N7_SCENE_SERVER_ID,
DEFAULT_N7_SCENE_SERVER_NAME,
ModeSceneCatalogPublisher,
ModeSceneMqttBurstPublisher,
build_mode_scene_catalog,
)
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
@@ -129,6 +130,7 @@ from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
VehicleMqttSettings,
)
from cognitive_agent.infrastructure.can.mqtt_transport import ( # noqa: E402
MQTT_MODE_SCENE_TOPIC,
MqttCanTransport,
)
from cognitive_agent.infrastructure.llm.openai_responses import ( # noqa: E402
@@ -440,6 +442,10 @@ class SeatDemoService:
"scene_count": 0,
"last_error": None,
}
self._mode_scene_mqtt_publisher = ModeSceneMqttBurstPublisher(
lambda scene: self._vehicle_mqtt_transport.publish_mode_scene(scene),
topic=MQTT_MODE_SCENE_TOPIC,
)
self.scene_templates = SceneTemplateRegistry(
repository_root / "config" / "scene_templates"
)
@@ -540,6 +546,51 @@ class SeatDemoService:
def mode_scene_catalog_status(self) -> dict[str, Any]:
return deepcopy(self._mode_scene_catalog_status)
def publish_saved_mode_scene(
self,
mode_id: str,
vehicle_id: str | None = None,
) -> dict[str, Any]:
"""Queue the just-saved card from the canonical scene projection.
Both a new user mode and a user override of a repository preset are
projected by the same registry. Reading the card back from that
projection guarantees that MQTT and ``/n7/scenes`` expose exactly the
same post-save JSON object.
"""
clean_mode_id = str(mode_id).strip()
if not clean_mode_id:
raise ValueError("mode scene id must not be empty")
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
catalog = self.mode_scene_catalog(resolved_vehicle_id)
catalog_publisher = self._mode_scene_catalog_publisher
if catalog_publisher is not None and (
catalog_publisher.vehicle_id != resolved_vehicle_id
or self._mode_scene_catalog_status.get("state") != "ready"
):
raise RuntimeError("mode scene catalog is not synchronized for this vehicle")
scene = next(
(
item
for item in catalog.get("scenes", [])
if isinstance(item, dict) and item.get("id") == clean_mode_id
),
None,
)
if scene is None:
raise KeyError(clean_mode_id)
return self._mode_scene_mqtt_publisher.queue(scene)
def publish_created_mode_scene(
self,
mode_id: str,
vehicle_id: str | None = None,
) -> dict[str, Any]:
"""Backward-compatible alias for callers predating update publishing."""
return self.publish_saved_mode_scene(mode_id, vehicle_id)
@property
def driver_only_ch1_execution_active(self) -> bool:
"""Return whether deployment policy authorizes only the CH1 vehicle path."""
@@ -697,6 +748,7 @@ class SeatDemoService:
"mode_profile_storage": "persistent_yaml",
"mode_profile_user_root": str(self.mode_profile_user_root),
"mode_scene_catalog": self.mode_scene_catalog_status(),
"mode_scene_mqtt": self._mode_scene_mqtt_publisher.snapshot(),
"preset_storage": "conversation_durable",
"inferred_level_range": [1, 3],
"motion_position_range_pct": [0, 100],
@@ -1326,11 +1378,13 @@ class SeatDemoService:
def start(self) -> None:
if self._vehicle_state_monitor is not None:
self._vehicle_state_monitor.start()
self._mode_scene_mqtt_publisher.start()
self._vehicle_mqtt_transport.start()
def close(self) -> None:
if self._vehicle_state_monitor is not None:
self._vehicle_state_monitor.close()
self._mode_scene_mqtt_publisher.close()
self._vehicle_mqtt_transport.close()
def publish_pet_message(self, payload: dict[str, str]) -> dict[str, Any]:
@@ -1338,6 +1392,30 @@ class SeatDemoService:
return self._vehicle_mqtt_transport.publish_pet_message(payload)
def publish_extension_toy_message(
self,
payload: dict[str, Any],
) -> dict[str, Any]:
"""Publish one NFC-detected extension capability announcement."""
return self._vehicle_mqtt_transport.publish_extension_toy_message(payload)
def set_extension_control_handler(
self,
handler: Callable[[bytes], None] | None,
) -> None:
"""Route ``extension/ctl`` payloads from the shared broker connection."""
self._vehicle_mqtt_transport.set_extension_control_handler(handler)
def set_extension_control_rejection_handler(
self,
handler: Callable[[str, str], None] | None,
) -> None:
"""Project broker-level ``extension/ctl`` rejections into the UI status."""
self._vehicle_mqtt_transport.set_extension_control_rejection_handler(handler)
def _synchronize_vehicle_state_once(
self,
conversation_id: str,