Files
esp32-hacking/get-started/csi_recv_router/main/app_main.c
user 8c79d20cd8 feat: Add POWERTEST command, update roadmap with v2.0 FTM milestone
Add 7-phase power profiling command (POWERTEST) that cycles through
idle, CSI 10/100 Hz, BLE-only, combined, tx_low/tx_high with EVENT
markers for external power meter correlation. Saves/restores all
settings on completion.

Update roadmap: mark v1.4 done, add v2.0 hardware upgrade milestone
for ESP32-S3/C6 with WiFi FTM / 802.11mc inter-sensor ranging.
2026-02-04 21:47:28 +01:00

1800 lines
61 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_heap_caps.h"
#include "esp_ota_ops.h"
#include "esp_https_ota.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 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 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;
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;
/* --- 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;
}
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",
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);
} 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;
}
/* --- 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 = LED_SLOW_BLINK;
}
}
/* 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)
{
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;
}
}
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->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;
}
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 and adaptive sampling) */
csi_features_t features = {0};
if (s_csi_mode != CSI_MODE_RAW || s_adaptive) {
csi_extract_features(info->buf, info->len, compensate_gain, &features);
}
/* 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 */
struct ble_hs_adv_fields fields;
int rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data);
char name[32] = "";
if (rc == 0 && 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';
}
/* Send BLE_DATA via UDP */
char buf[160];
int len = snprintf(buf, sizeof(buf),
"BLE_DATA,%s,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%s\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);
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();
}
/* --- 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[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);
}
}
}
/* --- 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
#define PROBE_DEDUP_DEFAULT_US 10000000LL
static int64_t s_probe_dedup_us = PROBE_DEDUP_DEFAULT_US;
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\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);
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;
/* 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 */
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);
}
/* 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";
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_mode=%s hybrid_n=%d auth=%s flood_thresh=%d/%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,
csi_mode_str, s_hybrid_interval,
s_auth_secret[0] ? "on" : "off",
s_flood_thresh, s_flood_window_s);
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;
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", 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;
snprintf(reply, reply_size, "OK PROBERATE %ds", val);
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) {
snprintf(reply, reply_size, "ERR OTA url must start with http://");
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);
}
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);
/* 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 = 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();
}
}
}