Compare commits
No commits in common. "538d2031857d18474f3f33298f5133a9ef66bde7" and "d1be8327900d3ae0e7a39ad373ac0dc679fc04d7" have entirely different histories.
538d203185
...
d1be832790
|
|
@ -1,3 +1,4 @@
|
||||||
idf_component_register(SRCS "app_console.c"
|
idf_component_register(SRCS "app_console.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
PRIV_REQUIRES console wifi_cfg iperf)
|
REQUIRES console
|
||||||
|
PRIV_REQUIRES wifi_controller csi_manager status_led gps_sync esp_wifi iperf)
|
||||||
|
|
|
||||||
|
|
@ -2,161 +2,111 @@
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "argtable3/argtable3.h"
|
#include "argtable3/argtable3.h"
|
||||||
#include "wifi_cfg.h"
|
#include <stdio.h>
|
||||||
#include "iperf.h"
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
// ============================================================================
|
// Dependencies
|
||||||
// COMMAND: iperf
|
#include "wifi_controller.h"
|
||||||
// ============================================================================
|
#include "status_led.h"
|
||||||
static struct {
|
#include "gps_sync.h"
|
||||||
struct arg_lit *start;
|
#include "iperf.h"
|
||||||
struct arg_lit *stop;
|
|
||||||
struct arg_lit *status;
|
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
struct arg_int *pps;
|
#include "csi_manager.h"
|
||||||
struct arg_lit *help;
|
#endif
|
||||||
struct arg_end *end;
|
|
||||||
} iperf_args;
|
// --- Command Handlers ---
|
||||||
|
|
||||||
static int cmd_iperf(int argc, char **argv) {
|
static int cmd_iperf(int argc, char **argv) {
|
||||||
int nerrors = arg_parse(argc, argv, (void **)&iperf_args);
|
if (argc < 2) {
|
||||||
if (nerrors > 0) {
|
printf("Usage: iperf <start|stop|pps|status>\n");
|
||||||
arg_print_errors(stderr, iperf_args.end, argv[0]);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iperf_args.help->count > 0) {
|
if (strcmp(argv[1], "start") == 0) {
|
||||||
printf("Usage: iperf [start|stop|status] [--pps <n>]\n");
|
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;
|
return 0;
|
||||||
}
|
|
||||||
|
|
||||||
if (iperf_args.stop->count > 0) {
|
} else if (strcmp(argv[1], "stop") == 0) {
|
||||||
iperf_stop();
|
iperf_stop();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
|
||||||
|
|
||||||
if (iperf_args.pps->count > 0) {
|
} else if (strcmp(argv[1], "pps") == 0) {
|
||||||
int val = iperf_args.pps->ival[0];
|
// Syntax: iperf pps 100
|
||||||
if (val > 0) {
|
if (argc < 3) {
|
||||||
iperf_set_pps((uint32_t)val);
|
printf("Error: Missing value. Usage: iperf pps <rate>\n");
|
||||||
} else {
|
return 1;
|
||||||
printf("Error: PPS must be > 0\n");
|
|
||||||
}
|
}
|
||||||
|
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;
|
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();
|
iperf_print_status();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iperf_args.start->count > 0) {
|
printf("Error: Unknown subcommand '%s'.\n", argv[1]);
|
||||||
// Start using saved NVS config
|
return 1;
|
||||||
iperf_cfg_t cfg = { .time = 0 };
|
}
|
||||||
iperf_start(&cfg);
|
|
||||||
return 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;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void register_iperf_cmd(void) {
|
static int cmd_mode_sta(int argc, char **argv) {
|
||||||
iperf_args.start = arg_lit0(NULL, "start", "Start iperf traffic");
|
if (wifi_ctl_switch_to_sta(WIFI_BAND_MODE_AUTO) != ESP_OK) {
|
||||||
iperf_args.stop = arg_lit0(NULL, "stop", "Stop iperf traffic");
|
printf("Failed to switch to STA mode\n");
|
||||||
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]);
|
|
||||||
return 1;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void register_wifi_cmd(void) {
|
static int cmd_mode_status(int argc, char **argv) {
|
||||||
wifi_args.ssid = arg_str0("s", "ssid", "<ssid>", "WiFi SSID");
|
wifi_ctl_mode_t mode = wifi_ctl_get_mode();
|
||||||
wifi_args.pass = arg_str0("p", "password", "<pass>", "WiFi Password");
|
printf("\n=== WiFi Mode Status ===\n");
|
||||||
wifi_args.ip = arg_str0("i", "ip", "<ip>", "Static IP");
|
printf("Current mode: %s\n", mode == WIFI_CTL_MODE_STA ? "STA" : "MONITOR");
|
||||||
wifi_args.dhcp = arg_lit0("d", "dhcp", "Enable DHCP");
|
printf("LED state: %d\n", status_led_get_state());
|
||||||
wifi_args.help = arg_lit0("h", "help", "Show help");
|
printf("GPS synced: %s\n", gps_is_synced() ? "Yes" : "No");
|
||||||
wifi_args.end = arg_end(20);
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const esp_console_cmd_t cmd = {
|
static int cmd_csi_dump(int argc, char **argv) {
|
||||||
.command = "wifi_config",
|
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
.help = "Configure WiFi credentials",
|
csi_mgr_schedule_dump();
|
||||||
.hint = NULL,
|
#else
|
||||||
.func = &cmd_wifi_config,
|
printf("Error: CSI feature is disabled in this firmware build.\n");
|
||||||
.argtable = &wifi_args
|
#endif
|
||||||
};
|
return 0;
|
||||||
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void app_console_register_commands(void) {
|
void app_console_register_commands(void) {
|
||||||
register_iperf_cmd();
|
const esp_console_cmd_t cmds[] = {
|
||||||
register_wifi_cmd();
|
{ .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]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ extern "C" {
|
||||||
#endif
|
#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);
|
void app_console_register_commands(void);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
idf_component_register(SRCS "cmd_transport.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
PRIV_REQUIRES console driver soc esp_driver_usb_serial_jtag)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -39,58 +39,14 @@ typedef struct {
|
||||||
|
|
||||||
static iperf_ctrl_t s_iperf_ctrl = {0};
|
static iperf_ctrl_t s_iperf_ctrl = {0};
|
||||||
static TaskHandle_t s_iperf_task_handle = NULL;
|
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
|
// Global Stats Tracker
|
||||||
static iperf_stats_t s_stats = {0};
|
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_any_id;
|
||||||
static esp_event_handler_instance_t instance_got_ip;
|
static esp_event_handler_instance_t instance_got_ip;
|
||||||
|
|
||||||
// --- Helper: Pattern Initialization ---
|
// --- Status Reporting ---
|
||||||
// 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] ...
|
|
||||||
|
|
||||||
void iperf_get_stats(iperf_stats_t *stats) {
|
void iperf_get_stats(iperf_stats_t *stats) {
|
||||||
if (stats) {
|
if (stats) {
|
||||||
s_stats.config_pps = (s_iperf_ctrl.cfg.pacing_period_us > 0) ?
|
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) {
|
void iperf_print_status(void) {
|
||||||
iperf_get_stats(&s_stats);
|
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;
|
float err = 0.0f;
|
||||||
if (s_stats.running && s_stats.config_pps > 0) {
|
if (s_stats.running && s_stats.config_pps > 0) {
|
||||||
int32_t diff = (int32_t)s_stats.config_pps - (int32_t)s_stats.actual_pps;
|
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;
|
err = (float)diff * 100.0f / (float)s_stats.config_pps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Compute Session Bandwidth
|
printf("IPERF_STATUS: Running=%d, Config=%" PRIu32 ", Actual=%" PRIu32 ", Err=%.1f%%\n",
|
||||||
float avg_bw_mbps = 0.0f;
|
s_stats.running, s_stats.config_pps, s_stats.actual_pps, err);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Network Events ---
|
// --- Network Events ---
|
||||||
|
|
@ -256,8 +167,8 @@ uint32_t iperf_get_pps(void) {
|
||||||
return 1000000 / s_iperf_ctrl.cfg.pacing_period_us;
|
return 1000000 / s_iperf_ctrl.cfg.pacing_period_us;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
|
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()) {
|
if (!iperf_wait_for_ip()) {
|
||||||
printf("IPERF_STOPPED\n");
|
printf("IPERF_STOPPED\n");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
|
|
@ -276,26 +187,13 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
|
||||||
if (sockfd < 0) {
|
if (sockfd < 0) {
|
||||||
status_led_set_state(LED_STATE_FAILED);
|
status_led_set_state(LED_STATE_FAILED);
|
||||||
ESP_LOGE(TAG, "Socket creation failed: %d", errno);
|
ESP_LOGE(TAG, "Socket creation failed: %d", errno);
|
||||||
|
// FIX 2: Print STOPPED on failure so controller doesn't timeout
|
||||||
printf("IPERF_STOPPED\n");
|
printf("IPERF_STOPPED\n");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
status_led_set_state(LED_STATE_TRANSMITTING_SLOW);
|
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_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");
|
printf("IPERF_STARTED\n");
|
||||||
|
|
||||||
int64_t next_send_time = esp_timer_get_time();
|
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();
|
int64_t last_rate_check = esp_timer_get_time();
|
||||||
uint32_t packets_since_check = 0;
|
uint32_t packets_since_check = 0;
|
||||||
int64_t packet_id = 0;
|
int32_t packet_id = 0;
|
||||||
|
|
||||||
struct timespec ts;
|
struct timespec ts;
|
||||||
|
|
||||||
while (!ctrl->finish && esp_timer_get_time() < end_time) {
|
while (!ctrl->finish && esp_timer_get_time() < end_time) {
|
||||||
|
|
@ -314,31 +213,25 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
|
||||||
else while (esp_timer_get_time() < next_send_time) taskYIELD();
|
else while (esp_timer_get_time() < next_send_time) taskYIELD();
|
||||||
|
|
||||||
for (int k = 0; k < ctrl->cfg.burst_count; k++) {
|
for (int k = 0; k < ctrl->cfg.burst_count; k++) {
|
||||||
int64_t current_id = packet_id++;
|
udp_datagram *hdr = (udp_datagram *)ctrl->buffer;
|
||||||
|
|
||||||
udp_hdr->id = htonl((uint32_t)(current_id & 0xFFFFFFFF));
|
|
||||||
udp_hdr->id2 = htonl((uint32_t)((current_id >> 32) & 0xFFFFFFFF));
|
|
||||||
|
|
||||||
|
hdr->id = htonl(packet_id++);
|
||||||
clock_gettime(CLOCK_REALTIME, &ts);
|
clock_gettime(CLOCK_REALTIME, &ts);
|
||||||
udp_hdr->tv_sec = htonl((uint32_t)ts.tv_sec);
|
hdr->tv_sec = htonl(ts.tv_sec);
|
||||||
udp_hdr->tv_usec = htonl(ts.tv_nsec / 1000);
|
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));
|
int sent = sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr));
|
||||||
|
|
||||||
if (sent > 0) {
|
if (sent > 0) {
|
||||||
packets_since_check++;
|
packets_since_check++;
|
||||||
s_session_packets++;
|
|
||||||
} else {
|
} else {
|
||||||
// --- ROBUST FIX: Never Abort ---
|
if (errno != 12) {
|
||||||
// If send fails (buffer full, routing issue, etc.), we just yield and retry next loop.
|
ESP_LOGE(TAG, "Send failed: %d", errno);
|
||||||
// We do NOT goto exit.
|
status_led_set_state(LED_STATE_FAILED);
|
||||||
if (errno != 12) {
|
goto exit;
|
||||||
// Log rarely to avoid spamming serial
|
}
|
||||||
if ((packet_id % 100) == 0) {
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
ESP_LOGW(TAG, "Send error: %d (Ignored)", errno);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
uint32_t interval_us = (uint32_t)(now - last_rate_check);
|
||||||
if (interval_us > 0) {
|
if (interval_us > 0) {
|
||||||
s_stats.actual_pps = (uint32_t)((uint64_t)packets_since_check * 1000000 / interval_us);
|
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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
uint32_t config_pps = iperf_get_pps();
|
||||||
|
uint32_t threshold = (config_pps * 3) / 4;
|
||||||
|
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);
|
||||||
|
|
||||||
last_rate_check = now;
|
last_rate_check = now;
|
||||||
packets_since_check = 0;
|
packets_since_check = 0;
|
||||||
}
|
}
|
||||||
next_send_time += ctrl->cfg.pacing_period_us;
|
next_send_time += ctrl->cfg.pacing_period_us;
|
||||||
}
|
}
|
||||||
|
|
||||||
udp_datagram *hdr = (udp_datagram *)ctrl->buffer;
|
exit:
|
||||||
int64_t final_id = -packet_id;
|
// Termination Packets
|
||||||
hdr->id = htonl((uint32_t)(final_id & 0xFFFFFFFF));
|
{
|
||||||
hdr->id2 = htonl((uint32_t)((final_id >> 32) & 0xFFFFFFFF));
|
udp_datagram *hdr = (udp_datagram *)ctrl->buffer;
|
||||||
|
int32_t final_id = -packet_id;
|
||||||
|
hdr->id = htonl(final_id);
|
||||||
|
hdr->id2 = hdr->id;
|
||||||
|
|
||||||
clock_gettime(CLOCK_REALTIME, &ts);
|
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);
|
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));
|
for(int i=0; i<10; i++) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(2));
|
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: %ld)", (long)final_id);
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "Sent termination packets (ID: %" PRId64 ")", final_id);
|
|
||||||
|
|
||||||
close(sockfd);
|
close(sockfd);
|
||||||
s_stats.running = false;
|
s_stats.running = false;
|
||||||
s_session_end_time = esp_timer_get_time();
|
|
||||||
s_stats.actual_pps = 0;
|
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");
|
printf("IPERF_STOPPED\n");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void iperf_task(void *arg) {
|
static void iperf_task(void *arg) {
|
||||||
iperf_ctrl_t *ctrl = (iperf_ctrl_t *)arg;
|
iperf_ctrl_t *ctrl = (iperf_ctrl_t *)arg;
|
||||||
|
if (ctrl->cfg.flag & IPERF_FLAG_UDP && ctrl->cfg.flag & IPERF_FLAG_CLIENT) {
|
||||||
do {
|
iperf_start_udp_client(ctrl);
|
||||||
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);
|
free(ctrl->buffer);
|
||||||
s_iperf_task_handle = NULL;
|
s_iperf_task_handle = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void iperf_start(iperf_cfg_t *cfg) {
|
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) {
|
if (s_iperf_task_handle) {
|
||||||
ESP_LOGI(TAG, "Task running. Staging hot reload.");
|
ESP_LOGW(TAG, "Iperf already running");
|
||||||
s_next_cfg = new_cfg;
|
|
||||||
s_reload_req = true;
|
|
||||||
iperf_stop();
|
|
||||||
printf("IPERF_RELOADING\n");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_iperf_ctrl.cfg = new_cfg;
|
s_iperf_ctrl.cfg = *cfg;
|
||||||
|
iperf_read_nvs_config(&s_iperf_ctrl.cfg);
|
||||||
|
|
||||||
|
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.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);
|
||||||
|
|
||||||
if (s_iperf_ctrl.buffer == NULL) {
|
s_iperf_event_group = xEventGroupCreate();
|
||||||
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);
|
xTaskCreate(iperf_task, "iperf", 4096, &s_iperf_ctrl, 5, &s_iperf_task_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,6 @@
|
||||||
#define IPERF_FLAG_TCP (1 << 2)
|
#define IPERF_FLAG_TCP (1 << 2)
|
||||||
#define IPERF_FLAG_UDP (1 << 3)
|
#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 ---
|
// --- Defaults ---
|
||||||
#define IPERF_DEFAULT_PORT 5001
|
#define IPERF_DEFAULT_PORT 5001
|
||||||
#define IPERF_DEFAULT_INTERVAL 3
|
#define IPERF_DEFAULT_INTERVAL 3
|
||||||
|
|
@ -54,41 +48,27 @@ typedef struct {
|
||||||
float error_rate;
|
float error_rate;
|
||||||
} iperf_stats_t;
|
} iperf_stats_t;
|
||||||
|
|
||||||
// --- Wire Formats (Strict Layout) ---
|
// --- Header ---
|
||||||
|
|
||||||
// 1. Basic UDP Datagram Header (16 bytes)
|
|
||||||
// Corresponds to 'struct UDP_datagram' in payloads.h
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int32_t id; // Lower 32 bits of seqno
|
int32_t id;
|
||||||
uint32_t tv_sec; // Seconds
|
uint32_t tv_sec;
|
||||||
uint32_t tv_usec; // Microseconds
|
uint32_t tv_usec;
|
||||||
int32_t id2; // Upper 32 bits of seqno (when HEADER_SEQNO64B is set)
|
uint32_t id2;
|
||||||
} udp_datagram;
|
} 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 ---
|
// --- API ---
|
||||||
|
|
||||||
void iperf_init_led(led_strip_handle_t handle);
|
void iperf_init_led(led_strip_handle_t handle);
|
||||||
void iperf_set_pps(uint32_t pps);
|
void iperf_set_pps(uint32_t pps);
|
||||||
uint32_t iperf_get_pps(void);
|
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);
|
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_print_status(void);
|
||||||
|
|
||||||
void iperf_start(iperf_cfg_t *cfg);
|
void iperf_start(iperf_cfg_t *cfg);
|
||||||
void iperf_stop(void);
|
void iperf_stop(void);
|
||||||
|
|
||||||
#endif
|
#endif // IPERF_H
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
idf_component_register(SRCS "wifi_cfg.c"
|
idf_component_register(SRCS "wifi_cfg.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES nvs_flash esp_wifi esp_netif)
|
PRIV_REQUIRES nvs_flash esp_wifi esp_netif driver cmd_transport csi_manager)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
|
@ -9,98 +11,149 @@
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
|
#include "esp_check.h"
|
||||||
|
|
||||||
#include "wifi_cfg.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 esp_netif_t *sta_netif = NULL;
|
||||||
|
static bool cfg_dhcp = true;
|
||||||
|
|
||||||
// --- Helper: NVS Write ---
|
// --- NVS Helper ---
|
||||||
static void nvs_write_str(const char *key, const char *val) {
|
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;
|
nvs_handle_t h;
|
||||||
if (nvs_open("netcfg", NVS_READWRITE, &h) == ESP_OK) {
|
if (nvs_open("netcfg", NVS_READWRITE, &h) != ESP_OK) return;
|
||||||
if (val) nvs_set_str(h, key, val);
|
|
||||||
else nvs_erase_key(h, key);
|
if (ssid) nvs_set_str2(h, "ssid", ssid);
|
||||||
nvs_commit(h);
|
if (pass) nvs_set_str2(h, "pass", pass);
|
||||||
nvs_close(h);
|
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;
|
nvs_handle_t h;
|
||||||
if (nvs_open("netcfg", NVS_READWRITE, &h) == ESP_OK) {
|
if (nvs_open("storage", NVS_READWRITE, &h) != ESP_OK) return;
|
||||||
nvs_set_u8(h, key, val);
|
|
||||||
nvs_commit(h);
|
// Note: Keys must match what iperf.c reads (NVS_KEY_IPERF_DST_IP etc)
|
||||||
nvs_close(h);
|
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);
|
||||||
|
|
||||||
// --- Public Setters ---
|
nvs_set_u32(h, "iperf_period", period);
|
||||||
|
nvs_set_u32(h, "iperf_burst", burst);
|
||||||
void wifi_cfg_set_credentials(const char* ssid, const char* pass) {
|
nvs_set_u32(h, "iperf_len", len);
|
||||||
nvs_write_str("ssid", ssid);
|
nvs_set_u32(h, "iperf_port", port);
|
||||||
nvs_write_str("pass", pass);
|
nvs_set_u8(h, "iperf_enabled", enable ? 1 : 0);
|
||||||
}
|
|
||||||
|
nvs_commit(h);
|
||||||
void wifi_cfg_set_static_ip(const char* ip, const char* mask, const char* gw) {
|
nvs_close(h);
|
||||||
nvs_write_str("ip", ip);
|
ESP_LOGI(TAG, "Iperf Config Saved: Target=%s Role=%s Period=%lu", dst_ip?dst_ip:"", role?role:"", (unsigned long)period);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Load Logic (Network Only - Iperf loads itself) ---
|
||||||
static bool load_cfg(char* ssid, size_t ssz, char* pass, size_t psz,
|
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* 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* 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){
|
char* mode, size_t modesz, uint8_t* mon_ch, bool* dhcp){
|
||||||
nvs_handle_t h;
|
nvs_handle_t h;
|
||||||
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return false;
|
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return false;
|
||||||
|
|
||||||
size_t len;
|
size_t len;
|
||||||
// Load SSID (Mandatory)
|
esp_err_t e;
|
||||||
len = ssz;
|
if ((e = nvs_get_str(h, "ssid", NULL, &len)) != ESP_OK){ nvs_close(h); return false; }
|
||||||
if (nvs_get_str(h, "ssid", ssid, &len) != ESP_OK) { nvs_close(h); return false; }
|
if (len >= ssz){ nvs_close(h); return false; }
|
||||||
|
nvs_get_str(h, "ssid", ssid, &len);
|
||||||
// Load Optionals
|
len = psz; e = nvs_get_str(h, "pass", pass, &len); if (e!=ESP_OK) pass[0]=0;
|
||||||
len = psz; if (nvs_get_str(h, "pass", pass, &len) != ESP_OK) pass[0]=0;
|
len = isz; e = nvs_get_str(h, "ip", ip, &len); if (e!=ESP_OK) ip[0]=0;
|
||||||
len = isz; if (nvs_get_str(h, "ip", ip, &len) != ESP_OK) ip[0]=0;
|
len = msz; e = nvs_get_str(h, "mask", mask, &len); if (e!=ESP_OK) mask[0]=0;
|
||||||
len = msz; if (nvs_get_str(h, "mask", mask, &len) != ESP_OK) mask[0]=0;
|
len = gsz; e = nvs_get_str(h, "gw", gw, &len); if (e!=ESP_OK) gw[0]=0;
|
||||||
len = gsz; if (nvs_get_str(h, "gw", gw, &len) != 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");
|
||||||
// Defaults
|
len = pssz; e = nvs_get_str(h, "powersave", powersave, &len); if (e!=ESP_OK) strcpy(powersave, "NONE");
|
||||||
len = bsz; if (nvs_get_str(h, "band", band, &len) != ESP_OK) strcpy(band, "2.4G");
|
len = modesz; e = nvs_get_str(h, "mode", mode, &len); if (e!=ESP_OK) strcpy(mode, "STA");
|
||||||
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");
|
|
||||||
|
|
||||||
uint8_t ch=36; nvs_get_u8(h, "mon_ch", &ch); *mon_ch = ch;
|
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);
|
uint8_t d=1; nvs_get_u8(h, "dhcp", &d); *dhcp = (d!=0);
|
||||||
|
|
||||||
nvs_close(h);
|
nvs_close(h);
|
||||||
return true;
|
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){
|
static void apply_ip_static(const char* ip, const char* mask, const char* gw){
|
||||||
if (!sta_netif) return;
|
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_ip_info_t info = {0};
|
||||||
esp_netif_dhcpc_stop(sta_netif);
|
esp_netif_dhcpc_stop(sta_netif);
|
||||||
|
|
||||||
info.ip.addr = esp_ip4addr_aton(ip);
|
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.netmask.addr = esp_ip4addr_aton(mask);
|
||||||
info.gw.addr = (gw && gw[0]) ? esp_ip4addr_aton(gw) : 0;
|
info.gw.addr = esp_ip4addr_aton(gw);
|
||||||
|
ESP_ERROR_CHECK( esp_netif_set_ip_info(sta_netif, &info) );
|
||||||
esp_netif_set_ip_info(sta_netif, &info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool wifi_cfg_apply_from_nvs(void) {
|
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)){
|
band,sizeof(band), bw,sizeof(bw), powersave,sizeof(powersave), mode,sizeof(mode), &mon_ch, &dhcp)){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (ssid[0] == '\0') return false;
|
||||||
|
|
||||||
if (sta_netif == NULL) sta_netif = esp_netif_create_default_wifi_sta();
|
static bool inited = false;
|
||||||
|
if (!inited){
|
||||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
nvs_flash_init();
|
||||||
esp_wifi_init(&cfg);
|
ensure_net_stack_once();
|
||||||
|
if (sta_netif == NULL) sta_netif = esp_netif_create_default_wifi_sta();
|
||||||
|
wifi_ensure_inited();
|
||||||
|
inited = true;
|
||||||
|
}
|
||||||
|
|
||||||
wifi_config_t wcfg = {0};
|
wifi_config_t wcfg = {0};
|
||||||
strlcpy((char*)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid));
|
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.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;
|
||||||
wcfg.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
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);
|
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);
|
esp_wifi_set_config(WIFI_IF_STA, &wcfg);
|
||||||
|
|
||||||
if (!dhcp && ip[0]) apply_ip_static(ip, mask, gw);
|
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();
|
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();
|
esp_wifi_connect();
|
||||||
return true;
|
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) {
|
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;
|
return WIFI_PS_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool wifi_cfg_get_bandwidth(char *buf, size_t buf_size) {
|
bool wifi_cfg_get_bandwidth(char *buf, size_t buf_size) {
|
||||||
if (buf) strncpy(buf, "HT20", buf_size);
|
if (!buf || buf_size < 1) return false;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool wifi_cfg_get_mode(char *mode, uint8_t *mon_ch) {
|
|
||||||
nvs_handle_t h;
|
nvs_handle_t h;
|
||||||
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return false;
|
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) {
|
||||||
size_t len = 16;
|
strncpy(buf, "Unknown", buf_size); return false;
|
||||||
if (nvs_get_str(h, "mode", mode, &len) != ESP_OK) strcpy(mode, "STA");
|
}
|
||||||
nvs_get_u8(h, "mon_ch", mon_ch);
|
size_t len = buf_size;
|
||||||
|
esp_err_t err = nvs_get_str(h, "bw", buf, &len);
|
||||||
nvs_close(h);
|
nvs_close(h);
|
||||||
return true;
|
return (err == ESP_OK);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,59 @@
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
* @brief Ensure WiFi driver is initialized (thread-safe, idempotent)
|
||||||
void wifi_cfg_set_static_ip(const char* ip, const char* mask, const char* gw);
|
*
|
||||||
void wifi_cfg_set_dhcp(bool enable);
|
* @return ESP_OK on success, error code otherwise
|
||||||
|
*/
|
||||||
|
esp_err_t wifi_ensure_inited(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
224
control_iperf.py
224
control_iperf.py
|
|
@ -4,137 +4,78 @@ import argparse
|
||||||
import serial_asyncio
|
import serial_asyncio
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import glob
|
|
||||||
|
|
||||||
# --- 1. Protocol Class (Async Logic) ---
|
|
||||||
class SerialController(asyncio.Protocol):
|
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.port_name = port_name
|
||||||
self.cmd_type = cmd_type # 'start', 'stop', 'pps', 'status'
|
self.args = args
|
||||||
self.value = value
|
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.transport = None
|
self.transport = None
|
||||||
self.buffer = ""
|
self.buffer = ""
|
||||||
self.completion_future = completion_future
|
self.completion_future = completion_future
|
||||||
self.sync_event = sync_event # Global trigger for tight sync
|
|
||||||
self.status_data = {}
|
|
||||||
|
|
||||||
# Command Construction
|
if args.action == 'pps':
|
||||||
if self.cmd_type == 'pps':
|
self.cmd_str = f"iperf pps {args.value}\n"
|
||||||
self.cmd_str = f"iperf pps {self.value}\n"
|
|
||||||
self.target_key = "IPERF_PPS_UPDATED"
|
self.target_key = "IPERF_PPS_UPDATED"
|
||||||
elif self.cmd_type == 'status':
|
elif args.action == 'status':
|
||||||
self.cmd_str = "iperf status\n"
|
self.cmd_str = "iperf status\n"
|
||||||
self.target_key = "IPERF_STATUS"
|
self.target_key = "IPERF_STATUS"
|
||||||
elif self.cmd_type == 'start':
|
elif args.action == 'start':
|
||||||
self.cmd_str = "iperf start\n"
|
self.cmd_str = "iperf start\n"
|
||||||
self.target_key = "IPERF_STARTED"
|
self.target_key = "IPERF_STARTED"
|
||||||
elif self.cmd_type == 'stop':
|
elif args.action == 'stop':
|
||||||
self.cmd_str = "iperf stop\n"
|
self.cmd_str = "iperf stop\n"
|
||||||
self.target_key = "IPERF_STOPPED"
|
self.target_key = "IPERF_STOPPED"
|
||||||
|
|
||||||
def connection_made(self, transport):
|
def connection_made(self, transport):
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
# 1. Wake up the device immediately
|
|
||||||
transport.write(b'\n')
|
transport.write(b'\n')
|
||||||
# 2. Schedule the command to wait for the global trigger
|
self.loop.create_task(self.send_command())
|
||||||
self.loop.create_task(self.await_trigger_and_send())
|
|
||||||
|
|
||||||
async def await_trigger_and_send(self):
|
async def send_command(self):
|
||||||
# 3. Wait here until trigger is fired (allows sync start/stop)
|
await asyncio.sleep(0.1)
|
||||||
if self.sync_event:
|
|
||||||
await self.sync_event.wait()
|
|
||||||
|
|
||||||
# 4. FIRE!
|
|
||||||
self.transport.write(self.cmd_str.encode())
|
self.transport.write(self.cmd_str.encode())
|
||||||
|
|
||||||
def data_received(self, data):
|
def data_received(self, data):
|
||||||
self.buffer += data.decode(errors='ignore')
|
self.buffer += data.decode(errors='ignore')
|
||||||
|
|
||||||
|
# FIX: Process complete lines only to avoid partial regex matching
|
||||||
while '\n' in self.buffer:
|
while '\n' in self.buffer:
|
||||||
line, self.buffer = self.buffer.split('\n', 1)
|
line, self.buffer = self.buffer.split('\n', 1)
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
|
|
||||||
# --- Status Parsing ---
|
if self.target_key in line:
|
||||||
if self.cmd_type == 'status':
|
if not self.completion_future.done():
|
||||||
if "IPERF_STATUS" in line:
|
if self.args.action == 'status':
|
||||||
m = re.search(r'Src=([\d\.]+), Dst=([\d\.]+), Running=(\d+), Config=(\d+), Actual=(\d+), Err=([-\d\.]+)%, Pkts=(\d+), AvgBW=([\d\.]+) Mbps', line)
|
# FIX: Added [-]? to allow negative error rates (overshoot)
|
||||||
if m:
|
# Regex: Err=([-\d\.]+)%
|
||||||
self.status_data['main'] = {
|
m = re.search(r'Running=(\d+), Config=(\d+), Actual=(\d+), Err=([-\d\.]+)%', line)
|
||||||
'src': m.group(1), 'dst': m.group(2),
|
if m:
|
||||||
'run': "Run" if m.group(3) == '1' else "Stop",
|
state = "Running" if m.group(1) == '1' else "Stopped"
|
||||||
'cfg': m.group(4), 'act': m.group(5),
|
self.completion_future.set_result(f"{state}, Cfg: {m.group(2)}, Act: {m.group(3)}, Err: {m.group(4)}%")
|
||||||
'err': m.group(6), 'pkts': m.group(7), 'bw': m.group(8)
|
else:
|
||||||
}
|
# Now if it fails, it's a true format mismatch, not fragmentation
|
||||||
|
self.completion_future.set_result(f"Parse Error on line: {line}")
|
||||||
elif "IPERF_STATES" in line:
|
else:
|
||||||
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():
|
|
||||||
self.completion_future.set_result(True)
|
self.completion_future.set_result(True)
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
if not self.completion_future.done():
|
if not self.completion_future.done():
|
||||||
if self.cmd_type == 'status' and 'main' in self.status_data:
|
self.completion_future.set_exception(Exception("Closed"))
|
||||||
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 ---
|
async def run_device(port, args):
|
||||||
def parse_arguments():
|
loop = asyncio.get_running_loop()
|
||||||
parser = argparse.ArgumentParser()
|
fut = loop.create_future()
|
||||||
parser.add_argument('action', choices=['start', 'stop', 'pps', 'status', 'step-all'])
|
try:
|
||||||
parser.add_argument('value_arg', nargs='?', type=int, help='Value for PPS')
|
await serial_asyncio.create_serial_connection(
|
||||||
parser.add_argument('--value', type=int, help='Value for PPS')
|
loop, lambda: SerialController(port, args, loop, fut), port, baudrate=115200)
|
||||||
parser.add_argument('--devices', required=True, help="List (e.g. /dev/ttyUSB0, /dev/ttyUSB1), Range (/dev/ttyUSB0-29), or 'all'")
|
return await asyncio.wait_for(fut, timeout=2.0)
|
||||||
|
except:
|
||||||
args = parser.parse_args()
|
return None
|
||||||
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)]
|
|
||||||
|
|
||||||
def expand_devices(device_str):
|
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 = []
|
devices = []
|
||||||
parts = [d.strip() for d in device_str.split(',')]
|
parts = [d.strip() for d in device_str.split(',')]
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
|
@ -148,89 +89,34 @@ def expand_devices(device_str):
|
||||||
devices.append(part)
|
devices.append(part)
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
# --- 3. Async Execution Core ---
|
async def main():
|
||||||
async def run_single_cmd(port, cmd_type, value, sync_event=None):
|
parser = argparse.ArgumentParser()
|
||||||
"""Runs a single command on a single port."""
|
parser.add_argument('action', choices=['start', 'stop', 'pps', 'status'])
|
||||||
loop = asyncio.get_running_loop()
|
parser.add_argument('value_arg', nargs='?', type=int, help='Value for PPS')
|
||||||
fut = loop.create_future()
|
parser.add_argument('--value', type=int, help='Value for PPS')
|
||||||
try:
|
parser.add_argument('--devices', required=True, help="/dev/ttyUSB0-29")
|
||||||
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 run_parallel_action(devices, action, value):
|
args = parser.parse_args()
|
||||||
"""Runs the specified action on all devices in parallel."""
|
if args.value_arg is not None: args.value = args.value_arg
|
||||||
print(f"Initializing {len(devices)} devices for '{action}'...")
|
if args.action == 'pps' and args.value is None:
|
||||||
sync_event = asyncio.Event()
|
print("Error: 'pps' action requires a value")
|
||||||
tasks = [run_single_cmd(d, action, value, sync_event) for d in devices]
|
sys.exit(1)
|
||||||
|
|
||||||
# Allow connections to settle
|
if sys.platform == 'win32': asyncio.set_event_loop(asyncio.ProactorEventLoop())
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
# Fire all commands at once
|
devs = expand_devices(args.devices)
|
||||||
sync_event.set()
|
print(f"Executing '{args.action}' on {len(devs)} devices...")
|
||||||
|
|
||||||
|
tasks = [run_device(d, args) for d in devs]
|
||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
print("\nResults:")
|
print("\nResults:")
|
||||||
for dev, res in zip(devices, results):
|
for dev, res in zip(devs, results):
|
||||||
if action == 'status':
|
if args.action == 'status':
|
||||||
print(f"{dev}: {res if res else 'TIMEOUT'}")
|
print(f"{dev}: {res if res else 'TIMEOUT'}")
|
||||||
else:
|
else:
|
||||||
status = "OK" if res is True else "FAIL"
|
status = "OK" if res is True else "FAIL"
|
||||||
print(f"{dev}: {status}")
|
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__':
|
if __name__ == '__main__':
|
||||||
args = parse_arguments()
|
asyncio.run(main())
|
||||||
dev_list = expand_devices(args.devices)
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
asyncio.set_event_loop(asyncio.ProactorEventLoop())
|
|
||||||
|
|
||||||
asyncio.run(async_main(args, dev_list))
|
|
||||||
|
|
|
||||||
477
esp32_deploy.py
477
esp32_deploy.py
|
|
@ -1,8 +1,17 @@
|
||||||
#!/usr/bin/env python3
|
#!/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 asyncio
|
||||||
import serial_asyncio
|
import serial_asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import re
|
import re
|
||||||
|
|
@ -10,7 +19,6 @@ import time
|
||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
import glob
|
import glob
|
||||||
import random
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Ensure detection script is available
|
# Ensure detection script is available
|
||||||
|
|
@ -69,58 +77,33 @@ def auto_detect_devices():
|
||||||
return detect_esp32.detect_esp32_devices()
|
return detect_esp32.detect_esp32_devices()
|
||||||
|
|
||||||
class UnifiedDeployWorker:
|
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.port = port
|
||||||
self.target_ip = target_ip
|
self.target_ip = target_ip
|
||||||
self.args = args
|
self.args = args
|
||||||
self.project_dir = Path(project_dir)
|
self.project_dir = Path(project_dir)
|
||||||
self.flash_sem = flash_sem
|
self.flash_sem = flash_sem
|
||||||
self.total_devs = total_devs
|
|
||||||
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
||||||
|
|
||||||
self.regex_chip_type = re.compile(r'Detecting chip type... (ESP32\S*)')
|
self.regex_chip_type = re.compile(r'Detecting chip type... (ESP32\S*)')
|
||||||
# Updated regex to look for the Shell Prompt
|
# Matches the log from your updated main.c
|
||||||
self.regex_prompt = re.compile(r'esp32>', re.IGNORECASE)
|
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_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):
|
async def run(self):
|
||||||
try:
|
try:
|
||||||
if self.args.check_version:
|
if not self.args.config_only:
|
||||||
return await self._query_version()
|
async with self.flash_sem:
|
||||||
|
|
||||||
# --- 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:
|
|
||||||
if self.args.flash_erase:
|
if self.args.flash_erase:
|
||||||
if not await self._erase_flash(): return False
|
if not await self._erase_flash(): return False
|
||||||
|
|
||||||
self.target_for_flash = target_to_use
|
|
||||||
if not await self._flash_firmware(): return False
|
if not await self._flash_firmware(): return False
|
||||||
|
# Give it a moment to stabilize after flash reset
|
||||||
# --- Semaphore Released Here ---
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
await asyncio.sleep(2.0)
|
|
||||||
|
|
||||||
if not self.args.flash_only:
|
if not self.args.flash_only:
|
||||||
if self.args.ssid and self.args.password:
|
if self.args.ssid and self.args.password:
|
||||||
# Thundering Herd Mitigation
|
# Retry logic
|
||||||
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)
|
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
for attempt in range(1, 4):
|
for attempt in range(1, 4):
|
||||||
self.log.info(f"Configuring (Attempt {attempt}/3)...")
|
self.log.info(f"Configuring (Attempt {attempt}/3)...")
|
||||||
|
|
@ -135,140 +118,21 @@ class UnifiedDeployWorker:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
self.log.warning("No SSID/Password provided, skipping config")
|
self.log.warning("No SSID/Password provided, skipping config")
|
||||||
|
if self.args.config_only: return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Worker Exception: {e}")
|
self.log.error(f"Worker Exception: {e}")
|
||||||
return False
|
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):
|
async def _identify_chip(self):
|
||||||
for attempt in range(1, 4):
|
cmd = ['esptool.py', '-p', self.port, 'chip_id']
|
||||||
cmd = ['esptool.py', '-p', self.port, 'chip_id']
|
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||||
try:
|
stdout, stderr = await proc.communicate()
|
||||||
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
output = stdout.decode() + stderr.decode()
|
||||||
stdout, stderr = await proc.communicate()
|
match = self.regex_chip_type.search(output)
|
||||||
output = stdout.decode() + stderr.decode()
|
if match:
|
||||||
match = self.regex_chip_type.search(output)
|
return match.group(1).lower().replace('-', '')
|
||||||
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
|
return None
|
||||||
|
|
||||||
async def _erase_flash(self):
|
async def _erase_flash(self):
|
||||||
|
|
@ -280,21 +144,27 @@ class UnifiedDeployWorker:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _flash_firmware(self):
|
async def _flash_firmware(self):
|
||||||
target_to_use = getattr(self, 'target_for_flash', self.args.target)
|
detected_target = None
|
||||||
|
if self.args.target == 'auto':
|
||||||
if target_to_use == 'auto':
|
detected_target = await self._identify_chip()
|
||||||
self.log.error("Logic Error: Target is still 'auto' inside flash firmware.")
|
if not detected_target:
|
||||||
return False
|
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)
|
suffix = generate_config_suffix(target_to_use, self.args.csi_enable, self.args.ampdu)
|
||||||
firmware_dir = self.project_dir / "firmware"
|
firmware_dir = self.project_dir / "firmware"
|
||||||
unique_app = None
|
|
||||||
|
|
||||||
|
unique_app = None
|
||||||
if firmware_dir.exists():
|
if firmware_dir.exists():
|
||||||
for f in os.listdir(firmware_dir):
|
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"):
|
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
|
unique_app = f
|
||||||
break
|
break
|
||||||
|
|
||||||
if not unique_app:
|
if not unique_app:
|
||||||
self.log.error(f"Binary for config '{suffix}' not found in firmware/.")
|
self.log.error(f"Binary for config '{suffix}' not found in firmware/.")
|
||||||
return False
|
return False
|
||||||
|
|
@ -302,8 +172,9 @@ class UnifiedDeployWorker:
|
||||||
unique_boot = f"bootloader_{suffix}.bin"
|
unique_boot = f"bootloader_{suffix}.bin"
|
||||||
unique_part = f"partition-table_{suffix}.bin"
|
unique_part = f"partition-table_{suffix}.bin"
|
||||||
unique_ota = f"ota_data_initial_{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():
|
if not flash_args_path.exists():
|
||||||
self.log.error(f"flash_args for {suffix} not found")
|
self.log.error(f"flash_args for {suffix} not found")
|
||||||
return False
|
return False
|
||||||
|
|
@ -311,24 +182,33 @@ class UnifiedDeployWorker:
|
||||||
try:
|
try:
|
||||||
with open(flash_args_path, 'r') as f:
|
with open(flash_args_path, 'r') as f:
|
||||||
content = f.read().replace('\n', ' ').strip()
|
content = f.read().replace('\n', ' ').strip()
|
||||||
|
|
||||||
raw_args = [x for x in content.split(' ') if x]
|
raw_args = [x for x in content.split(' ') if x]
|
||||||
final_args = []
|
final_args = []
|
||||||
|
|
||||||
for arg in raw_args:
|
for arg in raw_args:
|
||||||
if arg.endswith('bootloader.bin'): final_args.append(str(firmware_dir / unique_boot))
|
if arg.endswith('bootloader.bin'):
|
||||||
elif arg.endswith('partition-table.bin'): final_args.append(str(firmware_dir / unique_part))
|
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'):
|
elif arg.endswith('ota_data_initial.bin'):
|
||||||
if (firmware_dir / unique_ota).exists(): final_args.append(str(firmware_dir / unique_ota))
|
if (firmware_dir / unique_ota).exists():
|
||||||
else: continue
|
final_args.append(str(firmware_dir / unique_ota))
|
||||||
elif arg.endswith('phy_init_data.bin'): final_args.append(arg)
|
else:
|
||||||
elif arg.endswith('.bin'): final_args.append(str(firmware_dir / unique_app))
|
continue
|
||||||
else: final_args.append(arg)
|
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),
|
cmd = ['esptool.py', '-p', self.port, '-b', str(self.args.baud),
|
||||||
'--before', 'default_reset', '--after', 'hard_reset',
|
'--before', 'default_reset', '--after', 'hard_reset',
|
||||||
'write_flash'] + final_args
|
'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)
|
proc = await asyncio.create_subprocess_exec(*cmd, cwd=self.project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||||
try:
|
try:
|
||||||
|
|
@ -340,10 +220,146 @@ class UnifiedDeployWorker:
|
||||||
if proc.returncode == 0: return True
|
if proc.returncode == 0: return True
|
||||||
self.log.error(f"Flash failed: {stderr.decode()}")
|
self.log.error(f"Flash failed: {stderr.decode()}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Flash Prep Error: {e}")
|
self.log.error(f"Flash Prep Error: {e}")
|
||||||
return False
|
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():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(description='ESP32 Unified Deployment Tool')
|
parser = argparse.ArgumentParser(description='ESP32 Unified Deployment Tool')
|
||||||
parser.add_argument('-i', '--interactive', action='store_true', help='Prompt for build options')
|
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('--config-only', action='store_true')
|
||||||
parser.add_argument('--flash-only', action='store_true')
|
parser.add_argument('--flash-only', action='store_true')
|
||||||
parser.add_argument('--flash-erase', 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('-d', '--dir', default=os.getcwd())
|
||||||
parser.add_argument('-b', '--baud', type=int, default=460800)
|
parser.add_argument('-b', '--baud', type=int, default=460800)
|
||||||
parser.add_argument('--devices', type=str)
|
parser.add_argument('--devices', type=str)
|
||||||
parser.add_argument('--max-concurrent', type=int, default=None)
|
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")
|
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('-s', '--ssid', default='ClubHouse2G')
|
||||||
parser.add_argument('-P', '--password', default='ez2remember')
|
parser.add_argument('-P', '--password', default='ez2remember')
|
||||||
parser.add_argument('-g', '--gateway', default='192.168.1.1')
|
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('--band', default='2.4G')
|
||||||
parser.add_argument('-B', '--bandwidth', default='HT20')
|
parser.add_argument('-B', '--bandwidth', default='HT20')
|
||||||
parser.add_argument('-ps', '--powersave', default='NONE')
|
parser.add_argument('-ps', '--powersave', default='NONE')
|
||||||
|
|
||||||
|
# --- Iperf Defaults (Robustness) ---
|
||||||
parser.add_argument('--iperf-period', type=float, default=0.01)
|
parser.add_argument('--iperf-period', type=float, default=0.01)
|
||||||
parser.add_argument('--iperf-burst', type=int, default=1)
|
parser.add_argument('--iperf-burst', type=int, default=1)
|
||||||
parser.add_argument('--iperf-len', type=int, default=1470)
|
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('--no-iperf', action='store_true')
|
||||||
parser.add_argument('--iperf-client', action='store_true')
|
parser.add_argument('--iperf-client', action='store_true')
|
||||||
parser.add_argument('--iperf-server', action='store_true')
|
parser.add_argument('--iperf-server', action='store_true')
|
||||||
|
|
||||||
|
# --- Monitor Mode Defaults ---
|
||||||
parser.add_argument('-M', '--mode', default='STA')
|
parser.add_argument('-M', '--mode', default='STA')
|
||||||
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
|
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
|
||||||
parser.add_argument('--csi', dest='csi_enable', action='store_true')
|
parser.add_argument('--csi', dest='csi_enable', action='store_true')
|
||||||
|
|
||||||
args = parser.parse_args()
|
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")
|
parser.error("the following arguments are required: --start-ip")
|
||||||
if args.config_only and args.flash_only: parser.error("Conflicting modes")
|
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
|
return args
|
||||||
|
|
||||||
def extract_device_number(device_path):
|
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)
|
match = re.search(r'(\d+)$', device_path)
|
||||||
return int(match.group(1)) if match else 0
|
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'}"
|
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 ""
|
prefix = f"[{current_step}/{total_steps}] " if current_step else ""
|
||||||
print(f" {prefix}Building [{desc}] ... ", end='', flush=True)
|
print(f" {prefix}Building [{desc}] ... ", end='', flush=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output_dir = project_dir / "firmware"
|
output_dir = project_dir / "firmware"
|
||||||
output_dir.mkdir(exist_ok=True)
|
output_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
sdkconfig_path = project_dir / "sdkconfig"
|
sdkconfig_path = project_dir / "sdkconfig"
|
||||||
build_path = project_dir / "build"
|
build_path = project_dir / "build"
|
||||||
if sdkconfig_path.exists(): os.remove(sdkconfig_path)
|
if sdkconfig_path.exists(): os.remove(sdkconfig_path)
|
||||||
if build_path.exists(): shutil.rmtree(build_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)
|
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()
|
_, stderr = await proc.communicate()
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
print(f"{Colors.RED}FAIL (Set Target){Colors.RESET}")
|
print(f"{Colors.RED}FAIL (Set Target){Colors.RESET}")
|
||||||
return False, f"Set Target Failed", 0
|
return False, f"Set Target Failed", 0
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
build_cmd = ['idf.py', '-D', f'SDKCONFIG_DEFAULTS={defaults_str}', 'build']
|
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)
|
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:
|
if proc.returncode != 0:
|
||||||
print(f"{Colors.RED}FAIL{Colors.RESET}")
|
print(f"{Colors.RED}FAIL{Colors.RESET}")
|
||||||
return False, f"Build Failed", duration
|
return False, f"Build Failed", duration
|
||||||
|
|
||||||
build_dir = project_dir / 'build'
|
build_dir = project_dir / 'build'
|
||||||
suffix = generate_config_suffix(target, csi, ampdu)
|
suffix = generate_config_suffix(target, csi, ampdu)
|
||||||
unique_app_name = "Unknown"
|
unique_app_name = "Unknown"
|
||||||
|
|
||||||
project_bin = get_project_binary_name(build_dir)
|
project_bin = get_project_binary_name(build_dir)
|
||||||
if project_bin:
|
if project_bin:
|
||||||
unique_app_name = f"{os.path.splitext(project_bin)[0]}_{suffix}.bin"
|
unique_app_name = f"{os.path.splitext(project_bin)[0]}_{suffix}.bin"
|
||||||
shutil.copy2(build_dir / project_bin, output_dir / unique_app_name)
|
shutil.copy2(build_dir / project_bin, output_dir / unique_app_name)
|
||||||
|
|
||||||
boot_src = build_dir / "bootloader" / "bootloader.bin"
|
boot_src = build_dir / "bootloader" / "bootloader.bin"
|
||||||
if boot_src.exists(): shutil.copy2(boot_src, output_dir / f"bootloader_{suffix}.bin")
|
if boot_src.exists(): shutil.copy2(boot_src, output_dir / f"bootloader_{suffix}.bin")
|
||||||
|
|
||||||
part_src = build_dir / "partition_table" / "partition-table.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")
|
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"
|
ota_src = build_dir / "ota_data_initial.bin"
|
||||||
if ota_src.exists(): shutil.copy2(ota_src, output_dir / f"ota_data_initial_{suffix}.bin")
|
if ota_src.exists(): shutil.copy2(ota_src, output_dir / f"ota_data_initial_{suffix}.bin")
|
||||||
|
|
||||||
flash_src = build_dir / "flash_args"
|
flash_src = build_dir / "flash_args"
|
||||||
if flash_src.exists(): shutil.copy2(flash_src, output_dir / f"flash_args_{suffix}")
|
if flash_src.exists(): shutil.copy2(flash_src, output_dir / f"flash_args_{suffix}")
|
||||||
|
|
||||||
full_path = output_dir / unique_app_name
|
full_path = output_dir / unique_app_name
|
||||||
print(f"{Colors.GREEN}OK ({duration:.1f}s) -> {full_path}{Colors.RESET}")
|
print(f"{Colors.GREEN}OK ({duration:.1f}s) -> {full_path}{Colors.RESET}")
|
||||||
return True, "Success", duration
|
return True, "Success", duration
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{Colors.RED}ERROR: {e}{Colors.RESET}")
|
print(f"{Colors.RED}ERROR: {e}{Colors.RESET}")
|
||||||
return False, str(e), 0
|
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):
|
async def run_deployment(args):
|
||||||
print(f"\n{Colors.BLUE}{'='*60}{Colors.RESET}\n ESP32 Unified Deployment Tool\n{Colors.BLUE}{'='*60}{Colors.RESET}")
|
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()
|
project_dir = Path(args.dir).resolve()
|
||||||
|
|
||||||
|
# --- Target 'ALL' Mode ---
|
||||||
if args.target == 'all':
|
if args.target == 'all':
|
||||||
print(f"{Colors.YELLOW}Starting Batch Build Verification (12 Combinations){Colors.RESET}")
|
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"
|
firmware_dir = project_dir / "firmware"
|
||||||
if firmware_dir.exists():
|
if firmware_dir.exists():
|
||||||
try: shutil.rmtree(firmware_dir)
|
try:
|
||||||
except Exception as e: return
|
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)
|
firmware_dir.mkdir(exist_ok=True)
|
||||||
|
print("") # Spacer
|
||||||
|
|
||||||
targets = ['esp32', 'esp32s3', 'esp32c5']
|
targets = ['esp32', 'esp32s3', 'esp32c5']
|
||||||
booleans = [False, True]
|
booleans = [False, True]
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
total_steps = len(targets) * len(booleans) * len(booleans)
|
total_steps = len(targets) * len(booleans) * len(booleans)
|
||||||
current_step = 0
|
current_step = 0
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
for csi in booleans:
|
for csi in booleans:
|
||||||
for ampdu in booleans:
|
for ampdu in booleans:
|
||||||
current_step += 1
|
current_step += 1
|
||||||
success, msg, dur = await build_task(project_dir, target, csi, ampdu, current_step, total_steps)
|
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})
|
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}")
|
print(f"\n{Colors.BLUE}Batch Summary:{Colors.RESET}")
|
||||||
for r in results:
|
for r in results:
|
||||||
status = f"{Colors.GREEN}PASS{Colors.RESET}" if r['ok'] else f"{Colors.RED}FAIL{Colors.RESET}"
|
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)")
|
print(f" {r['cfg']} : {status} ({r['dur']:.1f}s)")
|
||||||
return
|
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'
|
target = args.target if args.target else 'esp32s3'
|
||||||
csi = args.csi_enable
|
csi = args.csi_enable
|
||||||
ampdu = args.ampdu
|
ampdu = args.ampdu
|
||||||
|
|
||||||
if args.interactive:
|
if args.interactive:
|
||||||
print(f"\n{Colors.YELLOW}--- Build Configuration ---{Colors.RESET}")
|
print(f"\n{Colors.YELLOW}--- Build Configuration ---{Colors.RESET}")
|
||||||
target = ask_user("Target Chip", default=target, choices=['esp32', 'esp32s3', 'esp32c5'])
|
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.csi_enable = csi
|
||||||
args.target = target
|
args.target = target
|
||||||
args.ampdu = ampdu
|
args.ampdu = ampdu
|
||||||
|
|
||||||
success, msg, _ = await build_task(project_dir, target, csi, ampdu, 1, 1)
|
success, msg, _ = await build_task(project_dir, target, csi, ampdu, 1, 1)
|
||||||
if not success:
|
if not success:
|
||||||
print(f"{Colors.RED}{msg}{Colors.RESET}")
|
print(f"{Colors.RED}{msg}{Colors.RESET}")
|
||||||
return
|
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}")
|
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(',')]
|
devs = [type('obj', (object,), {'device': d.strip()}) for d in args.devices.split(',')]
|
||||||
else:
|
else:
|
||||||
|
# Use AUTO DETECT first (for static names), then standard fallback
|
||||||
devs = auto_detect_devices()
|
devs = auto_detect_devices()
|
||||||
if not devs: print("No devices found"); return
|
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)])
|
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}")
|
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)
|
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)
|
flash_sem = asyncio.Semaphore(max_c)
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
for i, dev in enumerate(devs):
|
for i, dev in enumerate(devs):
|
||||||
raw_port_number = extract_device_number(dev.device)
|
# --- ROBUST IP CALCULATION LOGIC ---
|
||||||
|
|
||||||
if args.ip_device_based:
|
if args.ip_device_based:
|
||||||
if "esp_port" in dev.device:
|
# Mode A: Offset based on physical port number (e.g. 14 for ttyUSB14)
|
||||||
offset = raw_port_number - 1
|
offset = extract_device_number(dev.device)
|
||||||
else:
|
print(f" [{dev.device}] Using device-based IP offset: +{offset}")
|
||||||
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})")
|
|
||||||
else:
|
else:
|
||||||
|
# Mode B: Sequential offset based on loop index
|
||||||
offset = i
|
offset = i
|
||||||
target_ip = str(start_ip + offset)
|
print(f" [{dev.device}] Using sequential IP offset: +{offset}")
|
||||||
if not args.check_version:
|
|
||||||
print(f" [{dev.device}] Sequential IP: {target_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)
|
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)
|
success = results.count(True)
|
||||||
print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}")
|
print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,23 @@
|
||||||
idf_component_register(SRCS "main.c"
|
idf_component_register(
|
||||||
INCLUDE_DIRS "."
|
SRCS "main.c"
|
||||||
PRIV_REQUIRES nvs_flash esp_netif wifi_controller wifi_cfg app_console iperf status_led gps_sync console)
|
INCLUDE_DIRS "."
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,37 @@
|
||||||
#ifndef BOARD_CONFIG_H
|
#pragma once
|
||||||
#define BOARD_CONFIG_H
|
#include "driver/gpio.h"
|
||||||
|
|
||||||
#include "sdkconfig.h"
|
// --- Hardware Configuration ---
|
||||||
|
|
||||||
// ============================================================================
|
#if defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||||
// ESP32-C5 (DevKitC-1)
|
// ESP32-S3
|
||||||
// ============================================================================
|
#define RGB_LED_GPIO 48
|
||||||
#ifdef CONFIG_IDF_TARGET_ESP32C5
|
#define HAS_RGB_LED 1
|
||||||
#define RGB_LED_GPIO 8 // Common addressable LED pin for C5
|
#define GPS_TX_PIN GPIO_NUM_5
|
||||||
#define HAS_RGB_LED 1
|
#define GPS_RX_PIN GPIO_NUM_4
|
||||||
|
#define GPS_PPS_PIN GPIO_NUM_6
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 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.
|
|
||||||
#define RGB_LED_GPIO 48
|
|
||||||
#define HAS_RGB_LED 1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Fallbacks (Prevent Compilation Errors)
|
|
||||||
// ============================================================================
|
|
||||||
#ifndef RGB_LED_GPIO
|
|
||||||
#define RGB_LED_GPIO 2
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAS_RGB_LED
|
|
||||||
#define HAS_RGB_LED 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // BOARD_CONFIG_H
|
|
||||||
|
|
|
||||||
169
main/main.c
169
main/main.c
|
|
@ -1,110 +1,153 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "sdkconfig.h"
|
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
|
#include "esp_event.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "esp_vfs_dev.h"
|
// REMOVED: #include "linenoise/linenoise.h" <-- CAUSE OF CONFLICT
|
||||||
#include "driver/uart.h"
|
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "esp_event.h"
|
#include "lwip/inet.h"
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
#include "status_led.h"
|
|
||||||
#include "board_config.h"
|
#include "board_config.h"
|
||||||
#include "wifi_controller.h"
|
#include "status_led.h"
|
||||||
|
#include "gps_sync.h"
|
||||||
#include "wifi_cfg.h"
|
#include "wifi_cfg.h"
|
||||||
|
#include "wifi_controller.h"
|
||||||
#include "app_console.h"
|
#include "app_console.h"
|
||||||
|
#include "iperf.h"
|
||||||
|
|
||||||
|
// GUARDED INCLUDE
|
||||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
#include "csi_log.h"
|
#include "csi_log.h"
|
||||||
#include "csi_manager.h"
|
#include "csi_manager.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define APP_VERSION "2.0.0-SHELL"
|
|
||||||
|
|
||||||
static const char *TAG = "MAIN";
|
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) {
|
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||||
ESP_LOGI(TAG, "Restarting...");
|
if (wifi_ctl_get_mode() != WIFI_CTL_MODE_STA) return;
|
||||||
esp_restart();
|
|
||||||
return 0;
|
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) {
|
// --- Main ----------------------------------------------------------
|
||||||
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 ---
|
|
||||||
|
|
||||||
void app_main(void) {
|
void app_main(void) {
|
||||||
// 1. Initialize NVS
|
// 1. System Init
|
||||||
esp_err_t ret = nvs_flash_init();
|
esp_err_t ret = nvs_flash_init();
|
||||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
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());
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||||
ret = nvs_flash_init();
|
ret = nvs_flash_init();
|
||||||
}
|
}
|
||||||
ESP_ERROR_CHECK(ret);
|
ESP_ERROR_CHECK(ret);
|
||||||
|
|
||||||
// 2. Initialize Netif & Event Loop
|
|
||||||
ESP_ERROR_CHECK(esp_netif_init());
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
|
||||||
// 3. Hardware Init
|
// 2. Hardware/Driver Init
|
||||||
status_led_init(RGB_LED_GPIO, HAS_RGB_LED);
|
|
||||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
ESP_ERROR_CHECK(csi_log_init());
|
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();
|
csi_mgr_init();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 4. Initialize WiFi Controller (Loads config from NVS automatically)
|
|
||||||
wifi_ctl_init();
|
wifi_ctl_init();
|
||||||
|
|
||||||
// 5. Initialize Console
|
// THIS call starts the cmd_transport (UART listener task)
|
||||||
esp_console_repl_t *repl = NULL;
|
// which effectively replaces the manual console loop below.
|
||||||
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
|
wifi_cfg_init();
|
||||||
|
|
||||||
// This prompt is the anchor for your Python script
|
// 4. Console Registry Init (Still needed for registering commands)
|
||||||
repl_config.prompt = "esp32> ";
|
esp_console_config_t console_config = {
|
||||||
repl_config.max_cmdline_length = 1024;
|
.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)
|
// Register App Commands
|
||||||
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();
|
|
||||||
app_console_register_commands();
|
app_console_register_commands();
|
||||||
|
|
||||||
// 7. Start Shell
|
// 5. Register Events
|
||||||
printf("\n ==================================================\n");
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
|
||||||
printf(" | ESP32 iPerf Shell - Ready |\n");
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
|
||||||
printf(" | Type 'help' for commands |\n");
|
|
||||||
printf(" ==================================================\n");
|
|
||||||
|
|
||||||
// This function runs the REPL loop and does not return
|
// 6. Application Start
|
||||||
ESP_ERROR_CHECK(esp_console_start_repl(repl));
|
#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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue