feat: integrate cabin CAN and verified dynamic seat modes

This commit is contained in:
vee
2026-08-04 17:51:38 +08:00
parent 155fbb59ba
commit dc7c91800e
99 changed files with 51897 additions and 3618 deletions
+181 -39
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import json
import subprocess
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Protocol
@@ -19,6 +20,9 @@ class CanFrame:
arbitration_id: int
data: bytes
timestamp: float | None = None
channel: int | None = None
is_fd: bool = False
bitrate_switch: bool = False
def public(self) -> dict[str, Any]:
return {
@@ -26,13 +30,25 @@ class CanFrame:
"payload_size": len(self.data),
"data_hex": " ".join(f"{byte:02X}" for byte in self.data),
"timestamp": self.timestamp,
"channel": self.channel,
"is_fd": self.is_fd,
"bitrate_switch": self.bitrate_switch,
}
class CanBus(Protocol):
def send(self, arbitration_id: int, data: bytes) -> CanFrame: ...
def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame: ...
def receive(self, timeout_ms: int) -> CanFrame | None: ...
def receive(
self, timeout_ms: int, *, include_fd: bool = False
) -> CanFrame | None: ...
def close(self) -> None: ...
@@ -58,7 +74,16 @@ class PythonCanBus:
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:
def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame:
if is_fd:
raise CanGatewayError("PythonCanBus was opened for classic CAN only")
if len(data) > 8:
raise CanGatewayError("the T2G bench accepts classic CAN frames of at most 8 bytes")
message = self._can.Message(
@@ -71,9 +96,11 @@ class PythonCanBus:
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))
return CanFrame(arbitration_id, bytes(data), is_fd=False)
def receive(self, timeout_ms: int) -> CanFrame | None:
def receive(
self, timeout_ms: int, *, include_fd: bool = False
) -> CanFrame | None:
try:
message = self._bus.recv(max(0, timeout_ms) / 1000)
except Exception as exc: # noqa: BLE001
@@ -84,14 +111,36 @@ class PythonCanBus:
int(message.arbitration_id),
bytes(message.data),
float(message.timestamp) if message.timestamp is not None else None,
is_fd=bool(getattr(message, "is_fd", False)),
bitrate_switch=bool(getattr(message, "bitrate_switch", False)),
)
def close(self) -> None:
self._bus.shutdown()
class ZlgCanBridgeBus:
"""Use the installed 32-bit ZLG driver from the 64-bit application process."""
@dataclass(frozen=True)
class ZlgChannelConfig:
"""Configuration for one channel on a shared ZLG CAN FD adapter."""
channel: int
arbitration_bitrate: int = 500_000
data_bitrate: int = 2_000_000
can_fd_iso: bool = True
termination_enabled: bool = True
def public(self) -> dict[str, Any]:
return {
"channel": self.channel,
"arbitration_bitrate": self.arbitration_bitrate,
"data_bitrate": self.data_bitrate,
"can_fd_iso": self.can_fd_iso,
"termination_enabled": self.termination_enabled,
}
class ZlgCanBridgeProcess:
"""Own one device handle and expose several independently addressed channels."""
def __init__(
self,
@@ -100,9 +149,15 @@ class ZlgCanBridgeBus:
dll_path: Path,
device_type: int,
device_index: int,
channel: int,
bitrate: int,
channels: list[ZlgChannelConfig],
transmit_type: int = 0,
):
if not channels:
raise CanGatewayError("at least one ZLG channel must be configured")
channel_ids = [item.channel for item in channels]
if len(channel_ids) != len(set(channel_ids)):
raise CanGatewayError("ZLG channel numbers must be unique")
self._channels = frozenset(channel_ids)
command = [
str(python_executable),
str(bridge_script),
@@ -112,10 +167,10 @@ class ZlgCanBridgeBus:
str(device_type),
"--device-index",
str(device_index),
"--channel",
str(channel),
"--bitrate",
str(bitrate),
"--channels-json",
json.dumps([item.public() for item in channels], separators=(",", ":")),
"--transmit-type",
str(transmit_type),
]
try:
self._process = subprocess.Popen( # noqa: S603 - paths are trusted server config
@@ -135,6 +190,11 @@ class ZlgCanBridgeBus:
self.close()
raise CanGatewayError(str(ready.get("message", "ZLG CAN bridge did not start")))
def channel_bus(self, channel: int) -> "ZlgCanChannelBus":
if channel not in self._channels:
raise CanGatewayError(f"ZLG channel {channel} was not initialized")
return ZlgCanChannelBus(self, channel)
def _read_response(self) -> dict[str, Any]:
if self._process.stdout is None:
raise CanGatewayError("ZLG CAN bridge stdout is unavailable")
@@ -160,32 +220,6 @@ class ZlgCanBridgeBus:
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:
@@ -202,6 +236,114 @@ class ZlgCanBridgeBus:
except subprocess.TimeoutExpired:
process.kill()
def __enter__(self) -> "ZlgCanBridgeProcess":
return self
def __exit__(self, *_: object) -> None:
self.close()
class ZlgCanChannelBus:
"""Channel-scoped view over a shared :class:`ZlgCanBridgeProcess`."""
def __init__(self, bridge: ZlgCanBridgeProcess, channel: int):
self._bridge = bridge
self.channel = channel
def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame:
response = self._bridge._request(
{
"operation": "send",
"channel": self.channel,
"arbitration_id": arbitration_id,
"data_hex": data.hex(),
"is_fd": is_fd,
"bitrate_switch": bitrate_switch,
}
)
return CanFrame(
int(response["arbitration_id"]),
bytes.fromhex(response["data_hex"]),
channel=self.channel,
is_fd=bool(response.get("is_fd", False)),
bitrate_switch=bool(response.get("bitrate_switch", False)),
)
def receive(
self, timeout_ms: int, *, include_fd: bool = False
) -> CanFrame | None:
requested_ms = max(0, int(timeout_ms))
deadline = time.monotonic() + requested_ms / 1000
while True:
remaining_ms = max(0, int((deadline - time.monotonic()) * 1000))
response = self._bridge._request(
{
"operation": "receive",
"channel": self.channel,
# Short slices let another channel transmit or receive while
# this synchronous JSON bridge is waiting for feedback.
"timeout_ms": min(20, remaining_ms),
"include_fd": include_fd,
}
)
frame = response.get("frame")
if frame is not None:
return CanFrame(
int(frame["arbitration_id"]),
bytes.fromhex(frame["data_hex"]),
float(frame["timestamp_us"]) / 1_000_000,
channel=self.channel,
is_fd=bool(frame.get("is_fd", False)),
bitrate_switch=bool(frame.get("bitrate_switch", False)),
)
if requested_ms == 0 or time.monotonic() >= deadline:
return None
def close(self) -> None:
# The owner closes all channel handles together. Executors may safely close views.
return None
class ZlgCanBridgeBus(ZlgCanChannelBus):
"""Backward-compatible single-channel ZLG bus."""
def __init__(
self,
python_executable: Path,
bridge_script: Path,
dll_path: Path,
device_type: int,
device_index: int,
channel: int,
bitrate: int,
termination_enabled: bool = True,
):
self._owner = ZlgCanBridgeProcess(
python_executable=python_executable,
bridge_script=bridge_script,
dll_path=dll_path,
device_type=device_type,
device_index=device_index,
channels=[
ZlgChannelConfig(
channel=channel,
arbitration_bitrate=bitrate,
termination_enabled=termination_enabled,
)
],
)
super().__init__(self._owner, channel)
def close(self) -> None:
self._owner.close()
def __enter__(self) -> "ZlgCanBridgeBus":
return self
@@ -19,7 +19,14 @@ from cognitive_agent.infrastructure.firmware import (
T2GSeatProgrammer,
)
from .gateway import CanBus, CanGatewayError, PythonCanBus, ZlgCanBridgeBus
from .gateway import (
CanBus,
CanGatewayError,
PythonCanBus,
ZlgCanBridgeBus,
ZlgCanBridgeProcess,
ZlgChannelConfig,
)
TRUE_VALUES = {"1", "true", "yes", "on"}
@@ -32,6 +39,8 @@ CAPABILITY_PROFILE = {
"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",
}
@@ -132,6 +141,7 @@ class SeatHardwareSettings:
profiles: frozenset[str]
backend: str
bitrate: int
termination_enabled: bool
channel: str
interface: str
device_type: int
@@ -196,6 +206,9 @@ class SeatHardwareSettings:
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),
@@ -234,7 +247,7 @@ class SeatHardwareSettings:
"COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S", 120
),
firmware_probe_timeout_ms=_env_int(
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROBE_TIMEOUT_MS", 1_200
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROBE_TIMEOUT_MS", 3_500
),
firmware_boot_wait_ms=_env_int(
"COGNITIVE_AGENT_SEAT_FIRMWARE_BOOT_WAIT_MS", 1_500
@@ -368,6 +381,7 @@ class SeatHardwareSettings:
device_index=self.device_index,
channel=numeric_channel,
bitrate=self.bitrate,
termination_enabled=self.termination_enabled,
)
return create_zlg_bus
@@ -390,9 +404,11 @@ class SeatHardwareSettings:
timeout_ms=self.firmware_program_timeout_ms,
)
def firmware_guard(self) -> T2GFirmwareGuard:
def firmware_guard(
self, bus_factory: Callable[[], CanBus] | None = None
) -> T2GFirmwareGuard:
return T2GFirmwareGuard(
self.bus_factory(),
bus_factory or 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,
@@ -420,6 +436,7 @@ class SeatHardwareSettings:
"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,
@@ -450,3 +467,209 @@ class SeatHardwareSettings:
"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":
return cls(
repository_root=repository_root,
enabled=_env_bool(
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED",
default=_env_bool("COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED"),
),
approval_required=_env_bool(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED", default=True
),
approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN", ""),
backend=os.getenv(
"COGNITIVE_AGENT_VEHICLE_CAN_BACKEND", "zlgcan"
).strip().lower(),
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", 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",
)
@property
def adapter_configured(self) -> bool:
return bool(
self.backend == "zlgcan"
and self.zlg_python.is_file()
and self.zlg_dll.is_file()
and self.bridge_script.is_file()
)
@property
def configured(self) -> bool:
return bool(
self.enabled
and (not self.approval_required or self.approval_token)
and self.adapter_configured
and self.timing_is_verified
)
@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 != "zlgcan":
raise HardwareSafetyError(
"vehicle CAN FD currently requires the zlgcan multi-channel backend"
)
if self.channel < 0:
raise HardwareSafetyError("vehicle CAN channel cannot be negative")
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,
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]) -> 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,
)
def public_info(self) -> dict[str, Any]:
return {
"enabled": self.enabled,
"configured": self.configured,
"backend": self.backend,
"platform": "t2g_cabin_bench",
"channel": self.channel,
"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",
}
@@ -1,7 +1,9 @@
"""32-bit JSON-line bridge for the ZLG zlgcan.dll API.
"""32-bit JSON-line bridge for a multi-channel ZLG CAN FD adapter.
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.
The module deliberately has no project or third-party imports. It runs under
the 32-bit Python bundled with ZCANPRO while the application uses 64-bit Python.
One bridge owns the physical adapter once and initializes every requested
channel, which avoids two processes racing to open the same USB device.
"""
from __future__ import annotations
@@ -10,10 +12,12 @@ import argparse
import ctypes
import json
import sys
import time
ZCAN_TYPE_CANFD = 1
ZCAN_STATUS_OK = 1
CANFD_BRS = 0x01
class ZcanClassicInit(ctypes.Structure):
@@ -62,6 +66,17 @@ class ZcanFrame(ctypes.Structure):
]
class ZcanFdFrame(ctypes.Structure):
_fields_ = [
("can_id", ctypes.c_uint32),
("len", ctypes.c_uint8),
("flags", ctypes.c_uint8),
("reserved0", ctypes.c_uint8),
("reserved1", ctypes.c_uint8),
("data", ctypes.c_uint8 * 64),
]
class ZcanTransmitData(ctypes.Structure):
_fields_ = [("frame", ZcanFrame), ("transmit_type", ctypes.c_uint32)]
@@ -70,6 +85,14 @@ class ZcanReceiveData(ctypes.Structure):
_fields_ = [("frame", ZcanFrame), ("timestamp", ctypes.c_uint64)]
class ZcanTransmitFdData(ctypes.Structure):
_fields_ = [("frame", ZcanFdFrame), ("transmit_type", ctypes.c_uint32)]
class ZcanReceiveFdData(ctypes.Structure):
_fields_ = [("frame", ZcanFdFrame), ("timestamp", ctypes.c_uint64)]
class ZcanChannelErrorInfo(ctypes.Structure):
_fields_ = [
("error_code", ctypes.c_uint32),
@@ -87,45 +110,85 @@ class Driver:
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
)
self.device = self.dll.ZCAN_OpenDevice(args.device_type, args.device_index, 0)
if not self.device:
raise RuntimeError(
f"ZCAN_OpenDevice failed for device_type={args.device_type}, "
f"device_index={args.device_index}; connect the adapter and close "
"ZCANPRO first"
"ZCAN_OpenDevice failed for device_type={}, device_index={}; "
"connect the adapter and close ZCANPRO first".format(
args.device_type, args.device_index
)
)
self.channel = None
self.channels = {}
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)
for config in self._channel_configs(args):
self._initialize_channel(config)
except Exception:
self.close()
raise
@staticmethod
def _channel_configs(args):
if args.channels_json:
configs = json.loads(args.channels_json)
else:
configs = [
{
"channel": args.channel,
"arbitration_bitrate": args.bitrate,
"data_bitrate": args.data_bitrate,
"can_fd_iso": True,
"termination_enabled": True,
}
]
if not isinstance(configs, list) or not configs:
raise ValueError("channels-json must be a non-empty JSON array")
channel_ids = [int(item["channel"]) for item in configs]
if len(channel_ids) != len(set(channel_ids)):
raise ValueError("channel numbers must be unique")
return configs
def _set_value(self, path, value):
result = self.dll.ZCAN_SetValue(
self.device, path.encode("ascii"), str(value).encode("ascii")
)
if result != ZCAN_STATUS_OK:
raise RuntimeError("ZCAN_SetValue({}) failed".format(path))
def _initialize_channel(self, config):
channel_id = int(config["channel"])
arbitration_bitrate = int(config.get("arbitration_bitrate", 500000))
data_bitrate = int(config.get("data_bitrate", 2000000))
can_fd_iso = bool(config.get("can_fd_iso", True))
if channel_id < 0:
raise ValueError("channel number cannot be negative")
if arbitration_bitrate <= 0 or data_bitrate <= 0:
raise ValueError("CAN bitrates must be positive")
prefix = str(channel_id)
self._set_value(prefix + "/protocol", 1)
self._set_value(prefix + "/canfd_standard", 0 if can_fd_iso else 1)
self._set_value(prefix + "/canfd_exp", 1)
self._set_value(prefix + "/canfd_abit_baud_rate", arbitration_bitrate)
self._set_value(prefix + "/canfd_dbit_baud_rate", data_bitrate)
init = ZcanChannelInit()
init.can_type = ZCAN_TYPE_CANFD
init.canfd.acc_code = 0
init.canfd.acc_mask = 0xFFFFFFFF
init.canfd.filter = 0
init.canfd.mode = 0
handle = self.dll.ZCAN_InitCAN(self.device, channel_id, ctypes.byref(init))
if not handle:
raise RuntimeError("ZCAN_InitCAN failed for channel {}".format(channel_id))
self.channels[channel_id] = handle
self._set_value(
prefix + "/initenal_resistance",
1 if bool(config.get("termination_enabled", True)) else 0,
)
if self.dll.ZCAN_StartCAN(handle) != ZCAN_STATUS_OK:
raise RuntimeError("ZCAN_StartCAN failed for channel {}".format(channel_id))
self.dll.ZCAN_ClearBuffer(handle)
def _declare_functions(self):
handle = ctypes.c_void_p
self.dll.ZCAN_OpenDevice.argtypes = [
@@ -168,10 +231,60 @@ class Driver:
ctypes.c_int32,
]
self.dll.ZCAN_Receive.restype = ctypes.c_uint32
self.dll.ZCAN_TransmitFD.argtypes = [
handle,
ctypes.POINTER(ZcanTransmitFdData),
ctypes.c_uint32,
]
self.dll.ZCAN_TransmitFD.restype = ctypes.c_uint32
self.dll.ZCAN_ReceiveFD.argtypes = [
handle,
ctypes.POINTER(ZcanReceiveFdData),
ctypes.c_uint32,
ctypes.c_int32,
]
self.dll.ZCAN_ReceiveFD.restype = ctypes.c_uint32
def send(self, arbitration_id, data):
def _handle(self, channel):
channel_id = int(channel)
try:
return self.channels[channel_id]
except KeyError:
raise ValueError("channel {} was not initialized".format(channel_id))
def _transmit_error(self, handle, operation):
error = ZcanChannelErrorInfo()
read_ok = self.dll.ZCAN_ReadChannelErrInfo(handle, ctypes.byref(error))
detail = (
"; channel_error=0x{:08X}".format(error.error_code)
if read_ok == ZCAN_STATUS_OK
else "; channel error info unavailable"
)
raise RuntimeError(operation + " did not accept the frame" + detail)
def send(self, channel, arbitration_id, data, is_fd=False, bitrate_switch=False):
if not 0 <= arbitration_id <= 0x7FF:
raise ValueError("only 11-bit standard CAN identifiers are allowed")
handle = self._handle(channel)
if is_fd:
valid_lengths = set(range(9)) | {12, 16, 20, 24, 32, 48, 64}
if len(data) not in valid_lengths:
raise ValueError(
"CAN FD payload length must be 0-8, 12, 16, 20, 24, 32, 48 or 64 bytes"
)
transmit = ZcanTransmitFdData()
transmit.frame.can_id = arbitration_id
transmit.frame.len = len(data)
transmit.frame.flags = CANFD_BRS if bitrate_switch else 0
for index, byte in enumerate(data):
transmit.frame.data[index] = byte
transmit.transmit_type = self.transmit_type
if self.dll.ZCAN_TransmitFD(handle, ctypes.byref(transmit), 1) != 1:
self._transmit_error(handle, "ZCAN_TransmitFD")
return
if bitrate_switch:
raise ValueError("bitrate switching is only valid for CAN FD frames")
if len(data) > 8:
raise ValueError("classic CAN payload exceeds 8 bytes")
transmit = ZcanTransmitData()
@@ -180,38 +293,63 @@ class Driver:
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)
if self.dll.ZCAN_Transmit(handle, ctypes.byref(transmit), 1) != 1:
self._transmit_error(handle, "ZCAN_Transmit")
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")
@staticmethod
def _classic_public(receive):
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),
"is_fd": False,
"bitrate_switch": False,
}
@staticmethod
def _fd_public(receive):
frame = receive.frame
return {
"arbitration_id": int(frame.can_id & 0x1FFFFFFF),
"data_hex": bytes(frame.data[: frame.len]).hex(),
"timestamp_us": int(receive.timestamp),
"is_fd": True,
"bitrate_switch": bool(frame.flags & CANFD_BRS),
}
def receive(self, channel, timeout_ms, include_fd=False):
handle = self._handle(channel)
deadline = time.monotonic() + max(0, int(timeout_ms)) / 1000.0
while True:
classic = ZcanReceiveData()
count = self.dll.ZCAN_Receive(handle, ctypes.byref(classic), 1, 0)
if count == 1:
return self._classic_public(classic)
if count != 0:
raise RuntimeError("ZCAN_Receive returned an unexpected frame count")
if include_fd:
fd_receive = ZcanReceiveFdData()
count = self.dll.ZCAN_ReceiveFD(
handle, ctypes.byref(fd_receive), 1, 0
)
if count == 1:
return self._fd_public(fd_receive)
if count != 0:
raise RuntimeError(
"ZCAN_ReceiveFD returned an unexpected frame count"
)
remaining = deadline - time.monotonic()
if remaining <= 0:
return None
time.sleep(min(0.002, remaining))
def close(self):
if self.channel:
self.dll.ZCAN_ResetCAN(self.channel)
self.channel = None
for channel_id in sorted(self.channels, reverse=True):
handle = self.channels.pop(channel_id)
self.dll.ZCAN_ResetCAN(handle)
if self.device:
self.dll.ZCAN_CloseDevice(self.device)
self.device = None
@@ -222,8 +360,10 @@ def parse_args():
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("--channels-json")
parser.add_argument("--channel", type=int, default=0)
parser.add_argument("--bitrate", type=int, default=500000)
parser.add_argument("--data-bitrate", type=int, default=2000000)
parser.add_argument(
"--transmit-type",
type=int,
@@ -247,6 +387,8 @@ def main():
"ZCAN_StartCAN",
"ZCAN_Transmit",
"ZCAN_Receive",
"ZCAN_TransmitFD",
"ZCAN_ReceiveFD",
)
missing = [name for name in required if not hasattr(dll, name)]
emit(
@@ -256,9 +398,12 @@ def main():
"missing_exports": missing,
"structure_sizes": {
"init": ctypes.sizeof(ZcanChannelInit),
"frame": ctypes.sizeof(ZcanFrame),
"tx": ctypes.sizeof(ZcanTransmitData),
"rx": ctypes.sizeof(ZcanReceiveData),
"classic_frame": ctypes.sizeof(ZcanFrame),
"classic_tx": ctypes.sizeof(ZcanTransmitData),
"classic_rx": ctypes.sizeof(ZcanReceiveData),
"fd_frame": ctypes.sizeof(ZcanFdFrame),
"fd_tx": ctypes.sizeof(ZcanTransmitFdData),
"fd_rx": ctypes.sizeof(ZcanReceiveFdData),
"error": ctypes.sizeof(ZcanChannelErrorInfo),
},
}
@@ -268,27 +413,43 @@ def main():
driver = None
try:
driver = Driver(args)
emit({"ok": True, "event": "ready"})
emit({"ok": True, "event": "ready", "channels": sorted(driver.channels)})
for line in sys.stdin:
try:
request = json.loads(line)
operation = request.get("operation")
if operation == "send":
channel = int(request.get("channel", args.channel))
arbitration_id = int(request["arbitration_id"])
data = bytes.fromhex(request["data_hex"])
driver.send(arbitration_id, data)
is_fd = bool(request.get("is_fd", False))
bitrate_switch = bool(request.get("bitrate_switch", False))
driver.send(
channel,
arbitration_id,
data,
is_fd=is_fd,
bitrate_switch=bitrate_switch,
)
emit(
{
"ok": True,
"channel": channel,
"arbitration_id": arbitration_id,
"data_hex": data.hex(),
"is_fd": is_fd,
"bitrate_switch": bitrate_switch,
}
)
elif operation == "receive":
emit(
{
"ok": True,
"frame": driver.receive(int(request.get("timeout_ms", 0))),
"frame": driver.receive(
int(request.get("channel", args.channel)),
int(request.get("timeout_ms", 0)),
include_fd=bool(request.get("include_fd", False)),
),
}
)
elif operation == "close":