fix(firmware): fall detection false positives + 4MB flash support (#263, #265)

Issue #263: Default fall_thresh raised from 2.0 to 15.0 rad/s² — normal
walking produces accelerations of 2.5-5.0 which triggered constant false
"Fall Detected" alerts. Added consecutive-frame requirement (3 frames)
and 5-second cooldown debounce to prevent alert storms.

Issue #265: Added partitions_4mb.csv and sdkconfig.defaults.4mb for
ESP32-S3 boards with 4MB flash (e.g. SuperMini). OTA slots are 1.856MB
each, fitting the ~978KB firmware binary with room to spare.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-03-15 09:44:23 -04:00
parent 1d4af7c757
commit 8afd76da20
6 changed files with 79 additions and 7 deletions

View File

@ -244,6 +244,10 @@ static uint32_t s_frame_count;
/** Previous phase velocity for fall detection (acceleration). */ /** Previous phase velocity for fall detection (acceleration). */
static float s_prev_phase_velocity; static float s_prev_phase_velocity;
/** Fall detection debounce state (issue #263). */
static uint8_t s_fall_consec_count; /**< Consecutive frames above threshold. */
static int64_t s_fall_last_alert_us; /**< Timestamp of last fall alert (debounce). */
/** Adaptive calibration state. */ /** Adaptive calibration state. */
static bool s_calibrated; static bool s_calibrated;
static float s_calib_sum; static float s_calib_sum;
@ -689,7 +693,7 @@ static void process_frame(const edge_ring_slot_t *slot)
} }
s_presence_detected = (s_presence_score > threshold); s_presence_detected = (s_presence_score > threshold);
/* --- Step 10: Fall detection (phase acceleration) --- */ /* --- Step 10: Fall detection (phase acceleration + debounce, issue #263) --- */
if (s_history_len >= 3) { if (s_history_len >= 3) {
uint16_t i0 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 1) % EDGE_PHASE_HISTORY_LEN; uint16_t i0 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 1) % EDGE_PHASE_HISTORY_LEN;
uint16_t i1 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 2) % EDGE_PHASE_HISTORY_LEN; uint16_t i1 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 2) % EDGE_PHASE_HISTORY_LEN;
@ -697,10 +701,26 @@ static void process_frame(const edge_ring_slot_t *slot)
float accel = fabsf(velocity - s_prev_phase_velocity); float accel = fabsf(velocity - s_prev_phase_velocity);
s_prev_phase_velocity = velocity; s_prev_phase_velocity = velocity;
s_fall_detected = (accel > s_cfg.fall_thresh); if (accel > s_cfg.fall_thresh) {
if (s_fall_detected) { s_fall_consec_count++;
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f", } else {
accel, s_cfg.fall_thresh); s_fall_consec_count = 0;
}
/* Require EDGE_FALL_CONSEC_MIN consecutive frames above threshold,
* plus a cooldown period to prevent alert storms. */
int64_t now_us = esp_timer_get_time();
int64_t cooldown_us = (int64_t)EDGE_FALL_COOLDOWN_MS * 1000;
if (s_fall_consec_count >= EDGE_FALL_CONSEC_MIN
&& (now_us - s_fall_last_alert_us) >= cooldown_us)
{
s_fall_detected = true;
s_fall_last_alert_us = now_us;
s_fall_consec_count = 0;
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f (consec=%u)",
accel, s_cfg.fall_thresh, EDGE_FALL_CONSEC_MIN);
} else if (s_fall_consec_count == 0) {
s_fall_detected = false;
} }
} }
@ -850,6 +870,8 @@ esp_err_t edge_processing_init(const edge_config_t *cfg)
s_latest_rssi = 0; s_latest_rssi = 0;
s_frame_count = 0; s_frame_count = 0;
s_prev_phase_velocity = 0.0f; s_prev_phase_velocity = 0.0f;
s_fall_consec_count = 0;
s_fall_last_alert_us = 0;
s_last_vitals_send_us = 0; s_last_vitals_send_us = 0;
s_has_prev_iq = false; s_has_prev_iq = false;
s_prev_iq_len = 0; s_prev_iq_len = 0;

View File

