🎨 style: compact 2-col Utility panel, both reboot buttons red

- Reboot (UDP) now uses DANGER_COLOR like Reboot (JTAG)
- 2×2 grid layout: reboot row + version row
- Buttons stretch with sticky=ew, equal column weights
- Title spans both columns
This commit is contained in:
2026-06-11 14:27:48 +08:00
parent f04fbe2130
commit 84142bb229
+173 -10
View File
@@ -85,6 +85,7 @@ class MainWindow(ctk.CTk):
self._uart_monitor: UartMonitor | None = None self._uart_monitor: UartMonitor | None = None
self._uart_window: UartMonitorWindow | None = None self._uart_window: UartMonitorWindow | None = None
self._flash_phase: str = "" self._flash_phase: str = ""
self._tftp_phase: str = ""
# StringVar references for config sync # StringVar references for config sync
self._ip_string_var: ctk.StringVar | None = None self._ip_string_var: ctk.StringVar | None = None
@@ -276,7 +277,7 @@ class MainWindow(ctk.CTk):
# ── Left Panel: Workflow Steps ── # ── Left Panel: Workflow Steps ──
left_frame = ctk.CTkFrame(main_frame) left_frame = ctk.CTkFrame(main_frame)
left_frame.grid(row=1, column=0, sticky="nsew", padx=(0, PADDING)) left_frame.grid(row=1, column=0, sticky="nsew", padx=(0, PADDING))
left_frame.grid_rowconfigure(3, weight=1) left_frame.grid_rowconfigure(1, weight=1)
left_frame.grid_columnconfigure(0, weight=1) left_frame.grid_columnconfigure(0, weight=1)
self._build_workflow_panel(left_frame) self._build_workflow_panel(left_frame)
@@ -285,10 +286,12 @@ class MainWindow(ctk.CTk):
# ── Right Panel: Configuration ── # ── Right Panel: Configuration ──
right_frame = ctk.CTkFrame(main_frame) right_frame = ctk.CTkFrame(main_frame)
right_frame.grid(row=1, column=1, sticky="nsew", padx=(PADDING, 0)) right_frame.grid(row=1, column=1, sticky="nsew", padx=(PADDING, 0))
right_frame.grid_rowconfigure(4, weight=1) right_frame.grid_rowconfigure(3, weight=1)
right_frame.grid_columnconfigure(0, weight=1)
self._build_config_panel(right_frame) self._build_config_panel(right_frame)
self._build_serial_panel(right_frame) self._build_serial_panel(right_frame)
self._build_utility_panel(right_frame)
self._build_status_panel(right_frame) self._build_status_panel(right_frame)
def _build_header(self, parent: ctk.CTkFrame) -> None: def _build_header(self, parent: ctk.CTkFrame) -> None:
@@ -601,7 +604,6 @@ class MainWindow(ctk.CTk):
row=1, column=0, sticky="nsew", row=1, column=0, sticky="nsew",
padx=PADDING, pady=PADDING, padx=PADDING, pady=PADDING,
) )
panel.grid_rowconfigure(3, weight=1)
panel.grid_columnconfigure(0, weight=1) panel.grid_columnconfigure(0, weight=1)
title = ctk.CTkLabel( title = ctk.CTkLabel(
@@ -644,20 +646,65 @@ class MainWindow(ctk.CTk):
) )
uart_btn.grid(row=2, column=0, padx=PADDING, pady=PADDING_SMALL) uart_btn.grid(row=2, column=0, padx=PADDING, pady=PADDING_SMALL)
# Test version button def _build_utility_panel(self, parent: ctk.CTkFrame) -> None:
"""Build the utility tools panel."""
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
panel.grid(
row=2, column=0, sticky="nsew",
padx=PADDING, pady=PADDING,
)
panel.grid_columnconfigure((0, 1), weight=1)
title = ctk.CTkLabel(
panel,
text="Utilities",
font=FONT_HEADING,
)
title.grid(row=0, column=0, columnspan=2, padx=PADDING, pady=PADDING)
# Row 1: Reboot buttons (both red)
self._jtag_reboot_btn = ctk.CTkButton(
panel,
text="Reboot (JTAG)",
font=FONT_BODY,
fg_color=DANGER_COLOR,
hover_color="#B92B2F",
command=self._jtag_reboot,
)
self._jtag_reboot_btn.grid(row=1, column=0, padx=PADDING_SMALL, pady=PADDING_SMALL, sticky="ew")
self._udp_reboot_btn = ctk.CTkButton(
panel,
text="Reboot (UDP)",
font=FONT_BODY,
fg_color=DANGER_COLOR,
hover_color="#B92B2F",
command=self._udp_reboot,
)
self._udp_reboot_btn.grid(row=1, column=1, padx=PADDING_SMALL, pady=PADDING_SMALL, sticky="ew")
# Row 2: Version buttons
self._udp_version_btn = ctk.CTkButton(
panel,
text="Version (UDP)",
font=FONT_BODY,
command=self._udp_version,
)
self._udp_version_btn.grid(row=2, column=0, padx=PADDING_SMALL, pady=PADDING_SMALL, sticky="ew")
self._test_version_btn = ctk.CTkButton( self._test_version_btn = ctk.CTkButton(
panel, panel,
text="Test Version", text="Version (Serial)",
font=FONT_BODY, font=FONT_BODY,
command=self._test_serial_version, command=self._test_serial_version,
) )
self._test_version_btn.grid(row=3, column=0, padx=PADDING, pady=PADDING_SMALL) self._test_version_btn.grid(row=2, column=1, padx=PADDING_SMALL, pady=PADDING_SMALL, sticky="ew")
def _build_status_panel(self, parent: ctk.CTkFrame) -> None: def _build_status_panel(self, parent: ctk.CTkFrame) -> None:
"""Build the status display panel.""" """Build the status display panel."""
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS) panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
panel.grid( panel.grid(
row=2, column=0, sticky="nsew", row=3, column=0, sticky="nsew",
padx=PADDING, pady=PADDING, padx=PADDING, pady=PADDING,
) )
@@ -930,6 +977,7 @@ class MainWindow(ctk.CTk):
# 4a: TFTP Upload # 4a: TFTP Upload
self._log_message("Step 4a: TFTP upload...") self._log_message("Step 4a: TFTP upload...")
self._tftp_phase = "Upload"
if hasattr(self, '_tftp_sub_step_frame'): if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("Upload") self._tftp_sub_step_frame.set_phase("Upload")
try: try:
@@ -952,14 +1000,20 @@ class MainWindow(ctk.CTk):
self._log_message(" Skipping download verify (TFTP upload failed)") self._log_message(" Skipping download verify (TFTP upload failed)")
return False return False
# Delay to let Zynq TFTP server stabilize
self._log_message(" ⏳ Waiting 2s before download...")
time.sleep(2)
# 4b: TFTP Download Verify # 4b: TFTP Download Verify
self._log_message("Step 4b: TFTP download verify...") self._log_message("Step 4b: TFTP download verify...")
self._tftp_phase = "Download Verify"
if hasattr(self, '_tftp_sub_step_frame'): if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("Download Verify") self._tftp_sub_step_frame.set_phase("Download Verify")
try: try:
download_result = tftp_download( download_result = tftp_download(
self._config, remote_name, self._config, remote_name,
callback=self._tftp_sub_callback, callback=self._tftp_sub_callback,
expected_size=bin_path.stat().st_size,
) )
status = "" if download_result.success else "" status = "" if download_result.success else ""
self._log_message(f" {status} {download_result.message}") self._log_message(f" {status} {download_result.message}")
@@ -1007,7 +1061,7 @@ class MainWindow(ctk.CTk):
try: try:
reboot_result = reboot_zynq( reboot_result = reboot_zynq(
self._config, self._config,
method="auto", method="jtag",
callback=self._reboot_callback, callback=self._reboot_callback,
) )
status = "" if reboot_result.success else "" status = "" if reboot_result.success else ""
@@ -1058,7 +1112,15 @@ class MainWindow(ctk.CTk):
self._log_message(f" [{status}] {message}") self._log_message(f" [{status}] {message}")
def _tftp_sub_callback(self, status: str, message: str) -> None: def _tftp_sub_callback(self, status: str, message: str) -> None:
"""Callback for TFTP sub-step progress.""" """Callback for TFTP sub-step progress — drives TftpSubStepFrame."""
if status == "progress":
try:
pct = int(message.rstrip('%'))
except ValueError:
pct = -1
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase(self._tftp_phase, pct)
return
self._log_message(f" [{status}] {message}") self._log_message(f" [{status}] {message}")
def _reboot_callback(self, status: str, message: str) -> None: def _reboot_callback(self, status: str, message: str) -> None:
@@ -1402,10 +1464,111 @@ class MainWindow(ctk.CTk):
self._status.set_status(f"Error: {e}", "error") self._status.set_status(f"Error: {e}", "error")
finally: finally:
# Re-enable button # Re-enable button
self.after(0, lambda: self._test_version_btn.configure(state="normal", text="Test Version")) self.after(0, lambda: self._test_version_btn.configure(state="normal", text="Version (Serial)"))
threading.Thread(target=test_version_thread, daemon=True).start() threading.Thread(target=test_version_thread, daemon=True).start()
def _jtag_reboot(self) -> None:
"""Reboot the Zynq board via JTAG system reset."""
if not self._config:
self._log_message("No config loaded — cannot reboot")
return
self._log_message(f"JTAG reboot: sending system reset to {self._config.zynq_ip}...")
self._jtag_reboot_btn.configure(state="disabled", text="Rebooting...")
def _reboot_thread():
try:
from reboot_manager import reboot_via_jtag
result = reboot_via_jtag(
self._config,
callback=lambda s, m: self._log_message(f" [{s}] {m}"),
)
if result.success:
self._log_message(f" ✓ JTAG reset: {result.message}")
self._status.set_status("JTAG reset complete", "success")
else:
self._log_message(f" ✗ JTAG reset: {result.message}")
self._status.set_status(f"JTAG reset failed: {result.message}", "error")
if result.output:
self._log_message(f" Output: {result.output[:500]}")
except Exception as e:
self._log_message(f" ✗ JTAG reboot error: {e}")
self._status.set_status(f"Error: {e}", "error")
finally:
self.after(0, lambda: self._jtag_reboot_btn.configure(
state="normal", text="Reboot (JTAG)",
))
threading.Thread(target=_reboot_thread, daemon=True).start()
def _udp_reboot(self) -> None:
"""Reboot the Zynq board via UDP command."""
if not self._config:
self._log_message("No config loaded — cannot reboot")
return
self._log_message(f"UDP reboot: sending reboot() to {self._config.zynq_ip}...")
self._udp_reboot_btn.configure(state="disabled", text="Rebooting...")
def _thread():
try:
from reboot_manager import reboot_via_udp
result = reboot_via_udp(
self._config,
callback=lambda s, m: self._log_message(f" [{s}] {m}"),
)
if result.success:
self._log_message(f" ✓ UDP reboot: {result.message}")
self._status.set_status("UDP reboot complete", "success")
else:
self._log_message(f" ✗ UDP reboot: {result.message}")
self._status.set_status(f"UDP reboot failed", "error")
except Exception as e:
self._log_message(f" ✗ UDP reboot error: {e}")
self._status.set_status(f"Error: {e}", "error")
finally:
self.after(0, lambda: self._udp_reboot_btn.configure(
state="normal", text="Reboot (UDP)",
))
threading.Thread(target=_thread, daemon=True).start()
def _udp_version(self) -> None:
"""Query Zynq firmware version via UDP."""
if not self._config:
self._log_message("No config loaded")
return
self._log_message(f"UDP version: sending ver() to {self._config.zynq_ip}...")
self._udp_version_btn.configure(state="disabled", text="Querying...")
def _thread():
try:
from reboot_manager import get_version_via_udp
result = get_version_via_udp(
self._config,
callback=lambda s, m: self._log_message(f" [{s}] {m}"),
)
if result.success:
self._log_message(f" ✓ Version: {result.version}")
self._status.set_status(f"Version: {result.version}", "success")
else:
self._log_message(f"{result.message}")
self._status.set_status(f"Query failed", "error")
except Exception as e:
self._log_message(f" ✗ UDP version error: {e}")
self._status.set_status(f"Error: {e}", "error")
finally:
self.after(0, lambda: self._udp_version_btn.configure(
state="normal", text="Version (UDP)",
))
threading.Thread(target=_thread, daemon=True).start()
# ── Vitis Check ──────────────────────────────────────────── # ── Vitis Check ────────────────────────────────────────────
def _check_vitis(self) -> None: def _check_vitis(self) -> None: