feat!: mux-py on DongLoRa Protocol v2 with tag-aware dispatch
The 1.0 Python mux, mirroring the Rust donglora-mux 1.0 one-for-one and implementing the full §13 multi-client semantics on the DongLoRa Protocol v2 wire format.
* Structural rewrite. donglora_mux/{daemon,intercept,session,tagmap}.py replace the 601-line __init__.py monolith.
* Tag-aware dispatch. TagMapper with skip-zero allocation and per-client bulk drop on disconnect. Multiple clients can have commands in flight concurrently; the single-oneshot response slot is gone.
* Full §13.2 multi-client SET_CONFIG semantics. Tracks (client_id, modulation) lock owner and synthesizes OK(APPLIED/MINE), OK(ALREADY_MATCHED/OTHER), or OK(LOCKED_MISMATCH/OTHER).
* TX loopback fan-out (§13.4). On TX_DONE(TRANSMITTED) the mux synthesizes an RX frame with origin = LOCAL_LOOPBACK for every connected client except the transmitter.
* Async ERR(EFRAME) fan-out: tag-0 framing errors are broadcast to all clients.
* Per-client TX cache on ClientSession so the loopback fan-out does not need the sender to re-buffer.
* Socket and serial fcntl.flock to prevent multiple mux instances from fighting over the same dongle or socket path.
* 500 ms keepalive matching PROTOCOL.md §3.4 (2x margin on the 1 s inactivity timer), replacing the 5 s cadence from 0.x.
* Clean SIGINT / SIGTERM shutdown: per-client handler tasks are tracked and cancelled, so blocked reader.read() calls no longer pin the event loop open.
This commit is contained in:
12 files changed
+1693
-694
No files matched your search
+56
-9
@@ -1,19 +1,66 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 1.0.0 — 2026-04-21
|
||||
|
||||
### Breaking
|
||||
|
||||
- **Upgraded to the DongLoRa Protocol v2 wire format.** The 0.x line spoke the legacy
|
||||
v0.1 protocol with hardcoded tag constants (`TAG_OK=4`, `TAG_ERROR=5`,
|
||||
`CMD_TAG_SET_CONFIG=2`, …) that don't exist in v1.0. This release is
|
||||
wire-incompatible with 0.x clients and firmware.
|
||||
- **Pins `donglora>=1.0.0`** and uses its `donglora.frame` /
|
||||
`donglora.events` / `donglora.commands` modules as the wire-level
|
||||
backbone. No more in-package reimplementation of the wire protocol.
|
||||
- **Structural rewrite** mirroring the Rust sibling
|
||||
[`donglora-mux`](https://crates.io/crates/donglora-mux) one-to-one:
|
||||
`donglora_mux/{daemon,intercept,session,tagmap}.py` replace the single
|
||||
601-line `__init__.py`.
|
||||
|
||||
### Added
|
||||
|
||||
- Keepalive ping: detects unresponsive dongles (e.g. UART boards behind a
|
||||
CP2102 bridge where the serial port stays open across ESP32 resets). Pings
|
||||
every 5 seconds of idle, declares dongle lost after 2-second timeout.
|
||||
- Radio state restoration after reconnect: saved SetConfig and StartRx are
|
||||
re-sent to the dongle so clients don't need to know a reset happened.
|
||||
- **`TagMapper`** — per-command device-tag allocator. Enables concurrent
|
||||
out-of-order command/response correlation for multi-client setups
|
||||
(the v0.1-era single-oneshot-response slot is gone).
|
||||
- **Full `§13.2` multi-client `SET_CONFIG` semantics** — tracks the
|
||||
`(client_id, modulation)` lock owner. Cross-client calls synthesize:
|
||||
- `OK(APPLIED / MINE, params)` when the caller owns the lock
|
||||
(forwarded to dongle).
|
||||
- `OK(ALREADY_MATCHED / OTHER, current)` when another client holds
|
||||
the lock and the requested params byte-match.
|
||||
- `OK(LOCKED_MISMATCH / OTHER, current)` when they differ; `current`
|
||||
echoes the lock owner's actual modulation.
|
||||
- Lock auto-releases when the owning client disconnects.
|
||||
- **TX loopback fan-out (`§13.4`)** — on `TX_DONE(TRANSMITTED)` the mux
|
||||
synthesizes an `RX` frame with `origin = LOCAL_LOOPBACK` and sends it
|
||||
to every connected client except the transmitter.
|
||||
- **Async `ERR(EFRAME)` fan-out** — if the dongle emits async framing
|
||||
errors (tag 0), they're broadcast to all clients rather than swallowed.
|
||||
- **Per-client TX cache** on `ClientSession` so the loopback fan-out
|
||||
doesn't need the sender to re-buffer the payload.
|
||||
- **Socket and serial file locking** (`fcntl.flock`) to prevent multiple
|
||||
mux instances from fighting over the same dongle or socket path.
|
||||
- **500 ms keepalive** matching `PROTOCOL.md §3.4` (2× margin on the
|
||||
1 s inactivity timer). Replaces the 5 s cadence from 0.x.
|
||||
- **Clean ^C / SIGTERM shutdown** — per-client handler tasks are tracked
|
||||
and cancelled on shutdown, so blocked `reader.read()` calls no longer
|
||||
pin the event loop open.
|
||||
|
||||
### Fixed
|
||||
### Removed
|
||||
|
||||
- Serial port opened without baud rate (defaulted to 9600 instead of 115200),
|
||||
preventing connection to UART boards behind CP2102 bridges.
|
||||
- Every v0.1 wire constant and the inline `cobs` + raw `serial.Serial`
|
||||
frame codec.
|
||||
- The single `pending_response: asyncio.Future` slot (fundamentally
|
||||
incompatible with v1.0's per-tag concurrency).
|
||||
|
||||
### Test coverage
|
||||
|
||||
- 28 tests (up from 11):
|
||||
- `test_tagmap.py` (6) — tag allocator behaviour.
|
||||
- `test_intercept.py` (13) — every branch of the `decide()` matrix,
|
||||
including the `ALREADY_MATCHED` / `LOCKED_MISMATCH` synthesis paths.
|
||||
- `test_multiclient.py` (9) — asyncio-driven daemon dispatch tests:
|
||||
concurrent tag routing, `SET_CONFIG` lock installation, TX loopback
|
||||
to peer clients, lock release on disconnect, unsolicited-frame drop.
|
||||
|
||||
## 0.2.0 — 2026-04-08
|
||||
|
||||
|
||||
Reference in new issue
Block a user