feat: integrate cabin CAN and verified dynamic seat modes

This commit is contained in:
vee
2026-08-04 17:51:38 +08:00
parent 155fbb59ba
commit dc7c91800e
99 changed files with 51897 additions and 3618 deletions
+438 -48
View File
@@ -8,6 +8,7 @@ import re
import sys
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from datetime import UTC, datetime
from pathlib import Path
@@ -46,10 +47,16 @@ from cognitive_agent.application.mode_profile import ( # noqa: E402
from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
ModelPlanAdapter,
APPLY_PRESET_CAPABILITY,
DYNAMIC_MODE_STOP_CAPABILITY,
SAVE_PRESET_CAPABILITY,
SEAT_CAPABILITIES,
SEAT_MOTION_CAPABILITIES,
)
from cognitive_agent.application.native_can_intent import merge_native_can_intent # noqa: E402
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
NativeCanExecutor,
NativeCanRuntime,
)
from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402
from cognitive_agent.application.vehicle_profile import ( # noqa: E402
VehicleProfile,
@@ -63,7 +70,9 @@ from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
HardwareSafetyError,
PHYSICAL_BENCH_COORDINATOR,
SeatHardwareSettings,
VehicleCanSettings,
)
from cognitive_agent.infrastructure.can.gateway import ZlgChannelConfig # noqa: E402
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
PHYSICAL_BENCH_ACCESS,
)
@@ -112,6 +121,18 @@ SEAT_AXIS_TO_CAPABILITY = {
SEAT_FUNCTION_TO_CAPABILITY = {
function: capability for capability, function in SEAT_CAPABILITIES.items()
}
DYNAMIC_MODE_STOP_WORDS = ("停止", "关闭", "结束", "取消", "停下", "退出")
DYNAMIC_MODE_WORDS = (
"动态模式",
"动态座椅",
"木马模式",
"木马",
"摇摇马",
"骑木马",
"rocking horse",
"动态摇篮",
"摇篮模式",
)
class SeatDemoService:
@@ -142,6 +163,8 @@ class SeatDemoService:
repository_root / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
)
self.hardware_settings = SeatHardwareSettings.from_env(repository_root)
self.vehicle_can_settings = VehicleCanSettings.from_env(repository_root)
self.native_can_runtime = NativeCanRuntime(repository_root)
self.hardware_execution = PHYSICAL_BENCH_COORDINATOR
self.hardware_access = PHYSICAL_BENCH_ACCESS
# Shared by the short-lived hardware executor instances. A process must
@@ -192,8 +215,18 @@ class SeatDemoService:
"platform": "reference_vehicle",
"execution_mode": "dry_run",
"execution_modes": ["dry_run", "simulated_hardware", "hardware"],
"hardware_execution_allowed": self.hardware_settings.configured,
"hardware_execution_allowed": (
self.hardware_settings.configured
and self.vehicle_can_settings.configured
),
"hardware": hardware_info,
"vehicle_can": self.vehicle_can_settings.public_info(),
"multi_channel_can": {
"adapter_ownership": "single_process",
"seat_channel": self.hardware_settings.channel,
"vehicle_channel": self.vehicle_can_settings.channel,
"channels_must_be_distinct": True,
},
"seat_position_memory_synchronized": bool(
self._seat_position_memory_state.get("synchronized", False)
),
@@ -298,10 +331,13 @@ class SeatDemoService:
"actuator_state": context["devices"],
}
def _new_hardware_executor(self) -> HardwareSeatExecutor:
def _new_hardware_executor(
self, bus_factory: Any | None = None
) -> HardwareSeatExecutor:
resolved_bus_factory = bus_factory or self.hardware_settings.bus_factory()
return HardwareSeatExecutor(
self.hardware_matrix,
self.hardware_settings.bus_factory(),
resolved_bus_factory,
thermal_timeout_ms=self.hardware_settings.thermal_timeout_ms,
ack_timeout_ms=self.hardware_settings.ack_timeout_ms,
axis_maximum_x100={
@@ -319,7 +355,7 @@ class SeatDemoService:
),
auto_home=self.hardware_settings.auto_home_enabled,
home_timeout_s=self.hardware_settings.home_timeout_s,
firmware_guard=self.hardware_settings.firmware_guard(),
firmware_guard=self.hardware_settings.firmware_guard(resolved_bus_factory),
synchronize_position_before_first_motion=(
self.hardware_settings.position_sync_enabled
),
@@ -414,8 +450,6 @@ class SeatDemoService:
or mode_update is not None
)
hardware_request = execution_mode == "hardware"
if hardware_request and not control_plane_request:
self.hardware_settings.authorize(approval_token)
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")
@@ -512,7 +546,40 @@ class SeatDemoService:
context,
vehicle_profile,
)
parsed = self._apply_mode_profiles(parsed, matched_modes, context, vehicle_profile)
if self._is_dynamic_mode_stop_request(utterance):
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
else:
parsed = self._apply_mode_profiles(
parsed, matched_modes, context, vehicle_profile
)
parsed = merge_native_can_intent(parsed, utterance)
parsed = self._partition_native_actions(
parsed, promote_supported=hardware_request
)
if hardware_request and not control_plane_request:
if parsed.get("actions"):
self.hardware_settings.authorize(approval_token)
if parsed.get("native_can_actions"):
self.vehicle_can_settings.authorize(approval_token)
if not parsed.get("actions") and not parsed.get("native_can_actions"):
raise HardwareSafetyError(
"hardware mode requires at least one verified seat or vehicle CAN operation"
)
if parsed.get("actions") and parsed.get("native_can_actions"):
if self.hardware_settings.backend != "zlgcan":
raise HardwareSafetyError(
"linked seat/vehicle execution requires the shared zlgcan backend"
)
try:
seat_channel = int(self.hardware_settings.channel, 0)
except ValueError as exc:
raise HardwareSafetyError(
"ZLG seat CAN channel must be an integer"
) from exc
if seat_channel == self.vehicle_can_settings.channel:
raise HardwareSafetyError(
"seat and vehicle CAN must use different adapter channels"
)
self._trace(
traces,
request_id,
@@ -580,14 +647,9 @@ class SeatDemoService:
},
)
if parsed.get("domain_actions") or any(
if parsed.get("domain_actions") or parsed.get("native_can_actions") or any(
domain != "seat" for domain in detected_domains
):
if hardware_request and not parsed.get("actions"):
raise HardwareSafetyError(
"hardware mode requires at least one passenger seat operation; "
"non-seat-only requests stay dry-run"
)
return self._process_orchestrated(
utterance,
request_id,
@@ -1405,6 +1467,63 @@ class SeatDemoService:
)
return merged
@staticmethod
def _is_dynamic_mode_stop_request(utterance: str) -> bool:
compact = re.sub(r"\s+", "", utterance)
if any(negation in compact for negation in ("不要停止", "无需停止", "别停止")):
return False
return any(word in compact for word in DYNAMIC_MODE_STOP_WORDS) and any(
word in compact for word in DYNAMIC_MODE_WORDS
)
def _apply_dynamic_mode_stop_request(
self,
parsed: dict[str, Any],
utterance: str,
) -> dict[str, Any]:
"""Make explicit stop language authoritative over a matched start profile."""
stopped = deepcopy(parsed)
stopped["actions"] = [
action
for action in stopped.get("actions", [])
if action.get("action_type") not in {"dynamic_mode", "dynamic_mode_stop"}
]
stopped["actions"].append(
{"action_type": "dynamic_mode_stop", "target": "passenger"}
)
stopped["model_requests"] = [
request
for request in stopped.get("model_requests", [])
if request.get("capability")
not in {"seat.start_dynamic_mode", DYNAMIC_MODE_STOP_CAPABILITY}
]
stopped["model_requests"].append(
{
"domain": "seat",
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
"target_scope": "passenger",
"operation": "deactivate",
"parameters": [],
"reason": "用户明确要求停止动态座椅循环。",
"source_span": utterance,
}
)
stopped["ambiguities"] = [
item
for item in stopped.get("ambiguities", [])
if item.get("field") != "capability"
]
stopped["detected_domains"] = self._merge_domains(
["seat"], stopped.get("detected_domains", [])
)
stopped["decision_summary"] = [
"识别到停止动态座椅模式指令;不会重新启动同名长期模式。",
*stopped.get("decision_summary", []),
]
stopped["confidence"] = max(float(stopped.get("confidence", 0)), 0.99)
return stopped
@staticmethod
def _contains_seat_preset_request(parsed: dict[str, Any]) -> bool:
capabilities = {
@@ -1454,6 +1573,19 @@ class SeatDemoService:
action.get("target"),
action.get("axis"),
)
if action.get("action_type") == "dynamic_mode_stop":
return (
"seat",
"dynamic_mode_stop",
action.get("target"),
)
if action.get("action_type") == "dynamic_mode":
return (
"seat",
"dynamic_mode",
action.get("target"),
action.get("mode_id"),
)
return (
"seat",
"function",
@@ -2136,6 +2268,24 @@ class SeatDemoService:
"parameters": parameters,
"reason": "用户固化的座椅位置模式动作。",
}
if action.get("action_type") == "dynamic_mode_stop":
return {
"domain": "seat",
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
"target_scope": action["target"],
"operation": "deactivate",
"parameters": {},
"reason": "用户固化的动态座椅停止动作。",
}
if action.get("action_type") == "dynamic_mode":
return {
"domain": "seat",
"capability": "seat.start_dynamic_mode",
"target_scope": action["target"],
"operation": "set",
"parameters": {"mode_id": int(action["mode_id"])},
"reason": "用户固化的固件原生动态座椅模式。",
}
function = action.get("function")
capability = SEAT_FUNCTION_TO_CAPABILITY.get(str(function))
if capability is None:
@@ -2152,6 +2302,71 @@ class SeatDemoService:
"reason": "用户固化的座椅舒适功能模式动作。",
}
def _partition_native_actions(
self, parsed: dict[str, Any], *, promote_supported: bool = False
) -> dict[str, Any]:
"""Promote verified CAN_DEV capabilities and leave unsupported domains virtual."""
result = deepcopy(parsed)
native_actions = list(result.get("native_can_actions", []))
native_signatures = {
(
str(action.get("domain")),
str(action.get("capability")),
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
}
virtual_actions: list[dict[str, Any]] = []
for action in result.get("domain_actions", []):
signature = (
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
if signature in native_signatures:
continue
if promote_supported and self.native_can_runtime.supports(action):
promoted = dict(action)
promoted["execution_backend"] = "native_can"
native_actions.append(promoted)
else:
virtual_actions.append(action)
deduplicated: list[dict[str, Any]] = []
signatures: set[tuple[str, str, str, str]] = set()
for action in native_actions:
signature = (
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
if signature not in signatures:
signatures.add(signature)
deduplicated.append(action)
result["domain_actions"] = virtual_actions
result["native_can_actions"] = deduplicated
detected_domains = list(result.get("detected_domains", []))
if result.get("actions") and "seat" not in detected_domains:
detected_domains.insert(0, "seat")
for action in [*virtual_actions, *deduplicated]:
if action["domain"] not in detected_domains:
detected_domains.append(action["domain"])
result["detected_domains"] = detected_domains
result["deferred_requests"] = [
item
for item in result.get("deferred_requests", [])
if not (
item.get("execution_backend") == "native_can"
and self.native_can_runtime.supports(item)
)
]
return result
def _process_orchestrated(
self,
utterance: str,
@@ -2172,10 +2387,17 @@ class SeatDemoService:
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)
for action in parsed.get("domain_actions", []):
capability = self.domain_runtimes[action["domain"]].capabilities[
action["capability"]
]
intent_actions = [
*parsed.get("domain_actions", []),
*parsed.get("native_can_actions", []),
]
for action in intent_actions:
if action in parsed.get("native_can_actions", []):
capability = self.native_can_runtime.capabilities[action["capability"]]
else:
capability = self.domain_runtimes[action["domain"]].capabilities[
action["capability"]
]
intent_ir["spec"]["requests"].append(
{
"domain": action["domain"],
@@ -2212,6 +2434,20 @@ class SeatDemoService:
provider_used,
)
native_config_keys: set[str] = set()
native_actions_by_domain: dict[str, list[dict[str, Any]]] = {}
for action in parsed.get("native_can_actions", []):
native_actions_by_domain.setdefault(action["domain"], []).append(action)
for domain, actions in native_actions_by_domain.items():
key = f"native_{domain}"
native_config_keys.add(key)
configs[key] = self.native_can_runtime.build_function_config(
domain,
actions,
request_id,
provider_used,
)
result["function_configs"] = configs
result["function_config"] = (
configs.get("seat") or next(iter(configs.values()), None)
@@ -2252,7 +2488,8 @@ class SeatDemoService:
"message": ambiguity["question"],
}
)
validation.append(
if not native_config_keys:
validation.append(
{
"severity": "warning",
"code": "VIRTUAL_EXECUTION_ONLY",
@@ -2269,8 +2506,8 @@ class SeatDemoService:
"code": "NO_EXECUTABLE_OPERATION",
"path": "spec.operations",
"message": "模型没有生成任何可执行领域操作。",
}
)
}
)
if hardware_request:
self._adapt_validation_for_hardware(validation)
result["validation"] = validation
@@ -2310,6 +2547,8 @@ class SeatDemoService:
)
else:
plans[domain] = self.compiler.compile(config)
elif domain in native_config_keys:
plans[domain] = self.native_can_runtime.compile(config)
else:
runtime = self.domain_runtimes[domain]
plans[domain] = runtime.compile(config)
@@ -2388,37 +2627,47 @@ class SeatDemoService:
execute_started = time.perf_counter()
execution_started_at = _now()
reports: dict[str, dict[str, Any]] = {}
for domain, config in configs.items():
if domain == "seat":
if hardware_request:
reports[domain] = self._new_hardware_executor().execute(
plans[domain], config, context["devices"]
)
elif simulated_hardware_request:
reports[domain] = self.simulated_hardware_executor.execute(
plans[domain],
config,
context["devices"],
fault_scenario=simulation_fault,
)
else:
reports[domain] = self.executor.execute(
plans[domain], config, context["devices"]
)
else:
reports[domain] = self.domain_runtimes[domain].execute(
plans[domain], config
)
reports = self._execute_orchestrated_reports(
configs,
plans,
native_config_keys,
context,
hardware_request=hardware_request,
simulated_hardware_request=simulated_hardware_request,
simulation_fault=simulation_fault,
)
report_results = [
item
for domain_report in reports.values()
for item in domain_report["spec"]["results"]
]
operation_results = [
item
for item in report_results
if item.get("ref") != "seat-controller-firmware"
]
operation_domains = {
str(item.get("domain")) for item in operation_results
}
successful_domains = {
domain
for domain in operation_domains
if all(
item.get("status") == "succeeded"
for item in operation_results
if str(item.get("domain")) == domain
)
}
failed_domains = {
str(item.get("domain"))
for item in operation_results
if item.get("status") == "failed"
}
execution_succeeded = all(
domain_report["spec"]["status"] == "succeeded"
for domain_report in reports.values()
) and all(item.get("status") == "succeeded" for item in report_results)
execution_partially_succeeded = bool(successful_domains and failed_domains)
physical_state = (
reports.get("seat", {}).get("spec", {}).get("physical_state")
if hardware_request
@@ -2477,6 +2726,8 @@ class SeatDemoService:
result["validation"] = validation
if report_issues:
result["status"] = "execution_report_invalid"
elif execution_partially_succeeded:
result["status"] = "partially_succeeded"
elif not execution_succeeded:
result["status"] = "execution_failed"
elif parsed.get("deferred_requests"):
@@ -2496,7 +2747,14 @@ class SeatDemoService:
"副驾座椅已按物理 CAN 回读闭环执行,其他领域已完成虚拟 CAN 执行。"
if hardware_request and execution_succeeded
else (
f"副驾座椅真实执行失败:{failed_execution['message']}"
(
"跨域指令部分执行成功;成功域="
f"{','.join(sorted(successful_domains))},失败域="
f"{','.join(sorted(failed_domains))}:"
f"{failed_execution['message']}"
)
if execution_partially_succeeded and failed_execution
else f"真实 CAN 执行失败:{failed_execution['message']}"
if hardware_request and failed_execution
else (
f"模拟台架已执行 {len(report_results)} 个领域操作,物理 CAN 未打开。"
@@ -2523,19 +2781,40 @@ class SeatDemoService:
"failed_domain": (
failed_execution.get("domain") if failed_execution else None
),
"successful_domains": sorted(successful_domains),
"failed_domains": sorted(failed_domains),
"partially_succeeded": execution_partially_succeeded,
},
domain="orchestration",
)
if execution_succeeded and not report_issues:
seat_actions = parsed.get("actions", [])
if hardware_request:
if (execution_succeeded or execution_partially_succeeded) and not report_issues:
seat_actions = (
parsed.get("actions", [])
if execution_succeeded or "seat" in successful_domains
else []
)
if hardware_request and seat_actions and "seat" in reports:
seat_actions = self._hardware_feedback_actions(
seat_actions,
configs["seat"],
reports["seat"],
)
all_actions = [*seat_actions, *parsed.get("domain_actions", [])]
all_actions = [
*seat_actions,
*[
action
for action in parsed.get("domain_actions", [])
if execution_succeeded
or str(action.get("domain")) in successful_domains
],
*[
action
for action in parsed.get("native_can_actions", [])
if execution_succeeded
or str(action.get("domain")) in successful_domains
],
]
updated_context = self.conversations.apply_successful_actions(
conversation_id,
utterance,
@@ -2559,11 +2838,17 @@ class SeatDemoService:
request_id,
"state_update",
"succeeded",
f"跨域会话状态已更新为第 {updated_context['turn_count']} 轮。",
(
f"跨域会话状态已更新为第 {updated_context['turn_count']} 轮。"
if execution_succeeded
else "仅成功设备已写入会话状态;失败设备保持执行前状态。"
),
time.perf_counter(),
{
"seat_state": updated_context["devices"],
"domain_state": updated_context["domain_devices"],
"successful_domains": sorted(successful_domains),
"failed_domains": sorted(failed_domains),
},
domain="orchestration",
)
@@ -2595,6 +2880,111 @@ class SeatDemoService:
self._persist(result)
return result
def _execute_orchestrated_reports(
self,
configs: dict[str, dict[str, Any]],
plans: dict[str, dict[str, Any]],
native_config_keys: set[str],
context: dict[str, Any],
*,
hardware_request: bool,
simulated_hardware_request: bool,
simulation_fault: str,
) -> dict[str, dict[str, Any]]:
reports: dict[str, dict[str, Any]] = {}
bridge = None
seat_bus_factory = None
native_bus_factory = 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,
),
)
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)
try:
def execute_native_configs() -> dict[str, dict[str, Any]]:
native_reports: dict[str, dict[str, Any]] = {}
for key in sorted(native_config_keys):
if not hardware_request:
native_reports[key] = self.native_can_runtime.execute_virtual(
plans[key], configs[key]
)
continue
executor = NativeCanExecutor(
native_bus_factory,
feedback_timeout_ms=(
self.vehicle_can_settings.feedback_timeout_ms
),
)
native_reports[key] = executor.execute(
self.native_can_runtime,
plans[key],
configs[key],
)
return native_reports
def execute_seat_config() -> dict[str, Any]:
config = configs["seat"]
if hardware_request:
seat_executor = (
self._new_hardware_executor(seat_bus_factory)
if seat_bus_factory is not None
else self._new_hardware_executor()
)
return seat_executor.execute(
plans["seat"], config, context["devices"]
)
if simulated_hardware_request:
return self.simulated_hardware_executor.execute(
plans["seat"],
config,
context["devices"],
fault_scenario=simulation_fault,
)
return self.executor.execute(
plans["seat"], config, context["devices"]
)
if hardware_request and native_config_keys and "seat" in configs:
# Both channel views share one adapter owner; executor threads let
# the two channels start within normal USB scheduling jitter.
with ThreadPoolExecutor(max_workers=2) as pool:
native_future = pool.submit(execute_native_configs)
seat_future = pool.submit(execute_seat_config)
reports.update(native_future.result())
reports["seat"] = seat_future.result()
else:
reports.update(execute_native_configs())
if "seat" in configs:
reports["seat"] = execute_seat_config()
for domain, config in configs.items():
if domain in native_config_keys or domain == "seat":
continue
reports[domain] = self.domain_runtimes[domain].execute(
plans[domain], config
)
finally:
if bridge is not None:
bridge.close()
return reports
@staticmethod
def _build_scene_config(
request_id: str,