Files
esp32-hacking/get-started/csi_recv_router/main/app_main.c
user ca526ef667 feat: Add probe request capture and mDNS service advertisement
- Capture WiFi probe requests (subtype 0x04) in promiscuous callback
- Parse SSID from tagged parameters, emit PROBE_DATA via UDP
- Per-MAC deduplication (10s cooldown) to limit probe flood
- Advertise _esp-csi._udp mDNS service for sensor discovery
2026-02-04 19:07:16 +01:00

1246 lines
41 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 "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;
/* 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_US 30000000LL /* restart scan every 30s to refresh duplicate filter */
static bool s_ble_enabled = false;
static uint8_t s_ble_own_addr_type;
/* 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) */
/* --- 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);
nvs_close(h);
ESP_LOGI(TAG, "NVS loaded: hostname=%s rate=%d tx_power=%d adaptive=%d threshold=%.6f ble=%d target=%s:%d",
s_hostname, s_send_frequency, s_tx_power_dbm, s_adaptive, s_motion_threshold, s_ble_enabled,
s_target_ip, s_target_port);
} else {
ESP_LOGI(TAG, "NVS: no saved config, using defaults");
}
}
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 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;
static int s_count = 0;
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_count < 100) {
esp_csi_gain_ctrl_record_rx_gain(agc_gain, fft_gain);
} else if (s_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
/* 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_count) {
ESP_LOGI(TAG, "================ CSI RECV (UDP) ================");
}
pos = snprintf(s_udp_buffer, sizeof(s_udp_buffer),
"CSI_DATA,%s,%d," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d",
s_hostname, s_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_count) {
ESP_LOGI(TAG, "================ CSI RECV (UDP) ================");
}
pos = snprintf(s_udp_buffer, sizeof(s_udp_buffer),
"CSI_DATA,%s,%d," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
s_hostname, s_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 (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");
/* 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));
}
/* Compute CSI energy for adaptive sampling */
if (s_adaptive) {
uint32_t energy = 0;
for (int i = 0; i < info->len; i++) {
energy += abs(info->buf[i]);
}
s_energy_buf[s_energy_idx % WANDER_WINDOW] = energy;
s_energy_idx++;
}
s_count++;
}
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 10 seconds */
#define PROBE_DEDUP_SIZE 32
#define PROBE_DEDUP_US 10000000LL
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 < 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 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";
char alert[160];
int 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");
}
/* --- Command handler --- */
static void reboot_after_delay(void *arg)
{
vTaskDelay(pdMS_TO_TICKS(200));
esp_restart();
}
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;
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
rssi = ap.rssi;
}
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
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);
}
snprintf(reply, reply_size,
"OK STATUS uptime=%s heap=%lu rssi=%d tx_power=%d rate=%d hostname=%s version=%s adaptive=%s motion=%d ble=%s target=%s:%d temp=%.1f",
uptime_str, (unsigned long)heap, rssi, (int)s_tx_power_dbm,
s_send_frequency, 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);
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);
}
/* 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;
}
/* 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);
}
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[128];
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);
int reply_len = cmd_handle(rx_buf, reply_buf, sizeof(reply_buf));
sendto(sock, reply_buf, reply_len, 0,
(struct sockaddr *)&src_addr, src_len);
ESP_LOGI(TAG, "CMD tx: \"%s\"", reply_buf);
}
}
/* --- 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());
/**
* @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());
/* 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_handle_t ble_timer;
esp_timer_create(&ble_timer_args, &ble_timer);
esp_timer_start_periodic(ble_timer, BLE_SCAN_RESTART_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();
wifi_promiscuous_init();
wifi_ping_router_start();
xTaskCreate(cmd_task, "cmd_task", 4096, NULL, 5, NULL);
xTaskCreate(adaptive_task, "adaptive", 3072, NULL, 3, NULL);
/* OTA rollback: mark firmware valid if we got this far */
const esp_partition_t *running = esp_ota_get_running_partition();
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();
}
}
}