diff --git a/.github/workflows/firmware-ci.yml b/.github/workflows/firmware-ci.yml index 252a47ee8..0a11ae8c2 100644 --- a/.github/workflows/firmware-ci.yml +++ b/.github/workflows/firmware-ci.yml @@ -98,6 +98,32 @@ jobs: echo "Flash image integrity verified" fi + - name: Verify embedded version string matches version.txt (fixes #505) + working-directory: firmware/esp32-csi-node + run: | + EXPECTED=$(cat version.txt | tr -d '[:space:]') + BIN=build/esp32-csi-node.bin + # Extract version from ESP-IDF app_desc: magic 0xABCD5432 at offset 0 + # followed by version string at offset 16, null-terminated, max 32 chars. + EMBEDDED=$(python3 -c " +import struct, sys +data = open('$BIN','rb').read() +magic = struct.pack('&1) + echo "Expected version: $EXPECTED" + echo "Embedded version: $EMBEDDED" + if [ "$EMBEDDED" != "$EXPECTED" ]; then + echo "::error::Version string mismatch! version.txt='$EXPECTED' but binary reports '$EMBEDDED'." + echo "::error::Ensure version.txt is updated before building and tagging." + exit 1 + fi + echo "Version string verified: $EMBEDDED" + - name: Stage release binaries with variant-specific names working-directory: firmware/esp32-csi-node run: | diff --git a/firmware/esp32-csi-node/version.txt b/firmware/esp32-csi-node/version.txt index b61604874..d2b13eb64 100644 --- a/firmware/esp32-csi-node/version.txt +++ b/firmware/esp32-csi-node/version.txt @@ -1 +1 @@ -0.6.2 +0.6.4 diff --git a/v2/crates/wifi-densepose-pointcloud/src/csi_pipeline.rs b/v2/crates/wifi-densepose-pointcloud/src/csi_pipeline.rs index 966f48d14..b1fe4df4f 100644 --- a/v2/crates/wifi-densepose-pointcloud/src/csi_pipeline.rs +++ b/v2/crates/wifi-densepose-pointcloud/src/csi_pipeline.rs @@ -65,6 +65,9 @@ pub struct CsiPipelineState { pub current_location: Option<(String, f32)>, /// Night mode — true when camera luminance is below threshold pub is_dark: bool, + /// Wall-clock instant the last real ESP32 UDP CSI frame was received. + /// `None` if no frame has arrived since startup. + pub last_csi_received: Option, /// Metadata from the on-disk WiFlow JSON, if one is present. NOTE: the /// weights themselves are NOT loaded or executed in this crate — this /// flag merely enables the amplitude-energy heuristic pose code path. @@ -91,6 +94,7 @@ impl Default for CsiPipelineState { fingerprints: Vec::new(), current_location: None, is_dark: false, + last_csi_received: None, pose_model_present: detect_pose_model_metadata(), } } @@ -133,6 +137,7 @@ impl CsiPipelineState { pub fn process_frame(&mut self, frame: CsiFrame) { let node_id = frame.node_id; self.total_frames += 1; + self.last_csi_received = Some(std::time::Instant::now()); // Once every 500 frames log a one-line node stats summary. This keeps // us honest about the CSI shape we are actually receiving and also @@ -584,6 +589,9 @@ pub fn get_pipeline_output(state: &Arc>) -> PipelineOutp num_nodes: st.node_frames.len(), current_location: st.current_location.clone(), is_dark: st.is_dark, + csi_live: st.last_csi_received + .map(|t| t.elapsed() < std::time::Duration::from_secs(5)) + .unwrap_or(false), } } @@ -598,6 +606,10 @@ pub struct PipelineOutput { pub num_nodes: usize, pub current_location: Option<(String, f32)>, pub is_dark: bool, + /// True when a real ESP32 CSI frame was received in the last 5 seconds. + /// False means the pipeline is running on stale data — show a NO SIGNAL + /// indicator in the UI rather than presenting stale skeletons as live. + pub csi_live: bool, } // Serialize implementations diff --git a/v2/crates/wifi-densepose-pointcloud/src/stream.rs b/v2/crates/wifi-densepose-pointcloud/src/stream.rs index 808f62314..fb00db815 100644 --- a/v2/crates/wifi-densepose-pointcloud/src/stream.rs +++ b/v2/crates/wifi-densepose-pointcloud/src/stream.rs @@ -219,10 +219,12 @@ async fn api_splats(State(state): State>) -> Json @@ -29,6 +40,10 @@ +
+ ● NO CSI SIGNAL +
No ESP32 frames received for >5s.
Check that your node is powered and provisioned.
+

RuView · Seldon Vault

"Psychohistory deals with reactions of human conglomerates to fixed social and economic stimuli." — Hari Seldon
@@ -56,6 +71,11 @@

RuView · Seldon Vault

var skeletonGroup = null; var prevTimestamp = 0; var frameRateVal = 0; + // No-signal detection: track server-reported csi_live flag + var noSignalBanner = document.getElementById("no-signal"); + function setNoSignal(isNoSignal) { + noSignalBanner.style.display = isNoSignal ? "block" : "none"; + } // COCO skeleton connections: pairs of keypoint indices // 0=nose 1=leftEye 2=rightEye 3=leftEar 4=rightEar @@ -579,9 +599,18 @@

RuView · Seldon Vault

data._faceOverlay = faceOverlay; updateSplats(rendered); - // Draw skeleton if available + // No-signal detection: hide skeleton and show banner when + // the server reports no live CSI frames in the last 5s. var pipe = data.pipeline; - if (pipe && pipe.skeleton && pipe.skeleton.keypoints) { + var csiLive = data.csi_live || (pipe && pipe.csi_live); + // Only show no-signal when connected to a real backend + // (not demo/face-mesh mode where csi_live is always false). + var showNoSignal = (transportMode === "live" || transportMode === "remote") + && csiLive === false; + setNoSignal(showNoSignal); + if (showNoSignal) { + clearSkeleton(); + } else if (pipe && pipe.skeleton && pipe.skeleton.keypoints) { drawSkeleton(pipe.skeleton.keypoints); } else { clearSkeleton();