1. SubStepFrame defaults to expanded (▼), not collapsed 2. erase_all checkbox has command=_on_erase_toggle → immediate config update 3. _deferred_init runs _check_vitis/_refresh_ports in background thread Fixes UI freeze during startup (subprocess.run for tool versions) 4. Sub-step colors now match StepHeader: - pending: gray ACCENT_PENDING (#888888) - running: blue CIRCLE_RUNNING (#0078D4) with pulse animation - complete: green ACCENT_COMPLETE (#107C10) - error: red ACCENT_ERROR (#D13438) - Running sub-step pulses between light/dark blue like StepHeader
1274 lines
48 KiB
Python
1274 lines
48 KiB
Python
"""Main GUI window for Zynq XC7Z100 Flasher.
|
|
|
|
Integrates all backend modules into a modern, step-by-step workflow GUI
|
|
built with CustomTkinter.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import threading
|
|
import time
|
|
import tkinter as tk
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
import customtkinter as ctk
|
|
|
|
from config_manager import Config
|
|
from vitis_checker import check_vitis, get_tool_status_dict
|
|
from zynq_checker import check_zynq_jtag, get_zynq_status_dict
|
|
from flash_programmer import full_flash_program, FlashResult
|
|
from bitstream_programmer import full_bitstream_program, BitstreamResult
|
|
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, UartMonitor
|
|
from gui.styles import (
|
|
WINDOW_WIDTH,
|
|
WINDOW_HEIGHT,
|
|
WINDOW_TITLE,
|
|
FONT_TITLE,
|
|
FONT_HEADING,
|
|
FONT_BODY,
|
|
FONT_SMALL,
|
|
FONT_MONO,
|
|
PADDING,
|
|
PADDING_LARGE,
|
|
PADDING_SMALL,
|
|
CORNER_RADIUS,
|
|
PRIMARY_COLOR,
|
|
SUCCESS_COLOR,
|
|
WARNING_COLOR,
|
|
DANGER_COLOR,
|
|
INFO_COLOR,
|
|
CONNECTOR_COLOR,
|
|
get_theme,
|
|
)
|
|
from gui.widgets import (
|
|
StepHeader,
|
|
ProgressIndicator,
|
|
StatusDisplay,
|
|
FileSelector,
|
|
LogDisplay,
|
|
SubStepFrame,
|
|
)
|
|
|
|
|
|
class MainWindow(ctk.CTk):
|
|
"""Main application window for Zynq Flasher GUI."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the main window."""
|
|
super().__init__()
|
|
|
|
self.title(WINDOW_TITLE)
|
|
self.geometry(f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}")
|
|
self.minsize(800, 600)
|
|
|
|
ctk.set_appearance_mode("dark")
|
|
ctk.set_default_color_theme("blue")
|
|
|
|
self._config: Config | None = None
|
|
self._config_path = self._find_config()
|
|
self._steps: list[StepHeader] = []
|
|
self._step_statuses: dict[int, str] = {}
|
|
self._is_running = False
|
|
self._worker_thread: threading.Thread | None = None
|
|
self._uart_available: bool = False
|
|
self._uart_message: str = ""
|
|
self._zynq_jtag_present: bool = False
|
|
self._zynq_jtag_info = None
|
|
self._uart_monitor: UartMonitor | None = None
|
|
self._flash_phase: str = ""
|
|
|
|
# StringVar references for config sync
|
|
self._ip_string_var: ctk.StringVar | None = None
|
|
|
|
self._load_config()
|
|
self._build_ui()
|
|
self._update_step_dependencies()
|
|
|
|
# Handle window close
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
# Defer blocking operations until after the window is displayed
|
|
self.after(100, self._deferred_init)
|
|
|
|
def _deferred_init(self) -> None:
|
|
"""Run blocking operations in background thread after window shown."""
|
|
def _bg_init():
|
|
self._check_vitis()
|
|
self._refresh_ports_initial()
|
|
threading.Thread(target=_bg_init, daemon=True).start()
|
|
|
|
# ── Config ─────────────────────────────────────────────────
|
|
|
|
def _find_config(self) -> Path | None:
|
|
"""Find the user configuration file.
|
|
|
|
Checks common locations:
|
|
1. config.yaml (project root, user's config)
|
|
2. config/default_config.yaml (template, not for saving)
|
|
3. User-specified path via env var
|
|
|
|
Returns:
|
|
Path to user config file, or None if not found.
|
|
"""
|
|
# Navigate from src/gui/main_window.py -> project root
|
|
app_dir = Path(__file__).parent.parent.parent
|
|
|
|
# 1. Check for user config at project root
|
|
user_config = app_dir / "config.yaml"
|
|
if user_config.exists():
|
|
return user_config
|
|
|
|
# 2. Check for default config (template)
|
|
config_dir = app_dir / "config"
|
|
default_config = config_dir / "default_config.yaml"
|
|
if default_config.exists():
|
|
return default_config
|
|
|
|
# 3. Check env var
|
|
env_path = os.environ.get("ZYNQ_CONFIG")
|
|
if env_path:
|
|
return Path(env_path)
|
|
|
|
return None
|
|
|
|
def _get_user_config_path(self) -> Path:
|
|
"""Get the path to the user config file (config.yaml).
|
|
|
|
Creates config.yaml in project root if it doesn't exist.
|
|
|
|
Returns:
|
|
Path to user config file.
|
|
"""
|
|
# Navigate from src/gui/main_window.py -> project root
|
|
app_dir = Path(__file__).parent.parent.parent
|
|
config_path = app_dir / "config.yaml"
|
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
return config_path
|
|
|
|
def _load_config(self) -> None:
|
|
"""Load configuration from file or create default."""
|
|
if self._config_path:
|
|
try:
|
|
self._config = Config.from_file(self._config_path)
|
|
# If loaded from default_config.yaml, set save path to config.yaml
|
|
if self._config_path.name == "default_config.yaml":
|
|
user_config_path = self._get_user_config_path()
|
|
self._config._config_path = user_config_path
|
|
except Exception:
|
|
self._config = Config.from_default()
|
|
# Set save path to config.yaml for new configs
|
|
user_config_path = self._get_user_config_path()
|
|
self._config._config_path = user_config_path
|
|
else:
|
|
self._config = Config.from_default()
|
|
# Set save path to config.yaml for new configs
|
|
user_config_path = self._get_user_config_path()
|
|
self._config._config_path = user_config_path
|
|
|
|
def _sync_config_from_ui(self) -> None:
|
|
"""Read current UI values and update the Config object.
|
|
|
|
This must be called before _save_config() to ensure
|
|
the Config object has the latest UI values.
|
|
"""
|
|
if not self._config:
|
|
return
|
|
# Sync IP address
|
|
if self._ip_string_var:
|
|
self._config.zynq_ip = self._ip_string_var.get()
|
|
# Sync serial port
|
|
self._config.serial_port = self._port_var.get()
|
|
# Sync file paths from FileSelectors
|
|
files = self._get_selected_files()
|
|
self._config.bootloader_bit_path = files["bootloader_bit_path"]
|
|
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
|
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
|
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
|
self._config.erase_all = self._erase_cb_var.get()
|
|
self._config.firmware_bin_path = files["firmware_bin_path"]
|
|
|
|
def _save_config(self) -> None:
|
|
"""Save current configuration to user config file (config.yaml).
|
|
|
|
Always saves to config/config.yaml, not to default_config.yaml.
|
|
"""
|
|
self._sync_config_from_ui() # Sync UI values first
|
|
if not self._config:
|
|
self._log_message(" No config loaded")
|
|
return
|
|
|
|
# Save to user config file (config.yaml)
|
|
user_config_path = self._get_user_config_path()
|
|
self._config._config_path = user_config_path # Set save path
|
|
try:
|
|
self._config.save()
|
|
self._log_message(f" Config saved to {user_config_path}")
|
|
self._status.set_status("Config saved", "success")
|
|
except Exception as e:
|
|
self._log_message(f" ✗ Save failed: {e}")
|
|
self._status.set_status(f"Save failed: {e}", "error")
|
|
|
|
def _on_erase_toggle(self) -> None:
|
|
"""Update config.erase_all immediately when checkbox toggles."""
|
|
if self._config:
|
|
self._config.erase_all = self._erase_cb_var.get()
|
|
|
|
# ── UI Construction ────────────────────────────────────────
|
|
|
|
def _build_ui(self) -> None:
|
|
"""Build the complete UI layout."""
|
|
# Main container with padding
|
|
main_frame = ctk.CTkFrame(self)
|
|
main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
|
|
main_frame.grid_rowconfigure(1, weight=1)
|
|
main_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# ── Header ──
|
|
self._build_header(main_frame)
|
|
|
|
# ── Left Panel: Workflow Steps ──
|
|
left_frame = ctk.CTkFrame(main_frame)
|
|
left_frame.grid(row=1, column=0, sticky="nsew", padx=(0, PADDING))
|
|
left_frame.grid_rowconfigure(3, weight=1)
|
|
left_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
self._build_workflow_panel(left_frame)
|
|
self._build_log_panel(left_frame)
|
|
|
|
# ── Right Panel: Configuration ──
|
|
right_frame = ctk.CTkFrame(main_frame)
|
|
right_frame.grid(row=1, column=1, sticky="nsew", padx=(PADDING, 0))
|
|
right_frame.grid_rowconfigure(4, weight=1)
|
|
|
|
self._build_config_panel(right_frame)
|
|
self._build_serial_panel(right_frame)
|
|
self._build_status_panel(right_frame)
|
|
|
|
def _build_header(self, parent: ctk.CTkFrame) -> None:
|
|
"""Build the application header."""
|
|
header = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
|
header.grid(row=0, column=0, columnspan=2, sticky="nsew", pady=(0, PADDING))
|
|
header.grid_columnconfigure(1, weight=1)
|
|
|
|
title = ctk.CTkLabel(
|
|
header,
|
|
text="Zynq XC7Z100 Flasher",
|
|
font=FONT_TITLE,
|
|
)
|
|
title.grid(row=0, column=0, padx=PADDING, pady=PADDING)
|
|
|
|
self._vitis_status = ctk.CTkLabel(
|
|
header,
|
|
text="Vitis: Checking...",
|
|
font=FONT_BODY,
|
|
)
|
|
self._vitis_status.grid(row=0, column=1, sticky="e", padx=PADDING)
|
|
|
|
self._ip_status = ctk.CTkLabel(
|
|
header,
|
|
text=f"IP: {self._config.zynq_ip}",
|
|
font=FONT_BODY,
|
|
)
|
|
self._ip_status.grid(row=0, column=2, sticky="e", padx=PADDING)
|
|
|
|
self._jtag_status = ctk.CTkLabel(
|
|
header,
|
|
text="JTAG: Checking...",
|
|
font=FONT_BODY,
|
|
)
|
|
self._jtag_status.grid(row=0, column=3, sticky="e", padx=PADDING)
|
|
|
|
def _build_workflow_panel(self, parent: ctk.CTkFrame) -> None:
|
|
"""Build the step-by-step workflow panel."""
|
|
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
|
panel.grid(
|
|
row=0, column=0, sticky="nsew",
|
|
padx=PADDING, pady=(PADDING, 0),
|
|
)
|
|
panel.grid_rowconfigure(10, weight=1)
|
|
panel.grid_columnconfigure(0, weight=1)
|
|
|
|
# ── Title ──
|
|
title = ctk.CTkLabel(
|
|
panel,
|
|
text="Workflow",
|
|
font=FONT_HEADING,
|
|
)
|
|
title.grid(row=0, column=0, padx=PADDING_SMALL, pady=(PADDING_SMALL, PADDING_SMALL))
|
|
|
|
# ── Steps ──
|
|
step_defs = [
|
|
{"num": 1, "title": "Check Environment", "desc": "Verify tools & connectivity"},
|
|
{"num": 2, "title": "Program & Verify Flash", "desc": "Wipe, program, and verify flash"},
|
|
{"num": 3, "title": "Load Bootloader", "desc": "Program BIT + run ELF"},
|
|
{"num": 4, "title": "Load Main Program", "desc": "TFTP upload, reboot, verify boot"},
|
|
]
|
|
|
|
self._steps = []
|
|
self._step_statuses = {}
|
|
for i, defn in enumerate(step_defs):
|
|
step = StepHeader(
|
|
panel,
|
|
step_number=defn["num"],
|
|
title=defn["title"],
|
|
description=defn["desc"],
|
|
callback=lambda idx=i: self._execute_step(idx),
|
|
can_run=(i == 0),
|
|
)
|
|
self._steps.append(step)
|
|
self._step_statuses[i] = "pending"
|
|
|
|
# Divider line between steps
|
|
if i > 0:
|
|
divider = ctk.CTkFrame(
|
|
panel,
|
|
height=1,
|
|
fg_color=CONNECTOR_COLOR,
|
|
)
|
|
divider.grid(
|
|
row=i * 2, column=0,
|
|
sticky="ew",
|
|
padx=PADDING,
|
|
pady=(2, 0),
|
|
)
|
|
|
|
# Position step
|
|
step.grid(
|
|
row=i * 2 + 1, column=0,
|
|
sticky="nsew",
|
|
padx=PADDING_SMALL,
|
|
pady=(0, 2),
|
|
)
|
|
|
|
# Step 2: add collapsible sub-step frame
|
|
if i == 1:
|
|
self._sub_step_frame = SubStepFrame(panel)
|
|
self._sub_step_frame.grid(
|
|
row=i * 2 + 2, column=0,
|
|
sticky="nsew",
|
|
padx=PADDING_SMALL + 16,
|
|
pady=(0, 2),
|
|
)
|
|
|
|
# ── Progress indicator ──
|
|
self._progress = ProgressIndicator(panel)
|
|
self._progress.grid(
|
|
row=8, column=0,
|
|
sticky="nsew",
|
|
padx=PADDING,
|
|
pady=(PADDING, PADDING_SMALL),
|
|
)
|
|
|
|
# ── Run All button ──
|
|
self._run_btn = ctk.CTkButton(
|
|
panel,
|
|
text="▶ Run All Steps",
|
|
font=FONT_HEADING,
|
|
command=self._run_all_steps,
|
|
height=40,
|
|
corner_radius=CORNER_RADIUS,
|
|
)
|
|
self._run_btn.grid(
|
|
row=9, column=0,
|
|
sticky="nsew",
|
|
padx=PADDING,
|
|
pady=PADDING_SMALL,
|
|
)
|
|
|
|
def _build_log_panel(self, parent: ctk.CTkFrame) -> None:
|
|
"""Build the log display panel."""
|
|
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
|
panel.grid(
|
|
row=1, column=0, sticky="nsew",
|
|
padx=PADDING, pady=PADDING,
|
|
)
|
|
panel.grid_rowconfigure(1, weight=1)
|
|
panel.grid_columnconfigure(0, weight=1)
|
|
|
|
title = ctk.CTkLabel(
|
|
panel,
|
|
text="Log",
|
|
font=FONT_HEADING,
|
|
)
|
|
title.grid(row=0, column=0, padx=PADDING, pady=PADDING)
|
|
|
|
self._log_display = LogDisplay(panel, height=250)
|
|
self._log_display.grid(row=1, column=0, sticky="nsew", padx=PADDING, pady=PADDING)
|
|
|
|
# Clear button
|
|
clear_btn = ctk.CTkButton(
|
|
panel,
|
|
text="Clear Log",
|
|
font=FONT_SMALL,
|
|
command=self._log_display.clear,
|
|
width=100,
|
|
)
|
|
clear_btn.grid(row=0, column=1, padx=PADDING, pady=PADDING)
|
|
|
|
def _build_config_panel(self, parent: ctk.CTkFrame) -> None:
|
|
"""Build the configuration panel."""
|
|
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
|
panel.grid(
|
|
row=0, column=0, sticky="nsew",
|
|
padx=PADDING, pady=PADDING,
|
|
)
|
|
panel.grid_rowconfigure(7, weight=1)
|
|
|
|
title = ctk.CTkLabel(
|
|
panel,
|
|
text="Configuration",
|
|
font=FONT_HEADING,
|
|
)
|
|
title.grid(row=0, column=0, padx=PADDING, pady=PADDING)
|
|
|
|
# IP address
|
|
ip_frame = ctk.CTkFrame(panel)
|
|
ip_frame.grid(row=1, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
ip_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
ctk.CTkLabel(ip_frame, text="Zynq IP:", font=FONT_BODY).grid(row=0, column=0, sticky="w")
|
|
self._ip_string_var = ctk.StringVar(value=self._config.zynq_ip)
|
|
self._ip_entry = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
|
|
self._ip_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
|
|
|
# BIT path (Bootloader)
|
|
self._bit_selector = FileSelector(
|
|
panel,
|
|
label_text="Bootloader BIT (.bit):",
|
|
file_types=[("BIT files", "*.bit"), ("All files", "*")],
|
|
)
|
|
if self._config.bootloader_bit_path:
|
|
resolved = self._config.resolve_path(self._config.bootloader_bit_path)
|
|
self._bit_selector.set_path(str(resolved))
|
|
self._bit_selector.grid(row=2, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
|
|
# ELF path (Bootloader)
|
|
self._elf_selector = FileSelector(
|
|
panel,
|
|
label_text="Bootloader ELF (.elf):",
|
|
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
|
)
|
|
if self._config.bootloader_elf_path:
|
|
resolved = self._config.resolve_path(self._config.bootloader_elf_path)
|
|
self._elf_selector.set_path(str(resolved))
|
|
self._elf_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
|
|
# Bootloader BIN path (Flash programming)
|
|
self._bootloader_bin_selector = FileSelector(
|
|
panel,
|
|
label_text="Bootloader BIN (.bin):",
|
|
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
|
)
|
|
if self._config.bootloader_bin_path:
|
|
resolved = self._config.resolve_path(self._config.bootloader_bin_path)
|
|
self._bootloader_bin_selector.set_path(str(resolved))
|
|
self._bootloader_bin_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
|
|
# FSBL ELF path (required by program_flash for QSPI programming)
|
|
self._fsbl_selector = FileSelector(
|
|
panel,
|
|
label_text="FSBL ELF (.elf):",
|
|
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
|
)
|
|
if self._config.fsbl_elf_path:
|
|
resolved = self._config.resolve_path(self._config.fsbl_elf_path)
|
|
self._fsbl_selector.set_path(str(resolved))
|
|
self._fsbl_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
|
|
# Flash options
|
|
flash_frame = ctk.CTkFrame(panel)
|
|
flash_frame.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
flash_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
self._erase_cb_var = ctk.BooleanVar(value=self._config.erase_all)
|
|
self._erase_cb = ctk.CTkCheckBox(
|
|
flash_frame,
|
|
text="整片Flash擦除 (Erase entire flash)",
|
|
variable=self._erase_cb_var,
|
|
font=FONT_SMALL,
|
|
command=self._on_erase_toggle,
|
|
)
|
|
self._erase_cb.grid(row=0, column=0, sticky="w", padx=PADDING_SMALL)
|
|
|
|
flash_model_label = ctk.CTkLabel(
|
|
flash_frame,
|
|
text=f"Flash: {self._config.flash_model} (32 MiB)",
|
|
font=FONT_SMALL,
|
|
)
|
|
flash_model_label.grid(row=1, column=0, sticky="w", padx=PADDING_SMALL, pady=(2, 0))
|
|
|
|
# Firmware BIN path (TFTP upload)
|
|
self._firmware_bin_selector = FileSelector(
|
|
panel,
|
|
label_text="Firmware BIN (.bin):",
|
|
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
|
)
|
|
if self._config.firmware_bin_path:
|
|
resolved = self._config.resolve_path(self._config.firmware_bin_path)
|
|
self._firmware_bin_selector.set_path(str(resolved))
|
|
self._firmware_bin_selector.grid(row=7, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
|
|
# Save button
|
|
save_btn = ctk.CTkButton(
|
|
panel,
|
|
text="Save Config",
|
|
font=FONT_SMALL,
|
|
command=self._save_config,
|
|
)
|
|
save_btn.grid(row=8, column=0, padx=PADDING, pady=PADDING)
|
|
|
|
def _build_serial_panel(self, parent: ctk.CTkFrame) -> None:
|
|
"""Build the serial monitor panel."""
|
|
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
|
panel.grid(
|
|
row=1, column=0, sticky="nsew",
|
|
padx=PADDING, pady=PADDING,
|
|
)
|
|
panel.grid_rowconfigure(3, weight=1)
|
|
panel.grid_columnconfigure(0, weight=1)
|
|
|
|
title = ctk.CTkLabel(
|
|
panel,
|
|
text="Serial Monitor",
|
|
font=FONT_HEADING,
|
|
)
|
|
title.grid(row=0, column=0, padx=PADDING, pady=PADDING)
|
|
|
|
# Port selector
|
|
port_frame = ctk.CTkFrame(panel)
|
|
port_frame.grid(row=1, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
|
port_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
ctk.CTkLabel(port_frame, text="Port:", font=FONT_BODY).grid(row=0, column=0, sticky="w")
|
|
self._port_var = ctk.StringVar(value=self._config.serial_port)
|
|
self._port_menu = ctk.CTkComboBox(
|
|
port_frame,
|
|
values=[""] + [p.device for p in detect_serial_ports()],
|
|
variable=self._port_var,
|
|
)
|
|
self._port_menu.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
|
|
|
# Refresh button
|
|
refresh_btn = ctk.CTkButton(
|
|
port_frame,
|
|
text="Refresh",
|
|
font=FONT_SMALL,
|
|
command=self._refresh_ports,
|
|
width=80,
|
|
)
|
|
refresh_btn.grid(row=0, column=2, padx=PADDING_SMALL)
|
|
|
|
# Read button
|
|
read_btn = ctk.CTkButton(
|
|
panel,
|
|
text="Read Serial",
|
|
font=FONT_BODY,
|
|
command=self._read_serial,
|
|
)
|
|
read_btn.grid(row=2, column=0, padx=PADDING, pady=PADDING_SMALL)
|
|
|
|
# Test version button
|
|
self._test_version_btn = ctk.CTkButton(
|
|
panel,
|
|
text="Test Version",
|
|
font=FONT_BODY,
|
|
command=self._test_serial_version,
|
|
)
|
|
self._test_version_btn.grid(row=3, column=0, padx=PADDING, pady=PADDING_SMALL)
|
|
|
|
def _build_status_panel(self, parent: ctk.CTkFrame) -> None:
|
|
"""Build the status display panel."""
|
|
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
|
panel.grid(
|
|
row=2, column=0, sticky="nsew",
|
|
padx=PADDING, pady=PADDING,
|
|
)
|
|
|
|
self._status = StatusDisplay(panel)
|
|
self._status.pack(fill="x", padx=PADDING, pady=PADDING)
|
|
|
|
# UART warning label (persistent, not overwritten by other status updates)
|
|
self._uart_warning_label = ctk.CTkLabel(
|
|
panel,
|
|
text="",
|
|
font=FONT_BODY,
|
|
anchor="w",
|
|
text_color=WARNING_COLOR,
|
|
)
|
|
self._uart_warning_label.pack(
|
|
fill="x", padx=PADDING, pady=(0, PADDING_SMALL)
|
|
)
|
|
self._uart_warning_label.pack_forget() # Hide by default
|
|
|
|
# ── Workflow Steps ─────────────────────────────────────────
|
|
|
|
def _log_message(self, message: str) -> None:
|
|
"""Append a message to the log display (thread-safe)."""
|
|
self.after(0, lambda: self._log_display.append(message))
|
|
|
|
def _set_step_status(self, index: int, status: str) -> None:
|
|
"""Set the status of a workflow step.
|
|
|
|
Args:
|
|
index: Step index (0-based).
|
|
status: Status string ('pending', 'running', 'complete', 'error').
|
|
"""
|
|
if 0 <= index < len(self._steps):
|
|
self._steps[index].set_status(status)
|
|
|
|
def _get_selected_files(self) -> dict:
|
|
"""Get selected file paths from the UI.
|
|
|
|
Returns:
|
|
Dictionary with all file paths from UI selectors.
|
|
"""
|
|
return {
|
|
"bootloader_bit_path": self._bit_selector.get_path(),
|
|
"bootloader_elf_path": self._elf_selector.get_path(),
|
|
"bootloader_bin_path": self._bootloader_bin_selector.get_path(),
|
|
"fsbl_elf_path": self._fsbl_selector.get_path(),
|
|
"firmware_bin_path": self._firmware_bin_selector.get_path(),
|
|
}
|
|
|
|
# ── Step Implementations ───────────────────────────────────
|
|
|
|
def _run_step_1_check_environment(self) -> bool:
|
|
"""Step 1: Check environment (Vitis/Vivado tools + Zynq IP + UART)."""
|
|
self._log_message("Step 1: Checking environment...")
|
|
|
|
if not self._config:
|
|
self._log_message(" No config loaded")
|
|
return False
|
|
|
|
# Check Vitis/Vivado tools
|
|
self._log_message(" Checking Vitis/Vivado tools...")
|
|
result = check_vitis(self._config)
|
|
status_dict = get_tool_status_dict(result)
|
|
|
|
for tool_name, tool_info in status_dict["tools"].items():
|
|
if tool_info["found"]:
|
|
alias_note = f" (via {tool_info['alias']})" if tool_info.get("alias") and tool_info["alias"] != tool_name else ""
|
|
self._log_message(f" ✓ {tool_name}: {tool_info['path']}{alias_note}")
|
|
else:
|
|
self._log_message(f" ✗ {tool_name}: {tool_info['error']}")
|
|
|
|
if result.is_ready:
|
|
self._vitis_status.configure(text="Vitis: Ready", text_color=SUCCESS_COLOR)
|
|
self._log_message(" Vitis/Vivado tools available")
|
|
else:
|
|
self._vitis_status.configure(text="Vitis: Partial", text_color=WARNING_COLOR)
|
|
self._log_message(f" Issues: {'; '.join(result.errors)}")
|
|
|
|
# Check Zynq JTAG presence
|
|
self._log_message(" Checking Zynq JTAG presence...")
|
|
jtag_result = check_zynq_jtag(self._config, self._jtag_callback)
|
|
jtag_dict = get_zynq_status_dict(jtag_result)
|
|
|
|
# Log JTAG chain information
|
|
if jtag_result.jtag_chain:
|
|
self._log_message(f" JTAG chain: {len(jtag_result.jtag_chain)} device(s)")
|
|
for jtag_dev in jtag_result.jtag_chain:
|
|
dev_type = "ARM DAP (PS)" if jtag_dev.is_arm_dap else "PL" if jtag_dev.is_pl_device else "Other"
|
|
self._log_message(f" - {jtag_dev.name} ({dev_type})")
|
|
|
|
if jtag_result.is_present:
|
|
self._zynq_jtag_present = True
|
|
self._zynq_jtag_info = jtag_result.devices[0]
|
|
part = jtag_result.devices[0].part_number
|
|
|
|
# Log PS and PL detection status
|
|
ps_status = "✓" if jtag_result.ps_detected else "✗"
|
|
pl_status = "✓" if jtag_result.pl_detected else "✗"
|
|
self._log_message(f" PS (ARM DAP): {ps_status}")
|
|
self._log_message(f" PL: {pl_status}")
|
|
|
|
self._log_message(f" ✓ Zynq {part} detected on JTAG chain")
|
|
self._jtag_status.configure(text=f"JTAG: {part} ✓", text_color=SUCCESS_COLOR)
|
|
else:
|
|
self._zynq_jtag_present = False
|
|
self._zynq_jtag_info = None
|
|
self._log_message(f" ✗ Zynq not detected on JTAG: {jtag_result.error}")
|
|
self._jtag_status.configure(text="JTAG: Not detected", text_color=WARNING_COLOR)
|
|
|
|
# Check UART availability
|
|
self._log_message(" Checking UART serial port...")
|
|
port = self._config.serial_port
|
|
if port:
|
|
available, reason = check_uart_available(port, self._config.serial_baudrate)
|
|
self._uart_available = available
|
|
self._uart_message = reason
|
|
if available:
|
|
self._log_message(f" ✓ UART available: {reason}")
|
|
else:
|
|
self._log_message(f" ✗ UART unavailable: {reason}")
|
|
self._show_uart_notification(reason)
|
|
else:
|
|
self._uart_available = False
|
|
self._uart_message = "No serial port configured"
|
|
self._log_message(" ✗ No serial port configured")
|
|
self._show_uart_notification(self._uart_message)
|
|
|
|
return True
|
|
|
|
def _show_uart_notification(self, reason: str) -> None:
|
|
"""Show a persistent UART unavailable notification.
|
|
|
|
This notification stays visible and is not overwritten by other status updates.
|
|
|
|
Args:
|
|
reason: Why UART is unavailable.
|
|
"""
|
|
msg = f"⚠ UART 不可用 ({reason}),日志分析及检查将禁用"
|
|
self.after(0, lambda: self._show_uart_warning(msg))
|
|
|
|
def _show_uart_warning(self, msg: str) -> None:
|
|
"""Display the persistent UART warning message.
|
|
|
|
Args:
|
|
msg: Warning message to display.
|
|
"""
|
|
self._uart_warning_label.configure(text=msg)
|
|
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
|
|
|
|
def _run_step_2_program_flash(self) -> bool:
|
|
"""Step 2: Program and verify Flash using program_flash."""
|
|
if not self._config:
|
|
return False
|
|
|
|
files = self._get_selected_files()
|
|
if not files["bootloader_bin_path"]:
|
|
self._log_message(" No Bootloader BIN file selected")
|
|
return False
|
|
|
|
if not files["fsbl_elf_path"]:
|
|
self._log_message(" No FSBL ELF file selected (required by program_flash)")
|
|
return False
|
|
|
|
self._log_message(f"Step 2: Programming Flash with {files['bootloader_bin_path']}...")
|
|
self._log_message(f" FSBL: {files['fsbl_elf_path']}")
|
|
|
|
try:
|
|
bin_path = Path(files["bootloader_bin_path"])
|
|
results = full_flash_program(self._config, bin_path, self._flash_callback)
|
|
|
|
for result in results:
|
|
status = "✓" if result.success else "✗"
|
|
self._log_message(f" {status} {result.step}: {result.message}")
|
|
|
|
return all(r.success for r in results)
|
|
except Exception as e:
|
|
self._log_message(f" ✗ Flash error: {e}")
|
|
return False
|
|
|
|
def _flash_callback(self, status: str, message: str) -> None:
|
|
"""Callback for flash programming — drives sub-step UI."""
|
|
if status == "phase":
|
|
self._flash_phase = message
|
|
phase_names = {"erase": "Erasing", "program": "Programming", "verify": "Verifying", "done": "Done"}
|
|
label = phase_names.get(message, message)
|
|
self._log_message(f" ▸ {label}")
|
|
if hasattr(self, '_sub_step_frame'):
|
|
self._sub_step_frame.set_phase(message)
|
|
return
|
|
if status == "progress":
|
|
try:
|
|
pct = int(message.rstrip('%'))
|
|
except ValueError:
|
|
pct = -1
|
|
phase_label = {"erase": "Erase", "program": "Program", "verify": "Verify"}.get(
|
|
self._flash_phase, "Flash")
|
|
self._log_message(f" [{phase_label}] {pct}%")
|
|
if hasattr(self, '_sub_step_frame'):
|
|
self._sub_step_frame.set_phase(self._flash_phase, pct)
|
|
return
|
|
self._log_message(f" [{status}] {message}")
|
|
|
|
def _jtag_callback(self, status: str, message: str) -> None:
|
|
"""Callback for JTAG scan progress."""
|
|
self._log_message(f" [{status}] {message}")
|
|
|
|
def _run_step_3_load_bootloader(self) -> bool:
|
|
"""Step 3: Load bootloader (BIT + ELF) and verify."""
|
|
if not self._config:
|
|
return False
|
|
|
|
files = self._get_selected_files()
|
|
if not files["bootloader_bit_path"]:
|
|
self._log_message(" No Bootloader BIT file selected")
|
|
return False
|
|
|
|
self._log_message(f"Step 3: Loading bootloader (BIT + ELF)...")
|
|
self._log_message(f" BIT: {files['bootloader_bit_path']}")
|
|
if files["bootloader_elf_path"]:
|
|
self._log_message(f" ELF: {files['bootloader_elf_path']}")
|
|
|
|
try:
|
|
bit_path = Path(files["bootloader_bit_path"])
|
|
elf_path = Path(files["bootloader_elf_path"]) if files["bootloader_elf_path"] else bit_path.parent / "temp.elf"
|
|
|
|
results = full_bitstream_program(
|
|
self._config, bit_path, elf_path,
|
|
self._bitstream_callback,
|
|
)
|
|
|
|
for result in results:
|
|
status = "✓" if result.success else "✗"
|
|
self._log_message(f" {status} {result.step}: {result.message}")
|
|
|
|
return all(r.success for r in results)
|
|
except Exception as e:
|
|
self._log_message(f" ✗ Bootloader error: {e}")
|
|
return False
|
|
|
|
def _bitstream_callback(self, status: str, message: str) -> None:
|
|
"""Callback for bitstream programming progress."""
|
|
self._log_message(f" [{status}] {message}")
|
|
|
|
def _run_step_4_load_main_program(self) -> bool:
|
|
"""Step 4: Load main program (TFTP upload + reboot + boot verify)."""
|
|
if not self._config:
|
|
return False
|
|
|
|
files = self._get_selected_files()
|
|
if not files["firmware_bin_path"]:
|
|
self._log_message(" No Firmware BIN file selected")
|
|
return False
|
|
|
|
all_results: list[bool] = []
|
|
|
|
# 4a: TFTP upload
|
|
self._log_message("Step 4a: TFTP upload & verify...")
|
|
try:
|
|
bin_path = Path(files["firmware_bin_path"])
|
|
tftp_result = tftp_upload_verify(
|
|
self._config, bin_path,
|
|
callback=self._tftp_callback,
|
|
)
|
|
status = "✓" if tftp_result.success else "✗"
|
|
self._log_message(f" {status} {tftp_result.message}")
|
|
all_results.append(tftp_result.success)
|
|
except Exception as e:
|
|
self._log_message(f" ✗ TFTP error: {e}")
|
|
all_results.append(False)
|
|
|
|
if not all_results[-1]:
|
|
self._log_message(" Skipping reboot & boot verify (TFTP failed)")
|
|
return False
|
|
|
|
# 4a2: Post-download Zynq status check
|
|
self._log_message("Step 4b: Checking Zynq status after download...")
|
|
try:
|
|
zynq_status = verify_zynq_status(
|
|
self._config,
|
|
uart_available=self._uart_available,
|
|
callback=self._zynq_status_callback,
|
|
)
|
|
status = "✓" if zynq_status.success else "✗"
|
|
self._log_message(f" {status} {zynq_status.message}")
|
|
except Exception as e:
|
|
self._log_message(f" ✗ Zynq status check error: {e}")
|
|
|
|
# 4c: Reboot
|
|
self._log_message("Step 4c: Rebooting Zynq...")
|
|
try:
|
|
reboot_result = reboot_zynq(
|
|
self._config,
|
|
method="auto",
|
|
callback=self._reboot_callback,
|
|
)
|
|
status = "✓" if reboot_result.success else "✗"
|
|
self._log_message(f" {status} {reboot_result.message}")
|
|
all_results.append(reboot_result.success)
|
|
except Exception as e:
|
|
self._log_message(f" ✗ Reboot error: {e}")
|
|
all_results.append(False)
|
|
|
|
if not all_results[-1]:
|
|
self._log_message(" Skipping boot verify (reboot failed)")
|
|
return False
|
|
|
|
# 4d: Verify boot
|
|
self._log_message("Step 4d: Verifying boot...")
|
|
try:
|
|
boot_result = verify_boot(
|
|
self._config,
|
|
callback=self._boot_callback,
|
|
)
|
|
status = "✓" if boot_result.success else "✗"
|
|
self._log_message(f" {status} {boot_result.message}")
|
|
|
|
if boot_result.ip_reachable:
|
|
self._log_message(f" IP {self._config.zynq_ip} is reachable")
|
|
if boot_result.serial_found:
|
|
self._log_message(f" Boot info: {boot_result.boot_info}")
|
|
|
|
all_results.append(boot_result.success)
|
|
except Exception as e:
|
|
self._log_message(f" ✗ Boot verify error: {e}")
|
|
all_results.append(False)
|
|
|
|
return all(all_results)
|
|
|
|
def _tftp_callback(self, status: str, message: str) -> None:
|
|
"""Callback for TFTP progress."""
|
|
self._log_message(f" [{status}] {message}")
|
|
|
|
def _reboot_callback(self, status: str, message: str) -> None:
|
|
"""Callback for reboot progress."""
|
|
self._log_message(f" [{status}] {message}")
|
|
|
|
def _boot_callback(self, status: str, message: str) -> None:
|
|
"""Callback for boot verification progress."""
|
|
self._log_message(f" [{status}] {message}")
|
|
|
|
def _zynq_status_callback(self, status: str, message: str) -> None:
|
|
"""Callback for post-download Zynq status check."""
|
|
self._log_message(f" [{status}] {message}")
|
|
|
|
# ── Run All Steps ──────────────────────────────────────────
|
|
|
|
def _update_step_dependencies(self) -> None:
|
|
"""Update step run-ability based on dependency status.
|
|
|
|
All steps are runnable independently.
|
|
"""
|
|
for i, step in enumerate(self._steps):
|
|
step.set_can_run(True)
|
|
status = self._step_statuses.get(i, "pending")
|
|
if status == "pending":
|
|
step.set_status("pending")
|
|
elif status == "complete":
|
|
step.set_status("complete")
|
|
elif status == "error":
|
|
step.set_status("error")
|
|
|
|
def _execute_step(self, index: int) -> None:
|
|
"""Execute a single step independently.
|
|
|
|
Args:
|
|
index: Step index (0-based).
|
|
"""
|
|
if self._is_running:
|
|
return
|
|
|
|
if index >= len(self._steps):
|
|
return
|
|
|
|
self._is_running = True
|
|
self._run_btn.configure(state="disabled", text="Running...")
|
|
self._progress.reset()
|
|
|
|
# Reset status for this step only
|
|
self._step_statuses[index] = "pending"
|
|
self._steps[index].set_status("pending")
|
|
|
|
self._progress.set_value(0.0)
|
|
|
|
# Start background worker thread with single_step=True
|
|
self._worker_thread = threading.Thread(
|
|
target=self._run_steps_worker,
|
|
daemon=True,
|
|
args=(index, True), # single_step=True
|
|
)
|
|
self._worker_thread.start()
|
|
|
|
def _set_step_status(self, index: int, status: str) -> None:
|
|
"""Update the status of a step in the UI.
|
|
|
|
Args:
|
|
index: Step index (0-based).
|
|
status: New status string.
|
|
"""
|
|
self._step_statuses[index] = status
|
|
self._steps[index].set_status(status)
|
|
|
|
def _run_all_steps(self) -> None:
|
|
"""Execute all workflow steps sequentially in a background thread."""
|
|
if self._is_running:
|
|
return
|
|
|
|
self._is_running = True
|
|
self._run_btn.configure(state="disabled", text="Running...")
|
|
self._progress.reset()
|
|
|
|
# Reset all step statuses
|
|
for i in range(len(self._steps)):
|
|
self._step_statuses[i] = "pending"
|
|
self._steps[i].set_status("pending")
|
|
|
|
# Start background worker thread
|
|
self._worker_thread = threading.Thread(
|
|
target=self._run_steps_worker,
|
|
daemon=True,
|
|
args=(0, False), # single_step=False
|
|
)
|
|
self._worker_thread.start()
|
|
|
|
def _run_steps_worker(self, start_index: int = 0, single_step: bool = False) -> None:
|
|
"""Worker method that runs workflow steps in the background.
|
|
|
|
Args:
|
|
start_index: Index of the first step to run (0-based).
|
|
single_step: If True, only run the step at start_index.
|
|
"""
|
|
step_funcs = [
|
|
self._run_step_1_check_environment,
|
|
self._run_step_2_program_flash,
|
|
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)
|
|
total = end_index - start_index
|
|
results: list[bool] = []
|
|
|
|
for i in range(start_index, end_index):
|
|
step_idx = i - start_index
|
|
self._set_step_status(i, "running")
|
|
|
|
# Step 2: expand sub-step frame and reset
|
|
if i == 1 and hasattr(self, '_sub_step_frame'):
|
|
self._sub_step_frame.reset()
|
|
self._sub_step_frame.set_expanded(True)
|
|
self._flash_phase = ""
|
|
|
|
# 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")
|
|
|
|
# Step 2: mark sub-steps based on result
|
|
if i == 1 and hasattr(self, '_sub_step_frame'):
|
|
if success:
|
|
self._sub_step_frame.set_phase("done")
|
|
else:
|
|
self._sub_step_frame.set_phase("error")
|
|
except Exception as e:
|
|
results.append(False)
|
|
self._set_step_status(i, "error")
|
|
self._log_message(f" Exception in step {i+1}: {e}")
|
|
|
|
self._progress.set_value((step_idx + 1) / total)
|
|
|
|
# Stop on first failure when running all steps
|
|
if not single_step and not success:
|
|
break
|
|
|
|
# Inter-step delay (skip after last step)
|
|
if i < len(step_funcs) - 1:
|
|
delay = self._config.inter_step_delay if self._config else 2
|
|
self._log_message(f" ⏳ Waiting {delay}s before next step...")
|
|
time.sleep(delay)
|
|
|
|
self._progress.set_value(1.0)
|
|
self._is_running = False
|
|
self._run_btn.configure(state="normal", text="▶ Run All Steps")
|
|
|
|
# Update dependency statuses
|
|
self._update_step_dependencies()
|
|
|
|
# Set final status message
|
|
if results:
|
|
all_ok = all(results)
|
|
any_fail = any(not r for r in results)
|
|
if all_ok:
|
|
self._status.set_status("Workflow complete — all steps passed", "success")
|
|
elif any_fail:
|
|
failed_count = results.count(False)
|
|
self._status.set_status(
|
|
f"Workflow complete — {failed_count}/{len(results)} step(s) failed",
|
|
"warning",
|
|
)
|
|
else:
|
|
self._status.set_status("Workflow complete", "info")
|
|
else:
|
|
self._status.set_status("Workflow complete", "info")
|
|
|
|
# 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:
|
|
"""Refresh the serial port list."""
|
|
ports = detect_serial_ports()
|
|
self._port_menu.configure(values=[""] + [p.device for p in ports])
|
|
self._log_message(f"Found {len(ports)} serial port(s)")
|
|
|
|
def _refresh_ports_initial(self) -> None:
|
|
"""Refresh the serial port list on startup (silent, no log)."""
|
|
ports = detect_serial_ports()
|
|
self._port_menu.configure(values=[""] + [p.device for p in ports])
|
|
# Auto-select if only one port found and config has one
|
|
if ports and self._config and self._config.serial_port:
|
|
devices = [p.device for p in ports]
|
|
if self._config.serial_port in devices:
|
|
self._port_var.set(self._config.serial_port)
|
|
|
|
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)
|
|
user_config_path = self._get_user_config_path()
|
|
self._config._config_path = user_config_path
|
|
try:
|
|
self._config.save()
|
|
except Exception:
|
|
pass
|
|
self.destroy()
|
|
|
|
def _read_serial(self) -> None:
|
|
"""Read and display serial output."""
|
|
port = self._port_var.get()
|
|
if not port:
|
|
self._log_message("No serial port selected")
|
|
return
|
|
|
|
self._log_message(f"Reading from {port}...")
|
|
try:
|
|
import serial
|
|
|
|
with serial.Serial(port, self._config.serial_baudrate if self._config else 115200, timeout=5) as ser:
|
|
ser.reset_input_buffer()
|
|
lines = []
|
|
while ser.in_waiting:
|
|
line = ser.readline().decode("utf-8", errors="replace").strip()
|
|
if line:
|
|
lines.append(line)
|
|
|
|
if lines:
|
|
info = parse_boot_output(lines)
|
|
self._log_message(f" Parsed: IP={info.ip_address}, Ver={info.version}")
|
|
for line in lines[-10:]:
|
|
self._log_message(f" > {line}")
|
|
else:
|
|
self._log_message(" No data received")
|
|
except Exception as e:
|
|
self._log_message(f" Error: {e}")
|
|
|
|
def _test_serial_version(self) -> None:
|
|
"""Test serial port by sending 'ver()' command and parsing response."""
|
|
port = self._port_var.get()
|
|
if not port:
|
|
self._log_message("No serial port selected")
|
|
return
|
|
|
|
self._log_message(f"Testing version on {port}...")
|
|
self._test_version_btn.configure(state="disabled", text="Testing...")
|
|
|
|
# Run in background thread
|
|
def test_version_thread():
|
|
try:
|
|
from serial_monitor import test_serial_version
|
|
|
|
success, message, version = test_serial_version(
|
|
port,
|
|
self._config.serial_baudrate if self._config else 115200,
|
|
)
|
|
|
|
if success:
|
|
if version:
|
|
self._log_message(f" ✓ {message}")
|
|
self._log_message(f" ✓ Version detected: {version}")
|
|
self._status.set_status(f"Version: {version}", "success")
|
|
else:
|
|
self._log_message(f" ✓ {message}")
|
|
self._status.set_status("Port responded", "info")
|
|
else:
|
|
self._log_message(f" ✗ {message}")
|
|
self._status.set_status(message, "error")
|
|
except Exception as e:
|
|
self._log_message(f" ✗ Error: {e}")
|
|
self._status.set_status(f"Error: {e}", "error")
|
|
finally:
|
|
# Re-enable button
|
|
self.after(0, lambda: self._test_version_btn.configure(state="normal", text="Test Version"))
|
|
|
|
threading.Thread(target=test_version_thread, daemon=True).start()
|
|
|
|
# ── Vitis Check ────────────────────────────────────────────
|
|
|
|
def _check_vitis(self) -> None:
|
|
"""Check Vitis/Vivado on startup."""
|
|
if not self._config:
|
|
return
|
|
|
|
result = check_vitis(self._config)
|
|
status_dict = get_tool_status_dict(result)
|
|
|
|
if result.is_ready:
|
|
self._vitis_status.configure(text="Vitis: Ready", text_color=SUCCESS_COLOR)
|
|
self._log_message("Vitis/Vivado tools available")
|
|
else:
|
|
self._vitis_status.configure(text="Vitis: Partial", text_color=WARNING_COLOR)
|
|
self._log_message(f"Vitis issues: {'; '.join(result.errors)}")
|
|
|
|
|
|
# ── Entry Point ────────────────────────────────────────────────
|
|
|
|
def main() -> None:
|
|
"""Launch the Zynq Flasher GUI application."""
|
|
app = MainWindow()
|
|
app.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|