Add LVSolarBuck64 firmware and debug console with uv support
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>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user