This commit is contained in:
vee
2026-08-14 15:01:29 +08:00
parent 6d768aaef4
commit f77dda70fe
20 changed files with 1125 additions and 122 deletions
@@ -96,6 +96,31 @@ class CalibrationService:
with self._lock:
return json.loads(json.dumps(self.state, ensure_ascii=False))
def reset_virtual_session(self) -> dict[str, Any]:
"""Return a simulation run to its safe initial state without touching hardware."""
with self._lock:
mode = str(self.state["transport"].get("mode", "hardware"))
if mode != "simulation":
raise ApiError("真实台架会话不能通过虚拟重启接口复位", 409)
self.video.stop()
if self._mqtt:
self._mqtt.close()
self._mqtt = None
if self._transport:
self._transport.close()
self._transport = None
with self._lock:
self._pending.clear()
self._saved_job = -1
self.state = self._initial_state()
self.state["transport"].update(
{"mode": "simulation", "state": "disconnected", "detail": "虚拟运行已恢复初始状态"}
)
self.state["operation"]["label"] = "等待连接标定仿真台架"
self._touch()
return self.snapshot()
def wait_for_version(self, after: int, timeout: float = 15.0) -> dict[str, Any]:
deadline = time.monotonic() + timeout
while not self._closing.is_set() and time.monotonic() < deadline:
+45 -20
View File
@@ -363,22 +363,33 @@ class ModeProfile:
class ModeProfileRegistry:
"""Load, match, and persist long-term cabin mode profiles."""
"""Load preset profiles and optionally keep user profiles in a session overlay."""
def __init__(self, profile_root: Path):
def __init__(self, profile_root: Path, user_root: Path | None = None):
self.profile_root = Path(profile_root)
self.user_root = Path(user_root) if user_root is not None else self.profile_root
self.session_user_profiles = user_root is not None
self._profiles: list[ModeProfile] = []
self.reload()
def reload(self) -> None:
if not self.profile_root.exists():
self._profiles = []
return
self._profiles = [
ModeProfile.from_yaml(path, self.profile_root)
for path in sorted(self.profile_root.rglob("*.yaml"))
if path.is_file()
]
profiles: list[ModeProfile] = []
if self.profile_root.exists():
for path in sorted(self.profile_root.rglob("*.yaml")):
if not path.is_file():
continue
if self.session_user_profiles and _is_user_profile_path(
path, self.profile_root
):
continue
profiles.append(ModeProfile.from_yaml(path, self.profile_root))
if self.session_user_profiles and self.user_root.exists():
profiles.extend(
ModeProfile.from_yaml(path, self.user_root)
for path in sorted(self.user_root.rglob("*.yaml"))
if path.is_file()
)
self._profiles = profiles
def profiles(self) -> list[ModeProfile]:
return list(self._profiles)
@@ -459,9 +470,9 @@ class ModeProfileRegistry:
],
}
path = (
self.profile_root / "vehicles" / vehicle_id / "user" / f"{profile_id}.yaml"
self.user_root / "vehicles" / vehicle_id / "user" / f"{profile_id}.yaml"
if vehicle_id
else self.profile_root / "common" / "user" / f"{profile_id}.yaml"
else self.user_root / "common" / "user" / f"{profile_id}.yaml"
)
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
@@ -471,7 +482,7 @@ class ModeProfileRegistry:
)
temporary.replace(path)
self.reload()
return ModeProfile.from_yaml(path, self.profile_root)
return ModeProfile.from_yaml(path, self.user_root)
def delete_user_mode(
self,
@@ -495,11 +506,15 @@ class ModeProfileRegistry:
raise PermissionError("preset mode profiles cannot be deleted")
path = Path(profile.source).resolve()
profile_root = self.profile_root.resolve()
try:
relative_parts = path.relative_to(profile_root).parts
except ValueError as exc:
raise PermissionError("mode profile is outside the configured registry") from exc
relative_parts = None
for root in {self.profile_root.resolve(), self.user_root.resolve()}:
try:
relative_parts = path.relative_to(root).parts
break
except ValueError:
continue
if relative_parts is None:
raise PermissionError("mode profile is outside the configured registry")
if "user" not in relative_parts or path.suffix.lower() != ".yaml":
raise PermissionError("only user-defined mode profiles can be deleted")
if not path.is_file():
@@ -532,7 +547,7 @@ class ModeProfileRegistry:
document = _mode_profile_document(profile, merged_operations)
_replace_yaml(path, document)
self.reload()
return ModeProfile.from_yaml(path, self.profile_root)
return ModeProfile.from_yaml(path, self._root_for_path(path))
def restore_initial_mode(self, profile: ModeProfile) -> ModeProfile:
path = Path(profile.source)
@@ -544,7 +559,17 @@ class ModeProfileRegistry:
temporary.write_text(backup_path.read_text(encoding="utf-8"), encoding="utf-8")
temporary.replace(path)
self.reload()
return ModeProfile.from_yaml(path, self.profile_root)
return ModeProfile.from_yaml(path, self._root_for_path(path))
def _root_for_path(self, path: Path) -> Path:
resolved = path.resolve()
for root in (self.user_root.resolve(), self.profile_root.resolve()):
try:
resolved.relative_to(root)
return root
except ValueError:
continue
return self.profile_root
def _profiles_for_vehicle(self, vehicle_id: str | None) -> list[ModeProfile]:
selected: dict[str, ModeProfile] = {}
@@ -6,6 +6,7 @@ import json
import os
import re
import sys
import tempfile
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
@@ -237,7 +238,14 @@ class SeatDemoService:
}
self.model_plan_adapter = ModelPlanAdapter(self.capability_context)
self.vehicle_profiles = VehicleProfileRegistry(repository_root / "config" / "vehicles")
self.mode_profiles = ModeProfileRegistry(repository_root / "config" / "modes")
self._mode_session_directory = tempfile.TemporaryDirectory(
prefix="cognitive-agent-mode-session-",
ignore_cleanup_errors=True,
)
self.mode_profiles = ModeProfileRegistry(
repository_root / "config" / "modes",
user_root=Path(self._mode_session_directory.name),
)
self._artifact_root = repository_root / "artifacts" / "runs"
self.conversations = ConversationStateStore(
preset_root=repository_root / "artifacts" / "user-presets"
@@ -343,6 +351,7 @@ class SeatDemoService:
"vehicle": default_vehicle.public_dict(),
"vehicles": self.vehicle_profiles.public_profiles(),
"modes": self.mode_profiles.public_modes(default_vehicle.id),
"mode_profile_storage": "runtime_session_volatile",
"preset_storage": "conversation_durable",
"inferred_level_range": [1, 3],
"motion_position_range_pct": [0, 100],