feat: synchronize CH1 vehicle control and feedback
This commit is contained in:
@@ -53,6 +53,12 @@ from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
|
||||
SEAT_MOTION_CAPABILITIES,
|
||||
)
|
||||
from cognitive_agent.application.native_can_intent import merge_native_can_intent # noqa: E402
|
||||
from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||||
VehicleSafetyPreflightError,
|
||||
authoritative_physical_ch1_feedback_state,
|
||||
observe_vehicle_safety,
|
||||
require_stationary_vehicle,
|
||||
)
|
||||
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
|
||||
NativeCanExecutor,
|
||||
NativeCanRuntime,
|
||||
@@ -93,6 +99,27 @@ def _clean_vehicle_number(value: float | None) -> int | float | None:
|
||||
|
||||
|
||||
CONVERSATION_ID_PATTERN = re.compile(r"^conv-[a-z0-9-]{6,80}$")
|
||||
SEAT_SCOPE_PATTERN = re.compile(r"\bseats?\b|座椅|坐椅", re.IGNORECASE)
|
||||
DRIVER_SEAT_SCOPE_PATTERN = re.compile(
|
||||
r"\bdriver\b|\bfront[-_ ]?left\b|\bleft[-_ ]?front\b|主驾|驾驶位|司机位|左前",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
PASSENGER_SEAT_SCOPE_PATTERN = re.compile(
|
||||
r"\bpassenger\b|\bfront[-_ ]?right\b|\bright[-_ ]?front\b|副驾|乘客|右前",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _has_mixed_driver_passenger_seat_scope(utterance: str) -> bool:
|
||||
"""Detect one request that crosses the CH1-driver and CH0-passenger boundary."""
|
||||
|
||||
return bool(
|
||||
SEAT_SCOPE_PATTERN.search(utterance)
|
||||
and DRIVER_SEAT_SCOPE_PATTERN.search(utterance)
|
||||
and PASSENGER_SEAT_SCOPE_PATTERN.search(utterance)
|
||||
)
|
||||
|
||||
|
||||
SAVE_MODE_PROFILE_NAME = re.compile(
|
||||
r"(?:固化为|保存为长期|保存成长期|长期保存为|记为长期|命名为)"
|
||||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||||
@@ -201,6 +228,17 @@ class SeatDemoService:
|
||||
if hasattr(self, "conversations"):
|
||||
self.conversations.preset_root = self._artifact_root / "user-presets"
|
||||
|
||||
@property
|
||||
def driver_only_ch1_execution_active(self) -> bool:
|
||||
"""Return whether deployment policy authorizes only the CH1 vehicle path."""
|
||||
|
||||
return bool(
|
||||
not self.hardware_settings.enabled
|
||||
and self.vehicle_can_settings.configured
|
||||
and self.vehicle_can_settings.channel_is_allowed
|
||||
and self.vehicle_can_settings.channel == 1
|
||||
)
|
||||
|
||||
def runtime_info(self) -> dict[str, Any]:
|
||||
catalog = yaml.safe_load(
|
||||
(self.domain_root / "config" / "capabilities.yaml").read_text(encoding="utf-8")
|
||||
@@ -216,7 +254,9 @@ class SeatDemoService:
|
||||
"service": "cabin-cognitive-orchestrator",
|
||||
"domain": "cabin",
|
||||
"platform": "reference_vehicle",
|
||||
"execution_mode": "dry_run",
|
||||
"execution_mode": os.getenv(
|
||||
"COGNITIVE_AGENT_EXECUTION_MODE", "dry_run"
|
||||
).strip(),
|
||||
"execution_modes": ["dry_run", "simulated_hardware", "hardware"],
|
||||
"hardware_execution_allowed": (
|
||||
self.hardware_settings.configured
|
||||
@@ -224,11 +264,26 @@ class SeatDemoService:
|
||||
),
|
||||
"hardware": hardware_info,
|
||||
"vehicle_can": self.vehicle_can_settings.public_info(),
|
||||
"vehicle_safety_policy": {
|
||||
"required_channel": 1,
|
||||
"required_gear": "P",
|
||||
"maximum_speed_kmh": 0.05,
|
||||
"fresh_feedback_ids": ["0x342", "0x4EB"],
|
||||
"preflight": "read_only_before_every_physical_request",
|
||||
},
|
||||
"multi_channel_can": {
|
||||
"adapter_ownership": "single_process",
|
||||
"seat_channel": self.hardware_settings.channel,
|
||||
"seat_channel": (
|
||||
self.hardware_settings.channel
|
||||
if self.hardware_settings.enabled
|
||||
else None
|
||||
),
|
||||
"vehicle_channel": self.vehicle_can_settings.channel,
|
||||
"channels_must_be_distinct": True,
|
||||
"channels_must_be_distinct": bool(
|
||||
self.hardware_settings.enabled
|
||||
and self.vehicle_can_settings.enabled
|
||||
),
|
||||
"active_policy": "ch1_body_can_driver_only",
|
||||
},
|
||||
"seat_position_memory_synchronized": bool(
|
||||
self._seat_position_memory_state.get("synchronized", False)
|
||||
@@ -411,7 +466,7 @@ class SeatDemoService:
|
||||
vehicle_id,
|
||||
simulation_fault,
|
||||
)
|
||||
if hardware_access_bypass:
|
||||
if hardware_access_bypass or self.driver_only_ch1_execution_active:
|
||||
with self.hardware_execution.reserve():
|
||||
return self._process_once(
|
||||
utterance,
|
||||
@@ -476,6 +531,11 @@ class SeatDemoService:
|
||||
or mode_update is not None
|
||||
)
|
||||
hardware_request = execution_mode == "hardware"
|
||||
if hardware_request and _has_mixed_driver_passenger_seat_scope(utterance):
|
||||
raise HardwareSafetyError(
|
||||
"hardware mode refuses a mixed CH1 driver/front-left and "
|
||||
"CH0 passenger/front-right seat request"
|
||||
)
|
||||
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
|
||||
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
|
||||
raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens")
|
||||
@@ -584,6 +644,17 @@ class SeatDemoService:
|
||||
promote_supported=(hardware_request and self.vehicle_can_settings.enabled),
|
||||
)
|
||||
if hardware_request and not control_plane_request:
|
||||
if parsed.get("domain_actions"):
|
||||
unsupported = sorted(
|
||||
{
|
||||
str(action.get("capability", "unknown"))
|
||||
for action in parsed["domain_actions"]
|
||||
}
|
||||
)
|
||||
raise HardwareSafetyError(
|
||||
"hardware mode refuses virtual or unverified vehicle actions: "
|
||||
+ ", ".join(unsupported)
|
||||
)
|
||||
if parsed.get("actions"):
|
||||
self.hardware_settings.authorize(approval_token)
|
||||
if parsed.get("native_can_actions"):
|
||||
@@ -1343,6 +1414,20 @@ class SeatDemoService:
|
||||
action["physical_target_alias"] = "t2g_primary_seat"
|
||||
return updated
|
||||
|
||||
@staticmethod
|
||||
def _physical_ch1_feedback_states(
|
||||
reports: dict[str, dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return only fully verified decoded CH1 feedback state patches."""
|
||||
|
||||
states: list[dict[str, Any]] = []
|
||||
for report in reports.values():
|
||||
for item in report.get("spec", {}).get("results", []):
|
||||
feedback_state = authoritative_physical_ch1_feedback_state(item)
|
||||
if feedback_state is not None:
|
||||
states.append(feedback_state)
|
||||
return states
|
||||
|
||||
@staticmethod
|
||||
def _parse_mode_profile_save_name(utterance: str) -> str | None:
|
||||
explicit = SAVE_MODE_PROFILE_NAME.search(utterance)
|
||||
@@ -2335,7 +2420,12 @@ class SeatDemoService:
|
||||
"""Promote verified CAN_DEV capabilities and leave unsupported domains virtual."""
|
||||
|
||||
result = deepcopy(parsed)
|
||||
native_actions = list(result.get("native_can_actions", []))
|
||||
candidate_native_actions = list(result.get("native_can_actions", []))
|
||||
native_actions = [
|
||||
action
|
||||
for action in candidate_native_actions
|
||||
if self.native_can_runtime.supports(action)
|
||||
]
|
||||
native_signatures = {
|
||||
(
|
||||
str(action.get("domain")),
|
||||
@@ -2343,7 +2433,7 @@ class SeatDemoService:
|
||||
str(action.get("target", action.get("target_scope", "vehicle"))),
|
||||
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
|
||||
)
|
||||
for action in native_actions
|
||||
for action in candidate_native_actions
|
||||
}
|
||||
virtual_actions: list[dict[str, Any]] = []
|
||||
for action in result.get("domain_actions", []):
|
||||
@@ -2384,7 +2474,7 @@ class SeatDemoService:
|
||||
if action["domain"] not in detected_domains:
|
||||
detected_domains.append(action["domain"])
|
||||
result["detected_domains"] = detected_domains
|
||||
result["deferred_requests"] = [
|
||||
deferred_requests = [
|
||||
item
|
||||
for item in result.get("deferred_requests", [])
|
||||
if not (
|
||||
@@ -2392,6 +2482,18 @@ class SeatDemoService:
|
||||
and self.native_can_runtime.supports(item)
|
||||
)
|
||||
]
|
||||
for item in deferred_requests:
|
||||
if item.get("capability") in {
|
||||
"door.set_vehicle_lock",
|
||||
"door.set_rear_lock",
|
||||
}:
|
||||
item["execution_backend"] = "unsupported"
|
||||
item["rejection_code"] = "CH1_DOOR_LOCK_CONTROL_UNSUPPORTED"
|
||||
item["reason"] = (
|
||||
"Current CH1 door-lock control is unsupported; lock feedback "
|
||||
"is observational only and no verified physical command will be built."
|
||||
)
|
||||
result["deferred_requests"] = deferred_requests
|
||||
return result
|
||||
|
||||
def _process_orchestrated(
|
||||
@@ -2410,7 +2512,7 @@ class SeatDemoService:
|
||||
simulation_fault: str = "normal",
|
||||
vehicle_profile: VehicleProfile | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run isolated adapters, with only the passenger seat eligible for HIL."""
|
||||
"""Run isolated adapters under the selected physical-bus policy."""
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
generation_started = time.perf_counter()
|
||||
intent_ir = build_intent_ir(parsed, utterance, request_id, provider_used)
|
||||
@@ -2626,7 +2728,9 @@ class SeatDemoService:
|
||||
"safety_check",
|
||||
"succeeded",
|
||||
(
|
||||
"副驾座椅已通过 T2G HIL 安全门,其他座舱领域保持虚拟 CAN。"
|
||||
"所有动作已通过 CH1 车身 CAN 白名单和物理反馈门禁。"
|
||||
if hardware_request and native_config_keys and "seat" not in configs
|
||||
else "副驾 T2G 台架动作已通过独立硬件安全门。"
|
||||
if hardware_request
|
||||
else (
|
||||
"模拟台架仅对座椅结果注入故障;全部领域的物理发送路径保持关闭。"
|
||||
@@ -2641,11 +2745,16 @@ class SeatDemoService:
|
||||
"simulation_fault": (
|
||||
simulation_fault if simulated_hardware_request else None
|
||||
),
|
||||
"hardware_domains": ["seat"] if hardware_request else [],
|
||||
"hardware_domains": (
|
||||
[configs[key]["spec"]["domain"] for key in native_config_keys]
|
||||
+ (["seat"] if "seat" in configs else [])
|
||||
if hardware_request
|
||||
else []
|
||||
),
|
||||
"virtual_domains": [
|
||||
domain
|
||||
for domain in configs
|
||||
if not hardware_request or domain != "seat"
|
||||
if not hardware_request
|
||||
],
|
||||
"domains": list(configs),
|
||||
},
|
||||
@@ -2729,7 +2838,9 @@ class SeatDemoService:
|
||||
"labels": {
|
||||
"simulation": "mixed" if hardware_request else "true",
|
||||
"seat_feedback": (
|
||||
"physical_can"
|
||||
"physical_can_ch1"
|
||||
if hardware_request and native_config_keys and "seat" not in configs
|
||||
else "physical_can"
|
||||
if hardware_request
|
||||
else (
|
||||
"simulated_hardware"
|
||||
@@ -2771,7 +2882,9 @@ class SeatDemoService:
|
||||
"execute",
|
||||
"succeeded" if execution_succeeded and not report_issues else "failed",
|
||||
(
|
||||
"副驾座椅已按物理 CAN 回读闭环执行,其他领域已完成虚拟 CAN 执行。"
|
||||
"CH1 车身 CAN 动作已按目标字段物理回读闭环执行。"
|
||||
if hardware_request and native_config_keys and execution_succeeded
|
||||
else "副驾座椅已按独立 T2G 物理 CAN 回读闭环执行。"
|
||||
if hardware_request and execution_succeeded
|
||||
else (
|
||||
(
|
||||
@@ -2836,10 +2949,20 @@ class SeatDemoService:
|
||||
or str(action.get("domain")) in successful_domains
|
||||
],
|
||||
*[
|
||||
action
|
||||
{
|
||||
**action,
|
||||
**(
|
||||
{"feedback_source": "physical_can_ch1"}
|
||||
if hardware_request
|
||||
else {}
|
||||
),
|
||||
}
|
||||
for action in parsed.get("native_can_actions", [])
|
||||
if execution_succeeded
|
||||
or str(action.get("domain")) in successful_domains
|
||||
if not hardware_request
|
||||
and (
|
||||
execution_succeeded
|
||||
or str(action.get("domain")) in successful_domains
|
||||
)
|
||||
],
|
||||
]
|
||||
updated_context = self.conversations.apply_successful_actions(
|
||||
@@ -2853,6 +2976,13 @@ class SeatDemoService:
|
||||
conversation_id,
|
||||
physical_state,
|
||||
)
|
||||
if hardware_request:
|
||||
physical_ch1_feedback = self._physical_ch1_feedback_states(reports)
|
||||
if physical_ch1_feedback:
|
||||
updated_context = self.conversations.synchronize_physical_ch1_feedback(
|
||||
conversation_id,
|
||||
physical_ch1_feedback,
|
||||
)
|
||||
updated_context = self._context_for_vehicle(
|
||||
updated_context,
|
||||
vehicle_profile,
|
||||
@@ -2922,6 +3052,7 @@ class SeatDemoService:
|
||||
bridge = None
|
||||
seat_bus_factory = None
|
||||
native_bus_factory = None
|
||||
vehicle_safety_preflight: dict[str, Any] | None = None
|
||||
|
||||
if hardware_request and native_config_keys:
|
||||
channel_configs = [self.vehicle_can_settings.channel_config()]
|
||||
@@ -2944,6 +3075,29 @@ class SeatDemoService:
|
||||
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
|
||||
# any actuator executor; CH0 traffic is ignored by the observer.
|
||||
try:
|
||||
vehicle_safety_preflight = observe_vehicle_safety(
|
||||
native_bus_factory(),
|
||||
timeout_ms=min(
|
||||
5_000,
|
||||
max(1_000, self.vehicle_can_settings.feedback_timeout_ms),
|
||||
),
|
||||
required_channel=self.vehicle_can_settings.channel,
|
||||
)
|
||||
require_stationary_vehicle(vehicle_safety_preflight)
|
||||
except VehicleSafetyPreflightError as exc:
|
||||
bridge.close()
|
||||
raise HardwareSafetyError(
|
||||
"CH1 vehicle safety preflight refused physical execution: "
|
||||
f"{exc}"
|
||||
) from exc
|
||||
except Exception:
|
||||
bridge.close()
|
||||
raise
|
||||
|
||||
try:
|
||||
def execute_native_configs() -> dict[str, dict[str, Any]]:
|
||||
native_reports: dict[str, dict[str, Any]] = {}
|
||||
@@ -2964,6 +3118,13 @@ class SeatDemoService:
|
||||
plans[key],
|
||||
configs[key],
|
||||
)
|
||||
native_reports[key]["spec"]["vehicle_safety_preflight"] = (
|
||||
vehicle_safety_preflight
|
||||
)
|
||||
for item in native_reports[key]["spec"].get("results", []):
|
||||
item.setdefault("telemetry", {})[
|
||||
"vehicle_safety_preflight"
|
||||
] = vehicle_safety_preflight
|
||||
return native_reports
|
||||
|
||||
def execute_seat_config() -> dict[str, Any]:
|
||||
@@ -3077,7 +3238,12 @@ class SeatDemoService:
|
||||
try:
|
||||
plan = self.openai.interpret(utterance, context, self.capability_context)
|
||||
return (
|
||||
self.model_plan_adapter.adapt(plan, context, vehicle_profile),
|
||||
self.model_plan_adapter.adapt(
|
||||
plan,
|
||||
context,
|
||||
vehicle_profile,
|
||||
utterance=utterance,
|
||||
),
|
||||
f"openai:{self.openai.model}",
|
||||
None,
|
||||
)
|
||||
@@ -3091,7 +3257,12 @@ class SeatDemoService:
|
||||
try:
|
||||
plan = self.openai.interpret(utterance, context, self.capability_context)
|
||||
return (
|
||||
self.model_plan_adapter.adapt(plan, context, vehicle_profile),
|
||||
self.model_plan_adapter.adapt(
|
||||
plan,
|
||||
context,
|
||||
vehicle_profile,
|
||||
utterance=utterance,
|
||||
),
|
||||
f"openai:{self.openai.model}",
|
||||
None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user