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:
swaits committed 2026-04-05 21:43:36 -06:00
1 parent 4508f317be
commit b4026ace34
7 files changed
+373 -175

No files matched your search

+17 -10
View File
@@ -142,6 +142,7 @@ async fn main() -> Result<()> {
requested: radio_config,
source: radio::ConfigSource::Ours,
device: String::new(),
connected: false,
},
);
@@ -175,18 +176,25 @@ async fn main() -> Result<()> {
router_cancel.cancel();
});
// Spawn a task that cancels on ctrl-c or SIGTERM.
{
let cancel = cancel.clone();
tokio::spawn(async move {
let mut sigterm = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
).expect("failed to register SIGTERM handler");
tokio::select! {
_ = tokio::signal::ctrl_c() => info!("received SIGINT"),
_ = sigterm.recv() => info!("received SIGTERM"),
}
cancel.cancel();
});
}
// Run TUI or headless.
if cli.log_only {
info!("running in headless mode");
tokio::select! {
() = cancel.cancelled() => {},
result = tokio::signal::ctrl_c() => {
if let Err(e) = result {
tracing::error!("signal handler error: {e}");
}
info!("received ctrl-c, shutting down");
}
}
cancel.cancelled().await;
} else {
let terminal = tui::run(
config_watch_rx,
@@ -210,7 +218,6 @@ async fn main() -> Result<()> {
}
// Shutdown (headless path).
cancel.cancel();
swarm.shutdown().await;
info!("donglora-bridge stopped");