mirror of
https://github.com/ruvnet/RuView
synced 2026-06-17 11:33:19 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dfb4884be |
@@ -98,6 +98,32 @@ jobs:
|
|||||||
echo "Flash image integrity verified"
|
echo "Flash image integrity verified"
|
||||||
fi
|
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
|
- name: Stage release binaries with variant-specific names
|
||||||
working-directory: firmware/esp32-csi-node
|
working-directory: firmware/esp32-csi-node
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.6.2
|
0.6.4
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ pub struct CsiPipelineState {
|
|||||||
pub current_location: Option<(String, f32)>,
|
pub current_location: Option<(String, f32)>,
|
||||||
/// Night mode — true when camera luminance is below threshold
|
/// Night mode — true when camera luminance is below threshold
|
||||||
pub is_dark: bool,
|
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
|
/// Metadata from the on-disk WiFlow JSON, if one is present. NOTE: the
|
||||||
/// weights themselves are NOT loaded or executed in this crate — this
|
/// weights themselves are NOT loaded or executed in this crate — this
|
||||||
/// flag merely enables the amplitude-energy heuristic pose code path.
|
/// flag merely enables the amplitude-energy heuristic pose code path.
|
||||||
@@ -91,6 +94,7 @@ impl Default for CsiPipelineState {
|
|||||||
fingerprints: Vec::new(),
|
fingerprints: Vec::new(),
|
||||||
current_location: None,
|
current_location: None,
|
||||||
is_dark: false,
|
is_dark: false,
|
||||||
|
last_csi_received: None,
|
||||||
pose_model_present: detect_pose_model_metadata(),
|
pose_model_present: detect_pose_model_metadata(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +137,7 @@ impl CsiPipelineState {
|
|||||||
pub fn process_frame(&mut self, frame: CsiFrame) {
|
pub fn process_frame(&mut self, frame: CsiFrame) {
|
||||||
let node_id = frame.node_id;
|
let node_id = frame.node_id;
|
||||||
self.total_frames += 1;
|
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
|
// 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
|
// us honest about the CSI shape we are actually receiving and also
|
||||||
@@ -584,6 +589,9 @@ pub fn get_pipeline_output(state: &Arc<Mutex<CsiPipelineState>>) -> PipelineOutp
|
|||||||
num_nodes: st.node_frames.len(),
|
num_nodes: st.node_frames.len(),
|
||||||
current_location: st.current_location.clone(),
|
current_location: st.current_location.clone(),
|
||||||
is_dark: st.is_dark,
|
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 num_nodes: usize,
|
||||||
pub current_location: Option<(String, f32)>,
|
pub current_location: Option<(String, f32)>,
|
||||||
pub is_dark: bool,
|
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
|
// Serialize implementations
|
||||||
|
|||||||
@@ -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 splats = state.latest_splats.lock().unwrap();
|
||||||
let frames = *state.frame_count.lock().unwrap();
|
let frames = *state.frame_count.lock().unwrap();
|
||||||
let pipeline = state.latest_pipeline.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!({
|
Json(serde_json::json!({
|
||||||
"splats": &*splats,
|
"splats": &*splats,
|
||||||
"count": splats.len(),
|
"count": splats.len(),
|
||||||
"live": state.use_camera,
|
"live": state.use_camera,
|
||||||
|
"csi_live": csi_live,
|
||||||
"frame": frames,
|
"frame": frames,
|
||||||
"pipeline": &*pipeline,
|
"pipeline": &*pipeline,
|
||||||
"timestamp": chrono::Utc::now().timestamp_millis(),
|
"timestamp": chrono::Utc::now().timestamp_millis(),
|
||||||
|
|||||||
@@ -21,6 +21,17 @@
|
|||||||
.face { color: #4cf; }
|
.face { color: #4cf; }
|
||||||
.section { margin-top: 6px; padding-top: 6px; border-top: 1px solid #333; }
|
.section { margin-top: 6px; padding-top: 6px; border-top: 1px solid #333; }
|
||||||
.label { color: #888; }
|
.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>
|
</style>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
<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>
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
||||||
@@ -29,6 +40,10 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.3/camera_utils.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.3/camera_utils.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="no-signal">
|
||||||
|
● NO CSI SIGNAL
|
||||||
|
<div class="sub">No ESP32 frames received for >5s.<br>Check that your node is powered and provisioned.</div>
|
||||||
|
</div>
|
||||||
<div id="info">
|
<div id="info">
|
||||||
<h3 style="margin:0 0 4px 0">RuView · Seldon Vault</h3>
|
<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>
|
<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>
|
||||||
@@ -56,6 +71,11 @@
|
|||||||
var skeletonGroup = null;
|
var skeletonGroup = null;
|
||||||
var prevTimestamp = 0;
|
var prevTimestamp = 0;
|
||||||
var frameRateVal = 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
|
// COCO skeleton connections: pairs of keypoint indices
|
||||||
// 0=nose 1=leftEye 2=rightEye 3=leftEar 4=rightEar
|
// 0=nose 1=leftEye 2=rightEye 3=leftEar 4=rightEar
|
||||||
@@ -579,9 +599,18 @@
|
|||||||
data._faceOverlay = faceOverlay;
|
data._faceOverlay = faceOverlay;
|
||||||
updateSplats(rendered);
|
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;
|
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);
|
drawSkeleton(pipe.skeleton.keypoints);
|
||||||
} else {
|
} else {
|
||||||
clearSkeleton();
|
clearSkeleton();
|
||||||
|
|||||||
Reference in New Issue
Block a user