feat: overhaul radio reconnection, TUI layout, DHT strategy, and signal handling
Radio reconnection: - Add connect_mux_auto() to donglora-client — mux-only connect that never silently falls back to USB serial (matches Python client's sticky mux) - Fix codec.rs read_frame() to handle WouldBlock alongside TimedOut - Add liveness ping (2s) to detect dead mux connections - Treat transmit errors as fatal (trigger reconnect) - Faster backoff: 250ms initial, 5s max, resets after stable connection TUI improvements: - Redesign packet log: RF+Net columns show full packet path (RX→TX), hash column with dedup ellipsis, fixed-width columns throughout - Move activity charts to right column (fills remaining width) - 1-second sparkline buckets instead of 10-second - Radio panel shows "(disconnected)" with red border when mux is down - Header: status pills left, clock with TZ right-justified, Ctrl-C quits - Local time via jiff crate with proper timezone abbreviation DHT strategy: - Alone: read every 3s, merge-publish every 30s (preserves existing peers) - Connected: publish authoritative [self + neighbors], purging stale peers - Publish immediately on first neighbor join Signal handling: - SIGINT and SIGTERM both trigger graceful shutdown via cancellation token - All paths go through swarm.shutdown() to update DHT before exit
This commit is contained in:
7 files changed
+373
-175
No files matched your search
+40
-4
@@ -50,10 +50,12 @@ fn radio_loop(
|
||||
event_tx: mpsc::Sender<RadioEvent>,
|
||||
mut tx_recv: mpsc::Receiver<Vec<u8>>,
|
||||
) {
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
let max_backoff = Duration::from_secs(30);
|
||||
let initial_backoff = Duration::from_millis(250);
|
||||
let max_backoff = Duration::from_secs(5);
|
||||
let mut backoff = initial_backoff;
|
||||
|
||||
loop {
|
||||
let started = std::time::Instant::now();
|
||||
match connect_and_run(&config, &port, &event_tx, &mut tx_recv) {
|
||||
Ok(()) => {
|
||||
info!("radio thread exiting");
|
||||
@@ -62,6 +64,13 @@ fn radio_loop(
|
||||
Err(e) => {
|
||||
error!("radio error: {e:#}");
|
||||
let _ = event_tx.blocking_send(RadioEvent::Disconnected);
|
||||
|
||||
// If we were connected for a while, reset backoff so
|
||||
// the next reconnect attempt is fast.
|
||||
if started.elapsed() > Duration::from_secs(5) {
|
||||
backoff = initial_backoff;
|
||||
}
|
||||
|
||||
info!("reconnecting in {backoff:?}");
|
||||
std::thread::sleep(backoff);
|
||||
backoff = (backoff * 2).min(max_backoff);
|
||||
@@ -85,7 +94,7 @@ fn connect_and_run(
|
||||
let c = donglora_client::connect(Some(port), timeout)?;
|
||||
(c, shorten_path(port))
|
||||
} else {
|
||||
let c = donglora_client::connect_default()?;
|
||||
let c = donglora_client::connect_mux_auto(timeout)?;
|
||||
(c, "mux".to_string())
|
||||
};
|
||||
info!("[radio] transport connected: {device}");
|
||||
@@ -128,18 +137,26 @@ fn connect_and_run(
|
||||
.set_timeout(Duration::from_millis(100))?;
|
||||
info!("[radio] entering main loop");
|
||||
|
||||
// Liveness: ping the mux periodically when idle to detect a dead connection.
|
||||
// Without this, a killed mux causes recv() to silently return Ok(None) forever.
|
||||
let liveness_interval = Duration::from_secs(2);
|
||||
let mut last_activity = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
// Check for TX requests (non-blocking).
|
||||
while let Ok(payload) = tx_recv.try_recv() {
|
||||
debug!("TX {} bytes", payload.len());
|
||||
if let Err(e) = client.transmit(&payload, None) {
|
||||
warn!("transmit error: {e:#}");
|
||||
return Err(e);
|
||||
}
|
||||
last_activity = std::time::Instant::now();
|
||||
}
|
||||
|
||||
// Check for RX packets.
|
||||
match client.recv() {
|
||||
Ok(Some(Response::RxPacket { rssi, snr, payload })) => {
|
||||
last_activity = std::time::Instant::now();
|
||||
let grade = snr_grade(snr, active_config.sf);
|
||||
if grade.should_forward() {
|
||||
debug!(
|
||||
@@ -157,12 +174,31 @@ fn connect_and_run(
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(Some(_)) => {}
|
||||
Ok(Some(_)) => {
|
||||
last_activity = std::time::Instant::now();
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) if is_timeout_error(&e) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Liveness check: if no real activity for a while, ping the mux.
|
||||
if last_activity.elapsed() >= liveness_interval {
|
||||
// Temporarily extend the timeout for the ping command.
|
||||
let _ = client.transport_mut().set_timeout(Duration::from_secs(2));
|
||||
match client.ping() {
|
||||
Ok(()) => {
|
||||
last_activity = std::time::Instant::now();
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("liveness ping failed: {e:#}");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
// Restore the short timeout.
|
||||
let _ = client.transport_mut().set_timeout(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
if event_tx.is_closed() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user