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:
19 files changed
+2897
-1605
No files matched your search
@@ -0,0 +1,23 @@
|
||||
//! Shared defaults for the example binaries.
|
||||
//!
|
||||
//! Mirrors `examples/_common.py::MESHCORE_US`. See that file for the
|
||||
//! full justification — short version: 910.525 MHz / BW 62.5 / SF 7 /
|
||||
//! CR 4/5, sync 0x1424 (RADIOLIB_SX126X_SYNC_WORD_PRIVATE), 20 dBm.
|
||||
|
||||
use donglora_client::{LoRaBandwidth, LoRaCodingRate, LoRaConfig, LoRaHeaderMode, Modulation};
|
||||
|
||||
/// MeshCore's "USA/Canada (Recommended)" preset (October 2025 narrow).
|
||||
pub fn meshcore_us() -> Modulation {
|
||||
Modulation::LoRa(LoRaConfig {
|
||||
freq_hz: 910_525_000,
|
||||
sf: 7,
|
||||
bw: LoRaBandwidth::Khz62,
|
||||
cr: LoRaCodingRate::Cr4_5,
|
||||
preamble_len: 16,
|
||||
sync_word: 0x1424,
|
||||
tx_power_dbm: 20,
|
||||
header_mode: LoRaHeaderMode::Explicit,
|
||||
payload_crc: true,
|
||||
iq_invert: false,
|
||||
})
|
||||
}
|
||||
+20
-15
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Connect, apply the MeshCore USA preset, and print every received
|
||||
//! packet until interrupted. Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo run --example rx
|
||||
//! ```
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use donglora_client::{ConnectOptions, connect_with};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let _ = tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::from_default_env()).try_init();
|
||||
|
||||
let dongle = connect_with(ConnectOptions::new().config(common::meshcore_us())).await?;
|
||||
println!("connected; listening...");
|
||||
|
||||
while let Some(pkt) = dongle.next_rx().await {
|
||||
let rssi_dbm = pkt.rssi_tenths_dbm as f32 / 10.0;
|
||||
let snr_db = pkt.snr_tenths_db as f32 / 10.0;
|
||||
let text = String::from_utf8_lossy(&pkt.data);
|
||||
println!(
|
||||
"RX {:>4} bytes rssi={:6.1} dBm snr={:5.1} dB crc={} \"{}\"",
|
||||
pkt.data.len(),
|
||||
rssi_dbm,
|
||||
snr_db,
|
||||
if pkt.crc_valid { "ok" } else { "bad" },
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! Connect, apply the MeshCore USA preset, and transmit a greeting
|
||||
//! every 5 seconds. Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo run --example tx
|
||||
//! ```
|
||||
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use donglora_client::{ConnectOptions, connect_with};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let _ = tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::from_default_env()).try_init();
|
||||
|
||||
let dongle = connect_with(ConnectOptions::new().config(common::meshcore_us())).await?;
|
||||
println!("connected; TX ready");
|
||||
|
||||
let mut seq: u32 = 0;
|
||||
loop {
|
||||
seq += 1;
|
||||
let payload = format!("hello #{seq}");
|
||||
match dongle.tx(payload.as_bytes()).await {
|
||||
Ok(td) => println!("TX #{seq} sent ({} us airtime)", td.airtime_us),
|
||||
Err(e) => eprintln!("TX #{seq} failed: {e}"),
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user