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
+30
-36
@@ -18,12 +18,14 @@ pub struct RadioConfigInfo {
|
||||
pub requested: donglora_client::RadioConfig,
|
||||
pub source: ConfigSource,
|
||||
pub device: String,
|
||||
pub connected: bool,
|
||||
}
|
||||
|
||||
/// Packet event for the TUI log.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PacketLogEntry {
|
||||
pub timestamp: Instant,
|
||||
pub hash: [u8; 32],
|
||||
pub direction: PacketDirection,
|
||||
pub size: usize,
|
||||
pub snr: Option<i16>,
|
||||
@@ -31,37 +33,32 @@ pub struct PacketLogEntry {
|
||||
pub action: PacketAction,
|
||||
}
|
||||
|
||||
/// Which side the packet arrived from.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PacketDirection {
|
||||
RadioRx,
|
||||
RadioTx,
|
||||
GossipRx,
|
||||
/// Heard on local radio → forwarded to gossip swarm.
|
||||
RadioIn,
|
||||
/// Received from gossip swarm → queued for radio TX.
|
||||
GossipIn,
|
||||
}
|
||||
|
||||
/// What happened to the packet.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PacketAction {
|
||||
Forwarded,
|
||||
Transmitted,
|
||||
/// Successfully bridged (radio→gossip or gossip→radio).
|
||||
Bridged,
|
||||
/// Dropped: duplicate payload.
|
||||
DroppedDedup,
|
||||
/// Dropped: TX queue full.
|
||||
DroppedQueueFull,
|
||||
/// Dropped: rate limiter.
|
||||
DroppedRateLimit,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PacketDirection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::RadioRx => write!(f, "RX "),
|
||||
Self::RadioTx => write!(f, "TX "),
|
||||
Self::GossipRx => write!(f, "GSP"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PacketAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Forwarded => write!(f, "FWD"),
|
||||
Self::Transmitted => write!(f, "TX"),
|
||||
Self::Bridged => write!(f, ""),
|
||||
Self::DroppedDedup => write!(f, "DUP"),
|
||||
Self::DroppedQueueFull => write!(f, "FULL"),
|
||||
Self::DroppedRateLimit => write!(f, "RATE"),
|
||||
@@ -188,7 +185,8 @@ pub async fn run(
|
||||
debug!("radio RX dedup suppressed ({} bytes)", pkt.payload.len());
|
||||
let _ = log_tx.send(PacketLogEntry {
|
||||
timestamp: Instant::now(),
|
||||
direction: PacketDirection::RadioRx,
|
||||
hash,
|
||||
direction: PacketDirection::RadioIn,
|
||||
size: pkt.payload.len(),
|
||||
snr: Some(pkt.snr),
|
||||
rssi: Some(pkt.rssi),
|
||||
@@ -204,11 +202,12 @@ pub async fn run(
|
||||
|
||||
let _ = log_tx.send(PacketLogEntry {
|
||||
timestamp: Instant::now(),
|
||||
direction: PacketDirection::RadioRx,
|
||||
hash,
|
||||
direction: PacketDirection::RadioIn,
|
||||
size: pkt.payload.len(),
|
||||
snr: Some(pkt.snr),
|
||||
rssi: Some(pkt.rssi),
|
||||
action: PacketAction::Forwarded,
|
||||
action: PacketAction::Bridged,
|
||||
}).await;
|
||||
}
|
||||
RadioEvent::Connected(active_config, source, device) => {
|
||||
@@ -218,10 +217,12 @@ pub async fn run(
|
||||
requested: requested_radio_config,
|
||||
source,
|
||||
device,
|
||||
connected: true,
|
||||
});
|
||||
}
|
||||
RadioEvent::Disconnected => {
|
||||
stats.radio_connected.store(0, Ordering::Relaxed);
|
||||
config_tx.send_modify(|info| info.connected = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +240,8 @@ pub async fn run(
|
||||
debug!("gossip RX dedup suppressed ({} bytes)", frame.payload.len());
|
||||
let _ = log_tx.send(PacketLogEntry {
|
||||
timestamp: Instant::now(),
|
||||
direction: PacketDirection::GossipRx,
|
||||
hash,
|
||||
direction: PacketDirection::GossipIn,
|
||||
size: frame.payload.len(),
|
||||
snr: Some(frame.snr),
|
||||
rssi: Some(frame.rssi),
|
||||
@@ -254,7 +256,8 @@ pub async fn run(
|
||||
debug!("rate limited ({} bytes)", frame.payload.len());
|
||||
let _ = log_tx.send(PacketLogEntry {
|
||||
timestamp: Instant::now(),
|
||||
direction: PacketDirection::GossipRx,
|
||||
hash,
|
||||
direction: PacketDirection::GossipIn,
|
||||
size: frame.payload.len(),
|
||||
snr: Some(frame.snr),
|
||||
rssi: Some(frame.rssi),
|
||||
@@ -269,7 +272,8 @@ pub async fn run(
|
||||
warn!("TX queue full, dropping packet ({} bytes)", frame.payload.len());
|
||||
let _ = log_tx.send(PacketLogEntry {
|
||||
timestamp: Instant::now(),
|
||||
direction: PacketDirection::GossipRx,
|
||||
hash,
|
||||
direction: PacketDirection::GossipIn,
|
||||
size: frame.payload.len(),
|
||||
snr: Some(frame.snr),
|
||||
rssi: Some(frame.rssi),
|
||||
@@ -281,11 +285,12 @@ pub async fn run(
|
||||
tx_queue.push_back(frame.payload.clone());
|
||||
let _ = log_tx.send(PacketLogEntry {
|
||||
timestamp: Instant::now(),
|
||||
direction: PacketDirection::GossipRx,
|
||||
hash,
|
||||
direction: PacketDirection::GossipIn,
|
||||
size: frame.payload.len(),
|
||||
snr: Some(frame.snr),
|
||||
rssi: Some(frame.rssi),
|
||||
action: PacketAction::Transmitted,
|
||||
action: PacketAction::Bridged,
|
||||
}).await;
|
||||
}
|
||||
GossipEvent::NeighborChanged(count) => {
|
||||
@@ -297,18 +302,7 @@ pub async fn run(
|
||||
|
||||
// Drain TX queue to radio.
|
||||
while let Some(payload) = tx_queue.pop_front() {
|
||||
let size = payload.len();
|
||||
stats.radio_tx.fetch_add(1, Ordering::Relaxed);
|
||||
let _ = log_tx
|
||||
.send(PacketLogEntry {
|
||||
timestamp: Instant::now(),
|
||||
direction: PacketDirection::RadioTx,
|
||||
size,
|
||||
snr: None,
|
||||
rssi: None,
|
||||
action: PacketAction::Transmitted,
|
||||
})
|
||||
.await;
|
||||
if radio_tx.send(payload).await.is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user