From c922e052661faf3c56b720649770b6d131349b37 Mon Sep 17 00:00:00 2001 From: user Date: Wed, 4 Feb 2026 16:34:19 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Add=20v0.4=20adaptive=20sampling=20?= =?UTF-8?q?=E2=80=94=20wander=20detection,=20auto=20rate=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On-device CSI wander calculation (coefficient of variation over 50-packet window). Rate drops to 10 Hz when idle, jumps to 100 Hz on motion with 3s holdoff. EVENT notifications sent to Pi on rate changes. New commands: ADAPTIVE ON/OFF, THRESHOLD. RATE command disables adaptive mode. All settings NVS-persisted. --- ROADMAP.md | 14 +- TASKS.md | 32 +++-- docs/CHEATSHEET.md | 22 ++- get-started/csi_recv_router/main/app_main.c | 146 +++++++++++++++++++- 4 files changed, 191 insertions(+), 23 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6e5b8fe..24180b5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -34,11 +34,15 @@ - [x] USB-flash first device (partition table change) - [x] End-to-end OTA test -## v0.4 - Adaptive Sampling -- [ ] On-device wander calculation (simplified) -- [ ] Reduce to 10 pkt/s when idle -- [ ] Increase to 100 pkt/s on motion detection -- [ ] Rate change notification to Pi +## v0.4 - Adaptive Sampling [DONE] +- [x] On-device CSI wander calculation (coefficient of variation) +- [x] Reduce to 10 pkt/s when idle (3s holdoff) +- [x] Increase to 100 pkt/s on motion detection +- [x] Rate change EVENT notification to Pi via UDP +- [x] ADAPTIVE ON/OFF command (NVS persisted) +- [x] THRESHOLD command for tuning sensitivity (NVS persisted) +- [x] RATE command disables adaptive mode +- [x] adaptive/motion fields in STATUS reply ## v0.5 - BLE Scanning - [ ] Enable Bluetooth alongside WiFi diff --git a/TASKS.md b/TASKS.md index 437bbd9..5651e9d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,15 +2,15 @@ **Last Updated:** 2026-02-04 -## Current Sprint: v0.4 - Adaptive Sampling +## Current Sprint: v0.5 - BLE Scanning ### P0 - Critical -- [ ] On-device CSI wander calculation (simplified) -- [ ] Adaptive rate: 10 pkt/s idle → 100 pkt/s on motion +- [ ] Enable Bluetooth alongside WiFi +- [ ] Periodic BLE advertisement scanning ### P1 - Important -- [ ] Rate change notification to Pi -- [ ] Tunable motion threshold via UDP command +- [ ] Report device MAC, RSSI, name via UDP +- [ ] Pi-side BLE device tracking ### P2 - Normal - [ ] OTA update remaining fleet (muddy-storm, hollow-acorn) via USB @@ -20,6 +20,17 @@ - [ ] Document esp-crab dual-antenna capabilities - [ ] Document esp-radar console features +## Completed: v0.4 - Adaptive Sampling + +- [x] On-device CSI wander calculation (coefficient of variation) +- [x] Adaptive rate: 10 pkt/s idle (3s holdoff) → 100 pkt/s on motion +- [x] EVENT notification to Pi on rate change +- [x] ADAPTIVE ON/OFF command (NVS persisted) +- [x] THRESHOLD command for tuning sensitivity (NVS persisted) +- [x] RATE command disables adaptive mode +- [x] adaptive/motion fields in STATUS reply +- [x] OTA deployed and verified on amber-maple + ## Completed: v0.3 - OTA Updates - [x] Dual OTA partition table (`partitions.csv`) @@ -44,8 +55,6 @@ - [x] Pi-side: `esp-cmd` CLI tool - [x] Pi-side: `esp-fleet` fleet management tool - [x] mDNS hostname, watchdog, human-readable uptime -- [x] Build and flash to device -- [x] Update CHEATSHEET.md ## Completed: v0.1 - Documentation @@ -58,8 +67,7 @@ ## Notes -- NVS offset changes with new partition table — first USB flash resets saved config -- First device must be USB-flashed to switch partition table, subsequent updates via OTA -- `esp_https_ota` is built into ESP-IDF core — no extra deps needed -- OTA download ~896 KB on LAN takes ~3-5s, well under 30s watchdog -- CSI data keeps flowing during OTA download +- Adaptive threshold varies by environment; 0.001-0.01 is a good starting range +- NVS keys: `send_rate`, `tx_power`, `adaptive`, `threshold` +- EVENT packets sent on CSI UDP port when adaptive rate changes +- OTA download ~904 KB on LAN takes ~3-5s, well under 30s watchdog diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index d68a020..d168e74 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -34,10 +34,13 @@ idf.py reconfigure # Re-fetch managed components ## Remote Management (esp-cmd) ```bash -esp-cmd STATUS # Uptime, heap, RSSI, tx_power, rate, version +esp-cmd STATUS # Uptime, heap, RSSI, rate, version, adaptive, motion esp-cmd IDENTIFY # LED solid 5s (find the device) -esp-cmd RATE 50 # Set ping rate to 50 Hz (NVS saved) +esp-cmd RATE 50 # Set ping rate to 50 Hz (disables adaptive) esp-cmd POWER 15 # Set TX power to 15 dBm (NVS saved) +esp-cmd ADAPTIVE ON # Enable adaptive sampling (NVS saved) +esp-cmd ADAPTIVE OFF # Disable adaptive sampling +esp-cmd THRESHOLD 0.005 # Set motion sensitivity (NVS saved) esp-cmd OTA http://pi:8070/fw # Trigger OTA update (use esp-ota instead) esp-cmd REBOOT # Restart device ``` @@ -84,6 +87,21 @@ After that, all updates are OTA. If new firmware crashes or hangs, the 30s watchdog reboots and bootloader automatically rolls back to the previous firmware. +## Adaptive Sampling + +When enabled, the device automatically adjusts ping rate based on CSI wander: + +- **Motion detected** (wander > threshold): 100 pkt/s +- **Idle** (wander < threshold for 3s): 10 pkt/s +- Rate changes send `EVENT motion=<0|1> rate= wander=` via UDP + +```bash +esp-cmd amber-maple.local ADAPTIVE ON # Enable +esp-cmd amber-maple.local THRESHOLD 0.005 # Tune sensitivity +# Lower threshold = more sensitive, higher = less sensitive +# Good starting range: 0.001 - 0.01 +``` + ### LED States | LED | Meaning | diff --git a/get-started/csi_recv_router/main/app_main.c b/get-started/csi_recv_router/main/app_main.c index 1368d82..f7c74ea 100644 --- a/get-started/csi_recv_router/main/app_main.c +++ b/get-started/csi_recv_router/main/app_main.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -80,6 +81,20 @@ static volatile int64_t s_last_csi_time = 0; static volatile int64_t s_identify_end_time = 0; static volatile bool s_ota_in_progress = false; +/* Adaptive sampling */ +#define WANDER_WINDOW 50 +#define RATE_ACTIVE 100 +#define RATE_IDLE 10 +#define IDLE_HOLDOFF_US 3000000LL /* 3s of no motion before dropping rate */ +#define DEFAULT_THRESHOLD 0.002f + +static bool s_adaptive = false; +static float s_motion_threshold = DEFAULT_THRESHOLD; +static volatile bool s_motion_detected = false; +static volatile int64_t s_last_motion_time = 0; +static uint32_t s_energy_buf[WANDER_WINDOW]; +static uint32_t s_energy_idx = 0; + /* UDP socket for CSI data transmission */ static int s_udp_socket = -1; static struct sockaddr_in s_dest_addr; @@ -99,8 +114,17 @@ static void config_load_nvs(void) if (nvs_get_i8(h, "tx_power", &pwr) == ESP_OK && pwr >= 2 && pwr <= 20) { s_tx_power_dbm = pwr; } + int8_t adaptive; + if (nvs_get_i8(h, "adaptive", &adaptive) == ESP_OK) { + s_adaptive = (adaptive != 0); + } + int32_t thresh; + if (nvs_get_i32(h, "threshold", &thresh) == ESP_OK && thresh > 0) { + s_motion_threshold = (float)thresh / 1000000.0f; + } nvs_close(h); - ESP_LOGI(TAG, "NVS loaded: rate=%d tx_power=%d", s_send_frequency, s_tx_power_dbm); + ESP_LOGI(TAG, "NVS loaded: rate=%d tx_power=%d adaptive=%d threshold=%.6f", + s_send_frequency, s_tx_power_dbm, s_adaptive, s_motion_threshold); } else { ESP_LOGI(TAG, "NVS: no saved config, using defaults"); } @@ -286,6 +310,16 @@ static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info) sendto(s_udp_socket, s_udp_buffer, pos, 0, (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr)); } + /* Compute CSI energy for adaptive sampling */ + if (s_adaptive) { + uint32_t energy = 0; + for (int i = 0; i < info->len; i++) { + energy += abs(info->buf[i]); + } + s_energy_buf[s_energy_idx % WANDER_WINDOW] = energy; + s_energy_idx++; + } + s_count++; } @@ -392,6 +426,71 @@ static esp_err_t wifi_ping_router_start(void) return ESP_OK; } +/* --- Adaptive sampling --- */ + +static void adaptive_task(void *arg) +{ + while (1) { + vTaskDelay(pdMS_TO_TICKS(500)); + + if (!s_adaptive || s_energy_idx < WANDER_WINDOW) continue; + + /* Compute mean */ + float mean = 0; + for (int i = 0; i < WANDER_WINDOW; i++) { + mean += s_energy_buf[i]; + } + mean /= WANDER_WINDOW; + + if (mean < 1.0f) continue; + + /* Compute variance */ + float var = 0; + for (int i = 0; i < WANDER_WINDOW; i++) { + float d = s_energy_buf[i] - mean; + var += d * d; + } + var /= WANDER_WINDOW; + + /* Wander = coefficient of variation squared */ + float wander = var / (mean * mean); + + int64_t now = esp_timer_get_time(); + bool motion = wander > s_motion_threshold; + + if (motion) { + s_last_motion_time = now; + } + + int target_rate; + if (motion || (now - s_last_motion_time < IDLE_HOLDOFF_US)) { + target_rate = RATE_ACTIVE; + } else { + target_rate = RATE_IDLE; + } + + s_motion_detected = motion; + + if (target_rate != s_send_frequency) { + s_send_frequency = target_rate; + wifi_ping_router_start(); + + /* Notify Pi */ + char event[80]; + int len = snprintf(event, sizeof(event), + "EVENT motion=%d rate=%d wander=%.6f", + motion ? 1 : 0, target_rate, wander); + if (s_udp_socket >= 0) { + sendto(s_udp_socket, event, len, 0, + (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr)); + } + + ESP_LOGI(TAG, "Adaptive: %s -> %d Hz (wander=%.6f)", + motion ? "motion" : "idle", target_rate, wander); + } + } +} + /* --- OTA --- */ static void ota_task(void *arg) @@ -478,9 +577,10 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) } snprintf(reply, reply_size, - "OK STATUS uptime=%s heap=%lu rssi=%d tx_power=%d rate=%d hostname=%s version=%s", + "OK STATUS uptime=%s heap=%lu rssi=%d tx_power=%d rate=%d hostname=%s version=%s adaptive=%s motion=%d", uptime_str, (unsigned long)heap, rssi, (int)s_tx_power_dbm, - s_send_frequency, CONFIG_CSI_HOSTNAME, app_desc->version); + s_send_frequency, CONFIG_CSI_HOSTNAME, app_desc->version, + s_adaptive ? "on" : "off", s_motion_detected ? 1 : 0); return strlen(reply); } @@ -491,10 +591,15 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) snprintf(reply, reply_size, "ERR RATE range 10-100"); return strlen(reply); } + if (s_adaptive) { + s_adaptive = false; + s_motion_detected = false; + config_save_i8("adaptive", 0); + } s_send_frequency = val; config_save_i32("send_rate", (int32_t)val); wifi_ping_router_start(); - snprintf(reply, reply_size, "OK RATE %d", val); + snprintf(reply, reply_size, "OK RATE %d (adaptive off)", val); return strlen(reply); } @@ -512,6 +617,38 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) return strlen(reply); } + /* ADAPTIVE ON/OFF */ + if (strncmp(cmd, "ADAPTIVE ", 9) == 0) { + const char *arg = cmd + 9; + if (strncmp(arg, "ON", 2) == 0) { + s_adaptive = true; + s_energy_idx = 0; + config_save_i8("adaptive", 1); + snprintf(reply, reply_size, "OK ADAPTIVE on threshold=%.6f", s_motion_threshold); + } else if (strncmp(arg, "OFF", 3) == 0) { + s_adaptive = false; + s_motion_detected = false; + config_save_i8("adaptive", 0); + snprintf(reply, reply_size, "OK ADAPTIVE off"); + } else { + snprintf(reply, reply_size, "ERR ADAPTIVE ON or OFF"); + } + return strlen(reply); + } + + /* THRESHOLD */ + if (strncmp(cmd, "THRESHOLD ", 10) == 0) { + float val = strtof(cmd + 10, NULL); + if (val <= 0.0f || val > 1.0f) { + snprintf(reply, reply_size, "ERR THRESHOLD range 0.000001-1.0"); + return strlen(reply); + } + s_motion_threshold = val; + config_save_i32("threshold", (int32_t)(val * 1000000.0f)); + snprintf(reply, reply_size, "OK THRESHOLD %.6f", val); + return strlen(reply); + } + /* OTA */ if (strncmp(cmd, "OTA ", 4) == 0) { const char *url = cmd + 4; @@ -639,6 +776,7 @@ void app_main() wifi_ping_router_start(); xTaskCreate(cmd_task, "cmd_task", 4096, NULL, 5, NULL); + xTaskCreate(adaptive_task, "adaptive", 3072, NULL, 3, NULL); /* OTA rollback: mark firmware valid if we got this far */ const esp_partition_t *running = esp_ota_get_running_partition();