Files
esp32-hacking/get-started/csi_recv_router/main/app_main.c
user ce5205eb29 feat: Add ALERT command for temp/heap threshold monitoring
ALERT TEMP <celsius> and ALERT HEAP <bytes> emit EVENT packets
when thresholds are crossed (60s holdoff). NVS-persisted, shown
in STATUS and CONFIG. Temp alerts require SOC_TEMP_SENSOR_SUPPORTED.
2026-02-14 17:40:12 +01:00

2610 lines
92 KiB
C

/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/* Get recv router csi
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <errno.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_mac.h"
#include "rom/ets_sys.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_now.h"
#include "esp_timer.h"
#include "esp_task_wdt.h"
#include "esp_pm.h"
#include "esp_heap_caps.h"
#include "esp_ota_ops.h"
#include "esp_https_ota.h"
#include "esp_partition.h"
#include "esp_chip_info.h"
#include "esp_http_client.h"
#include "driver/gpio.h"
#include "soc/soc_caps.h"
#if SOC_TEMP_SENSOR_SUPPORTED
#include "driver/temperature_sensor.h"
#endif
#include "mdns.h"
#include "mbedtls/md.h"
#include "lwip/inet.h"
#include "lwip/netdb.h"
#include "lwip/sockets.h"
#include "ping/ping_sock.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "protocol_examples_common.h"
#include "esp_csi_gain_ctrl.h"
#define CONFIG_SEND_FREQUENCY_DEFAULT 100
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61
#define CSI_FORCE_LLTF 0
#endif
#define CONFIG_FORCE_GAIN 0
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32C61
#define CONFIG_GAIN_CONTROL 1
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#define ESP_IF_WIFI_STA ESP_MAC_WIFI_STA
#endif
#define LED_GPIO GPIO_NUM_2
static const char *TAG = "csi_recv_router";
/* --- LED modes --- */
typedef enum {
LED_OFF,
LED_SLOW_BLINK,
LED_FAST_BLINK,
LED_SOLID,
LED_OTA,
} led_mode_t;
/* --- Globals --- */
static int s_send_frequency = CONFIG_SEND_FREQUENCY_DEFAULT;
static int8_t s_tx_power_dbm = 10;
static esp_ping_handle_t s_ping_handle = NULL;
static volatile led_mode_t s_led_mode = LED_OFF;
static bool s_led_quiet = true; /* quiet mode: off normally, solid on motion/presence */
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;
static volatile uint32_t s_csi_count = 0;
static volatile bool s_wifi_connected = false;
static volatile int8_t s_rssi_min = 0;
static volatile int8_t s_rssi_max = -128;
static uint32_t s_boot_count = 0;
/* CSI collection toggle */
static bool s_csi_enabled = true;
/* CSI output mode */
typedef enum {
CSI_MODE_RAW = 0,
CSI_MODE_COMPACT = 1,
CSI_MODE_HYBRID = 2,
} csi_mode_t;
typedef struct {
float amp_rms;
float amp_std;
float amp_max;
uint8_t amp_max_idx;
uint8_t n_sub;
uint32_t energy;
} csi_features_t;
static csi_mode_t s_csi_mode = CSI_MODE_RAW;
static int s_hybrid_interval = 10;
/* 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;
/* BLE scanning */
#define BLE_SCAN_RESTART_DEFAULT_US 30000000LL /* restart scan every 30s to refresh duplicate filter */
static int64_t s_ble_scan_interval_us = BLE_SCAN_RESTART_DEFAULT_US;
static bool s_ble_enabled = false;
static uint8_t s_ble_own_addr_type;
static esp_timer_handle_t s_ble_timer = NULL;
/* Chip temperature sensor */
#if SOC_TEMP_SENSOR_SUPPORTED
static temperature_sensor_handle_t s_temp_handle = NULL;
#endif
/* UDP socket for CSI data transmission */
static int s_udp_socket = -1;
static struct sockaddr_in s_dest_addr;
static char s_udp_buffer[2048];
static char s_target_ip[16]; /* runtime target IP (NVS or Kconfig default) */
static uint16_t s_target_port; /* runtime target port */
static char s_hostname[32]; /* runtime hostname (NVS or Kconfig default) */
static char s_auth_secret[65] = ""; /* empty = auth disabled */
/* Deauth flood detection */
#define FLOOD_WINDOW_DEFAULT 10
#define FLOOD_THRESH_DEFAULT 5
#define FLOOD_RING_SIZE 64
static int s_flood_thresh = FLOOD_THRESH_DEFAULT;
static int s_flood_window_s = FLOOD_WINDOW_DEFAULT;
static bool s_flood_active = false;
static int64_t s_flood_alert_ts = 0;
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;
static bool s_powersave = false;
/* Baseline calibration & presence detection */
#define BASELINE_MAX_SUBS 64
#define PRESENCE_WINDOW 50
#define PRESENCE_HOLDOFF_US 10000000LL /* 10s debounce */
#define DEFAULT_PR_THRESH 0.05f /* 5% normalized distance */
static float s_baseline_amps[BASELINE_MAX_SUBS];
static int s_baseline_nsub = 0; /* 0 = no baseline */
static bool s_presence_enabled = false;
static float s_pr_threshold = DEFAULT_PR_THRESH;
static volatile float s_pr_last_score = 0.0f;
static volatile bool s_presence_detected = false;
static int64_t s_last_presence_time = 0;
/* Calibration state (transient) */
static volatile bool s_calibrating = false;
static float s_calib_accum[BASELINE_MAX_SUBS];
static volatile uint32_t s_calib_count = 0;
static int s_calib_target = 0;
static int s_calib_nsub = 0;
/* Presence scoring buffer */
static float s_pr_scores[PRESENCE_WINDOW];
static uint32_t s_pr_score_idx = 0;
/* Multi-channel scanning */
#define CHANSCAN_CHANNELS 13
#define CHANSCAN_DWELL_MS 100
static bool s_chanscan_enabled = false;
static int s_chanscan_interval_s = 300; /* 5 min default */
static volatile bool s_chanscan_active = false;
static int64_t s_chanscan_last = 0;
/* Probe dedup rate (moved before config_load_nvs for NVS access) */
#define PROBE_DEDUP_DEFAULT_US 10000000LL
static int64_t s_probe_dedup_us = PROBE_DEDUP_DEFAULT_US;
/* Alert thresholds (0 = disabled) */
#define ALERT_HOLDOFF_US 60000000LL /* 60s debounce between alerts */
static float s_alert_temp_thresh = 0.0f; /* celsius, e.g. 70.0 */
static uint32_t s_alert_heap_thresh = 0; /* bytes, e.g. 30000 */
#if SOC_TEMP_SENSOR_SUPPORTED
static int64_t s_alert_temp_last = 0;
#endif
static int64_t s_alert_heap_last = 0;
/* --- NVS helpers --- */
static void config_load_nvs(void)
{
/* Start with Kconfig defaults */
strncpy(s_hostname, CONFIG_CSI_HOSTNAME, sizeof(s_hostname) - 1);
s_hostname[sizeof(s_hostname) - 1] = '\0';
strncpy(s_target_ip, CONFIG_CSI_UDP_TARGET_IP, sizeof(s_target_ip) - 1);
s_target_ip[sizeof(s_target_ip) - 1] = '\0';
s_target_port = CONFIG_CSI_UDP_TARGET_PORT;
nvs_handle_t h;
if (nvs_open("csi_config", NVS_READONLY, &h) == ESP_OK) {
int32_t val;
if (nvs_get_i32(h, "send_rate", &val) == ESP_OK && val >= 10 && val <= 100) {
s_send_frequency = (int)val;
}
int8_t pwr;
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;
}
int8_t ble;
if (nvs_get_i8(h, "ble_scan", &ble) == ESP_OK) {
s_ble_enabled = (ble != 0);
}
size_t ip_len = sizeof(s_target_ip);
nvs_get_str(h, "target_ip", s_target_ip, &ip_len);
int32_t port;
if (nvs_get_i32(h, "target_port", &port) == ESP_OK && port > 0 && port <= 65535) {
s_target_port = (uint16_t)port;
}
size_t hn_len = sizeof(s_hostname);
nvs_get_str(h, "hostname", s_hostname, &hn_len);
int8_t csi_mode;
if (nvs_get_i8(h, "csi_mode", &csi_mode) == ESP_OK && csi_mode >= 0 && csi_mode <= 2) {
s_csi_mode = (csi_mode_t)csi_mode;
}
int32_t hybrid_n;
if (nvs_get_i32(h, "hybrid_n", &hybrid_n) == ESP_OK && hybrid_n >= 1 && hybrid_n <= 100) {
s_hybrid_interval = (int)hybrid_n;
}
size_t sec_len = sizeof(s_auth_secret);
nvs_get_str(h, "auth_secret", s_auth_secret, &sec_len);
int32_t flood_t;
if (nvs_get_i32(h, "flood_thresh", &flood_t) == ESP_OK && flood_t >= 1 && flood_t <= 100) {
s_flood_thresh = (int)flood_t;
}
int32_t flood_w;
if (nvs_get_i32(h, "flood_window", &flood_w) == ESP_OK && flood_w >= 1 && flood_w <= 300) {
s_flood_window_s = (int)flood_w;
}
int32_t scan_r;
if (nvs_get_i32(h, "scan_rate", &scan_r) == ESP_OK && scan_r >= 5 && scan_r <= 300) {
s_ble_scan_interval_us = (int64_t)scan_r * 1000000LL;
}
int32_t probe_r;
if (nvs_get_i32(h, "probe_rate", &probe_r) == ESP_OK && probe_r >= 1 && probe_r <= 300) {
s_probe_dedup_us = (int64_t)probe_r * 1000000LL;
}
int8_t powersave;
if (nvs_get_i8(h, "powersave", &powersave) == ESP_OK) {
s_powersave = (powersave != 0);
}
int8_t presence;
if (nvs_get_i8(h, "presence", &presence) == ESP_OK) {
s_presence_enabled = (presence != 0);
}
int32_t pr_thresh;
if (nvs_get_i32(h, "pr_thresh", &pr_thresh) == ESP_OK && pr_thresh > 0) {
s_pr_threshold = (float)pr_thresh / 1000000.0f;
}
int8_t bl_nsub;
if (nvs_get_i8(h, "bl_nsub", &bl_nsub) == ESP_OK && bl_nsub > 0 && bl_nsub <= BASELINE_MAX_SUBS) {
size_t bl_len = (size_t)bl_nsub * sizeof(float);
if (nvs_get_blob(h, "bl_amps", s_baseline_amps, &bl_len) == ESP_OK) {
s_baseline_nsub = (int)bl_nsub;
}
}
int8_t chanscan;
if (nvs_get_i8(h, "chanscan", &chanscan) == ESP_OK) {
s_chanscan_enabled = (chanscan != 0);
}
int32_t chanscan_int;
if (nvs_get_i32(h, "chanscan_int", &chanscan_int) == ESP_OK && chanscan_int >= 60 && chanscan_int <= 3600) {
s_chanscan_interval_s = (int)chanscan_int;
}
int8_t led_quiet;
if (nvs_get_i8(h, "led_quiet", &led_quiet) == ESP_OK) {
s_led_quiet = (led_quiet != 0);
}
int8_t csi_en;
if (nvs_get_i8(h, "csi_enabled", &csi_en) == ESP_OK) {
s_csi_enabled = (csi_en != 0);
}
int32_t alert_temp;
if (nvs_get_i32(h, "alert_temp", &alert_temp) == ESP_OK && alert_temp > 0) {
s_alert_temp_thresh = (float)alert_temp / 10.0f; /* stored as 10ths of degree */
}
int32_t alert_heap;
if (nvs_get_i32(h, "alert_heap", &alert_heap) == ESP_OK && alert_heap > 0) {
s_alert_heap_thresh = (uint32_t)alert_heap;
}
nvs_close(h);
ESP_LOGI(TAG, "NVS loaded: hostname=%s rate=%d tx_power=%d adaptive=%d threshold=%.6f ble=%d target=%s:%d csi_mode=%d hybrid_n=%d powersave=%d presence=%d pr_thresh=%.4f baseline_nsub=%d led_quiet=%d csi=%d alert_temp=%.1f alert_heap=%lu",
s_hostname, s_send_frequency, s_tx_power_dbm, s_adaptive, s_motion_threshold, s_ble_enabled,
s_target_ip, s_target_port, (int)s_csi_mode, s_hybrid_interval, s_powersave,
s_presence_enabled, s_pr_threshold, s_baseline_nsub, s_led_quiet, s_csi_enabled,
s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh);
} else {
ESP_LOGI(TAG, "NVS: no saved config, using defaults");
}
/* Boot counter — always increment, even on first boot */
nvs_handle_t bh;
if (nvs_open("csi_config", NVS_READWRITE, &bh) == ESP_OK) {
int32_t bc = 0;
nvs_get_i32(bh, "boot_count", &bc);
s_boot_count = (uint32_t)(bc + 1);
nvs_set_i32(bh, "boot_count", (int32_t)s_boot_count);
nvs_commit(bh);
nvs_close(bh);
ESP_LOGI(TAG, "Boot count: %lu", (unsigned long)s_boot_count);
}
}
static esp_err_t config_save_i32(const char *key, int32_t value)
{
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_i32(h, key, value);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
static esp_err_t config_save_i8(const char *key, int8_t value)
{
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_i8(h, key, value);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
static esp_err_t config_save_str(const char *key, const char *value)
{
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_str(h, key, value);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
static esp_err_t config_save_blob(const char *key, const void *data, size_t len)
{
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_set_blob(h, key, data, len);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
static esp_err_t config_erase_key(const char *key)
{
nvs_handle_t h;
esp_err_t err = nvs_open("csi_config", NVS_READWRITE, &h);
if (err != ESP_OK) return err;
err = nvs_erase_key(h, key);
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
return err;
}
/* --- LED --- */
static void led_gpio_init(void)
{
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << LED_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
gpio_set_level(LED_GPIO, 0);
}
static void led_task(void *arg)
{
bool led_on = false;
while (1) {
/* Check identify timeout */
if (s_led_mode == LED_SOLID && s_identify_end_time > 0) {
if (esp_timer_get_time() >= s_identify_end_time) {
s_identify_end_time = 0;
s_led_mode = s_led_quiet ? LED_OFF : LED_SLOW_BLINK;
}
}
/* Quiet mode: off normally, solid on motion/presence, OTA pattern during OTA */
if (s_led_quiet && s_led_mode != LED_OTA && s_identify_end_time == 0) {
bool activity = s_motion_detected ||
(s_presence_enabled && s_baseline_nsub > 0 && s_pr_last_score > s_pr_threshold);
if (activity) {
gpio_set_level(LED_GPIO, 1);
led_on = true;
} else {
gpio_set_level(LED_GPIO, 0);
led_on = false;
}
vTaskDelay(pdMS_TO_TICKS(200));
continue;
}
/* Auto-switch between slow/fast blink based on CSI activity */
if (s_led_mode == LED_SLOW_BLINK || s_led_mode == LED_FAST_BLINK) {
int64_t now = esp_timer_get_time();
if (s_last_csi_time > 0 && (now - s_last_csi_time) < 500000) {
s_led_mode = LED_FAST_BLINK;
} else if (s_led_mode == LED_FAST_BLINK) {
s_led_mode = LED_SLOW_BLINK;
}
}
switch (s_led_mode) {
case LED_OFF:
gpio_set_level(LED_GPIO, 0);
led_on = false;
vTaskDelay(pdMS_TO_TICKS(200));
break;
case LED_SLOW_BLINK:
led_on = !led_on;
gpio_set_level(LED_GPIO, led_on ? 1 : 0);
vTaskDelay(pdMS_TO_TICKS(500));
break;
case LED_FAST_BLINK:
led_on = !led_on;
gpio_set_level(LED_GPIO, led_on ? 1 : 0);
vTaskDelay(pdMS_TO_TICKS(100));
break;
case LED_SOLID:
gpio_set_level(LED_GPIO, 1);
led_on = true;
vTaskDelay(pdMS_TO_TICKS(200));
break;
case LED_OTA:
/* Double-blink: on-off-on-off-pause */
gpio_set_level(LED_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(80));
gpio_set_level(LED_GPIO, 0);
vTaskDelay(pdMS_TO_TICKS(80));
gpio_set_level(LED_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(80));
gpio_set_level(LED_GPIO, 0);
vTaskDelay(pdMS_TO_TICKS(500));
led_on = false;
break;
}
}
}
/* --- CSI feature extraction --- */
static void csi_extract_features(const int8_t *buf, int len, float gain, csi_features_t *out, float *amps_out)
{
int n_pairs = len / 2;
if (n_pairs > 64) n_pairs = 64;
float amps[64];
float sum_sq = 0.0f;
float sum_amp = 0.0f;
float max_amp = 0.0f;
uint8_t max_idx = 0;
uint32_t energy = 0;
/* Pass 1: compute per-subcarrier amplitude, accumulate sums */
for (int i = 0; i < n_pairs; i++) {
float iv = gain * buf[i * 2];
float qv = gain * buf[i * 2 + 1];
float amp = sqrtf(iv * iv + qv * qv);
amps[i] = amp;
sum_amp += amp;
sum_sq += iv * iv + qv * qv;
energy += abs(buf[i * 2]) + abs(buf[i * 2 + 1]);
if (amp > max_amp) {
max_amp = amp;
max_idx = (uint8_t)i;
}
}
if (amps_out && n_pairs > 0) {
memcpy(amps_out, amps, n_pairs * sizeof(float));
}
float mean_amp = (n_pairs > 0) ? sum_amp / n_pairs : 0.0f;
/* Pass 2: compute variance */
float var = 0.0f;
for (int i = 0; i < n_pairs; i++) {
float d = amps[i] - mean_amp;
var += d * d;
}
var = (n_pairs > 0) ? var / n_pairs : 0.0f;
out->amp_rms = (n_pairs > 0) ? sqrtf(sum_sq / n_pairs) : 0.0f;
out->amp_std = sqrtf(var);
out->amp_max = max_amp;
out->amp_max_idx = max_idx;
out->n_sub = (uint8_t)n_pairs;
out->energy = energy;
}
/* --- CSI callback --- */
static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info)
{
if (!info || !info->buf) {
ESP_LOGW(TAG, "<%s> wifi_csi_cb", esp_err_to_name(ESP_ERR_INVALID_ARG));
return;
}
if (memcmp(info->mac, ctx, 6)) {
return;
}
if (!s_csi_enabled) {
return;
}
s_last_csi_time = esp_timer_get_time();
const wifi_pkt_rx_ctrl_t *rx_ctrl = &info->rx_ctrl;
/* Track RSSI min/max */
int8_t rssi = rx_ctrl->rssi;
if (s_csi_count == 0 || rssi < s_rssi_min) s_rssi_min = rssi;
if (s_csi_count == 0 || rssi > s_rssi_max) s_rssi_max = rssi;
float compensate_gain = 1.0f;
static uint8_t agc_gain = 0;
static int8_t fft_gain = 0;
#if CONFIG_GAIN_CONTROL
static uint8_t agc_gain_baseline = 0;
static int8_t fft_gain_baseline = 0;
esp_csi_gain_ctrl_get_rx_gain(rx_ctrl, &agc_gain, &fft_gain);
if (s_csi_count < 100) {
esp_csi_gain_ctrl_record_rx_gain(agc_gain, fft_gain);
} else if (s_csi_count == 100) {
esp_csi_gain_ctrl_get_rx_gain_baseline(&agc_gain_baseline, &fft_gain_baseline);
#if CONFIG_FORCE_GAIN
esp_csi_gain_ctrl_set_rx_force_gain(agc_gain_baseline, fft_gain_baseline);
ESP_LOGI(TAG, "fft_force %d, agc_force %d", fft_gain_baseline, agc_gain_baseline);
#endif
}
esp_csi_gain_ctrl_get_gain_compensation(&compensate_gain, agc_gain, fft_gain);
ESP_LOGD(TAG, "compensate_gain %f, agc_gain %d, fft_gain %d", compensate_gain, agc_gain, fft_gain);
#endif
/* Extract features (used for compact mode, adaptive sampling, calibration, presence) */
csi_features_t features = {0};
bool need_amps = s_calibrating || (s_presence_enabled && s_baseline_nsub > 0);
static float s_live_amps[64];
if (s_csi_mode != CSI_MODE_RAW || s_adaptive || need_amps) {
csi_extract_features(info->buf, info->len, compensate_gain, &features,
need_amps ? s_live_amps : NULL);
}
/* Calibration: accumulate per-subcarrier amplitudes */
if (s_calibrating && features.n_sub > 0) {
if (s_calib_count == 0) {
s_calib_nsub = features.n_sub;
memset(s_calib_accum, 0, sizeof(s_calib_accum));
}
int nsub = (features.n_sub < s_calib_nsub) ? features.n_sub : s_calib_nsub;
for (int i = 0; i < nsub; i++) {
s_calib_accum[i] += s_live_amps[i];
}
s_calib_count++;
}
/* Presence scoring: normalized Euclidean distance vs baseline */
if (!s_calibrating && s_presence_enabled && s_baseline_nsub > 0 && features.n_sub > 0) {
int nsub = (features.n_sub < s_baseline_nsub) ? features.n_sub : s_baseline_nsub;
float sum_diff_sq = 0.0f;
float sum_base_sq = 0.0f;
for (int i = 0; i < nsub; i++) {
float d = s_live_amps[i] - s_baseline_amps[i];
sum_diff_sq += d * d;
sum_base_sq += s_baseline_amps[i] * s_baseline_amps[i];
}
float score = (sum_base_sq > 0.0f) ? sqrtf(sum_diff_sq / sum_base_sq) : 0.0f;
s_pr_scores[s_pr_score_idx % PRESENCE_WINDOW] = score;
s_pr_score_idx++;
}
/* Determine whether to send raw I/Q data this packet */
csi_mode_t mode = s_csi_mode;
bool send_raw = (mode == CSI_MODE_RAW) ||
(mode == CSI_MODE_HYBRID && (s_csi_count % s_hybrid_interval) == 0);
/* Build CSI data into buffer for UDP transmission */
int pos = 0;
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32C61
if (!s_csi_count) {
ESP_LOGI(TAG, "================ CSI RECV (UDP) ================");
}
pos = snprintf(s_udp_buffer, sizeof(s_udp_buffer),
"CSI_DATA,%s,%lu," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d",
s_hostname, (unsigned long)s_csi_count, MAC2STR(info->mac), rx_ctrl->rssi, rx_ctrl->rate,
rx_ctrl->noise_floor, fft_gain, agc_gain, rx_ctrl->channel,
rx_ctrl->timestamp, rx_ctrl->sig_len, rx_ctrl->rx_state);
#else
if (!s_csi_count) {
ESP_LOGI(TAG, "================ CSI RECV (UDP) ================");
}
pos = snprintf(s_udp_buffer, sizeof(s_udp_buffer),
"CSI_DATA,%s,%lu," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
s_hostname, (unsigned long)s_csi_count, MAC2STR(info->mac), rx_ctrl->rssi, rx_ctrl->rate, rx_ctrl->sig_mode,
rx_ctrl->mcs, rx_ctrl->cwb, rx_ctrl->smoothing, rx_ctrl->not_sounding,
rx_ctrl->aggregation, rx_ctrl->stbc, rx_ctrl->fec_coding, rx_ctrl->sgi,
rx_ctrl->noise_floor, rx_ctrl->ampdu_cnt, rx_ctrl->channel, rx_ctrl->secondary_channel,
rx_ctrl->timestamp, rx_ctrl->ant, rx_ctrl->sig_len, rx_ctrl->rx_state);
#endif
if (send_raw) {
/* Raw I/Q array payload */
#if (CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61) && CSI_FORCE_LLTF
int16_t csi = ((int16_t)(((((uint16_t)info->buf[1]) << 8) | info->buf[0]) << 4) >> 4);
pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos,
",%d,%d,\"[%d", (info->len - 2) / 2, info->first_word_invalid, (int16_t)(compensate_gain * csi));
for (int i = 2; i < (info->len - 2); i += 2) {
csi = ((int16_t)(((((uint16_t)info->buf[i + 1]) << 8) | info->buf[i]) << 4) >> 4);
pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos, ",%d", (int16_t)(compensate_gain * csi));
}
#else
pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos,
",%d,%d,\"[%d", info->len, info->first_word_invalid, (int16_t)(compensate_gain * info->buf[0]));
for (int i = 1; i < info->len; i++) {
pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos, ",%d", (int16_t)(compensate_gain * info->buf[i]));
}
#endif
pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos, "]\"\n");
} else {
/* Compact feature payload */
pos += snprintf(s_udp_buffer + pos, sizeof(s_udp_buffer) - pos,
",%d,%d,\"F:%.1f,%.1f,%.1f,%u,%lu\"\n",
info->len, info->first_word_invalid,
features.amp_rms, features.amp_std, features.amp_max,
(unsigned)features.amp_max_idx, (unsigned long)features.energy);
}
/* Send via UDP */
if (s_udp_socket >= 0) {
sendto(s_udp_socket, s_udp_buffer, pos, 0, (struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
/* Adaptive sampling: reuse extracted energy (features always computed when adaptive is on) */
if (s_adaptive) {
s_energy_buf[s_energy_idx % WANDER_WINDOW] = features.energy;
s_energy_idx++;
}
s_csi_count++;
}
static void wifi_csi_init()
{
/**
* @brief In order to ensure the compatibility of routers, only LLTF sub-carriers are selected.
*/
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61
wifi_csi_config_t csi_config = {
.enable = true,
.acquire_csi_legacy = true,
.acquire_csi_force_lltf = CSI_FORCE_LLTF,
.acquire_csi_ht20 = true,
.acquire_csi_ht40 = true,
.acquire_csi_vht = false,
.acquire_csi_su = false,
.acquire_csi_mu = false,
.acquire_csi_dcm = false,
.acquire_csi_beamformed = false,
.acquire_csi_he_stbc_mode = 2,
.val_scale_cfg = 0,
.dump_ack_en = false,
.reserved = false
};
#elif CONFIG_IDF_TARGET_ESP32C6
wifi_csi_config_t csi_config = {
.enable = true,
.acquire_csi_legacy = true,
.acquire_csi_ht20 = true,
.acquire_csi_ht40 = true,
.acquire_csi_su = false,
.acquire_csi_mu = false,
.acquire_csi_dcm = false,
.acquire_csi_beamformed = false,
.acquire_csi_he_stbc = 2,
.val_scale_cfg = false,
.dump_ack_en = false,
.reserved = false
};
#else
wifi_csi_config_t csi_config = {
.lltf_en = true,
.htltf_en = false,
.stbc_htltf2_en = false,
.ltf_merge_en = true,
.channel_filter_en = true,
.manu_scale = true,
.shift = true,
};
#endif
static wifi_ap_record_t s_ap_info = {0};
ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&s_ap_info));
ESP_ERROR_CHECK(esp_wifi_set_csi_config(&csi_config));
ESP_ERROR_CHECK(esp_wifi_set_csi_rx_cb(wifi_csi_rx_cb, s_ap_info.bssid));
ESP_ERROR_CHECK(esp_wifi_set_csi(true));
}
static void udp_socket_init(void)
{
s_udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (s_udp_socket < 0) {
ESP_LOGE(TAG, "Failed to create UDP socket: errno %d", errno);
return;
}
memset(&s_dest_addr, 0, sizeof(s_dest_addr));
s_dest_addr.sin_family = AF_INET;
s_dest_addr.sin_port = htons(s_target_port);
inet_pton(AF_INET, s_target_ip, &s_dest_addr.sin_addr);
ESP_LOGI(TAG, "UDP socket initialized, sending to %s:%d",
s_target_ip, s_target_port);
}
/* --- Ping --- */
static esp_err_t wifi_ping_router_start(void)
{
/* Stop existing session if any */
if (s_ping_handle) {
esp_ping_stop(s_ping_handle);
esp_ping_delete_session(s_ping_handle);
s_ping_handle = NULL;
}
esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG();
ping_config.count = 0;
ping_config.interval_ms = 1000 / s_send_frequency;
ping_config.task_stack_size = 3072;
ping_config.data_size = 1;
esp_netif_ip_info_t local_ip;
esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"), &local_ip);
ESP_LOGI(TAG, "got ip:" IPSTR ", gw: " IPSTR, IP2STR(&local_ip.ip), IP2STR(&local_ip.gw));
ping_config.target_addr.u_addr.ip4.addr = ip4_addr_get_u32(&local_ip.gw);
ping_config.target_addr.type = ESP_IPADDR_TYPE_V4;
esp_ping_callbacks_t cbs = { 0 };
esp_ping_new_session(&ping_config, &cbs, &s_ping_handle);
esp_ping_start(s_ping_handle);
ESP_LOGI(TAG, "Ping started at %d Hz", s_send_frequency);
return ESP_OK;
}
/* --- BLE scanning --- */
static int ble_gap_event_cb(struct ble_gap_event *event, void *arg);
static void ble_scan_start(void)
{
struct ble_gap_disc_params disc_params = {0};
disc_params.passive = 1;
disc_params.filter_duplicates = 1;
disc_params.itvl = 0;
disc_params.window = 0;
disc_params.filter_policy = 0;
disc_params.limited = 0;
int rc = ble_gap_disc(s_ble_own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_gap_event_cb, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "BLE: scan start failed rc=%d", rc);
} else {
ESP_LOGI(TAG, "BLE: scan started ok");
}
}
static int ble_gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_DISC: {
struct ble_gap_disc_desc *disc = &event->disc;
ESP_LOGI(TAG, "BLE: disc event rssi=%d addr=%02x:%02x:%02x:%02x:%02x:%02x",
disc->rssi, disc->addr.val[5], disc->addr.val[4], disc->addr.val[3],
disc->addr.val[2], disc->addr.val[1], disc->addr.val[0]);
/* Parse advertisement for device name, manufacturer data, TX power, flags */
struct ble_hs_adv_fields fields;
int rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data);
char name[32] = "";
uint16_t company_id = 0;
int8_t tx_power = 127; /* 127 = not present */
uint8_t adv_flags = 0;
if (rc == 0) {
/* Device name */
if (fields.name != NULL && fields.name_len > 0) {
int nlen = fields.name_len < (int)sizeof(name) - 1 ? fields.name_len : (int)sizeof(name) - 1;
memcpy(name, fields.name, nlen);
name[nlen] = '\0';
}
/* Manufacturer-specific data: first 2 bytes = company ID (little-endian) */
if (fields.mfg_data != NULL && fields.mfg_data_len >= 2) {
company_id = fields.mfg_data[0] | (fields.mfg_data[1] << 8);
}
/* TX power level */
if (fields.tx_pwr_lvl_is_present) {
tx_power = fields.tx_pwr_lvl;
}
/* Advertisement flags (always present in struct, 0 if not in advert) */
adv_flags = fields.flags;
}
/* Send BLE_DATA via UDP with extended format */
char buf[192];
int len = snprintf(buf, sizeof(buf),
"BLE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%s,0x%04X,%d,%u\n",
s_hostname,
disc->addr.val[5], disc->addr.val[4], disc->addr.val[3],
disc->addr.val[2], disc->addr.val[1], disc->addr.val[0],
disc->rssi,
disc->addr.type == BLE_ADDR_PUBLIC ? "pub" : "rnd",
name,
company_id,
(int)tx_power,
(unsigned)adv_flags);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, buf, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
break;
}
case BLE_GAP_EVENT_DISC_COMPLETE:
if (s_ble_enabled) {
ble_scan_start();
}
break;
default:
break;
}
return 0;
}
static void ble_scan_restart_timer_cb(void *arg)
{
if (s_ble_enabled) {
ble_gap_disc_cancel();
ble_scan_start();
}
}
static void ble_on_sync(void)
{
int rc = ble_hs_util_ensure_addr(0);
if (rc != 0) {
ESP_LOGE(TAG, "BLE: ensure addr failed rc=%d", rc);
return;
}
rc = ble_hs_id_infer_auto(0, &s_ble_own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "BLE: infer addr type failed rc=%d", rc);
return;
}
ESP_LOGI(TAG, "BLE: stack synced, addr_type=%d", s_ble_own_addr_type);
if (s_ble_enabled) {
ble_scan_start();
}
}
static void ble_on_reset(int reason)
{
ESP_LOGW(TAG, "BLE: stack reset reason=%d", reason);
}
static void ble_host_task(void *param)
{
ESP_LOGI(TAG, "BLE host task started");
nimble_port_run();
nimble_port_freertos_deinit();
}
/* --- Forward declarations --- */
static void channel_scan_run(void);
/* --- Adaptive sampling --- */
static void adaptive_task(void *arg)
{
while (1) {
vTaskDelay(pdMS_TO_TICKS(500));
/* Calibration finalization (NVS I/O happens here, not in WiFi callback) */
if (s_calibrating && s_calib_count >= (uint32_t)s_calib_target) {
int nsub = s_calib_nsub;
uint32_t count = s_calib_count;
for (int i = 0; i < nsub; i++) {
s_baseline_amps[i] = s_calib_accum[i] / (float)count;
}
s_baseline_nsub = nsub;
config_save_blob("bl_amps", s_baseline_amps, nsub * sizeof(float));
config_save_i8("bl_nsub", (int8_t)nsub);
char event[128];
int len = snprintf(event, sizeof(event),
"EVENT,%s,calibrate=done packets=%lu nsub=%d",
s_hostname, (unsigned long)count, nsub);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, event, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
ESP_LOGI(TAG, "Calibration done: %lu packets, %d subcarriers",
(unsigned long)count, nsub);
s_calibrating = false;
}
/* Presence event emission */
if (s_presence_enabled && s_baseline_nsub > 0 && s_pr_score_idx >= PRESENCE_WINDOW) {
float sum = 0.0f;
for (int i = 0; i < PRESENCE_WINDOW; i++) {
sum += s_pr_scores[i];
}
float mean_score = sum / PRESENCE_WINDOW;
s_pr_last_score = mean_score;
bool detected = (mean_score > s_pr_threshold);
if (detected != s_presence_detected) {
int64_t now = esp_timer_get_time();
if (now - s_last_presence_time > PRESENCE_HOLDOFF_US) {
s_presence_detected = detected;
s_last_presence_time = now;
char event[128];
int len = snprintf(event, sizeof(event),
"EVENT,%s,presence=%d score=%.4f",
s_hostname, detected ? 1 : 0, mean_score);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, event, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
ESP_LOGI(TAG, "Presence: %s (score=%.4f threshold=%.4f)",
detected ? "detected" : "cleared", mean_score, s_pr_threshold);
}
}
}
/* Periodic channel scanning */
if (s_chanscan_enabled && !s_chanscan_active) {
int64_t now = esp_timer_get_time();
int64_t interval_us = (int64_t)s_chanscan_interval_s * 1000000LL;
if (s_chanscan_last == 0 || (now - s_chanscan_last) >= interval_us) {
channel_scan_run();
}
}
/* Alert threshold checks */
{
int64_t now_alert = esp_timer_get_time();
/* Heap alert */
if (s_alert_heap_thresh > 0) {
uint32_t free_heap = esp_get_free_heap_size();
if (free_heap < s_alert_heap_thresh &&
(now_alert - s_alert_heap_last) > ALERT_HOLDOFF_US) {
s_alert_heap_last = now_alert;
char event[128];
int len = snprintf(event, sizeof(event),
"EVENT,%s,alert=heap free=%lu thresh=%lu",
s_hostname, (unsigned long)free_heap,
(unsigned long)s_alert_heap_thresh);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, event, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
ESP_LOGW(TAG, "ALERT: heap low (%lu < %lu)",
(unsigned long)free_heap, (unsigned long)s_alert_heap_thresh);
}
}
/* Temperature alert */
#if SOC_TEMP_SENSOR_SUPPORTED
if (s_alert_temp_thresh > 0.0f && s_temp_handle) {
float temp = 0.0f;
if (temperature_sensor_get_celsius(s_temp_handle, &temp) == ESP_OK &&
temp > s_alert_temp_thresh &&
(now_alert - s_alert_temp_last) > ALERT_HOLDOFF_US) {
s_alert_temp_last = now_alert;
char event[128];
int len = snprintf(event, sizeof(event),
"EVENT,%s,alert=temp value=%.1f thresh=%.1f",
s_hostname, temp, s_alert_temp_thresh);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, event, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
ESP_LOGW(TAG, "ALERT: temp high (%.1f > %.1f)",
temp, s_alert_temp_thresh);
}
}
#endif
}
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[128];
int len = snprintf(event, sizeof(event),
"EVENT,%s,motion=%d rate=%d wander=%.6f",
s_hostname, 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);
}
}
}
/* --- Channel scanning --- */
static void channel_scan_run(void)
{
if (s_chanscan_active) return;
s_chanscan_active = true;
/* Get current AP info for return */
wifi_ap_record_t ap;
uint8_t home_channel = 1;
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
home_channel = ap.primary;
}
ESP_LOGI(TAG, "CHANSCAN: starting scan, home channel=%d", home_channel);
/* Stop ping to pause CSI during scan */
if (s_ping_handle) {
esp_ping_stop(s_ping_handle);
esp_ping_delete_session(s_ping_handle);
s_ping_handle = NULL;
}
/* Hop through channels 1-13 */
for (int ch = 1; ch <= CHANSCAN_CHANNELS; ch++) {
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
vTaskDelay(pdMS_TO_TICKS(CHANSCAN_DWELL_MS));
}
/* Return to AP channel */
esp_wifi_set_channel(home_channel, WIFI_SECOND_CHAN_NONE);
/* Restart ping */
wifi_ping_router_start();
s_chanscan_last = esp_timer_get_time();
s_chanscan_active = false;
/* Emit completion event */
char event[128];
int len = snprintf(event, sizeof(event),
"EVENT,%s,chanscan=done channels=%d",
s_hostname, CHANSCAN_CHANNELS);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, event, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
ESP_LOGI(TAG, "CHANSCAN: complete, returned to channel %d", home_channel);
}
/* --- OTA --- */
static void ota_task(void *arg)
{
char *url = (char *)arg;
ESP_LOGI(TAG, "OTA: downloading from %s", url);
s_led_mode = LED_OTA;
esp_http_client_config_t http_cfg = {
.url = url,
.timeout_ms = 30000,
};
esp_https_ota_config_t ota_cfg = {
.http_config = &http_cfg,
};
esp_err_t err = esp_https_ota(&ota_cfg);
free(url);
if (err == ESP_OK) {
ESP_LOGI(TAG, "OTA: success, rebooting...");
s_led_mode = LED_SOLID;
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
} else {
ESP_LOGE(TAG, "OTA: failed: %s", esp_err_to_name(err));
s_led_mode = LED_SLOW_BLINK;
s_ota_in_progress = false;
}
vTaskDelete(NULL);
}
/* --- Promiscuous mode: deauth/disassoc detection + probe request capture --- */
typedef struct {
uint16_t frame_ctrl;
uint16_t duration;
uint8_t addr1[6]; /* destination */
uint8_t addr2[6]; /* source */
uint8_t addr3[6]; /* BSSID */
uint16_t seq_ctrl;
} __attribute__((packed)) wifi_ieee80211_mac_hdr_t;
/* Probe request deduplication: report each MAC at most once per N seconds */
#define PROBE_DEDUP_SIZE 32
/* s_probe_dedup_us declared in globals section (before config_load_nvs) */
static struct {
uint8_t mac[6];
int64_t ts;
} s_probe_seen[PROBE_DEDUP_SIZE];
static bool probe_dedup_check(const uint8_t *mac)
{
int64_t now = esp_timer_get_time();
int oldest_idx = 0;
int64_t oldest_ts = INT64_MAX;
for (int i = 0; i < PROBE_DEDUP_SIZE; i++) {
if (memcmp(s_probe_seen[i].mac, mac, 6) == 0) {
if (now - s_probe_seen[i].ts < s_probe_dedup_us) {
return true; /* seen recently, skip */
}
s_probe_seen[i].ts = now;
return false; /* cooldown expired */
}
if (s_probe_seen[i].ts < oldest_ts) {
oldest_ts = s_probe_seen[i].ts;
oldest_idx = i;
}
}
/* New MAC — replace oldest entry */
memcpy(s_probe_seen[oldest_idx].mac, mac, 6);
s_probe_seen[oldest_idx].ts = now;
return false;
}
static int deauth_flood_check(void)
{
int64_t now = esp_timer_get_time();
int64_t window_us = (int64_t)s_flood_window_s * 1000000LL;
/* Record this event */
s_deauth_ring[s_deauth_ring_head].ts = now;
s_deauth_ring_head = (s_deauth_ring_head + 1) % FLOOD_RING_SIZE;
if (s_deauth_ring_count < FLOOD_RING_SIZE) {
s_deauth_ring_count++;
}
/* Count events within window */
int count = 0;
for (int i = 0; i < s_deauth_ring_count; i++) {
if (now - s_deauth_ring[i].ts <= window_us) {
count++;
}
}
return count;
}
static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type)
{
if (type != WIFI_PKT_MGMT) return;
const wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
const wifi_ieee80211_mac_hdr_t *hdr = (wifi_ieee80211_mac_hdr_t *)pkt->payload;
uint8_t subtype = (hdr->frame_ctrl >> 4) & 0x0F;
/* Deauth (0x0C) / Disassoc (0x0A) */
if (subtype == 0x0C || subtype == 0x0A) {
const char *type_str = (subtype == 0x0C) ? "deauth" : "disassoc";
int flood_count = deauth_flood_check();
char alert[192];
int len;
if (flood_count >= s_flood_thresh) {
/* Flood detected — send aggregate alert, suppress individual */
int64_t now = esp_timer_get_time();
if (!s_flood_active || (now - s_flood_alert_ts > 5000000LL)) {
/* Send flood alert at most every 5 seconds */
s_flood_active = true;
s_flood_alert_ts = now;
len = snprintf(alert, sizeof(alert),
"ALERT_DATA,%s,deauth_flood,%d,%d\n",
s_hostname, flood_count, s_flood_window_s);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, alert, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
ESP_LOGW(TAG, "ALERT: deauth_flood count=%d window=%ds", flood_count, s_flood_window_s);
}
} else {
/* Below threshold — send individual alert */
if (s_flood_active) {
s_flood_active = false;
}
len = snprintf(alert, sizeof(alert),
"ALERT_DATA,%s,%s,"
"%02x:%02x:%02x:%02x:%02x:%02x,"
"%02x:%02x:%02x:%02x:%02x:%02x,"
"%d\n",
s_hostname, type_str,
hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
hdr->addr2[3], hdr->addr2[4], hdr->addr2[5],
hdr->addr1[0], hdr->addr1[1], hdr->addr1[2],
hdr->addr1[3], hdr->addr1[4], hdr->addr1[5],
pkt->rx_ctrl.rssi);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, alert, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
ESP_LOGW(TAG, "ALERT: %s from " MACSTR " -> " MACSTR " rssi=%d",
type_str,
hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
hdr->addr2[3], hdr->addr2[4], hdr->addr2[5],
hdr->addr1[0], hdr->addr1[1], hdr->addr1[2],
hdr->addr1[3], hdr->addr1[4], hdr->addr1[5],
pkt->rx_ctrl.rssi);
}
return;
}
/* Probe request (0x04) */
if (subtype == 0x04) {
/* Dedup: skip if this MAC was reported recently */
if (probe_dedup_check(hdr->addr2)) return;
/* Parse SSID from tagged parameters after MAC header */
const uint8_t *body = pkt->payload + sizeof(wifi_ieee80211_mac_hdr_t);
int body_len = pkt->rx_ctrl.sig_len - sizeof(wifi_ieee80211_mac_hdr_t);
char ssid[33] = "";
if (body_len >= 2 && body[0] == 0) { /* Tag 0 = SSID */
int ssid_len = body[1];
if (ssid_len > 0 && ssid_len <= 32 && ssid_len + 2 <= body_len) {
memcpy(ssid, &body[2], ssid_len);
ssid[ssid_len] = '\0';
}
}
char probe[192];
int len = snprintf(probe, sizeof(probe),
"PROBE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%d\n",
s_hostname,
hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
hdr->addr2[3], hdr->addr2[4], hdr->addr2[5],
pkt->rx_ctrl.rssi,
ssid,
pkt->rx_ctrl.channel);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, probe, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
}
}
static void wifi_promiscuous_init(void)
{
wifi_promiscuous_filter_t filt = {
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT,
};
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filt));
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(wifi_promiscuous_cb));
ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
ESP_LOGI(TAG, "Promiscuous mode: deauth/disassoc/probe detection enabled");
}
/* --- HMAC command authentication --- */
/**
* Verify HMAC-signed command. Format: "HMAC:<16hex>:<cmd>"
* Returns pointer to actual command on success, or NULL on failure
* (with error message written to reply).
*/
static const char *auth_verify(const char *input, char *reply, size_t reply_size)
{
/* No secret configured — accept everything */
if (s_auth_secret[0] == '\0') {
return input;
}
/* Check for HMAC: prefix */
if (strncmp(input, "HMAC:", 5) != 0) {
snprintf(reply, reply_size, "ERR AUTH required");
return NULL;
}
/* Find second colon after 16 hex chars */
if (strlen(input) < 5 + 16 + 1) {
snprintf(reply, reply_size, "ERR AUTH malformed");
return NULL;
}
if (input[5 + 16] != ':') {
snprintf(reply, reply_size, "ERR AUTH malformed");
return NULL;
}
const char *cmd = input + 5 + 16 + 1;
/* Compute HMAC-SHA256 of the command */
uint8_t hmac[32];
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
mbedtls_md_setup(&ctx, md_info, 1);
mbedtls_md_hmac_starts(&ctx, (const uint8_t *)s_auth_secret, strlen(s_auth_secret));
mbedtls_md_hmac_update(&ctx, (const uint8_t *)cmd, strlen(cmd));
mbedtls_md_hmac_finish(&ctx, hmac);
mbedtls_md_free(&ctx);
/* Format first 8 bytes as 16 hex chars */
char expected[17];
for (int i = 0; i < 8; i++) {
snprintf(expected + i * 2, 3, "%02x", hmac[i]);
}
if (strncmp(input + 5, expected, 16) != 0) {
snprintf(reply, reply_size, "ERR AUTH failed");
return NULL;
}
return cmd;
}
/* --- Command handler --- */
static void reboot_after_delay(void *arg)
{
vTaskDelay(pdMS_TO_TICKS(200));
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;
bool saved_powersave = s_powersave;
/* Disable adaptive during test */
s_adaptive = false;
s_motion_detected = false;
/* Disable powersave during test */
if (s_powersave) {
s_powersave = false;
esp_wifi_set_ps(WIFI_PS_NONE);
}
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();
s_powersave = saved_powersave;
esp_wifi_set_ps(s_powersave ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE);
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 */
if (strncmp(cmd, "REBOOT", 6) == 0) {
snprintf(reply, reply_size, "OK REBOOTING");
xTaskCreate(reboot_after_delay, "reboot", 1024, NULL, 1, NULL);
return strlen(reply);
}
/* IDENTIFY */
if (strncmp(cmd, "IDENTIFY", 8) == 0) {
s_identify_end_time = esp_timer_get_time() + (5 * 1000000LL);
s_led_mode = LED_SOLID;
snprintf(reply, reply_size, "OK IDENTIFY 5s");
return strlen(reply);
}
/* LED [QUIET|AUTO] */
if (strncmp(cmd, "LED", 3) == 0) {
if (cmd[3] == '\0' || cmd[3] == '\n') {
snprintf(reply, reply_size, "OK LED %s", s_led_quiet ? "quiet" : "auto");
return strlen(reply);
}
if (strncmp(cmd + 4, "QUIET", 5) == 0) {
s_led_quiet = true;
s_led_mode = LED_OFF;
config_save_i8("led_quiet", 1);
snprintf(reply, reply_size, "OK LED quiet (off, solid on motion)");
return strlen(reply);
}
if (strncmp(cmd + 4, "AUTO", 4) == 0) {
s_led_quiet = false;
s_led_mode = LED_SLOW_BLINK;
config_save_i8("led_quiet", 0);
snprintf(reply, reply_size, "OK LED auto (blink)");
return strlen(reply);
}
snprintf(reply, reply_size, "ERR LED [QUIET|AUTO]");
return strlen(reply);
}
/* STATUS */
if (strncmp(cmd, "STATUS", 6) == 0) {
int64_t up = esp_timer_get_time() / 1000000LL;
int days = (int)(up / 86400);
int hours = (int)((up % 86400) / 3600);
int mins = (int)((up % 3600) / 60);
uint32_t heap = esp_get_free_heap_size();
wifi_ap_record_t ap;
int rssi = 0;
int channel = 0;
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
rssi = ap.rssi;
channel = ap.primary;
}
const esp_app_desc_t *app_desc = esp_app_get_description();
float chip_temp = 0.0f;
#if SOC_TEMP_SENSOR_SUPPORTED
if (s_temp_handle) {
temperature_sensor_get_celsius(s_temp_handle, &chip_temp);
}
#endif
int actual_rate = (up > 0) ? (int)((uint64_t)s_csi_count / (uint64_t)up) : 0;
char uptime_str[32];
if (days > 0) {
snprintf(uptime_str, sizeof(uptime_str), "%dd%dh%dm", days, hours, mins);
} else if (hours > 0) {
snprintf(uptime_str, sizeof(uptime_str), "%dh%dm", hours, mins);
} else {
snprintf(uptime_str, sizeof(uptime_str), "%dm", mins);
}
const char *csi_mode_str = (s_csi_mode == CSI_MODE_COMPACT) ? "compact" :
(s_csi_mode == CSI_MODE_HYBRID) ? "hybrid" : "raw";
/* NVS stats */
nvs_stats_t nvs_stats = {0};
nvs_get_stats("nvs", &nvs_stats);
/* Partition info */
const esp_partition_t *running = esp_ota_get_running_partition();
uint32_t part_size = running ? running->size : 0;
/* Chip info */
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
const char *chip_model = (chip_info.model == CHIP_ESP32S3) ? "ESP32S3" :
(chip_info.model == CHIP_ESP32C3) ? "ESP32C3" :
(chip_info.model == CHIP_ESP32) ? "ESP32" : "ESP32xx";
snprintf(reply, reply_size,
"OK STATUS uptime=%s uptime_s=%lld heap=%lu rssi=%d channel=%d tx_power=%d rate=%d csi_rate=%d"
" hostname=%s version=%s adaptive=%s motion=%d ble=%s target=%s:%d"
" temp=%.1f csi_count=%lu boots=%lu rssi_min=%d rssi_max=%d"
" csi=%s csi_mode=%s hybrid_n=%d auth=%s flood_thresh=%d/%d powersave=%s"
" presence=%s pr_score=%.4f chanscan=%s led=%s"
" alert_temp=%.1f alert_heap=%lu"
" nvs_used=%lu nvs_free=%lu nvs_total=%lu part_size=%lu"
" built=%s_%s idf=%s chip=%sr%dc%d",
uptime_str, (long long)up, (unsigned long)heap, rssi, channel, (int)s_tx_power_dbm,
s_send_frequency, actual_rate,
s_hostname, app_desc->version,
s_adaptive ? "on" : "off", s_motion_detected ? 1 : 0,
s_ble_enabled ? "on" : "off", s_target_ip, s_target_port,
chip_temp, (unsigned long)s_csi_count, (unsigned long)s_boot_count,
(int)s_rssi_min, (int)s_rssi_max,
s_csi_enabled ? "on" : "off", csi_mode_str, s_hybrid_interval,
s_auth_secret[0] ? "on" : "off",
s_flood_thresh, s_flood_window_s,
s_powersave ? "on" : "off",
s_presence_enabled ? "on" : "off", s_pr_last_score,
s_chanscan_enabled ? "on" : "off", s_led_quiet ? "quiet" : "auto",
s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh,
(unsigned long)nvs_stats.used_entries,
(unsigned long)nvs_stats.free_entries,
(unsigned long)nvs_stats.total_entries,
(unsigned long)part_size,
app_desc->date, app_desc->time, app_desc->idf_ver,
chip_model, chip_info.revision, chip_info.cores);
return strlen(reply);
}
/* RATE <10-100> */
if (strncmp(cmd, "RATE ", 5) == 0) {
int val = atoi(cmd + 5);
if (val < 10 || val > 100) {
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 (adaptive off)", val);
return strlen(reply);
}
/* POWER <2-20> */
if (strncmp(cmd, "POWER ", 6) == 0) {
int val = atoi(cmd + 6);
if (val < 2 || val > 20) {
snprintf(reply, reply_size, "ERR POWER range 2-20");
return strlen(reply);
}
s_tx_power_dbm = (int8_t)val;
esp_wifi_set_max_tx_power(s_tx_power_dbm * 4);
config_save_i8("tx_power", s_tx_power_dbm);
snprintf(reply, reply_size, "OK POWER %d dBm", val);
return strlen(reply);
}
/* TARGET <ip> [port] */
if (strncmp(cmd, "TARGET ", 7) == 0) {
char ip_buf[16] = {0};
int port = s_target_port;
/* parse: "TARGET 192.168.1.10" or "TARGET 192.168.1.10 5500" */
if (sscanf(cmd + 7, "%15s %d", ip_buf, &port) < 1) {
snprintf(reply, reply_size, "ERR TARGET <ip> [port]");
return strlen(reply);
}
/* validate IP */
struct in_addr test_addr;
if (inet_pton(AF_INET, ip_buf, &test_addr) != 1) {
snprintf(reply, reply_size, "ERR TARGET invalid IP");
return strlen(reply);
}
if (port < 1 || port > 65535) {
snprintf(reply, reply_size, "ERR TARGET port range 1-65535");
return strlen(reply);
}
strncpy(s_target_ip, ip_buf, sizeof(s_target_ip) - 1);
s_target_ip[sizeof(s_target_ip) - 1] = '\0';
s_target_port = (uint16_t)port;
/* update live socket destination */
s_dest_addr.sin_port = htons(s_target_port);
inet_pton(AF_INET, s_target_ip, &s_dest_addr.sin_addr);
/* persist */
config_save_str("target_ip", s_target_ip);
config_save_i32("target_port", (int32_t)s_target_port);
snprintf(reply, reply_size, "OK TARGET %s:%d", s_target_ip, s_target_port);
return strlen(reply);
}
/* HOSTNAME <name> */
if (strncmp(cmd, "HOSTNAME ", 9) == 0) {
const char *name = cmd + 9;
size_t nlen = strlen(name);
if (nlen == 0 || nlen >= sizeof(s_hostname)) {
snprintf(reply, reply_size, "ERR HOSTNAME length 1-%d", (int)sizeof(s_hostname) - 1);
return strlen(reply);
}
strncpy(s_hostname, name, sizeof(s_hostname) - 1);
s_hostname[sizeof(s_hostname) - 1] = '\0';
config_save_str("hostname", s_hostname);
mdns_hostname_set(s_hostname);
snprintf(reply, reply_size, "OK HOSTNAME %s (mDNS updated, reboot recommended)", s_hostname);
return strlen(reply);
}
if (strcmp(cmd, "HOSTNAME") == 0) {
snprintf(reply, reply_size, "OK HOSTNAME %s", s_hostname);
return strlen(reply);
}
/* BLE ON/OFF */
if (strncmp(cmd, "BLE ", 4) == 0) {
const char *arg = cmd + 4;
if (strncmp(arg, "ON", 2) == 0) {
s_ble_enabled = true;
config_save_i8("ble_scan", 1);
ble_scan_start();
snprintf(reply, reply_size, "OK BLE scanning on");
} else if (strncmp(arg, "OFF", 3) == 0) {
s_ble_enabled = false;
config_save_i8("ble_scan", 0);
ble_gap_disc_cancel();
snprintf(reply, reply_size, "OK BLE scanning off");
} else {
snprintf(reply, reply_size, "ERR BLE ON or OFF");
}
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);
}
/* SCANRATE <5-300> */
if (strncmp(cmd, "SCANRATE ", 9) == 0) {
int val = atoi(cmd + 9);
if (val < 5 || val > 300) {
snprintf(reply, reply_size, "ERR SCANRATE range 5-300 seconds");
return strlen(reply);
}
s_ble_scan_interval_us = (int64_t)val * 1000000LL;
config_save_i32("scan_rate", (int32_t)val);
if (s_ble_timer) {
esp_timer_stop(s_ble_timer);
esp_timer_start_periodic(s_ble_timer, s_ble_scan_interval_us);
}
snprintf(reply, reply_size, "OK SCANRATE %ds (saved)", val);
return strlen(reply);
}
/* PROBERATE <1-300> */
if (strncmp(cmd, "PROBERATE ", 10) == 0) {
int val = atoi(cmd + 10);
if (val < 1 || val > 300) {
snprintf(reply, reply_size, "ERR PROBERATE range 1-300 seconds");
return strlen(reply);
}
s_probe_dedup_us = (int64_t)val * 1000000LL;
config_save_i32("probe_rate", (int32_t)val);
snprintf(reply, reply_size, "OK PROBERATE %ds (saved)", val);
return strlen(reply);
}
/* CSI [ON|OFF] - enable/disable CSI collection */
if (strcmp(cmd, "CSI") == 0) {
snprintf(reply, reply_size, "OK CSI %s", s_csi_enabled ? "on" : "off");
return strlen(reply);
}
if (strncmp(cmd, "CSI ", 4) == 0) {
const char *arg = cmd + 4;
if (strncmp(arg, "ON", 2) == 0) {
s_csi_enabled = true;
config_save_i8("csi_enabled", 1);
wifi_ping_router_start();
snprintf(reply, reply_size, "OK CSI on");
} else if (strncmp(arg, "OFF", 3) == 0) {
s_csi_enabled = false;
config_save_i8("csi_enabled", 0);
snprintf(reply, reply_size, "OK CSI off (probe capture active)");
} else {
snprintf(reply, reply_size, "ERR CSI ON|OFF");
}
return strlen(reply);
}
/* CSIMODE [RAW|COMPACT|HYBRID N] */
if (strcmp(cmd, "CSIMODE") == 0) {
const char *mode_str = (s_csi_mode == CSI_MODE_COMPACT) ? "COMPACT" :
(s_csi_mode == CSI_MODE_HYBRID) ? "HYBRID" : "RAW";
if (s_csi_mode == CSI_MODE_HYBRID) {
snprintf(reply, reply_size, "OK CSIMODE %s %d", mode_str, s_hybrid_interval);
} else {
snprintf(reply, reply_size, "OK CSIMODE %s", mode_str);
}
return strlen(reply);
}
if (strncmp(cmd, "CSIMODE ", 8) == 0) {
const char *arg = cmd + 8;
if (strncmp(arg, "RAW", 3) == 0) {
s_csi_mode = CSI_MODE_RAW;
config_save_i8("csi_mode", (int8_t)CSI_MODE_RAW);
snprintf(reply, reply_size, "OK CSIMODE RAW");
} else if (strncmp(arg, "COMPACT", 7) == 0) {
s_csi_mode = CSI_MODE_COMPACT;
config_save_i8("csi_mode", (int8_t)CSI_MODE_COMPACT);
snprintf(reply, reply_size, "OK CSIMODE COMPACT");
} else if (strncmp(arg, "HYBRID", 6) == 0) {
int n = 10;
if (arg[6] == ' ') {
n = atoi(arg + 7);
}
if (n < 1 || n > 100) {
snprintf(reply, reply_size, "ERR CSIMODE HYBRID N range 1-100");
return strlen(reply);
}
s_csi_mode = CSI_MODE_HYBRID;
s_hybrid_interval = n;
config_save_i8("csi_mode", (int8_t)CSI_MODE_HYBRID);
config_save_i32("hybrid_n", (int32_t)n);
snprintf(reply, reply_size, "OK CSIMODE HYBRID %d", n);
} else {
snprintf(reply, reply_size, "ERR CSIMODE RAW|COMPACT|HYBRID [N]");
}
return strlen(reply);
}
/* PROFILE */
if (strcmp(cmd, "PROFILE") == 0) {
int pos = 0;
/* Heap info */
size_t free_heap = esp_get_free_heap_size();
size_t min_heap = esp_get_minimum_free_heap_size();
size_t free_dram = heap_caps_get_free_size(MALLOC_CAP_8BIT);
size_t total_dram = heap_caps_get_total_size(MALLOC_CAP_8BIT);
size_t free_iram = heap_caps_get_free_size(MALLOC_CAP_IRAM_8BIT);
pos += snprintf(reply + pos, reply_size - pos,
"OK PROFILE\nHEAP free=%u min=%u dram=%u/%u iram=%u\n",
(unsigned)free_heap, (unsigned)min_heap,
(unsigned)free_dram, (unsigned)total_dram, (unsigned)free_iram);
/* Per-task stack watermarks */
pos += snprintf(reply + pos, reply_size - pos, "TASKS\n");
const char *task_names[] = {"led_task", "cmd_task", "adaptive", "ble_host", "main", NULL};
for (int i = 0; task_names[i] != NULL && pos < (int)reply_size - 60; i++) {
TaskHandle_t th = xTaskGetHandle(task_names[i]);
if (th) {
UBaseType_t hwm = uxTaskGetStackHighWaterMark(th);
pos += snprintf(reply + pos, reply_size - pos,
" %-12s stack_free=%u\n", task_names[i], (unsigned)(hwm * sizeof(StackType_t)));
}
}
#if defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS) && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
/* CPU runtime stats */
UBaseType_t n = uxTaskGetNumberOfTasks();
TaskStatus_t *tasks = malloc(n * sizeof(TaskStatus_t));
if (tasks) {
uint32_t total_time;
n = uxTaskGetSystemState(tasks, n, &total_time);
if (total_time > 0) {
pos += snprintf(reply + pos, reply_size - pos, "CPU\n");
for (UBaseType_t i = 0; i < n && pos < (int)reply_size - 60; i++) {
uint32_t pct = (tasks[i].ulRunTimeCounter * 100) / total_time;
if (pct > 0 || tasks[i].ulRunTimeCounter > 0) {
pos += snprintf(reply + pos, reply_size - pos,
" %-12s %3lu%%\n", tasks[i].pcTaskName, (unsigned long)pct);
}
}
}
free(tasks);
}
#endif
return pos;
}
/* FLOODTHRESH [count [window_s]] */
if (strcmp(cmd, "FLOODTHRESH") == 0) {
snprintf(reply, reply_size, "OK FLOODTHRESH %d/%ds", s_flood_thresh, s_flood_window_s);
return strlen(reply);
}
if (strncmp(cmd, "FLOODTHRESH ", 12) == 0) {
int count = 0, window = s_flood_window_s;
if (sscanf(cmd + 12, "%d %d", &count, &window) < 1 || count < 1 || count > 100) {
snprintf(reply, reply_size, "ERR FLOODTHRESH <1-100> [window_s 1-300]");
return strlen(reply);
}
if (window < 1 || window > 300) {
snprintf(reply, reply_size, "ERR FLOODTHRESH window range 1-300");
return strlen(reply);
}
s_flood_thresh = count;
s_flood_window_s = window;
config_save_i32("flood_thresh", (int32_t)count);
config_save_i32("flood_window", (int32_t)window);
snprintf(reply, reply_size, "OK FLOODTHRESH %d/%ds", s_flood_thresh, s_flood_window_s);
return strlen(reply);
}
/* AUTH [secret|OFF] */
if (strcmp(cmd, "AUTH") == 0) {
snprintf(reply, reply_size, "OK AUTH %s", s_auth_secret[0] ? "on" : "off");
return strlen(reply);
}
if (strncmp(cmd, "AUTH ", 5) == 0) {
const char *arg = cmd + 5;
if (strcmp(arg, "OFF") == 0) {
s_auth_secret[0] = '\0';
config_save_str("auth_secret", "");
snprintf(reply, reply_size, "OK AUTH off");
} else {
size_t alen = strlen(arg);
if (alen < 8 || alen > 64) {
snprintf(reply, reply_size, "ERR AUTH secret length 8-64");
return strlen(reply);
}
strncpy(s_auth_secret, arg, sizeof(s_auth_secret) - 1);
s_auth_secret[sizeof(s_auth_secret) - 1] = '\0';
config_save_str("auth_secret", s_auth_secret);
snprintf(reply, reply_size, "OK AUTH on");
}
return strlen(reply);
}
/* OTA <url> */
if (strncmp(cmd, "OTA ", 4) == 0) {
const char *url = cmd + 4;
if (strncmp(url, "http://", 7) != 0 && strncmp(url, "https://", 8) != 0) {
snprintf(reply, reply_size, "ERR OTA url must start with http:// or https://");
return strlen(reply);
}
if (s_ota_in_progress) {
snprintf(reply, reply_size, "ERR OTA already in progress");
return strlen(reply);
}
char *url_copy = strdup(url);
if (!url_copy) {
snprintf(reply, reply_size, "ERR OTA out of memory");
return strlen(reply);
}
s_ota_in_progress = true;
xTaskCreate(ota_task, "ota_task", 8192, url_copy, 5, NULL);
snprintf(reply, reply_size, "OK OTA started");
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);
}
/* POWERSAVE [ON|OFF] */
if (strcmp(cmd, "POWERSAVE") == 0) {
snprintf(reply, reply_size, "OK POWERSAVE %s", s_powersave ? "on" : "off");
return strlen(reply);
}
if (strncmp(cmd, "POWERSAVE ", 10) == 0) {
const char *arg = cmd + 10;
if (strncmp(arg, "ON", 2) == 0) {
s_powersave = true;
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
config_save_i8("powersave", 1);
snprintf(reply, reply_size, "OK POWERSAVE on (modem sleep)");
} else if (strncmp(arg, "OFF", 3) == 0) {
s_powersave = false;
esp_wifi_set_ps(WIFI_PS_NONE);
config_save_i8("powersave", 0);
snprintf(reply, reply_size, "OK POWERSAVE off");
} else {
snprintf(reply, reply_size, "ERR POWERSAVE ON or OFF");
}
return strlen(reply);
}
/* CALIBRATE [seconds | STATUS | CLEAR] */
if (strcmp(cmd, "CALIBRATE") == 0 || strncmp(cmd, "CALIBRATE ", 10) == 0) {
const char *arg = (cmd[9] == ' ') ? cmd + 10 : "";
if (strcmp(arg, "STATUS") == 0) {
if (s_calibrating) {
snprintf(reply, reply_size, "OK CALIBRATE in_progress packets=%lu target=%d",
(unsigned long)s_calib_count, s_calib_target);
} else if (s_baseline_nsub > 0) {
snprintf(reply, reply_size, "OK CALIBRATE baseline valid nsub=%d", s_baseline_nsub);
} else {
snprintf(reply, reply_size, "OK CALIBRATE baseline none");
}
return strlen(reply);
}
if (strcmp(arg, "CLEAR") == 0) {
s_baseline_nsub = 0;
s_presence_enabled = false;
s_presence_detected = false;
s_pr_last_score = 0.0f;
config_erase_key("bl_amps");
config_erase_key("bl_nsub");
config_save_i8("presence", 0);
snprintf(reply, reply_size, "OK CALIBRATE cleared (presence off)");
return strlen(reply);
}
if (s_calibrating) {
snprintf(reply, reply_size, "ERR CALIBRATE already running");
return strlen(reply);
}
if (s_csi_count < 200) {
snprintf(reply, reply_size, "ERR CALIBRATE need 200+ CSI packets first (have %lu)",
(unsigned long)s_csi_count);
return strlen(reply);
}
int seconds = 10;
if (arg[0] != '\0') {
seconds = atoi(arg);
}
if (seconds < 3 || seconds > 60) {
snprintf(reply, reply_size, "ERR CALIBRATE range 3-60 seconds");
return strlen(reply);
}
/* Start calibration */
s_calib_count = 0;
s_calib_nsub = 0;
s_calib_target = seconds * s_send_frequency;
if (s_calib_target < 30) s_calib_target = 30;
s_calibrating = true;
snprintf(reply, reply_size, "OK CALIBRATE started %ds target=%d packets", seconds, s_calib_target);
return strlen(reply);
}
/* PRESENCE [ON|OFF|THRESHOLD <val>] */
if (strcmp(cmd, "PRESENCE") == 0) {
snprintf(reply, reply_size, "OK PRESENCE %s baseline=%s threshold=%.4f score=%.4f",
s_presence_enabled ? "on" : "off",
s_baseline_nsub > 0 ? "yes" : "no",
s_pr_threshold, s_pr_last_score);
return strlen(reply);
}
if (strncmp(cmd, "PRESENCE ", 9) == 0) {
const char *arg = cmd + 9;
if (strncmp(arg, "ON", 2) == 0) {
if (s_baseline_nsub == 0) {
snprintf(reply, reply_size, "ERR PRESENCE need baseline (run CALIBRATE first)");
return strlen(reply);
}
s_presence_enabled = true;
s_presence_detected = false;
s_pr_last_score = 0.0f;
s_pr_score_idx = 0;
s_last_presence_time = 0;
config_save_i8("presence", 1);
snprintf(reply, reply_size, "OK PRESENCE on threshold=%.4f", s_pr_threshold);
} else if (strncmp(arg, "OFF", 3) == 0) {
s_presence_enabled = false;
s_presence_detected = false;
s_pr_last_score = 0.0f;
config_save_i8("presence", 0);
snprintf(reply, reply_size, "OK PRESENCE off");
} else if (strncmp(arg, "THRESHOLD ", 10) == 0) {
float val = strtof(arg + 10, NULL);
if (val < 0.001f || val > 1.0f) {
snprintf(reply, reply_size, "ERR PRESENCE THRESHOLD range 0.001-1.0");
return strlen(reply);
}
s_pr_threshold = val;
config_save_i32("pr_thresh", (int32_t)(val * 1000000.0f));
snprintf(reply, reply_size, "OK PRESENCE THRESHOLD %.4f", val);
} else {
snprintf(reply, reply_size, "ERR PRESENCE ON|OFF|THRESHOLD <0.001-1.0>");
}
return strlen(reply);
}
/* CHANSCAN [ON|OFF|NOW|INTERVAL <60-3600>] */
if (strcmp(cmd, "CHANSCAN") == 0) {
snprintf(reply, reply_size, "OK CHANSCAN %s interval=%ds active=%s",
s_chanscan_enabled ? "on" : "off",
s_chanscan_interval_s,
s_chanscan_active ? "yes" : "no");
return strlen(reply);
}
if (strncmp(cmd, "CHANSCAN ", 9) == 0) {
const char *arg = cmd + 9;
if (strncmp(arg, "ON", 2) == 0) {
s_chanscan_enabled = true;
config_save_i8("chanscan", 1);
snprintf(reply, reply_size, "OK CHANSCAN on interval=%ds", s_chanscan_interval_s);
} else if (strncmp(arg, "OFF", 3) == 0) {
s_chanscan_enabled = false;
config_save_i8("chanscan", 0);
snprintf(reply, reply_size, "OK CHANSCAN off");
} else if (strncmp(arg, "NOW", 3) == 0) {
if (s_chanscan_active) {
snprintf(reply, reply_size, "ERR CHANSCAN already in progress");
} else {
channel_scan_run();
snprintf(reply, reply_size, "OK CHANSCAN triggered");
}
} else if (strncmp(arg, "INTERVAL ", 9) == 0) {
int val = atoi(arg + 9);
if (val < 60 || val > 3600) {
snprintf(reply, reply_size, "ERR CHANSCAN INTERVAL range 60-3600 seconds");
return strlen(reply);
}
s_chanscan_interval_s = val;
config_save_i32("chanscan_int", (int32_t)val);
snprintf(reply, reply_size, "OK CHANSCAN INTERVAL %ds (saved)", val);
} else {
snprintf(reply, reply_size, "ERR CHANSCAN ON|OFF|NOW|INTERVAL <60-3600>");
}
return strlen(reply);
}
/* PING — echo reply for connectivity tests */
if (strcmp(cmd, "PING") == 0) {
snprintf(reply, reply_size, "OK PONG");
return strlen(reply);
}
/* LOG <NONE|ERROR|WARN|INFO|DEBUG|VERBOSE> */
if (strncmp(cmd, "LOG ", 4) == 0) {
const char *arg = cmd + 4;
esp_log_level_t level;
if (strcmp(arg, "NONE") == 0) level = ESP_LOG_NONE;
else if (strcmp(arg, "ERROR") == 0) level = ESP_LOG_ERROR;
else if (strcmp(arg, "WARN") == 0) level = ESP_LOG_WARN;
else if (strcmp(arg, "INFO") == 0) level = ESP_LOG_INFO;
else if (strcmp(arg, "DEBUG") == 0) level = ESP_LOG_DEBUG;
else if (strcmp(arg, "VERBOSE") == 0) level = ESP_LOG_VERBOSE;
else {
snprintf(reply, reply_size, "ERR LOG NONE|ERROR|WARN|INFO|DEBUG|VERBOSE");
return strlen(reply);
}
esp_log_level_set("*", level);
snprintf(reply, reply_size, "OK LOG %s", arg);
return strlen(reply);
}
if (strcmp(cmd, "LOG") == 0) {
snprintf(reply, reply_size, "OK LOG (use LOG <NONE|ERROR|WARN|INFO|DEBUG|VERBOSE>)");
return strlen(reply);
}
/* RSSI RESET — reset min/max counters */
if (strcmp(cmd, "RSSI RESET") == 0) {
s_rssi_min = 0;
s_rssi_max = -128;
snprintf(reply, reply_size, "OK RSSI min/max reset");
return strlen(reply);
}
/* ALERT — set temp/heap alert thresholds */
if (strcmp(cmd, "ALERT") == 0) {
snprintf(reply, reply_size, "OK ALERT temp=%.1f heap=%lu (0=off)",
s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh);
return strlen(reply);
}
if (strncmp(cmd, "ALERT ", 6) == 0) {
const char *arg = cmd + 6;
if (strncmp(arg, "TEMP ", 5) == 0) {
float val = strtof(arg + 5, NULL);
if (val < 0 || val > 125) {
snprintf(reply, reply_size, "ERR ALERT TEMP range 0-125 (0=off)");
return strlen(reply);
}
s_alert_temp_thresh = val;
config_save_i32("alert_temp", (int32_t)(val * 10.0f));
snprintf(reply, reply_size, "OK ALERT TEMP %.1f", val);
return strlen(reply);
}
if (strncmp(arg, "HEAP ", 5) == 0) {
int val = atoi(arg + 5);
if (val < 0 || val > 300000) {
snprintf(reply, reply_size, "ERR ALERT HEAP range 0-300000 (0=off)");
return strlen(reply);
}
s_alert_heap_thresh = (uint32_t)val;
config_save_i32("alert_heap", val);
snprintf(reply, reply_size, "OK ALERT HEAP %d", val);
return strlen(reply);
}
if (strcmp(arg, "OFF") == 0) {
s_alert_temp_thresh = 0.0f;
s_alert_heap_thresh = 0;
config_save_i32("alert_temp", 0);
config_save_i32("alert_heap", 0);
snprintf(reply, reply_size, "OK ALERT all disabled");
return strlen(reply);
}
snprintf(reply, reply_size, "ERR ALERT TEMP <c>|HEAP <bytes>|OFF");
return strlen(reply);
}
/* HELP */
if (strcmp(cmd, "HELP") == 0) {
int pos = 0;
pos += snprintf(reply + pos, reply_size - pos,
"OK HELP\n"
"STATUS CONFIG PROFILE PING HELP\n"
"RATE <10-100> POWER <2-20> TARGET <ip> [port]\n"
"HOSTNAME [name] CSI [ON|OFF] CSIMODE [RAW|COMPACT|HYBRID N]\n"
"ADAPTIVE [ON|OFF] THRESHOLD <0-1>\n"
"BLE [ON|OFF] SCANRATE <5-300> PROBERATE <1-300>\n"
"CALIBRATE [STATUS|CLEAR|N] PRESENCE [ON|OFF|THRESHOLD]\n"
"CHANSCAN [ON|OFF|NOW|INTERVAL] LED [QUIET|AUTO]\n"
"POWERSAVE [ON|OFF] AUTH [secret|OFF] FLOODTHRESH <n> [win]\n"
"ALERT [TEMP <c>|HEAP <bytes>|OFF] LOG <level> RSSI RESET\n"
"OTA <url> POWERTEST [dwell] IDENTIFY REBOOT FACTORY");
return pos;
}
/* FACTORY — erase all NVS config and reboot */
if (strcmp(cmd, "FACTORY") == 0) {
snprintf(reply, reply_size, "OK FACTORY erasing config and rebooting");
/* Send reply before erasing */
nvs_handle_t fh;
if (nvs_open("csi_config", NVS_READWRITE, &fh) == ESP_OK) {
nvs_erase_all(fh);
nvs_commit(fh);
nvs_close(fh);
}
xTaskCreate(reboot_after_delay, "reboot", 1024, NULL, 1, NULL);
return strlen(reply);
}
/* CONFIG — dump all NVS settings */
if (strcmp(cmd, "CONFIG") == 0) {
const char *csi_mode_str = (s_csi_mode == CSI_MODE_COMPACT) ? "compact" :
(s_csi_mode == CSI_MODE_HYBRID) ? "hybrid" : "raw";
int pos = 0;
pos += snprintf(reply + pos, reply_size - pos,
"OK CONFIG\n"
"hostname=%s\n"
"send_rate=%d\n"
"tx_power=%d\n"
"target=%s:%d\n"
"csi=%s\n"
"csi_mode=%s\n"
"hybrid_n=%d\n"
"adaptive=%s\n"
"threshold=%.6f\n"
"ble=%s\n"
"scan_rate=%ds\n"
"probe_rate=%ds\n"
"presence=%s\n"
"pr_thresh=%.4f\n"
"baseline_nsub=%d\n"
"chanscan=%s\n"
"chanscan_int=%ds\n"
"led=%s\n"
"powersave=%s\n"
"auth=%s\n"
"flood_thresh=%d/%ds\n"
"alert_temp=%.1f\n"
"alert_heap=%lu\n"
"boots=%lu",
s_hostname,
s_send_frequency,
(int)s_tx_power_dbm,
s_target_ip, s_target_port,
s_csi_enabled ? "on" : "off",
csi_mode_str,
s_hybrid_interval,
s_adaptive ? "on" : "off",
s_motion_threshold,
s_ble_enabled ? "on" : "off",
(int)(s_ble_scan_interval_us / 1000000LL),
(int)(s_probe_dedup_us / 1000000LL),
s_presence_enabled ? "on" : "off",
s_pr_threshold,
s_baseline_nsub,
s_chanscan_enabled ? "on" : "off",
s_chanscan_interval_s,
s_led_quiet ? "quiet" : "auto",
s_powersave ? "on" : "off",
s_auth_secret[0] ? "on" : "off",
s_flood_thresh, s_flood_window_s,
s_alert_temp_thresh, (unsigned long)s_alert_heap_thresh,
(unsigned long)s_boot_count);
return pos;
}
snprintf(reply, reply_size, "ERR UNKNOWN");
return strlen(reply);
}
static void cmd_task(void *arg)
{
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0) {
ESP_LOGE(TAG, "cmd_task: socket failed: errno %d", errno);
vTaskDelete(NULL);
return;
}
struct sockaddr_in bind_addr = {
.sin_family = AF_INET,
.sin_port = htons(CONFIG_CSI_CMD_PORT),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
if (bind(sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
ESP_LOGE(TAG, "cmd_task: bind failed: errno %d", errno);
close(sock);
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "Command listener on UDP port %d", CONFIG_CSI_CMD_PORT);
char rx_buf[192];
char reply_buf[1400];
struct sockaddr_in src_addr;
socklen_t src_len;
while (1) {
src_len = sizeof(src_addr);
int len = recvfrom(sock, rx_buf, sizeof(rx_buf) - 1, 0,
(struct sockaddr *)&src_addr, &src_len);
if (len < 0) {
ESP_LOGE(TAG, "cmd_task: recvfrom error: errno %d", errno);
vTaskDelay(pdMS_TO_TICKS(1000));
continue;
}
/* Strip trailing whitespace */
while (len > 0 && (rx_buf[len - 1] == '\n' || rx_buf[len - 1] == '\r' || rx_buf[len - 1] == ' ')) {
len--;
}
rx_buf[len] = '\0';
ESP_LOGI(TAG, "CMD rx: \"%s\"", rx_buf);
const char *verified = auth_verify(rx_buf, reply_buf, sizeof(reply_buf));
int reply_len;
if (verified) {
reply_len = cmd_handle(verified, reply_buf, sizeof(reply_buf));
} else {
reply_len = strlen(reply_buf);
}
sendto(sock, reply_buf, reply_len, 0,
(struct sockaddr *)&src_addr, src_len);
ESP_LOGI(TAG, "CMD tx: \"%s\"", reply_buf);
}
}
/* --- WiFi event handler --- */
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
s_wifi_connected = false;
ESP_LOGW(TAG, "WiFi disconnected");
}
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
bool was_disconnected = !s_wifi_connected;
s_wifi_connected = true;
if (was_disconnected && s_udp_socket >= 0) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
wifi_ap_record_t ap;
int rssi = 0;
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
rssi = ap.rssi;
}
char evt[128];
int len = snprintf(evt, sizeof(evt),
"EVENT,%s,wifi=reconnected rssi=%d ip=" IPSTR "\n",
s_hostname, rssi, IP2STR(&event->ip_info.ip));
sendto(s_udp_socket, evt, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
ESP_LOGI(TAG, "WiFi reconnected, event sent");
}
}
}
/* --- Main --- */
void app_main()
{
ESP_ERROR_CHECK(nvs_flash_init());
config_load_nvs();
led_gpio_init();
xTaskCreate(led_task, "led_task", 2048, NULL, 2, NULL);
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
/* Register WiFi event handler for reconnect notifications */
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, wifi_event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, NULL));
/**
* @brief This helper function configures Wi-Fi, as selected in menuconfig.
* Read "Establishing Wi-Fi Connection" section in esp-idf/examples/protocols/README.md
* for more information about this function.
*/
ESP_ERROR_CHECK(example_connect());
s_wifi_connected = true;
/* Apply saved TX power after WiFi is up */
esp_wifi_set_max_tx_power(s_tx_power_dbm * 4);
ESP_LOGI(TAG, "TX power set to %d dBm", (int)s_tx_power_dbm);
/* Power management: DFS (240→80 MHz) + light sleep */
esp_pm_config_t pm_config = {
.max_freq_mhz = 240,
.min_freq_mhz = 80,
.light_sleep_enable = true,
};
esp_err_t pm_err = esp_pm_configure(&pm_config);
if (pm_err == ESP_OK) {
ESP_LOGI(TAG, "PM: DFS 240/80 MHz, light sleep enabled");
} else {
ESP_LOGW(TAG, "PM configure failed: %s", esp_err_to_name(pm_err));
}
/* Apply saved WiFi power save mode */
if (s_powersave) {
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
ESP_LOGI(TAG, "WiFi modem sleep enabled");
} else {
esp_wifi_set_ps(WIFI_PS_NONE);
}
/* Chip temperature sensor (ESP32-S2/S3/C3/C6 only) */
#if SOC_TEMP_SENSOR_SUPPORTED
temperature_sensor_config_t temp_cfg = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
if (temperature_sensor_install(&temp_cfg, &s_temp_handle) == ESP_OK) {
temperature_sensor_enable(s_temp_handle);
ESP_LOGI(TAG, "Temperature sensor initialized");
} else {
ESP_LOGW(TAG, "Temperature sensor init failed");
s_temp_handle = NULL;
}
#endif
/* mDNS: announce as <hostname>.local with _esp-csi._udp service */
ESP_ERROR_CHECK(mdns_init());
mdns_hostname_set(s_hostname);
mdns_instance_name_set("ESP32 CSI Sensor");
mdns_service_add(NULL, "_esp-csi", "_udp", s_target_port, NULL, 0);
ESP_LOGI(TAG, "mDNS hostname: %s.local (_esp-csi._udp:%d)", s_hostname, s_target_port);
/* Watchdog: 30s timeout, auto-reboot on hang */
esp_task_wdt_config_t wdt_cfg = {
.timeout_ms = 30000,
.idle_core_mask = (1 << 0) | (1 << 1),
.trigger_panic = true,
};
ESP_ERROR_CHECK(esp_task_wdt_reconfigure(&wdt_cfg));
ESP_LOGI(TAG, "Watchdog configured: 30s timeout");
/* BLE: Initialize NimBLE stack */
ESP_ERROR_CHECK(nimble_port_init());
ble_hs_cfg.reset_cb = ble_on_reset;
ble_hs_cfg.sync_cb = ble_on_sync;
nimble_port_freertos_init(ble_host_task);
/* BLE: periodic scan restart to refresh duplicate filter */
const esp_timer_create_args_t ble_timer_args = {
.callback = ble_scan_restart_timer_cb,
.name = "ble_scan",
};
esp_timer_create(&ble_timer_args, &s_ble_timer);
esp_timer_start_periodic(s_ble_timer, s_ble_scan_interval_us);
ESP_LOGI(TAG, "BLE: NimBLE initialized, scan=%s", s_ble_enabled ? "on" : "off");
s_led_mode = s_led_quiet ? LED_OFF : LED_SLOW_BLINK;
udp_socket_init();
wifi_csi_init();
#if !CONFIG_IDF_TARGET_ESP32
/* Promiscuous mode disables CSI on original ESP32 — only enable on newer chips */
wifi_promiscuous_init();
#endif
wifi_ping_router_start();
xTaskCreate(cmd_task, "cmd_task", 6144, 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();
esp_ota_img_states_t ota_state;
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
ESP_LOGI(TAG, "OTA: marking firmware valid (rollback cancelled)");
esp_ota_mark_app_valid_cancel_rollback();
}
}
}