From cc0f443fccbf9e798f149fffd29f6bf0bfc55007 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 14 Sep 2026 17:21:29 -0400 Subject: [PATCH] feat(firmware): count rejected edge frames, and populate the two log fields that were always zero `node_log_periodic_t` has declared `frames_processed` and `frames_rejected` since the on-node log shipped, and `node_log_read.py` has always rendered both. Nothing ever assigned them. `fill_health()` memsets the struct and fills 20 of its 25 fields; these two are not among them, so every periodic record on every node reports 0/0 -- the memset, not a measurement. That is the most misleading pair of values this record can carry. `EDGE_MAX_SUBCARRIERS` is target-conditional: `edge_processing.h` picks 256 on `CONFIG_SOC_WIFI_HE_SUPPORT` and 128 otherwise, and `process_frame()` opens with `n_subcarriers > EDGE_MAX_SUBCARRIERS -> return`. A C6 that resolves the guard to 128 therefore rejects every HE20 frame while the Edge DSP task still starts and logs its banner. From outside, an edge stage that is idle and one that is rejecting 100% of frames look identical -- and the log whose entire purpose is diagnosing a node you cannot reach answered that question with a plausible constant zero. Half the data was already here: `s_frame_count` exists and is incremented on the accept path. Only the reject path was uncounted, so this adds `s_frame_rejected` next to it, increments it in the guard instead of returning silently, resets it alongside `s_frame_count` in init, exposes both through accessors, and wires them into `fill_health()`. Measured, not argued. Found by reading a real node's log: `frames_processed=0` across 64 records at 5.5 days uptime with CSI flowing at 19 fps, which reads as a dead edge pipeline and was very nearly reported as one. After this change, rolled across nine ESP32-C6 nodes: ``` node fps processed rejected heap rssi 0 18.0 2007 0 188K -67 1 23.0 1527 0 187K -70 2 32.0 1538 0 188K -51 3 27.0 3447 0 187K -66 4 26.0 1045 0 187K -60 5 23.0 565 0 188K -72 6 29.0 571 0 188K -72 7 27.0 87 0 184K -75 8 30.0 90 0 188K -67 ``` Non-zero processed with zero rejected on all nine is also the first runtime evidence that the guard is sized correctly on C6. At 128 the inequality inverts: every frame rejected, none processed. That was previously arguable only from include order. Builds clean for esp32c6 on this base; both accessors are present in the linked ELF. The only `main/` diagnostic is the pre-existing unused-variable warning in `mmwave_sensor.c:246`. Co-Authored-By: claude-flow --- .../esp32-csi-node/main/edge_processing.c | 24 ++++++++++++++++++- .../esp32-csi-node/main/edge_processing.h | 17 +++++++++++++ firmware/esp32-csi-node/main/node_log.c | 9 +++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/firmware/esp32-csi-node/main/edge_processing.c b/firmware/esp32-csi-node/main/edge_processing.c index da67c147b5..3069c1de87 100644 --- a/firmware/esp32-csi-node/main/edge_processing.c +++ b/firmware/esp32-csi-node/main/edge_processing.c @@ -382,6 +382,14 @@ static uint8_t s_presence_below_count; /**< Consecutive frames below low thres static bool s_fall_detected; static int8_t s_latest_rssi; static uint32_t s_frame_count; +/** Frames process_frame() refused. Counted separately from s_frame_count so + * the two together distinguish "the edge stage is idle" from "the edge stage + * is rejecting everything" -- states that look identical from the outside + * and have completely different causes. EDGE_MAX_SUBCARRIERS is + * target-conditional, so a build that selects the pre-HE 128 on an HE part + * rejects every frame here while the task still starts and logs its + * banner. */ +static uint32_t s_frame_rejected; /** Previous phase velocity for fall detection (acceleration). */ static float s_prev_phase_velocity; @@ -1062,7 +1070,10 @@ static void send_feature_vector(void) static void process_frame(const edge_ring_slot_t *slot) { uint16_t n_subcarriers = slot->iq_len / 2; - if (n_subcarriers == 0 || n_subcarriers > EDGE_MAX_SUBCARRIERS) return; + if (n_subcarriers == 0 || n_subcarriers > EDGE_MAX_SUBCARRIERS) { + s_frame_rejected++; + return; + } s_frame_count++; s_latest_rssi = slot->rssi; @@ -1389,6 +1400,16 @@ void edge_get_variances(float *out_variances, uint16_t n_subcarriers) } } +uint32_t edge_processing_get_frames_processed(void) +{ + return s_frame_count; +} + +uint32_t edge_processing_get_frames_rejected(void) +{ + return s_frame_rejected; +} + esp_err_t edge_processing_init(const edge_config_t *cfg) { if (cfg == NULL) { @@ -1421,6 +1442,7 @@ esp_err_t edge_processing_init(const edge_config_t *cfg) s_fall_detected = false; s_latest_rssi = 0; s_frame_count = 0; + s_frame_rejected = 0; s_sample_rate_hz = EDGE_CONFIGURED_SAMPLE_RATE_HZ; s_filter_design_fs = EDGE_CONFIGURED_SAMPLE_RATE_HZ; s_rate_window_start_us = 0; diff --git a/firmware/esp32-csi-node/main/edge_processing.h b/firmware/esp32-csi-node/main/edge_processing.h index 0fe280b0a6..543aa324e4 100644 --- a/firmware/esp32-csi-node/main/edge_processing.h +++ b/firmware/esp32-csi-node/main/edge_processing.h @@ -343,4 +343,21 @@ void edge_get_phase_history(const float **out_buf, uint16_t *out_len, */ void edge_get_variances(float *out_variances, uint16_t n_subcarriers); +/** + * Frames the edge pipeline accepted, cumulative since init. + * + * Read with edge_processing_get_frames_rejected(): the pair is what makes an + * idle edge stage distinguishable from one rejecting every frame. + */ +uint32_t edge_processing_get_frames_processed(void); + +/** + * Frames process_frame() refused, cumulative since init. + * + * Non-zero and climbing with frames_processed stuck at 0 means every frame is + * failing the EDGE_MAX_SUBCARRIERS guard -- the signature of a build that + * selected the pre-HE 128 on an HE-capable part. + */ +uint32_t edge_processing_get_frames_rejected(void); + #endif /* EDGE_PROCESSING_H */ diff --git a/firmware/esp32-csi-node/main/node_log.c b/firmware/esp32-csi-node/main/node_log.c index deb6720316..7a73b4369b 100644 --- a/firmware/esp32-csi-node/main/node_log.c +++ b/firmware/esp32-csi-node/main/node_log.c @@ -16,6 +16,7 @@ #include "c6_sync_espnow.h" #include "csi_collector.h" +#include "edge_processing.h" #include "nvs_config.h" #include "thermal.h" @@ -179,6 +180,14 @@ static void fill_health(node_log_periodic_t *h) h->csi_fps_x100 = csi_collector_get_pkt_yield_per_sec() * 100; h->tx_send_fail = csi_collector_get_send_fail_count(); + /* node_log_periodic_t has declared these two since the log shipped and + * node_log_read.py has always rendered them, but nothing assigned them, + * so every record on every node reported 0/0 -- the memset, not a + * measurement. On the one question this log exists to answer about an + * unreachable node, it returned a plausible constant zero. */ + h->frames_processed = edge_processing_get_frames_processed(); + h->frames_rejected = edge_processing_get_frames_rejected(); + h->disconnect_count = s_disconnect_count; h->last_disc_reason = s_last_disc_reason; h->last_disc_rssi = s_last_disc_rssi;