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.
51 lines
1.1 KiB
Python
51 lines
1.1 KiB
Python
"""Host-to-device command payload encoders and type constants."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
from dataclasses import dataclass
|
|
|
|
from donglora.modulation import Modulation, encode_modulation
|
|
|
|
# Message type identifiers (PROTOCOL.md §4)
|
|
TYPE_PING: int = 0x01
|
|
TYPE_GET_INFO: int = 0x02
|
|
TYPE_SET_CONFIG: int = 0x03
|
|
TYPE_TX: int = 0x04
|
|
TYPE_RX_START: int = 0x05
|
|
TYPE_RX_STOP: int = 0x06
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TxFlags:
|
|
skip_cad: bool = False
|
|
|
|
def as_byte(self) -> int:
|
|
return 0b0000_0001 if self.skip_cad else 0
|
|
|
|
|
|
def encode_ping_payload() -> bytes:
|
|
return b""
|
|
|
|
|
|
def encode_get_info_payload() -> bytes:
|
|
return b""
|
|
|
|
|
|
def encode_set_config_payload(modulation: Modulation) -> bytes:
|
|
return encode_modulation(modulation)
|
|
|
|
|
|
def encode_tx_payload(data: bytes, *, skip_cad: bool = False) -> bytes:
|
|
if not data:
|
|
raise ValueError("TX payload must be non-empty")
|
|
return struct.pack("<B", TxFlags(skip_cad).as_byte()) + data
|
|
|
|
|
|
def encode_rx_start_payload() -> bytes:
|
|
return b""
|
|
|
|
|
|
def encode_rx_stop_payload() -> bytes:
|
|
return b""
|