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
Binary file not shown.
Binary file not shown.
@@ -170,6 +170,63 @@ class ConversationStateStore:
state = self._state(conversation_id)
return deepcopy(state)
def synchronize_physical_passenger(
self,
conversation_id: str,
physical_state: dict[str, Any],
) -> dict[str, Any]:
"""Merge authoritative bench feedback into passenger state only."""
with self._lock:
state = self._state(conversation_id)
passenger = state["devices"]["passenger"]
for axis, feedback in physical_state.get("position", {}).items():
if axis not in MOTION_INITIAL_POSITIONS or not isinstance(
feedback, dict
):
continue
raw_position = feedback.get("position_pct")
if (
not isinstance(raw_position, (int, float))
or isinstance(raw_position, bool)
):
continue
position = max(0, min(100, round(float(raw_position))))
position_state = _motion_state(str(axis), position)
position_state["motion_state"] = str(
feedback.get("motion_state", "stopped")
)
position_state["fault"] = bool(feedback.get("fault", False))
if feedback.get("position_valid") is not None:
position_state["position_valid"] = bool(
feedback["position_valid"]
)
position_state["feedback_source"] = "physical_can"
passenger["position"][str(axis)] = position_state
for feature in THERMAL_FUNCTIONS:
feedback = physical_state.get(feature)
if not isinstance(feedback, dict):
continue
requested_state = str(feedback.get("state", "off"))
state_value = "on" if requested_state == "on" else "off"
level = (
max(1, min(3, int(feedback.get("level", 1))))
if state_value == "on"
else 0
)
passenger[feature] = {
"state": state_value,
"level": level,
"feedback_source": "physical_can",
}
state["physical_state_updated_at"] = str(
physical_state.get("captured_at") or _now()
)
state["updated_at"] = _now()
return deepcopy(state)
def apply_successful_actions(
self,
conversation_id: str,
@@ -329,11 +329,11 @@ class ModelPlanAdapter:
ambiguities: list[dict[str, str]],
) -> dict[str, str] | None:
target = request.get("target_scope")
if target not in {"driver", "passenger"}:
if target != "passenger":
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "座椅姿态预设需要明确主驾或副驾。",
"question": "当前座椅能力仅控制副驾,请将目标指定为 passenger。",
}
)
return None
@@ -379,11 +379,11 @@ class ModelPlanAdapter:
vehicle_profile: VehicleProfile | None = None,
) -> dict[str, Any] | None:
target = request.get("target_scope")
if target not in {"driver", "passenger"}:
if target != "passenger":
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "座椅执行适配器需要 driver 或 passenger 目标。",
"question": "当前座椅能力仅控制副驾,请将目标指定为 passenger。",
}
)
return None
@@ -16,12 +16,12 @@ ANGLE_CALIBRATIONS = {
"seat.set_backrest_recline": AngleCalibration(
axis="backrest_recline",
minimum_deg=95,
maximum_deg=140,
maximum_deg=163,
),
"seat.set_legrest_height": AngleCalibration(
axis="legrest_height",
minimum_deg=95,
maximum_deg=160,
maximum_deg=167,
),
}
AXIS_ANGLE_CALIBRATIONS = {
+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)
@@ -2,4 +2,4 @@
公共 CAN Gateway 负责加载 DBC、编码 message/signal、选择总线、限速、发送、订阅回读和记录审计。默认实现使用虚拟总线;只有平台 manifest、部署环境和权限同时允许时才能启用硬件 backend。
T2G 座椅台架新增 `PythonCanBus` 和 `ZlgCanBridgeBus`。后者使用 ZCANPRO 自带的 32 位 Python 隔离加载 `zlgcan.dll`,主服务仍运行在 64 位 Python。硬件执行要求环境开关、请求授权令牌、固件 profile、适配器依赖、目标座椅和标定状态全部通过;详见 [T2G 台架接入](../../../docs/workflows/t2g-seat-hardware-bench.md)。
T2G 座椅台架新增 `PythonCanBus` 和 `ZlgCanBridgeBus`。后者使用 ZCANPRO 自带的 32 位 Python 隔离加载 `zlgcan.dll`,主服务仍运行在 64 位 Python。硬件执行要求环境开关、固件 profile、适配器依赖、目标座椅和标定状态全部通过;生产默认还要求请求授权令牌,本地受控调试可显式关闭令牌门禁。详见 [T2G 台架接入](../../../docs/workflows/t2g-seat-hardware-bench.md)。
@@ -10,7 +10,7 @@ from pathlib import Path
from typing import Any, Callable
from cognitive_agent.infrastructure.firmware import (
EXPECTED_IDENTITY_PAYLOAD,
FIRMWARE_PROTOCOL_SIGNATURE,
FirmwareProgrammingError,
T2GFirmwareGuard,
T2GSeatProgrammer,
@@ -21,17 +21,14 @@ from .gateway import CanBus, CanGatewayError, PythonCanBus, ZlgCanBridgeBus
TRUE_VALUES = {"1", "true", "yes", "on"}
VALID_PROFILES = {"combined"}
SHARED_PHYSICAL_TARGETS = {
("front_left", "driver"),
("front_right", "passenger"),
}
SUPPORTED_PHYSICAL_TARGET = ("front_right", "passenger")
CAPABILITY_PROFILE = {
"seat.set_heating": "combined",
"seat.set_ventilation": "combined",
"seat.set_longitudinal_position": "combined",
"seat.set_cushion_tilt": "combined",
"seat.set_backrest_recline": "combined",
"seat.set_legrest_height": "combined",
"seat.set_legrest_extension": "combined",
}
@@ -76,6 +73,7 @@ class SeatHardwareSettings:
repository_root: Path
enabled: bool
approval_required: bool
approval_token: str
profiles: frozenset[str]
backend: str
@@ -87,13 +85,13 @@ class SeatHardwareSettings:
zlg_python: Path
zlg_dll: Path
bridge_script: Path
sdk_root: Path
cspybat: Path
auto_flash_enabled: bool
auto_home_enabled: bool
home_timeout_s: int
firmware_probe_timeout_ms: int
firmware_boot_wait_ms: int
firmware_program_timeout_ms: int
allow_unverified_longitudinal: bool
thermal_timeout_ms: int
ack_timeout_ms: int
firmware_images: dict[str, Path]
@@ -102,12 +100,11 @@ class SeatHardwareSettings:
@classmethod
def from_env(cls, repository_root: Path) -> "SeatHardwareSettings":
sdk_root = Path(
os.getenv(
"COGNITIVE_AGENT_T2G_SDK_ROOT",
r"C:\Users\D30042075206\Desktop\T2G_Sample_Driver_Library_7.9.0",
)
).expanduser()
hardware_enabled = _env_bool("COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED")
auto_flash_enabled = _env_bool(
"COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED",
default=hardware_enabled,
)
backend = os.getenv("COGNITIVE_AGENT_SEAT_CAN_BACKEND", "zlgcan").strip().lower()
if backend not in {"zlgcan", "python-can"}:
raise HardwareSafetyError(
@@ -135,7 +132,11 @@ class SeatHardwareSettings:
}
return cls(
repository_root=repository_root,
enabled=_env_bool("COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED"),
enabled=hardware_enabled,
approval_required=_env_bool(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED",
default=True,
),
approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN", ""),
profiles=_profiles_from_env(),
backend=backend,
@@ -162,16 +163,19 @@ class SeatHardwareSettings:
/ "infrastructure"
/ "can"
/ "zlgcan_bridge.py",
sdk_root=sdk_root,
cspybat=Path(
os.getenv(
"COGNITIVE_AGENT_IAR_CSPYBAT",
r"D:\Program Files\IAR Systems\Embedded Workbench 9.2\common\bin\cspybat.exe",
)
),
auto_flash_enabled=_env_bool("COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED"),
auto_flash_enabled=auto_flash_enabled,
auto_home_enabled=_env_bool("COGNITIVE_AGENT_SEAT_AUTO_HOME_ENABLED"),
home_timeout_s=_env_int(
"COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S", 120
),
firmware_probe_timeout_ms=_env_int(
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROBE_TIMEOUT_MS", 750
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROBE_TIMEOUT_MS", 1_200
),
firmware_boot_wait_ms=_env_int(
"COGNITIVE_AGENT_SEAT_FIRMWARE_BOOT_WAIT_MS", 1_500
@@ -179,9 +183,6 @@ class SeatHardwareSettings:
firmware_program_timeout_ms=_env_int(
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROGRAM_TIMEOUT_MS", 120_000
),
allow_unverified_longitudinal=_env_bool(
"COGNITIVE_AGENT_ENABLE_UNVERIFIED_LONGITUDINAL"
),
thermal_timeout_ms=_env_int(
"COGNITIVE_AGENT_SEAT_THERMAL_TIMEOUT_MS", 2_000
),
@@ -207,7 +208,7 @@ class SeatHardwareSettings:
def configured(self) -> bool:
return bool(
self.enabled
and self.approval_token
and (not self.approval_required or self.approval_token)
and self.profiles
and self.adapter_configured
and all(self.firmware_images[item].is_file() for item in self.profiles)
@@ -228,20 +229,25 @@ class SeatHardwareSettings:
raise HardwareSafetyError(
"hardware execution is disabled; set COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED=true"
)
if not self.approval_token:
raise HardwareSafetyError(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN is not configured"
)
if not supplied_token or not hmac.compare_digest(
supplied_token, self.approval_token
):
raise HardwareSafetyError("hardware approval token is invalid")
if self.approval_required:
if not self.approval_token:
raise HardwareSafetyError(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN is not configured"
)
if not supplied_token or not hmac.compare_digest(
supplied_token, self.approval_token
):
raise HardwareSafetyError("hardware approval token is invalid")
if not self.profiles:
raise HardwareSafetyError(
"the combined IAR seat-controller firmware profile is not selected"
)
if self.bitrate != 500_000:
raise HardwareSafetyError("the verified T2G seat protocol requires 500000 bit/s")
if not 10 <= self.home_timeout_s <= 600:
raise HardwareSafetyError(
"COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S must be between 10 and 600"
)
if not self.adapter_configured:
raise HardwareSafetyError("the configured CAN adapter runtime is not ready")
missing = [
@@ -268,10 +274,9 @@ class SeatHardwareSettings:
capability = str(operation.get("capability", ""))
target = operation.get("target", {})
target_key = (str(target.get("zone", "")), str(target.get("instance", "")))
if target_key not in SHARED_PHYSICAL_TARGETS:
if target_key != SUPPORTED_PHYSICAL_TARGET:
raise HardwareSafetyError(
"the T2G bench accepts driver/front_left and passenger/front_right; "
"both aliases operate the same physical seat actuators"
"the T2G bench accepts only passenger/front_right seat operations"
)
try:
profile = CAPABILITY_PROFILE[capability]
@@ -284,20 +289,7 @@ class SeatHardwareSettings:
f"{capability} requires the '{profile}' firmware profile"
)
parameters = dict(operation.get("parameters", {}))
previous = physical_requests.get(capability)
if previous is not None and previous != parameters:
raise HardwareSafetyError(
f"driver and passenger requests conflict for shared physical capability {capability}"
)
physical_requests[capability] = parameters
if (
capability == "seat.set_longitudinal_position"
and not self.allow_unverified_longitudinal
):
raise HardwareSafetyError(
"longitudinal Motor3 is unwired/unverified; explicitly set "
"COGNITIVE_AGENT_ENABLE_UNVERIFIED_LONGITUDINAL=true only after calibration"
)
def bus_factory(self) -> Callable[[], CanBus]:
if self.backend == "zlgcan":
@@ -362,7 +354,7 @@ class SeatHardwareSettings:
return {
"enabled": self.enabled,
"configured": self.configured,
"approval_required": True,
"approval_required": self.approval_required,
"approval_configured": bool(self.approval_token),
"platform": "t2g_seat_bench",
"backend": self.backend,
@@ -372,20 +364,23 @@ class SeatHardwareSettings:
"frame_format": "classic_can_11_bit",
"firmware_profiles": sorted(self.profiles),
"firmware_images": images,
"firmware_status": "identity_verified_before_each_hardware_request",
"firmware_identity": EXPECTED_IDENTITY_PAYLOAD.hex(" ").upper(),
"firmware_status": "passive_protocol_verified_before_each_hardware_request",
"firmware_protocol_signature": FIRMWARE_PROTOCOL_SIGNATURE,
"firmware_source": "repository",
"auto_flash_enabled": self.auto_flash_enabled,
"programmer_configured": self.programmer_configured,
"programmer_backend": "iar_cspy_cmsisdap",
"supported_targets": ["front_left", "front_right"],
"supported_targets": ["front_right"],
"supported_seat_instances": ["passenger"],
"physical_target": "t2g_primary_seat",
"target_aliasing": "driver_and_passenger_share_one_physical_actuator_group",
"unverified_longitudinal_enabled": self.allow_unverified_longitudinal,
"target_aliasing": "disabled_front_passenger_only",
"auto_home_enabled": self.auto_home_enabled,
"home_timeout_s": self.home_timeout_s,
"limitations": [
"hardware execution requires the unified Seat_Controller_demo image on one development board",
"driver and passenger commands are aliases for the same physical motor/heater/fan group",
"Motor3 longitudinal motion stays blocked until explicitly enabled after wiring and calibration",
"cushion tilt has no physical actuator in Seat_Move_demo",
"all seat capabilities are restricted to passenger/front_right",
"M0/M1/M2/M3 map to horizontal, seat-pan, backrest and legrest on one physical seat",
"known-range homing can move an invalid axis to its near endpoint and remains separately disabled by default",
"current diagnostics use conservative bench thresholds and do not replace external power limiting",
],
}
@@ -92,7 +92,9 @@ class Driver:
)
if not self.device:
raise RuntimeError(
"ZCAN_OpenDevice failed; connect the adapter and close ZCANPRO first"
f"ZCAN_OpenDevice failed for device_type={args.device_type}, "
f"device_index={args.device_index}; connect the adapter and close "
"ZCANPRO first"
)
self.channel = None
try:
@@ -1,18 +1,18 @@
"""Firmware verification and programming infrastructure."""
from .t2g_seat import (
EXPECTED_IDENTITY_PAYLOAD,
FIRMWARE_IDENTITY_REQUEST_ID,
FIRMWARE_IDENTITY_RESPONSE_ID,
EXPECTED_MOTOR_MAX_HALL,
FIRMWARE_PROTOCOL_FRAME_IDS,
FIRMWARE_PROTOCOL_SIGNATURE,
FirmwareProgrammingError,
T2GFirmwareGuard,
T2GSeatProgrammer,
)
__all__ = [
"EXPECTED_IDENTITY_PAYLOAD",
"FIRMWARE_IDENTITY_REQUEST_ID",
"FIRMWARE_IDENTITY_RESPONSE_ID",
"EXPECTED_MOTOR_MAX_HALL",
"FIRMWARE_PROTOCOL_FRAME_IDS",
"FIRMWARE_PROTOCOL_SIGNATURE",
"FirmwareProgrammingError",
"T2GFirmwareGuard",
"T2GSeatProgrammer",
@@ -12,10 +12,10 @@ from typing import Any, Callable
from cognitive_agent.infrastructure.can.gateway import CanBus
FIRMWARE_IDENTITY_REQUEST_ID = 0x304
FIRMWARE_IDENTITY_RESPONSE_ID = 0x305
FIRMWARE_IDENTITY_REQUEST_PAYLOAD = bytes.fromhex("53 45 41 54 01 00 00 00")
EXPECTED_IDENTITY_PAYLOAD = bytes.fromhex("53 45 41 54 01 01 00 3F")
FIRMWARE_PROTOCOL_SIGNATURE = "seat-controller-smv8-current-diagnostics-v3"
FIRMWARE_PROTOCOL_FRAME_IDS = frozenset({0x301, 0x302, 0x303, 0x304, 0x305, 0x4FA})
EXPECTED_FIRMWARE_REVISION = 3
EXPECTED_MOTOR_MAX_HALL = (480, 374, 917, 647)
class FirmwareProgrammingError(RuntimeError):
@@ -152,7 +152,7 @@ class T2GSeatProgrammer:
class T2GFirmwareGuard:
"""Return a CAN bus only after the matching unified firmware has answered."""
"""Return a CAN bus only after the latest passive protocol signature is seen."""
def __init__(
self,
@@ -169,22 +169,66 @@ class T2GFirmwareGuard:
self.probe_timeout_ms = probe_timeout_ms
self.boot_wait_ms = boot_wait_ms
def _probe(self, bus: CanBus) -> bytes | None:
bus.send(FIRMWARE_IDENTITY_REQUEST_ID, FIRMWARE_IDENTITY_REQUEST_PAYLOAD)
def _probe(self, bus: CanBus) -> dict[str, Any] | None:
"""Observe the cyclic 0x301..0x305/0x4FA contract without transmitting.
The 2026-07-30 firmware uses 0x304 and 0x305 for current diagnostics,
so the previous request/response identity handshake would collide with
live telemetry. Requiring all new diagnostic families plus the four
verified MaxHall values distinguishes the repository image without an
actuator command or any CAN write at all.
"""
deadline = time.monotonic() + self.probe_timeout_ms / 1000
observed: bytes | None = None
observed_ids: set[int] = set()
motor_status_ids: set[int] = set()
motor_diagnostic: dict[int, int] = {}
motor_current_ids: set[int] = set()
firmware_revision: int | None = None
while True:
remaining_ms = round((deadline - time.monotonic()) * 1000)
if remaining_ms <= 0:
return observed
return None
frame = bus.receive(remaining_ms)
if frame is None:
return observed
if frame.arbitration_id != FIRMWARE_IDENTITY_RESPONSE_ID:
return None
arbitration_id = int(frame.arbitration_id)
if arbitration_id not in FIRMWARE_PROTOCOL_FRAME_IDS or len(frame.data) < 8:
continue
observed = bytes(frame.data)
if observed == EXPECTED_IDENTITY_PAYLOAD:
return observed
observed_ids.add(arbitration_id)
data = bytes(frame.data)
if arbitration_id == 0x301:
firmware_revision = data[7] >> 4
elif arbitration_id == 0x302 and data[0] < 4:
motor_status_ids.add(data[0])
elif arbitration_id == 0x303 and data[0] < 4:
motor_diagnostic[data[0]] = data[4] | (data[5] << 8)
elif arbitration_id == 0x304 and data[0] < 4:
motor_current_ids.add(data[0])
if (
observed_ids == FIRMWARE_PROTOCOL_FRAME_IDS
and firmware_revision == EXPECTED_FIRMWARE_REVISION
and motor_status_ids == {0, 1, 2, 3}
and motor_current_ids == {0, 1, 2, 3}
and set(motor_diagnostic) == {0, 1, 2, 3}
):
ranges_match = all(
abs(motor_diagnostic[motor] - expected)
<= max(4, round(expected * 0.05))
for motor, expected in enumerate(EXPECTED_MOTOR_MAX_HALL)
)
if ranges_match:
return {
"protocol_signature": FIRMWARE_PROTOCOL_SIGNATURE,
"firmware_revision": firmware_revision,
"observed_frame_ids": [
f"0x{item:03X}" for item in sorted(observed_ids)
],
"max_hall_counts": [
motor_diagnostic[motor] for motor in range(4)
],
}
def prepare_bus(self) -> tuple[CanBus, dict[str, Any]]:
bus = self.bus_factory()
@@ -194,12 +238,11 @@ class T2GFirmwareGuard:
except Exception as exc: # an unprogrammed target may not ACK the probe
observed = None
probe_error = exc
if observed == EXPECTED_IDENTITY_PAYLOAD:
if observed is not None:
return bus, {
"firmware_verified": True,
"programmed_this_request": False,
"identity_frame_id": f"0x{FIRMWARE_IDENTITY_RESPONSE_ID:03X}",
"identity_data_hex": observed.hex(" ").upper(),
**observed,
}
bus.close()
@@ -209,10 +252,9 @@ class T2GFirmwareGuard:
"Seat_Controller_demo firmware probe failed "
f"({probe_error}); automatic programming is disabled"
) from probe_error
actual = observed.hex(" ").upper() if observed is not None else "no response"
raise FirmwareProgrammingError(
"matching Seat_Controller_demo firmware was not detected "
f"({actual}); automatic programming is disabled"
"matching Seat_Controller_demo passive protocol signature was not detected; "
"automatic programming is disabled"
)
if self.programmer is None:
raise FirmwareProgrammingError(
@@ -224,11 +266,9 @@ class T2GFirmwareGuard:
verified_bus = self.bus_factory()
try:
verified = self._probe(verified_bus)
if verified != EXPECTED_IDENTITY_PAYLOAD:
actual = verified.hex(" ").upper() if verified is not None else "no response"
if verified is None:
raise FirmwareProgrammingError(
"programming completed but firmware identity verification failed: "
+ actual
"programming completed but passive firmware protocol verification failed"
)
except Exception as exc:
verified_bus.close()
@@ -241,6 +281,5 @@ class T2GFirmwareGuard:
**programming,
"firmware_verified": True,
"programmed_this_request": True,
"identity_frame_id": f"0x{FIRMWARE_IDENTITY_RESPONSE_ID:03X}",
"identity_data_hex": EXPECTED_IDENTITY_PAYLOAD.hex(" ").upper(),
**verified,
}
+29 -2
View File
@@ -14,7 +14,7 @@ from http.cookies import CookieError, SimpleCookie
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import quote, unquote, urlparse
from urllib.parse import parse_qs, quote, unquote, urlparse
from cognitive_agent.config_loader import load_local_env
@@ -62,7 +62,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
server_version = "CognitiveAgentCabinDemo/2.0"
def do_GET(self) -> None: # noqa: N802 - stdlib handler API
path = unquote(urlparse(self.path).path)
parsed_url = urlparse(self.path)
path = unquote(parsed_url.path)
if path == "/favicon.ico":
self.send_response(HTTPStatus.NO_CONTENT)
self._send_security_headers()
@@ -94,6 +95,32 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if path == "/api/runtime":
self._json(self.service.runtime_info())
return
if path == "/api/hardware/seat-state":
query = parse_qs(parsed_url.query)
conversation_id = str(query.get("conversation_id", [""])[0])
vehicle_id = str(query.get("vehicle_id", [""])[0]) or None
try:
self._json(
self.service.synchronize_hardware_state(
conversation_id,
vehicle_id,
)
)
except ValueError as exc:
self._json(
{"error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except Exception as exc: # noqa: BLE001 - surface passive CAN availability
self.log_error("physical seat-state synchronization failed: %s", exc)
self._json(
{
"error": "hardware_state_unavailable",
"message": str(exc),
},
HTTPStatus.SERVICE_UNAVAILABLE,
)
return
if path == "/api/gesture/runtime":
self._json(self.gesture.runtime_info())
return