894 lines
33 KiB
Python
Executable File
894 lines
33 KiB
Python
Executable File
"""Configuration and fail-closed gates for the T2G seat hardware bench."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Iterator
|
|
|
|
from cognitive_agent.infrastructure.firmware import (
|
|
FIRMWARE_PROTOCOL_SIGNATURE,
|
|
FirmwareProgrammingError,
|
|
T2GFirmwareGuard,
|
|
T2GSeatProgrammer,
|
|
)
|
|
|
|
from .gateway import (
|
|
CanBus,
|
|
CanGatewayError,
|
|
PythonCanBus,
|
|
ZlgCanBridgeBus,
|
|
ZlgCanBridgeProcess,
|
|
ZlgChannelConfig,
|
|
)
|
|
|
|
|
|
TRUE_VALUES = {"1", "true", "yes", "on"}
|
|
VALID_PROFILES = {"combined"}
|
|
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.start_dynamic_mode": "combined",
|
|
"seat.stop_dynamic_mode": "combined",
|
|
}
|
|
|
|
|
|
class HardwareSafetyError(ValueError):
|
|
"""Raised before any CAN device is opened when a safety gate fails."""
|
|
|
|
|
|
class HardwareBusyError(RuntimeError):
|
|
"""Raised when another request already owns the single physical bench."""
|
|
|
|
def __init__(self, retry_after_seconds: int = 5) -> None:
|
|
self.retry_after_seconds = retry_after_seconds
|
|
super().__init__(
|
|
"真实座椅台架正在执行其他请求。为避免多人指令交叉,本次请求未进入CAN链路;"
|
|
"请等待当前动作完成后重试。"
|
|
)
|
|
|
|
|
|
class HardwareExecutionCoordinator:
|
|
"""Provide process-wide, fail-fast exclusive ownership of the physical bench."""
|
|
|
|
def __init__(self, retry_after_seconds: int = 5) -> None:
|
|
self.retry_after_seconds = retry_after_seconds
|
|
self._execution_lock = threading.Lock()
|
|
self._state_lock = threading.Lock()
|
|
self._active_since: float | None = None
|
|
|
|
@contextmanager
|
|
def reserve(self) -> Iterator[None]:
|
|
if not self._execution_lock.acquire(blocking=False):
|
|
raise HardwareBusyError(self.retry_after_seconds)
|
|
with self._state_lock:
|
|
self._active_since = time.monotonic()
|
|
try:
|
|
yield
|
|
finally:
|
|
with self._state_lock:
|
|
self._active_since = None
|
|
self._execution_lock.release()
|
|
|
|
def public_info(self) -> dict[str, Any]:
|
|
with self._state_lock:
|
|
active_since = self._active_since
|
|
return {
|
|
"busy": active_since is not None,
|
|
"active_for_ms": (
|
|
max(0, int((time.monotonic() - active_since) * 1000))
|
|
if active_since is not None
|
|
else 0
|
|
),
|
|
"concurrency_policy": "single_flight_fail_fast",
|
|
"retry_after_seconds": self.retry_after_seconds,
|
|
}
|
|
|
|
|
|
PHYSICAL_BENCH_COORDINATOR = HardwareExecutionCoordinator()
|
|
|
|
|
|
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 _module_available(name: str) -> bool:
|
|
try:
|
|
return importlib.util.find_spec(name) is not None
|
|
except ModuleNotFoundError:
|
|
return False
|
|
|
|
|
|
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_required: bool
|
|
approval_token: str
|
|
profiles: frozenset[str]
|
|
backend: str
|
|
bitrate: int
|
|
termination_enabled: bool
|
|
channel: str
|
|
interface: str
|
|
device_type: int
|
|
device_index: int
|
|
zlg_python: Path
|
|
zlg_dll: Path
|
|
bridge_script: Path
|
|
cspybat: Path
|
|
auto_flash_enabled: bool
|
|
auto_home_enabled: bool
|
|
position_sync_enabled: bool
|
|
home_timeout_s: int
|
|
firmware_probe_timeout_ms: int
|
|
firmware_boot_wait_ms: int
|
|
firmware_program_timeout_ms: int
|
|
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":
|
|
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(
|
|
"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=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,
|
|
bitrate=_env_int("COGNITIVE_AGENT_SEAT_CAN_BITRATE", 500_000),
|
|
termination_enabled=_env_bool(
|
|
"COGNITIVE_AGENT_SEAT_CAN_TERMINATION_ENABLED", default=True
|
|
),
|
|
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",
|
|
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=auto_flash_enabled,
|
|
auto_home_enabled=_env_bool("COGNITIVE_AGENT_SEAT_AUTO_HOME_ENABLED"),
|
|
position_sync_enabled=_env_bool(
|
|
"COGNITIVE_AGENT_SEAT_POSITION_SYNC_ENABLED",
|
|
default=hardware_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", 3_500
|
|
),
|
|
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
|
|
),
|
|
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 (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)
|
|
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 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 = [
|
|
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 != SUPPORTED_PHYSICAL_TARGET:
|
|
raise HardwareSafetyError(
|
|
"the T2G bench accepts only passenger/front_right seat operations"
|
|
)
|
|
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", {}))
|
|
physical_requests[capability] = parameters
|
|
|
|
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,
|
|
termination_enabled=self.termination_enabled,
|
|
)
|
|
|
|
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,
|
|
bus_factory: Callable[[], CanBus] | None = None,
|
|
*,
|
|
allow_programming: bool = True,
|
|
) -> T2GFirmwareGuard:
|
|
return T2GFirmwareGuard(
|
|
bus_factory or self.bus_factory(),
|
|
(
|
|
self._programmer()
|
|
if self.auto_flash_enabled and allow_programming
|
|
else None
|
|
),
|
|
auto_flash=self.auto_flash_enabled and allow_programming,
|
|
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": self.approval_required,
|
|
"approval_configured": bool(self.approval_token),
|
|
"platform": "t2g_seat_bench",
|
|
"backend": self.backend,
|
|
"adapter_configured": self.adapter_configured,
|
|
"channel": self.channel,
|
|
"bitrate": self.bitrate,
|
|
"termination_enabled": self.termination_enabled,
|
|
"frame_format": "classic_can_11_bit",
|
|
"firmware_profiles": sorted(self.profiles),
|
|
"firmware_images": images,
|
|
"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_right"],
|
|
"supported_seat_instances": ["passenger"],
|
|
"physical_target": "t2g_primary_seat",
|
|
"target_aliasing": "disabled_front_passenger_only",
|
|
"auto_home_enabled": self.auto_home_enabled,
|
|
"position_sync_enabled": self.position_sync_enabled,
|
|
"position_sync_policy": (
|
|
"four_axis_known_range_home_before_first_process_motion_and_after_firmware_change"
|
|
if self.position_sync_enabled
|
|
else "disabled"
|
|
),
|
|
"home_timeout_s": self.home_timeout_s,
|
|
"limitations": [
|
|
"hardware execution requires the unified Seat_Controller_demo image on one development board",
|
|
"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",
|
|
"on-demand recovery of an unexpectedly invalid target axis remains separately disabled by default",
|
|
"when position synchronization is enabled, the first process motion and every repository firmware replacement rebuild all four physical Hall references",
|
|
"current diagnostics use conservative bench thresholds and do not replace external power limiting",
|
|
],
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class VehicleCanSettings:
|
|
"""Fail-closed channel/timing contract imported from the verified CAN_DEV branch."""
|
|
|
|
repository_root: Path
|
|
enabled: bool
|
|
approval_required: bool
|
|
approval_token: str
|
|
backend: str
|
|
channel: int
|
|
arbitration_bitrate: int
|
|
data_bitrate: int
|
|
termination_enabled: bool
|
|
clock_hz: int
|
|
arbitration_tseg1: int
|
|
arbitration_tseg2: int
|
|
arbitration_sjw: int
|
|
arbitration_brp: int
|
|
data_tseg1: int
|
|
data_tseg2: int
|
|
data_sjw: int
|
|
data_brp: int
|
|
feedback_timeout_ms: int
|
|
device_type: int
|
|
device_index: int
|
|
zlg_python: Path
|
|
zlg_dll: Path
|
|
bridge_script: Path
|
|
|
|
@classmethod
|
|
def from_env(cls, repository_root: Path) -> "VehicleCanSettings":
|
|
backend = os.getenv(
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_BACKEND", "zlgcan"
|
|
).strip().lower()
|
|
linux_vci = backend == "zlgcan-linux"
|
|
default_linux_library = (
|
|
repository_root.parent / "project-code" / "canfd" / "libusbcanfd.so"
|
|
)
|
|
if linux_vci:
|
|
zlg_python = Path(
|
|
os.getenv("COGNITIVE_AGENT_ZLGCAN_PYTHON", sys.executable)
|
|
)
|
|
zlg_library = Path(
|
|
os.getenv(
|
|
"COGNITIVE_AGENT_ZLGCAN_LINUX_LIBRARY",
|
|
str(default_linux_library),
|
|
)
|
|
)
|
|
bridge_script = (
|
|
repository_root
|
|
/ "src"
|
|
/ "cognitive_agent"
|
|
/ "infrastructure"
|
|
/ "can"
|
|
/ "zlgcan_linux_bridge.py"
|
|
)
|
|
else:
|
|
zlg_python = Path(
|
|
os.getenv(
|
|
"COGNITIVE_AGENT_ZLGCAN_PYTHON",
|
|
r"C:\Program Files (x86)\ZCANPRO\python38\python.exe",
|
|
)
|
|
)
|
|
zlg_library = 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"
|
|
)
|
|
return cls(
|
|
repository_root=repository_root,
|
|
enabled=_env_bool(
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED",
|
|
# The seat bench and the vehicle CAN harness are independent
|
|
# pieces of hardware. Enabling physical seat execution must
|
|
# not silently open/transmit on the unconnected vehicle
|
|
# channel; dual-channel execution is an explicit opt-in.
|
|
default=False,
|
|
),
|
|
approval_required=_env_bool(
|
|
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED", default=True
|
|
),
|
|
approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN", ""),
|
|
backend=backend,
|
|
channel=_env_int("COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL", 1),
|
|
arbitration_bitrate=_env_int(
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_ARBITRATION_BITRATE", 500_000
|
|
),
|
|
data_bitrate=_env_int(
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_DATA_BITRATE", 2_000_000
|
|
),
|
|
termination_enabled=_env_bool(
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_TERMINATION_ENABLED", default=False
|
|
),
|
|
clock_hz=60_000_000,
|
|
arbitration_tseg1=14,
|
|
arbitration_tseg2=3,
|
|
arbitration_sjw=2,
|
|
arbitration_brp=5,
|
|
data_tseg1=10,
|
|
data_tseg2=2,
|
|
data_sjw=2,
|
|
data_brp=1,
|
|
feedback_timeout_ms=_env_int(
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_FEEDBACK_TIMEOUT_MS", 1_500
|
|
),
|
|
device_type=_env_int(
|
|
"COGNITIVE_AGENT_ZLGCAN_DEVICE_TYPE", 33 if linux_vci else 41
|
|
),
|
|
device_index=_env_int("COGNITIVE_AGENT_ZLGCAN_DEVICE_INDEX", 0),
|
|
zlg_python=zlg_python,
|
|
zlg_dll=zlg_library,
|
|
bridge_script=bridge_script,
|
|
)
|
|
|
|
@property
|
|
def adapter_configured(self) -> bool:
|
|
return bool(
|
|
self.backend in {"zlgcan", "zlgcan-linux"}
|
|
and self.zlg_python.is_file()
|
|
and self.zlg_dll.is_file()
|
|
and self.bridge_script.is_file()
|
|
)
|
|
|
|
@property
|
|
def state_configured(self) -> bool:
|
|
"""Whether authoritative read-only CH1 synchronization can run."""
|
|
|
|
return bool(
|
|
self.enabled
|
|
and self.adapter_configured
|
|
and self.timing_is_verified
|
|
and self.channel_is_allowed
|
|
)
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return bool(
|
|
self.state_configured
|
|
and (not self.approval_required or self.approval_token)
|
|
)
|
|
|
|
@property
|
|
def required_channel(self) -> int:
|
|
"""Physical body-CAN channel selected by the fail-closed bench policy."""
|
|
|
|
return _env_int("COGNITIVE_AGENT_VEHICLE_CAN_REQUIRED_CHANNEL", 1)
|
|
|
|
@property
|
|
def channel_is_allowed(self) -> bool:
|
|
return self.channel == self.required_channel == 1
|
|
|
|
@property
|
|
def timing_is_verified(self) -> bool:
|
|
return (
|
|
self.arbitration_bitrate == 500_000
|
|
and self.data_bitrate == 2_000_000
|
|
and 60_000_000
|
|
// ((self.arbitration_brp + 1) * (self.arbitration_tseg1 + self.arbitration_tseg2 + 3))
|
|
== 500_000
|
|
and 60_000_000
|
|
// ((self.data_brp + 1) * (self.data_tseg1 + self.data_tseg2 + 3))
|
|
== 2_000_000
|
|
)
|
|
|
|
def authorize(self, supplied_token: str | None) -> None:
|
|
if not self.enabled:
|
|
raise HardwareSafetyError(
|
|
"vehicle CAN execution is disabled; set COGNITIVE_AGENT_VEHICLE_CAN_ENABLED=true"
|
|
)
|
|
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 self.backend not in {"zlgcan", "zlgcan-linux"}:
|
|
raise HardwareSafetyError(
|
|
"vehicle CAN FD requires the zlgcan or zlgcan-linux backend"
|
|
)
|
|
if not self.channel_is_allowed:
|
|
raise HardwareSafetyError(
|
|
"CH1 driver/body-CAN policy requires "
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL=1 and "
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_REQUIRED_CHANNEL=1; CH0 is forbidden"
|
|
)
|
|
if not self.timing_is_verified:
|
|
raise HardwareSafetyError(
|
|
"vehicle CAN timing must remain ISO CAN FD 500 kbit/s / 2 Mbit/s at 80% sample point"
|
|
)
|
|
if not 100 <= self.feedback_timeout_ms <= 10_000:
|
|
raise HardwareSafetyError(
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_FEEDBACK_TIMEOUT_MS must be between 100 and 10000"
|
|
)
|
|
if not self.adapter_configured:
|
|
raise HardwareSafetyError("the configured ZLG CAN FD runtime is not ready")
|
|
|
|
def channel_config(self) -> ZlgChannelConfig:
|
|
return ZlgChannelConfig(
|
|
channel=self.channel,
|
|
frame_format="can_fd",
|
|
arbitration_bitrate=self.arbitration_bitrate,
|
|
data_bitrate=self.data_bitrate,
|
|
can_fd_iso=True,
|
|
termination_enabled=self.termination_enabled,
|
|
)
|
|
|
|
def bridge(
|
|
self,
|
|
channels: list[ZlgChannelConfig],
|
|
*,
|
|
monitor_only: bool = False,
|
|
trace_all: bool = False,
|
|
) -> ZlgCanBridgeProcess:
|
|
return ZlgCanBridgeProcess(
|
|
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,
|
|
channels=channels,
|
|
monitor_only=monitor_only,
|
|
trace_all=trace_all,
|
|
)
|
|
|
|
def public_info(self) -> dict[str, Any]:
|
|
return {
|
|
"enabled": self.enabled,
|
|
"configured": self.configured,
|
|
"state_configured": self.state_configured,
|
|
"backend": self.backend,
|
|
"platform": "t2g_cabin_bench",
|
|
"channel": self.channel,
|
|
"required_channel": self.required_channel,
|
|
"channel_is_allowed": self.channel_is_allowed,
|
|
"target_policy": "ch1_body_can_driver_only",
|
|
"channel_role": "driver_front_left",
|
|
"coexists_with_seat_channel": True,
|
|
"forbidden_paths": ["ch0", "t2g_seat_bench", "passenger", "front_right"],
|
|
"mode": 0,
|
|
"work_mode": "normal",
|
|
"can_fd_standard": "ISO",
|
|
"termination_enabled": self.termination_enabled,
|
|
"clock_hz": self.clock_hz,
|
|
"arbitration": {
|
|
"bitrate": self.arbitration_bitrate,
|
|
"tseg1": self.arbitration_tseg1,
|
|
"tseg2": self.arbitration_tseg2,
|
|
"sjw": self.arbitration_sjw,
|
|
"smp": 0,
|
|
"brp": self.arbitration_brp,
|
|
"total_tq": 20,
|
|
"sample_point_percent": 80,
|
|
},
|
|
"data": {
|
|
"bitrate": self.data_bitrate,
|
|
"tseg1": self.data_tseg1,
|
|
"tseg2": self.data_tseg2,
|
|
"sjw": self.data_sjw,
|
|
"smp": 0,
|
|
"brp": self.data_brp,
|
|
"total_tq": 15,
|
|
"sample_point_percent": 80,
|
|
},
|
|
"source_branch": "project-code/CAN_DEV",
|
|
"source_commit": "6e7ed04a468dfcde7efda9e30bc15144bf02faf9",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class VehicleMqttSettings:
|
|
"""Wireless CAN0/CAN1 relay settings for the local head-unit broker."""
|
|
|
|
enabled: bool
|
|
approval_required: bool
|
|
approval_token: str
|
|
host: str
|
|
port: int
|
|
condition_topic: str
|
|
control_topic: str
|
|
qos: int
|
|
keepalive_s: int
|
|
username: str | None
|
|
password: str | None
|
|
tls_enabled: bool
|
|
feedback_timeout_ms: int
|
|
|
|
@classmethod
|
|
def from_env(cls) -> "VehicleMqttSettings":
|
|
username = os.getenv("COGNITIVE_AGENT_VEHICLE_MQTT_USERNAME")
|
|
return cls(
|
|
enabled=_env_bool("COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED", default=True),
|
|
approval_required=_env_bool(
|
|
"COGNITIVE_AGENT_VEHICLE_MQTT_APPROVAL_TOKEN_REQUIRED",
|
|
default=_env_bool(
|
|
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED",
|
|
default=False,
|
|
),
|
|
),
|
|
approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN", ""),
|
|
host=os.getenv("COGNITIVE_AGENT_VEHICLE_MQTT_HOST", "127.0.0.1").strip(),
|
|
port=_env_int("COGNITIVE_AGENT_VEHICLE_MQTT_PORT", 1883),
|
|
condition_topic=os.getenv(
|
|
"COGNITIVE_AGENT_VEHICLE_MQTT_CONDITION_TOPIC",
|
|
"n7/n7-headunit/can/state",
|
|
).strip(),
|
|
control_topic=os.getenv(
|
|
"COGNITIVE_AGENT_VEHICLE_MQTT_CONTROL_TOPIC",
|
|
"n7/n7-headunit/can/command",
|
|
).strip(),
|
|
qos=_env_int("COGNITIVE_AGENT_VEHICLE_MQTT_QOS", 1),
|
|
keepalive_s=_env_int("COGNITIVE_AGENT_VEHICLE_MQTT_KEEPALIVE_S", 30),
|
|
username=username.strip() if username and username.strip() else None,
|
|
password=os.getenv("COGNITIVE_AGENT_VEHICLE_MQTT_PASSWORD"),
|
|
tls_enabled=_env_bool("COGNITIVE_AGENT_VEHICLE_MQTT_TLS"),
|
|
feedback_timeout_ms=_env_int(
|
|
"COGNITIVE_AGENT_VEHICLE_MQTT_FEEDBACK_TIMEOUT_MS", 1_500
|
|
),
|
|
)
|
|
|
|
@property
|
|
def adapter_configured(self) -> bool:
|
|
return bool(
|
|
self.host
|
|
and 1 <= self.port <= 65_535
|
|
and self.condition_topic
|
|
and self.control_topic
|
|
and self.condition_topic != self.control_topic
|
|
and self.qos in {0, 1, 2}
|
|
and 5 <= self.keepalive_s <= 3_600
|
|
and 100 <= self.feedback_timeout_ms <= 10_000
|
|
and _module_available("paho.mqtt.client")
|
|
)
|
|
|
|
@property
|
|
def state_configured(self) -> bool:
|
|
return self.enabled and self.adapter_configured
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return bool(
|
|
self.state_configured
|
|
and (not self.approval_required or self.approval_token)
|
|
)
|
|
|
|
def authorize(self, supplied_token: str | None) -> None:
|
|
if not self.enabled:
|
|
raise HardwareSafetyError(
|
|
"vehicle MQTT execution is disabled; set "
|
|
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED=true"
|
|
)
|
|
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.adapter_configured:
|
|
raise HardwareSafetyError("the configured MQTT CAN relay is not ready")
|
|
|
|
def public_info(self) -> dict[str, Any]:
|
|
return {
|
|
"enabled": self.enabled,
|
|
"configured": self.configured,
|
|
"state_configured": self.state_configured,
|
|
"backend": "mqtt",
|
|
"host": self.host,
|
|
"port": self.port,
|
|
"condition_topic": self.condition_topic,
|
|
"control_topic": self.control_topic,
|
|
"qos": self.qos,
|
|
"tls_enabled": self.tls_enabled,
|
|
"authentication_configured": bool(self.username),
|
|
"channels": {"seat": 0, "vehicle": 1},
|
|
"target_policy": "dual_channel_mqtt_can0_passenger_can1_vehicle",
|
|
"frame_contract": "n7.can.frame.v1",
|
|
"state_contract": "n7-can-signals/v1",
|
|
"control_contract": "n7-headunit-can-command/v1",
|
|
"can0_seat_protocol": "seat-ota-runtime",
|
|
"can0_state_contract": "0x6A0-SEAT_INFO-plus-0x6A2-to-0x302",
|
|
"can0_control_id": "0x6A0",
|
|
"can0_control_opcodes": ["0x63", "0x64", "0x65", "0x66"],
|
|
"can0_forbidden_workflows": [
|
|
"flash",
|
|
"bootloader_entry",
|
|
"test",
|
|
"calibration",
|
|
"homing",
|
|
],
|
|
"approval_required": self.approval_required,
|
|
"approval_configured": bool(self.approval_token),
|
|
}
|