feat(ppclock): 命令层/传输层/CLI/本地模板渲染,90 测试全绿
This commit is contained in:
1 parent
6f5b0c60be
commit
2b01e5f589
60 files changed
+16869
No files matched your search
@@ -0,0 +1,91 @@
|
||||
"""BLE 传输层(bleak 封装)。硬件相关,单测以 FakeTransport 替代。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from bleak import BleakClient, BleakScanner
|
||||
from bleak.exc import BleakError
|
||||
|
||||
from . import protocol as P
|
||||
|
||||
|
||||
class TransportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
async def scan(timeout: float = 5.0, name_prefix: str = P.DEVICE_NAME_PREFIX):
|
||||
"""扫描设备,返回 [{name, address, rssi}]。"""
|
||||
found = await BleakScanner.discover(timeout=timeout, return_adv=True)
|
||||
out = []
|
||||
for addr, (dev, adv) in found.items():
|
||||
name = dev.name or adv.local_name or ""
|
||||
if name.startswith(name_prefix):
|
||||
out.append({"name": name, "address": dev.address, "rssi": adv.rssi})
|
||||
return out
|
||||
|
||||
|
||||
class BLETransport:
|
||||
"""与 FakeTransport 同接口:write_epd/write_rxtx/request_device_id/delay。"""
|
||||
|
||||
def __init__(self, address: str | None = None, timeout: float = 10.0,
|
||||
name_prefix: str = P.DEVICE_NAME_PREFIX):
|
||||
self.address = address
|
||||
self.timeout = timeout
|
||||
self.name_prefix = name_prefix
|
||||
self._client: BleakClient | None = None
|
||||
self._id_buf = bytearray()
|
||||
self._id_event = asyncio.Event()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.connect()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
await self.close()
|
||||
|
||||
async def connect(self):
|
||||
if not self.address:
|
||||
devices = await scan(timeout=self.timeout, name_prefix=self.name_prefix)
|
||||
if not devices:
|
||||
raise TransportError(f"未找到名称前缀 {self.name_prefix!r} 的设备")
|
||||
self.address = devices[0]["address"]
|
||||
self._client = BleakClient(self.address, timeout=self.timeout)
|
||||
try:
|
||||
await self._client.connect()
|
||||
except BleakError as e:
|
||||
raise TransportError(f"连接失败: {e}") from e
|
||||
await self._client.start_notify(P.RXTX_CHAR_UUID, self._on_notify)
|
||||
|
||||
async def close(self):
|
||||
if self._client and self._client.is_connected:
|
||||
try:
|
||||
await self._client.stop_notify(P.RXTX_CHAR_UUID)
|
||||
finally:
|
||||
await self._client.disconnect()
|
||||
|
||||
def _on_notify(self, _sender, data: bytearray):
|
||||
self._id_buf.extend(data)
|
||||
if len(self._id_buf) >= 14:
|
||||
self._id_event.set()
|
||||
|
||||
async def write_epd(self, data: bytes, response: bool = True):
|
||||
await self._client.write_gatt_char(P.EPD_CHAR_UUID, data, response=response)
|
||||
|
||||
async def write_rxtx(self, data: bytes, response: bool = True):
|
||||
await self._client.write_gatt_char(P.RXTX_CHAR_UUID, data, response=response)
|
||||
|
||||
async def read_rxtx(self) -> bytes:
|
||||
return bytes(await self._client.read_gatt_char(P.RXTX_CHAR_UUID))
|
||||
|
||||
async def request_device_id(self, timeout: float = 8.0) -> str:
|
||||
self._id_buf.clear()
|
||||
self._id_event.clear()
|
||||
await self.write_rxtx(P.request_id_frame())
|
||||
try:
|
||||
await asyncio.wait_for(self._id_event.wait(), timeout)
|
||||
except asyncio.TimeoutError as e:
|
||||
raise TransportError("等待设备 ID 应答超时") from e
|
||||
return P.parse_device_id(bytes(self._id_buf[:14]))
|
||||
|
||||
async def delay(self, seconds: float):
|
||||
await asyncio.sleep(seconds)
|
||||
Reference in new issue
Block a user