Compare commits

..

No commits in common. "538d2031857d18474f3f33298f5133a9ef66bde7" and "d1be8327900d3ae0e7a39ad373ac0dc679fc04d7" have entirely different histories.

16 changed files with 1139 additions and 935 deletions

View File

@ -1,3 +1,4 @@
idf_component_register(SRCS "app_console.c"
INCLUDE_DIRS "."
PRIV_REQUIRES console wifi_cfg iperf)
REQUIRES console
PRIV_REQUIRES wifi_controller csi_manager status_led gps_sync esp_wifi iperf)

View File

@ -2,161 +2,111 @@
#include "esp_console.h"
#include "esp_log.h"
#include "argtable3/argtable3.h"
#include "wifi_cfg.h"
#include "iperf.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// ============================================================================
// COMMAND: iperf
// ============================================================================
static struct {
struct arg_lit *start;
struct arg_lit *stop;
struct arg_lit *status;
struct arg_int *pps;
struct arg_lit *help;
struct arg_end *end;
} iperf_args;
// Dependencies
#include "wifi_controller.h"
#include "status_led.h"
#include "gps_sync.h"
#include "iperf.h"
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
#include "csi_manager.h"
#endif
// --- Command Handlers ---
static int cmd_iperf(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&iperf_args);
if (nerrors > 0) {
arg_print_errors(stderr, iperf_args.end, argv[0]);
if (argc < 2) {
printf("Usage: iperf <start|stop|pps|status>\n");
return 1;
}
if (iperf_args.help->count > 0) {
printf("Usage: iperf [start|stop|status] [--pps <n>]\n");
if (strcmp(argv[1], "start") == 0) {
iperf_cfg_t cfg = { .time = 0 }; // Infinite
iperf_start(&cfg);
// iperf_start already logs "IPERF_STARTED" via printf in iperf.c,
// but keeping it here is fine/redundant.
// To be safe and clean, we rely on iperf.c's output or just return success.
return 0;
}
if (iperf_args.stop->count > 0) {
} else if (strcmp(argv[1], "stop") == 0) {
iperf_stop();
return 0;
}
if (iperf_args.pps->count > 0) {
int val = iperf_args.pps->ival[0];
if (val > 0) {
iperf_set_pps((uint32_t)val);
} else {
printf("Error: PPS must be > 0\n");
} else if (strcmp(argv[1], "pps") == 0) {
// Syntax: iperf pps 100
if (argc < 3) {
printf("Error: Missing value. Usage: iperf pps <rate>\n");
return 1;
}
int pps = atoi(argv[2]);
if (pps <= 0) {
printf("Error: Invalid PPS.\n");
return 1;
}
iperf_set_pps((uint32_t)pps);
// iperf_set_pps prints "IPERF_PPS_UPDATED: ..."
return 0;
}
if (iperf_args.status->count > 0) {
} else if (strcmp(argv[1], "status") == 0) {
// [FIXED] Use the new API to print detailed stats
iperf_print_status();
return 0;
}
if (iperf_args.start->count > 0) {
// Start using saved NVS config
iperf_cfg_t cfg = { .time = 0 };
iperf_start(&cfg);
return 0;
}
return 0;
printf("Error: Unknown subcommand '%s'.\n", argv[1]);
return 1;
}
static void register_iperf_cmd(void) {
iperf_args.start = arg_lit0(NULL, "start", "Start iperf traffic");
iperf_args.stop = arg_lit0(NULL, "stop", "Stop iperf traffic");
iperf_args.status = arg_lit0(NULL, "status", "Show current statistics");
iperf_args.pps = arg_int0(NULL, "pps", "<n>", "Set packets per second");
iperf_args.help = arg_lit0(NULL, "help", "Show help");
iperf_args.end = arg_end(20);
const esp_console_cmd_t cmd = {
.command = "iperf",
.help = "Control iperf traffic generator",
.hint = NULL,
.func = &cmd_iperf,
.argtable = &iperf_args
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}
// ============================================================================
// COMMAND: wifi_config
// ============================================================================
static struct {
struct arg_str *ssid;
struct arg_str *pass;
struct arg_str *ip;
struct arg_lit *dhcp;
struct arg_lit *help;
struct arg_end *end;
} wifi_args;
static int cmd_wifi_config(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&wifi_args);
if (nerrors > 0) {
arg_print_errors(stderr, wifi_args.end, argv[0]);
static int cmd_mode_monitor(int argc, char **argv) {
int channel = wifi_ctl_get_monitor_channel();
if (argc > 1) channel = atoi(argv[1]);
if (wifi_ctl_switch_to_monitor(channel, WIFI_BW_HT20) != ESP_OK) {
printf("Failed to switch to monitor mode\n");
return 1;
}
if (wifi_args.help->count > 0) {
printf("Usage: wifi_config -s <ssid> -p <pass> [-i <ip>] [-d]\n");
return 0;
}
if (wifi_args.ssid->count == 0) {
printf("Error: SSID is required (-s)\n");
return 1;
}
const char* ssid = wifi_args.ssid->sval[0];
const char* pass = (wifi_args.pass->count > 0) ? wifi_args.pass->sval[0] : "";
const char* ip = (wifi_args.ip->count > 0) ? wifi_args.ip->sval[0] : NULL;
bool dhcp = (wifi_args.dhcp->count > 0);
printf("Saving WiFi Config: SSID='%s' DHCP=%d\n", ssid, dhcp);
wifi_cfg_set_credentials(ssid, pass);
if (ip) {
char mask[] = "255.255.255.0";
char gw[32];
// FIXED: Use strlcpy instead of strncpy to prevent truncation warnings
strlcpy(gw, ip, sizeof(gw));
char *last_dot = strrchr(gw, '.');
if (last_dot) strcpy(last_dot, ".1");
wifi_cfg_set_static_ip(ip, mask, gw);
wifi_cfg_set_dhcp(false);
} else {
wifi_cfg_set_dhcp(dhcp);
}
printf("Config saved. Rebooting to apply...\n");
esp_restart();
return 0;
}
static void register_wifi_cmd(void) {
wifi_args.ssid = arg_str0("s", "ssid", "<ssid>", "WiFi SSID");
wifi_args.pass = arg_str0("p", "password", "<pass>", "WiFi Password");
wifi_args.ip = arg_str0("i", "ip", "<ip>", "Static IP");
wifi_args.dhcp = arg_lit0("d", "dhcp", "Enable DHCP");
wifi_args.help = arg_lit0("h", "help", "Show help");
wifi_args.end = arg_end(20);
static int cmd_mode_sta(int argc, char **argv) {
if (wifi_ctl_switch_to_sta(WIFI_BAND_MODE_AUTO) != ESP_OK) {
printf("Failed to switch to STA mode\n");
return 1;
}
return 0;
}
const esp_console_cmd_t cmd = {
.command = "wifi_config",
.help = "Configure WiFi credentials",
.hint = NULL,
.func = &cmd_wifi_config,
.argtable = &wifi_args
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
static int cmd_mode_status(int argc, char **argv) {
wifi_ctl_mode_t mode = wifi_ctl_get_mode();
printf("\n=== WiFi Mode Status ===\n");
printf("Current mode: %s\n", mode == WIFI_CTL_MODE_STA ? "STA" : "MONITOR");
printf("LED state: %d\n", status_led_get_state());
printf("GPS synced: %s\n", gps_is_synced() ? "Yes" : "No");
return 0;
}
static int cmd_csi_dump(int argc, char **argv) {
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
csi_mgr_schedule_dump();
#else
printf("Error: CSI feature is disabled in this firmware build.\n");
#endif
return 0;
}
void app_console_register_commands(void) {
register_iperf_cmd();
register_wifi_cmd();
const esp_console_cmd_t cmds[] = {
{ .command = "mode_monitor", .help = "Switch to monitor mode", .func = &cmd_mode_monitor },
{ .command = "mode_sta", .help = "Switch to STA mode", .func = &cmd_mode_sta },
{ .command = "mode_status", .help = "Show device status", .func = &cmd_mode_status },
{ .command = "csi_dump", .help = "Dump collected CSI data", .func = &cmd_csi_dump },
{ .command = "iperf", .help = "Control iperf (start, stop, pps, status)", .func = &cmd_iperf },
};
for (int i = 0; i < sizeof(cmds)/sizeof(cmds[0]); i++) {
ESP_ERROR_CHECK(esp_console_cmd_register(&cmds[i]));
}
}

View File

@ -5,7 +5,8 @@ extern "C" {
#endif
/**
* @brief Register application-specific console commands
* @brief Register all application-specific console commands
* (mode_monitor, mode_sta, mode_status, csi_dump)
*/
void app_console_register_commands(void);

View File

@ -0,0 +1,3 @@
idf_component_register(SRCS "cmd_transport.c"
INCLUDE_DIRS "."
PRIV_REQUIRES console driver soc esp_driver_usb_serial_jtag)

View File

@ -0,0 +1,131 @@
#include "cmd_transport.h"
#include "esp_log.h"
#include "esp_console.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/soc_caps.h"
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#if SOC_USB_SERIAL_JTAG_SUPPORTED
#include "driver/usb_serial_jtag.h"
#endif
static const char *TAG = "CMD_TP";
#define MAX_LISTENERS 4
static cmd_line_handler_t s_listeners[MAX_LISTENERS] = {0};
static int s_listener_count = 0;
static bool s_inited = false;
void cmd_transport_register_listener(cmd_line_handler_t handler) {
if (s_listener_count < MAX_LISTENERS) {
s_listeners[s_listener_count++] = handler;
}
}
// Trim trailing whitespace (CR, LF)
static void trim_trailing(char *s) {
int n = strlen(s);
while (n > 0 && (s[n-1] == '\r' || s[n-1] == '\n' || isspace((unsigned char)s[n-1]))) {
s[--n] = 0;
}
}
// Dispatch line to listeners, then to ESP Console
static void dispatch_line(char *line, cmd_reply_func_t reply_func, void *reply_ctx) {
bool handled = false;
// 1. Offer to registered listeners (e.g. wifi_cfg)
for (int i = 0; i < s_listener_count; i++) {
if (s_listeners[i] && s_listeners[i](line, reply_func, reply_ctx)) {
handled = true;
break;
}
}
// 2. If not handled, pass to system console (for commands like 'mode_monitor')
if (!handled && strlen(line) > 0) {
int ret;
esp_err_t err = esp_console_run(line, &ret);
if (err == ESP_ERR_NOT_FOUND) {
// Unrecognized command - silent ignore or reply error
} else if (err != ESP_OK) {
ESP_LOGE(TAG, "Console run error: %s", esp_err_to_name(err));
}
}
}
// --- UART (stdin/stdout) Support ---
static void uart_reply(const char *msg, void *ctx) {
(void)ctx;
printf("%s", msg);
fflush(stdout);
}
static void uart_listener_task(void *arg) {
char line[256];
// Disable buffering
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
while (1) {
if (fgets(line, sizeof(line), stdin)) {
trim_trailing(line);
dispatch_line(line, uart_reply, NULL);
} else {
vTaskDelay(pdMS_TO_TICKS(20));
}
}
}
// --- USB Serial/JTAG Support ---
#if SOC_USB_SERIAL_JTAG_SUPPORTED
static void usb_reply(const char *msg, void *ctx) {
(void)ctx;
usb_serial_jtag_write_bytes((const uint8_t*)msg, strlen(msg), pdMS_TO_TICKS(50));
}
static void usb_listener_task(void *arg) {
usb_serial_jtag_driver_config_t d = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT();
if (usb_serial_jtag_driver_install(&d) != ESP_OK) {
ESP_LOGE(TAG, "Failed to install USB-Serial/JTAG driver");
vTaskDelete(NULL);
}
char buf[256];
size_t idx = 0;
uint8_t c;
while (1) {
int n = usb_serial_jtag_read_bytes(&c, 1, pdMS_TO_TICKS(20));
if (n > 0) {
if (c == '\n' || c == '\r') {
if (idx > 0) {
buf[idx] = 0;
dispatch_line(buf, usb_reply, NULL);
idx = 0;
}
} else {
if (idx < sizeof(buf) - 1) {
buf[idx++] = (char)c;
}
}
}
}
}
#endif
void cmd_transport_init(void) {
if (s_inited) return;
s_inited = true;
// Start UART Listener
xTaskCreatePinnedToCore(uart_listener_task, "cmd_uart", 4096, NULL, 5, NULL, tskNO_AFFINITY);
// Start USB Listener (if supported)
#if SOC_USB_SERIAL_JTAG_SUPPORTED
xTaskCreatePinnedToCore(usb_listener_task, "cmd_usb", 4096, NULL, 5, NULL, tskNO_AFFINITY);
#endif
}

View File

@ -0,0 +1,37 @@
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Function pointer structure for replying to the command source
*/
typedef void (*cmd_reply_func_t)(const char *msg, void *ctx);
/**
* @brief Callback for handling incoming lines
* * @param line The received line (null-terminated, trimmed of trailing CR/LF)
* @param reply_func Function to call to send a response back to the source
* @param reply_ctx Context pointer to pass to reply_func
* @return true if the line was consumed/handled
* @return false if the line should be passed to the next listener (or system console)
*/
typedef bool (*cmd_line_handler_t)(const char *line, cmd_reply_func_t reply_func, void *reply_ctx);
/**
* @brief Initialize the command transport (starts UART and USB listener tasks)
*/
void cmd_transport_init(void);
/**
* @brief Register a listener for console input
* @param handler The callback function
*/
void cmd_transport_register_listener(cmd_line_handler_t handler);
#ifdef __cplusplus
}
#endif

View File

@ -39,58 +39,14 @@ typedef struct {
static iperf_ctrl_t s_iperf_ctrl = {0};
static TaskHandle_t s_iperf_task_handle = NULL;
static iperf_cfg_t s_next_cfg; // Holding area for the new config
static bool s_reload_req = false; // Flag to trigger internal restart
// Global Stats Tracker
static iperf_stats_t s_stats = {0};
// --- Session Persistence Variables ---
static int64_t s_session_start_time = 0;
static int64_t s_session_end_time = 0;
static uint64_t s_session_packets = 0;
// --- State Duration & Edge Counters ---
typedef enum {
IPERF_STATE_IDLE = 0,
IPERF_STATE_TX,
IPERF_STATE_TX_SLOW,
IPERF_STATE_TX_STALLED
} iperf_fsm_state_t;
static int64_t s_time_tx_us = 0;
static int64_t s_time_slow_us = 0;
static int64_t s_time_stalled_us = 0;
static uint32_t s_edge_tx = 0;
static uint32_t s_edge_slow = 0;
static uint32_t s_edge_stalled = 0;
static iperf_fsm_state_t s_current_fsm_state = IPERF_STATE_IDLE;
static esp_event_handler_instance_t instance_any_id;
static esp_event_handler_instance_t instance_got_ip;
// --- Helper: Pattern Initialization ---
// Fills buffer with 0-9 cyclic ASCII pattern (matches iperf2 "pattern" function)
static void iperf_pattern(uint8_t *buf, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
buf[i] = (i % 10) + '0';
}
}
// --- Helper: Generate Client Header ---
// Modified to set all zeros except HEADER_SEQNO64B
static void iperf_generate_client_hdr(iperf_cfg_t *cfg, client_hdr_v1 *hdr) {
// Zero out the entire structure
memset(hdr, 0, sizeof(client_hdr_v1));
// Set only the SEQNO64B flag (Server will detect 64-bit seqno in UDP header)
hdr->flags = htonl(HEADER_SEQNO64B);
}
// ... [Existing Status Reporting & Event Handler Code] ...
// --- Status Reporting ---
void iperf_get_stats(iperf_stats_t *stats) {
if (stats) {
s_stats.config_pps = (s_iperf_ctrl.cfg.pacing_period_us > 0) ?
@ -101,59 +57,14 @@ void iperf_get_stats(iperf_stats_t *stats) {
void iperf_print_status(void) {
iperf_get_stats(&s_stats);
// 1. Get Source IP
char src_ip[32] = "0.0.0.0";
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif) {
esp_netif_ip_info_t ip_info;
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
inet_ntop(AF_INET, &ip_info.ip, src_ip, sizeof(src_ip));
}
}
// 2. Get Destination IP
char dst_ip[32] = "0.0.0.0";
struct in_addr daddr;
daddr.s_addr = s_iperf_ctrl.cfg.dip;
inet_ntop(AF_INET, &daddr, dst_ip, sizeof(dst_ip));
float err = 0.0f;
if (s_stats.running && s_stats.config_pps > 0) {
int32_t diff = (int32_t)s_stats.config_pps - (int32_t)s_stats.actual_pps;
err = (float)diff * 100.0f / (float)s_stats.config_pps;
}
// 3. Compute Session Bandwidth
float avg_bw_mbps = 0.0f;
if (s_session_start_time > 0) {
int64_t end_t = (s_stats.running) ? esp_timer_get_time() : s_session_end_time;
if (end_t > s_session_start_time) {
double duration_sec = (double)(end_t - s_session_start_time) / 1000000.0;
if (duration_sec > 0.001) {
double total_bits = (double)s_session_packets * (double)s_iperf_ctrl.cfg.send_len * 8.0;
avg_bw_mbps = (float)(total_bits / duration_sec / 1000000.0);
}
}
}
// 4. Calculate State Percentages
double total_us = (double)(s_time_tx_us + s_time_slow_us + s_time_stalled_us);
if (total_us < 1.0) total_us = 1.0;
double pct_tx = ((double)s_time_tx_us / total_us) * 100.0;
double pct_slow = ((double)s_time_slow_us / total_us) * 100.0;
double pct_stalled = ((double)s_time_stalled_us / total_us) * 100.0;
// Standard Stats
printf("IPERF_STATUS: Src=%s, Dst=%s, Running=%d, Config=%" PRIu32 ", Actual=%" PRIu32 ", Err=%.1f%%, Pkts=%" PRIu64 ", AvgBW=%.2f Mbps\n",
src_ip, dst_ip, s_stats.running, s_stats.config_pps, s_stats.actual_pps, err, s_session_packets, avg_bw_mbps);
// New Format: Time + Percentage + Edges
printf("IPERF_STATES: TX=%.2fs/%.1f%% (%lu), SLOW=%.2fs/%.1f%% (%lu), STALLED=%.2fs/%.1f%% (%lu)\n",
(double)s_time_tx_us/1000000.0, pct_tx, (unsigned long)s_edge_tx,
(double)s_time_slow_us/1000000.0, pct_slow, (unsigned long)s_edge_slow,
(double)s_time_stalled_us/1000000.0, pct_stalled, (unsigned long)s_edge_stalled);
printf("IPERF_STATUS: Running=%d, Config=%" PRIu32 ", Actual=%" PRIu32 ", Err=%.1f%%\n",
s_stats.running, s_stats.config_pps, s_stats.actual_pps, err);
}
// --- Network Events ---
@ -256,8 +167,8 @@ uint32_t iperf_get_pps(void) {
return 1000000 / s_iperf_ctrl.cfg.pacing_period_us;
}
static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
// FIX 1: If wait is aborted (stop requested), print STOPPED so controller knows
if (!iperf_wait_for_ip()) {
printf("IPERF_STOPPED\n");
return ESP_OK;
@ -276,26 +187,13 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
if (sockfd < 0) {
status_led_set_state(LED_STATE_FAILED);
ESP_LOGE(TAG, "Socket creation failed: %d", errno);
// FIX 2: Print STOPPED on failure so controller doesn't timeout
printf("IPERF_STOPPED\n");
return ESP_FAIL;
}
status_led_set_state(LED_STATE_TRANSMITTING_SLOW);
udp_datagram *udp_hdr = (udp_datagram *)ctrl->buffer;
client_hdr_v1 *client_hdr = (client_hdr_v1 *)(ctrl->buffer + sizeof(udp_datagram));
iperf_generate_client_hdr(&ctrl->cfg, client_hdr);
s_stats.running = true;
s_session_start_time = esp_timer_get_time();
s_session_end_time = 0;
s_session_packets = 0;
// Reset FSM
s_time_tx_us = 0; s_time_slow_us = 0; s_time_stalled_us = 0;
s_edge_tx = 0; s_edge_slow = 0; s_edge_stalled = 0;
s_current_fsm_state = IPERF_STATE_IDLE;
printf("IPERF_STARTED\n");
int64_t next_send_time = esp_timer_get_time();
@ -303,7 +201,8 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
int64_t last_rate_check = esp_timer_get_time();
uint32_t packets_since_check = 0;
int64_t packet_id = 0;
int32_t packet_id = 0;
struct timespec ts;
while (!ctrl->finish && esp_timer_get_time() < end_time) {
@ -314,29 +213,23 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
else while (esp_timer_get_time() < next_send_time) taskYIELD();
for (int k = 0; k < ctrl->cfg.burst_count; k++) {
int64_t current_id = packet_id++;
udp_hdr->id = htonl((uint32_t)(current_id & 0xFFFFFFFF));
udp_hdr->id2 = htonl((uint32_t)((current_id >> 32) & 0xFFFFFFFF));
udp_datagram *hdr = (udp_datagram *)ctrl->buffer;
hdr->id = htonl(packet_id++);
clock_gettime(CLOCK_REALTIME, &ts);
udp_hdr->tv_sec = htonl((uint32_t)ts.tv_sec);
udp_hdr->tv_usec = htonl(ts.tv_nsec / 1000);
hdr->tv_sec = htonl(ts.tv_sec);
hdr->tv_usec = htonl(ts.tv_nsec / 1000);
hdr->id2 = hdr->id;
int sent = sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr));
if (sent > 0) {
packets_since_check++;
s_session_packets++;
} else {
// --- ROBUST FIX: Never Abort ---
// If send fails (buffer full, routing issue, etc.), we just yield and retry next loop.
// We do NOT goto exit.
if (errno != 12) {
// Log rarely to avoid spamming serial
if ((packet_id % 100) == 0) {
ESP_LOGW(TAG, "Send error: %d (Ignored)", errno);
}
ESP_LOGE(TAG, "Send failed: %d", errno);
status_led_set_state(LED_STATE_FAILED);
goto exit;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
@ -347,119 +240,73 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
uint32_t interval_us = (uint32_t)(now - last_rate_check);
if (interval_us > 0) {
s_stats.actual_pps = (uint32_t)((uint64_t)packets_since_check * 1000000 / interval_us);
}
uint32_t config_pps = iperf_get_pps();
uint32_t threshold = (config_pps * 3) / 4;
iperf_fsm_state_t next_state;
if (s_stats.actual_pps == 0) next_state = IPERF_STATE_TX_STALLED;
else if (s_stats.actual_pps >= threshold) next_state = IPERF_STATE_TX;
else next_state = IPERF_STATE_TX_SLOW;
led_state_t target = (s_stats.actual_pps >= threshold) ? LED_STATE_TRANSMITTING : LED_STATE_TRANSMITTING_SLOW;
if (status_led_get_state() != target) status_led_set_state(target);
switch (next_state) {
case IPERF_STATE_TX: s_time_tx_us += interval_us; break;
case IPERF_STATE_TX_SLOW: s_time_slow_us += interval_us; break;
case IPERF_STATE_TX_STALLED: s_time_stalled_us += interval_us; break;
default: break;
}
if (next_state != s_current_fsm_state) {
switch (next_state) {
case IPERF_STATE_TX: s_edge_tx++; break;
case IPERF_STATE_TX_SLOW: s_edge_slow++; break;
case IPERF_STATE_TX_STALLED: s_edge_stalled++; break;
default: break;
}
s_current_fsm_state = next_state;
}
led_state_t led_target = (s_current_fsm_state == IPERF_STATE_TX) ? LED_STATE_TRANSMITTING : LED_STATE_TRANSMITTING_SLOW;
if (status_led_get_state() != led_target) status_led_set_state(led_target);
}
last_rate_check = now;
packets_since_check = 0;
}
next_send_time += ctrl->cfg.pacing_period_us;
}
exit:
// Termination Packets
{
udp_datagram *hdr = (udp_datagram *)ctrl->buffer;
int64_t final_id = -packet_id;
hdr->id = htonl((uint32_t)(final_id & 0xFFFFFFFF));
hdr->id2 = htonl((uint32_t)((final_id >> 32) & 0xFFFFFFFF));
int32_t final_id = -packet_id;
hdr->id = htonl(final_id);
hdr->id2 = hdr->id;
clock_gettime(CLOCK_REALTIME, &ts);
hdr->tv_sec = htonl((uint32_t)ts.tv_sec);
hdr->tv_sec = htonl(ts.tv_sec);
hdr->tv_usec = htonl(ts.tv_nsec / 1000);
for(int i=0; i<10; i++) {
sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr));
vTaskDelay(pdMS_TO_TICKS(2));
}
ESP_LOGI(TAG, "Sent termination packets (ID: %" PRId64 ")", final_id);
ESP_LOGI(TAG, "Sent termination packets (ID: %ld)", (long)final_id);
}
close(sockfd);
s_stats.running = false;
s_session_end_time = esp_timer_get_time();
s_stats.actual_pps = 0;
status_led_set_state(LED_STATE_CONNECTED); // <--- This is your "Solid Green"
status_led_set_state(LED_STATE_CONNECTED);
printf("IPERF_STOPPED\n");
return ESP_OK;
}
static void iperf_task(void *arg) {
iperf_ctrl_t *ctrl = (iperf_ctrl_t *)arg;
do {
s_reload_req = false;
ctrl->finish = false;
xEventGroupClearBits(s_iperf_event_group, IPERF_STOP_REQ_BIT);
if (ctrl->cfg.flag & IPERF_FLAG_UDP && ctrl->cfg.flag & IPERF_FLAG_CLIENT) {
iperf_start_udp_client(ctrl);
}
if (s_reload_req) {
ESP_LOGI(TAG, "Hot reloading iperf task with new config...");
ctrl->cfg = s_next_cfg;
vTaskDelay(pdMS_TO_TICKS(100));
}
} while (s_reload_req);
free(ctrl->buffer);
s_iperf_task_handle = NULL;
vTaskDelete(NULL);
}
void iperf_start(iperf_cfg_t *cfg) {
iperf_cfg_t new_cfg = *cfg;
iperf_read_nvs_config(&new_cfg);
if (new_cfg.send_len == 0) new_cfg.send_len = 1470;
if (new_cfg.pacing_period_us == 0) new_cfg.pacing_period_us = 10000;
if (new_cfg.burst_count == 0) new_cfg.burst_count = 1;
if (s_iperf_task_handle) {
ESP_LOGI(TAG, "Task running. Staging hot reload.");
s_next_cfg = new_cfg;
s_reload_req = true;
iperf_stop();
printf("IPERF_RELOADING\n");
ESP_LOGW(TAG, "Iperf already running");
return;
}
s_iperf_ctrl.cfg = new_cfg;
s_iperf_ctrl.finish = false;
s_iperf_ctrl.cfg = *cfg;
iperf_read_nvs_config(&s_iperf_ctrl.cfg);
if (s_iperf_ctrl.buffer == NULL) {
if (s_iperf_ctrl.cfg.send_len == 0) s_iperf_ctrl.cfg.send_len = 1470;
if (s_iperf_ctrl.cfg.pacing_period_us == 0) s_iperf_ctrl.cfg.pacing_period_us = 10000;
if (s_iperf_ctrl.cfg.burst_count == 0) s_iperf_ctrl.cfg.burst_count = 1;
s_iperf_ctrl.finish = false;
s_iperf_ctrl.buffer_len = s_iperf_ctrl.cfg.send_len + 128;
s_iperf_ctrl.buffer = calloc(1, s_iperf_ctrl.buffer_len);
}
// Initialize Buffer Pattern
if (s_iperf_ctrl.buffer) {
iperf_pattern(s_iperf_ctrl.buffer, s_iperf_ctrl.buffer_len);
}
if (s_iperf_event_group == NULL) {
s_iperf_event_group = xEventGroupCreate();
}
xTaskCreate(iperf_task, "iperf", 4096, &s_iperf_ctrl, 5, &s_iperf_task_handle);
}

View File

@ -11,12 +11,6 @@
#define IPERF_FLAG_TCP (1 << 2)
#define IPERF_FLAG_UDP (1 << 3)
// --- Standard Iperf2 Header Flags (from payloads.h) ---
#define HEADER_VERSION1 0x80000000
#define HEADER_EXTEND 0x40000000
#define HEADER_UDPTESTS 0x20000000
#define HEADER_SEQNO64B 0x08000000
// --- Defaults ---
#define IPERF_DEFAULT_PORT 5001
#define IPERF_DEFAULT_INTERVAL 3
@ -54,41 +48,27 @@ typedef struct {
float error_rate;
} iperf_stats_t;
// --- Wire Formats (Strict Layout) ---
// 1. Basic UDP Datagram Header (16 bytes)
// Corresponds to 'struct UDP_datagram' in payloads.h
// --- Header ---
typedef struct {
int32_t id; // Lower 32 bits of seqno
uint32_t tv_sec; // Seconds
uint32_t tv_usec; // Microseconds
int32_t id2; // Upper 32 bits of seqno (when HEADER_SEQNO64B is set)
int32_t id;
uint32_t tv_sec;
uint32_t tv_usec;
uint32_t id2;
} udp_datagram;
// 2. Client Header V1 (Used for First Packet Exchange)
// Corresponds to 'struct client_hdr_v1' in payloads.h
typedef struct {
int32_t flags;
int32_t numThreads;
int32_t mPort;
int32_t mBufLen;
int32_t mWinBand;
int32_t mAmount;
} client_hdr_v1;
// --- API ---
void iperf_init_led(led_strip_handle_t handle);
void iperf_set_pps(uint32_t pps);
uint32_t iperf_get_pps(void);
// Get snapshot of current stats
// New: Get snapshot of current stats
void iperf_get_stats(iperf_stats_t *stats);
// Print formatted status to stdout (for CLI/Python)
// New: Print formatted status to stdout (for CLI/Python)
void iperf_print_status(void);
void iperf_start(iperf_cfg_t *cfg);
void iperf_stop(void);
#endif
#endif // IPERF_H

View File

@ -1,3 +1,3 @@
idf_component_register(SRCS "wifi_cfg.c"
INCLUDE_DIRS "."
REQUIRES nvs_flash esp_wifi esp_netif)
PRIV_REQUIRES nvs_flash esp_wifi esp_netif driver cmd_transport csi_manager)

View File

@ -1,6 +1,8 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
@ -9,98 +11,149 @@
#include "esp_netif.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_check.h"
#include "wifi_cfg.h"
#include "cmd_transport.h"
// Removed unused TAG
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
#include "csi_manager.h"
#endif
static const char *TAG = "wifi_cfg";
static esp_netif_t *sta_netif = NULL;
static bool cfg_dhcp = true;
// --- Helper: NVS Write ---
static void nvs_write_str(const char *key, const char *val) {
// --- NVS Helper ---
static esp_err_t nvs_set_str2(nvs_handle_t h, const char *key, const char *val){
return val ? nvs_set_str(h, key, val) : nvs_erase_key(h, key);
}
// --- NVS Save Functions ---
// 1. Save Network Settings (Namespace: "netcfg")
static void save_net_cfg(const char* ssid, const char* pass, const char* ip, const char* mask, const char* gw, bool dhcp, const char* band, const char* bw, const char* powersave, const char* mode, uint8_t mon_ch){
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READWRITE, &h) == ESP_OK) {
if (val) nvs_set_str(h, key, val);
else nvs_erase_key(h, key);
if (nvs_open("netcfg", NVS_READWRITE, &h) != ESP_OK) return;
if (ssid) nvs_set_str2(h, "ssid", ssid);
if (pass) nvs_set_str2(h, "pass", pass);
if (ip) nvs_set_str2(h, "ip", ip);
if (mask) nvs_set_str2(h, "mask", mask);
if (gw) nvs_set_str2(h, "gw", gw);
if (band) nvs_set_str2(h, "band", band);
if (bw) nvs_set_str2(h, "bw", bw);
if (powersave) nvs_set_str2(h, "powersave", powersave);
if (mode) nvs_set_str2(h, "mode", mode);
nvs_set_u8(h, "mon_ch", mon_ch);
nvs_set_u8(h, "dhcp", dhcp ? 1 : 0);
nvs_commit(h);
nvs_close(h);
}
cfg_dhcp = dhcp;
ESP_LOGI(TAG, "Net Config Saved: SSID=%s IP=%s DHCP=%d", ssid?ssid:"", ip?ip:"", dhcp);
}
static void nvs_write_u8(const char *key, uint8_t val) {
// 2. Save Iperf Settings (Namespace: "storage") -> Matches iperf.c keys
static void save_iperf_cfg(const char* dst_ip, const char* role, const char* proto, uint32_t period, uint32_t burst, uint32_t len, uint32_t port, bool enable){
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READWRITE, &h) == ESP_OK) {
nvs_set_u8(h, key, val);
if (nvs_open("storage", NVS_READWRITE, &h) != ESP_OK) return;
// Note: Keys must match what iperf.c reads (NVS_KEY_IPERF_DST_IP etc)
if (dst_ip && dst_ip[0]) nvs_set_str(h, "iperf_dst_ip", dst_ip);
if (role && role[0]) nvs_set_str(h, "iperf_role", role);
if (proto && proto[0]) nvs_set_str(h, "iperf_proto", proto);
nvs_set_u32(h, "iperf_period", period);
nvs_set_u32(h, "iperf_burst", burst);
nvs_set_u32(h, "iperf_len", len);
nvs_set_u32(h, "iperf_port", port);
nvs_set_u8(h, "iperf_enabled", enable ? 1 : 0);
nvs_commit(h);
nvs_close(h);
}
}
// --- Public Setters ---
void wifi_cfg_set_credentials(const char* ssid, const char* pass) {
nvs_write_str("ssid", ssid);
nvs_write_str("pass", pass);
}
void wifi_cfg_set_static_ip(const char* ip, const char* mask, const char* gw) {
nvs_write_str("ip", ip);
nvs_write_str("mask", mask);
nvs_write_str("gw", gw);
}
void wifi_cfg_set_dhcp(bool enable) {
nvs_write_u8("dhcp", enable ? 1 : 0);
}
// --- Init & Load ---
void wifi_cfg_init(void) {
nvs_flash_init();
ESP_LOGI(TAG, "Iperf Config Saved: Target=%s Role=%s Period=%lu", dst_ip?dst_ip:"", role?role:"", (unsigned long)period);
}
// --- Load Logic (Network Only - Iperf loads itself) ---
static bool load_cfg(char* ssid, size_t ssz, char* pass, size_t psz,
char* ip, size_t isz, char* mask, size_t msz, char* gw, size_t gsz,
char* band, size_t bsz, char* bw, size_t bwsz, char* powersave, size_t pssz,
char* mode, size_t modesz, uint8_t* mon_ch, bool* dhcp){
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return false;
size_t len;
// Load SSID (Mandatory)
len = ssz;
if (nvs_get_str(h, "ssid", ssid, &len) != ESP_OK) { nvs_close(h); return false; }
// Load Optionals
len = psz; if (nvs_get_str(h, "pass", pass, &len) != ESP_OK) pass[0]=0;
len = isz; if (nvs_get_str(h, "ip", ip, &len) != ESP_OK) ip[0]=0;
len = msz; if (nvs_get_str(h, "mask", mask, &len) != ESP_OK) mask[0]=0;
len = gsz; if (nvs_get_str(h, "gw", gw, &len) != ESP_OK) gw[0]=0;
// Defaults
len = bsz; if (nvs_get_str(h, "band", band, &len) != ESP_OK) strcpy(band, "2.4G");
len = bwsz; if (nvs_get_str(h, "bw", bw, &len) != ESP_OK) strcpy(bw, "HT20");
len = pssz; if (nvs_get_str(h, "powersave", powersave, &len) != ESP_OK) strcpy(powersave, "NONE");
len = modesz; if (nvs_get_str(h, "mode", mode, &len) != ESP_OK) strcpy(mode, "STA");
esp_err_t e;
if ((e = nvs_get_str(h, "ssid", NULL, &len)) != ESP_OK){ nvs_close(h); return false; }
if (len >= ssz){ nvs_close(h); return false; }
nvs_get_str(h, "ssid", ssid, &len);
len = psz; e = nvs_get_str(h, "pass", pass, &len); if (e!=ESP_OK) pass[0]=0;
len = isz; e = nvs_get_str(h, "ip", ip, &len); if (e!=ESP_OK) ip[0]=0;
len = msz; e = nvs_get_str(h, "mask", mask, &len); if (e!=ESP_OK) mask[0]=0;
len = gsz; e = nvs_get_str(h, "gw", gw, &len); if (e!=ESP_OK) gw[0]=0;
len = bsz; e = nvs_get_str(h, "band", band, &len); if (e!=ESP_OK) strcpy(band, "2.4G");
len = bwsz; e = nvs_get_str(h, "bw", bw, &len); if (e!=ESP_OK) strcpy(bw, "HT20");
len = pssz; e = nvs_get_str(h, "powersave", powersave, &len); if (e!=ESP_OK) strcpy(powersave, "NONE");
len = modesz; e = nvs_get_str(h, "mode", mode, &len); if (e!=ESP_OK) strcpy(mode, "STA");
uint8_t ch=36; nvs_get_u8(h, "mon_ch", &ch); *mon_ch = ch;
uint8_t d=1; nvs_get_u8(h, "dhcp", &d); *dhcp = (d!=0);
nvs_close(h);
return true;
}
void wifi_cfg_force_dhcp(bool enable){ cfg_dhcp = enable; }
bool wifi_cfg_get_mode(char *mode, uint8_t *mon_ch) {
if (!mode || !mon_ch) return false;
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) {
strcpy(mode, "STA"); *mon_ch = 36; return false;
}
size_t len = 16;
if (nvs_get_str(h, "mode", mode, &len) != ESP_OK) strcpy(mode, "STA");
uint8_t ch = 36;
nvs_get_u8(h, "mon_ch", &ch);
*mon_ch = ch;
nvs_close(h);
return true;
}
static atomic_bool s_net_stack_ready = false;
static esp_err_t ensure_net_stack_once(void) {
bool expected = false;
if (atomic_compare_exchange_strong(&s_net_stack_ready, &expected, true)) {
ESP_RETURN_ON_ERROR(esp_netif_init(), TAG, "esp_netif_init");
esp_err_t err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_RETURN_ON_ERROR(err, TAG, "event loop create");
}
}
return ESP_OK;
}
static atomic_bool s_wifi_inited = false;
esp_err_t wifi_ensure_inited(void) {
bool expected = false;
if (!atomic_compare_exchange_strong(&s_wifi_inited, &expected, true)) return ESP_OK;
ESP_RETURN_ON_ERROR(ensure_net_stack_once(), TAG, "net stack");
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_err_t err = esp_wifi_init(&cfg);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
atomic_store(&s_wifi_inited, false);
ESP_RETURN_ON_ERROR(err, TAG, "esp_wifi_init");
}
return ESP_OK;
}
static void apply_ip_static(const char* ip, const char* mask, const char* gw){
if (!sta_netif) return;
if (!ip || !ip[0]) return;
if (!ip || !ip[0] || !mask || !mask[0] || !gw || !gw[0]) return;
esp_netif_ip_info_t info = {0};
esp_netif_dhcpc_stop(sta_netif);
info.ip.addr = esp_ip4addr_aton(ip);
info.netmask.addr = (mask && mask[0]) ? esp_ip4addr_aton(mask) : esp_ip4addr_aton("255.255.255.0");
info.gw.addr = (gw && gw[0]) ? esp_ip4addr_aton(gw) : 0;
esp_netif_set_ip_info(sta_netif, &info);
info.netmask.addr = esp_ip4addr_aton(mask);
info.gw.addr = esp_ip4addr_aton(gw);
ESP_ERROR_CHECK( esp_netif_set_ip_info(sta_netif, &info) );
}
bool wifi_cfg_apply_from_nvs(void) {
@ -112,11 +165,16 @@ bool wifi_cfg_apply_from_nvs(void) {
band,sizeof(band), bw,sizeof(bw), powersave,sizeof(powersave), mode,sizeof(mode), &mon_ch, &dhcp)){
return false;
}
if (ssid[0] == '\0') return false;
static bool inited = false;
if (!inited){
nvs_flash_init();
ensure_net_stack_once();
if (sta_netif == NULL) sta_netif = esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
wifi_ensure_inited();
inited = true;
}
wifi_config_t wcfg = {0};
strlcpy((char*)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid));
@ -125,32 +183,192 @@ bool wifi_cfg_apply_from_nvs(void) {
wcfg.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;
wcfg.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
if (strcmp(band, "5G") == 0) wcfg.sta.channel = 0;
else wcfg.sta.channel = 0;
esp_wifi_set_mode(WIFI_MODE_STA);
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C6
wifi_protocols_t protocols = {
.ghz_2g = WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N,
.ghz_5g = WIFI_PROTOCOL_11A | WIFI_PROTOCOL_11N | WIFI_PROTOCOL_11AC | WIFI_PROTOCOL_11AX,
};
esp_wifi_set_protocols(WIFI_IF_STA, &protocols);
#else
esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N);
#endif
esp_wifi_set_config(WIFI_IF_STA, &wcfg);
if (!dhcp && ip[0]) apply_ip_static(ip, mask, gw);
else esp_netif_dhcpc_start(sta_netif);
else if (sta_netif) esp_netif_dhcpc_start(sta_netif);
#if CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C6
wifi_bandwidths_t bandwidths = {.ghz_2g = WIFI_BW_HT20, .ghz_5g = WIFI_BW_HT20};
if (strcmp(bw, "VHT80") == 0) { bandwidths.ghz_2g = WIFI_BW_HT40; bandwidths.ghz_5g = WIFI_BW80; }
else if (strcmp(bw, "HT40") == 0) { bandwidths.ghz_2g = WIFI_BW_HT40; bandwidths.ghz_5g = WIFI_BW_HT40; }
esp_wifi_set_bandwidths(WIFI_IF_STA, &bandwidths);
#else
wifi_bandwidth_t bandwidth = WIFI_BW_HT20;
if (strcmp(bw, "HT40") == 0) bandwidth = WIFI_BW_HT40;
esp_wifi_set_bandwidth(WIFI_IF_STA, bandwidth);
#endif
esp_wifi_start();
wifi_ps_type_t ps_mode = WIFI_PS_NONE;
if (strcmp(powersave, "MIN") == 0) ps_mode = WIFI_PS_MIN_MODEM;
else if (strcmp(powersave, "MAX") == 0) ps_mode = WIFI_PS_MAX_MODEM;
esp_wifi_set_ps(ps_mode);
esp_wifi_connect();
return true;
}
// --- Parsing State ---
typedef struct {
// Network
char ssid[64];
char pass[64];
char ip[32];
char mask[32];
char gw[32];
char band[16];
char bw[16];
char powersave[16];
char mode[16];
uint8_t mon_ch;
bool dhcp;
bool csi_enable;
// Iperf
char iperf_dest[32];
char iperf_role[16];
char iperf_proto[8];
uint32_t iperf_period_us;
uint32_t iperf_burst;
uint32_t iperf_len;
uint32_t iperf_port;
bool iperf_enable;
} cfg_state_t;
static void on_cfg_line(const char *line, cfg_state_t *s){
// Network Parsing
if (strncmp(line, "SSID:",5)==0){ strncpy(s->ssid, line+5, 63); s->ssid[63]=0; return; }
if (strncmp(line, "PASS:",5)==0){ strncpy(s->pass, line+5, 63); s->pass[63]=0; return; }
if (strncmp(line, "IP:",3)==0){ strncpy(s->ip, line+3, 31); s->ip[31]=0; return; }
if (strncmp(line, "MASK:",5)==0){ strncpy(s->mask, line+5, 31); s->mask[31]=0; return; }
if (strncmp(line, "GW:",3)==0){ strncpy(s->gw, line+3, 31); s->gw[31]=0; return; }
if (strncmp(line, "BAND:",5)==0){ strncpy(s->band, line+5, 15); s->band[15]=0; return; }
if (strncmp(line, "BW:",3)==0){ strncpy(s->bw, line+3, 15); s->bw[15]=0; return; }
if (strncmp(line, "POWERSAVE:",10)==0){ strncpy(s->powersave, line+10, 15); s->powersave[15]=0; return; }
if (strncmp(line, "MODE:",5)==0){ strncpy(s->mode, line+5, 15); s->mode[15]=0; return; }
if (strncmp(line, "MON_CH:",7)==0){ s->mon_ch = atoi(line+7); return; }
if (strncmp(line, "DHCP:",5)==0){ s->dhcp = atoi(line+5) ? true:false; return; }
// Iperf Parsing (Matches Python Script Keys)
// Support both DEST and DST to be safe
if (strncmp(line, "IPERF_DEST_IP:", 14) == 0) { strncpy(s->iperf_dest, line+14, 31); s->iperf_dest[31]=0; return; }
if (strncmp(line, "IPERF_DST_IP:", 13) == 0) { strncpy(s->iperf_dest, line+13, 31); s->iperf_dest[31]=0; return; }
if (strncmp(line, "IPERF_ROLE:", 11) == 0) { strncpy(s->iperf_role, line+11, 15); s->iperf_role[15]=0; return; }
if (strncmp(line, "IPERF_PROTO:", 12) == 0) { strncpy(s->iperf_proto, line+12, 7); s->iperf_proto[7]=0; return; }
if (strncmp(line, "IPERF_PERIOD_US:", 16) == 0) { s->iperf_period_us = atoi(line+16); return; }
if (strncmp(line, "IPERF_BURST:", 12) == 0) { s->iperf_burst = atoi(line+12); return; }
if (strncmp(line, "IPERF_LEN:", 10) == 0) { s->iperf_len = atoi(line+10); return; }
if (strncmp(line, "IPERF_PORT:", 11) == 0) { s->iperf_port = atoi(line+11); return; }
if (strncmp(line, "IPERF_ENABLED:", 14) == 0) { s->iperf_enable = atoi(line+14) ? true:false; return; }
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
if (strncmp(line, "CSI:",4)==0){ s->csi_enable = atoi(line+4) ? true:false; return; }
#endif
}
static bool wifi_cfg_cmd_handler(const char *line, cmd_reply_func_t reply_func, void *reply_ctx) {
static bool in_cfg = false;
static cfg_state_t s;
if (!in_cfg) {
if (strcmp(line, "CFG") == 0) {
in_cfg = true;
// Clear all buffers
memset(&s, 0, sizeof(s));
// Set Defaults
s.mon_ch = 36;
s.dhcp = true;
s.csi_enable = false;
s.iperf_period_us = 10000;
s.iperf_burst = 1;
s.iperf_len = 1470;
s.iperf_port = 5001;
s.iperf_enable = true;
strcpy(s.iperf_dest, "192.168.1.50");
strcpy(s.iperf_role, "CLIENT");
strcpy(s.iperf_proto, "UDP");
return true;
}
return false;
}
if (strcmp(line, "END") == 0) {
// Apply Network Defaults if missing
if (!s.band[0]) strcpy(s.band, "2.4G");
if (!s.bw[0]) strcpy(s.bw, "HT20");
if (!s.powersave[0]) strcpy(s.powersave, "NONE");
if (!s.mode[0]) strcpy(s.mode, "STA");
// 1. Save Network
save_net_cfg(s.ssid, s.pass, s.ip, s.mask, s.gw, s.dhcp, s.band, s.bw, s.powersave, s.mode, s.mon_ch);
// 2. Save Iperf
save_iperf_cfg(s.iperf_dest, s.iperf_role, s.iperf_proto, s.iperf_period_us, s.iperf_burst, s.iperf_len, s.iperf_port, s.iperf_enable);
// 3. Save CSI
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
csi_mgr_save_enable_state(s.csi_enable);
#endif
if (reply_func) reply_func("Config saved. Reconnecting...\n", reply_ctx);
// Apply changes immediately
wifi_cfg_apply_from_nvs();
in_cfg = false;
return true;
}
on_cfg_line(line, &s);
return true;
}
void wifi_cfg_init(void){
nvs_flash_init();
cmd_transport_init();
cmd_transport_register_listener(wifi_cfg_cmd_handler);
}
wifi_ps_type_t wifi_cfg_get_power_save_mode(void) {
char powersave[16] = {0};
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return WIFI_PS_NONE;
size_t len = sizeof(powersave);
nvs_get_str(h, "powersave", powersave, &len);
nvs_close(h);
if (strcmp(powersave, "MIN") == 0) return WIFI_PS_MIN_MODEM;
if (strcmp(powersave, "MAX") == 0) return WIFI_PS_MAX_MODEM;
return WIFI_PS_NONE;
}
bool wifi_cfg_get_bandwidth(char *buf, size_t buf_size) {
if (buf) strncpy(buf, "HT20", buf_size);
return true;
}
bool wifi_cfg_get_mode(char *mode, uint8_t *mon_ch) {
if (!buf || buf_size < 1) return false;
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return false;
size_t len = 16;
if (nvs_get_str(h, "mode", mode, &len) != ESP_OK) strcpy(mode, "STA");
nvs_get_u8(h, "mon_ch", mon_ch);
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) {
strncpy(buf, "Unknown", buf_size); return false;
}
size_t len = buf_size;
esp_err_t err = nvs_get_str(h, "bw", buf, &len);
nvs_close(h);
return true;
return (err == ESP_OK);
}

