"""Continuos serial frame reader (Live-mode source)""" from __future__ import annotations import logging import threading from typing import Callable from pygui.backend.models.frame import Frame log = logging.getLogger(__name__) ParseLine = Callable[[str, int], Frame] class StreamReader: def __init__(self, transport, parse_line: ParseLine, on_frame: Callable[[Frame], None], on_error: Callable[[Exception], None] | None = None ) -> None: self._transport = transport self._parse = parse_line self._on_frame = on_frame self._on_error = on_error or (lambda e: None) self._thread: threading.Thread | None = None self._stop = threading.Event() self._seq = 0 self.error_count = 0 def start(self) -> None: self._stop.clear() self._thread = threading.Thread(target=self._run, daemon = True) self._thread.start() def _run(self) -> None: try: while not self._stop.is_set(): raw = self._transport.readline() if not raw: continue text = raw.decode(errors="ignore").strip() if not text: continue try: frame= self._parse(text, self._seq) self._seq += 1 self._on_frame(frame) except Exception as exc: self.error_count += 1 self._on_error(exc) finally: try: self._transport.close() except Exception: pass def stop(self) -> None: self._stop.set() if self._thread is not None and self._thread.is_alive(): self._thread.join(timeout = 2.0) self._thread = None