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
+144
View File
@@ -0,0 +1,144 @@
"""Serial port monitoring and log parsing for Zynq Flasher GUI.
Detects available serial ports, reads output streams, and parses
boot logs for IP addresses, version strings, and other diagnostic info.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Callable
import serial
import serial.tools.list_ports
@dataclass
class BootInfo:
"""Parsed information extracted from Zynq boot output."""
ip_address: str = ""
version: str = ""
boot_message: str = ""
raw_lines: list[str] | None = None
@dataclass
class SerialInfo:
"""Information about an available serial port."""
device: str
description: str = ""
hwid: str = ""
def detect_serial_ports() -> list[SerialInfo]:
"""Detect all available serial/USB ports on the system.
Returns:
List of SerialInfo objects for each available port.
"""
ports = serial.tools.list_ports.comports()
results: list[SerialInfo] = []
for port in ports:
results.append(
SerialInfo(
device=port.device,
description=port.description or "",
hwid=port.hwid or "",
)
)
return results
def parse_boot_output(lines: list[str]) -> BootInfo:
"""Parse boot output lines for IP, version, and other info.
Looks for common patterns in Zynq boot logs:
- IP addresses (IPv4)
- Version strings (e.g., "v1.2.3", "version: x.y.z")
- Boot completion messages
Args:
lines: List of log output lines to parse.
Returns:
BootInfo with extracted data.
"""
info = BootInfo(raw_lines=lines)
ip_pattern = re.compile(r"\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b")
version_patterns = [
re.compile(r"[Vv]ersion[:\s]+([^\s;,\n]+)", re.IGNORECASE),
re.compile(r"[Bb]oot\s+([^\s;,\n]+)", re.IGNORECASE),
re.compile(r"[Ff]irmware\s+([^\s;,\n]+)", re.IGNORECASE),
re.compile(r"(?:^|[\s:=])(v\d+\.\d+\.\d+)(?:\s|$)", re.IGNORECASE),
]
boot_complete_patterns = [
re.compile(r"boot\s+complete", re.IGNORECASE),
re.compile(r"system\s+ready", re.IGNORECASE),
re.compile(r"starting\s+application", re.IGNORECASE),
re.compile(r"Linux\s+booted", re.IGNORECASE),
re.compile(r"QSYS\s+ready", re.IGNORECASE),
]
for line in lines:
# Extract IP address
if not info.ip_address:
match = ip_pattern.search(line)
if match:
info.ip_address = match.group(1)
# Extract version
if not info.version:
for pattern in version_patterns:
match = pattern.search(line)
if match:
info.version = match.group(1)
break
# Check for boot completion
if not info.boot_message:
for pattern in boot_complete_patterns:
if pattern.search(line):
info.boot_message = line.strip()
break
return info
def read_serial_stream(
port: str,
baudrate: int = 115200,
timeout: float = 1.0,
line_callback: Callable[[str], None] | None = None,
) -> list[str]:
"""Read lines from a serial port.
Opens the serial port, reads available lines, and closes it.
Optionally calls line_callback for each line as it arrives.
Args:
port: Serial port device path (e.g., '/dev/ttyUSB0').
baudrate: Baud rate for serial communication.
timeout: Read timeout in seconds.
line_callback: Optional callback invoked for each line.
Returns:
List of lines read from the serial port.
Raises:
serial.SerialException: If the port cannot be opened.
"""
lines: list[str] = []
with serial.Serial(port, baudrate, timeout=timeout) as ser:
# Clear any pending data
ser.reset_input_buffer()
while ser.in_waiting:
line = ser.readline().decode("utf-8", errors="replace").strip()
if line:
lines.append(line)
if line_callback:
line_callback(line)
return lines