View File

@ -8,19 +8,59 @@
extern "C" {
#endif
// --- Initialization ---
/**
* @brief Initialize the WiFi configuration system
*
* Spawns listener tasks for both UART and USB-Serial/JTAG interfaces
* to receive configuration commands.
*/
void wifi_cfg_init(void);
// --- Getters (Used by Controller) ---
/**
* @brief Apply WiFi configuration from NVS
*
* @return true if configuration was found and applied, false otherwise
*/
bool wifi_cfg_apply_from_nvs(void);
/**
* @brief Force DHCP mode enable/disable
*
* @param enable true to enable DHCP, false to disable
*/
void wifi_cfg_force_dhcp(bool enable);
/**
* @brief Get the configured power save mode from NVS
*
* @return wifi_ps_type_t Power save mode (WIFI_PS_NONE, WIFI_PS_MIN_MODEM, or WIFI_PS_MAX_MODEM)
*/
wifi_ps_type_t wifi_cfg_get_power_save_mode(void);
/**
* @brief Get the configured bandwidth from NVS
*
* @param buf Buffer to store bandwidth string (e.g., "HT20", "HT40", "VHT80")
* @param buf_size Size of buffer
* @return true if bandwidth was retrieved, false otherwise
*/
bool wifi_cfg_get_bandwidth(char *buf, size_t buf_size);
/**
* @brief Get operating mode and monitor channel from NVS
*
* @param mode Output buffer for mode string (min 16 bytes)
* @param mon_ch Output pointer for monitor channel
* @return true if mode retrieved, false if no config in NVS
*/
bool wifi_cfg_get_mode(char *mode, uint8_t *mon_ch);
// --- Setters (Used by Console) ---
void wifi_cfg_set_credentials(const char* ssid, const char* pass);
void wifi_cfg_set_static_ip(const char* ip, const char* mask, const char* gw);
void wifi_cfg_set_dhcp(bool enable);
/**
* @brief Ensure WiFi driver is initialized (thread-safe, idempotent)
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t wifi_ensure_inited(void);
#ifdef __cplusplus
}

View File

@ -4,137 +4,78 @@ import argparse
import serial_asyncio
import sys
import re
import glob
# --- 1. Protocol Class (Async Logic) ---
class SerialController(asyncio.Protocol):
def __init__(self, port_name, cmd_type, value, loop, completion_future, sync_event):
def __init__(self, port_name, args, loop, completion_future):
self.port_name = port_name
self.cmd_type = cmd_type # 'start', 'stop', 'pps', 'status'
self.value = value
self.args = args
self.loop = loop
self.transport = None
self.buffer = ""
self.completion_future = completion_future
self.sync_event = sync_event # Global trigger for tight sync
self.status_data = {}
# Command Construction
if self.cmd_type == 'pps':
self.cmd_str = f"iperf pps {self.value}\n"
if args.action == 'pps':
self.cmd_str = f"iperf pps {args.value}\n"
self.target_key = "IPERF_PPS_UPDATED"
elif self.cmd_type == 'status':
elif args.action == 'status':
self.cmd_str = "iperf status\n"
self.target_key = "IPERF_STATUS"
elif self.cmd_type == 'start':
elif args.action == 'start':
self.cmd_str = "iperf start\n"
self.target_key = "IPERF_STARTED"
elif self.cmd_type == 'stop':
elif args.action == 'stop':
self.cmd_str = "iperf stop\n"
self.target_key = "IPERF_STOPPED"
def connection_made(self, transport):
self.transport = transport
# 1. Wake up the device immediately
transport.write(b'\n')
# 2. Schedule the command to wait for the global trigger
self.loop.create_task(self.await_trigger_and_send())
self.loop.create_task(self.send_command())
async def await_trigger_and_send(self):
# 3. Wait here until trigger is fired (allows sync start/stop)
if self.sync_event:
await self.sync_event.wait()
# 4. FIRE!
async def send_command(self):
await asyncio.sleep(0.1)
self.transport.write(self.cmd_str.encode())
def data_received(self, data):
self.buffer += data.decode(errors='ignore')
# FIX: Process complete lines only to avoid partial regex matching
while '\n' in self.buffer:
line, self.buffer = self.buffer.split('\n', 1)
line = line.strip()
# --- Status Parsing ---
if self.cmd_type == 'status':
if "IPERF_STATUS" in line:
m = re.search(r'Src=([\d\.]+), Dst=([\d\.]+), Running=(\d+), Config=(\d+), Actual=(\d+), Err=([-\d\.]+)%, Pkts=(\d+), AvgBW=([\d\.]+) Mbps', line)
if m:
self.status_data['main'] = {
'src': m.group(1), 'dst': m.group(2),
'run': "Run" if m.group(3) == '1' else "Stop",
'cfg': m.group(4), 'act': m.group(5),
'err': m.group(6), 'pkts': m.group(7), 'bw': m.group(8)
}
elif "IPERF_STATES" in line:
m = re.search(r'TX=([\d\.]+)s/([\d\.]+)% \((\d+)\), SLOW=([\d\.]+)s/([\d\.]+)% \((\d+)\), STALLED=([\d\.]+)s/([\d\.]+)% \((\d+)\)', line)
if m:
self.status_data['states'] = {
'tx_t': m.group(1), 'tx_p': m.group(2), 'tx_c': m.group(3),
'sl_t': m.group(4), 'sl_p': m.group(5), 'sl_c': m.group(6),
'st_t': m.group(7), 'st_p': m.group(8), 'st_c': m.group(9)
}
if 'main' in self.status_data and 'states' in self.status_data:
if not self.completion_future.done():
d = self.status_data['main']
s = self.status_data['states']
output = (f"{d['src']} -> {d['dst']} | {d['run']}, "
f"Cfg:{d['cfg']}, Act:{d['act']}, Err:{d['err']}%, Pkts:{d['pkts']}, BW:{d['bw']}M | "
f"TX:{s['tx_t']}s/{s['tx_p']}%({s['tx_c']}) "
f"SL:{s['sl_t']}s/{s['sl_p']}%({s['sl_c']}) "
f"ST:{s['st_t']}s/{s['st_p']}%({s['st_c']})")
self.completion_future.set_result(output)
self.transport.close()
return
# --- Simple Command Parsing ---
else:
if self.target_key in line:
if not self.completion_future.done():
if self.args.action == 'status':
# FIX: Added [-]? to allow negative error rates (overshoot)
# Regex: Err=([-\d\.]+)%
m = re.search(r'Running=(\d+), Config=(\d+), Actual=(\d+), Err=([-\d\.]+)%', line)
if m:
state = "Running" if m.group(1) == '1' else "Stopped"
self.completion_future.set_result(f"{state}, Cfg: {m.group(2)}, Act: {m.group(3)}, Err: {m.group(4)}%")
else:
# Now if it fails, it's a true format mismatch, not fragmentation
self.completion_future.set_result(f"Parse Error on line: {line}")
else:
self.completion_future.set_result(True)
self.transport.close()
return
def connection_lost(self, exc):
if not self.completion_future.done():
if self.cmd_type == 'status' and 'main' in self.status_data:
d = self.status_data['main']
output = (f"{d['src']} -> {d['dst']} | {d['run']}, "
f"Cfg:{d['cfg']}, Act:{d['act']}, BW:{d['bw']}M (Partial)")
self.completion_future.set_result(output)
else:
self.completion_future.set_exception(Exception("Closed"))
# --- 2. Helper Functions ---
def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument('action', choices=['start', 'stop', 'pps', 'status', 'step-all'])
parser.add_argument('value_arg', nargs='?', type=int, help='Value for PPS')
parser.add_argument('--value', type=int, help='Value for PPS')
parser.add_argument('--devices', required=True, help="List (e.g. /dev/ttyUSB0, /dev/ttyUSB1), Range (/dev/ttyUSB0-29), or 'all'")
args = parser.parse_args()
if args.value_arg is not None: args.value = args.value_arg
if args.action == 'pps' and args.value is None:
print("Error: 'pps' action requires a value")
sys.exit(1)
return args
def natural_sort_key(s):
return [int(text) if text.isdigit() else text.lower()
for text in re.split('([0-9]+)', s)]
async def run_device(port, args):
loop = asyncio.get_running_loop()
fut = loop.create_future()
try:
await serial_asyncio.create_serial_connection(
loop, lambda: SerialController(port, args, loop, fut), port, baudrate=115200)
return await asyncio.wait_for(fut, timeout=2.0)
except:
return None
def expand_devices(device_str):
if device_str.lower() == 'all':
devices = glob.glob('/dev/ttyUSB*')
devices.sort(key=natural_sort_key)
if not devices:
print("Error: No /dev/ttyUSB* devices found!")
sys.exit(1)
return devices
devices = []
parts = [d.strip() for d in device_str.split(',')]
for part in parts:
@ -148,89 +89,34 @@ def expand_devices(device_str):
devices.append(part)
return devices
# --- 3. Async Execution Core ---
async def run_single_cmd(port, cmd_type, value, sync_event=None):
"""Runs a single command on a single port."""
loop = asyncio.get_running_loop()
fut = loop.create_future()
try:
await serial_asyncio.create_serial_connection(
loop,
lambda: SerialController(port, cmd_type, value, loop, fut, sync_event),
port,
baudrate=115200
)
return await asyncio.wait_for(fut, timeout=5.0)
except asyncio.TimeoutError:
return None
except Exception:
return None
async def main():
parser = argparse.ArgumentParser()
parser.add_argument('action', choices=['start', 'stop', 'pps', 'status'])
parser.add_argument('value_arg', nargs='?', type=int, help='Value for PPS')
parser.add_argument('--value', type=int, help='Value for PPS')
parser.add_argument('--devices', required=True, help="/dev/ttyUSB0-29")
async def run_parallel_action(devices, action, value):
"""Runs the specified action on all devices in parallel."""
print(f"Initializing {len(devices)} devices for '{action}'...")
sync_event = asyncio.Event()
tasks = [run_single_cmd(d, action, value, sync_event) for d in devices]
args = parser.parse_args()
if args.value_arg is not None: args.value = args.value_arg
if args.action == 'pps' and args.value is None:
print("Error: 'pps' action requires a value")
sys.exit(1)
# Allow connections to settle
await asyncio.sleep(0.5)
if sys.platform == 'win32': asyncio.set_event_loop(asyncio.ProactorEventLoop())
# Fire all commands at once
sync_event.set()
devs = expand_devices(args.devices)
print(f"Executing '{args.action}' on {len(devs)} devices...")
tasks = [run_device(d, args) for d in devs]
results = await asyncio.gather(*tasks)
print("\nResults:")
for dev, res in zip(devices, results):
if action == 'status':
for dev, res in zip(devs, results):
if args.action == 'status':
print(f"{dev}: {res if res else 'TIMEOUT'}")
else:
status = "OK" if res is True else "FAIL"
print(f"{dev}: {status}")
async def run_step_all(devices):
"""Stops all, then starts/stops devices one by one."""
print("\n>>> STEP-ALL PHASE 1: STOPPING ALL DEVICES <<<")
await run_parallel_action(devices, 'stop', None)
print("\n>>> STEP-ALL PHASE 2: SEQUENTIAL TEST <<<")
for i, dev in enumerate(devices):
print(f"\n[{i+1}/{len(devices)}] Testing {dev}...")
# Start
print(f" -> Starting {dev}...")
res_start = await run_single_cmd(dev, 'start', None, None) # No sync needed for single
if res_start is not True:
print(f" -> FAILED to start {dev}. Skipping.")
continue
# Wait (Run Traffic)
print(" -> Running traffic (5 seconds)...")
await asyncio.sleep(5)
# Stop
print(f" -> Stopping {dev}...")
res_stop = await run_single_cmd(dev, 'stop', None, None)
if res_stop is not True:
print(f" -> Warning: Failed to stop {dev}")
else:
print(f" -> {dev} OK")
# Wait (Gap between devices)
print(" -> Waiting 1 second...")
await asyncio.sleep(1)
async def async_main(args, devices):
if args.action == 'step-all':
await run_step_all(devices)
else:
await run_parallel_action(devices, args.action, args.value)
# --- 4. Main Execution Block ---
if __name__ == '__main__':
args = parse_arguments()
dev_list = expand_devices(args.devices)
if sys.platform == 'win32':
asyncio.set_event_loop(asyncio.ProactorEventLoop())
asyncio.run(async_main(args, dev_list))
asyncio.run(main())

View File

@ -1,8 +1,17 @@
#!/usr/bin/env python3
import sys
import os
"""
ESP32 Unified Deployment Tool (esp32_deploy)
Combines firmware flashing and device configuration with full control.
Updates:
- ADDED: --ip-device-based support (IP = Start_IP + Port_Number)
- FIXED: Robust IP calculation logic
- PRESERVED: Existing flash/monitor/config workflow
"""
import asyncio
import serial_asyncio
import sys
import os
import argparse
import ipaddress
import re
@ -10,7 +19,6 @@ import time
import shutil
import logging
import glob
import random
from pathlib import Path
# Ensure detection script is available
@ -69,58 +77,33 @@ def auto_detect_devices():
return detect_esp32.detect_esp32_devices()
class UnifiedDeployWorker:
def __init__(self, port, target_ip, args, project_dir, flash_sem, total_devs):
def __init__(self, port, target_ip, args, project_dir, flash_sem):
self.port = port
self.target_ip = target_ip
self.args = args
self.project_dir = Path(project_dir)
self.flash_sem = flash_sem
self.total_devs = total_devs
self.log = DeviceLoggerAdapter(logger, {'connid': port})
self.regex_chip_type = re.compile(r'Detecting chip type... (ESP32\S*)')
# Updated regex to look for the Shell Prompt
self.regex_prompt = re.compile(r'esp32>', re.IGNORECASE)
# Matches the log from your updated main.c
self.regex_ready = re.compile(r'Entering idle loop|esp32>', re.IGNORECASE)
self.regex_got_ip = re.compile(r'got ip:(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE)
self.regex_version = re.compile(r'APP_VERSION:\s*([0-9\.]+)', re.IGNORECASE)
self.regex_csi_saved = re.compile(r'CSI enable state saved|Config saved', re.IGNORECASE)
async def run(self):
try:
if self.args.check_version:
return await self._query_version()
# --- Acquire Semaphore EARLY to protect Chip ID Detection ---
async with self.flash_sem:
detected_target = None
if self.args.target == 'auto' and not self.args.config_only:
detected_target = await self._identify_chip()
if not detected_target:
self.log.error("Failed to auto-detect chip type.")
return False
self.log.info(f"Auto-detected: {Colors.CYAN}{detected_target}{Colors.RESET}")
target_to_use = detected_target
else:
target_to_use = self.args.target
if not self.args.config_only:
async with self.flash_sem:
if self.args.flash_erase:
if not await self._erase_flash(): return False
self.target_for_flash = target_to_use
if not await self._flash_firmware(): return False
# --- Semaphore Released Here ---
# Give it a moment to stabilize after flash reset
await asyncio.sleep(2.0)
if not self.args.flash_only:
if self.args.ssid and self.args.password:
# Thundering Herd Mitigation
if self.total_devs > 1:
delay = random.uniform(0, self.total_devs * 0.5)
self.log.info(f"Staggering config start by {delay:.1f}s...")
await asyncio.sleep(delay)
# Retry logic
success = False
for attempt in range(1, 4):
self.log.info(f"Configuring (Attempt {attempt}/3)...")
@ -135,140 +118,21 @@ class UnifiedDeployWorker:
return False
else:
self.log.warning("No SSID/Password provided, skipping config")
if self.args.config_only: return False
return True
except Exception as e:
self.log.error(f"Worker Exception: {e}")
return False
async def _configure_device(self):
try:
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
except Exception as e:
return False
try:
# Reset DTR/RTS logic
writer.transport.serial.dtr = False
writer.transport.serial.rts = True
await asyncio.sleep(0.1)
writer.transport.serial.rts = False
writer.transport.serial.dtr = False
# 1. Wait for Shell Prompt
if not await self._wait_for_prompt(reader, writer, timeout=15):
self.log.error("Shell prompt not detected.")
return False
# 2. Send Configuration via CLI
# Command: wifi_config -s "SSID" -p "PASS" -i "IP"
# Note: The Shell will auto-reboot after this command.
cmd = f'wifi_config -s "{self.args.ssid}" -p "{self.args.password}" -i "{self.target_ip}"'
if not self.args.iperf_client and not self.args.iperf_server:
# If just connecting, maybe we want DHCP?
# But if target_ip is set, we force static.
pass
self.log.info(f"Sending: {cmd}")
writer.write(f"{cmd}\n".encode())
await writer.drain()
# 3. Wait for the reboot and new prompt
# The device prints "Rebooting..." then restarts.
self.log.info("Waiting for reboot...")
await asyncio.sleep(3.0) # Give it time to actually reset
if not await self._wait_for_prompt(reader, writer, timeout=20):
self.log.error("Device did not return to prompt after reboot.")
return False
self.log.info(f"{Colors.GREEN}Reboot complete. Shell Ready.{Colors.RESET}")
# 4. (Optional) Start iperf if requested
# The new firmware does not auto-start iperf on boot unless commanded.
if not self.args.no_iperf:
self.log.info("Starting iperf listener...")
writer.write(b"iperf start\n")
await writer.drain()
await asyncio.sleep(0.5)
return True
except Exception as e:
self.log.error(f"Config Error: {e}")
return False
finally:
writer.close()
await writer.wait_closed()
async def _wait_for_prompt(self, reader, writer, timeout):
end_time = time.time() + timeout
last_poke = time.time()
while time.time() < end_time:
# Poke 'enter' occasionally to solicit a prompt
if time.time() - last_poke > 1.0:
writer.write(b'\n')
await writer.drain()
last_poke = time.time()
try:
line_bytes = await asyncio.wait_for(reader.read(1024), timeout=0.1)
output = line_bytes.decode('utf-8', errors='ignore')
if "esp32>" in output:
return True
except asyncio.TimeoutError:
continue
except Exception:
break
return False
# [Keep _query_version, _identify_chip, _erase_flash, _flash_firmware AS IS]
async def _query_version(self):
try:
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
writer.transport.serial.dtr = False
writer.transport.serial.rts = False
writer.write(b'\n')
await writer.drain()
await asyncio.sleep(0.1)
writer.write(b'version\n')
await writer.drain()
found_version = "Unknown"
timeout = time.time() + 2.0
while time.time() < timeout:
try:
line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.5)
line = line_bytes.decode('utf-8', errors='ignore').strip()
m = self.regex_version.search(line)
if m:
found_version = m.group(1)
break
except asyncio.TimeoutError:
continue
writer.close()
await writer.wait_closed()
return found_version
except Exception as e:
self.log.error(f"Version Check Error: {e}")
return "Error"
async def _identify_chip(self):
for attempt in range(1, 4):
cmd = ['esptool.py', '-p', self.port, 'chip_id']
try:
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate()
output = stdout.decode() + stderr.decode()
match = self.regex_chip_type.search(output)
if match:
return match.group(1).lower().replace('-', '')
if attempt < 3: await asyncio.sleep(1.0)
except Exception as e:
self.log.warning(f"Chip ID check exception (Attempt {attempt}): {e}")
return None
async def _erase_flash(self):
@ -280,21 +144,27 @@ class UnifiedDeployWorker:
return False
async def _flash_firmware(self):
target_to_use = getattr(self, 'target_for_flash', self.args.target)
if target_to_use == 'auto':
self.log.error("Logic Error: Target is still 'auto' inside flash firmware.")
detected_target = None
if self.args.target == 'auto':
detected_target = await self._identify_chip()
if not detected_target:
self.log.error("Failed to auto-detect chip type.")
return False
self.log.info(f"Auto-detected: {Colors.CYAN}{detected_target}{Colors.RESET}")
target_to_use = detected_target
else:
target_to_use = self.args.target
suffix = generate_config_suffix(target_to_use, self.args.csi_enable, self.args.ampdu)
firmware_dir = self.project_dir / "firmware"
unique_app = None
unique_app = None
if firmware_dir.exists():
for f in os.listdir(firmware_dir):
if f.endswith(f"_{suffix}.bin") and not f.startswith("bootloader") and not f.startswith("partition") and not f.startswith("ota_data") and not f.startswith("phy_init"):
unique_app = f
break
if not unique_app:
self.log.error(f"Binary for config '{suffix}' not found in firmware/.")
return False
@ -302,8 +172,9 @@ class UnifiedDeployWorker:
unique_boot = f"bootloader_{suffix}.bin"
unique_part = f"partition-table_{suffix}.bin"
unique_ota = f"ota_data_initial_{suffix}.bin"
flash_args_path = firmware_dir / f"flash_args_{suffix}"
unique_args_file = f"flash_args_{suffix}"
flash_args_path = firmware_dir / unique_args_file
if not flash_args_path.exists():
self.log.error(f"flash_args for {suffix} not found")
return False
@ -311,24 +182,33 @@ class UnifiedDeployWorker:
try:
with open(flash_args_path, 'r') as f:
content = f.read().replace('\n', ' ').strip()
raw_args = [x for x in content.split(' ') if x]
final_args = []
for arg in raw_args:
if arg.endswith('bootloader.bin'): final_args.append(str(firmware_dir / unique_boot))
elif arg.endswith('partition-table.bin'): final_args.append(str(firmware_dir / unique_part))
if arg.endswith('bootloader.bin'):
final_args.append(str(firmware_dir / unique_boot))
elif arg.endswith('partition-table.bin'):
final_args.append(str(firmware_dir / unique_part))
elif arg.endswith('ota_data_initial.bin'):
if (firmware_dir / unique_ota).exists(): final_args.append(str(firmware_dir / unique_ota))
else: continue
elif arg.endswith('phy_init_data.bin'): final_args.append(arg)
elif arg.endswith('.bin'): final_args.append(str(firmware_dir / unique_app))
else: final_args.append(arg)
if (firmware_dir / unique_ota).exists():
final_args.append(str(firmware_dir / unique_ota))
else:
continue
elif arg.endswith('phy_init_data.bin'):
final_args.append(arg)
elif arg.endswith('.bin'):
final_args.append(str(firmware_dir / unique_app))
else:
final_args.append(arg)
cmd = ['esptool.py', '-p', self.port, '-b', str(self.args.baud),
'--before', 'default_reset', '--after', 'hard_reset',
'write_flash'] + final_args
self.log.info(f"Flashing {firmware_dir / unique_app}...")
full_path = firmware_dir / unique_app
self.log.info(f"Flashing {full_path}...")
proc = await asyncio.create_subprocess_exec(*cmd, cwd=self.project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
try:
@ -340,10 +220,146 @@ class UnifiedDeployWorker:
if proc.returncode == 0: return True
self.log.error(f"Flash failed: {stderr.decode()}")
return False
except Exception as e:
self.log.error(f"Flash Prep Error: {e}")
return False
async def _configure_device(self):
try:
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
except Exception as e:
return False
try:
# 1. Reset
writer.transport.serial.dtr = False
writer.transport.serial.rts = True
await asyncio.sleep(0.1)
writer.transport.serial.rts = False
# FIX: DTR Must be False to allow Booting (True=Low=Bootloader Mode)
writer.transport.serial.dtr = False
# 2. Robust Wait (with Poke)
if not await self._wait_for_boot(reader, writer):
self.log.warning("Boot prompt missed (sending blindly)...")
# 3. Send
await self._send_config(writer)
# 4. Verify
is_configured = await self._verify_configuration(reader)
if is_configured:
self.log.info(f"{Colors.GREEN}Config verified. IP: {self.target_ip}{Colors.RESET}")
# Final Reset to apply
writer.transport.serial.dtr = False
writer.transport.serial.rts = True
await asyncio.sleep(0.1)
writer.transport.serial.rts = False
return True
else:
self.log.error(f"{Colors.RED}Config verification failed.{Colors.RESET}")
return False
except Exception as e:
self.log.error(f"Config Error: {e}")
return False
finally:
writer.close()
await writer.wait_closed()
async def _wait_for_boot(self, reader, writer):
# Timeout covers GPS delay (~3.5s) + boot overhead
end_time = time.time() + 12
last_poke = time.time()
while time.time() < end_time:
try:
# Poke every 1.5 seconds if we haven't seen the prompt
if time.time() - last_poke > 1.5:
writer.write(b'\n')
await writer.drain()
last_poke = time.time()
try:
# Short timeout to allow polling loop
line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.1)
line = line_bytes.decode('utf-8', errors='ignore').strip()
if not line: continue
if self.regex_ready.search(line):
return True
except asyncio.TimeoutError:
continue
except Exception as e:
self.log.error(f"Read error: {e}")
return False
return False
async def _send_config(self, writer):
# Wait a moment for any last boot logs to clear
await asyncio.sleep(0.5)
# Wake up console
writer.write(b'\n')
await writer.drain()
await asyncio.sleep(0.2)
csi_val = '1' if self.args.csi_enable else '0'
role_str = "SERVER" if self.args.iperf_server else "CLIENT"
iperf_enable_val = '0' if self.args.no_iperf else '1'
period_us = int(self.args.iperf_period * 1000000)
# Build list of commands using args (which have robust defaults)
config_lines = [
"CFG",
f"SSID:{self.args.ssid}",
f"PASS:{self.args.password}",
f"IP:{self.target_ip}",
f"MASK:{self.args.netmask}",
f"GW:{self.args.gateway}",
f"DHCP:0",
f"BAND:{self.args.band}",
f"BW:{self.args.bandwidth}",
f"POWERSAVE:{self.args.powersave}",
f"MODE:{self.args.mode}",
f"MON_CH:{self.args.monitor_channel}",
f"CSI:{csi_val}",
f"IPERF_PERIOD_US:{period_us}",
f"IPERF_ROLE:{role_str}",
f"IPERF_PROTO:{self.args.iperf_proto}",
f"IPERF_DST_IP:{self.args.iperf_dest_ip}",
f"IPERF_PORT:{self.args.iperf_port}",
f"IPERF_BURST:{self.args.iperf_burst}",
f"IPERF_LEN:{self.args.iperf_len}",
f"IPERF_ENABLED:{iperf_enable_val}",
"END"
]
# CHANGED: Send line-by-line with a delay to prevent UART FIFO overflow
for line in config_lines:
cmd = line + "\r\n"
writer.write(cmd.encode('utf-8'))
await writer.drain()
# 50ms delay allows the ESP32 (running at 115200 baud) to process the line
await asyncio.sleep(0.1)
async def _verify_configuration(self, reader):
timeout = time.time() + 15
while time.time() < timeout:
try:
line_bytes = await asyncio.wait_for(reader.readline(), timeout=1.0)
line = line_bytes.decode('utf-8', errors='ignore').strip()
if not line: continue
if self.regex_csi_saved.search(line): return True
m = self.regex_got_ip.search(line)
if m and m.group(1) == self.target_ip: return True
except asyncio.TimeoutError: continue
return False
def parse_args():
parser = argparse.ArgumentParser(description='ESP32 Unified Deployment Tool')
parser.add_argument('-i', '--interactive', action='store_true', help='Prompt for build options')
@ -354,13 +370,16 @@ def parse_args():
parser.add_argument('--config-only', action='store_true')
parser.add_argument('--flash-only', action='store_true')
parser.add_argument('--flash-erase', action='store_true')
parser.add_argument('--check-version', action='store_true', help='Check version of connected devices')
parser.add_argument('-d', '--dir', default=os.getcwd())
parser.add_argument('-b', '--baud', type=int, default=460800)
parser.add_argument('--devices', type=str)
parser.add_argument('--max-concurrent', type=int, default=None)
parser.add_argument('--start-ip', help='Start IP (Required unless --target all or --check-version)')
# --- IP Configuration ---
parser.add_argument('--start-ip', help='Start IP (Required unless --target all)')
parser.add_argument('--ip-device-based', action='store_true', help="Use /dev/ttyUSBx number as IP offset")
# --- Network Defaults (Robustness) ---
parser.add_argument('-s', '--ssid', default='ClubHouse2G')
parser.add_argument('-P', '--password', default='ez2remember')
parser.add_argument('-g', '--gateway', default='192.168.1.1')
@ -368,6 +387,8 @@ def parse_args():
parser.add_argument('--band', default='2.4G')
parser.add_argument('-B', '--bandwidth', default='HT20')
parser.add_argument('-ps', '--powersave', default='NONE')
# --- Iperf Defaults (Robustness) ---
parser.add_argument('--iperf-period', type=float, default=0.01)
parser.add_argument('--iperf-burst', type=int, default=1)
parser.add_argument('--iperf-len', type=int, default=1470)
@ -377,16 +398,28 @@ def parse_args():
parser.add_argument('--no-iperf', action='store_true')
parser.add_argument('--iperf-client', action='store_true')
parser.add_argument('--iperf-server', action='store_true')
# --- Monitor Mode Defaults ---
parser.add_argument('-M', '--mode', default='STA')
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
parser.add_argument('--csi', dest='csi_enable', action='store_true')
args = parser.parse_args()
if args.target != 'all' and not args.start_ip and not args.check_version:
if args.target != 'all' and not args.start_ip:
parser.error("the following arguments are required: --start-ip")
if args.config_only and args.flash_only: parser.error("Conflicting modes")
if not args.config_only and not args.flash_only and args.target != 'all':
if not args.ssid or not args.password:
parser.error("SSID/PASS required")
return args
def extract_device_number(device_path):
"""
Extracts the integer number from a device path.
e.g. /dev/ttyUSB14 -> 14
/dev/esp_port_14 -> 14
"""
match = re.search(r'(\d+)$', device_path)
return int(match.group(1)) if match else 0
@ -419,18 +452,22 @@ async def build_task(project_dir, target, csi, ampdu, current_step=None, total_s
desc = f"Target={target}, CSI={'ON' if csi else 'OFF'}, AMPDU={'ON' if ampdu else 'OFF'}"
prefix = f"[{current_step}/{total_steps}] " if current_step else ""
print(f" {prefix}Building [{desc}] ... ", end='', flush=True)
try:
output_dir = project_dir / "firmware"
output_dir.mkdir(exist_ok=True)
sdkconfig_path = project_dir / "sdkconfig"
build_path = project_dir / "build"
if sdkconfig_path.exists(): os.remove(sdkconfig_path)
if build_path.exists(): shutil.rmtree(build_path)
proc = await asyncio.create_subprocess_exec('idf.py', 'set-target', target, cwd=project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
_, stderr = await proc.communicate()
if proc.returncode != 0:
print(f"{Colors.RED}FAIL (Set Target){Colors.RESET}")
return False, f"Set Target Failed", 0
start_time = time.time()
build_cmd = ['idf.py', '-D', f'SDKCONFIG_DEFAULTS={defaults_str}', 'build']
proc = await asyncio.create_subprocess_exec(*build_cmd, cwd=project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
@ -439,24 +476,33 @@ async def build_task(project_dir, target, csi, ampdu, current_step=None, total_s
if proc.returncode != 0:
print(f"{Colors.RED}FAIL{Colors.RESET}")
return False, f"Build Failed", duration
build_dir = project_dir / 'build'
suffix = generate_config_suffix(target, csi, ampdu)
unique_app_name = "Unknown"
project_bin = get_project_binary_name(build_dir)
if project_bin:
unique_app_name = f"{os.path.splitext(project_bin)[0]}_{suffix}.bin"
shutil.copy2(build_dir / project_bin, output_dir / unique_app_name)
boot_src = build_dir / "bootloader" / "bootloader.bin"
if boot_src.exists(): shutil.copy2(boot_src, output_dir / f"bootloader_{suffix}.bin")
part_src = build_dir / "partition_table" / "partition-table.bin"
if part_src.exists(): shutil.copy2(part_src, output_dir / f"partition-table_{suffix}.bin")
# Fix: Save OTA data binary if it exists
ota_src = build_dir / "ota_data_initial.bin"
if ota_src.exists(): shutil.copy2(ota_src, output_dir / f"ota_data_initial_{suffix}.bin")
flash_src = build_dir / "flash_args"
if flash_src.exists(): shutil.copy2(flash_src, output_dir / f"flash_args_{suffix}")
full_path = output_dir / unique_app_name
print(f"{Colors.GREEN}OK ({duration:.1f}s) -> {full_path}{Colors.RESET}")
return True, "Success", duration
except Exception as e:
print(f"{Colors.RED}ERROR: {e}{Colors.RESET}")
return False, str(e), 0
@ -464,34 +510,52 @@ async def build_task(project_dir, target, csi, ampdu, current_step=None, total_s
async def run_deployment(args):
print(f"\n{Colors.BLUE}{'='*60}{Colors.RESET}\n ESP32 Unified Deployment Tool\n{Colors.BLUE}{'='*60}{Colors.RESET}")
project_dir = Path(args.dir).resolve()
# --- Target 'ALL' Mode ---
if args.target == 'all':
print(f"{Colors.YELLOW}Starting Batch Build Verification (12 Combinations){Colors.RESET}")
# SAFETY: Wipe firmware dir to ensure no stale binaries exist
firmware_dir = project_dir / "firmware"
if firmware_dir.exists():
try: shutil.rmtree(firmware_dir)
except Exception as e: return
try:
shutil.rmtree(firmware_dir)
print(f"{Colors.YELLOW} [Clean] Removed old firmware/ directory.{Colors.RESET}")
except Exception as e:
print(f"{Colors.RED} [Error] Could not clean firmware dir: {e}{Colors.RESET}")
return
# Re-create it fresh
firmware_dir.mkdir(exist_ok=True)
print("") # Spacer
targets = ['esp32', 'esp32s3', 'esp32c5']
booleans = [False, True]
results = []
total_steps = len(targets) * len(booleans) * len(booleans)
current_step = 0
for target in targets:
for csi in booleans:
for ampdu in booleans:
current_step += 1
success, msg, dur = await build_task(project_dir, target, csi, ampdu, current_step, total_steps)
results.append({"cfg": f"{target.ljust(9)} CSI:{'ON ' if csi else 'OFF'} AMPDU:{'ON ' if ampdu else 'OFF'}", "ok": success, "dur": dur})
print(f"\n{Colors.BLUE}Batch Summary:{Colors.RESET}")
for r in results:
status = f"{Colors.GREEN}PASS{Colors.RESET}" if r['ok'] else f"{Colors.RED}FAIL{Colors.RESET}"
print(f" {r['cfg']} : {status} ({r['dur']:.1f}s)")
return
if not args.config_only and args.target != 'auto' and not args.check_version:
# --- Single Build Configuration ---
# Skip build if we are in AUTO mode (we assume binaries exist in firmware/)
if not args.config_only and args.target != 'auto':
target = args.target if args.target else 'esp32s3'
csi = args.csi_enable
ampdu = args.ampdu
if args.interactive:
print(f"\n{Colors.YELLOW}--- Build Configuration ---{Colors.RESET}")
target = ask_user("Target Chip", default=target, choices=['esp32', 'esp32s3', 'esp32c5'])
@ -500,55 +564,46 @@ async def run_deployment(args):
args.csi_enable = csi
args.target = target
args.ampdu = ampdu
success, msg, _ = await build_task(project_dir, target, csi, ampdu, 1, 1)
if not success:
print(f"{Colors.RED}{msg}{Colors.RESET}")
return
elif args.target == 'auto' and not args.config_only and not args.check_version:
elif args.target == 'auto' and not args.config_only:
print(f"{Colors.YELLOW}Target 'auto' selected. Skipping build step (assuming artifacts in firmware/).{Colors.RESET}")
if args.devices and args.devices.lower() != 'all':
# --- Device Detection & Flash ---
if args.devices:
devs = [type('obj', (object,), {'device': d.strip()}) for d in args.devices.split(',')]
else:
# Use AUTO DETECT first (for static names), then standard fallback
devs = auto_detect_devices()
if not devs: print("No devices found"); return
# Sort naturally (esp_port_01 before esp_port_10)
devs.sort(key=lambda d: [int(c) if c.isdigit() else c for c in re.split(r'(\d+)', d.device)])
print(f"\n{Colors.GREEN}Found {len(devs)} devices{Colors.RESET}")
start_ip = ipaddress.IPv4Address(args.start_ip) if args.start_ip else ipaddress.IPv4Address('0.0.0.0')
start_ip = ipaddress.IPv4Address(args.start_ip)
max_c = args.max_concurrent if args.max_concurrent else (1 if args.devices and not args.config_only else DEFAULT_MAX_CONCURRENT_FLASH)
flash_sem = asyncio.Semaphore(max_c)
tasks = []
for i, dev in enumerate(devs):
raw_port_number = extract_device_number(dev.device)
# --- ROBUST IP CALCULATION LOGIC ---
if args.ip_device_based:
if "esp_port" in dev.device:
offset = raw_port_number - 1
else:
offset = raw_port_number
target_ip = str(start_ip + offset)
if not args.check_version:
print(f" [{dev.device}] Device-based IP: {target_ip} (Raw: {raw_port_number}, Offset: {offset})")
# Mode A: Offset based on physical port number (e.g. 14 for ttyUSB14)
offset = extract_device_number(dev.device)
print(f" [{dev.device}] Using device-based IP offset: +{offset}")
else:
# Mode B: Sequential offset based on loop index
offset = i
target_ip = str(start_ip + offset)
if not args.check_version:
print(f" [{dev.device}] Sequential IP: {target_ip} (Offset: +{offset})")
print(f" [{dev.device}] Using sequential IP offset: +{offset}")
tasks.append(UnifiedDeployWorker(dev.device, target_ip, args, project_dir, flash_sem, len(devs)).run())
target_ip = str(start_ip + offset)
tasks.append(UnifiedDeployWorker(dev.device, target_ip, args, project_dir, flash_sem).run())
results = await asyncio.gather(*tasks)
if args.check_version:
print(f"\n{Colors.BLUE}--- FIRMWARE VERSION AUDIT ---{Colors.RESET}")
print(f"{'Device':<20} | {'Version':<15}")
print("-" * 40)
for dev, res in zip(devs, results):
ver_color = Colors.GREEN if res != "Unknown" and res != "Error" else Colors.RED
print(f"{dev.device:<20} | {ver_color}{res:<15}{Colors.RESET}")
return
success = results.count(True)
print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}")

View File

@ -1,3 +1,23 @@
idf_component_register(SRCS "main.c"
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
PRIV_REQUIRES nvs_flash esp_netif wifi_controller wifi_cfg app_console iperf status_led gps_sync console)
PRIV_REQUIRES
nvs_flash
esp_wifi
esp_netif
esp_event
lwip
console
driver # <--- Added (For GPIO definitions)
cmd_transport # <--- Added (For cmd_transport.h)
iperf
wifi_cfg
csi_log
wifi_monitor
gps_sync
led_strip
status_led
csi_manager
wifi_controller
app_console
)

View File

@ -1,45 +1,37 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H
#pragma once
#include "driver/gpio.h"
#include "sdkconfig.h"
// --- Hardware Configuration ---
// ============================================================================
// ESP32-C5 (DevKitC-1)
// ============================================================================
#ifdef CONFIG_IDF_TARGET_ESP32C5
#define RGB_LED_GPIO 8 // Common addressable LED pin for C5
#define HAS_RGB_LED 1
#endif
// ============================================================================
// ESP32-S3 (DevKitC-1)
// ============================================================================
#ifdef CONFIG_IDF_TARGET_ESP32S3
// Most S3 DevKits use GPIO 48 for the addressable RGB LED.
// If yours uses GPIO 38, change this value.
#if defined(CONFIG_IDF_TARGET_ESP32S3)
// ESP32-S3
#define RGB_LED_GPIO 48
#define HAS_RGB_LED 1
#endif
#define GPS_TX_PIN GPIO_NUM_5
#define GPS_RX_PIN GPIO_NUM_4
#define GPS_PPS_PIN GPIO_NUM_6
// ============================================================================
// ESP32 (Original / Standard)
// ============================================================================
#ifdef CONFIG_IDF_TARGET_ESP32
// Standard ESP32 DevKits usually have a single blue LED on GPIO 2.
// They rarely have an addressable RGB LED built-in.
#define RGB_LED_GPIO 2
#define HAS_RGB_LED 0
#endif
#elif defined(CONFIG_IDF_TARGET_ESP32C5)
// ESP32-C5
#define RGB_LED_GPIO 27
#define HAS_RGB_LED 1
#define GPS_TX_PIN GPIO_NUM_24
#define GPS_RX_PIN GPIO_NUM_23
#define GPS_PPS_PIN GPIO_NUM_25
// ============================================================================
// Fallbacks (Prevent Compilation Errors)
// ============================================================================
#ifndef RGB_LED_GPIO
#define RGB_LED_GPIO 2
#endif
#elif defined(CONFIG_IDF_TARGET_ESP32)
// ESP32 (Original)
#define RGB_LED_GPIO 2 // Standard Blue LED
#define HAS_RGB_LED 0 // Not RGB
#define GPS_TX_PIN GPIO_NUM_17
#define GPS_RX_PIN GPIO_NUM_16
#define GPS_PPS_PIN GPIO_NUM_4
#ifndef HAS_RGB_LED
#define HAS_RGB_LED 0
#else
// Fallback
#define RGB_LED_GPIO 8
#define HAS_RGB_LED 1
#define GPS_TX_PIN GPIO_NUM_1
#define GPS_RX_PIN GPIO_NUM_3
#define GPS_PPS_PIN GPIO_NUM_5
#endif
#endif // BOARD_CONFIG_H

View File

@ -1,110 +1,153 @@
#include <stdio.h>
#include <string.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_console.h"
#include "esp_vfs_dev.h"
#include "driver/uart.h"
// REMOVED: #include "linenoise/linenoise.h" <-- CAUSE OF CONFLICT
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "lwip/inet.h"
// Components
#include "status_led.h"
#include "board_config.h"
#include "wifi_controller.h"
#include "status_led.h"
#include "gps_sync.h"
#include "wifi_cfg.h"
#include "wifi_controller.h"
#include "app_console.h"
#include "iperf.h"
// GUARDED INCLUDE
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
#include "csi_log.h"
#include "csi_manager.h"
#endif
#define APP_VERSION "2.0.0-SHELL"
static const char *TAG = "MAIN";
// --- System Commands ---
// --- Event Handler -------------------------------------------------
static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
if (event_base == WIFI_EVENT) {
if (event_id == WIFI_EVENT_STA_START) {
if (wifi_ctl_get_mode() == WIFI_CTL_MODE_STA) {
status_led_set_state(LED_STATE_WAITING);
}
}
else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (wifi_ctl_get_mode() == WIFI_CTL_MODE_STA) {
status_led_set_state(LED_STATE_FAILED);
}
}
}
// In event_handler function inside main.c
static int cmd_restart(int argc, char **argv) {
ESP_LOGI(TAG, "Restarting...");
esp_restart();
return 0;
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
if (wifi_ctl_get_mode() != WIFI_CTL_MODE_STA) return;
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
status_led_set_state(LED_STATE_CONNECTED);
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
if (csi_mgr_should_enable()) {
ESP_LOGI(TAG, "CSI enabled in config - starting capture");
csi_mgr_enable_async();
csi_mgr_schedule_dump();
} else {
ESP_LOGI(TAG, "CSI disabled in config - skipping capture");
}
#endif
// iperf_start() will fill this from NVS (including Dest IP and Role)
iperf_cfg_t cfg = { 0 };
iperf_start(&cfg);
}
}
static int cmd_version(int argc, char **argv) {
printf("APP_VERSION: %s\n", APP_VERSION);
printf("IDF_VERSION: %s\n", esp_get_idf_version());
return 0;
}
static void register_system_common(void) {
const esp_console_cmd_t restart_cmd = {
.command = "reset",
.help = "Software reset of the device",
.func = &cmd_restart
};
ESP_ERROR_CHECK(esp_console_cmd_register(&restart_cmd));
const esp_console_cmd_t version_cmd = {
.command = "version",
.help = "Get firmware version",
.func = &cmd_version
};
ESP_ERROR_CHECK(esp_console_cmd_register(&version_cmd));
}
// --- Main Application ---
// --- Main ----------------------------------------------------------
void app_main(void) {
// 1. Initialize NVS
// 1. System Init
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 2. Initialize Netif & Event Loop
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
// 3. Hardware Init
status_led_init(RGB_LED_GPIO, HAS_RGB_LED);
// 2. Hardware/Driver Init
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
ESP_ERROR_CHECK(csi_log_init());
#endif
status_led_init(RGB_LED_GPIO, HAS_RGB_LED);
const gps_sync_config_t gps_cfg = {
.uart_port = UART_NUM_1,
.tx_pin = GPS_TX_PIN,
.rx_pin = GPS_RX_PIN,
.pps_pin = GPS_PPS_PIN,
};
gps_sync_init(&gps_cfg, true);
// 3. Subsystem Init
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
csi_mgr_init();
#endif
// 4. Initialize WiFi Controller (Loads config from NVS automatically)
wifi_ctl_init();
// 5. Initialize Console
esp_console_repl_t *repl = NULL;
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
// THIS call starts the cmd_transport (UART listener task)
// which effectively replaces the manual console loop below.
wifi_cfg_init();
// This prompt is the anchor for your Python script
repl_config.prompt = "esp32> ";
repl_config.max_cmdline_length = 1024;
// 4. Console Registry Init (Still needed for registering commands)
esp_console_config_t console_config = {
.max_cmdline_args = 8,
.max_cmdline_length = 256,
};
ESP_ERROR_CHECK(esp_console_init(&console_config));
esp_console_register_help_command();
// Install UART driver for Console (Standard IO)
esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl));
// 6. Register Commands
register_system_common();
// Register App Commands
app_console_register_commands();
// 7. Start Shell
printf("\n ==================================================\n");
printf(" | ESP32 iPerf Shell - Ready |\n");
printf(" | Type 'help' for commands |\n");
printf(" ==================================================\n");
// 5. Register Events
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
// This function runs the REPL loop and does not return
ESP_ERROR_CHECK(esp_console_start_repl(repl));
// 6. Application Start
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
bool csi_enabled = csi_mgr_should_enable();
ESP_LOGI(TAG, "CSI Capture: %s", csi_enabled ? "ENABLED" : "DISABLED");
#endif
if (wifi_cfg_apply_from_nvs()) {
status_led_set_state(LED_STATE_WAITING);
char mode[16] = {0};
uint8_t mon_ch = 36;
if (wifi_cfg_get_mode(mode, &mon_ch) && strcmp(mode, "MONITOR") == 0) {
wifi_ctl_auto_monitor_start(mon_ch);
}
} else {
status_led_set_state(LED_STATE_NO_CONFIG);
ESP_LOGW(TAG, "No Config Found. Waiting for setup...");
}
// 7. Keep Main Task Alive
// We removed linenoise because cmd_transport.c is already reading UART.
ESP_LOGI(TAG, "Initialization complete. Entering idle loop.");
while (true) {
// Just sleep forever. cmd_transport task handles input.
// main event loop task handles wifi events.
vTaskDelay(pdMS_TO_TICKS(1000));
}
}