Skip to content

Commit 40f1962

Browse files
authored
fix(firmware,server): watchdog crash + no detection from edge vitals (#321, #323)
* fix(firmware,server): watchdog crash on busy LANs + no detection from edge vitals (#321, #323) **Firmware (#321):** edge_dsp task now batch-limits frame processing to 4 frames before a 10ms yield. On corporate LANs with high CSI frame rates, the previous 1-tick-per-frame yield wasn't enough to prevent IDLE1 starvation and task watchdog triggers. **Sensing server (#323):** When ESP32 runs the edge DSP pipeline (Tier 2+), it sends vitals packets (magic 0xC5110002) instead of raw CSI frames. Previously, the server broadcast these as raw edge_vitals but never generated a sensing_update, so the UI showed "connected" but "0 persons". Now synthesizes a full sensing_update from vitals data including classification, person count, and pose generation. Closes #321 Closes #323 Co-Authored-By: claude-flow <ruv@ruv.net> * fix(firmware): address review findings — idle busy-spin and observability - Fix pdMS_TO_TICKS(5)==0 at 100Hz causing busy-spin in idle path (use vTaskDelay(1) instead) - Post-batch yield now 2 ticks (20ms) for genuinely longer pause - Add s_ring_drops counter to ring_push for diagnosing frame drops - Expose drop count in periodic vitals log line Co-Authored-By: claude-flow <ruv@ruv.net> * fix(server): set breathing_band_power for skeleton animation from vitals When presence is detected via edge vitals, set breathing_band_power to 0.5 so the UI's torso breathing animation works. Previously hardcoded to 0.0 which made the skeleton appear static even when breathing rate was being reported. Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent 022499b commit 40f1962

File tree

2 files changed

+111
-10
lines changed
  • firmware/esp32-csi-node/main
  • rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src

2 files changed

+111
-10
lines changed

firmware/esp32-csi-node/main/edge_processing.c

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ static const char *TAG = "edge_proc";
4141
* ====================================================================== */
4242

4343
static edge_ring_buf_t s_ring;
44+
static uint32_t s_ring_drops; /* Frames dropped due to full ring buffer. */
4445

4546
static inline bool ring_push(const uint8_t *iq, uint16_t len,
4647
int8_t rssi, uint8_t channel)
4748
{
4849
uint32_t next = (s_ring.head + 1) % EDGE_RING_SLOTS;
4950
if (next == s_ring.tail) {
51+
s_ring_drops++;
5052
return false; /* Full — drop frame. */
5153
}
5254

@@ -788,12 +790,13 @@ static void process_frame(const edge_ring_slot_t *slot)
788790

789791
if ((s_frame_count % 200) == 0) {
790792
ESP_LOGI(TAG, "Vitals: br=%.1f hr=%.1f motion=%.4f pres=%s "
791-
"fall=%s persons=%u frames=%lu",
793+
"fall=%s persons=%u frames=%lu drops=%lu",
792794
s_breathing_bpm, s_heartrate_bpm, s_motion_energy,
793795
s_presence_detected ? "YES" : "no",
794796
s_fall_detected ? "YES" : "no",
795797
(unsigned)s_latest_pkt.n_persons,
796-
(unsigned long)s_frame_count);
798+
(unsigned long)s_frame_count,
799+
(unsigned long)s_ring_drops);
797800
}
798801
}
799802

@@ -831,18 +834,32 @@ static void edge_task(void *arg)
831834

832835
edge_ring_slot_t slot;
833836

837+
/* Maximum frames to process before a longer yield. On busy LANs
838+
* (corporate networks, many APs), the ring buffer fills continuously.
839+
* Without a batch limit the task processes frames back-to-back with
840+
* only 1-tick yields, which on high frame rates can still starve
841+
* IDLE1 enough to trip the 5-second task watchdog. See #266, #321. */
842+
const uint8_t BATCH_LIMIT = 4;
843+
834844
while (1) {
835-
if (ring_pop(&slot)) {
845+
uint8_t processed = 0;
846+
847+
while (processed < BATCH_LIMIT && ring_pop(&slot)) {
836848
process_frame(&slot);
837-
/* Yield after every frame to feed the Core 1 watchdog.
838-
* process_frame() is CPU-intensive (biquad filters, Welford stats,
839-
* BPM estimation, multi-person vitals) and can take several ms.
840-
* Without this yield, edge_dsp at priority 5 starves IDLE1 at
841-
* priority 0, triggering the task watchdog. See issue #266. */
849+
processed++;
850+
/* 1-tick yield between frames within a batch. */
842851
vTaskDelay(1);
852+
}
853+
854+
if (processed > 0) {
855+
/* Post-batch yield: 2 ticks (~20 ms at 100 Hz) so IDLE1 can
856+
* run and feed the Core 1 watchdog even under sustained load.
857+
* This is intentionally longer than the 1-tick inter-frame yield. */
858+
vTaskDelay(2);
843859
} else {
844-
/* No frames available — yield briefly. */
845-
vTaskDelay(pdMS_TO_TICKS(1));
860+
/* No frames available — sleep one full tick.
861+
* NOTE: pdMS_TO_TICKS(5) == 0 at 100 Hz, which would busy-spin. */
862+
vTaskDelay(1);
846863
}
847864
}
848865
}

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2820,6 +2820,90 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
28202820
})) {
28212821
let _ = s.tx.send(json);
28222822
}
2823+
2824+
// Issue #323: Also emit a sensing_update so the UI renders
2825+
// detections for ESP32 nodes running the edge DSP pipeline
2826+
// (Tier 2+). Without this, vitals arrive but the UI shows
2827+
// "no detection" because it only renders sensing_update msgs.
2828+
s.source = "esp32".to_string();
2829+
s.last_esp32_frame = Some(std::time::Instant::now());
2830+
s.tick += 1;
2831+
let tick = s.tick;
2832+
2833+
let motion_level = if vitals.motion { "present_moving" }
2834+
else if vitals.presence { "present_still" }
2835+
else { "absent" };
2836+
let motion_score = if vitals.motion { 0.8 }
2837+
else if vitals.presence { 0.3 }
2838+
else { 0.05 };
2839+
let est_persons = if vitals.presence {
2840+
(vitals.n_persons as usize).max(1)
2841+
} else {
2842+
0
2843+
};
2844+
2845+
let features = FeatureInfo {
2846+
mean_rssi: vitals.rssi as f64,
2847+
variance: vitals.motion_energy as f64,
2848+
motion_band_power: vitals.motion_energy as f64,
2849+
breathing_band_power: if vitals.presence { 0.5 } else { 0.0 },
2850+
dominant_freq_hz: vitals.breathing_rate_bpm / 60.0,
2851+
change_points: 0,
2852+
spectral_power: vitals.motion_energy as f64,
2853+
};
2854+
let classification = ClassificationInfo {
2855+
motion_level: motion_level.to_string(),
2856+
presence: vitals.presence,
2857+
confidence: vitals.presence_score as f64,
2858+
};
2859+
let signal_field = generate_signal_field(
2860+
vitals.rssi as f64, motion_score, vitals.breathing_rate_bpm / 60.0,
2861+
(vitals.presence_score as f64).min(1.0), &[],
2862+
);
2863+
2864+
let mut update = SensingUpdate {
2865+
msg_type: "sensing_update".to_string(),
2866+
timestamp: chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
2867+
source: "esp32".to_string(),
2868+
tick,
2869+
nodes: vec![NodeInfo {
2870+
node_id: vitals.node_id,
2871+
rssi_dbm: vitals.rssi as f64,
2872+
position: [2.0, 0.0, 1.5],
2873+
amplitude: vec![],
2874+
subcarrier_count: 0,
2875+
}],
2876+
features: features.clone(),
2877+
classification,
2878+
signal_field,
2879+
vital_signs: Some(VitalSigns {
2880+
breathing_rate_bpm: if vitals.breathing_rate_bpm > 0.0 { Some(vitals.breathing_rate_bpm) } else { None },
2881+
heart_rate_bpm: if vitals.heartrate_bpm > 0.0 { Some(vitals.heartrate_bpm) } else { None },
2882+
breathing_confidence: if vitals.presence { 0.7 } else { 0.0 },
2883+
heartbeat_confidence: if vitals.presence { 0.7 } else { 0.0 },
2884+
signal_quality: vitals.presence_score as f64,
2885+
}),
2886+
enhanced_motion: None,
2887+
enhanced_breathing: None,
2888+
posture: None,
2889+
signal_quality_score: None,
2890+
quality_verdict: None,
2891+
bssid_count: None,
2892+
pose_keypoints: None,
2893+
model_status: None,
2894+
persons: None,
2895+
estimated_persons: if est_persons > 0 { Some(est_persons) } else { None },
2896+
};
2897+
2898+
let persons = derive_pose_from_sensing(&update);
2899+
if !persons.is_empty() {
2900+
update.persons = Some(persons);
2901+
}
2902+
2903+
if let Ok(json) = serde_json::to_string(&update) {
2904+
let _ = s.tx.send(json);
2905+
}
2906+
s.latest_update = Some(update);
28232907
s.edge_vitals = Some(vitals);
28242908
continue;
28252909
}

0 commit comments

Comments
 (0)