diff --git a/components/app_console/CMakeLists.txt b/components/app_console/CMakeLists.txt index a87a393..7d08ba6 100644 --- a/components/app_console/CMakeLists.txt +++ b/components/app_console/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "app_console.c" INCLUDE_DIRS "." REQUIRES console - PRIV_REQUIRES wifi_controller csi_manager status_led gps_sync esp_wifi) + PRIV_REQUIRES wifi_controller csi_manager status_led gps_sync esp_wifi iperf) diff --git a/components/app_console/app_console.c b/components/app_console/app_console.c index 1c77147..b3c7661 100644 --- a/components/app_console/app_console.c +++ b/components/app_console/app_console.c @@ -10,27 +10,59 @@ #include "wifi_controller.h" #include "status_led.h" #include "gps_sync.h" +#include "iperf.h" -// 1. GUARDED INCLUDE #ifdef CONFIG_ESP_WIFI_CSI_ENABLED #include "csi_manager.h" #endif // --- Command Handlers --- -static int cmd_mode_monitor(int argc, char **argv) { - // Default to current channel if not provided - int channel = wifi_ctl_get_monitor_channel(); - - if (argc > 1) { - channel = atoi(argv[1]); - if (channel < 1 || channel > 165) { - printf("Error: Invalid channel %d\n", channel); - return 1; - } +static int cmd_iperf(int argc, char **argv) { + if (argc < 2) { + printf("Usage: iperf \n"); + return 1; } - // Default bandwidth HT20 for monitor mode + if (strcmp(argv[1], "start") == 0) { + iperf_cfg_t cfg = { .time = 0 }; // Infinite + iperf_start(&cfg); + printf("IPERF_STARTED\n"); + return 0; + + } else if (strcmp(argv[1], "stop") == 0) { + iperf_stop(); + printf("IPERF_STOPPED\n"); + return 0; + + } else if (strcmp(argv[1], "pps") == 0) { + // Syntax: iperf pps 100 + if (argc < 3) { + printf("Error: Missing value. Usage: iperf pps \n"); + return 1; + } + int pps = atoi(argv[2]); + if (pps <= 0) { + printf("Error: Invalid PPS.\n"); + return 1; + } + iperf_set_pps((uint32_t)pps); + printf("IPERF_PPS_UPDATED: %d\n", pps); + return 0; + + } else if (strcmp(argv[1], "status") == 0) { + uint32_t pps = iperf_get_pps(); + printf("IPERF_STATUS: PPS=%lu\n", (unsigned long)pps); + return 0; + } + + printf("Error: Unknown subcommand '%s'.\n", argv[1]); + return 1; +} + +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; @@ -39,79 +71,38 @@ static int cmd_mode_monitor(int argc, char **argv) { } static int cmd_mode_sta(int argc, char **argv) { - // Simple switch to STA, auto band if (wifi_ctl_switch_to_sta(WIFI_BAND_MODE_AUTO) != ESP_OK) { printf("Failed to switch to STA mode\n"); return 1; } - printf("Switching to STA mode...\n"); return 0; } static int cmd_mode_status(int argc, char **argv) { wifi_ctl_mode_t mode = wifi_ctl_get_mode(); - printf("\n=== WiFi Mode Status ===\n"); printf("Current mode: %s\n", mode == WIFI_CTL_MODE_STA ? "STA" : "MONITOR"); printf("LED state: %d\n", status_led_get_state()); printf("GPS synced: %s\n", gps_is_synced() ? "Yes" : "No"); - - if (mode == WIFI_CTL_MODE_STA) { - // 2. GUARDED STATUS PRINT -#ifdef CONFIG_ESP_WIFI_CSI_ENABLED - printf("CSI Enabled: %s\n", csi_mgr_is_enabled() ? "Yes" : "No"); - printf("CSI Packets: %lu\n", (unsigned long)csi_mgr_get_packet_count()); -#else - printf("CSI Support: Disabled in build\n"); -#endif - } else { - printf("Monitor Ch: %d\n", wifi_ctl_get_monitor_channel()); - printf("Captured: %lu frames\n", (unsigned long)wifi_ctl_get_monitor_frame_count()); - } return 0; } static int cmd_csi_dump(int argc, char **argv) { - if (wifi_ctl_get_mode() != WIFI_CTL_MODE_STA) { - printf("Error: CSI only available in STA mode\n"); - return 1; - } - - // 3. GUARDED DUMP ACTION #ifdef CONFIG_ESP_WIFI_CSI_ENABLED - printf("Scheduling CSI dump...\n"); csi_mgr_schedule_dump(); #else printf("Error: CSI feature is disabled in this firmware build.\n"); #endif - return 0; } -// --- Registration --- - void app_console_register_commands(void) { const esp_console_cmd_t cmds[] = { - { - .command = "mode_monitor", - .help = "Switch to monitor mode (Usage: mode_monitor [channel])", - .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 to UART", - .func = &cmd_csi_dump - }, + { .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++) { diff --git a/components/iperf/iperf.c b/components/iperf/iperf.c index fba1e4d..5fc2919 100644 --- a/components/iperf/iperf.c +++ b/components/iperf/iperf.c @@ -26,13 +26,16 @@ static EventGroupHandle_t s_iperf_event_group = NULL; #define IPERF_IP_READY_BIT (1 << 0) #define IPERF_STOP_REQ_BIT (1 << 1) +// Check rate every 500ms +#define RATE_CHECK_INTERVAL_US 500000 +// Minimum gap (Safety Limit) +#define MIN_PACING_INTERVAL_US 100 + typedef struct { iperf_cfg_t cfg; bool finish; - uint32_t total_len; uint32_t buffer_len; uint8_t *buffer; - uint32_t sockfd; } iperf_ctrl_t; static iperf_ctrl_t s_iperf_ctrl = {0}; @@ -42,16 +45,12 @@ static esp_event_handler_instance_t instance_got_ip; static void iperf_network_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (s_iperf_event_group == NULL) return; - - if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { - // Do nothing, wait for IP - } - else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { xEventGroupSetBits(s_iperf_event_group, IPERF_IP_READY_BIT); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { xEventGroupClearBits(s_iperf_event_group, IPERF_IP_READY_BIT); - status_led_set_state(LED_STATE_FAILED); // Red Blink on disconnect + status_led_set_state(LED_STATE_NO_CONFIG); // Yellow } } @@ -67,12 +66,10 @@ static bool iperf_wait_for_ip(void) { xEventGroupSetBits(s_iperf_event_group, IPERF_IP_READY_BIT); } } - ESP_LOGI(TAG, "Waiting for IP address..."); EventBits_t bits = xEventGroupWaitBits(s_iperf_event_group, IPERF_IP_READY_BIT | IPERF_STOP_REQ_BIT, pdFALSE, pdFALSE, portMAX_DELAY); esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id); esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip); - if (bits & IPERF_STOP_REQ_BIT) return false; - return true; + return !(bits & IPERF_STOP_REQ_BIT); } static void trim_whitespace(char *str) { @@ -83,216 +80,155 @@ static void trim_whitespace(char *str) { static void iperf_read_nvs_config(iperf_cfg_t *cfg) { nvs_handle_t my_handle; - esp_err_t err = nvs_open("storage", NVS_READONLY, &my_handle); - uint32_t val = 0; - size_t required_size; + if (nvs_open("storage", NVS_READONLY, &my_handle) != ESP_OK) return; - // --- DEFAULTS --- - // Change: 5ms Gap to test Media Access Contention - cfg->pacing_period_us = 5000; - cfg->burst_count = 1; - cfg->send_len = IPERF_UDP_TX_LEN; - cfg->dport = 5001; - cfg->dip = inet_addr("192.168.1.50"); // Default Target if NVS missing - if (cfg->time == 0) cfg->time = UINT32_MAX; + uint32_t val; + if (nvs_get_u32(my_handle, NVS_KEY_IPERF_PERIOD, &val) == ESP_OK) cfg->pacing_period_us = val; + if (nvs_get_u32(my_handle, NVS_KEY_IPERF_BURST, &val) == ESP_OK) cfg->burst_count = val; + if (nvs_get_u32(my_handle, NVS_KEY_IPERF_LEN, &val) == ESP_OK) cfg->send_len = val; + if (nvs_get_u32(my_handle, NVS_KEY_IPERF_PORT, &val) == ESP_OK) cfg->dport = (uint16_t)val; - if (err != ESP_OK) return; - - if (nvs_get_u32(my_handle, NVS_KEY_IPERF_PERIOD, &val) == ESP_OK && val > 0) cfg->pacing_period_us = val; - if (nvs_get_u32(my_handle, NVS_KEY_IPERF_BURST, &val) == ESP_OK && val > 0) cfg->burst_count = val; - if (nvs_get_u32(my_handle, NVS_KEY_IPERF_LEN, &val) == ESP_OK && val > 0) cfg->send_len = val; - - if (nvs_get_u32(my_handle, NVS_KEY_IPERF_PORT, &val) == ESP_OK && val > 0) { - cfg->dport = (uint16_t)val; - } - - // --- RESTORED NVS IP LOGIC (NO OVERRIDE) --- - if (nvs_get_str(my_handle, NVS_KEY_IPERF_DST_IP, NULL, &required_size) == ESP_OK) { - char *ip_str = malloc(required_size); + size_t req; + if (nvs_get_str(my_handle, NVS_KEY_IPERF_DST_IP, NULL, &req) == ESP_OK) { + char *ip_str = malloc(req); if (ip_str) { - nvs_get_str(my_handle, NVS_KEY_IPERF_DST_IP, ip_str, &required_size); + nvs_get_str(my_handle, NVS_KEY_IPERF_DST_IP, ip_str, &req); trim_whitespace(ip_str); - - // FIX: Use whatever is in NVS, do not auto-correct cfg->dip = inet_addr(ip_str); - ESP_LOGI(TAG, "NVS Target IP: %s", ip_str); - free(ip_str); } } - nvs_close(my_handle); } -// ... (Unused functions omitted) ... -static void __attribute__((unused)) socket_send(int sockfd, const uint8_t *buffer, int len) {} -static int __attribute__((unused)) socket_recv(int sockfd, uint8_t *buffer, int len, TickType_t timeout_ticks) { return 0; } -static esp_err_t iperf_start_tcp_server(iperf_ctrl_t *ctrl) { ESP_LOGW(TAG, "TCP Server not implemented"); return ESP_FAIL; } -static esp_err_t iperf_start_tcp_client(iperf_ctrl_t *ctrl) { ESP_LOGW(TAG, "TCP Client not implemented"); return ESP_FAIL; } -static esp_err_t iperf_start_udp_server(iperf_ctrl_t *ctrl) { ESP_LOGW(TAG, "UDP Server not implemented"); return ESP_FAIL; } +void iperf_set_pps(uint32_t pps) { + if (pps == 0) pps = 1; + uint32_t period_us = 1000000 / pps; -static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) -{ + if (period_us < MIN_PACING_INTERVAL_US) { + period_us = MIN_PACING_INTERVAL_US; + ESP_LOGW(TAG, "PPS %" PRIu32 " clamped to max safe rate", pps); + } + + if (s_iperf_task_handle != NULL) { + s_iperf_ctrl.cfg.pacing_period_us = period_us; + ESP_LOGI(TAG, "Runtime pacing updated to %" PRIu32 " PPS", pps); + } else { + s_iperf_ctrl.cfg.pacing_period_us = period_us; + } +} + +uint32_t iperf_get_pps(void) { + if (s_iperf_ctrl.cfg.pacing_period_us == 0) return 0; + return 1000000 / s_iperf_ctrl.cfg.pacing_period_us; +} + +static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) { if (!iperf_wait_for_ip()) return ESP_FAIL; struct sockaddr_in addr; - int sockfd; - struct timespec ts; - - sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sockfd < 0) { - ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); - status_led_set_state(LED_STATE_FAILED); - return ESP_FAIL; - } - addr.sin_family = AF_INET; addr.sin_port = htons(ctrl->cfg.dport > 0 ? ctrl->cfg.dport : 5001); addr.sin_addr.s_addr = ctrl->cfg.dip; - char ip_str[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &addr.sin_addr, ip_str, INET_ADDRSTRLEN); - ESP_LOGI(TAG, "Target: %s:%d", ip_str, ntohs(addr.sin_port)); - - uint32_t burst_count = ctrl->cfg.burst_count; - uint32_t payload_len = ctrl->cfg.send_len; - uint32_t pacing_period_us = ctrl->cfg.pacing_period_us; - - double total_mbps = (double)((uint64_t)burst_count * payload_len * 8 * (1000000.0 / pacing_period_us)) / 1000000.0; - ESP_LOGI(TAG, "Pacing: %" PRIu32 " pkts every %" PRIu32 " us (Approx %.2f Mbps)", burst_count, pacing_period_us, total_mbps); - - client_hdr_v1 *client_hdr = (client_hdr_v1 *)(ctrl->buffer + sizeof(udp_datagram)); - client_hdr->flags = htonl(HEADER_VERSION1); - client_hdr->numThreads = htonl(1); - client_hdr->mPort = htonl(ntohs(addr.sin_port)); - client_hdr->mBufLen = htonl(payload_len); - client_hdr->mWinBand = htonl(0); - client_hdr->mAmount = htonl(-(int)(10000)); - - status_led_set_state(LED_STATE_TRANSMITTING); - - uint64_t total_len = 0; - uint32_t packet_count = 0; - int64_t start_time_us = esp_timer_get_time(); - int64_t next_send_time = start_time_us; - int64_t end_time_us = (ctrl->cfg.time == UINT32_MAX) ? INT64_MAX : start_time_us + (int64_t)ctrl->cfg.time * 1000000LL; - - // Error Tracking - int64_t enomem_start_time = 0; - const int64_t ENOMEM_TIMEOUT_US = 10 * 1000 * 1000; // 10 Seconds - - while (!ctrl->finish && esp_timer_get_time() < end_time_us) { - - int64_t current_time = esp_timer_get_time(); - int64_t time_to_wait = next_send_time - current_time; - - if (time_to_wait > 0) { - if (time_to_wait > 2000) { - vTaskDelay(pdMS_TO_TICKS(time_to_wait / 1000)); - } else { - while (esp_timer_get_time() < next_send_time) { - taskYIELD(); - } - } - } - - for (int k = 0; k < burst_count; k++) { - udp_datagram *header = (udp_datagram *)ctrl->buffer; - clock_gettime(CLOCK_MONOTONIC, &ts); - header->id = htonl(packet_count); - header->tv_sec = htonl(ts.tv_sec); - header->tv_usec = htonl(ts.tv_nsec / 1000); - header->id2 = 0; - - int send_len = sendto(sockfd, ctrl->buffer, payload_len, 0, (struct sockaddr *)&addr, sizeof(addr)); - - if (send_len > 0) { - total_len += send_len; - packet_count++; - - enomem_start_time = 0; - - if (status_led_get_state() != LED_STATE_TRANSMITTING) { - status_led_set_state(LED_STATE_TRANSMITTING); - } - - } else { - // --- ERROR HANDLING --- - if (errno == 12) { // ENOMEM - int64_t now = esp_timer_get_time(); - - if (status_led_get_state() != LED_STATE_STALLED) { - status_led_set_state(LED_STATE_STALLED); - } - - if (enomem_start_time == 0) { - enomem_start_time = now; - } else if ((now - enomem_start_time) > ENOMEM_TIMEOUT_US) { - ESP_LOGE(TAG, "UDP send ENOMEM persistent for 10s. STOPPING."); - status_led_set_state(LED_STATE_FAILED); - goto exit_client; - } - - vTaskDelay(pdMS_TO_TICKS(10)); - continue; - } else { - ESP_LOGE(TAG, "UDP send failed: %d. STOPPING.", errno); - status_led_set_state(LED_STATE_FAILED); - goto exit_client; - } - } - } - next_send_time += pacing_period_us; - if (esp_timer_get_time() > next_send_time + 4000) next_send_time = esp_timer_get_time() + pacing_period_us; + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sockfd < 0) { + status_led_set_state(LED_STATE_FAILED); + return ESP_FAIL; } -exit_client: + status_led_set_state(LED_STATE_TRANSMITTING_SLOW); // Default state + + int64_t next_send_time = esp_timer_get_time(); + int64_t end_time = (ctrl->cfg.time == 0) ? INT64_MAX : esp_timer_get_time() + (int64_t)ctrl->cfg.time * 1000000LL; + + int64_t last_rate_check = esp_timer_get_time(); + uint32_t packets_since_check = 0; + int64_t enomem_start_time = 0; + + while (!ctrl->finish && esp_timer_get_time() < end_time) { + int64_t now = esp_timer_get_time(); + int64_t wait = next_send_time - now; + + if (wait > 2000) vTaskDelay(pdMS_TO_TICKS(wait / 1000)); + else while (esp_timer_get_time() < next_send_time) taskYIELD(); + + for (int k = 0; k < ctrl->cfg.burst_count; k++) { + int sent = sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr)); + + if (sent > 0) { + packets_since_check++; + enomem_start_time = 0; + + // --- DYNAMIC RATE HEALTH CHECK --- + if (now - last_rate_check > RATE_CHECK_INTERVAL_US) { + int64_t interval = now - last_rate_check; + double cycles = (double)interval / (double)ctrl->cfg.pacing_period_us; + uint32_t expected_pkts = (uint32_t)(cycles * ctrl->cfg.burst_count); + + // Threshold: 75% of expected rate + uint32_t threshold = (expected_pkts * 3) / 4; + + led_state_t target = (packets_since_check >= threshold) + ? LED_STATE_TRANSMITTING // Busy (Fast Flash) + : LED_STATE_TRANSMITTING_SLOW; // Slow (Slow Pulse) + + if (status_led_get_state() != target) status_led_set_state(target); + + last_rate_check = now; + packets_since_check = 0; + } + + } else { + if (errno == 12) { // ENOMEM + if (status_led_get_state() != LED_STATE_STALLED) status_led_set_state(LED_STATE_STALLED); + + if (enomem_start_time == 0) enomem_start_time = now; + else if (now - enomem_start_time > 10000000) { + status_led_set_state(LED_STATE_FAILED); goto exit; + } + vTaskDelay(pdMS_TO_TICKS(10)); + } else { + status_led_set_state(LED_STATE_FAILED); goto exit; + } + } + } + next_send_time += ctrl->cfg.pacing_period_us; + } + +exit: if (status_led_get_state() != LED_STATE_FAILED) { - status_led_set_state(LED_STATE_CONNECTED); + EventBits_t bits = xEventGroupGetBits(s_iperf_event_group); + status_led_set_state((bits & IPERF_IP_READY_BIT) ? LED_STATE_CONNECTED : LED_STATE_NO_CONFIG); } close(sockfd); return ESP_OK; } static void iperf_task(void *arg) { - iperf_ctrl_t *ctrl = (iperf_ctrl_t *)arg; - iperf_start_udp_client(ctrl); - if (ctrl->buffer) { free(ctrl->buffer); ctrl->buffer = NULL; } - if (s_iperf_event_group) { vEventGroupDelete(s_iperf_event_group); s_iperf_event_group = NULL; } - s_iperf_task_handle = NULL; + iperf_start_udp_client((iperf_ctrl_t *)arg); + free(((iperf_ctrl_t *)arg)->buffer); vTaskDelete(NULL); } void iperf_start(iperf_cfg_t *cfg) { - nvs_handle_t my_handle; - uint8_t enabled = 1; - if (nvs_open("storage", NVS_READONLY, &my_handle) == ESP_OK) { - nvs_get_u8(my_handle, NVS_KEY_IPERF_ENABLE, &enabled); - nvs_close(my_handle); - } - if (enabled == 0) return; - if (s_iperf_task_handle != NULL) return; - - memcpy(&s_iperf_ctrl.cfg, cfg, sizeof(iperf_cfg_t)); - s_iperf_ctrl.cfg.flag = IPERF_FLAG_CLIENT | IPERF_FLAG_UDP; + if (s_iperf_task_handle) return; + s_iperf_ctrl.cfg = *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; iperf_read_nvs_config(&s_iperf_ctrl.cfg); s_iperf_ctrl.finish = false; - - uint32_t alloc_len = s_iperf_ctrl.cfg.send_len > 0 ? s_iperf_ctrl.cfg.send_len : IPERF_UDP_TX_LEN; - uint32_t min_hdr = sizeof(udp_datagram) + sizeof(client_hdr_v1); - if (alloc_len < min_hdr) alloc_len = min_hdr; - - s_iperf_ctrl.buffer_len = alloc_len; - s_iperf_ctrl.buffer = malloc(s_iperf_ctrl.buffer_len); - memset(s_iperf_ctrl.buffer, 0, s_iperf_ctrl.buffer_len); + s_iperf_ctrl.buffer_len = s_iperf_ctrl.cfg.send_len + 128; + s_iperf_ctrl.buffer = calloc(1, s_iperf_ctrl.buffer_len); s_iperf_event_group = xEventGroupCreate(); - xTaskCreate(iperf_task, "iperf", 4096, &s_iperf_ctrl, IPERF_TRAFFIC_TASK_PRIORITY, &s_iperf_task_handle); + xTaskCreate(iperf_task, "iperf", 4096, &s_iperf_ctrl, 5, &s_iperf_task_handle); } void iperf_stop(void) { - if (s_iperf_task_handle != NULL) { + if (s_iperf_task_handle) { s_iperf_ctrl.finish = true; if (s_iperf_event_group) xEventGroupSetBits(s_iperf_event_group, IPERF_STOP_REQ_BIT); } diff --git a/components/iperf/iperf.h b/components/iperf/iperf.h index fde4c6a..9a5ef23 100644 --- a/components/iperf/iperf.h +++ b/components/iperf/iperf.h @@ -91,6 +91,18 @@ typedef struct { */ void iperf_init_led(led_strip_handle_t handle); +/** + * @brief Set the target pacing rate in Packets Per Second (PPS). + * Converts PPS to microsecond interval internally. + * @param pps Target rate (e.g., 100 = 100 packets/sec) + */ +void iperf_set_pps(uint32_t pps); + +/** + * @brief Get the current target rate in PPS. + */ +uint32_t iperf_get_pps(void); + void iperf_start(iperf_cfg_t *cfg); void iperf_stop(void); diff --git a/components/status_led/status_led.c b/components/status_led/status_led.c index 13be6ef..7e253f9 100644 --- a/components/status_led/status_led.c +++ b/components/status_led/status_led.c @@ -5,79 +5,51 @@ #include "led_strip.h" #include "esp_log.h" -static const char *TAG = "STATUS_LED"; - static led_strip_handle_t s_led_strip = NULL; static bool s_is_rgb = false; static int s_gpio_pin = -1; static volatile led_state_t s_current_state = LED_STATE_NO_CONFIG; -// Internal helper to abstract hardware differences static void set_color(uint8_t r, uint8_t g, uint8_t b) { if (s_is_rgb && s_led_strip) { - // Addressable RGB (WS2812) led_strip_set_pixel(s_led_strip, 0, r, g, b); led_strip_refresh(s_led_strip); } else if (!s_is_rgb && s_gpio_pin >= 0) { - // Simple LED: Any color > 0 is treated as ON - bool on = (r + g + b) > 0; - gpio_set_level(s_gpio_pin, on ? 1 : 0); + gpio_set_level(s_gpio_pin, (r+g+b) > 0); } } static void led_task(void *arg) { - int blink_toggle = 0; + int toggle = 0; while (1) { switch (s_current_state) { - case LED_STATE_NO_CONFIG: - if (s_is_rgb) { - set_color(25, 25, 0); // Yellow - vTaskDelay(pdMS_TO_TICKS(1000)); - } else { - set_color(255, 255, 255); vTaskDelay(pdMS_TO_TICKS(100)); - set_color(0, 0, 0); vTaskDelay(pdMS_TO_TICKS(100)); - } + case LED_STATE_NO_CONFIG: // Yellow + if (s_is_rgb) { set_color(25, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000)); } + else { set_color(1,1,1); vTaskDelay(100); set_color(0,0,0); vTaskDelay(100); } break; - - case LED_STATE_WAITING: - // Blue Blink (Slow) - if (blink_toggle) set_color(0, 0, 50); - else set_color(0, 0, 0); - blink_toggle = !blink_toggle; + case LED_STATE_WAITING: // Blue Blink + set_color(0, 0, toggle ? 50 : 0); toggle = !toggle; vTaskDelay(pdMS_TO_TICKS(500)); break; - - case LED_STATE_CONNECTED: - // Green Solid - set_color(0, 25, 0); - vTaskDelay(pdMS_TO_TICKS(1000)); + case LED_STATE_CONNECTED: // Green Solid + set_color(0, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000)); break; - - case LED_STATE_MONITORING: - // Blue Solid - set_color(0, 0, 50); - vTaskDelay(pdMS_TO_TICKS(1000)); + case LED_STATE_MONITORING: // Blue Solid + set_color(0, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000)); break; - - case LED_STATE_TRANSMITTING: - // Behavior: Purple Flash (Fast) - "Sending" - if (blink_toggle) set_color(50, 0, 50); // Purple - else set_color(0, 0, 0); // Off - blink_toggle = !blink_toggle; - vTaskDelay(pdMS_TO_TICKS(100)); // Fast toggle + case LED_STATE_TRANSMITTING: // Fast Purple (Busy) + set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle; + vTaskDelay(pdMS_TO_TICKS(50)); break; - - case LED_STATE_STALLED: - // Behavior: Purple Solid - "Buffered/Error 12" - set_color(50, 0, 50); - vTaskDelay(pdMS_TO_TICKS(1000)); + case LED_STATE_TRANSMITTING_SLOW: // Slow Purple (Relaxed) + set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle; + vTaskDelay(pdMS_TO_TICKS(250)); break; - - case LED_STATE_FAILED: - // Red Blink (Fast) - if (blink_toggle) set_color(50, 0, 0); - else set_color(0, 0, 0); - blink_toggle = !blink_toggle; + case LED_STATE_STALLED: // Purple Solid + set_color(50, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000)); + break; + case LED_STATE_FAILED: // Red Blink + set_color(toggle ? 50 : 0, 0, 0); toggle = !toggle; vTaskDelay(pdMS_TO_TICKS(200)); break; } @@ -87,27 +59,15 @@ static void led_task(void *arg) { void status_led_init(int gpio_pin, bool is_rgb_strip) { s_gpio_pin = gpio_pin; s_is_rgb = is_rgb_strip; - - ESP_LOGI(TAG, "Initializing LED on GPIO %d (Type: %s)", - gpio_pin, is_rgb_strip ? "RGB Strip" : "Simple GPIO"); - if (s_is_rgb) { - led_strip_config_t strip_config = { - .strip_gpio_num = gpio_pin, - .max_leds = 1, - }; - led_strip_rmt_config_t rmt_config = { - .resolution_hz = 10 * 1000 * 1000, - .flags.with_dma = false, - }; - ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &s_led_strip)); + led_strip_config_t s_cfg = { .strip_gpio_num = gpio_pin, .max_leds = 1 }; + led_strip_rmt_config_t r_cfg = { .resolution_hz = 10 * 1000 * 1000 }; + led_strip_new_rmt_device(&s_cfg, &r_cfg, &s_led_strip); led_strip_clear(s_led_strip); } else { gpio_reset_pin(gpio_pin); gpio_set_direction(gpio_pin, GPIO_MODE_OUTPUT); - gpio_set_level(gpio_pin, 0); } - xTaskCreate(led_task, "led_task", 2048, NULL, 5, NULL); } diff --git a/components/status_led/status_led.h b/components/status_led/status_led.h index cf05fa7..21a8757 100644 --- a/components/status_led/status_led.h +++ b/components/status_led/status_led.h @@ -12,15 +12,16 @@ typedef enum { LED_STATE_WAITING, LED_STATE_CONNECTED, LED_STATE_MONITORING, - LED_STATE_TRANSMITTING, // Flashing Purple (Active) - LED_STATE_STALLED, // Solid Purple (Non-Fatal Error/Buffering) + LED_STATE_TRANSMITTING, // Busy / Fast Flash (Healthy) + LED_STATE_TRANSMITTING_SLOW, // Slow Pulse (Falling behind) + LED_STATE_STALLED, // Solid Purple (Blocked) LED_STATE_FAILED } led_state_t; /** * @brief Initialize the status LED driver * Supports both Addressable RGB (WS2812) and Simple GPIO LEDs. - * * @param gpio_pin The GPIO pin number + * @param gpio_pin The GPIO pin number * @param is_rgb_strip Set true for NeoPixel/WS2812, false for simple ON/OFF LED */ void status_led_init(int gpio_pin, bool is_rgb_strip); diff --git a/control_iperf.py b/control_iperf.py index 8fa664d..a1812cc 100755 --- a/control_iperf.py +++ b/control_iperf.py @@ -1,4 +1,3 @@ - #!/usr/bin/env python3 import asyncio import argparse @@ -7,89 +6,68 @@ import sys import re class SerialController(asyncio.Protocol): - def __init__(self, port_name, command, loop, completion_future): + def __init__(self, port_name, args, loop, completion_future): self.port_name = port_name - self.command = command + self.args = args self.loop = loop self.transport = None - self.response_buffer = "" + self.buffer = "" self.completion_future = completion_future - self.target_keyword = "IPERF_STARTED" if "start" in command else "IPERF_STOPPED" + + # Determine command string + if args.action == 'pps': + self.cmd_str = f"iperf pps {args.value}\n" + self.target_key = "IPERF_PPS_UPDATED" + elif args.action == 'status': + self.cmd_str = "iperf status\n" + self.target_key = "IPERF_STATUS" + else: + self.cmd_str = f"iperf {args.action}\n" + self.target_key = f"IPERF_{args.action.upper()}ED" # STARTED / STOPPED def connection_made(self, transport): self.transport = transport - # 1. Clear line noise - transport.write(b'\n') - # 2. Schedule command + transport.write(b'\n') # Clear noise self.loop.create_task(self.send_command()) async def send_command(self): await asyncio.sleep(0.1) - # SPACE separated subcommand: "iperf start" or "iperf stop" - full_cmd = f"iperf {self.command}\n" - self.transport.write(full_cmd.encode()) + self.transport.write(self.cmd_str.encode()) def data_received(self, data): - text = data.decode(errors='ignore') - self.response_buffer += text + self.buffer += data.decode(errors='ignore') - # Check for confirmation keyword - if self.target_keyword in self.response_buffer: + if self.target_key in self.buffer: if not self.completion_future.done(): - self.completion_future.set_result(True) + if self.args.action == 'status': + m = re.search(r'PPS=(\d+)', self.buffer) + val = m.group(1) if m else "Unknown" + self.completion_future.set_result(val) + else: + self.completion_future.set_result(True) self.transport.close() def connection_lost(self, exc): if not self.completion_future.done(): - # If we closed it intentionally (set_result called), this is fine. - # If it closed unexpectedly, set exception. - self.completion_future.set_exception(exc if exc else Exception("Connection closed without confirmation")) + self.completion_future.set_exception(Exception("Closed")) -async def run_single_device(port, action): +async def run_device(port, args): loop = asyncio.get_running_loop() - completion_future = loop.create_future() - - transport = None + fut = loop.create_future() try: - transport, protocol = await serial_asyncio.create_serial_connection( - loop, - lambda: SerialController(port, action, loop, completion_future), - port, - baudrate=115200 - ) - - # Wait for success or timeout - await asyncio.wait_for(completion_future, timeout=5.0) - print(f"[{port}] {action.upper()} SUCCESS") - return True - - except asyncio.TimeoutError: - print(f"[{port}] TIMEOUT (No confirmation received)") - return False - except Exception as e: - print(f"[{port}] FAILED: {e}") - return False - finally: - if transport and not transport.is_closing(): - transport.close() + await serial_asyncio.create_serial_connection( + loop, lambda: SerialController(port, args, loop, fut), port, baudrate=115200) + return await asyncio.wait_for(fut, timeout=2.0) + except: + return None def expand_devices(device_str): - """ - Expands device strings like: - - "/dev/ttyUSB0, /dev/ttyUSB1" -> ['/dev/ttyUSB0', '/dev/ttyUSB1'] - - "/dev/ttyUSB0-5" -> ['/dev/ttyUSB0', ... '/dev/ttyUSB5'] - """ devices = [] parts = [d.strip() for d in device_str.split(',')] - for part in parts: - # Check for range syntax (e.g. /dev/ttyUSB0-29) - # Matches "prefix" + "start_num" + "-" + "end_num" match = re.match(r'^(.*?)(\d+)-(\d+)$', part) if match: - prefix = match.group(1) - start = int(match.group(2)) - end = int(match.group(3)) + prefix, start, end = match.group(1), int(match.group(2)), int(match.group(3)) step = 1 if end >= start else -1 for i in range(start, end + step, step): devices.append(f"{prefix}{i}") @@ -98,119 +76,31 @@ def expand_devices(device_str): return devices async def main(): - parser = argparse.ArgumentParser(description='Control ESP32 iperf concurrently') - parser.add_argument('action', choices=['start', 'stop'], help='Action to perform') - parser.add_argument('--devices', required=True, - help='Device list (e.g., "/dev/ttyUSB0-29" or "/dev/ttyUSB0,/dev/ttyUSB1")') + parser = argparse.ArgumentParser() + parser.add_argument('action', choices=['start', 'stop', 'pps', 'status']) + parser.add_argument('--value', type=int, help='Value for PPS command') + parser.add_argument('--devices', required=True, help="/dev/ttyUSB0-29") args = parser.parse_args() + if args.action == 'pps' and not args.value: + print("Error: 'pps' action requires --value") + sys.exit(1) - if sys.platform == 'win32': - asyncio.set_event_loop(asyncio.ProactorEventLoop()) + if sys.platform == 'win32': asyncio.set_event_loop(asyncio.ProactorEventLoop()) - # 1. Expand device list - device_list = expand_devices(args.devices) - print(f"Targeting {len(device_list)} devices for '{args.action.upper()}'...") + devs = expand_devices(args.devices) + print(f"Executing '{args.action}' on {len(devs)} devices...") - # 2. Create tasks for all devices - tasks = [run_single_device(dev, args.action) for dev in device_list] - - # 3. Run all concurrently + tasks = [run_device(d, args) for d in devs] results = await asyncio.gather(*tasks) - # 4. Summary - success_count = results.count(True) - print(f"\nSummary: {success_count}/{len(device_list)} Succeeded") + print("\nResults:") + for dev, res in zip(devs, results): + if args.action == 'status': + print(f"{dev}: {res if res else 'TIMEOUT'} PPS") + else: + status = "OK" if res is True else "FAIL" + print(f"{dev}: {status}") if __name__ == '__main__': - try: - asyncio.run(main()) - except KeyboardInterrupt: - pass - -#!/usr/bin/env python3 -import asyncio -import argparse -import serial_asyncio -import sys - -class SerialController(asyncio.Protocol): - def __init__(self, command, loop): - self.command = command - self.loop = loop - self.transport = None - self.response_buffer = "" - # Keywords the firmware will print to confirm action - self.target_keyword = "IPERF_STARTED" if "start" in command else "IPERF_STOPPED" - - def connection_made(self, transport): - self.transport = transport - print(f"Connected. Sending: iperf {self.command}") - - # 1. Clear line noise - transport.write(b'\n') - - # 2. Schedule command - self.loop.create_task(self.send_command()) - - async def send_command(self): - await asyncio.sleep(0.1) - # SPACE separated subcommand: "iperf start" or "iperf stop" - full_cmd = f"iperf {self.command}\n" - self.transport.write(full_cmd.encode()) - - def data_received(self, data): - text = data.decode(errors='ignore') - sys.stdout.write(text) # Echo firmware output to console - self.response_buffer += text - - # Check for confirmation keyword - if self.target_keyword in self.response_buffer: - print(f"\n[SUCCESS] Confirmed: {self.target_keyword}") - self.transport.close() - self.loop.stop() - - def connection_lost(self, exc): - if exc: - print(f"Serial connection lost: {exc}") - self.loop.stop() - -async def run_control(port, action): - loop = asyncio.get_running_loop() - - try: - transport, protocol = await serial_asyncio.create_serial_connection( - loop, - lambda: SerialController(action, loop), - port, - baudrate=115200 - ) - except Exception as e: - print(f"Failed to open port {port}: {e}") - return - - # Safety timeout: 5 seconds - try: - await asyncio.wait_for(loop.create_future(), timeout=5.0) - except asyncio.TimeoutError: - print("\n[TIMEOUT] Firmware did not respond with confirmation keyword.") - except asyncio.CancelledError: - pass - - if transport and not transport.is_closing(): - transport.close() - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Control ESP32 iperf via USB') - parser.add_argument('port', help='Serial port (e.g., /dev/ttyUSB0)') - parser.add_argument('action', choices=['start', 'stop'], help='Action to perform') - - args = parser.parse_args() - - if sys.platform == 'win32': - asyncio.set_event_loop(asyncio.ProactorEventLoop()) - - try: - asyncio.run(run_control(args.port, args.action)) - except KeyboardInterrupt: - pass + asyncio.run(main())