Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/firmware-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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('<I', 0xABCD5432)
i = data.find(magic)
if i < 0:
sys.exit('app_desc magic not found')
ver = data[i+16:i+48].split(b'\\x00',1)[0].decode('ascii','replace')
print(ver)
" 2>&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: |
Expand Down
2 changes: 1 addition & 1 deletion firmware/esp32-csi-node/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.2
0.6.4
12 changes: 12 additions & 0 deletions v2/crates/wifi-densepose-pointcloud/src/csi_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::time::Instant>,
/// 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.
Expand All @@ -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(),
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -584,6 +589,9 @@ pub fn get_pipeline_output(state: &Arc<Mutex<CsiPipelineState>>) -> 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),
}
}

Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions v2/crates/wifi-densepose-pointcloud/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,12 @@ async fn api_splats(State(state): State<Arc<AppState>>) -> Json<serde_json::Valu
let splats = state.latest_splats.lock().unwrap();
let frames = *state.frame_count.lock().unwrap();
let pipeline = state.latest_pipeline.lock().unwrap();
let csi_live = pipeline.as_ref().map(|p| p.csi_live).unwrap_or(false);
Json(serde_json::json!({
"splats": &*splats,
"count": splats.len(),
"live": state.use_camera,
"csi_live": csi_live,
"frame": frames,
"pipeline": &*pipeline,
"timestamp": chrono::Utc::now().timestamp_millis(),
Expand Down
33 changes: 31 additions & 2 deletions v2/crates/wifi-densepose-pointcloud/src/viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@
.face { color: #4cf; }
.section { margin-top: 6px; padding-top: 6px; border-top: 1px solid #333; }
.label { color: #888; }
#no-signal {
display: none;
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: rgba(160,0,0,0.93); color: #fff;
font-family: monospace; font-size: 18px; font-weight: bold;
padding: 18px 32px; border-radius: 8px;
border: 2px solid #f44; text-align: center;
pointer-events: none; z-index: 20;
}
#no-signal .sub { font-size: 12px; font-weight: normal; margin-top: 6px; color: #fbb; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
Expand All @@ -29,6 +40,10 @@
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.3/camera_utils.js"></script>
</head>
<body>
<div id="no-signal">
&#x25CF; NO CSI SIGNAL
<div class="sub">No ESP32 frames received for &gt;5s.<br>Check that your node is powered and provisioned.</div>
</div>
<div id="info">
<h3 style="margin:0 0 4px 0">RuView · Seldon Vault</h3>
<div style="font-size: 11px; color: #888; margin-bottom: 8px; max-width: 240px; line-height: 1.4; font-style: italic;">"Psychohistory deals with reactions of human conglomerates to fixed social and economic stimuli." — Hari Seldon</div>
Expand Down Expand Up @@ -56,6 +71,11 @@ <h3 style="margin:0 0 4px 0">RuView · Seldon Vault</h3>
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
Expand Down Expand Up @@ -579,9 +599,18 @@ <h3 style="margin:0 0 4px 0">RuView · Seldon Vault</h3>
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();
Expand Down
Loading