diff --git a/ROADMAP.md b/ROADMAP.md index 36157a2..a526978 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,14 +8,18 @@ - [x] List firmware modification ideas with priorities - [x] Verify build from repo (ESP-IDF v5.5.2, aarch64) -## v0.2 - Remote Management -- [ ] Add UDP command listener on ESP32 -- [ ] Implement REBOOT command -- [ ] Implement IDENTIFY command (blink LED) -- [ ] Implement STATUS command (uptime, heap, RSSI, temp) -- [ ] Implement RATE command (change sampling rate) -- [ ] Add LED status indicator (connected/sending/error) -- [ ] Pi-side command sender script +## v0.2 - Remote Management [IN PROGRESS] +- [x] Add UDP command listener on ESP32 (port 5501) +- [x] Implement REBOOT command +- [x] Implement IDENTIFY command (LED solid 5s) +- [x] Implement STATUS command (uptime, heap, RSSI, tx_power, rate) +- [x] Implement RATE command (change ping Hz, NVS persist) +- [x] Implement POWER command (TX power dBm, NVS persist) +- [x] Add LED status indicator (off/slow blink/fast blink/solid) +- [x] NVS persistence for rate and tx_power settings +- [x] Pi-side `esp-cmd` CLI tool +- [ ] Build and flash test on device +- [ ] Update CHEATSHEET.md with new commands ## v0.3 - OTA Updates - [ ] Modify partition table for dual OTA diff --git a/TASKS.md b/TASKS.md index 0b0898f..46046ab 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,28 +2,44 @@ **Last Updated:** 2026-02-04 -## Current Sprint: v0.1 - Documentation +## Current Sprint: v0.2 - Remote Management ### P0 - Critical -- [x] Copy firmware sources to project -- [x] Document current firmware and settings +- [x] Firmware: UDP command listener (port 5501) +- [x] Firmware: LED status indicator (GPIO2) +- [x] Firmware: NVS config persistence (rate, tx_power) +- [~] Build and flash firmware to device ### P1 - Important -- [x] Document build & flash workflow step by step -- [x] Create .gitignore for build artifacts -- [x] Test building firmware from this repo +- [x] Firmware: REBOOT, IDENTIFY, STATUS commands +- [x] Firmware: RATE command (10-100 Hz, restarts ping) +- [x] Firmware: POWER command (2-20 dBm) +- [x] Firmware: Refactor ping to support restart +- [x] Pi-side: `esp-cmd` CLI tool +- [ ] Update CHEATSHEET.md with esp-cmd usage ### P2 - Normal -- [x] Document CSI config options (what each sdkconfig flag does) -- [x] Compare csi_recv vs csi_recv_router differences +- [ ] Document esp-cmd in USAGE.md +- [ ] Add Kconfig CSI_CMD_PORT option ### P3 - Low - [ ] Document esp-crab dual-antenna capabilities - [ ] Document esp-radar console features +## Completed: v0.1 - Documentation + +- [x] Copy firmware sources to project +- [x] Document current firmware and settings +- [x] Document build & flash workflow +- [x] Create .gitignore for build artifacts +- [x] Test building firmware from this repo +- [x] Document CSI config options +- [x] Compare csi_recv vs csi_recv_router differences + ## Notes - Build confirmed working on ESP-IDF v5.5.2 (aarch64/Pi 5) -- Downgraded from IDF v6.1.0 to v5.5.2 for compatibility with deployed devices -- Branch renamed from `master` to `main` -- Docs created: `docs/INSTALL.md`, `docs/USAGE.md`, `docs/CHEATSHEET.md` +- v0.2 firmware adds ~1.5 KB heap + 6 KB stack usage +- NVS namespace: `csi_config` (keys: `send_rate`, `tx_power`) +- LED uses GPIO2 (built-in on most ESP32 dev boards) +- Command port default: 5501 (configurable via menuconfig) diff --git a/get-started/csi_recv_router/main/Kconfig.projbuild b/get-started/csi_recv_router/main/Kconfig.projbuild index 473bc9a..35bacf4 100644 --- a/get-started/csi_recv_router/main/Kconfig.projbuild +++ b/get-started/csi_recv_router/main/Kconfig.projbuild @@ -13,4 +13,11 @@ menu "CSI UDP Configuration" help UDP port on the target host for receiving CSI data. + config CSI_CMD_PORT + int "Command listener UDP port" + default 5501 + range 1024 65535 + help + UDP port for receiving management commands (STATUS, REBOOT, etc.). + endmenu diff --git a/get-started/csi_recv_router/main/app_main.c b/get-started/csi_recv_router/main/app_main.c index 4113697..fda4bbb 100644 --- a/get-started/csi_recv_router/main/app_main.c +++ b/get-started/csi_recv_router/main/app_main.c @@ -20,6 +20,7 @@ #include "freertos/event_groups.h" #include "nvs_flash.h" +#include "nvs.h" #include "esp_mac.h" #include "rom/ets_sys.h" @@ -27,6 +28,8 @@ #include "esp_wifi.h" #include "esp_netif.h" #include "esp_now.h" +#include "esp_timer.h" +#include "driver/gpio.h" #include "lwip/inet.h" #include "lwip/netdb.h" @@ -36,7 +39,7 @@ #include "protocol_examples_common.h" #include "esp_csi_gain_ctrl.h" -#define CONFIG_SEND_FREQUENCY 100 +#define CONFIG_SEND_FREQUENCY_DEFAULT 100 #if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61 #define CSI_FORCE_LLTF 0 #endif @@ -50,13 +53,138 @@ #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_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; + /* 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]; +/* --- NVS helpers --- */ + +static void config_load_nvs(void) +{ + 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; + } + nvs_close(h); + ESP_LOGI(TAG, "NVS loaded: rate=%d tx_power=%d", s_send_frequency, s_tx_power_dbm); + } 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; +} + +/* --- 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; + } + } +} + +/* --- CSI callback --- */ + static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info) { if (!info || !info->buf) { @@ -68,6 +196,8 @@ static void wifi_csi_rx_cb(void *ctx, wifi_csi_info_t *info) 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; @@ -212,13 +342,20 @@ static void udp_socket_init(void) CONFIG_CSI_UDP_TARGET_IP, CONFIG_CSI_UDP_TARGET_PORT); } -static esp_err_t wifi_ping_router_start() +/* --- Ping --- */ + +static esp_err_t wifi_ping_router_start(void) { - static esp_ping_handle_t ping_handle = NULL; + /* 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 / CONFIG_SEND_FREQUENCY; + ping_config.interval_ms = 1000 / s_send_frequency; ping_config.task_stack_size = 3072; ping_config.data_size = 1; @@ -229,15 +366,152 @@ static esp_err_t wifi_ping_router_start() ping_config.target_addr.type = ESP_IPADDR_TYPE_V4; esp_ping_callbacks_t cbs = { 0 }; - esp_ping_new_session(&ping_config, &cbs, &ping_handle); - esp_ping_start(ping_handle); + 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; } +/* --- 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 uptime_s = esp_timer_get_time() / 1000000LL; + 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; + } + + snprintf(reply, reply_size, + "OK STATUS uptime=%lld heap=%lu rssi=%d tx_power=%d rate=%d", + uptime_s, (unsigned long)heap, rssi, (int)s_tx_power_dbm, s_send_frequency); + 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); + } + s_send_frequency = val; + config_save_i32("send_rate", (int32_t)val); + wifi_ping_router_start(); + snprintf(reply, reply_size, "OK RATE %d", 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); + } + + 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[256]; + 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()); @@ -248,7 +522,15 @@ void app_main() */ 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); + + 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); } diff --git a/tools/esp-cmd b/tools/esp-cmd new file mode 100755 index 0000000..430c044 --- /dev/null +++ b/tools/esp-cmd @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Send management commands to ESP32 CSI devices over UDP.""" + +import socket +import sys + +DEFAULT_PORT = 5501 +TIMEOUT = 2.0 + +USAGE = """\ +Usage: esp-cmd [args...] + +Commands: + STATUS Query device state (uptime, heap, RSSI, tx_power, rate) + REBOOT Restart the ESP32 + IDENTIFY Blink LED solid for 5 seconds + RATE <10-100> Set ping frequency in Hz (saved to NVS) + POWER <2-20> Set TX power in dBm (saved to NVS) + +Examples: + esp-cmd 192.168.1.50 STATUS + esp-cmd 192.168.1.50 RATE 50 + esp-cmd 192.168.1.50 POWER 10 + esp-cmd 192.168.1.50 REBOOT + esp-cmd 192.168.1.50 IDENTIFY""" + + +def main(): + if len(sys.argv) < 3 or sys.argv[1] in ("-h", "--help"): + print(USAGE) + sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2) + + ip = sys.argv[1] + cmd = " ".join(sys.argv[2:]).strip() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(TIMEOUT) + + try: + sock.sendto(cmd.encode(), (ip, DEFAULT_PORT)) + data, _ = sock.recvfrom(512) + print(data.decode().strip()) + except socket.timeout: + print(f"ERR: no reply from {ip}:{DEFAULT_PORT} (timeout {TIMEOUT}s)", file=sys.stderr) + sys.exit(1) + except OSError as e: + print(f"ERR: {e}", file=sys.stderr) + sys.exit(1) + finally: + sock.close() + + +if __name__ == "__main__": + main()