diff --git a/components/iperf/iperf.c b/components/iperf/iperf.c index bc80521..3655da2 100644 --- a/components/iperf/iperf.c +++ b/components/iperf/iperf.c @@ -263,6 +263,22 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) #if defined(CONFIG_FREERTOS_USE_TRACE_FACILITY) && defined(CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS) print_all_task_priorities(); #endif + + // --- OPTIMIZATION START --- + // 1. Initialize Payload ONCE (Static Memory Concept) + // We assume ctrl->buffer was calloc'd or memset to 0 in iperf_start + + // Construct Client Header (Static Data) + // This sits at offset 16 (after udp_datagram) and persists for the whole test + 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)); + // --- OPTIMIZATION END --- + // Force LED to Purple immediately s_led_state = LED_PURPLE_SOLID; iperf_set_physical_led(64, 0, 64); @@ -280,12 +296,9 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) int64_t time_to_wait = next_send_time - current_time; if (time_to_wait > 0) { - // If the wait is long (> 2ms), sleep to save power and let lower priority tasks run if (time_to_wait > 2000) { vTaskDelay(pdMS_TO_TICKS(time_to_wait / 1000)); - } - // If the wait is short, spin but yield to other ready tasks (like WiFi/TCP-IP) - else { + } else { while (esp_timer_get_time() < next_send_time) { taskYIELD(); } @@ -293,6 +306,8 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) } for (int k = 0; k < burst_count; k++) { + // 2. Update Dynamic Data Only (Sequence ID & Timestamp) + // This overwrites the first 16 bytes. The Client Header (bytes 16-40) remains untouched. udp_datagram *header = (udp_datagram *)ctrl->buffer; clock_gettime(CLOCK_MONOTONIC, &ts); header->id = htonl(packet_count); @@ -300,16 +315,7 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) header->tv_usec = htonl(ts.tv_nsec / 1000); header->id2 = 0; - if (packet_count == 0) { - 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)); - } - + // 3. Send the full buffer (UDP Header + Client Header + Zeros) int send_len = sendto(sockfd, ctrl->buffer, payload_len, 0, (struct sockaddr *)&addr, sizeof(addr)); if (send_len > 0) { diff --git a/esp32_deploy.py b/esp32_deploy.py index 846f8ff..2cec440 100755 --- a/esp32_deploy.py +++ b/esp32_deploy.py @@ -74,9 +74,10 @@ class UnifiedDeployWorker: self.log = DeviceLoggerAdapter(logger, {'connid': port}) self.regex_chip_type = re.compile(r'Detecting chip type... (ESP32\S*)') - self.regex_ready = re.compile(r'Initialization complete|GPS synced|GPS initialization aborted|No Config Found', re.IGNORECASE) + # We look for "Entering idle loop" (App Ready) or the prompt + 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_csi_saved = re.compile(r'CSI enable state saved', re.IGNORECASE) + self.regex_csi_saved = re.compile(r'CSI enable state saved|Config saved', re.IGNORECASE) async def run(self): try: @@ -85,12 +86,24 @@ class UnifiedDeployWorker: if self.args.flash_erase: if not await self._erase_flash(): return False if not await self._flash_firmware(): return False + # Wait for flash tool to release port await asyncio.sleep(1.0) if not self.args.flash_only: if self.args.ssid and self.args.password: - if not await self._configure_device(): - self.log.warning(f"{Colors.YELLOW}Config verify failed. Marking SUCCESS (Flash OK).{Colors.RESET}") + # RETRY LOOP + success = False + for attempt in range(1, 4): + self.log.info(f"Configuring (Attempt {attempt}/3)...") + if await self._configure_device(): + success = True + break + self.log.warning(f"Config failed on attempt {attempt}. Retrying...") + await asyncio.sleep(2.0) + + if not success: + self.log.error(f"{Colors.RED}Config verify failed after 3 attempts.{Colors.RESET}") + return False else: self.log.warning("No SSID/Password provided, skipping config") if self.args.config_only: return False @@ -119,7 +132,6 @@ class UnifiedDeployWorker: return False async def _flash_firmware(self): - # 1. Determine Target detected_target = None if self.args.target == 'auto': detected_target = await self._identify_chip() @@ -131,11 +143,9 @@ class UnifiedDeployWorker: else: target_to_use = self.args.target - # 2. Locate Artifacts suffix = generate_config_suffix(target_to_use, self.args.csi_enable, self.args.ampdu) firmware_dir = self.project_dir / "firmware" - # Find unique binary for this specific target config unique_app = None if firmware_dir.exists(): for f in os.listdir(firmware_dir): @@ -144,7 +154,7 @@ class UnifiedDeployWorker: break if not unique_app: - self.log.error(f"Binary for config '{suffix}' not found in firmware/. Run --target all first?") + self.log.error(f"Binary for config '{suffix}' not found in firmware/.") return False unique_boot = f"bootloader_{suffix}.bin" @@ -152,7 +162,6 @@ class UnifiedDeployWorker: unique_ota = f"ota_data_initial_{suffix}.bin" unique_args_file = f"flash_args_{suffix}" - # 3. Read flash_args flash_args_path = firmware_dir / unique_args_file if not flash_args_path.exists(): self.log.error(f"flash_args for {suffix} not found") @@ -165,25 +174,19 @@ class UnifiedDeployWorker: raw_args = [x for x in content.split(' ') if x] final_args = [] - # 4. Construct Flash Command (Safe Swapping) for arg in raw_args: if arg.endswith('bootloader.bin'): final_args.append(str(firmware_dir / unique_boot)) elif arg.endswith('partition-table.bin'): final_args.append(str(firmware_dir / unique_part)) elif arg.endswith('ota_data_initial.bin'): - # Only use unique if it exists, otherwise assume standard path relative to build (risky if build gone) if (firmware_dir / unique_ota).exists(): final_args.append(str(firmware_dir / unique_ota)) else: - # Skip if missing to avoid error - self.log.warning(f"OTA binary {unique_ota} missing. Skipping arg to prevent crash.") continue elif arg.endswith('phy_init_data.bin'): - # System binary: Do NOT replace with App. final_args.append(arg) elif arg.endswith('.bin'): - # This catch-all is for the MAIN APP only. final_args.append(str(firmware_dir / unique_app)) else: final_args.append(arg) @@ -213,26 +216,31 @@ class UnifiedDeployWorker: 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 + except Exception as e: + self.log.error(f"Serial open failed: {e}") + return False try: - if self.args.config_only: - writer.transport.serial.dtr = False - writer.transport.serial.rts = True - await asyncio.sleep(0.1) - writer.transport.serial.rts = False - await asyncio.sleep(0.1) - writer.transport.serial.dtr = True + # 1. Reset (Standard Sequence: DTR=0, RTS=0 -> Idle) + # Assert Reset (EN=L) + writer.transport.serial.dtr = False + writer.transport.serial.rts = True + await asyncio.sleep(0.2) + # Release Reset (EN=H) + writer.transport.serial.rts = False + # Ensure DTR is Idle (IO0=H) + writer.transport.serial.dtr = False - if not await self._wait_for_boot(reader): - self.log.warning("Boot prompt missed...") + # 2. Wait for App (Active Poking) + if not await self._wait_for_boot(reader, writer): + self.log.warning("Boot prompt missed (sending blindly)...") + # 3. Send Config await self._send_config(writer) - is_configured = await self._verify_configuration(reader) - if is_configured: + # 4. Verify + if await self._verify_configuration(reader): self.log.info(f"{Colors.GREEN}Config verified.{Colors.RESET}") - await self._perform_reset(writer) return True else: self.log.error(f"{Colors.RED}Config verification failed.{Colors.RESET}") @@ -245,56 +253,96 @@ class UnifiedDeployWorker: writer.close() await writer.wait_closed() - async def _perform_reset(self, writer): - try: - writer.transport.serial.dtr = False - writer.transport.serial.rts = True - await asyncio.sleep(0.2) - writer.transport.serial.rts = False - await asyncio.sleep(0.1) - except Exception as e: - self.log.error(f"Failed to reset device: {e}") + async def _wait_for_boot(self, reader, writer): + # Timeout 20s to cover GPS delay + end_time = time.time() + 20 + last_poke = time.time() - async def _wait_for_boot(self, reader): - timeout = time.time() + 10 - while time.time() < timeout: + self.log.info("Waiting for boot logs...") + + while time.time() < end_time: try: - line = (await asyncio.wait_for(reader.readline(), timeout=0.5)).decode('utf-8', errors='ignore').strip() - if self.regex_ready.search(line): return True - except asyncio.TimeoutError: continue + # Poke every 2 seconds to wake up console + if time.time() - last_poke > 2.0: + writer.write(b'\r\n') + await writer.drain() + last_poke = time.time() + + # Read with short timeout + try: + line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.1) + line = line_bytes.decode('utf-8', errors='ignore').strip() + if not line: continue + + # DEBUG LOG: Show us what the device is saying! + # print(f"[{self.port}] [Log] {line}") + + 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): + # 1. Clear buffer with a newline + await asyncio.sleep(0.5) + writer.write(b'\r\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) - config_str = ( - f"CFG\nSSID:{self.args.ssid}\nPASS:{self.args.password}\nIP:{self.target_ip}\n" - f"MASK:{self.args.netmask}\nGW:{self.args.gateway}\nDHCP:0\nBAND:{self.args.band}\n" - f"BW:{self.args.bandwidth}\nPOWERSAVE:{self.args.powersave}\nMODE:{self.args.mode}\n" - f"MON_CH:{self.args.monitor_channel}\nCSI:{csi_val}\n" - f"IPERF_PERIOD_US:{period_us}\n" - f"IPERF_ROLE:{role_str}\n" - f"IPERF_PROTO:{self.args.iperf_proto}\n" - f"IPERF_DEST_IP:{self.args.iperf_dest_ip}\n" - f"IPERF_PORT:{self.args.iperf_port}\n" - f"IPERF_BURST:{self.args.iperf_burst}\n" - f"IPERF_LEN:{self.args.iperf_len}\n" - f"IPERF_ENABLED:{iperf_enable_val}\n" - f"END\n" - ) - writer.write(config_str.encode('utf-8')) - await writer.drain() + 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_DEST_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" + ] + + # 2. Send Line-by-Line to prevent FIFO Overflow (128 bytes max) + for line in config_lines: + cmd = line + "\r\n" + writer.write(cmd.encode('utf-8')) + await writer.drain() + # 50ms delay between lines allows the ESP32 task to empty the FIFO + await asyncio.sleep(0.05) async def _verify_configuration(self, reader): - timeout = time.time() + 20 + timeout = time.time() + 15 while time.time() < timeout: try: - line = (await asyncio.wait_for(reader.readline(), timeout=1.0)).decode('utf-8', errors='ignore').strip() + 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 "Config saved" in line or self.regex_csi_saved.search(line): return True + + # print(f"[{self.port}] [Verify] {line}") # Debug + + 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