123 lines
4.0 KiB
Python
123 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
FR-2.1: Asynchronous Serial Reader
|
|
Reads binary packets from STM32 via USB CDC
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Callable, Awaitable
|
|
|
|
import serial
|
|
from serial import SerialException
|
|
|
|
from protocol_parser import ProtocolParser, AudioMetrics
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SerialReader:
|
|
"""
|
|
Asynchronous serial port reader with protocol parsing.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
port: str,
|
|
baudrate: int = 115200,
|
|
on_packet: Callable[[AudioMetrics], Awaitable[None]] = None,
|
|
):
|
|
"""
|
|
Initialize serial reader.
|
|
|
|
Args:
|
|
port: Serial port path (e.g., /dev/ttyACM0)
|
|
baudrate: Serial baudrate (default 115200)
|
|
on_packet: Async callback for each received packet
|
|
"""
|
|
self.port = port
|
|
self.baudrate = baudrate
|
|
self.on_packet = on_packet
|
|
|
|
self.serial: serial.Serial = None
|
|
self.parser = ProtocolParser()
|
|
self._running = False
|
|
self._read_task: asyncio.Task = None
|
|
|
|
async def connect(self):
|
|
"""Open serial port connection"""
|
|
try:
|
|
self.serial = serial.Serial(
|
|
self.port, self.baudrate, timeout=0.1, write_timeout=1.0
|
|
)
|
|
logger.info(f"Connected to {self.port} @ {self.baudrate} baud")
|
|
except SerialException as e:
|
|
logger.error(f"Failed to open serial port {self.port}: {e}")
|
|
raise
|
|
|
|
async def disconnect(self):
|
|
"""Close serial port connection"""
|
|
self._running = False
|
|
|
|
if self._read_task and not self._read_task.done():
|
|
self._read_task.cancel()
|
|
try:
|
|
await self._read_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
if self.serial and self.serial.is_open:
|
|
self.serial.close()
|
|
logger.info("Serial port closed")
|
|
|
|
async def start_reading(self):
|
|
"""Start background task for reading serial data"""
|
|
self._running = True
|
|
self._read_task = asyncio.create_task(self._read_loop())
|
|
|
|
async def _read_loop(self):
|
|
"""Background task: continuously read and parse serial data"""
|
|
while self._running:
|
|
try:
|
|
# Read available data (non-blocking due to timeout)
|
|
if self.serial.in_waiting > 0:
|
|
data = self.serial.read(self.serial.in_waiting or 1)
|
|
|
|
if data:
|
|
# Parse packets
|
|
packets = self.parser.feed(data)
|
|
|
|
# Process each packet
|
|
for packet in packets:
|
|
if self.on_packet:
|
|
try:
|
|
await self.on_packet(packet)
|
|
except Exception as e:
|
|
logger.error(f"Error in packet handler: {e}")
|
|
|
|
# Log statistics periodically
|
|
stats = self.parser.get_stats()
|
|
if stats.packets_received % 100 == 0:
|
|
logger.info(
|
|
f"Stats: RX={stats.packets_received} "
|
|
f"CRC_err={stats.crc_errors} "
|
|
f"LEN_err={stats.length_errors} "
|
|
f"Range_err={stats.range_errors}"
|
|
)
|
|
|
|
# Small delay to prevent CPU spinning
|
|
await asyncio.sleep(0.01)
|
|
|
|
except SerialException as e:
|
|
logger.error(f"Serial read error: {e}")
|
|
self._running = False
|
|
break
|
|
except asyncio.CancelledError:
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error in read loop: {e}")
|
|
|
|
def get_stats(self):
|
|
"""Get protocol parser statistics"""
|
|
return self.parser.get_stats()
|