Files
Cognitive_Agent/src/cognitive_agent/infrastructure/can/hardware.py
T

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),
}