init
This commit is contained in:
+391
@@ -0,0 +1,391 @@
|
||||
"""Configuration and fail-closed gates for the T2G seat hardware bench."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from cognitive_agent.infrastructure.firmware import (
|
||||
EXPECTED_IDENTITY_PAYLOAD,
|
||||
FirmwareProgrammingError,
|
||||
T2GFirmwareGuard,
|
||||
T2GSeatProgrammer,
|
||||
)
|
||||
|
||||
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"),
|
||||
}
|
||||
CAPABILITY_PROFILE = {
|
||||
"seat.set_heating": "combined",
|
||||
"seat.set_ventilation": "combined",
|
||||
"seat.set_longitudinal_position": "combined",
|
||||
"seat.set_backrest_recline": "combined",
|
||||
"seat.set_legrest_height": "combined",
|
||||
"seat.set_legrest_extension": "combined",
|
||||
}
|
||||
|
||||
|
||||
class HardwareSafetyError(ValueError):
|
||||
"""Raised before any CAN device is opened when a safety gate fails."""
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool = False) -> bool:
|
||||
value = os.getenv(name)
|
||||
return default if value is None else value.strip().lower() in TRUE_VALUES
|
||||
|
||||
|
||||
def _env_int(name: str, default: int) -> int:
|
||||
value = os.getenv(name)
|
||||
if value is None or not value.strip():
|
||||
return default
|
||||
try:
|
||||
return int(value, 0)
|
||||
except ValueError as exc:
|
||||
raise HardwareSafetyError(f"{name} must be an integer") from exc
|
||||
|
||||
|
||||
def _profiles_from_env() -> frozenset[str]:
|
||||
raw = os.getenv(
|
||||
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROFILES",
|
||||
os.getenv("COGNITIVE_AGENT_SEAT_FIRMWARE_PROFILE", ""),
|
||||
)
|
||||
profiles = frozenset(
|
||||
item.strip().lower() for item in raw.replace(";", ",").split(",") if item.strip()
|
||||
)
|
||||
invalid = profiles - VALID_PROFILES
|
||||
if invalid:
|
||||
raise HardwareSafetyError(
|
||||
"unknown seat firmware profile(s): " + ", ".join(sorted(invalid))
|
||||
)
|
||||
return profiles
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SeatHardwareSettings:
|
||||
"""Environment-owned hardware configuration; secrets are never made public."""
|
||||
|
||||
repository_root: Path
|
||||
enabled: bool
|
||||
approval_token: str
|
||||
profiles: frozenset[str]
|
||||
backend: str
|
||||
bitrate: int
|
||||
channel: str
|
||||
interface: str
|
||||
device_type: int
|
||||
device_index: int
|
||||
zlg_python: Path
|
||||
zlg_dll: Path
|
||||
bridge_script: Path
|
||||
sdk_root: Path
|
||||
cspybat: Path
|
||||
auto_flash_enabled: bool
|
||||
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]
|
||||
firmware_debug_files: dict[str, Path]
|
||||
firmware_macro_files: dict[str, Path]
|
||||
|
||||
@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()
|
||||
backend = os.getenv("COGNITIVE_AGENT_SEAT_CAN_BACKEND", "zlgcan").strip().lower()
|
||||
if backend not in {"zlgcan", "python-can"}:
|
||||
raise HardwareSafetyError(
|
||||
"COGNITIVE_AGENT_SEAT_CAN_BACKEND must be zlgcan or python-can"
|
||||
)
|
||||
firmware_root = (
|
||||
repository_root / "firmware" / "t2g" / "Seat_Controller_demo"
|
||||
)
|
||||
images = {
|
||||
"combined": firmware_root
|
||||
/ "IAR"
|
||||
/ "rev_d"
|
||||
/ "Exe"
|
||||
/ "Seat_Controller_demo.hex",
|
||||
}
|
||||
debug_files = {
|
||||
"combined": firmware_root
|
||||
/ "IAR"
|
||||
/ "rev_d"
|
||||
/ "Exe"
|
||||
/ "Seat_Controller_demo.out",
|
||||
}
|
||||
macro_files = {
|
||||
"combined": firmware_root / "IAR" / "Seat_Controller_program.mac",
|
||||
}
|
||||
return cls(
|
||||
repository_root=repository_root,
|
||||
enabled=_env_bool("COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED"),
|
||||
approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN", ""),
|
||||
profiles=_profiles_from_env(),
|
||||
backend=backend,
|
||||
bitrate=_env_int("COGNITIVE_AGENT_SEAT_CAN_BITRATE", 500_000),
|
||||
channel=os.getenv("COGNITIVE_AGENT_SEAT_CAN_CHANNEL", "0").strip(),
|
||||
interface=os.getenv("COGNITIVE_AGENT_SEAT_CAN_INTERFACE", "socketcan").strip(),
|
||||
device_type=_env_int("COGNITIVE_AGENT_ZLGCAN_DEVICE_TYPE", 41),
|
||||
device_index=_env_int("COGNITIVE_AGENT_ZLGCAN_DEVICE_INDEX", 0),
|
||||
zlg_python=Path(
|
||||
os.getenv(
|
||||
"COGNITIVE_AGENT_ZLGCAN_PYTHON",
|
||||
r"C:\Program Files (x86)\ZCANPRO\python38\python.exe",
|
||||
)
|
||||
),
|
||||
zlg_dll=Path(
|
||||
os.getenv(
|
||||
"COGNITIVE_AGENT_ZLGCAN_DLL",
|
||||
r"C:\Program Files (x86)\ZCANPRO\zlgcan.dll",
|
||||
)
|
||||
),
|
||||
bridge_script=repository_root
|
||||
/ "src"
|
||||
/ "cognitive_agent"
|
||||
/ "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"),
|
||||
firmware_probe_timeout_ms=_env_int(
|
||||
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROBE_TIMEOUT_MS", 750
|
||||
),
|
||||
firmware_boot_wait_ms=_env_int(
|
||||
"COGNITIVE_AGENT_SEAT_FIRMWARE_BOOT_WAIT_MS", 1_500
|
||||
),
|
||||
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
|
||||
),
|
||||
ack_timeout_ms=_env_int("COGNITIVE_AGENT_SEAT_ACK_TIMEOUT_MS", 1_500),
|
||||
firmware_images=images,
|
||||
firmware_debug_files=debug_files,
|
||||
firmware_macro_files=macro_files,
|
||||
)
|
||||
|
||||
@property
|
||||
def adapter_configured(self) -> bool:
|
||||
if self.bitrate != 500_000:
|
||||
return False
|
||||
if self.backend == "zlgcan":
|
||||
return (
|
||||
self.zlg_python.is_file()
|
||||
and self.zlg_dll.is_file()
|
||||
and self.bridge_script.is_file()
|
||||
)
|
||||
return bool(self.interface and self.channel)
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(
|
||||
self.enabled
|
||||
and self.approval_token
|
||||
and self.profiles
|
||||
and self.adapter_configured
|
||||
and all(self.firmware_images[item].is_file() for item in self.profiles)
|
||||
and (not self.auto_flash_enabled or self.programmer_configured)
|
||||
)
|
||||
|
||||
@property
|
||||
def programmer_configured(self) -> bool:
|
||||
if not self.profiles:
|
||||
return False
|
||||
try:
|
||||
return all(self._programmer(item).configured for item in self.profiles)
|
||||
except (FirmwareProgrammingError, OSError):
|
||||
return False
|
||||
|
||||
def authorize(self, supplied_token: str | None) -> None:
|
||||
if not self.enabled:
|
||||
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 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 self.adapter_configured:
|
||||
raise HardwareSafetyError("the configured CAN adapter runtime is not ready")
|
||||
missing = [
|
||||
str(self.firmware_images[item])
|
||||
for item in self.profiles
|
||||
if not self.firmware_images[item].is_file()
|
||||
]
|
||||
if missing:
|
||||
raise HardwareSafetyError(
|
||||
"selected IAR firmware image is missing: " + ", ".join(missing)
|
||||
)
|
||||
if self.auto_flash_enabled and not self.programmer_configured:
|
||||
raise HardwareSafetyError(
|
||||
"automatic seat-firmware programming is enabled but IAR C-SPY, "
|
||||
"the repository OUT image, or the C-SPY macro is unavailable"
|
||||
)
|
||||
|
||||
def validate_function_config(self, function_config: dict[str, Any]) -> None:
|
||||
operations = function_config.get("spec", {}).get("operations", [])
|
||||
if not operations:
|
||||
raise HardwareSafetyError("hardware execution requires at least one seat operation")
|
||||
physical_requests: dict[str, dict[str, Any]] = {}
|
||||
for operation in operations:
|
||||
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:
|
||||
raise HardwareSafetyError(
|
||||
"the T2G bench accepts driver/front_left and passenger/front_right; "
|
||||
"both aliases operate the same physical seat actuators"
|
||||
)
|
||||
try:
|
||||
profile = CAPABILITY_PROFILE[capability]
|
||||
except KeyError as exc:
|
||||
raise HardwareSafetyError(
|
||||
f"the T2G IAR firmware does not implement {capability}"
|
||||
) from exc
|
||||
if profile not in self.profiles:
|
||||
raise HardwareSafetyError(
|
||||
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":
|
||||
try:
|
||||
numeric_channel = int(self.channel, 0)
|
||||
except ValueError as exc:
|
||||
raise CanGatewayError(
|
||||
"ZLG CAN channel must be an integer such as 0"
|
||||
) from exc
|
||||
|
||||
def create_zlg_bus() -> CanBus:
|
||||
return ZlgCanBridgeBus(
|
||||
python_executable=self.zlg_python,
|
||||
bridge_script=self.bridge_script,
|
||||
dll_path=self.zlg_dll,
|
||||
device_type=self.device_type,
|
||||
device_index=self.device_index,
|
||||
channel=numeric_channel,
|
||||
bitrate=self.bitrate,
|
||||
)
|
||||
|
||||
return create_zlg_bus
|
||||
|
||||
def create_python_can_bus() -> CanBus:
|
||||
return PythonCanBus(
|
||||
interface=self.interface,
|
||||
channel=self.channel,
|
||||
bitrate=self.bitrate,
|
||||
)
|
||||
|
||||
return create_python_can_bus
|
||||
|
||||
def _programmer(self, profile: str = "combined") -> T2GSeatProgrammer:
|
||||
return T2GSeatProgrammer(
|
||||
cspybat=self.cspybat,
|
||||
debug_file=self.firmware_debug_files[profile],
|
||||
hex_image=self.firmware_images[profile],
|
||||
macro_file=self.firmware_macro_files[profile],
|
||||
timeout_ms=self.firmware_program_timeout_ms,
|
||||
)
|
||||
|
||||
def firmware_guard(self) -> T2GFirmwareGuard:
|
||||
return T2GFirmwareGuard(
|
||||
self.bus_factory(),
|
||||
self._programmer() if self.auto_flash_enabled else None,
|
||||
auto_flash=self.auto_flash_enabled,
|
||||
probe_timeout_ms=self.firmware_probe_timeout_ms,
|
||||
boot_wait_ms=self.firmware_boot_wait_ms,
|
||||
)
|
||||
|
||||
def public_info(self) -> dict[str, Any]:
|
||||
images = {}
|
||||
for profile, path in self.firmware_images.items():
|
||||
exists = path.is_file()
|
||||
images[profile] = {
|
||||
"exists": exists,
|
||||
"filename": path.name,
|
||||
"sha256": (
|
||||
hashlib.sha256(path.read_bytes()).hexdigest() if exists else None
|
||||
),
|
||||
}
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"configured": self.configured,
|
||||
"approval_required": True,
|
||||
"approval_configured": bool(self.approval_token),
|
||||
"platform": "t2g_seat_bench",
|
||||
"backend": self.backend,
|
||||
"adapter_configured": self.adapter_configured,
|
||||
"channel": self.channel,
|
||||
"bitrate": self.bitrate,
|
||||
"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_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"],
|
||||
"physical_target": "t2g_primary_seat",
|
||||
"target_aliasing": "driver_and_passenger_share_one_physical_actuator_group",
|
||||
"unverified_longitudinal_enabled": self.allow_unverified_longitudinal,
|
||||
"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",
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user