This commit is contained in:
orin
2026-07-30 08:45:38 +08:00
commit 25b24ac4cd
1333 changed files with 170092 additions and 0 deletions
+5
View File
@@ -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
View File
@@ -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",
]
+209
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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())