"""Shared CAN protocol selection and disk-backed session buffer (no Qt).""" import csv import tempfile from collections import Counter, deque from datetime import datetime from pathlib import Path from protocan import protocan as pc from .can_bridge import decode_frame PROTOCOLS = (("auto", "Авто (ProtoCAN / Balsam)"), ("devboard", "DevBoard_V1 / SETCAN")) def decode_protocol(frame, protocol="auto"): if protocol == "devboard": return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) return decode_frame(frame) def protocol_values(identifier, extended): if not extended: return ("",) * 7 value = pc.ProtoCanId.parse(identifier) return (str(value.priority), str(value.pm), str(value.device_type), str(value.device), value.device_name, "0x%X — %s" % (value.msg_type, value.msg_type_name), "0x%04X" % value.body) def parse_filter(text): if not text.strip(): return None try: ids = {int(part.strip(), 16) for part in text.replace(";", ",").split(",")} if any(value < 0 or value > 0x1FFFFFFF for value in ids): raise ValueError() return ids except ValueError: raise ValueError("Введите CAN ID в hex, например 701 или 0x701, 201") from None class CanSession: """All frames spool to disk; only the latest 5000 stay in display memory. Starting recording includes the current session buffer. Reset clears the buffer/counters but never removes frames already written to a recording. """ HEADER = ("Время", "Канал", "Тип", "ID", "RTR", "DLC", "Данные") def __init__(self): self.recent = deque(maxlen=5000) self.counts = Counter() self.output = None self.path = None self.buffer = tempfile.TemporaryFile(mode="w+", encoding="utf-8", newline="") def append(self, identifier, data, extended, rtr=False, source="CAN", timestamp=None): row = ((datetime.fromtimestamp(timestamp) if timestamp is not None else datetime.now()).isoformat(timespec="milliseconds"), source, "EXT" if extended else "STD", ("%08X" if extended else "%03X") % identifier, int(rtr), len(data), bytes(data).hex(" ").upper()) csv.writer(self.buffer, delimiter=";").writerow(row) self.recent.append((identifier, row)) self.counts[identifier] += 1 if self.output is not None: csv.writer(self.output, delimiter=";").writerow(row) def start(self, directory): directory = Path(directory) / datetime.now().strftime("session_%Y%m%d_%H%M%S_%f") directory.mkdir(parents=True, exist_ok=False) path = directory / "frames.csv" output = path.open("w", encoding="utf-8-sig", newline="") try: csv.writer(output, delimiter=";").writerow(self.HEADER) self.buffer.seek(0) while chunk := self.buffer.read(65536): output.write(chunk) output.flush() except Exception: output.close() raise finally: self.buffer.seek(0, 2) self.output, self.path = output, path return path def flush(self): if self.output is not None: self.output.flush() def stop(self): if self.output is not None: self.output.close() self.output = None def reset(self): self.buffer.seek(0) self.buffer.truncate() self.recent.clear() self.counts.clear() def close(self): self.stop() self.buffer.close() def format_data(data: bytes, decimal: bool = False) -> str: """Format individual payload bytes without changing their order or width.""" return " ".join(str(byte) if decimal else "%02X" % byte for byte in data)