feat: UART monitor, step timeouts, and non-blocking subprocess output

Based on real hardware timing data from XC7Z100 testing:

UART Monitoring:
- New UartMonitor class in serial_monitor.py: background thread reads
  serial output continuously, parses for IP/version/boot messages
- GUI starts monitor when workflow runs, stops on completion/close
- Serial lines flow into log display in real time

Step Timing:
- New step_timeouts config (experience × 2):
  check_env:30s flash_erase:120s flash_program:600s
  bitstream:60s elf_download:60s tftp_reboot:120s
- GUI shows estimated timeout before each step
- Shows actual elapsed time after each step completes
- All subprocess calls now use config-driven timeouts

Non-blocking Output:
- New subprocess_utils.py: run_streaming() uses Popen + threaded
  stdout/stderr readers, parses WARNING/ERROR/progress% in real time
- flash_programmer and bitstream_programmer use it
- Callbacks receive categorized events (out/err/warn/error/progress/done)
This commit is contained in:
Jeremy Shen
2026-06-10 13:21:32 +08:00
parent ab7264d5c7
commit 2c48224aea
8 changed files with 434 additions and 33 deletions
+50 -1
View File
@@ -24,7 +24,7 @@ from ip_verifier import ping_ip
from tftp_manager import tftp_upload_verify, TftpResult
from reboot_manager import reboot_zynq, RebootResult
from boot_verifier import verify_boot, verify_zynq_status, BootVerificationResult
from serial_monitor import detect_serial_ports, parse_boot_output, check_uart_available
from serial_monitor import detect_serial_ports, parse_boot_output, check_uart_available, UartMonitor
from gui.styles import (
WINDOW_WIDTH,
WINDOW_HEIGHT,
@@ -79,6 +79,7 @@ class MainWindow(ctk.CTk):
self._uart_message: str = ""
self._zynq_jtag_present: bool = False
self._zynq_jtag_info = None
self._uart_monitor: UartMonitor | None = None
# StringVar references for config sync
self._ip_string_var: ctk.StringVar | None = None
@@ -961,6 +962,15 @@ class MainWindow(ctk.CTk):
self._run_step_3_load_bootloader,
self._run_step_4_load_main_program,
]
step_timeout_keys = [
("check_env", 30),
("flash_program", 600),
("bitstream", 60),
("tftp_reboot", 120),
]
# Start UART monitor if available
self._start_uart_monitor()
# Determine end index based on single_step flag
end_index = start_index + 1 if single_step else len(step_funcs)
@@ -970,12 +980,21 @@ class MainWindow(ctk.CTk):
for i in range(start_index, end_index):
step_idx = i - start_index
self._set_step_status(i, "running")
# Log step timeout estimate
timeout_key, default_timeout = step_timeout_keys[i] if i < len(step_timeout_keys) else ("", 30)
step_timeout = self._config.step_timeouts.get(timeout_key, default_timeout) if self._config else default_timeout
self._log_message(f" ⏱ Step {i+1} timeout: {step_timeout}s")
self._progress.set_value(step_idx / total)
t_step_start = time.time()
try:
success = step_funcs[i]()
results.append(success)
self._set_step_status(i, "complete" if success else "error")
elapsed = time.time() - t_step_start
self._log_message(f" ⏱ Step {i+1} completed in {elapsed:.0f}s")
except Exception as e:
results.append(False)
self._set_step_status(i, "error")
@@ -1020,6 +1039,35 @@ class MainWindow(ctk.CTk):
# Save config after running
self._save_config()
# Stop UART monitor
self._stop_uart_monitor()
# ── UART Monitor ──────────────────────────────────────────
def _start_uart_monitor(self) -> None:
"""Start background serial monitoring if UART is available."""
if not self._uart_available or not self._config:
return
port = self._config.serial_port
if not port:
return
self._uart_monitor = UartMonitor(port, self._config.serial_baudrate)
self._uart_monitor.on_line = lambda line: self._log_message(f" [UART] {line}")
self._uart_monitor.on_boot_info = lambda info: (
self._log_message(f" [UART] Boot detected: IP={info.ip_address}, Ver={info.version}")
if info.ip_address or info.version else None
)
self._uart_monitor.on_error = lambda e: self._log_message(f" [UART] Error: {e}")
if self._uart_monitor.start():
self._log_message(" [UART] Serial monitor started")
def _stop_uart_monitor(self) -> None:
"""Stop background serial monitoring."""
if self._uart_monitor:
self._uart_monitor.stop()
self._uart_monitor = None
# ── Serial ─────────────────────────────────────────────────
def _refresh_ports(self) -> None:
@@ -1040,6 +1088,7 @@ class MainWindow(ctk.CTk):
def _on_close(self) -> None:
"""Handle window close event: save config and cleanup."""
self._stop_uart_monitor()
if self._config:
self._sync_config_from_ui() # Sync UI values first
# Save to user config file (config.yaml)