diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/main.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/main.rs index 7c074bf84..4bd84c066 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/main.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/main.rs @@ -285,6 +285,8 @@ struct AppStateInner { frame_history: VecDeque>, tick: u64, source: String, + /// Instant of the last ESP32 UDP frame received (for offline detection). + last_esp32_frame: Option, tx: broadcast::Sender, total_detections: u64, start_time: std::time::Instant, @@ -364,6 +366,25 @@ struct AppStateInner { adaptive_model: Option, } +/// If no ESP32 frame arrives within this duration, source reverts to offline. +const ESP32_OFFLINE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); + +impl AppStateInner { + /// Return the effective data source, accounting for ESP32 frame timeout. + /// If the source is "esp32" but no frame has arrived in 5 seconds, returns + /// "esp32:offline" so the UI can distinguish active vs stale connections. + fn effective_source(&self) -> String { + if self.source == "esp32" { + if let Some(last) = self.last_esp32_frame { + if last.elapsed() > ESP32_OFFLINE_TIMEOUT { + return "esp32:offline".to_string(); + } + } + } + self.source.clone() + } +} + /// Number of frames retained in `frame_history` for temporal analysis. /// At 500 ms ticks this covers ~50 seconds; at 100 ms ticks ~10 seconds. const FRAME_HISTORY_CAPACITY: usize = 100; @@ -1669,7 +1690,7 @@ async fn health(State(state): State) -> Json { let s = state.read().await; Json(serde_json::json!({ "status": "ok", - "source": s.source, + "source": s.effective_source(), "tick": s.tick, "clients": s.tx.receiver_count(), })) @@ -1977,7 +1998,7 @@ async fn health_ready(State(state): State) -> Json) -> Json 0 { "healthy" } else { "idle" }, "message": format!("{} client(s)", s.tx.receiver_count()) }, @@ -2028,7 +2052,7 @@ async fn api_info(State(state): State) -> Json { "version": env!("CARGO_PKG_VERSION"), "environment": "production", "backend": "rust", - "source": s.source, + "source": s.effective_source(), "features": { "wifi_sensing": true, "pose_estimation": true, @@ -2049,7 +2073,7 @@ async fn pose_current(State(state): State) -> Json) -> Json "total_detections": s.total_detections, "average_confidence": 0.87, "frames_processed": s.tick, - "source": s.source, + "source": s.effective_source(), })) } @@ -2083,7 +2107,7 @@ async fn stream_status(State(state): State) -> Json 1 { 10u64 } else { 0u64 }, - "source": s.source, + "source": s.effective_source(), })) } @@ -2619,7 +2643,7 @@ async fn vital_signs_endpoint(State(state): State) -> Json