"""Threading-based serial reader/writer.""" import threading import queue import time from typing import Callable, Optional import serial from .protocol import FrameParser, build_ping class SerialWorker: """Manages serial port in a background thread.""" def __init__(self, port: str, baudrate: int = 460800): self.port = port self.baudrate = baudrate self.ser: Optional[serial.Serial] = None self._rx_thread: Optional[threading.Thread] = None self._running = False self._parser = FrameParser() self._tx_queue: queue.Queue = queue.Queue() self._frame_callback: Optional[Callable] = None self._status_callback: Optional[Callable] = None self.connected = False self.pkt_count = 0 self.drop_count = 0 self._last_seq = -1 def set_frame_callback(self, cb: Callable): self._frame_callback = cb def set_status_callback(self, cb: Callable): self._status_callback = cb def start(self): self._running = True self._rx_thread = threading.Thread(target=self._run, daemon=True) self._rx_thread.start() def stop(self): self._running = False if self._rx_thread: self._rx_thread.join(timeout=2) if self.ser and self.ser.is_open: self.ser.close() self.connected = False def send(self, data: bytes): self._tx_queue.put(data) def _notify_status(self, connected: bool): self.connected = connected if self._status_callback: self._status_callback(connected) def _run(self): while self._running: # Connect if not self.ser or not self.ser.is_open: try: self.ser = serial.Serial( self.port, self.baudrate, timeout=0.1 ) self._parser = FrameParser() self._notify_status(True) except serial.SerialException: self._notify_status(False) time.sleep(1) continue # TX try: while not self._tx_queue.empty(): data = self._tx_queue.get_nowait() self.ser.write(data) except (serial.SerialException, OSError): self._handle_disconnect() continue # RX try: data = self.ser.read(256) if data: for cmd, payload in self._parser.feed(data): self.pkt_count += 1 if self._frame_callback: self._frame_callback(cmd, payload) except (serial.SerialException, OSError): self._handle_disconnect() def _handle_disconnect(self): try: if self.ser: self.ser.close() except Exception: pass self.ser = None self._notify_status(False) time.sleep(1)