feat: complete dual-seat whole-vehicle orchestration

This commit is contained in:
vee
2026-08-12 15:12:40 +08:00
parent 32379a1573
commit 9ce64b3a01
62 changed files with 9741 additions and 3950 deletions
+328 -25
View File
@@ -56,6 +56,7 @@ from cognitive_agent.application.native_can_intent import merge_native_can_inten
from cognitive_agent.application.native_can_feedback import ( # noqa: E402
VehicleSafetyPreflightError,
authoritative_physical_ch1_feedback_state,
decode_feedback,
observe_vehicle_safety,
require_stationary_vehicle,
)
@@ -124,6 +125,12 @@ SAVE_MODE_PROFILE_NAME = re.compile(
r"(?:固化为|保存为长期|保存成长期|长期保存为|记为长期|命名为)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
)
CREATE_MODE_PROFILE_NAME = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:构建|创建|新建|新增|生成|添加)"
r"(?:一个|个|一套|套)?"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?[。.!!]?$"
)
UPDATE_MODE_PROFILE_PREFIX = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:修改一下|修改|调整|更改|更新|改一下)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
@@ -283,7 +290,20 @@ class SeatDemoService:
self.hardware_settings.enabled
and self.vehicle_can_settings.enabled
),
"active_policy": "ch1_body_can_driver_only",
"active_policy": (
"dual_channel_passenger_ch0_driver_ch1"
if self.hardware_settings.configured
and self.vehicle_can_settings.configured
else "ch1_body_can_driver_only"
if self.vehicle_can_settings.configured
else "ch0_passenger_only"
if self.hardware_settings.configured
else "virtual_only"
),
"seat_roles": {
"passenger": {"zone": "front_right", "channel": 0},
"driver": {"zone": "front_left", "channel": 1},
},
},
"seat_position_memory_synchronized": bool(
self._seat_position_memory_state.get("synchronized", False)
@@ -339,6 +359,19 @@ class SeatDemoService:
"domain_runtimes": domain_runtime_info,
}
def delete_user_mode_profile(
self,
mode_id: str,
vehicle_id: str | None = None,
) -> dict[str, Any]:
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
profile = self.mode_profiles.delete_user_mode(mode_id, resolved_vehicle_id)
return {
"status": "succeeded",
"deleted_mode_profile": profile.public_dict(),
"vehicle_id": resolved_vehicle_id,
}
def synchronize_hardware_state(
self,
conversation_id: str,
@@ -389,11 +422,152 @@ class SeatDemoService:
"actuator_state": context["devices"],
}
def synchronize_vehicle_state(
self,
conversation_id: str,
vehicle_id: str | None = None,
*,
hardware_client_id: str | None = None,
hardware_access_bypass: bool = True,
) -> dict[str, Any]:
"""Passively receive CH1 driver-seat feedback without transmitting CAN."""
if hardware_access_bypass:
with self.hardware_execution.reserve():
return self._synchronize_vehicle_state_once(conversation_id, vehicle_id)
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._synchronize_vehicle_state_once(conversation_id, vehicle_id)
def _synchronize_vehicle_state_once(
self,
conversation_id: str,
vehicle_id: str | None = None,
) -> dict[str, Any]:
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
raise ValueError(
"conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens"
)
if not self.vehicle_can_settings.configured:
raise HardwareSafetyError("the CH1 vehicle CAN runtime is not configured")
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
shared_runtime_enabled = os.getenv(
"COGNITIVE_AGENT_SHARED_CAN_RUNTIME_ENABLED",
os.getenv("COGNITIVE_AGENT_SEAT_SHARED_ZCANPRO_BRIDGE_ENABLED", "true"),
).strip().lower() in {"1", "true", "yes", "on"}
bridge = None
if shared_runtime_enabled:
from cognitive_agent.application.self_repair.service import BRIDGE_MANAGER
if not hasattr(BRIDGE_MANAGER, "channel_bus_factory"):
raise HardwareSafetyError(
"CH1 feedback requires the standalone multi-channel CAN runtime"
)
bus = BRIDGE_MANAGER.channel_bus_factory(
self.vehicle_can_settings.channel
)()
else:
bridge = self.vehicle_can_settings.bridge(
[self.vehicle_can_settings.channel_config()]
)
bus = bridge.channel_bus(self.vehicle_can_settings.channel)
feedback_states: list[dict[str, Any]] = []
observed_ids: set[int] = set()
# Keep driver-seat completeness compatible with the existing endpoint,
# while passively collecting every recognized CH1 body-state frame for
# the whole-vehicle dashboard during the same bounded receive window.
required_seat_ids = {0x4FA, 0x494}
required_body_ids = {0x342, 0x4EB, 0x407}
observable_ids = {
*required_seat_ids,
*required_body_ids,
0x388,
0x404,
0x41A,
0x42E,
}
deadline = time.monotonic() + self.vehicle_can_settings.feedback_timeout_ms / 1000
try:
while time.monotonic() < deadline and observed_ids != observable_ids:
remaining_ms = max(1, int((deadline - time.monotonic()) * 1000))
frame = bus.receive(remaining_ms, include_fd=True)
if frame is None:
break
if frame.channel != self.vehicle_can_settings.channel:
continue
if frame.arbitration_id not in observable_ids or len(frame.data) < 8:
continue
decoded = decode_feedback(frame)
if not any(
key in decoded
for key in ("actuator_state", "domain_state", "vehicle_safety")
):
# 0x4EB is multiplexed. Only subtype 0x07 carries the
# verified speed signal, so unrelated subframes must not
# make the body-state snapshot look complete.
continue
frame_id = str(decoded.get("frame_id", ""))
decoded.update(
{
"valid": True,
"feedback_frame_id": frame_id,
"captured_at": _now(),
"verification": {
"authoritative": True,
"channel": 1,
"simulated": False,
"read_only": True,
"frame_id": frame_id,
"payload_size": len(frame.data),
},
}
)
feedback_states.append(decoded)
observed_ids.add(frame.arbitration_id)
finally:
if bridge is not None:
bridge.close()
else:
bus.close()
context = self.conversations.synchronize_physical_ch1_feedback(
conversation_id,
feedback_states,
)
context = self._context_for_vehicle(context, vehicle_profile)
missing = [
f"0x{frame_id:03X}"
for frame_id in sorted(required_seat_ids - observed_ids)
]
body_state_missing = [
f"0x{frame_id:03X}"
for frame_id in sorted(required_body_ids - observed_ids)
]
return {
"status": "succeeded",
"conversation_id": conversation_id,
"conversation_turn": context["turn_count"],
"source": "physical_can_ch1",
"read_only": True,
"captured_at": feedback_states[-1].get("captured_at") if feedback_states else _now(),
"complete": not missing,
"missing": missing,
"body_state_complete": not body_state_missing,
"body_state_missing": body_state_missing,
"observed_frame_ids": [f"0x{frame_id:03X}" for frame_id in sorted(observed_ids)],
"actuator_state": context["devices"],
"domain_state": context["domain_devices"],
}
def _new_hardware_executor(
self, bus_factory: Any | None = None
) -> HardwareSeatExecutor:
shared_bridge_enabled = os.getenv(
"COGNITIVE_AGENT_SEAT_SHARED_ZCANPRO_BRIDGE_ENABLED", "true"
"COGNITIVE_AGENT_SHARED_CAN_RUNTIME_ENABLED",
os.getenv("COGNITIVE_AGENT_SEAT_SHARED_ZCANPRO_BRIDGE_ENABLED", "true"),
).strip().lower() in {"1", "true", "yes", "on"}
if bus_factory is None and shared_bridge_enabled:
# Import lazily so the ordinary dry-run service does not initialize
@@ -404,12 +578,16 @@ class SeatDemoService:
PENDING_DIR,
REQUIRED_BRIDGE_VERSION,
STATUS_PATH,
ZCANPRO_EXE,
)
if ZCANPRO_EXE.is_file():
manager = BRIDGE_MANAGER
manager_snapshot = manager.snapshot()
if (
manager_snapshot.get("backend") == "standalone_zlg"
or Path(str(manager_snapshot.get("zcan_exe") or "")).is_file()
):
return ZcanBridgeSeatExecutor(
self.hardware_matrix,
BRIDGE_MANAGER,
manager,
STATUS_PATH,
PENDING_DIR,
required_version=REQUIRED_BRIDGE_VERSION,
@@ -456,6 +634,12 @@ class SeatDemoService:
hardware_client_id: str | None = None,
hardware_access_bypass: bool = True,
) -> dict[str, Any]:
# Creating a reusable scene is a configuration workflow. Synthesize
# and validate it in dry-run even when the console currently targets
# the real bench, otherwise virtual domains such as climate or ambient
# light would be rejected before the profile can be persisted.
if self._parse_mode_profile_create_name(utterance.strip()) is not None:
execution_mode = "dry_run"
if execution_mode != "hardware":
return self._process_once(
utterance,
@@ -523,6 +707,7 @@ class SeatDemoService:
+ ", ".join(SIMULATED_HARDWARE_FAULTS)
)
mode_save_name = self._parse_mode_profile_save_name(utterance)
mode_create_name = self._parse_mode_profile_create_name(utterance)
mode_restore_name = self._parse_mode_profile_restore_name(utterance)
mode_update = self._parse_mode_profile_update(utterance)
control_plane_request = (
@@ -541,6 +726,18 @@ class SeatDemoService:
raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens")
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
if mode_create_name is not None:
return self._process_mode_profile_create(
utterance,
provider,
conversation_id,
execution_mode,
approval_token,
vehicle_profile,
simulation_fault,
mode_create_name,
)
request_id = f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
context = self._context_for_vehicle(
self.conversations.snapshot(conversation_id),
@@ -1447,6 +1644,11 @@ class SeatDemoService:
)
return generic.group("name").strip() if generic is not None else None
@staticmethod
def _parse_mode_profile_create_name(utterance: str) -> str | None:
explicit = CREATE_MODE_PROFILE_NAME.search(utterance)
return explicit.group("name").strip() if explicit is not None else None
@staticmethod
def _parse_mode_profile_update(utterance: str) -> tuple[str, str] | None:
explicit = UPDATE_MODE_PROFILE_PREFIX.search(utterance)
@@ -2169,6 +2371,80 @@ class SeatDemoService:
except ValueError:
return str(path)
def _process_mode_profile_create(
self,
utterance: str,
provider: str,
conversation_id: str,
execution_mode: str,
approval_token: str | None,
vehicle_profile: VehicleProfile,
simulation_fault: str,
mode_name: str,
) -> dict[str, Any]:
"""Build, verify, and persist a new named mode in one user turn.
A request such as ``构建一个K歌模式`` used to be interpreted as an
ordinary temporary scene. Although that scene could execute
successfully, it never reached the mode registry. Run the generated
scene first so only validated successful actions are persisted, then
use the existing control-plane save path to create the YAML profile.
"""
source_utterance = (
f"进入{mode_name},请根据该场景名称使用当前可控原子能力构建并执行场景"
)
source_result = self._process_once(
source_utterance,
provider,
conversation_id,
"dry_run",
approval_token,
vehicle_profile.id,
simulation_fault,
)
if source_result.get("status") not in {"succeeded", "partially_succeeded"}:
source_result["decision_summary"] = [
f"未能完成“{mode_name}”的场景动作验证,因此没有创建长期模式。",
*list(source_result.get("decision_summary", [])),
]
self._persist(source_result)
return source_result
saved_result = self._process_once(
f"这个模式不错,固化为{mode_name}",
"local",
conversation_id,
"dry_run",
None,
vehicle_profile.id,
"normal",
)
if saved_result.get("status") != "succeeded" or not saved_result.get(
"mode_profile"
):
return saved_result
saved_result["mode_creation"] = {
"requested_utterance": utterance,
"source_utterance": source_utterance,
"source_request_id": source_result.get("request_id"),
"source_status": source_result.get("status"),
"source_provider": source_result.get("provider_used"),
"operation_count": saved_result["mode_profile"].get(
"operation_count", 0
),
}
source_summary = list(source_result.get("decision_summary", []))
saved_result["decision_summary"] = [
f"已构建并验证“{mode_name}”场景。",
*source_summary,
*list(saved_result.get("decision_summary", [])),
f"“{mode_name}”已加入功能模式,可直接点击执行。",
]
self._persist(saved_result)
return saved_result
def _process_mode_profile_save(
self,
utterance: str,
@@ -3055,25 +3331,50 @@ class SeatDemoService:
vehicle_safety_preflight: dict[str, Any] | None = None
if hardware_request and native_config_keys:
channel_configs = [self.vehicle_can_settings.channel_config()]
if "seat" in configs:
seat_channel = int(self.hardware_settings.channel, 0)
channel_configs.insert(
0,
ZlgChannelConfig(
channel=seat_channel,
arbitration_bitrate=self.hardware_settings.bitrate,
data_bitrate=self.vehicle_can_settings.data_bitrate,
can_fd_iso=True,
),
shared_runtime_enabled = os.getenv(
"COGNITIVE_AGENT_SHARED_CAN_RUNTIME_ENABLED",
os.getenv(
"COGNITIVE_AGENT_SEAT_SHARED_ZCANPRO_BRIDGE_ENABLED", "true"
),
).strip().lower() in {"1", "true", "yes", "on"}
if shared_runtime_enabled:
from cognitive_agent.application.self_repair.service import (
BRIDGE_MANAGER,
)
bridge = self.vehicle_can_settings.bridge(channel_configs)
native_bus_factory = lambda: bridge.channel_bus(
self.vehicle_can_settings.channel
)
if "seat" in configs:
seat_channel = int(self.hardware_settings.channel, 0)
seat_bus_factory = lambda: bridge.channel_bus(seat_channel)
if not hasattr(BRIDGE_MANAGER, "channel_bus_factory"):
raise HardwareSafetyError(
"vehicle CAN requires the standalone multi-channel runtime; "
"the ZCANPRO compatibility backend owns CH0 only"
)
native_bus_factory = BRIDGE_MANAGER.channel_bus_factory(
self.vehicle_can_settings.channel
)
# Seat commands continue through the high-level request bridge
# on CH0; passing a raw bus factory here would bypass its one-
# owner queue and allow self-repair to race orchestration.
seat_bus_factory = None
else:
channel_configs = [self.vehicle_can_settings.channel_config()]
if "seat" in configs:
seat_channel = int(self.hardware_settings.channel, 0)
channel_configs.insert(
0,
ZlgChannelConfig(
channel=seat_channel,
frame_format="classic_can",
arbitration_bitrate=self.hardware_settings.bitrate,
data_bitrate=self.vehicle_can_settings.data_bitrate,
can_fd_iso=True,
),
)
bridge = self.vehicle_can_settings.bridge(channel_configs)
native_bus_factory = lambda: bridge.channel_bus(
self.vehicle_can_settings.channel
)
if "seat" in configs:
seat_channel = int(self.hardware_settings.channel, 0)
seat_bus_factory = lambda: bridge.channel_bus(seat_channel)
# The bridge is open but no control frame has been submitted yet.
# Require two independent, fresh CH1 status frames before entering
@@ -3089,13 +3390,15 @@ class SeatDemoService:
)
require_stationary_vehicle(vehicle_safety_preflight)
except VehicleSafetyPreflightError as exc:
bridge.close()
if bridge is not None:
bridge.close()
raise HardwareSafetyError(
"CH1 vehicle safety preflight refused physical execution: "
f"{exc}"
) from exc
except Exception:
bridge.close()
if bridge is not None:
bridge.close()
raise
try: