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
+15 -16
View File
@@ -41,31 +41,30 @@ def _run_command(
timeout: int = 300,
callback: ProgressCallback = None,
) -> subprocess.CompletedProcess[str]:
"""Run a subprocess command with progress reporting.
"""Run a subprocess command with real-time streaming output.
Uses subprocess_utils.run_streaming to stream stdout/stderr in
real time, parsing for warnings, errors, and progress.
Args:
cmd: Command and arguments.
timeout: Maximum seconds to wait (flash ops can take minutes).
timeout: Maximum seconds.
callback: Optional callback(status, message).
Returns:
CompletedProcess result.
"""
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
def _build_program_flash_cmd_base(
@@ -135,7 +134,7 @@ def wipe_flash(
cmd += ["-erase_only"] # Erase sectors matching the BIN size (safer than -erase_all)
try:
result = _run_command(cmd, timeout=300, callback=callback)
result = _run_command(cmd, timeout=config.step_timeouts.get("flash_erase", 120), callback=callback)
success = result.returncode == 0
return FlashResult(
step="wipe",
@@ -195,7 +194,7 @@ def program_flash_bin(
]
try:
result = _run_command(cmd, timeout=600, callback=callback)
result = _run_command(cmd, timeout=config.step_timeouts.get("flash_program", 600), callback=callback)
success = result.returncode == 0
return FlashResult(
step="program",