"""pod_serial.py —— 吊舱串口链路(pyserial 8N1 + FrameParser 分帧)。 文档:波特率 115200、无校验、8N1。本模块只做字节搬运与分帧,不理解帧语义: * poll() —— 非阻塞读取已到达字节(timeout=0 + in_waiting),喂 FrameParser, 返回本次解析出的帧列表(粘包/半帧由 FrameParser 处理); * send() —— 写入一帧已编码字节; * close() —— 关闭串口。 非阻塞轮询设计便于与 asyncio 桥接主循环单线程协作(无需读线程)。PodLink 为 duck-typed 接口,桥接层据此工作,测试可注入内存 FakePod,无需硬件。 """ import logging from typing import List, Protocol, runtime_checkable from .protocol import FrameParser logger = logging.getLogger(__name__) @runtime_checkable class PodLink(Protocol): """吊舱链路接口(PodSerial 实机实现 / 测试 FakePod 均满足)。""" def poll(self) -> List[object]: """返回本次解析出的帧列表(Telemetry / TargetGeo 等)。""" ... def send(self, data: bytes) -> None: """写入一帧已编码字节。""" ... def close(self) -> None: ... class PodSerial: """实机串口实现。pyserial 惰性导入:未装 pyserial 也能 import 本模块并用 mock。""" def __init__(self, port: str, baudrate: int = 115200, parser: FrameParser | None = None) -> None: if not port: raise ValueError("吊舱串口未配置: pod_serial.port 为空") import serial # 惰性导入 self._ser = serial.Serial( port=port, baudrate=baudrate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=0, write_timeout=1.0, ) self._parser = parser or FrameParser() logger.info("[PodSerial] 打开 %s @%d 8N1", port, baudrate) def poll(self) -> List[object]: waiting = self._ser.in_waiting if waiting <= 0: return [] return self._parser.feed(self._ser.read(waiting)) def send(self, data: bytes) -> None: self._ser.write(data) def close(self) -> None: try: self._ser.close() logger.info("[PodSerial] 串口已关闭") except Exception as exc: logger.warning("[PodSerial] 关闭异常: %s", exc)