feat!: Protocol v2 client rewrite with Dongle/Session model

The full 1.0 client surface, matching the protocol v2 wire definitions from donglora-protocol 1.0.0.

* Public API is now async. The blocking Client<T: Transport> is replaced by Dongle (from donglora_client::connect / connect_with / ConnectOptions).

* src/client.rs and src/codec.rs are removed in favor of src/dongle.rs (high-level surface), src/session.rs (per-client tag/slot state), src/retry.rs (RetryPolicy + tx_with_retry), src/errors.rs (thiserror-based ClientError / ClientResult). src/protocol.rs is replaced by direct donglora-protocol imports.

* Transport is now tokio-serial for USB and tokio::net::{UnixStream, TcpStream} for mux, replacing the blocking serial + shim model.

* Dongle::tx_with_retry(data, &RetryPolicy) implements PROTOCOL.md §6.10 randomized backoff for CHANNEL_BUSY / EBUSY, returning a TxOutcome with per-attempt trace.

* Background keepalive task pings every 500 ms on host idle, tracking the 1 s inactivity timeout from §3.4 with 2x margin. Disable via ConnectOptions::keepalive(false).

* Auto-recovery from ERR(ENOTCONFIGURED): the Dongle caches the last SET_CONFIG and silently re-applies on session drop, retrying once.

* Error taxonomy switches from anyhow to thiserror. Transport-level conditions (Timeout, TransportClosed, ReaderExited, BadFrame) are first-class.

* Examples rewritten for the async API: examples/ping.rs, examples/tx.rs, examples/rx.rs all target the MeshCore US preset (910.525 MHz / BW 62.5 / SF 7 / CR 4/5 / sync 0x1424 / 20 dBm). examples/common/mod.rs holds the shared helper.

* tests/vectors.rs gains protocol-level roundtrip coverage anchored to the published donglora-protocol 1.0.0 types.
This commit is contained in:
swaits committed 2026-04-22 09:45:38 -06:00
1 parent 055fc76b87
commit 6937ad47ba
19 files changed
+2897 -1605

No files matched your search

+20 -15
View File
@@ -1,25 +1,30 @@
//! Connect to a DongLoRa device, ping it, and print the MAC address.
//! Connect to a DongLoRa device, ping it, and print its info.
//!
//! Run with: cargo run --example ping
//! Run with: `cargo run --example ping`
use donglora_client::{RadioConfig, try_connect};
use std::time::Duration;
use donglora_client::connect;
fn main() -> anyhow::Result<()> {
let mut client = try_connect(Duration::from_secs(2))?;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber_init();
let dongle = connect().await?;
println!("connected!");
client.ping()?;
dongle.ping().await?;
println!("ping ok");
let mac = client.get_mac()?;
println!("MAC: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
let config = client.get_config()?;
println!("current config: {config:?}");
client.set_config(RadioConfig::default())?;
println!("config set to defaults");
let info = dongle.info();
println!("firmware: v{}.{}.{} ({:?})", info.fw_major, info.fw_minor, info.fw_patch, info.chip_id());
println!("TX power range: {} .. {} dBm", info.tx_power_min_dbm, info.tx_power_max_dbm);
println!("max OTA payload: {} bytes", info.max_payload_bytes);
dongle.close().await;
Ok(())
}
fn tracing_subscriber_init() {
// Best-effort subscriber; ignore errors if one is already set.
use tracing_subscriber::{EnvFilter, fmt};
let _ = fmt().with_env_filter(EnvFilter::from_default_env()).try_init();
}