feat!: Python client Protocol v2 rewrite with async Dongle surface
The 1.0 Python client, mirroring the Rust client 1.0 API and the DongLoRa Protocol v2 wire format. * Module split: monolithic protocol.py / client.py replaced with dongle.py (high-level async Dongle), session.py (reader task + tag routing), frame.py (streaming FrameDecoder + encode_frame), commands.py / events.py / errors.py / modulation.py / info.py / crc.py / chip_id (re-export) matching the donglora-protocol Rust crate. * Async API throughout. Serial I/O via asyncio-serial; mux connections via asyncio streams. The blocking client surface is gone. * thiserror-equivalent error hierarchy (DongloraError base with Timeout, TransportClosed, ReaderExited, BadFrame, ErrClientError variants mirroring the Rust ClientError). * Tag-aware dispatch with concurrent in-flight commands: TagAllocator hands out 16-bit device tags, Session routes each response back to the originating coroutine. * Auto-recovery from ERR(ENOTCONFIGURED): the Dongle caches the last SET_CONFIG and silently re-applies on timeout. * 500 ms background keepalive task per PROTOCOL.md §3.4. * Tests rewritten: test_dongle, test_session, test_frame, test_events, test_modulation, test_crc, test_ergonomics; old test_client / test_protocol removed. * Sticky mux reconnect (from the prior fix commit) is preserved: once a mux socket is in use, reconnects commit to it instead of falling back to USB.
This commit is contained in:
25 files changed
+3579
-1010
No files matched your search
+5
-43
@@ -1,12 +1,13 @@
|
||||
"""Tests for donglora.codec — COBS framing."""
|
||||
"""COBS helper tests — the thin re-export layer.
|
||||
|
||||
Frame-level codec tests live in ``test_frame.py``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from cobs import cobs
|
||||
|
||||
from donglora.codec import cobs_encode, read_frame
|
||||
from donglora.codec import cobs_encode
|
||||
|
||||
|
||||
class TestCobsEncode:
|
||||
@@ -34,42 +35,3 @@ class TestCobsEncode:
|
||||
encoded = cobs_encode(data)
|
||||
decoded = cobs.decode(encoded[:-1])
|
||||
assert decoded == data
|
||||
|
||||
|
||||
class _MockSerial:
|
||||
"""Minimal mock that behaves like serial.Serial for read_frame."""
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
self._stream = BytesIO(data)
|
||||
|
||||
def read(self, n: int = 1) -> bytes:
|
||||
return self._stream.read(n)
|
||||
|
||||
|
||||
class TestReadFrame:
|
||||
def test_basic(self) -> None:
|
||||
data = b"hello"
|
||||
frame = cobs_encode(data)
|
||||
result = read_frame(_MockSerial(frame))
|
||||
assert result == data
|
||||
|
||||
def test_timeout(self) -> None:
|
||||
result = read_frame(_MockSerial(b""))
|
||||
assert result is None
|
||||
|
||||
def test_empty_frame(self) -> None:
|
||||
result = read_frame(_MockSerial(b"\x00"))
|
||||
assert result is None
|
||||
|
||||
def test_corrupt_frame(self) -> None:
|
||||
result = read_frame(_MockSerial(b"\xff\x00"))
|
||||
# cobs.decode of [0xff] may or may not raise — just verify we get bytes or None
|
||||
assert result is None or isinstance(result, bytes)
|
||||
|
||||
def test_multiple_frames(self) -> None:
|
||||
frame1 = cobs_encode(b"first")
|
||||
frame2 = cobs_encode(b"second")
|
||||
mock = _MockSerial(frame1 + frame2)
|
||||
assert read_frame(mock) == b"first"
|
||||
assert read_frame(mock) == b"second"
|
||||
assert read_frame(mock) is None
|
||||
Reference in new issue
Block a user