feat: update CAN integration and T2G seat firmware
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user