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:
user
2026-02-04 16:34:19 +01:00
parent 2f90a099b7
commit c922e05266
4 changed files with 191 additions and 23 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 |

View File

@@ -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();