Files
esp32-hacking/get-started/csi_recv_router/main/app_main.c
user 7ec70a653d feat: Add PROFILE command — heap, stack watermarks, CPU runtime stats
- PROFILE command returns heap usage (free/min/dram/iram),
  per-task stack high watermark, and per-task CPU % (when
  CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is enabled)
- Enable FreeRTOS runtime stats in sdkconfig.defaults
- Enlarge cmd reply buffer to 1400 bytes for multi-line output
- Add esp_heap_caps.h include
2026-02-04 17:59:30 +01:00

1057 lines
35 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 "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;
/* 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 */
/* --- NVS helpers --- */
static void config_load_nvs(void)
{
/* Start with Kconfig defaults for target */
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;
}
nvs_close(h);
ESP_LOGI(TAG, "NVS loaded: rate=%d tx_power=%d adaptive=%d threshold=%.6f ble=%d target=%s:%d",
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,%d," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d",
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,%d," MACSTR ",%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
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[128];
int len = snprintf(buf, sizeof(buf),
"BLE_DATA,%02x:%02x:%02x:%02x:%02x:%02x,%d,%s,%s\n",
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[80];
int len = snprintf(event, sizeof(event),
"EVENT motion=%d rate=%d wander=%.6f",
motion ? 1 : 0, target_rate, wander);
if (s_udp_socket >= 0) {
sendto(s_udp_socket, event, len, 0,
(struct sockaddr *)&s_dest_addr, sizeof(s_dest_addr));
}
ESP_LOGI(TAG, "Adaptive: %s -> %d Hz (wander=%.6f)",
motion ? "motion" : "idle", target_rate, wander);
}
}
}
/* --- OTA --- */
static void ota_task(void *arg)
{
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);
}
/* --- 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();
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",
uptime_str, (unsigned long)heap, rssi, (int)s_tx_power_dbm,
s_send_frequency, CONFIG_CSI_HOSTNAME, app_desc->version,
s_adaptive ? "on" : "off", s_motion_detected ? 1 : 0,
s_ble_enabled ? "on" : "off", s_target_ip, s_target_port);
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);
}
/* 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);
/* mDNS: announce as <hostname>.local */
ESP_ERROR_CHECK(mdns_init());
mdns_hostname_set(CONFIG_CSI_HOSTNAME);
mdns_instance_name_set("ESP32 CSI Sensor");
ESP_LOGI(TAG, "mDNS hostname: %s.local", CONFIG_CSI_HOSTNAME);
/* 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_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();
}
}
}