more on build w/iperf fixes
This commit is contained in:
parent
d0597d048b
commit
a62790cbb4
|
|
@ -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)
|
#if defined(CONFIG_FREERTOS_USE_TRACE_FACILITY) && defined(CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS)
|
||||||
print_all_task_priorities();
|
print_all_task_priorities();
|
||||||
#endif
|
#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
|
// Force LED to Purple immediately
|
||||||
s_led_state = LED_PURPLE_SOLID;
|
s_led_state = LED_PURPLE_SOLID;
|
||||||
iperf_set_physical_led(64, 0, 64);
|
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;
|
int64_t time_to_wait = next_send_time - current_time;
|
||||||
|
|
||||||
if (time_to_wait > 0) {
|
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) {
|
if (time_to_wait > 2000) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(time_to_wait / 1000));
|
vTaskDelay(pdMS_TO_TICKS(time_to_wait / 1000));
|
||||||
}
|
} else {
|
||||||
// If the wait is short, spin but yield to other ready tasks (like WiFi/TCP-IP)
|
|
||||||
else {
|
|
||||||
while (esp_timer_get_time() < next_send_time) {
|
while (esp_timer_get_time() < next_send_time) {
|
||||||
taskYIELD();
|
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++) {
|
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;
|
udp_datagram *header = (udp_datagram *)ctrl->buffer;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
header->id = htonl(packet_count);
|
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->tv_usec = htonl(ts.tv_nsec / 1000);
|
||||||
header->id2 = 0;
|
header->id2 = 0;
|
||||||
|
|
||||||
if (packet_count == 0) {
|
// 3. Send the full buffer (UDP Header + Client Header + Zeros)
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
int send_len = sendto(sockfd, ctrl->buffer, payload_len, 0, (struct sockaddr *)&addr, sizeof(addr));
|
int send_len = sendto(sockfd, ctrl->buffer, payload_len, 0, (struct sockaddr *)&addr, sizeof(addr));
|
||||||
|
|
||||||
if (send_len > 0) {
|
if (send_len > 0) {
|
||||||
|
|
|
||||||
174
esp32_deploy.py
174
esp32_deploy.py
|
|
@ -74,9 +74,10 @@ class UnifiedDeployWorker:
|
||||||
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*)')
|
||||||
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_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):
|
async def run(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -85,12 +86,24 @@ class UnifiedDeployWorker:
|
||||||
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
|
||||||
if not await self._flash_firmware(): return False
|
if not await self._flash_firmware(): return False
|
||||||
|
# Wait for flash tool to release port
|
||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.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:
|
||||||
if not await self._configure_device():
|
# RETRY LOOP
|
||||||
self.log.warning(f"{Colors.YELLOW}Config verify failed. Marking SUCCESS (Flash OK).{Colors.RESET}")
|
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:
|
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
|
if self.args.config_only: return False
|
||||||
|
|
@ -119,7 +132,6 @@ class UnifiedDeployWorker:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _flash_firmware(self):
|
async def _flash_firmware(self):
|
||||||
# 1. Determine Target
|
|
||||||
detected_target = None
|
detected_target = None
|
||||||
if self.args.target == 'auto':
|
if self.args.target == 'auto':
|
||||||
detected_target = await self._identify_chip()
|
detected_target = await self._identify_chip()
|
||||||
|
|
@ -131,11 +143,9 @@ class UnifiedDeployWorker:
|
||||||
else:
|
else:
|
||||||
target_to_use = self.args.target
|
target_to_use = self.args.target
|
||||||
|
|
||||||
# 2. Locate Artifacts
|
|
||||||
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"
|
||||||
|
|
||||||
# Find unique binary for this specific target config
|
|
||||||
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):
|
||||||
|
|
@ -144,7 +154,7 @@ class UnifiedDeployWorker:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not unique_app:
|
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
|
return False
|
||||||
|
|
||||||
unique_boot = f"bootloader_{suffix}.bin"
|
unique_boot = f"bootloader_{suffix}.bin"
|
||||||
|
|
@ -152,7 +162,6 @@ class UnifiedDeployWorker:
|
||||||
unique_ota = f"ota_data_initial_{suffix}.bin"
|
unique_ota = f"ota_data_initial_{suffix}.bin"
|
||||||
unique_args_file = f"flash_args_{suffix}"
|
unique_args_file = f"flash_args_{suffix}"
|
||||||
|
|
||||||
# 3. Read flash_args
|
|
||||||
flash_args_path = firmware_dir / unique_args_file
|
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")
|
||||||
|
|
@ -165,25 +174,19 @@ class UnifiedDeployWorker:
|
||||||
raw_args = [x for x in content.split(' ') if x]
|
raw_args = [x for x in content.split(' ') if x]
|
||||||
final_args = []
|
final_args = []
|
||||||
|
|
||||||
# 4. Construct Flash Command (Safe Swapping)
|
|
||||||
for arg in raw_args:
|
for arg in raw_args:
|
||||||
if arg.endswith('bootloader.bin'):
|
if arg.endswith('bootloader.bin'):
|
||||||
final_args.append(str(firmware_dir / unique_boot))
|
final_args.append(str(firmware_dir / unique_boot))
|
||||||
elif arg.endswith('partition-table.bin'):
|
elif arg.endswith('partition-table.bin'):
|
||||||
final_args.append(str(firmware_dir / unique_part))
|
final_args.append(str(firmware_dir / unique_part))
|
||||||
elif arg.endswith('ota_data_initial.bin'):
|
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():
|
if (firmware_dir / unique_ota).exists():
|
||||||
final_args.append(str(firmware_dir / unique_ota))
|
final_args.append(str(firmware_dir / unique_ota))
|
||||||
else:
|
else:
|
||||||
# Skip if missing to avoid error
|
|
||||||
self.log.warning(f"OTA binary {unique_ota} missing. Skipping arg to prevent crash.")
|
|
||||||
continue
|
continue
|
||||||
elif arg.endswith('phy_init_data.bin'):
|
elif arg.endswith('phy_init_data.bin'):
|
||||||
# System binary: Do NOT replace with App.
|
|
||||||
final_args.append(arg)
|
final_args.append(arg)
|
||||||
elif arg.endswith('.bin'):
|
elif arg.endswith('.bin'):
|
||||||
# This catch-all is for the MAIN APP only.
|
|
||||||
final_args.append(str(firmware_dir / unique_app))
|
final_args.append(str(firmware_dir / unique_app))
|
||||||
else:
|
else:
|
||||||
final_args.append(arg)
|
final_args.append(arg)
|
||||||
|
|
@ -213,26 +216,31 @@ class UnifiedDeployWorker:
|
||||||
async def _configure_device(self):
|
async def _configure_device(self):
|
||||||
try:
|
try:
|
||||||
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
|
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:
|
try:
|
||||||
if self.args.config_only:
|
# 1. Reset (Standard Sequence: DTR=0, RTS=0 -> Idle)
|
||||||
writer.transport.serial.dtr = False
|
# Assert Reset (EN=L)
|
||||||
writer.transport.serial.rts = True
|
writer.transport.serial.dtr = False
|
||||||
await asyncio.sleep(0.1)
|
writer.transport.serial.rts = True
|
||||||
writer.transport.serial.rts = False
|
await asyncio.sleep(0.2)
|
||||||
await asyncio.sleep(0.1)
|
# Release Reset (EN=H)
|
||||||
writer.transport.serial.dtr = True
|
writer.transport.serial.rts = False
|
||||||
|
# Ensure DTR is Idle (IO0=H)
|
||||||
|
writer.transport.serial.dtr = False
|
||||||
|
|
||||||
if not await self._wait_for_boot(reader):
|
# 2. Wait for App (Active Poking)
|
||||||
self.log.warning("Boot prompt missed...")
|
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)
|
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}")
|
self.log.info(f"{Colors.GREEN}Config verified.{Colors.RESET}")
|
||||||
await self._perform_reset(writer)
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.log.error(f"{Colors.RED}Config verification failed.{Colors.RESET}")
|
self.log.error(f"{Colors.RED}Config verification failed.{Colors.RESET}")
|
||||||
|
|
@ -245,56 +253,96 @@ class UnifiedDeployWorker:
|
||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
|
|
||||||
async def _perform_reset(self, writer):
|
async def _wait_for_boot(self, reader, writer):
|
||||||
try:
|
# Timeout 20s to cover GPS delay
|
||||||
writer.transport.serial.dtr = False
|
end_time = time.time() + 20
|
||||||
writer.transport.serial.rts = True
|
last_poke = time.time()
|
||||||
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):
|
self.log.info("Waiting for boot logs...")
|
||||||
timeout = time.time() + 10
|
|
||||||
while time.time() < timeout:
|
while time.time() < end_time:
|
||||||
try:
|
try:
|
||||||
line = (await asyncio.wait_for(reader.readline(), timeout=0.5)).decode('utf-8', errors='ignore').strip()
|
# Poke every 2 seconds to wake up console
|
||||||
if self.regex_ready.search(line): return True
|
if time.time() - last_poke > 2.0:
|
||||||
except asyncio.TimeoutError: continue
|
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
|
return False
|
||||||
|
|
||||||
async def _send_config(self, writer):
|
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'
|
csi_val = '1' if self.args.csi_enable else '0'
|
||||||
role_str = "SERVER" if self.args.iperf_server else "CLIENT"
|
role_str = "SERVER" if self.args.iperf_server else "CLIENT"
|
||||||
iperf_enable_val = '0' if self.args.no_iperf else '1'
|
iperf_enable_val = '0' if self.args.no_iperf else '1'
|
||||||
period_us = int(self.args.iperf_period * 1000000)
|
period_us = int(self.args.iperf_period * 1000000)
|
||||||
|
|
||||||
config_str = (
|
config_lines = [
|
||||||
f"CFG\nSSID:{self.args.ssid}\nPASS:{self.args.password}\nIP:{self.target_ip}\n"
|
"CFG",
|
||||||
f"MASK:{self.args.netmask}\nGW:{self.args.gateway}\nDHCP:0\nBAND:{self.args.band}\n"
|
f"SSID:{self.args.ssid}",
|
||||||
f"BW:{self.args.bandwidth}\nPOWERSAVE:{self.args.powersave}\nMODE:{self.args.mode}\n"
|
f"PASS:{self.args.password}",
|
||||||
f"MON_CH:{self.args.monitor_channel}\nCSI:{csi_val}\n"
|
f"IP:{self.target_ip}",
|
||||||
f"IPERF_PERIOD_US:{period_us}\n"
|
f"MASK:{self.args.netmask}",
|
||||||
f"IPERF_ROLE:{role_str}\n"
|
f"GW:{self.args.gateway}",
|
||||||
f"IPERF_PROTO:{self.args.iperf_proto}\n"
|
f"DHCP:0",
|
||||||
f"IPERF_DEST_IP:{self.args.iperf_dest_ip}\n"
|
f"BAND:{self.args.band}",
|
||||||
f"IPERF_PORT:{self.args.iperf_port}\n"
|
f"BW:{self.args.bandwidth}",
|
||||||
f"IPERF_BURST:{self.args.iperf_burst}\n"
|
f"POWERSAVE:{self.args.powersave}",
|
||||||
f"IPERF_LEN:{self.args.iperf_len}\n"
|
f"MODE:{self.args.mode}",
|
||||||
f"IPERF_ENABLED:{iperf_enable_val}\n"
|
f"MON_CH:{self.args.monitor_channel}",
|
||||||
f"END\n"
|
f"CSI:{csi_val}",
|
||||||
)
|
f"IPERF_PERIOD_US:{period_us}",
|
||||||
writer.write(config_str.encode('utf-8'))
|
f"IPERF_ROLE:{role_str}",
|
||||||
await writer.drain()
|
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):
|
async def _verify_configuration(self, reader):
|
||||||
timeout = time.time() + 20
|
timeout = time.time() + 15
|
||||||
while time.time() < timeout:
|
while time.time() < timeout:
|
||||||
try:
|
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 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)
|
m = self.regex_got_ip.search(line)
|
||||||
if m and m.group(1) == self.target_ip: return True
|
if m and m.group(1) == self.target_ip: return True
|
||||||
except asyncio.TimeoutError: continue
|
except asyncio.TimeoutError: continue
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue