feat(n7web): sync N7 scene cards and optimize runtime
This commit is contained in:
@@ -2,14 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
@@ -50,6 +52,10 @@ from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime
|
||||
from cognitive_agent.application.mode_profile import ( # noqa: E402
|
||||
ModeProfile,
|
||||
ModeProfileRegistry,
|
||||
ModeTimeline,
|
||||
)
|
||||
from cognitive_agent.application.mode_scene_ingress import ( # noqa: E402
|
||||
ModeSceneIngressConsumer,
|
||||
)
|
||||
from cognitive_agent.application.mode_scene_catalog import ( # noqa: E402
|
||||
DEFAULT_N7_SCENE_SERVER_ID,
|
||||
@@ -130,7 +136,9 @@ from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
|
||||
VehicleMqttSettings,
|
||||
)
|
||||
from cognitive_agent.infrastructure.can.mqtt_transport import ( # noqa: E402
|
||||
MQTT_MODE_SCENE_INGRESS_TOPIC,
|
||||
MQTT_MODE_SCENE_TOPIC,
|
||||
MQTT_N7_CONTROL_TRANSFER_TOPIC,
|
||||
MqttCanTransport,
|
||||
)
|
||||
from cognitive_agent.infrastructure.llm.openai_responses import ( # noqa: E402
|
||||
@@ -446,6 +454,14 @@ class SeatDemoService:
|
||||
lambda scene: self._vehicle_mqtt_transport.publish_mode_scene(scene),
|
||||
topic=MQTT_MODE_SCENE_TOPIC,
|
||||
)
|
||||
self._mode_scene_ingress = ModeSceneIngressConsumer(
|
||||
self.import_n7_mode_scene,
|
||||
)
|
||||
self._vehicle_mqtt_transport.set_mode_scene_ingress_handler(
|
||||
self._mode_scene_ingress.submit
|
||||
)
|
||||
self._n7_transfer_lock = threading.RLock()
|
||||
self._n7_transfer_publications: dict[str, float] = {}
|
||||
self.scene_templates = SceneTemplateRegistry(
|
||||
repository_root / "config" / "scene_templates"
|
||||
)
|
||||
@@ -546,6 +562,18 @@ class SeatDemoService:
|
||||
def mode_scene_catalog_status(self) -> dict[str, Any]:
|
||||
return deepcopy(self._mode_scene_catalog_status)
|
||||
|
||||
def mode_profile_sync_status(self) -> dict[str, Any]:
|
||||
"""Return the lightweight state used by the browser's scene watcher."""
|
||||
|
||||
catalog = self.mode_scene_catalog_status()
|
||||
return {
|
||||
"catalog_revision": catalog.get("catalog_revision"),
|
||||
"scene_count": int(catalog.get("scene_count") or 0),
|
||||
"catalog_state": str(catalog.get("state") or "unknown"),
|
||||
"catalog_error": catalog.get("last_error"),
|
||||
"mode_scene_ingress": self._mode_scene_ingress.snapshot(),
|
||||
}
|
||||
|
||||
def publish_saved_mode_scene(
|
||||
self,
|
||||
mode_id: str,
|
||||
@@ -591,6 +619,142 @@ class SeatDemoService:
|
||||
|
||||
return self.publish_saved_mode_scene(mode_id, vehicle_id)
|
||||
|
||||
@staticmethod
|
||||
def _imported_scene_function_configs(
|
||||
scene: dict[str, Any],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Retain only local peripheral metadata needed for later authorization."""
|
||||
|
||||
extension_operations: list[dict[str, Any]] = []
|
||||
for index, action in enumerate(scene.get("actions", [])):
|
||||
if (
|
||||
not isinstance(action, dict)
|
||||
or action.get("domain") != "self_extension"
|
||||
or action.get("capability") != "infrared.emit"
|
||||
):
|
||||
continue
|
||||
target = action.get("target")
|
||||
target_document = (
|
||||
deepcopy(target)
|
||||
if isinstance(target, dict)
|
||||
else {"device": str(action.get("target_scope") or "")}
|
||||
)
|
||||
extension_operations.append(
|
||||
{
|
||||
"id": str(action.get("operation_ref") or f"n7-ir-{index + 1}"),
|
||||
"domain": "self_extension",
|
||||
"capability": "infrared.emit",
|
||||
"target_scope": str(action.get("target_scope") or ""),
|
||||
"target": target_document,
|
||||
"operation": str(action.get("operation") or "emit"),
|
||||
"parameters": deepcopy(dict(action.get("parameters") or {})),
|
||||
"parse_source": "from_n7_scene",
|
||||
}
|
||||
)
|
||||
if not extension_operations:
|
||||
return {}
|
||||
return {
|
||||
"self_extension": {
|
||||
"apiVersion": "cognitive-agent.io/v1alpha1",
|
||||
"kind": "FunctionConfig",
|
||||
"metadata": {
|
||||
"id": f"from-n7-{scene['id']}-self-extension",
|
||||
"name": f"{scene['name']} imported infrared actions",
|
||||
"labels": {"source_topic": MQTT_MODE_SCENE_INGRESS_TOPIC},
|
||||
},
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"operations": extension_operations,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
def import_n7_mode_scene(
|
||||
self,
|
||||
scene: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Upsert one validated N7 card without publishing it back to the broker."""
|
||||
|
||||
document = deepcopy(dict(scene))
|
||||
actions = document.get("actions")
|
||||
if not isinstance(actions, list) or not actions:
|
||||
raise ValueError("from_n7/scenes card must contain executable actions")
|
||||
operations = [
|
||||
{
|
||||
"domain": str(action["domain"]),
|
||||
"capability": str(action["capability"]),
|
||||
"target_scope": str(action["target_scope"]),
|
||||
"operation": str(action["operation"]),
|
||||
"parameters": deepcopy(dict(action["parameters"])),
|
||||
"reason": (
|
||||
f"由 {MQTT_MODE_SCENE_INGRESS_TOPIC} 导入的模式动作。"
|
||||
),
|
||||
}
|
||||
for action in actions
|
||||
]
|
||||
timeline = None
|
||||
raw_timeline = document.get("timeline")
|
||||
if raw_timeline is not None:
|
||||
if not isinstance(raw_timeline, dict):
|
||||
raise ValueError("from_n7/scenes timeline must be an object")
|
||||
timeline = ModeTimeline.from_dict(raw_timeline)
|
||||
command_flow = {
|
||||
"schema": "cognitive-agent-mode-command-flow/v1",
|
||||
"source_topic": MQTT_MODE_SCENE_INGRESS_TOPIC,
|
||||
"imported_at": _now(),
|
||||
"source_scene": document,
|
||||
"function_configs": self._imported_scene_function_configs(document),
|
||||
# The execution path recompiles the canonical ModeOperation list
|
||||
# against this PC's capability registry. This marker makes the
|
||||
# card directly executable while avoiding trust in foreign CAN bytes.
|
||||
"plans": {
|
||||
"from_n7_scene": {
|
||||
"spec": {
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": str(
|
||||
action.get("operation_ref") or f"action-{index + 1}"
|
||||
),
|
||||
"at_ms": int(action.get("at_ms", 0)),
|
||||
}
|
||||
for index, action in enumerate(actions)
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"execution_steps": [],
|
||||
}
|
||||
scope = document.get("scope") or {}
|
||||
vehicle_ids = scope.get("vehicle_ids", []) if isinstance(scope, dict) else []
|
||||
common = bool(scope.get("common", not vehicle_ids)) if isinstance(scope, dict) else False
|
||||
resolved_vehicle_id = (
|
||||
None
|
||||
if common and not vehicle_ids
|
||||
else self.vehicle_profiles.default().id
|
||||
)
|
||||
saved = self.mode_profiles.save_mode(
|
||||
str(document["name"]),
|
||||
operations,
|
||||
mode_id=str(document["id"]),
|
||||
aliases=[str(alias) for alias in document.get("aliases", [])],
|
||||
vehicle_id=resolved_vehicle_id,
|
||||
description=str(document.get("description") or "由 N7 同步的模式卡片。"),
|
||||
icon=str(document.get("icon") or "radio-tower"),
|
||||
instruction=f"进入{document['name']}",
|
||||
command_flow=command_flow,
|
||||
timeline=timeline,
|
||||
sync_origin=MQTT_MODE_SCENE_INGRESS_TOPIC,
|
||||
upstream_origin=str(document.get("origin") or ""),
|
||||
)
|
||||
return {
|
||||
"status": "imported",
|
||||
"scene_id": saved.id,
|
||||
"scene_name": saved.name,
|
||||
"source_topic": MQTT_MODE_SCENE_INGRESS_TOPIC,
|
||||
"profile_revision": self.mode_profiles.profile_revision(saved),
|
||||
"storage": "persistent_yaml",
|
||||
}
|
||||
|
||||
@property
|
||||
def driver_only_ch1_execution_active(self) -> bool:
|
||||
"""Return whether deployment policy authorizes only the CH1 vehicle path."""
|
||||
@@ -646,6 +810,7 @@ class SeatDemoService:
|
||||
"last_error": None,
|
||||
}
|
||||
vehicle_mqtt_info = self._vehicle_mqtt_transport.public_info()
|
||||
vehicle_mqtt_info["mode_scene_import"] = self._mode_scene_ingress.snapshot()
|
||||
return {
|
||||
"service": "cabin-cognitive-orchestrator",
|
||||
"domain": "cabin",
|
||||
@@ -807,6 +972,207 @@ class SeatDemoService:
|
||||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||||
return self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
|
||||
|
||||
@staticmethod
|
||||
def _fallback_scene_action(
|
||||
operation: Any,
|
||||
*,
|
||||
index: int,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "control",
|
||||
"at_ms": 0,
|
||||
"domain": operation.domain,
|
||||
"capability": operation.capability,
|
||||
"target_scope": operation.target_scope,
|
||||
"operation": operation.operation,
|
||||
"parameters": deepcopy(operation.parameters),
|
||||
"operation_ref": f"action-{index + 1}",
|
||||
}
|
||||
|
||||
def _mode_requests_and_n7_transfers(
|
||||
self,
|
||||
profile: ModeProfile,
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""Separate locally understood operations from N7-only capabilities."""
|
||||
|
||||
requests = self.mode_profiles.requests_for_profiles([profile])
|
||||
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
|
||||
return requests, []
|
||||
source_scene = (
|
||||
profile.command_flow.get("source_scene")
|
||||
if isinstance(profile.command_flow, dict)
|
||||
else None
|
||||
)
|
||||
source_actions = (
|
||||
source_scene.get("actions")
|
||||
if isinstance(source_scene, dict)
|
||||
and isinstance(source_scene.get("actions"), list)
|
||||
else []
|
||||
)
|
||||
local_requests: list[dict[str, Any]] = []
|
||||
transfer_actions: list[dict[str, Any]] = []
|
||||
for index, (operation, request) in enumerate(
|
||||
zip(profile.operations, requests, strict=True)
|
||||
):
|
||||
if (
|
||||
operation.domain == "self_extension"
|
||||
and operation.capability == "infrared.emit"
|
||||
):
|
||||
# The server's durable NFC allowlist owns this local TCP path.
|
||||
# Authorization failures must not be converted into N7 retries.
|
||||
continue
|
||||
registered = self.model_plan_adapter.capabilities.get(
|
||||
operation.capability
|
||||
)
|
||||
native_candidate = {
|
||||
"domain": operation.domain,
|
||||
"capability": operation.capability,
|
||||
"target": operation.target_scope,
|
||||
"parameters": deepcopy(operation.parameters),
|
||||
"operation": operation.operation,
|
||||
}
|
||||
if registered is not None or self.native_can_runtime.supports(
|
||||
native_candidate
|
||||
):
|
||||
local_requests.append(request)
|
||||
continue
|
||||
raw_action = (
|
||||
source_actions[index]
|
||||
if index < len(source_actions)
|
||||
and isinstance(source_actions[index], dict)
|
||||
else None
|
||||
)
|
||||
action = (
|
||||
deepcopy(raw_action)
|
||||
if raw_action is not None
|
||||
else self._fallback_scene_action(operation, index=index)
|
||||
)
|
||||
transfer_actions.append(action)
|
||||
return local_requests, transfer_actions
|
||||
|
||||
@staticmethod
|
||||
def _has_local_mode_peripheral(profile: ModeProfile) -> bool:
|
||||
return any(
|
||||
operation.domain == "self_extension"
|
||||
and operation.capability == "infrared.emit"
|
||||
for operation in profile.operations
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _n7_transfer_deferred_requests(
|
||||
actions: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"domain": str(action.get("domain") or ""),
|
||||
"capability": str(action.get("capability") or ""),
|
||||
"target_scope": str(action.get("target_scope") or ""),
|
||||
"desired_change": str(action.get("operation") or "set"),
|
||||
"parameters": deepcopy(dict(action.get("parameters") or {})),
|
||||
"reason": "当前 PC 未注册该能力,将由 N7 端继续处理。",
|
||||
"execution_backend": "n7_vehicle_transfer",
|
||||
"rejection_code": "PC_CAPABILITY_UNAVAILABLE",
|
||||
"transport_submitted": False,
|
||||
}
|
||||
for action in actions
|
||||
]
|
||||
|
||||
def _finalize_n7_control_transfer(
|
||||
self,
|
||||
result: dict[str, Any],
|
||||
*,
|
||||
execution_mode: str,
|
||||
local_preview: bool,
|
||||
) -> dict[str, Any]:
|
||||
"""Forward only N7-originated actions never submitted by this PC."""
|
||||
|
||||
actions = result.get("n7_transfer_candidates")
|
||||
scene = result.get("n7_source_scene")
|
||||
if (
|
||||
execution_mode != "hardware"
|
||||
or local_preview
|
||||
or not isinstance(actions, list)
|
||||
or not actions
|
||||
or not isinstance(scene, dict)
|
||||
):
|
||||
return result
|
||||
if str(result.get("status") or "") not in {
|
||||
"succeeded",
|
||||
"partially_succeeded",
|
||||
"capability_unavailable",
|
||||
}:
|
||||
return result
|
||||
if result.get("execution_outcome") == "failed_or_blocked":
|
||||
return result
|
||||
request_id = str(result.get("request_id") or "").strip()
|
||||
scene_id = str(scene.get("id") or "").strip()
|
||||
scene_name = str(scene.get("name") or "").strip()
|
||||
if not request_id or not scene_id or not scene_name:
|
||||
return result
|
||||
transfer_actions = [deepcopy(action) for action in actions if isinstance(action, dict)]
|
||||
if not transfer_actions:
|
||||
return result
|
||||
payload = {
|
||||
"scene_id": scene_id,
|
||||
"scene_name": scene_name,
|
||||
"request_id": request_id,
|
||||
"operation_count": len(transfer_actions),
|
||||
"actions": transfer_actions,
|
||||
}
|
||||
digest = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
publication_key = f"{request_id}:{hashlib.sha256(digest.encode('utf-8')).hexdigest()}"
|
||||
now = time.monotonic()
|
||||
with self._n7_transfer_lock:
|
||||
cutoff = now - 300.0
|
||||
self._n7_transfer_publications = {
|
||||
key: published_at
|
||||
for key, published_at in self._n7_transfer_publications.items()
|
||||
if published_at >= cutoff
|
||||
}
|
||||
if publication_key in self._n7_transfer_publications:
|
||||
result["n7_control_transfer"] = {
|
||||
"state": "duplicate_ignored",
|
||||
"topic": MQTT_N7_CONTROL_TRANSFER_TOPIC,
|
||||
"scene_id": scene_id,
|
||||
"operation_count": len(transfer_actions),
|
||||
}
|
||||
return result
|
||||
try:
|
||||
publication = self._vehicle_mqtt_transport.publish_n7_control_transfer(
|
||||
payload
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - local execution result remains valid
|
||||
result["n7_control_transfer"] = {
|
||||
"state": "publish_failed",
|
||||
"topic": MQTT_N7_CONTROL_TRANSFER_TOPIC,
|
||||
"scene_id": scene_id,
|
||||
"operation_count": len(transfer_actions),
|
||||
"last_error": str(exc)[:240],
|
||||
}
|
||||
return result
|
||||
with self._n7_transfer_lock:
|
||||
self._n7_transfer_publications[publication_key] = now
|
||||
result["n7_control_transfer"] = {
|
||||
"state": "published",
|
||||
"scene_id": scene_id,
|
||||
"operation_count": len(transfer_actions),
|
||||
"payload": payload,
|
||||
**dict(publication),
|
||||
}
|
||||
result["decision_summary"] = [
|
||||
*list(result.get("decision_summary") or []),
|
||||
(
|
||||
f"当前 PC 未注册的 {len(transfer_actions)} 条 N7 来源动作已发送至 "
|
||||
f"{MQTT_N7_CONTROL_TRANSFER_TOPIC};PC 已提交或被安全门禁拦截的动作未转发。"
|
||||
),
|
||||
]
|
||||
return result
|
||||
|
||||
def execute_mode_profile_card(
|
||||
self,
|
||||
mode_id: str,
|
||||
@@ -1183,6 +1549,10 @@ class SeatDemoService:
|
||||
instruction=clean_instruction,
|
||||
command_flow=command_flow,
|
||||
timeline=profile.timeline if profile is not None else None,
|
||||
sync_origin=profile.sync_origin if profile is not None else "",
|
||||
upstream_origin=(
|
||||
profile.upstream_origin if profile is not None else ""
|
||||
),
|
||||
expected_revision=draft_existing_mode_revision or None,
|
||||
create_only=not bool(clean_mode_id),
|
||||
)
|
||||
@@ -1379,6 +1749,7 @@ class SeatDemoService:
|
||||
if self._vehicle_state_monitor is not None:
|
||||
self._vehicle_state_monitor.start()
|
||||
self._mode_scene_mqtt_publisher.start()
|
||||
self._mode_scene_ingress.start()
|
||||
self._vehicle_mqtt_transport.start()
|
||||
|
||||
def close(self) -> None:
|
||||
@@ -1386,6 +1757,7 @@ class SeatDemoService:
|
||||
self._vehicle_state_monitor.close()
|
||||
self._mode_scene_mqtt_publisher.close()
|
||||
self._vehicle_mqtt_transport.close()
|
||||
self._mode_scene_ingress.close()
|
||||
|
||||
def publish_pet_message(self, payload: dict[str, str]) -> dict[str, Any]:
|
||||
"""Publish one NFC-authorized pet card through the shared vehicle broker."""
|
||||
@@ -2416,7 +2788,7 @@ class SeatDemoService:
|
||||
# The head unit owns both physical CAN channels in wireless mode.
|
||||
# Local T2G/ZLG hardware access, firmware profiles and the local
|
||||
# physical-bench lease are unrelated to MQTT relay execution.
|
||||
return self._process_once(
|
||||
result = self._process_once(
|
||||
utterance,
|
||||
provider,
|
||||
conversation_id,
|
||||
@@ -2438,11 +2810,16 @@ class SeatDemoService:
|
||||
mode_profile_draft_context=_mode_profile_draft_context,
|
||||
cloud_instruction_context=_cloud_instruction_context,
|
||||
)
|
||||
return self._finalize_n7_control_transfer(
|
||||
result,
|
||||
execution_mode=execution_mode,
|
||||
local_preview=_local_preview,
|
||||
)
|
||||
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
|
||||
control_transport
|
||||
):
|
||||
with self.hardware_execution.reserve():
|
||||
return self._process_once(
|
||||
result = self._process_once(
|
||||
utterance,
|
||||
provider,
|
||||
conversation_id,
|
||||
@@ -2464,10 +2841,15 @@ class SeatDemoService:
|
||||
mode_profile_draft_context=_mode_profile_draft_context,
|
||||
cloud_instruction_context=_cloud_instruction_context,
|
||||
)
|
||||
return self._finalize_n7_control_transfer(
|
||||
result,
|
||||
execution_mode=execution_mode,
|
||||
local_preview=_local_preview,
|
||||
)
|
||||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||||
with self.hardware_execution.reserve():
|
||||
with self.hardware_access.use(authorized_client_id):
|
||||
return self._process_once(
|
||||
result = self._process_once(
|
||||
utterance,
|
||||
provider,
|
||||
conversation_id,
|
||||
@@ -2489,6 +2871,11 @@ class SeatDemoService:
|
||||
mode_profile_draft_context=_mode_profile_draft_context,
|
||||
cloud_instruction_context=_cloud_instruction_context,
|
||||
)
|
||||
return self._finalize_n7_control_transfer(
|
||||
result,
|
||||
execution_mode=execution_mode,
|
||||
local_preview=_local_preview,
|
||||
)
|
||||
|
||||
def _process_once(
|
||||
self,
|
||||
@@ -2768,8 +3155,8 @@ class SeatDemoService:
|
||||
traces,
|
||||
)
|
||||
if direct_mode_profile is not None:
|
||||
mode_requests = self.mode_profiles.requests_for_profiles(
|
||||
[direct_mode_profile]
|
||||
mode_requests, n7_transfer_actions = (
|
||||
self._mode_requests_and_n7_transfers(direct_mode_profile)
|
||||
)
|
||||
detected_domains = list(
|
||||
dict.fromkeys(
|
||||
@@ -2814,6 +3201,28 @@ class SeatDemoService:
|
||||
*list(parsed.get("native_can_actions", [])),
|
||||
*saved_native_actions,
|
||||
]
|
||||
if n7_transfer_actions:
|
||||
parsed["deferred_requests"] = [
|
||||
*list(parsed.get("deferred_requests", [])),
|
||||
*self._n7_transfer_deferred_requests(
|
||||
n7_transfer_actions
|
||||
),
|
||||
]
|
||||
parsed["_n7_transfer_actions"] = n7_transfer_actions
|
||||
parsed["_n7_scene_profile"] = {
|
||||
"id": direct_mode_profile.id,
|
||||
"name": direct_mode_profile.name,
|
||||
}
|
||||
parsed["detected_domains"] = self._merge_domains(
|
||||
list(parsed.get("detected_domains", [])),
|
||||
[
|
||||
str(action.get("domain") or "")
|
||||
for action in n7_transfer_actions
|
||||
if str(action.get("domain") or "")
|
||||
],
|
||||
)
|
||||
if self._has_local_mode_peripheral(direct_mode_profile):
|
||||
parsed["_saved_mode_local_peripheral"] = True
|
||||
provider_used = "saved_mode_command_flow"
|
||||
provider_notice = None
|
||||
elif direct_can_batch is not None:
|
||||
@@ -2951,6 +3360,12 @@ class SeatDemoService:
|
||||
)
|
||||
),
|
||||
)
|
||||
n7_transfer_actions = parsed.get("_n7_transfer_actions", [])
|
||||
n7_scene_profile = parsed.get("_n7_scene_profile")
|
||||
if isinstance(n7_transfer_actions, list) and n7_transfer_actions:
|
||||
result["n7_transfer_candidates"] = deepcopy(n7_transfer_actions)
|
||||
if isinstance(n7_scene_profile, dict):
|
||||
result["n7_source_scene"] = deepcopy(n7_scene_profile)
|
||||
result["parsed_control_signatures"] = [
|
||||
list(self._control_signature(action))
|
||||
for action in [
|
||||
@@ -2969,7 +3384,10 @@ class SeatDemoService:
|
||||
)
|
||||
)
|
||||
if (
|
||||
allow_empty_control_result
|
||||
(
|
||||
allow_empty_control_result
|
||||
or parsed.get("_saved_mode_local_peripheral") is True
|
||||
)
|
||||
and not has_control
|
||||
and not parsed.get("ambiguities")
|
||||
):
|
||||
@@ -3019,7 +3437,11 @@ class SeatDemoService:
|
||||
"hardware mode refuses virtual or unverified vehicle actions: "
|
||||
+ ", ".join(unsupported)
|
||||
)
|
||||
if not parsed.get("actions") and not parsed.get("native_can_actions"):
|
||||
if (
|
||||
not parsed.get("actions")
|
||||
and not parsed.get("native_can_actions")
|
||||
and not parsed.get("_n7_transfer_actions")
|
||||
):
|
||||
raise HardwareSafetyError(
|
||||
"hardware mode requires at least one verified seat or vehicle CAN operation"
|
||||
)
|
||||
@@ -4070,7 +4492,10 @@ class SeatDemoService:
|
||||
if safety_ambiguities:
|
||||
return parsed
|
||||
|
||||
mode_requests = self.mode_profiles.requests_for_profiles(matched_modes)
|
||||
matched_profile = matched_modes[0]
|
||||
mode_requests, n7_transfer_actions = (
|
||||
self._mode_requests_and_n7_transfers(matched_profile)
|
||||
)
|
||||
detected_domains = []
|
||||
for request in mode_requests:
|
||||
if request["domain"] not in detected_domains:
|
||||
@@ -4136,6 +4561,26 @@ class SeatDemoService:
|
||||
float(mode_parsed.get("confidence", 0)),
|
||||
float(parsed.get("confidence", 0)),
|
||||
)
|
||||
if n7_transfer_actions:
|
||||
merged["deferred_requests"] = self._merge_unique(
|
||||
list(merged.get("deferred_requests", [])),
|
||||
self._n7_transfer_deferred_requests(n7_transfer_actions),
|
||||
)
|
||||
merged["_n7_transfer_actions"] = deepcopy(n7_transfer_actions)
|
||||
merged["_n7_scene_profile"] = {
|
||||
"id": matched_profile.id,
|
||||
"name": matched_profile.name,
|
||||
}
|
||||
merged["detected_domains"] = self._merge_domains(
|
||||
list(merged.get("detected_domains", [])),
|
||||
[
|
||||
str(action.get("domain") or "")
|
||||
for action in n7_transfer_actions
|
||||
if str(action.get("domain") or "")
|
||||
],
|
||||
)
|
||||
if self._has_local_mode_peripheral(matched_profile):
|
||||
merged["_saved_mode_local_peripheral"] = True
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user