init
This commit is contained in:
+5
@@ -0,0 +1,5 @@
|
||||
# CAN Infrastructure
|
||||
|
||||
公共 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)。
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
"""CAN matrix infrastructure shared by executable domain adapters."""
|
||||
|
||||
from .matrix import CanMatrix, MatrixError
|
||||
from .gateway import CanBus, CanFrame, CanGatewayError, PythonCanBus, ZlgCanBridgeBus
|
||||
from .hardware import HardwareSafetyError, SeatHardwareSettings
|
||||
|
||||
__all__ = [
|
||||
"CanBus",
|
||||
"CanFrame",
|
||||
"CanGatewayError",
|
||||
"CanMatrix",
|
||||
"HardwareSafetyError",
|
||||
"MatrixError",
|
||||
"PythonCanBus",
|
||||
"SeatHardwareSettings",
|
||||
"ZlgCanBridgeBus",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+209
@@ -0,0 +1,209 @@
|
||||
"""Small CAN transport abstraction used by approved hardware executors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class CanGatewayError(RuntimeError):
|
||||
"""Raised when a CAN adapter cannot be opened or used safely."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanFrame:
|
||||
arbitration_id: int
|
||||
data: bytes
|
||||
timestamp: float | None = None
|
||||
|
||||
def public(self) -> dict[str, Any]:
|
||||
return {
|
||||
"frame_id": f"0x{self.arbitration_id:03X}",
|
||||
"payload_size": len(self.data),
|
||||
"data_hex": " ".join(f"{byte:02X}" for byte in self.data),
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
class CanBus(Protocol):
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame: ...
|
||||
|
||||
def receive(self, timeout_ms: int) -> CanFrame | None: ...
|
||||
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
class PythonCanBus:
|
||||
"""Adapter for SocketCAN, PCAN and Vector through the optional python-can package."""
|
||||
|
||||
def __init__(self, interface: str, channel: str, bitrate: int):
|
||||
try:
|
||||
import can # type: ignore[import-not-found]
|
||||
except ImportError as exc:
|
||||
raise CanGatewayError(
|
||||
"python-can is not installed; install the project 'can' extra"
|
||||
) from exc
|
||||
self._can = can
|
||||
try:
|
||||
self._bus = can.Bus(
|
||||
interface=interface,
|
||||
channel=channel,
|
||||
bitrate=bitrate,
|
||||
fd=False,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - normalize vendor exceptions
|
||||
raise CanGatewayError(f"cannot open {interface} CAN channel {channel}: {exc}") from exc
|
||||
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
|
||||
if len(data) > 8:
|
||||
raise CanGatewayError("the T2G bench accepts classic CAN frames of at most 8 bytes")
|
||||
message = self._can.Message(
|
||||
arbitration_id=arbitration_id,
|
||||
data=data,
|
||||
is_extended_id=False,
|
||||
is_fd=False,
|
||||
)
|
||||
try:
|
||||
self._bus.send(message, timeout=0.5)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise CanGatewayError(f"CAN transmit failed: {exc}") from exc
|
||||
return CanFrame(arbitration_id, bytes(data))
|
||||
|
||||
def receive(self, timeout_ms: int) -> CanFrame | None:
|
||||
try:
|
||||
message = self._bus.recv(max(0, timeout_ms) / 1000)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise CanGatewayError(f"CAN receive failed: {exc}") from exc
|
||||
if message is None:
|
||||
return None
|
||||
return CanFrame(
|
||||
int(message.arbitration_id),
|
||||
bytes(message.data),
|
||||
float(message.timestamp) if message.timestamp is not None else None,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self._bus.shutdown()
|
||||
|
||||
|
||||
class ZlgCanBridgeBus:
|
||||
"""Use the installed 32-bit ZLG driver from the 64-bit application process."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
python_executable: Path,
|
||||
bridge_script: Path,
|
||||
dll_path: Path,
|
||||
device_type: int,
|
||||
device_index: int,
|
||||
channel: int,
|
||||
bitrate: int,
|
||||
):
|
||||
command = [
|
||||
str(python_executable),
|
||||
str(bridge_script),
|
||||
"--dll",
|
||||
str(dll_path),
|
||||
"--device-type",
|
||||
str(device_type),
|
||||
"--device-index",
|
||||
str(device_index),
|
||||
"--channel",
|
||||
str(channel),
|
||||
"--bitrate",
|
||||
str(bitrate),
|
||||
]
|
||||
try:
|
||||
self._process = subprocess.Popen( # noqa: S603 - paths are trusted server config
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
bufsize=1,
|
||||
)
|
||||
except OSError as exc:
|
||||
raise CanGatewayError(f"cannot start ZLG CAN bridge: {exc}") from exc
|
||||
self._lock = threading.Lock()
|
||||
ready = self._read_response()
|
||||
if ready.get("event") != "ready":
|
||||
self.close()
|
||||
raise CanGatewayError(str(ready.get("message", "ZLG CAN bridge did not start")))
|
||||
|
||||
def _read_response(self) -> dict[str, Any]:
|
||||
if self._process.stdout is None:
|
||||
raise CanGatewayError("ZLG CAN bridge stdout is unavailable")
|
||||
line = self._process.stdout.readline()
|
||||
if not line:
|
||||
error = ""
|
||||
if self._process.stderr is not None:
|
||||
error = self._process.stderr.read().strip()
|
||||
raise CanGatewayError(error or "ZLG CAN bridge exited unexpectedly")
|
||||
try:
|
||||
response = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise CanGatewayError(f"invalid ZLG CAN bridge response: {line!r}") from exc
|
||||
if response.get("ok") is False:
|
||||
raise CanGatewayError(str(response.get("message", "ZLG CAN operation failed")))
|
||||
return response
|
||||
|
||||
def _request(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
if self._process.poll() is not None or self._process.stdin is None:
|
||||
raise CanGatewayError("ZLG CAN bridge is not running")
|
||||
self._process.stdin.write(json.dumps(payload, separators=(",", ":")) + "\n")
|
||||
self._process.stdin.flush()
|
||||
return self._read_response()
|
||||
|
||||
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
|
||||
response = self._request(
|
||||
{
|
||||
"operation": "send",
|
||||
"arbitration_id": arbitration_id,
|
||||
"data_hex": data.hex(),
|
||||
}
|
||||
)
|
||||
return CanFrame(
|
||||
int(response["arbitration_id"]),
|
||||
bytes.fromhex(response["data_hex"]),
|
||||
)
|
||||
|
||||
def receive(self, timeout_ms: int) -> CanFrame | None:
|
||||
response = self._request(
|
||||
{"operation": "receive", "timeout_ms": max(0, int(timeout_ms))}
|
||||
)
|
||||
frame = response.get("frame")
|
||||
if frame is None:
|
||||
return None
|
||||
return CanFrame(
|
||||
int(frame["arbitration_id"]),
|
||||
bytes.fromhex(frame["data_hex"]),
|
||||
float(frame["timestamp_us"]) / 1_000_000,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
process = getattr(self, "_process", None)
|
||||
if process is None or process.poll() is not None:
|
||||
return
|
||||
try:
|
||||
if process.stdin is not None:
|
||||
process.stdin.write('{"operation":"close"}\n')
|
||||
process.stdin.flush()
|
||||
process.wait(timeout=2)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
|
||||
def __enter__(self) -> "ZlgCanBridgeBus":
|
||||
return self
|
||||
|
||||
def __exit__(self, *_: object) -> None:
|
||||
self.close()
|
||||
+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",
|
||||
],
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
"""Load and encode a normalized virtual CAN matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class MatrixError(ValueError):
|
||||
"""Raised when a signal cannot be represented by a normalized matrix."""
|
||||
|
||||
|
||||
class CanMatrix:
|
||||
"""Small LSB0/Intel encoder used only by the dry-run execution path."""
|
||||
|
||||
def __init__(self, matrix_path: Path):
|
||||
self.path = matrix_path
|
||||
self.data = yaml.safe_load(matrix_path.read_text(encoding="utf-8"))
|
||||
self.messages = {item["name"]: item for item in self.data["messages"]}
|
||||
|
||||
@property
|
||||
def hardware_execution_allowed(self) -> bool:
|
||||
return self.data.get("hardware_execution_allowed") is True
|
||||
|
||||
def message(self, name: str) -> dict[str, Any]:
|
||||
try:
|
||||
return self.messages[name]
|
||||
except KeyError as exc:
|
||||
raise MatrixError(f"Unknown matrix message: {name}") from exc
|
||||
|
||||
def signal(self, message_name: str, signal_name: str) -> dict[str, Any]:
|
||||
for signal in self.message(message_name)["signals"]:
|
||||
if signal["name"] == signal_name:
|
||||
return signal
|
||||
raise MatrixError(f"Unknown signal {signal_name} in {message_name}")
|
||||
|
||||
def encode(self, message_name: str, values: dict[str, int]) -> dict[str, Any]:
|
||||
message = self.message(message_name)
|
||||
encoded_values = dict(message.get("fixed_signals", {}))
|
||||
encoded_values.update(values)
|
||||
raw_value = 0
|
||||
for signal_name, value in encoded_values.items():
|
||||
signal = self.signal(message_name, signal_name)
|
||||
width = int(signal["bit_length"])
|
||||
if (
|
||||
not isinstance(value, int)
|
||||
or isinstance(value, bool)
|
||||
or value < 0
|
||||
or value >= 1 << width
|
||||
):
|
||||
raise MatrixError(
|
||||
f"Value {value!r} does not fit {signal_name}[{width}]"
|
||||
)
|
||||
start_bit = int(signal["start_bit"])
|
||||
mask = ((1 << width) - 1) << start_bit
|
||||
raw_value = (raw_value & ~mask) | (value << start_bit)
|
||||
|
||||
payload_size = int(message["minimum_payload_bytes"])
|
||||
payload = raw_value.to_bytes(payload_size, byteorder="little", signed=False)
|
||||
return {
|
||||
"message": message_name,
|
||||
"frame_id": f"0x{int(message['frame_id']):03X}",
|
||||
"payload_size": payload_size,
|
||||
"data_hex": " ".join(f"{byte:02X}" for byte in payload),
|
||||
"signals": encoded_values,
|
||||
"encoding": "lsb0/intel inferred for virtual execution",
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
"""32-bit JSON-line bridge for the ZLG zlgcan.dll API.
|
||||
|
||||
This module deliberately has no project or third-party imports so it can run under
|
||||
the 32-bit Python bundled with ZCANPRO while the web service uses 64-bit Python.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
ZCAN_TYPE_CANFD = 1
|
||||
ZCAN_STATUS_OK = 1
|
||||
|
||||
|
||||
class ZcanClassicInit(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("acc_code", ctypes.c_uint32),
|
||||
("acc_mask", ctypes.c_uint32),
|
||||
("reserved", ctypes.c_uint32),
|
||||
("filter", ctypes.c_uint8),
|
||||
("timing0", ctypes.c_uint8),
|
||||
("timing1", ctypes.c_uint8),
|
||||
("mode", ctypes.c_uint8),
|
||||
]
|
||||
|
||||
|
||||
class ZcanFdInit(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("acc_code", ctypes.c_uint32),
|
||||
("acc_mask", ctypes.c_uint32),
|
||||
("abit_timing", ctypes.c_uint32),
|
||||
("dbit_timing", ctypes.c_uint32),
|
||||
("brp", ctypes.c_uint32),
|
||||
("filter", ctypes.c_uint8),
|
||||
("mode", ctypes.c_uint8),
|
||||
("pad", ctypes.c_uint16),
|
||||
("reserved", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
class ZcanInitUnion(ctypes.Union):
|
||||
_fields_ = [("can", ZcanClassicInit), ("canfd", ZcanFdInit)]
|
||||
|
||||
|
||||
class ZcanChannelInit(ctypes.Structure):
|
||||
_anonymous_ = ("config",)
|
||||
_fields_ = [("can_type", ctypes.c_uint32), ("config", ZcanInitUnion)]
|
||||
|
||||
|
||||
class ZcanFrame(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("can_id", ctypes.c_uint32),
|
||||
("can_dlc", ctypes.c_uint8),
|
||||
("pad", ctypes.c_uint8),
|
||||
("reserved0", ctypes.c_uint8),
|
||||
("reserved1", ctypes.c_uint8),
|
||||
("data", ctypes.c_uint8 * 8),
|
||||
]
|
||||
|
||||
|
||||
class ZcanTransmitData(ctypes.Structure):
|
||||
_fields_ = [("frame", ZcanFrame), ("transmit_type", ctypes.c_uint32)]
|
||||
|
||||
|
||||
class ZcanReceiveData(ctypes.Structure):
|
||||
_fields_ = [("frame", ZcanFrame), ("timestamp", ctypes.c_uint64)]
|
||||
|
||||
|
||||
class ZcanChannelErrorInfo(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("error_code", ctypes.c_uint32),
|
||||
("passive_error_data", ctypes.c_uint8 * 3),
|
||||
("arbitration_lost_error_data", ctypes.c_uint8),
|
||||
]
|
||||
|
||||
|
||||
def emit(value):
|
||||
print(json.dumps(value, ensure_ascii=False, separators=(",", ":")), flush=True)
|
||||
|
||||
|
||||
class Driver:
|
||||
def __init__(self, args):
|
||||
self.transmit_type = args.transmit_type
|
||||
self.dll = ctypes.WinDLL(args.dll)
|
||||
self._declare_functions()
|
||||
self.device = self.dll.ZCAN_OpenDevice(
|
||||
args.device_type, args.device_index, 0
|
||||
)
|
||||
if not self.device:
|
||||
raise RuntimeError(
|
||||
"ZCAN_OpenDevice failed; connect the adapter and close ZCANPRO first"
|
||||
)
|
||||
self.channel = None
|
||||
try:
|
||||
bitrate = str(args.bitrate).encode("ascii")
|
||||
channel_prefix = str(args.channel)
|
||||
path = (channel_prefix + "/canfd_abit_baud_rate").encode("ascii")
|
||||
if self.dll.ZCAN_SetValue(self.device, path, bitrate) != ZCAN_STATUS_OK:
|
||||
raise RuntimeError("ZCAN_SetValue(canfd_abit_baud_rate) failed")
|
||||
data_path = (channel_prefix + "/canfd_dbit_baud_rate").encode("ascii")
|
||||
if self.dll.ZCAN_SetValue(self.device, data_path, b"2000000") != ZCAN_STATUS_OK:
|
||||
raise RuntimeError("ZCAN_SetValue(canfd_dbit_baud_rate) failed")
|
||||
init = ZcanChannelInit()
|
||||
# USBCANFD devices require a CANFD channel handle even when the
|
||||
# application only transmits classic CAN frames through ZCAN_Transmit.
|
||||
init.can_type = ZCAN_TYPE_CANFD
|
||||
init.canfd.acc_code = 0
|
||||
init.canfd.acc_mask = 0xFFFFFFFF
|
||||
init.canfd.filter = 0
|
||||
init.canfd.mode = 0
|
||||
self.channel = self.dll.ZCAN_InitCAN(
|
||||
self.device, args.channel, ctypes.byref(init)
|
||||
)
|
||||
if not self.channel:
|
||||
raise RuntimeError("ZCAN_InitCAN failed")
|
||||
if self.dll.ZCAN_StartCAN(self.channel) != ZCAN_STATUS_OK:
|
||||
raise RuntimeError("ZCAN_StartCAN failed")
|
||||
self.dll.ZCAN_ClearBuffer(self.channel)
|
||||
except Exception:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def _declare_functions(self):
|
||||
handle = ctypes.c_void_p
|
||||
self.dll.ZCAN_OpenDevice.argtypes = [
|
||||
ctypes.c_uint32,
|
||||
ctypes.c_uint32,
|
||||
ctypes.c_uint32,
|
||||
]
|
||||
self.dll.ZCAN_OpenDevice.restype = handle
|
||||
self.dll.ZCAN_CloseDevice.argtypes = [handle]
|
||||
self.dll.ZCAN_CloseDevice.restype = ctypes.c_uint32
|
||||
self.dll.ZCAN_SetValue.argtypes = [handle, ctypes.c_char_p, ctypes.c_char_p]
|
||||
self.dll.ZCAN_SetValue.restype = ctypes.c_uint32
|
||||
self.dll.ZCAN_InitCAN.argtypes = [
|
||||
handle,
|
||||
ctypes.c_uint32,
|
||||
ctypes.POINTER(ZcanChannelInit),
|
||||
]
|
||||
self.dll.ZCAN_InitCAN.restype = handle
|
||||
self.dll.ZCAN_StartCAN.argtypes = [handle]
|
||||
self.dll.ZCAN_StartCAN.restype = ctypes.c_uint32
|
||||
self.dll.ZCAN_ResetCAN.argtypes = [handle]
|
||||
self.dll.ZCAN_ResetCAN.restype = ctypes.c_uint32
|
||||
self.dll.ZCAN_ClearBuffer.argtypes = [handle]
|
||||
self.dll.ZCAN_ClearBuffer.restype = ctypes.c_uint32
|
||||
self.dll.ZCAN_ReadChannelErrInfo.argtypes = [
|
||||
handle,
|
||||
ctypes.POINTER(ZcanChannelErrorInfo),
|
||||
]
|
||||
self.dll.ZCAN_ReadChannelErrInfo.restype = ctypes.c_uint32
|
||||
self.dll.ZCAN_Transmit.argtypes = [
|
||||
handle,
|
||||
ctypes.POINTER(ZcanTransmitData),
|
||||
ctypes.c_uint32,
|
||||
]
|
||||
self.dll.ZCAN_Transmit.restype = ctypes.c_uint32
|
||||
self.dll.ZCAN_Receive.argtypes = [
|
||||
handle,
|
||||
ctypes.POINTER(ZcanReceiveData),
|
||||
ctypes.c_uint32,
|
||||
ctypes.c_int32,
|
||||
]
|
||||
self.dll.ZCAN_Receive.restype = ctypes.c_uint32
|
||||
|
||||
def send(self, arbitration_id, data):
|
||||
if not 0 <= arbitration_id <= 0x7FF:
|
||||
raise ValueError("only 11-bit standard CAN identifiers are allowed")
|
||||
if len(data) > 8:
|
||||
raise ValueError("classic CAN payload exceeds 8 bytes")
|
||||
transmit = ZcanTransmitData()
|
||||
transmit.frame.can_id = arbitration_id
|
||||
transmit.frame.can_dlc = len(data)
|
||||
for index, byte in enumerate(data):
|
||||
transmit.frame.data[index] = byte
|
||||
transmit.transmit_type = self.transmit_type
|
||||
if self.dll.ZCAN_Transmit(self.channel, ctypes.byref(transmit), 1) != 1:
|
||||
error = ZcanChannelErrorInfo()
|
||||
read_ok = self.dll.ZCAN_ReadChannelErrInfo(
|
||||
self.channel, ctypes.byref(error)
|
||||
)
|
||||
detail = (
|
||||
f"; channel_error=0x{error.error_code:08X}"
|
||||
if read_ok == ZCAN_STATUS_OK
|
||||
else "; channel error info unavailable"
|
||||
)
|
||||
raise RuntimeError("ZCAN_Transmit did not accept the frame" + detail)
|
||||
|
||||
def receive(self, timeout_ms):
|
||||
receive = ZcanReceiveData()
|
||||
count = self.dll.ZCAN_Receive(
|
||||
self.channel, ctypes.byref(receive), 1, int(timeout_ms)
|
||||
)
|
||||
if count == 0:
|
||||
return None
|
||||
if count != 1:
|
||||
raise RuntimeError("ZCAN_Receive returned an unexpected frame count")
|
||||
frame = receive.frame
|
||||
return {
|
||||
"arbitration_id": int(frame.can_id & 0x1FFFFFFF),
|
||||
"data_hex": bytes(frame.data[: frame.can_dlc]).hex(),
|
||||
"timestamp_us": int(receive.timestamp),
|
||||
}
|
||||
|
||||
def close(self):
|
||||
if self.channel:
|
||||
self.dll.ZCAN_ResetCAN(self.channel)
|
||||
self.channel = None
|
||||
if self.device:
|
||||
self.dll.ZCAN_CloseDevice(self.device)
|
||||
self.device = None
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dll", required=True)
|
||||
parser.add_argument("--device-type", type=int, required=True)
|
||||
parser.add_argument("--device-index", type=int, default=0)
|
||||
parser.add_argument("--channel", type=int, default=0)
|
||||
parser.add_argument("--bitrate", type=int, default=500000)
|
||||
parser.add_argument(
|
||||
"--transmit-type",
|
||||
type=int,
|
||||
choices=(0, 2),
|
||||
default=0,
|
||||
help="0 for normal transmission; 2 is diagnostic self-reception",
|
||||
)
|
||||
parser.add_argument("--self-test", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if args.self_test:
|
||||
dll = ctypes.WinDLL(args.dll)
|
||||
required = (
|
||||
"ZCAN_OpenDevice",
|
||||
"ZCAN_CloseDevice",
|
||||
"ZCAN_SetValue",
|
||||
"ZCAN_InitCAN",
|
||||
"ZCAN_StartCAN",
|
||||
"ZCAN_Transmit",
|
||||
"ZCAN_Receive",
|
||||
)
|
||||
missing = [name for name in required if not hasattr(dll, name)]
|
||||
emit(
|
||||
{
|
||||
"ok": not missing,
|
||||
"pointer_bits": ctypes.sizeof(ctypes.c_void_p) * 8,
|
||||
"missing_exports": missing,
|
||||
"structure_sizes": {
|
||||
"init": ctypes.sizeof(ZcanChannelInit),
|
||||
"frame": ctypes.sizeof(ZcanFrame),
|
||||
"tx": ctypes.sizeof(ZcanTransmitData),
|
||||
"rx": ctypes.sizeof(ZcanReceiveData),
|
||||
"error": ctypes.sizeof(ZcanChannelErrorInfo),
|
||||
},
|
||||
}
|
||||
)
|
||||
return 0 if not missing else 1
|
||||
|
||||
driver = None
|
||||
try:
|
||||
driver = Driver(args)
|
||||
emit({"ok": True, "event": "ready"})
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
request = json.loads(line)
|
||||
operation = request.get("operation")
|
||||
if operation == "send":
|
||||
arbitration_id = int(request["arbitration_id"])
|
||||
data = bytes.fromhex(request["data_hex"])
|
||||
driver.send(arbitration_id, data)
|
||||
emit(
|
||||
{
|
||||
"ok": True,
|
||||
"arbitration_id": arbitration_id,
|
||||
"data_hex": data.hex(),
|
||||
}
|
||||
)
|
||||
elif operation == "receive":
|
||||
emit(
|
||||
{
|
||||
"ok": True,
|
||||
"frame": driver.receive(int(request.get("timeout_ms", 0))),
|
||||
}
|
||||
)
|
||||
elif operation == "close":
|
||||
emit({"ok": True})
|
||||
break
|
||||
else:
|
||||
raise ValueError("unknown bridge operation")
|
||||
except Exception as exc:
|
||||
emit({"ok": False, "message": str(exc)})
|
||||
except Exception as exc:
|
||||
emit({"ok": False, "event": "error", "message": str(exc)})
|
||||
return 1
|
||||
finally:
|
||||
if driver is not None:
|
||||
driver.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user