feat: initial Zynq XC7Z100 Flasher GUI project

Add cross-platform GUI application for programming Zynq devices
with step-by-step workflow:

- CustomTkinter GUI with dark/light theme support
- 4-step workflow: Check Env → Flash → Bootloader → Main Program
- YAML configuration with relative path resolution
- Serial monitoring and boot log parsing
- TFTP upload with CRC32 verification
- SSH/serial reboot management
- Vivado/Impact tool auto-detection

Project structure:
- app.py: Entry point
- src/: Core modules (config, flash, bitstream, tftp, serial, gui)
- config/: Default configuration template
- .flasher_env/: Python virtual environment
This commit is contained in:
Jeremy Shen
2026-06-08 11:33:50 +08:00
commit bd39023ba7
19 changed files with 3859 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
"""Reboot manager for Zynq Flasher GUI.
Handles rebooting the Zynq device via SSH or serial commands.
"""
from __future__ import annotations
import importlib
import platform
import shutil
import subprocess
import time
from dataclasses import dataclass
from typing import Callable
from config_manager import Config
@dataclass
class RebootResult:
"""Result of a reboot operation."""
success: bool
message: str
method: str = ""
output: str = ""
ProgressCallback = Callable[[str, str], None] | None
def reboot_via_ssh(
config: Config,
timeout: int | None = None,
callback: ProgressCallback = None,
) -> RebootResult:
"""Reboot the Zynq device via SSH.
Sends a 'reboot' command over SSH to the Zynq device.
Args:
config: Application configuration.
timeout: Seconds to wait for reboot acknowledgment.
callback: Optional progress callback.
Returns:
RebootResult with status.
"""
timeout = timeout or config.reboot_timeout
ip = config.zynq_ip
if callback:
callback("start", f"Sending reboot via SSH to {ip}...")
# Check ssh availability
if not shutil.which("ssh"):
return RebootResult(
success=False,
message="ssh command not found in PATH",
method="ssh",
)
# Try SSH reboot
cmd = ["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no",
f"root@{ip}", "reboot"]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=10,
)
if callback:
if result.returncode == 0:
callback("progress", f"Reboot command sent to {ip}")
else:
callback("error", f"SSH reboot failed: {result.stderr.strip()}")
# Wait for device to come back
if result.returncode == 0 and callback:
callback("progress", f"Waiting {timeout}s for device to reboot...")
return RebootResult(
success=result.returncode == 0,
message="Reboot command sent" if result.returncode == 0 else f"SSH reboot failed: {result.stderr.strip()}",
method="ssh",
output=result.stdout,
)
except subprocess.TimeoutExpired:
return RebootResult(
success=False,
message="SSH reboot timed out",
method="ssh",
)
except FileNotFoundError:
return RebootResult(
success=False,
message="ssh command not found",
method="ssh",
)
except OSError as e:
return RebootResult(
success=False,
message=f"OS error: {e}",
method="ssh",
)
def reboot_via_serial(
config: Config,
callback: ProgressCallback = None,
) -> RebootResult:
"""Reboot the Zynq device via serial port.
Sends a reboot command through the serial console.
Args:
config: Application configuration.
callback: Optional progress callback.
Returns:
RebootResult with status.
"""
if not config.serial_port:
return RebootResult(
success=False,
message="No serial port configured",
method="serial",
)
if callback:
callback("start", f"Sending reboot via serial {config.serial_port}...")
try:
import serial
with serial.Serial(
config.serial_port,
config.serial_baudrate,
timeout=5,
) as ser:
ser.reset_input_buffer()
# Send Ctrl+C to break out of any running process
ser.write(b"\x03")
time.sleep(0.5)
# Send reboot command
ser.write(b"reboot\r\n")
ser.reset_input_buffer()
if callback:
callback("progress", "Reboot command sent via serial")
return RebootResult(
success=True,
message="Reboot command sent via serial",
method="serial",
)
except ImportError:
return RebootResult(
success=False,
message="pyserial not installed",
method="serial",
)
except serial.SerialException as e:
return RebootResult(
success=False,
message=f"Serial error: {e}",
method="serial",
)
except OSError as e:
return RebootResult(
success=False,
message=f"OS error: {e}",
method="serial",
)
def reboot_zynq(
config: Config,
method: str = "auto",
timeout: int | None = None,
callback: ProgressCallback = None,
) -> RebootResult:
"""Reboot the Zynq device using the best available method.
Tries SSH first, falls back to serial if SSH is unavailable.
Args:
config: Application configuration.
method: Reboot method: 'auto', 'ssh', or 'serial'.
timeout: Seconds to wait for reboot.
callback: Optional progress callback.
Returns:
RebootResult with status.
"""
timeout = timeout or config.reboot_timeout
if method == "ssh":
return reboot_via_ssh(config, timeout, callback)
elif method == "serial":
return reboot_via_serial(config, callback)
# Auto: try SSH first, then serial
result = reboot_via_ssh(config, timeout, callback)
if result.success:
return result
if callback:
callback("progress", "SSH failed, trying serial...")
return reboot_via_serial(config, callback)