feat: update CAN integration and T2G seat firmware
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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 = {
|
||||
|
||||
@@ -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)。
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user