feat!: bridge on DongLoRa Protocol v2 and async client

The 1.0 bridge running on the async Dongle API from donglora-client 1.0.0 and the Protocol v2 wire format.

* Radio loop is now a tokio task, not a dedicated std::thread. Everything is async end to end.

* [radio] config schema: tx_power renamed to tx_power_dbm (plain i8, no "max" keyword), cad field removed (CAD is a per-TX flag in v1.0), default sync_word changed 0x3444 to 0x1424 to match the SX126x register encoding of MeshCore private sync byte.

* TX retry with backoff (new [tx] config section). Every TX runs through Dongle::tx_with_retry, retrying TX_DONE(CHANNEL_BUSY) and ERR(EBUSY) per PROTOCOL.md §6.10 / §C.5.5. Defaults: 3 attempts, 20-100 ms randomized backoff, doubling to 500 ms cap, 5 s per-attempt timeout. Configurable via max_attempts, backoff_min_ms, backoff_max_ms, backoff_cap_ms, backoff_multiplier, per_attempt_timeout_secs.

* TUI packet log shows retry state: in-flight retries render with a cycle marker in yellow and an N/M attempt counter, terminal TX failures render in red, post-retry success shows final attempt count.

* Stats gain tx_retries and tx_failures counters surfaced in StatsSnapshot.

* Measured airtime from TX_DONE replaces the theoretical estimate, feeding the rate limiter with the real on-air duration.

* The explicit 2 s liveness ping loop is gone. The Dongle runs a 500 ms internal keepalive, matching spec §3.4 inactivity window with 2x margin.
This commit is contained in:
swaits committed 2026-04-22 09:48:01 -06:00
1 parent 030280791e
commit 0b9775c310
11 files changed
+1010 -669

No files matched your search

+4 -7
View File
@@ -58,15 +58,12 @@ pub fn run_wizard(config_path: &Path, existing: Option<&Config>) -> Result<()> {
let sync_word_str = prompt_default("Sync Word", &sw_default)?;
let sync_word = parse_sync_word(&sync_word_str)?;
let tx_power = prompt_default("TX Power (dBm or 'max')", &radio.tx_power)?;
let tx_power_default = format!("{}", radio.tx_power_dbm);
let tx_power_dbm: i8 = prompt_parse("TX Power (dBm)", &tx_power_default)?;
let pre_default = format!("{}", radio.preamble);
let preamble: u16 = prompt_parse("Preamble", &pre_default)?;
let cad_default = if radio.cad { "on" } else { "off" };
let cad_str = prompt_default("CAD", cad_default)?;
let cad = matches!(cad_str.to_lowercase().as_str(), "on" | "true" | "yes" | "1");
let port_default = radio.port.as_deref().unwrap_or("auto");
let port_str = prompt_default("Serial Port", port_default)?;
let port = if port_str == "auto" || port_str.is_empty() { None } else { Some(port_str) };
@@ -81,9 +78,8 @@ pub fn run_wizard(config_path: &Path, existing: Option<&Config>) -> Result<()> {
spreading_factor,
coding_rate,
sync_word,
tx_power,
tx_power_dbm,
preamble,
cad,
},
bridge: BridgeSection {
passphrase,
@@ -92,6 +88,7 @@ pub fn run_wizard(config_path: &Path, existing: Option<&Config>) -> Result<()> {
rate_limit_pps: bridge.rate_limit_pps,
log_file: bridge.log_file,
},
tx: existing.map(|c| c.tx.clone()).unwrap_or_default(),
};
write_config(config_path, &cfg)?;