@ -42,6 +42,10 @@
#define EDGE_CALIB_FRAMES 1200 /**< Frames for adaptive calibration (~60s at 20 Hz). */ #define EDGE_CALIB_FRAMES 1200 /**< Frames for adaptive calibration (~60s at 20 Hz). */
#define EDGE_CALIB_SIGMA_MULT 3.0f /**< Threshold = mean + 3*sigma of ambient. */ #define EDGE_CALIB_SIGMA_MULT 3.0f /**< Threshold = mean + 3*sigma of ambient. */
/* ---- Fall detection ---- */
#define EDGE_FALL_COOLDOWN_MS 5000 /**< Minimum ms between fall alerts (debounce). */
#define EDGE_FALL_CONSEC_MIN 3 /**< Consecutive frames above threshold to trigger. */
/* ---- SPSC ring buffer slot ---- */ /* ---- SPSC ring buffer slot ---- */
typedef struct { typedef struct {
uint8_t iq_data[EDGE_MAX_IQ_BYTES]; /**< Raw I/Q bytes from CSI callback. */ uint8_t iq_data[EDGE_MAX_IQ_BYTES]; /**< Raw I/Q bytes from CSI callback. */

View File

@ -61,7 +61,7 @@ void nvs_config_load(nvs_config_t *cfg)
#ifdef CONFIG_EDGE_FALL_THRESH #ifdef CONFIG_EDGE_FALL_THRESH
cfg->fall_thresh = (float)CONFIG_EDGE_FALL_THRESH / 1000.0f; cfg->fall_thresh = (float)CONFIG_EDGE_FALL_THRESH / 1000.0f;
#else #else
cfg->fall_thresh = 2.0f; cfg->fall_thresh = 15.0f; /* Default raised from 2.0 — see issue #263. */
#endif #endif
cfg->vital_window = 256; cfg->vital_window = 256;
#ifdef CONFIG_EDGE_VITAL_INTERVAL_MS #ifdef CONFIG_EDGE_VITAL_INTERVAL_MS

View File

@ -0,0 +1,15 @@
# ESP32-S3 CSI Node — 4MB flash partition table (issue #265)
# For boards with 4MB flash (e.g. ESP32-S3 SuperMini 4MB).
# Binary is ~978KB so each OTA slot is 1.875MB — plenty of room.
#
# Usage: copy to partitions_display.csv OR set in sdkconfig:
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
#
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xF000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x1D0000,
ota_1, app, ota_1, 0x1F0000, 0x1D0000,
Can't render this file because it contains an unexpected character in line 6 and column 44.

View File

@ -168,7 +168,9 @@ def main():
parser.add_argument("--edge-tier", type=int, choices=[0, 1, 2], parser.add_argument("--edge-tier", type=int, choices=[0, 1, 2],
help="Edge processing tier: 0=off, 1=stats, 2=vitals") help="Edge processing tier: 0=off, 1=stats, 2=vitals")
parser.add_argument("--pres-thresh", type=int, help="Presence detection threshold (default: 50)") parser.add_argument("--pres-thresh", type=int, help="Presence detection threshold (default: 50)")
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold (default: 500)") parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold in milli-units "
"(value/1000 = rad/s²). Default: 15000 → 15.0 rad/s². "
"Raise to reduce false positives in high-traffic areas.")
parser.add_argument("--vital-win", type=int, help="Phase history window in frames (default: 300)") parser.add_argument("--vital-win", type=int, help="Phase history window in frames (default: 300)")
parser.add_argument("--vital-int", type=int, help="Vitals packet interval in ms (default: 1000)") parser.add_argument("--vital-int", type=int, help="Vitals packet interval in ms (default: 1000)")
parser.add_argument("--subk-count", type=int, help="Top-K subcarrier count (default: 32)") parser.add_argument("--subk-count", type=int, help="Top-K subcarrier count (default: 32)")

View File

@ -0,0 +1,29 @@
# ESP32-S3 CSI Node — 4MB Flash SDK Configuration (issue #265)
# For boards with 4MB flash (e.g. ESP32-S3 SuperMini 4MB).
#
# Build: cp sdkconfig.defaults.4mb sdkconfig.defaults && idf.py set-target esp32s3 && idf.py build
# Or: idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults.4mb" set-target esp32s3 && idf.py build
CONFIG_IDF_TARGET="esp32s3"
# 4MB flash partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
# Compiler: optimize for size (critical for 4MB)
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# CSI support
CONFIG_ESP_WIFI_CSI_ENABLED=y
# Disable display support to save flash (ADR-045 display requires 8MB)
# CONFIG_DISPLAY_ENABLE is not set
# Reduce logging to save flash
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_LWIP_SO_RCVBUF=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192