Skip to content

Commit 6638796

Browse files
Reuvenruvnet
andcommitted
fix(sensing-server): detect ESP32 offline after 5s frame timeout
The source field was set to "esp32" on the first UDP frame but never reverted when frames stopped arriving. This caused the UI to show "Real hardware connected" indefinitely after powering off all nodes. Changes: - Add last_esp32_frame timestamp to AppStateInner - Add effective_source() method with 5-second timeout - Source becomes "esp32:offline" when no frames received within 5s - Health endpoint shows "degraded" instead of "healthy" when offline - All 6 status/health/info API endpoints use effective_source() Fixes #297 Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent 6c98c98 commit 6638796

File tree

1 file changed

+35
-9
lines changed
  • rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src

1 file changed

+35
-9
lines changed

rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/main.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ struct AppStateInner {
285285
frame_history: VecDeque<Vec<f64>>,
286286
tick: u64,
287287
source: String,
288+
/// Instant of the last ESP32 UDP frame received (for offline detection).
289+
last_esp32_frame: Option<std::time::Instant>,
288290
tx: broadcast::Sender<String>,
289291
total_detections: u64,
290292
start_time: std::time::Instant,
@@ -364,6 +366,25 @@ struct AppStateInner {
364366
adaptive_model: Option<adaptive_classifier::AdaptiveModel>,
365367
}
366368

369+
/// If no ESP32 frame arrives within this duration, source reverts to offline.
370+
const ESP32_OFFLINE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
371+
372+
impl AppStateInner {
373+
/// Return the effective data source, accounting for ESP32 frame timeout.
374+
/// If the source is "esp32" but no frame has arrived in 5 seconds, returns
375+
/// "esp32:offline" so the UI can distinguish active vs stale connections.
376+
fn effective_source(&self) -> String {
377+
if self.source == "esp32" {
378+
if let Some(last) = self.last_esp32_frame {
379+
if last.elapsed() > ESP32_OFFLINE_TIMEOUT {
380+
return "esp32:offline".to_string();
381+
}
382+
}
383+
}
384+
self.source.clone()
385+
}
386+
}
387+
367388
/// Number of frames retained in `frame_history` for temporal analysis.
368389
/// At 500 ms ticks this covers ~50 seconds; at 100 ms ticks ~10 seconds.
369390
const FRAME_HISTORY_CAPACITY: usize = 100;
@@ -1669,7 +1690,7 @@ async fn health(State(state): State<SharedState>) -> Json<serde_json::Value> {
16691690
let s = state.read().await;
16701691
Json(serde_json::json!({
16711692
"status": "ok",
1672-
"source": s.source,
1693+
"source": s.effective_source(),
16731694
"tick": s.tick,
16741695
"clients": s.tx.receiver_count(),
16751696
}))
@@ -1977,7 +1998,7 @@ async fn health_ready(State(state): State<SharedState>) -> Json<serde_json::Valu
19771998
let s = state.read().await;
19781999
Json(serde_json::json!({
19792000
"status": "ready",
1980-
"source": s.source,
2001+
"source": s.effective_source(),
19812002
}))
19822003
}
19832004

@@ -1988,7 +2009,10 @@ async fn health_system(State(state): State<SharedState>) -> Json<serde_json::Val
19882009
"status": "healthy",
19892010
"components": {
19902011
"api": { "status": "healthy", "message": "Rust Axum server" },
1991-
"hardware": { "status": "healthy", "message": format!("Source: {}", s.source) },
2012+
"hardware": {
2013+
"status": if s.effective_source().ends_with(":offline") { "degraded" } else { "healthy" },
2014+
"message": format!("Source: {}", s.effective_source())
2015+
},
19922016
"pose": { "status": "healthy", "message": "WiFi-derived pose estimation" },
19932017
"stream": { "status": if s.tx.receiver_count() > 0 { "healthy" } else { "idle" },
19942018
"message": format!("{} client(s)", s.tx.receiver_count()) },
@@ -2028,7 +2052,7 @@ async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> {
20282052
"version": env!("CARGO_PKG_VERSION"),
20292053
"environment": "production",
20302054
"backend": "rust",
2031-
"source": s.source,
2055+
"source": s.effective_source(),
20322056
"features": {
20332057
"wifi_sensing": true,
20342058
"pose_estimation": true,
@@ -2049,7 +2073,7 @@ async fn pose_current(State(state): State<SharedState>) -> Json<serde_json::Valu
20492073
"timestamp": chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
20502074
"persons": persons,
20512075
"total_persons": persons.len(),
2052-
"source": s.source,
2076+
"source": s.effective_source(),
20532077
}))
20542078
}
20552079

@@ -2059,7 +2083,7 @@ async fn pose_stats(State(state): State<SharedState>) -> Json<serde_json::Value>
20592083
"total_detections": s.total_detections,
20602084
"average_confidence": 0.87,
20612085
"frames_processed": s.tick,
2062-
"source": s.source,
2086+
"source": s.effective_source(),
20632087
}))
20642088
}
20652089

@@ -2083,7 +2107,7 @@ async fn stream_status(State(state): State<SharedState>) -> Json<serde_json::Val
20832107
"active": true,
20842108
"clients": s.tx.receiver_count(),
20852109
"fps": if s.tick > 1 { 10u64 } else { 0u64 },
2086-
"source": s.source,
2110+
"source": s.effective_source(),
20872111
}))
20882112
}
20892113

@@ -2619,7 +2643,7 @@ async fn vital_signs_endpoint(State(state): State<SharedState>) -> Json<serde_js
26192643
"heartbeat_samples": hb_len,
26202644
"heartbeat_capacity": hb_cap,
26212645
},
2622-
"source": s.source,
2646+
"source": s.effective_source(),
26232647
"tick": s.tick,
26242648
}))
26252649
}
@@ -2825,6 +2849,7 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
28252849

28262850
let mut s = state.write().await;
28272851
s.source = "esp32".to_string();
2852+
s.last_esp32_frame = Some(std::time::Instant::now());
28282853

28292854
// Append current amplitudes to history before extracting features so
28302855
// that temporal analysis includes the most recent frame.
@@ -3607,6 +3632,7 @@ async fn main() {
36073632
frame_history: VecDeque::new(),
36083633
tick: 0,
36093634
source: source.into(),
3635+
last_esp32_frame: None,
36103636
tx,
36113637
total_detections: 0,
36123638
start_time: std::time::Instant::now(),
@@ -3781,7 +3807,7 @@ async fn main() {
37813807
"WiFi DensePose sensing model state",
37823808
);
37833809
builder.add_metadata(&serde_json::json!({
3784-
"source": s.source,
3810+
"source": s.effective_source(),
37853811
"total_ticks": s.tick,
37863812
"total_detections": s.total_detections,
37873813
"uptime_secs": s.start_time.elapsed().as_secs(),

0 commit comments

Comments
 (0)