From 84142bb2299ac949f26c2da1984b427e0e7578c3 Mon Sep 17 00:00:00 2001 From: Jeremy Shen Date: Thu, 11 Jun 2026 14:27:48 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20style:=20compact=202-col=20Utili?= =?UTF-8?q?ty=20panel,=20both=20reboot=20buttons=20red?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/gui/main_window.py | 183 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 173 insertions(+), 10 deletions(-) diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 5ac1546..2196db9 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -85,6 +85,7 @@ class MainWindow(ctk.CTk): self._uart_monitor: UartMonitor | None = None self._uart_window: UartMonitorWindow | None = None self._flash_phase: str = "" + self._tftp_phase: str = "" # StringVar references for config sync self._ip_string_var: ctk.StringVar | None = None @@ -276,7 +277,7 @@ class MainWindow(ctk.CTk): # ── 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_rowconfigure(1, weight=1) left_frame.grid_columnconfigure(0, weight=1) self._build_workflow_panel(left_frame) @@ -285,10 +286,12 @@ class MainWindow(ctk.CTk): # ── 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) + right_frame.grid_rowconfigure(3, weight=1) + right_frame.grid_columnconfigure(0, weight=1) self._build_config_panel(right_frame) self._build_serial_panel(right_frame) + self._build_utility_panel(right_frame) self._build_status_panel(right_frame) def _build_header(self, parent: ctk.CTkFrame) -> None: @@ -601,7 +604,6 @@ class MainWindow(ctk.CTk): 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( @@ -644,20 +646,65 @@ class MainWindow(ctk.CTk): ) 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( panel, - text="Test Version", + text="Version (Serial)", font=FONT_BODY, 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: """Build the status display panel.""" panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS) panel.grid( - row=2, column=0, sticky="nsew", + row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING, ) @@ -930,6 +977,7 @@ class MainWindow(ctk.CTk): # 4a: TFTP Upload self._log_message("Step 4a: TFTP upload...") + self._tftp_phase = "Upload" if hasattr(self, '_tftp_sub_step_frame'): self._tftp_sub_step_frame.set_phase("Upload") try: @@ -952,14 +1000,20 @@ class MainWindow(ctk.CTk): self._log_message(" Skipping download verify (TFTP upload failed)") return False + # Delay to let Zynq TFTP server stabilize + 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}") @@ -1007,7 +1061,7 @@ class MainWindow(ctk.CTk): try: reboot_result = reboot_zynq( self._config, - method="auto", + method="jtag", callback=self._reboot_callback, ) status = "✓" if reboot_result.success else "✗" @@ -1058,7 +1112,15 @@ class MainWindow(ctk.CTk): self._log_message(f" [{status}] {message}") 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}") 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") finally: # 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() + 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 ──────────────────────────────────────────── def _check_vitis(self) -> None: