feat: update CAN integration and T2G seat firmware

This commit is contained in:
vee
2026-08-01 15:55:21 +08:00
parent 8cda026ac2
commit 4aded5c21d
1443 changed files with 1007122 additions and 4978 deletions
+311 -72
View File
@@ -218,6 +218,62 @@ class SeatDemoService:
"domain_runtimes": domain_runtime_info,
}
def synchronize_hardware_state(
self,
conversation_id: str,
vehicle_id: str | None = None,
) -> dict[str, Any]:
"""Read the bench and project it into the current passenger conversation."""
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.hardware_settings.configured:
raise HardwareSafetyError("the T2G seat hardware runtime is not configured")
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
physical_state = self._new_hardware_executor().read_physical_state()
context = self.conversations.synchronize_physical_passenger(
conversation_id,
physical_state,
)
context = self._context_for_vehicle(context, vehicle_profile)
return {
"status": "succeeded",
"conversation_id": conversation_id,
"conversation_turn": context["turn_count"],
"source": "physical_can",
"captured_at": physical_state.get("captured_at"),
"complete": bool(physical_state.get("complete")),
"missing": list(physical_state.get("missing", [])),
"physical_state": physical_state,
"actuator_state": context["devices"],
}
def _new_hardware_executor(self) -> HardwareSeatExecutor:
return HardwareSeatExecutor(
self.hardware_matrix,
self.hardware_settings.bus_factory(),
thermal_timeout_ms=self.hardware_settings.thermal_timeout_ms,
ack_timeout_ms=self.hardware_settings.ack_timeout_ms,
axis_maximum_x100={
capability: int(
self.hardware_compiler.protocol["normalized_position_max"]
)
for capability in self.hardware_compiler.axes
},
motor_max_hall={
int(axis["motor_selector"]): int(axis["default_max_hall_count"])
for axis in self.hardware_compiler.axes.values()
},
stop_all_motor_selector=int(
self.hardware_compiler.protocol["stop_all_motor_selector"]
),
auto_home=self.hardware_settings.auto_home_enabled,
home_timeout_s=self.hardware_settings.home_timeout_s,
firmware_guard=self.hardware_settings.firmware_guard(),
)
def process(
self,
utterance: str,
@@ -412,9 +468,10 @@ class SeatDemoService:
if parsed.get("domain_actions") or any(
domain != "seat" for domain in detected_domains
):
if hardware_request:
if hardware_request and not parsed.get("actions"):
raise HardwareSafetyError(
"hardware mode only accepts seat-only requests; cross-domain execution stays dry-run"
"hardware mode requires at least one passenger seat operation; "
"non-seat-only requests stay dry-run"
)
return self._process_orchestrated(
utterance,
@@ -425,6 +482,8 @@ class SeatDemoService:
parsed,
result,
traces,
hardware_request=hardware_request,
vehicle_profile=vehicle_profile,
)
intent_ir = build_intent_ir(parsed, utterance, request_id, provider_used)
@@ -697,8 +756,12 @@ class SeatDemoService:
"safety_check",
"succeeded",
(
"硬件启用、授权令牌、固件 profile 与目标座椅配置门禁已通过;"
"板端 Position_Valid 将在发送绝对移动命令前实时检查。"
(
"硬件启用、授权令牌、固件 profile 与目标座椅配置门禁已通过;"
if self.hardware_settings.approval_required
else "硬件启用、调试免令牌、固件 profile 与目标座椅配置门禁已通过;"
)
+ "板端 Position_Valid 将在发送绝对移动命令前实时检查。"
if hardware_request
else "平台、矩阵和策略均限制为 dry-run;硬件发送路径未启用。"
),
@@ -721,17 +784,7 @@ class SeatDemoService:
execute_started = time.perf_counter()
if hardware_request:
hardware_executor = HardwareSeatExecutor(
self.hardware_matrix,
self.hardware_settings.bus_factory(),
thermal_timeout_ms=self.hardware_settings.thermal_timeout_ms,
ack_timeout_ms=self.hardware_settings.ack_timeout_ms,
axis_maximum_x100={
capability: int(axis["maximum_value_x100"])
for capability, axis in self.hardware_compiler.axes.items()
},
firmware_guard=self.hardware_settings.firmware_guard(),
)
hardware_executor = self._new_hardware_executor()
execution_report = hardware_executor.execute(
command_plan, function_config, context["devices"]
)
@@ -821,6 +874,14 @@ class SeatDemoService:
state_actions,
parsed["decision_summary"],
)
physical_state = execution_report.get("spec", {}).get(
"physical_state"
)
if hardware_request and isinstance(physical_state, dict):
updated_context = self.conversations.synchronize_physical_passenger(
conversation_id,
physical_state,
)
updated_context = self._context_for_vehicle(
updated_context,
vehicle_profile,
@@ -837,6 +898,34 @@ class SeatDemoService:
time.perf_counter(),
{"state_after": updated_context["devices"]},
)
elif hardware_request:
physical_state = execution_report.get("spec", {}).get(
"physical_state"
)
if isinstance(physical_state, dict) and (
physical_state.get("position")
or physical_state.get("ventilation")
or physical_state.get("heating")
):
updated_context = self.conversations.synchronize_physical_passenger(
conversation_id,
physical_state,
)
updated_context = self._context_for_vehicle(
updated_context,
vehicle_profile,
)
result["actuator_state"] = updated_context["devices"]
result["domain_state"] = updated_context["domain_devices"]
self._trace(
traces,
request_id,
"state_sync",
"succeeded",
"真实执行未完成,但已按 CAN 物理回读同步副驾状态。",
time.perf_counter(),
{"state_after": updated_context["devices"]},
)
telemetry = next(
(
item.get("telemetry", {})
@@ -944,34 +1033,60 @@ class SeatDemoService:
raw = telemetry.get("feedback_value")
if raw is not None:
raw_value = int(raw)
action["state"] = "off" if raw_value == 1 else "on"
action["state"] = "off" if raw_value <= 1 else "on"
action["requested_level"] = max(0, raw_value - 1)
synchronized = list(updated)
existing = {
(
item.get("target"),
item.get("action_type"),
item.get("function"),
item.get("axis"),
)
for item in synchronized
}
physical_state = execution_report.get("spec", {}).get("physical_state")
if isinstance(physical_state, dict):
positions = physical_state.get("position", {})
by_axis = {
action.get("axis"): action
for action in updated
if action.get("action_type") == "position"
}
for axis, feedback in positions.items():
if not isinstance(feedback, dict) or feedback.get("position_pct") is None:
continue
action = by_axis.get(axis)
if action is None:
action = {
"domain": "seat",
"target": "passenger",
"action_type": "position",
"axis": axis,
}
updated.append(action)
by_axis[axis] = action
action["position_pct"] = int(feedback["position_pct"])
if feedback.get("position_valid") is not None:
action["position_valid"] = bool(feedback["position_valid"])
action["feedback_basis"] = "physical_can_snapshot"
by_function = {
action.get("function"): action
for action in updated
if action.get("function") in {"ventilation", "heating"}
}
for feature in ("ventilation", "heating"):
feedback = physical_state.get(feature)
if not isinstance(feedback, dict):
continue
action = by_function.get(feature)
if action is None:
action = {
"domain": "seat",
"target": "passenger",
"function": feature,
}
updated.append(action)
by_function[feature] = action
action["state"] = (
"on" if feedback.get("state") == "on" else "off"
)
action["requested_level"] = int(feedback.get("level", 0))
for action in updated:
counterpart = "passenger" if action.get("target") == "driver" else "driver"
signature = (
counterpart,
action.get("action_type"),
action.get("function"),
action.get("axis"),
)
if signature in existing:
continue
mirrored = deepcopy(action)
mirrored["target"] = counterpart
mirrored["physical_target_alias"] = "t2g_primary_seat"
synchronized.append(mirrored)
existing.add(signature)
return synchronized
action["physical_target_alias"] = "t2g_primary_seat"
return updated
@staticmethod
def _parse_mode_profile_save_name(utterance: str) -> str | None:
@@ -1048,6 +1163,13 @@ class SeatDemoService:
context,
vehicle_profile,
)
parsed_has_explicit_request = bool(
parsed.get("actions")
or parsed.get("domain_actions")
or parsed.get("preset_actions")
or parsed.get("preset_events")
or parsed.get("model_requests")
)
merged = deepcopy(parsed)
merged["actions"] = self._merge_control_actions(
mode_parsed.get("actions", []),
@@ -1064,9 +1186,15 @@ class SeatDemoService:
"ambiguities",
"model_requests",
):
parsed_values = parsed.get(key, [])
if key == "ambiguities" and not parsed_has_explicit_request:
# A local parser does not know arbitrary named modes and emits
# a generic unsupported-capability ambiguity. A unique,
# repository-backed mode match is authoritative for that case.
parsed_values = []
merged[key] = self._merge_unique(
mode_parsed.get(key, []),
parsed.get(key, []),
parsed_values,
)
merged["decision_summary"] = [
*mode_parsed.get("decision_summary", []),
@@ -1839,8 +1967,12 @@ class SeatDemoService:
parsed: dict[str, Any],
result: dict[str, Any],
traces: list[dict[str, Any]],
*,
hardware_request: bool = False,
vehicle_profile: VehicleProfile | None = None,
) -> dict[str, Any]:
"""Run one or more isolated domain adapters and aggregate their reports."""
"""Run isolated adapters, with only the passenger seat eligible for HIL."""
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", []):
@@ -1942,6 +2074,8 @@ class SeatDemoService:
"message": "模型没有生成任何可执行领域操作。",
}
)
if hardware_request:
self._adapt_validation_for_hardware(validation)
result["validation"] = validation
errors = [issue for issue in validation if issue["severity"] == "error"]
self._trace(
@@ -1971,7 +2105,14 @@ class SeatDemoService:
plans: dict[str, dict[str, Any]] = {}
for domain, config in configs.items():
if domain == "seat":
plans[domain] = self.compiler.compile(config)
if hardware_request:
self.hardware_settings.validate_function_config(config)
plans[domain] = self.hardware_compiler.compile(
config,
context["devices"],
)
else:
plans[domain] = self.compiler.compile(config)
else:
runtime = self.domain_runtimes[domain]
plans[domain] = runtime.compile(config)
@@ -2021,22 +2162,38 @@ class SeatDemoService:
request_id,
"safety_check",
"succeeded",
"所有领域均为 dry-run;矩阵缺少实车完整性信息,硬件发送保持禁用。",
(
"副驾座椅已通过 T2G HIL 安全门,其他座舱领域保持虚拟 CAN。"
if hardware_request
else "所有领域均为 dry-run;矩阵缺少实车完整性信息,硬件发送保持禁用。"
),
time.perf_counter(),
{
"hardware_execution_allowed": False,
"hardware_execution_allowed": hardware_request,
"hardware_domains": ["seat"] if hardware_request else [],
"virtual_domains": [
domain
for domain in configs
if not hardware_request or domain != "seat"
],
"domains": list(configs),
},
domain="orchestration",
)
execute_started = time.perf_counter()
execution_started_at = _now()
reports: dict[str, dict[str, Any]] = {}
for domain, config in configs.items():
if domain == "seat":
reports[domain] = self.executor.execute(
plans[domain], config, context["devices"]
)
if hardware_request:
reports[domain] = self._new_hardware_executor().execute(
plans[domain], config, context["devices"]
)
else:
reports[domain] = self.executor.execute(
plans[domain], config, context["devices"]
)
else:
reports[domain] = self.domain_runtimes[domain].execute(
plans[domain], config
@@ -2046,28 +2203,47 @@ class SeatDemoService:
for domain_report in reports.values()
for item in domain_report["spec"]["results"]
]
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)
physical_state = (
reports.get("seat", {}).get("spec", {}).get("physical_state")
if hardware_request
else None
)
report_spec: dict[str, Any] = {
"mode": "hil" if hardware_request else "dry_run",
"status": "succeeded" if execution_succeeded else "failed",
"started_at": execution_started_at,
"finished_at": _now(),
"results": report_results,
}
if isinstance(physical_state, dict):
report_spec["physical_state"] = physical_state
execution_report = {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "ExecutionReport",
"metadata": {
"id": f"report-{request_id}",
"name": "Cabin orchestration virtual execution report",
"name": (
"Cabin orchestration mixed HIL execution report"
if hardware_request
else "Cabin orchestration virtual execution report"
),
"version": "1.0.0",
"request_id": request_id,
"labels": {
"simulation": "true",
"simulation": "mixed" if hardware_request else "true",
"seat_feedback": (
"physical_can" if hardware_request else "simulated"
),
"orchestration": "cross_domain"
if len(configs) > 1
else "single_domain",
},
},
"spec": {
"mode": "dry_run",
"status": "succeeded",
"started_at": _now(),
"finished_at": _now(),
"results": report_results,
},
"spec": report_spec,
}
report_issues = self.contracts.validate(execution_report)
validation.extend(report_issues)
@@ -2075,36 +2251,67 @@ class SeatDemoService:
result["validation"] = validation
if report_issues:
result["status"] = "execution_report_invalid"
elif not execution_succeeded:
result["status"] = "execution_failed"
elif parsed.get("deferred_requests"):
result["status"] = "partially_succeeded"
else:
result["status"] = "succeeded"
failed_execution = next(
(item for item in report_results if item.get("status") == "failed"),
None,
)
self._trace(
traces,
request_id,
"execute",
"failed" if report_issues else "succeeded",
f"已虚拟执行 {len(report_results)} 个领域操作并汇总反馈。",
"succeeded" if execution_succeeded and not report_issues else "failed",
(
"副驾座椅已按物理 CAN 回读闭环执行,其他领域已完成虚拟 CAN 执行。"
if hardware_request and execution_succeeded
else (
f"副驾座椅真实执行失败:{failed_execution['message']}"
if hardware_request and failed_execution
else f"已虚拟执行 {len(report_results)} 个领域操作并汇总反馈。"
)
),
execute_started,
{
"mode": "dry_run",
"mode": execution_report["spec"]["mode"],
"simulation": execution_report["metadata"]["labels"]["simulation"],
"domains": list(configs),
"result_count": len(report_results),
"failed_domain": (
failed_execution.get("domain") if failed_execution else None
),
},
domain="orchestration",
)
if not report_issues:
all_actions = [
*parsed.get("actions", []),
*parsed.get("domain_actions", []),
]
if execution_succeeded and not report_issues:
seat_actions = parsed.get("actions", [])
if hardware_request:
seat_actions = self._hardware_feedback_actions(
seat_actions,
configs["seat"],
reports["seat"],
)
all_actions = [*seat_actions, *parsed.get("domain_actions", [])]
updated_context = self.conversations.apply_successful_actions(
conversation_id,
utterance,
all_actions,
parsed["decision_summary"],
)
if hardware_request and isinstance(physical_state, dict):
updated_context = self.conversations.synchronize_physical_passenger(
conversation_id,
physical_state,
)
updated_context = self._context_for_vehicle(
updated_context,
vehicle_profile,
)
result["conversation_turn"] = updated_context["turn_count"]
result["actuator_state"] = updated_context["devices"]
result["domain_state"] = updated_context["domain_devices"]
@@ -2121,6 +2328,31 @@ class SeatDemoService:
},
domain="orchestration",
)
elif hardware_request and isinstance(physical_state, dict) and (
physical_state.get("position")
or physical_state.get("ventilation")
or physical_state.get("heating")
):
updated_context = self.conversations.synchronize_physical_passenger(
conversation_id,
physical_state,
)
updated_context = self._context_for_vehicle(
updated_context,
vehicle_profile,
)
result["actuator_state"] = updated_context["devices"]
result["domain_state"] = updated_context["domain_devices"]
self._trace(
traces,
request_id,
"state_sync",
"succeeded",
"跨域真实执行未完成,但已按 CAN 回读同步副驾物理状态。",
time.perf_counter(),
{"seat_state": updated_context["devices"].get("passenger", {})},
domain="orchestration",
)
self._persist(result)
return result
@@ -2186,12 +2418,19 @@ class SeatDemoService:
if provider == "openai":
if not self.openai.available:
raise ValueError("GPT 模式需要设置 OPENAI_API_KEY")
plan = self.openai.interpret(utterance, context, self.capability_context)
return (
self.model_plan_adapter.adapt(plan, context, vehicle_profile),
f"openai:{self.openai.model}",
None,
)
try:
plan = self.openai.interpret(utterance, context, self.capability_context)
return (
self.model_plan_adapter.adapt(plan, context, vehicle_profile),
f"openai:{self.openai.model}",
None,
)
except OpenAIIntentError:
return (
parse_local_intent(utterance, context),
"local_rules_fallback",
"GPT 调用失败,已降级到本地规则解析;CAN 编译和执行路径不变。",
)
if self.openai.available:
try:
plan = self.openai.interpret(utterance, context, self.capability_context)