"""Background-thread wrapper around Candapter for use from the Streamlit UI.""" from __future__ import annotations import csv import sys import threading import time from pathlib import Path from typing import Optional sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from adapters.candapter import Candapter # noqa: E402 class CsvRecorder: """Appends frames to a CSV in the same format log_can.py uses.""" def __init__(self, path: Path): self._file = open(path, "w", newline="") self._writer = csv.writer(self._file) self._writer.writerow(["timestamp_ms", "arbitration_id", "is_extended", "dlc", "data_hex"]) def write(self, frame) -> None: self._writer.writerow( [ frame.timestamp_ms if frame.timestamp_ms is not None else "", f"{frame.arbitration_id:X}", int(frame.is_extended), len(frame.data), frame.data.hex().upper(), ] ) def close(self) -> None: self._file.close() class LiveCapture: """Runs Candapter.read_frame() on a background thread, feeding a FrameStore.""" def __init__(self) -> None: self._thread: Optional[threading.Thread] = None self._stop = threading.Event() self.error: Optional[str] = None self.connected = False self.frame_count = 0 def is_running(self) -> bool: return bool(self._thread and self._thread.is_alive()) def start(self, port: str, bitrate: int, store, record_path: Optional[Path] = None) -> None: if self.is_running(): return self._stop.clear() self.error = None self.frame_count = 0 self._thread = threading.Thread(target=self._run, args=(port, bitrate, store, record_path), daemon=True) self._thread.start() def stop(self) -> None: self._stop.set() if self._thread: self._thread.join(timeout=2) self.connected = False def _run(self, port: str, bitrate: int, store, record_path: Optional[Path]) -> None: try: adapter = Candapter(port, bitrate, timestamps=True) except (OSError, ValueError) as exc: self.error = str(exc) return recorder = CsvRecorder(record_path) if record_path else None self.connected = True start_wall = time.time() try: with adapter: while not self._stop.is_set(): frame = adapter.read_frame(timeout=0.5) if frame is None: continue store.feed(frame.arbitration_id, frame.data, frame.recv_time - start_wall) self.frame_count += 1 if recorder is not None: recorder.write(frame) finally: self.connected = False if recorder is not None: recorder.close()