feat: 5 improvements — file info, download verify, UART warning, status display, COM port reuse
- file_utils: compute_file_info() — CRC32, human-readable size, mtime - StatusDisplay: simplified to info-only key-value panel; status messages removed (log-only); file info shown in status panel at startup/select - config: added download_verify checkbox (default ON), skips Step 4.2 - UART warning: CTkTextbox with word-wrap and auto-height - COM port: check_uart_available / test_serial_version accept optional UartMonitor to avoid redundant serial opens (Error 13 on Windows) - layout: left:right 7:3 ratio, config panel stretches full width
This commit is contained in:
+147
-41
@@ -25,6 +25,7 @@ from tftp_manager import tftp_upload, tftp_download, tftp_upload_verify, TftpRes
|
||||
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 file_utils import compute_file_info, FileInfo
|
||||
from gui.styles import (
|
||||
WINDOW_WIDTH,
|
||||
WINDOW_HEIGHT,
|
||||
@@ -118,6 +119,9 @@ class MainWindow(ctk.CTk):
|
||||
except Exception as e:
|
||||
self._log_message(f" ✗ Config validation failed: {e}")
|
||||
|
||||
# Log file metadata for all configured paths
|
||||
self._log_all_file_info()
|
||||
|
||||
self._update_step_dependencies()
|
||||
|
||||
# Handle window close
|
||||
@@ -241,6 +245,8 @@ class MainWindow(ctk.CTk):
|
||||
except Exception:
|
||||
pass # Keep existing config on error
|
||||
|
||||
self._log_all_file_info()
|
||||
|
||||
def _sync_config_from_ui(self) -> None:
|
||||
"""Read current UI values and update the Config object.
|
||||
|
||||
@@ -284,6 +290,9 @@ class MainWindow(ctk.CTk):
|
||||
# Sync erase checkbox (may not exist yet)
|
||||
if hasattr(self, '_erase_cb_var'):
|
||||
self._config.erase_all = self._erase_cb_var.get()
|
||||
# Sync download verify checkbox
|
||||
if hasattr(self, '_download_verify_var'):
|
||||
self._config.download_verify = self._download_verify_var.get()
|
||||
|
||||
def _auto_save_config(self) -> None:
|
||||
"""Auto-save config to disk after UI changes.
|
||||
@@ -328,6 +337,11 @@ class MainWindow(ctk.CTk):
|
||||
if self._config:
|
||||
self._config.erase_all = self._erase_cb_var.get()
|
||||
|
||||
def _on_download_verify_toggle(self) -> None:
|
||||
"""Update config.download_verify immediately when checkbox toggles."""
|
||||
if self._config:
|
||||
self._config.download_verify = self._download_verify_var.get()
|
||||
|
||||
def _browse_xilinx_path(self) -> None:
|
||||
"""Open directory dialog to select Xilinx root folder."""
|
||||
from tkinter import filedialog
|
||||
@@ -349,13 +363,69 @@ class MainWindow(ctk.CTk):
|
||||
self._config.zynq_ip = self._ip_string_var.get()
|
||||
|
||||
def _on_file_selected(self, path: str) -> None:
|
||||
"""Handle file selection — sync and auto-save to disk.
|
||||
|
||||
When the user selects a file via the browse dialog, immediately
|
||||
sync the path to the Config object and persist it to YAML so
|
||||
that subsequent step executions pick up the new value.
|
||||
"""
|
||||
"""Handle file selection — sync, auto-save, and log file info."""
|
||||
from file_utils import compute_file_info
|
||||
self._auto_save_config()
|
||||
if path:
|
||||
info = compute_file_info(path)
|
||||
self._log_file_info(info)
|
||||
# Update status info panel
|
||||
self._log_all_file_info()
|
||||
|
||||
def _log_file_info(self, info: FileInfo) -> None:
|
||||
"""Log file metadata in the log window.
|
||||
|
||||
Args:
|
||||
info: FileInfo dataclass with computed metadata.
|
||||
"""
|
||||
filename = os.path.basename(info.path) if info.path else "(unknown)"
|
||||
if not info.exists:
|
||||
self._log_message(f" ⚠ {filename}: file not found")
|
||||
return
|
||||
self._log_message(
|
||||
f" ✓ {filename}: "
|
||||
f"{info.size_human} | CRC {info.crc32_hex} | {info.mtime_iso}"
|
||||
)
|
||||
|
||||
def _log_file_info(self, info: FileInfo) -> None:
|
||||
"""Log file metadata in a human-readable format.
|
||||
|
||||
Args:
|
||||
info: FileInfo dataclass with computed metadata.
|
||||
"""
|
||||
filename = os.path.basename(info.path) if info.path else "(unknown)"
|
||||
if not info.exists:
|
||||
self._log_message(f" ⚠ {filename}: file not found")
|
||||
return
|
||||
self._log_message(
|
||||
f" ✓ {filename}: "
|
||||
f"{info.size_human} | CRC {info.crc32_hex} | {info.mtime_iso}"
|
||||
)
|
||||
|
||||
def _log_all_file_info(self) -> None:
|
||||
"""Display file metadata in the status info panel."""
|
||||
if not self._config:
|
||||
return
|
||||
self._status.clear_info()
|
||||
paths = [
|
||||
("FSBL ELF", self._config.fsbl_elf_path),
|
||||
("Bitstream", self._config.bootloader_bit_path),
|
||||
("Bootloader ELF", self._config.bootloader_elf_path),
|
||||
("Bootloader BIN", self._config.bootloader_bin_path),
|
||||
("Firmware BIN", self._config.firmware_bin_path),
|
||||
]
|
||||
for label, path in paths:
|
||||
if not path:
|
||||
continue
|
||||
resolved = self._config.resolve_path(path)
|
||||
info = compute_file_info(resolved)
|
||||
if info.exists:
|
||||
self._status.set_info(
|
||||
label,
|
||||
f"{info.size_human} CRC {info.crc32_hex} {info.mtime_iso}"
|
||||
)
|
||||
else:
|
||||
self._status.set_info(label, "(not found)")
|
||||
|
||||
# ── UI Construction ────────────────────────────────────────
|
||||
|
||||
@@ -365,7 +435,8 @@ class MainWindow(ctk.CTk):
|
||||
self.main_frame = ctk.CTkScrollableFrame(self)
|
||||
self.main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
|
||||
self.main_frame.grid_rowconfigure(1, weight=1)
|
||||
self.main_frame.grid_columnconfigure(0, weight=1)
|
||||
self.main_frame.grid_columnconfigure(0, weight=7)
|
||||
self.main_frame.grid_columnconfigure(1, weight=3)
|
||||
|
||||
# ── Header ──
|
||||
self._build_header(self.main_frame)
|
||||
@@ -382,7 +453,7 @@ class MainWindow(ctk.CTk):
|
||||
# ── Right Panel: Configuration ──
|
||||
right_frame = ctk.CTkFrame(self.main_frame)
|
||||
right_frame.grid(row=1, column=1, sticky="nsew", padx=(PADDING, 0))
|
||||
right_frame.grid_rowconfigure(3, weight=1)
|
||||
right_frame.grid_rowconfigure(3, weight=3) # status panel — main info area
|
||||
right_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._build_config_panel(right_frame)
|
||||
@@ -581,6 +652,7 @@ class MainWindow(ctk.CTk):
|
||||
padx=PADDING, pady=PADDING,
|
||||
)
|
||||
panel.grid_rowconfigure(9, weight=1)
|
||||
panel.grid_columnconfigure(0, weight=1) # let content stretch
|
||||
|
||||
title = ctk.CTkLabel(
|
||||
panel,
|
||||
@@ -695,6 +767,17 @@ class MainWindow(ctk.CTk):
|
||||
)
|
||||
flash_model_label.grid(row=1, column=0, sticky="w", padx=PADDING_SMALL, pady=(2, 0))
|
||||
|
||||
# Download verify checkbox (Step 4.2)
|
||||
self._download_verify_var = ctk.BooleanVar(value=self._config.download_verify)
|
||||
self._download_verify_cb = ctk.CTkCheckBox(
|
||||
flash_frame,
|
||||
text="下载校验 (Step 4.2 Download Verify)",
|
||||
variable=self._download_verify_var,
|
||||
font=FONT_SMALL,
|
||||
command=self._on_download_verify_toggle,
|
||||
)
|
||||
self._download_verify_cb.grid(row=2, column=0, sticky="w", padx=PADDING_SMALL, pady=(2, 0))
|
||||
|
||||
# Firmware BIN path (TFTP upload)
|
||||
self._firmware_bin_selector = FileSelector(
|
||||
panel,
|
||||
@@ -843,18 +926,21 @@ class MainWindow(ctk.CTk):
|
||||
self._status = StatusDisplay(panel)
|
||||
self._status.grid(row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING)
|
||||
|
||||
# UART warning label (persistent, not overwritten by other status updates)
|
||||
self._uart_warning_label = ctk.CTkLabel(
|
||||
# UART warning — multi-line textbox, wraps long messages instead
|
||||
# of expanding the right panel horizontally
|
||||
self._uart_warning = ctk.CTkTextbox(
|
||||
panel,
|
||||
text="",
|
||||
font=FONT_BODY,
|
||||
anchor="w",
|
||||
text_color=WARNING_COLOR,
|
||||
height=40,
|
||||
corner_radius=4,
|
||||
fg_color="transparent",
|
||||
state="disabled",
|
||||
wrap="word",
|
||||
)
|
||||
self._uart_warning_label.grid(
|
||||
self._uart_warning.grid(
|
||||
row=1, column=0, sticky="ew", padx=PADDING, pady=(0, PADDING_SMALL)
|
||||
)
|
||||
self._uart_warning_label.grid_remove() # Hide by default
|
||||
self._uart_warning.grid_remove() # Hide by default
|
||||
|
||||
# ── Workflow Steps ─────────────────────────────────────────
|
||||
|
||||
@@ -916,6 +1002,10 @@ class MainWindow(ctk.CTk):
|
||||
if result.is_ready:
|
||||
self._vitis_status.configure(text="Vitis: Ready", text_color=SUCCESS_COLOR)
|
||||
self._log_message(" Vitis/Vivado tools available")
|
||||
# Show tool versions in status display
|
||||
for tool_name, tool_info in status_dict["tools"].items():
|
||||
if tool_info["found"] and tool_info.get("version"):
|
||||
self._status.set_info(tool_name, f"v{tool_info['version']}")
|
||||
# Report all found versions for diagnostics
|
||||
if self._config.xilinx_path:
|
||||
try:
|
||||
@@ -969,6 +1059,7 @@ class MainWindow(ctk.CTk):
|
||||
|
||||
self._log_message(f" ✓ Zynq {part} detected on JTAG chain")
|
||||
self._jtag_status.configure(text=f"JTAG: {part} ✓", text_color=SUCCESS_COLOR)
|
||||
self._status.set_info("Zynq", part)
|
||||
else:
|
||||
self._zynq_jtag_present = False
|
||||
self._zynq_jtag_info = None
|
||||
@@ -980,7 +1071,8 @@ class MainWindow(ctk.CTk):
|
||||
self._log_message(" Checking UART serial port...")
|
||||
port = self._config.serial_port
|
||||
if port:
|
||||
available, reason = check_uart_available(port, self._config.serial_baudrate)
|
||||
available, reason = check_uart_available(port, self._config.serial_baudrate,
|
||||
monitor=self._uart_monitor)
|
||||
self._uart_available = available
|
||||
self._uart_message = reason
|
||||
if available:
|
||||
@@ -1008,17 +1100,26 @@ class MainWindow(ctk.CTk):
|
||||
self.after(0, lambda: self._show_uart_warning(msg))
|
||||
|
||||
def _show_uart_warning(self, msg: str) -> None:
|
||||
"""Display the persistent UART warning message.
|
||||
"""Display the persistent UART warning message (multi-line).
|
||||
|
||||
Args:
|
||||
msg: Warning message to display.
|
||||
"""
|
||||
self._uart_warning_label.configure(text=msg)
|
||||
self._uart_warning_label.grid()
|
||||
self._uart_warning.configure(state="normal")
|
||||
self._uart_warning.delete("1.0", "end")
|
||||
self._uart_warning.insert("1.0", msg)
|
||||
self._uart_warning.tag_config("warn", foreground=WARNING_COLOR)
|
||||
self._uart_warning.tag_add("warn", "1.0", "end")
|
||||
self._uart_warning.configure(state="disabled")
|
||||
self._uart_warning.grid()
|
||||
|
||||
# Auto-size height based on line count
|
||||
line_count = msg.count("\n") + 1
|
||||
self._uart_warning.configure(height=max(2, line_count))
|
||||
|
||||
def _hide_uart_warning(self) -> None:
|
||||
"""Hide the persistent UART warning label."""
|
||||
self._uart_warning_label.grid_remove()
|
||||
"""Hide the persistent UART warning."""
|
||||
self._uart_warning.grid_remove()
|
||||
|
||||
def _on_window_resize(self, event) -> None:
|
||||
"""Handle window resize / maximize events.
|
||||
@@ -1316,27 +1417,31 @@ class MainWindow(ctk.CTk):
|
||||
self._log_message(" ⏳ Waiting 2s before download...")
|
||||
time.sleep(2)
|
||||
|
||||
# 4b: TFTP Download Verify
|
||||
self._log_message("Step 4b: TFTP download verify...")
|
||||
self._tftp_phase = "Download Verify"
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_phase("Download Verify")
|
||||
try:
|
||||
download_result = tftp_download(
|
||||
self._config, remote_name,
|
||||
callback=self._tftp_sub_callback,
|
||||
expected_size=bin_path.stat().st_size,
|
||||
)
|
||||
status = "✓" if download_result.success else "✗"
|
||||
self._log_message(f" {status} {download_result.message}")
|
||||
# 4b: TFTP Download Verify (skipped if disabled in config)
|
||||
if not self._config.download_verify:
|
||||
self._log_message("Step 4b: Download verify — skipped (disabled in config)")
|
||||
all_results.append(True)
|
||||
else:
|
||||
self._log_message("Step 4b: TFTP download verify...")
|
||||
self._tftp_phase = "Download Verify"
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_complete("Download Verify") if download_result.success else self._tftp_sub_step_frame.set_step_error("Download Verify")
|
||||
all_results.append(download_result.success)
|
||||
except Exception as e:
|
||||
self._log_message(f" ✗ TFTP download error: {e}")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_error("Download Verify")
|
||||
all_results.append(False)
|
||||
self._tftp_sub_step_frame.set_phase("Download Verify")
|
||||
try:
|
||||
download_result = tftp_download(
|
||||
self._config, remote_name,
|
||||
callback=self._tftp_sub_callback,
|
||||
expected_size=bin_path.stat().st_size,
|
||||
)
|
||||
status = "✓" if download_result.success else "✗"
|
||||
self._log_message(f" {status} {download_result.message}")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_complete("Download Verify") if download_result.success else self._tftp_sub_step_frame.set_step_error("Download Verify")
|
||||
all_results.append(download_result.success)
|
||||
except Exception as e:
|
||||
self._log_message(f" ✗ TFTP download error: {e}")
|
||||
if hasattr(self, '_tftp_sub_step_frame'):
|
||||
self._tftp_sub_step_frame.set_step_error("Download Verify")
|
||||
all_results.append(False)
|
||||
|
||||
if not all_results[-1]:
|
||||
self._log_message(" Skipping CRC check (download failed)")
|
||||
@@ -1845,6 +1950,7 @@ class MainWindow(ctk.CTk):
|
||||
success, message, version = test_serial_version(
|
||||
port,
|
||||
self._config.serial_baudrate if self._config else 115200,
|
||||
monitor=self._uart_monitor,
|
||||
)
|
||||
|
||||
if success:
|
||||
|
||||
Reference in New Issue
Block a user