Source code for cubemarspycan.frame

"""A transport-neutral CAN frame.

This module deliberately does **not** import ``can``. It is the shared vocabulary between
the pure codec layer and the transport layer; if it depended on python-can, the codec
would too, and the codec would stop being testable in isolation.
"""

from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any

STANDARD_ID_MAX = 0x7FF
EXTENDED_ID_MAX = 0x1FFFFFFF


[docs] @dataclass(frozen=True, slots=True) class Frame: """One classic-CAN data frame: an arbitration id, up to 8 payload bytes, and a flag.""" arbitration_id: int data: bytes is_extended_id: bool = False def __post_init__(self) -> None: # Accept bytearray/list/tuple for ergonomics, but store immutable bytes so the # frozen dataclass is genuinely frozen all the way down. Typed as Any so the # runtime coercion is not narrowed away by the declared annotation. raw: Any = self.data if isinstance(raw, int): # bytes(4) would silently produce four zero bytes, which is never intended. raise TypeError("Frame.data must be a byte sequence, not an int") object.__setattr__(self, "data", bytes(raw)) limit = EXTENDED_ID_MAX if self.is_extended_id else STANDARD_ID_MAX if not 0 <= self.arbitration_id <= limit: kind = "extended" if self.is_extended_id else "standard" raise ValueError( f"arbitration id 0x{self.arbitration_id:X} out of range for a " f"{kind} frame (max 0x{limit:X})" ) if len(self.data) > 8: raise ValueError(f"classic CAN payload is at most 8 bytes, got {len(self.data)}") @property def dlc(self) -> int: """Data length code: the payload length in bytes, 0-8. AK frames are DLC 8 in both modes; anything else is a foreign frame. """ return len(self.data)
[docs] def hex(self) -> str: """The payload as space-separated uppercase hex, e.g. ``"FF FF FF FF FF FF FF FC"``. For logs and for comparing against the manual's byte tables. """ return " ".join(f"{b:02X}" for b in self.data)
def __str__(self) -> str: width = 8 if self.is_extended_id else 3 tag = "x" if self.is_extended_id else " " return f"{self.arbitration_id:0{width}X}{tag} [{self.dlc}] {self.hex()}"
[docs] def frame(arbitration_id: int, data: Iterable[int], *, extended: bool = False) -> Frame: """Convenience constructor that accepts any iterable of byte values.""" return Frame(arbitration_id, bytes(data), extended)