fix(sensing-server): WS broadcast emits effective_source() not hardcoded "esp32" (closes #618) (#621)

Reported by @ArnonEnbar with a complete reproduction.

broadcast_tick_task() re-emits the cached `latest_update` every tick so
pose WS clients keep getting data even when ESP32 pauses between
frames. The `source` field of that cached update was set to "esp32" at
the moment a fresh ESP32 frame was last decoded (main.rs:3885, :4136).

After the ESP32 loses power or network, no fresh frame is decoded —
the cached `latest_update` is still re-broadcast every tick with the
stale source: "esp32" baked in. UI's "Sensing" tab keeps showing
"LIVE — ESP32 HARDWARE Connected" with frozen vitals/features/
classification re-broadcast indefinitely. REST `/health` correctly
reports source: "esp32:offline" (via effective_source(), which checks
last_esp32_frame elapsed time against ESP32_OFFLINE_TIMEOUT=5s) — but
the WS broadcast path was the one consumer that didn't call it.

Fix: clone the cached update per tick, overwrite source with
s.effective_source(), then serialize and broadcast. UI now switches to
"esp32:offline" on the same 5s budget as the REST surface.

cargo build -p wifi-densepose-sensing-server --no-default-features:
17s, no errors (1 pre-existing unused-import warning unchanged).
This commit is contained in:
rUv
2026-05-18 08:18:18 -04:00
committed by GitHub
parent 72bbd256e7
commit b2e2e6d6fd
2 changed files with 13 additions and 1 deletions
@@ -4331,7 +4331,18 @@ async fn broadcast_tick_task(state: SharedState, tick_ms: u64) {
if s.tx.receiver_count() > 0 {
// Re-broadcast the latest sensing_update so pose WS clients
// always get data even when ESP32 pauses between frames.
if let Ok(json) = serde_json::to_string(update) {
//
// Issue #618: overwrite `source` with `effective_source()`
// before each broadcast so a stale latest_update (frozen
// payload from a now-offline ESP32) is emitted with
// `source: "esp32:offline"` instead of `source: "esp32"`.
// The REST `/health` endpoint already does this; before
// this fix the WS path was the only consumer that didn't,
// so the UI's "LIVE — ESP32 HARDWARE Connected" banner
// stayed green long after the hardware went away.
let mut tagged = update.clone();
tagged.source = s.effective_source();
if let Ok(json) = serde_json::to_string(&tagged) {
let _ = s.tx.send(json);
}
}