init
This commit is contained in:
+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()
|
||||
Reference in New Issue
Block a user