diff --git a/ROADMAP.md b/ROADMAP.md index 395b274..2184f4a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -93,23 +93,41 @@ Note: Promiscuous mode (probe/deauth capture) disabled on original ESP32 — bre - [x] Home Assistant webhook integration (deauth_flood, unknown_probe, unknown_ble) - [x] Parallel OTA fleet updates (`esp-fleet ota --parallel`) -## v1.4 - Multi-Sensor & Validation -- [ ] Multi-sensor BLE correlation in esp-ctl (zone tracking by source sensor) +## v1.4 - Multi-Sensor & Validation [DONE] +- [x] Multi-sensor BLE correlation in esp-ctl (zone tracking by source sensor) +- [x] Zone tracking with EMA RSSI (`esp-ctl osint zones`, `device_zones` table) +- [x] Per-sensor breakdown in MAC profile (`esp-ctl osint mac`) +- [x] POWERTEST command (7-phase power profiling with EVENT markers) - [ ] Test OTA rollback (flash bad firmware, verify auto-revert) - [ ] Create HA webhook automations for deauth_flood / unknown_probe - [ ] Document esp-crab dual-antenna capabilities - [ ] Document esp-radar console features ## v1.5 - Power Management -- [ ] Power consumption measurements (per-mode baseline) +- [ ] Power consumption measurements using POWERTEST + external meter - [ ] Deep sleep mode with wake-on-CSI-motion - [ ] Battery-optimized duty cycling +## v2.0 - Hardware Upgrade (ESP32-S3/C6) + +Requires replacing current ESP32 (original) DevKitC V1 boards with ESP32-S3 +or ESP32-C6 modules. The original ESP32 lacks FTM and has CSI/promiscuous +mode conflicts. + +- [ ] Select target chip (ESP32-S3 for dual-core + BLE 5, or C6 for WiFi 6 + 802.15.4) +- [ ] Port firmware to new target (`idf.py set-target`, adjust `#if CONFIG_IDF_TARGET_*`) +- [ ] WiFi FTM / 802.11mc support (Fine Timing Measurement, ~1-2m accuracy) + - FTM initiator + responder mode on each sensor + - Inter-sensor ranging (3 pairs from 3 sensors) + - Auto-calibrate sensor positions for 3D floor plan +- [ ] Enable promiscuous mode alongside CSI (works on S2/S3/C3/C6) +- [ ] Validate CSI quality on new chip (subcarrier count differs) +- [ ] Update parsers for chip-specific CSI format + ## Future - AP+STA config portal (WIFI_MODE_APSTA, captive portal for initial setup) - ESP-NOW mesh (ESP32-to-ESP32 CSI) - Multi-channel scanning (hop across WiFi channels) -- RSSI triangulation with 3 sensors (approximate device location) - BLE device fingerprinting (identify phone models by advertisement patterns) - Historical presence logging (who was here, when, how long) - External sensor support (PIR, temp/humidity via GPIO) diff --git a/get-started/csi_recv_router/main/app_main.c b/get-started/csi_recv_router/main/app_main.c index ed11865..0f8a9f4 100644 --- a/get-started/csi_recv_router/main/app_main.c +++ b/get-started/csi_recv_router/main/app_main.c @@ -162,6 +162,9 @@ static struct { int64_t ts; } s_deauth_ring[FLOOD_RING_SIZE]; static int s_deauth_ring_head = 0; static int s_deauth_ring_count = 0; +/* Power test */ +static volatile bool s_powertest_running = false; + /* --- NVS helpers --- */ static void config_load_nvs(void) @@ -1071,6 +1074,117 @@ static void reboot_after_delay(void *arg) esp_restart(); } +/* --- Power test --- */ + +static void send_powertest_event(const char *phase, int dwell_s) +{ + char buf[128]; + int len = snprintf(buf, sizeof(buf), "EVENT,%s,powertest,%s,%d\n", + s_hostname, phase, dwell_s); + if (s_udp_socket >= 0) { + sendto(s_udp_socket, buf, len, 0, + (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr)); + } +} + +static void powertest_task(void *arg) +{ + int dwell_s = (int)(intptr_t)arg; + + /* Save current settings */ + int saved_freq = s_send_frequency; + bool saved_adaptive = s_adaptive; + bool saved_ble = s_ble_enabled; + int8_t saved_tx_power = s_tx_power_dbm; + led_mode_t saved_led = s_led_mode; + + /* Disable adaptive during test */ + s_adaptive = false; + s_motion_detected = false; + + typedef struct { + const char *name; + int rate; /* 0 = stop ping */ + bool ble; + int8_t tx_dbm; /* 0 = no change */ + bool led; + } phase_t; + + static const phase_t phases[] = { + { "idle", 0, false, 0, false }, + { "csi_10", 10, false, 0, true }, + { "csi_100", 100, false, 0, true }, + { "ble_only", 0, true, 0, true }, + { "all", 100, true, 0, true }, + { "tx_low", 100, false, 2, true }, + { "tx_high", 100, false, 20, true }, + }; + int n_phases = sizeof(phases) / sizeof(phases[0]); + + ESP_LOGI(TAG, "POWERTEST: starting %d phases, dwell=%ds", n_phases, dwell_s); + + for (int i = 0; i < n_phases; i++) { + const phase_t *p = &phases[i]; + + send_powertest_event(p->name, dwell_s); + ESP_LOGI(TAG, "POWERTEST: phase %s (rate=%d ble=%d tx=%d)", p->name, p->rate, p->ble, p->tx_dbm); + + /* LED */ + s_led_mode = p->led ? LED_FAST_BLINK : LED_OFF; + + /* BLE */ + if (p->ble && !s_ble_enabled) { + s_ble_enabled = true; + ble_scan_start(); + } else if (!p->ble && s_ble_enabled) { + s_ble_enabled = false; + ble_gap_disc_cancel(); + } + + /* TX power */ + if (p->tx_dbm > 0) { + s_tx_power_dbm = p->tx_dbm; + esp_wifi_set_max_tx_power(s_tx_power_dbm * 4); + } + + /* Ping rate */ + if (p->rate > 0) { + s_send_frequency = p->rate; + wifi_ping_router_start(); + } else { + if (s_ping_handle) { + esp_ping_stop(s_ping_handle); + esp_ping_delete_session(s_ping_handle); + s_ping_handle = NULL; + } + } + + vTaskDelay(pdMS_TO_TICKS(dwell_s * 1000)); + } + + /* Restore settings */ + s_adaptive = saved_adaptive; + s_ble_enabled = saved_ble; + s_tx_power_dbm = saved_tx_power; + esp_wifi_set_max_tx_power(s_tx_power_dbm * 4); + s_led_mode = saved_led; + s_send_frequency = saved_freq; + + if (saved_ble) { + ble_scan_start(); + } else { + ble_gap_disc_cancel(); + } + wifi_ping_router_start(); + + int total_s = dwell_s * n_phases; + send_powertest_event("done", total_s); + ESP_LOGI(TAG, "POWERTEST: done total=%ds", total_s); + + s_powertest_running = false; + vTaskDelete(NULL); +} + static int cmd_handle(const char *cmd, char *reply, size_t reply_size) { /* REBOOT */ @@ -1469,6 +1583,26 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) return strlen(reply); } + /* POWERTEST [dwell_s] */ + if (strncmp(cmd, "POWERTEST", 9) == 0) { + if (s_powertest_running) { + snprintf(reply, reply_size, "ERR POWERTEST already running"); + return strlen(reply); + } + int dwell = 15; + if (cmd[9] == ' ') { + dwell = atoi(cmd + 10); + if (dwell < 5 || dwell > 60) { + snprintf(reply, reply_size, "ERR POWERTEST dwell range 5-60"); + return strlen(reply); + } + } + s_powertest_running = true; + xTaskCreate(powertest_task, "powertest", 4096, (void *)(intptr_t)dwell, 3, NULL); + snprintf(reply, reply_size, "OK POWERTEST started dwell=%ds phases=7 total=~%ds", dwell, dwell * 7); + return strlen(reply); + } + snprintf(reply, reply_size, "ERR UNKNOWN"); return strlen(reply); }