"""CAN485 DevBoard_V1 USB command and frame helpers (no GUI/serial dependency).""" from __future__ import annotations import re from dataclasses import dataclass FRAME_RE = re.compile( r"^\[\s*(\d+)\.(\d{3})\]\s+(?:(RS485)\s+)?" r"(EXT|STD)\s+0x([0-9A-Fa-f]+)\s+" r"(RTR\s+)?DLC=(\d)(?:\s+DATA=([0-9A-Fa-f ]*))?" ) @dataclass(frozen=True, slots=True) class BoardFrame: timestamp: float extended: bool can_id: int data: bytes rtr: bool = False source: str = "CAN" def parse_frame_line(line: str) -> BoardFrame | None: match = FRAME_RE.match(line.strip()) if match is None: return None seconds, millis, source, frame_type, raw_id, raw_rtr, raw_dlc, raw_data = match.groups() dlc = int(raw_dlc) data = bytes.fromhex(raw_data or "") rtr = bool(raw_rtr) if (not rtr and len(data) != dlc) or (rtr and data): return None extended = frame_type == "EXT" can_id = int(raw_id, 16) if can_id > (0x1FFFFFFF if extended else 0x7FF): return None return BoardFrame(int(seconds) + int(millis) / 1000.0, extended, can_id, data, rtr, source or "CAN") def command_transmit(frame: BoardFrame) -> bytes: if len(frame.data) > 8: raise ValueError("DLC cannot exceed 8") kind = "E" if frame.extended else "S" return f"T,{kind},{frame.can_id:X},{len(frame.data)},{frame.data.hex().upper()}\n".encode("ascii") def command_setup(*, can_bitrate: int = 500, rs485_baud: int = 512000, route: int = 3) -> tuple[bytes, ...]: if can_bitrate not in (25, 50, 100, 125, 250, 500, 800, 1000): raise ValueError("unsupported CAN bitrate") if not 1200 <= rs485_baud <= 4_000_000: raise ValueError("unsupported RS485 baud") if route not in range(4): raise ValueError("route must be 0..3") return (f"S{can_bitrate}\n".encode("ascii"), b"M0\n", f"B{rs485_baud}\n".encode("ascii"), f"Q{route}\n".encode("ascii"))