STM32G474RB firmware for solar buck converter with MPPT, CC control, Vfly compensation, and adaptive deadtime. Includes Textual TUI debug console for real-time telemetry, parameter tuning, and SQLite logging. Added pyproject.toml for uv: `cd code64 && uv run debug-console` Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
3.0 KiB
Python
101 lines
3.0 KiB
Python
"""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)
|