feat: Add v0.4 adaptive sampling — wander detection, auto rate control
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.
This commit is contained in:
14
ROADMAP.md
14
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
|
||||
|
||||
32
TASKS.md
32
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
|
||||
|
||||
@@ -34,10 +34,13 @@ idf.py reconfigure # Re-fetch managed components
|
||||
## Remote Management (esp-cmd)
|
||||
|
||||
```bash
|
||||
esp-cmd <host> STATUS # Uptime, heap, RSSI, tx_power, rate, version
|
||||
esp-cmd <host> STATUS # Uptime, heap, RSSI, rate, version, adaptive, motion
|
||||
esp-cmd <host> IDENTIFY # LED solid 5s (find the device)
|
||||
esp-cmd <host> RATE 50 # Set ping rate to 50 Hz (NVS saved)
|
||||
esp-cmd <host> RATE 50 # Set ping rate to 50 Hz (disables adaptive)
|
||||
esp-cmd <host> POWER 15 # Set TX power to 15 dBm (NVS saved)
|
||||
esp-cmd <host> ADAPTIVE ON # Enable adaptive sampling (NVS saved)
|
||||
esp-cmd <host> ADAPTIVE OFF # Disable adaptive sampling
|
||||
esp-cmd <host> THRESHOLD 0.005 # Set motion sensitivity (NVS saved)
|
||||
esp-cmd <host> OTA http://pi:8070/fw # Trigger OTA update (use esp-ota instead)
|
||||
esp-cmd <host> 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=<hz> wander=<value>` 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 |
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <errno.h>
|
||||
#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 <value> */
|
||||
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 <url> */
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user