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
+17 -16
View File
@@ -117,21 +117,17 @@ def _run_command(
Raises:
subprocess.TimeoutExpired: If command exceeds timeout.
"""
from subprocess_utils import run_streaming
if callback:
callback("running", f"Executing: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
)
if callback:
if result.returncode == 0:
callback("complete", f"{cmd[0]} succeeded")
else:
callback("error", f"{cmd[0]} failed (exit {result.returncode})")
return result
try:
return run_streaming(cmd, timeout=timeout, callback=callback)
except subprocess.TimeoutExpired:
if callback:
callback("error", f"{cmd[0]} timed out after {timeout}s")
raise
# ── Bitstream Download ──────────────────────────────────────────────
@@ -172,7 +168,8 @@ def program_bitstream(
# Try xsct first (preferred)
xsct = get_xsct_path(config.vitis_path)
if xsct:
return _program_with_xsct(xsct, bit_path, callback, ps7_tcl)
timeout = config.step_timeouts.get("bitstream", 60)
return _program_with_xsct(xsct, bit_path, callback, ps7_tcl, timeout)
# Fallback to Vivado
vivado = _get_vivado_path(config.vitis_path)
@@ -191,6 +188,7 @@ def _program_with_xsct(
bit_path: Path,
callback: ProgressCallback = None,
ps7_init_tcl: str = "",
timeout: int = 60,
) -> BitstreamResult:
"""Program bitstream using xsct's fpga command.
@@ -203,6 +201,7 @@ def _program_with_xsct(
bit_path: Path to .bit file.
callback: Optional progress callback.
ps7_init_tcl: Path to ps7_init.tcl for PS initialization.
timeout: Subprocess timeout in seconds.
Returns:
BitstreamResult with status.
@@ -232,7 +231,7 @@ def _program_with_xsct(
[xsct_path, tcl_file],
capture_output=True,
text=True,
timeout=120,
timeout=timeout,
)
os.unlink(tcl_file)
success = result.returncode == 0
@@ -351,7 +350,8 @@ def run_elf(
# Auto-detect ps7_init.tcl from config or BIT file directory
ps7_tcl = _find_ps7_init_tcl(config)
return _run_elf_with_xsct(xsct, elf_path, callback, ps7_tcl)
timeout = config.step_timeouts.get("elf_download", 60)
return _run_elf_with_xsct(xsct, elf_path, callback, ps7_tcl, timeout)
def _run_elf_with_xsct(
@@ -359,6 +359,7 @@ def _run_elf_with_xsct(
elf_path: Path,
callback: ProgressCallback = None,
ps7_init_tcl: str = "",
timeout: int = 60,
) -> BitstreamResult:
"""Download and run ELF using xsct's dow command.
@@ -399,7 +400,7 @@ def _run_elf_with_xsct(
[xsct_path, tcl_file],
capture_output=True,
text=True,
timeout=120,
timeout=timeout,
)
os.unlink(tcl_file)