feat: complete dual-seat whole-vehicle orchestration
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user