"""Shared CAN protocol selection and disk-backed session buffer (no Qt).""" import csv import os 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"), ("deif", "DEIF XDi-192 / CANopen"), ("protocan", "ProtoCAN / GAS"), ("ump_balsam", "УМП / Balsam 167 (legacy CAN)"), ("ump1_gas167", "УМП 1 / ProtoCAN — карта GAS 167/v1"), ("ump2_gas167", "УМП 2 / ProtoCAN — карта GAS 167/v1"), ("ump_gas", "УМП / ProtoCAN GAS — новые ID 5000/6000")) def decode_protocol(frame, protocol="auto"): if protocol == "deif": from .deif_xdi import decode return decode(frame) if protocol == "ump_balsam": from .balsam_can import decode result = decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) if result is not None: return result result = pc.Decoded(pc.ProtoCanId.parse(frame.can_id), len(frame.data), bytes(frame.data), ide=frame.ide, rtr=frame.rtr, protocol="BALZAM 167", message_label="Не Balsam 167") result.summary = "УМП / Balsam 167: CAN ID или формат кадра не соответствует протоколу" result.warnings.append(result.summary) return result if protocol in ("ump1_gas167", "ump2_gas167"): from .ump_can import decode_gas167 return decode_gas167(frame, node=1 if protocol == "ump1_gas167" else 2) if protocol in ("devboard", "protocan", "ump_gas"): 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; retain recent frames and unique-frame totals. 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.unique_frames = {} 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 # Timestamp is not part of a frame's identity. Keep the latest arrival # and a session-wide count, including repeats outside the recent buffer. key = row[1:] previous = self.unique_frames.get(key) self.unique_frames[key] = (row, previous[1] + 1 if previous else 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 save(self, path): """Save a full buffer snapshot without stopping an active recording.""" path = Path(path) if self.output is not None and (path.resolve() == self.path.resolve() or (path.exists() and path.samefile(self.path))): raise ValueError("Выберите другой файл: в этот CSV сейчас идёт запись.") temporary = None try: with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8-sig", newline="", dir=path.parent, delete=False) as output: temporary = Path(output.name) csv.writer(output, delimiter=";").writerow(self.HEADER) self.buffer.seek(0) while chunk := self.buffer.read(65536): output.write(chunk) os.replace(temporary, path) finally: self.buffer.seek(0, 2) if temporary is not None and temporary.exists(): temporary.unlink() 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() self.unique_frames.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)