Compare commits

..
7 Commits
4 changed files with 159 additions and 211 deletions
+103 -95
View File
@@ -218,21 +218,16 @@ class MainWindow(ctk.CTk):
object's perspective); the FileSelector stores both the object's perspective); the FileSelector stores both the
absolute path (for internal use) and the relative path absolute path (for internal use) and the relative path
(for saving to YAML). (for saving to YAML).
All attribute accesses are defensive — widgets may not exist
yet during construction (e.g. _port_var created in _build_serial).
""" """
if not self._config: if not self._config:
return return
# Sync IP address (may not exist yet during _build_config_panel) # Sync IP address
if self._ip_string_var: if self._ip_string_var:
self._config.zynq_ip = self._ip_string_var.get() self._config.zynq_ip = self._ip_string_var.get()
# Sync Xilinx Kit path # Sync Xilinx Kit path
if hasattr(self, '_xilinx_path_var'): self._config.xilinx_path = self._xilinx_path_var.get()
self._config.xilinx_path = self._xilinx_path_var.get() # Sync serial port
# Sync serial port (may not exist yet during _build_config_panel) self._config.serial_port = self._port_var.get()
if hasattr(self, '_port_var'):
self._config.serial_port = self._port_var.get()
# Sync file paths from FileSelectors # Sync file paths from FileSelectors
files = self._get_selected_files() files = self._get_selected_files()
for attr, selector in [ for attr, selector in [
@@ -250,26 +245,7 @@ class MainWindow(ctk.CTk):
if abs_path: if abs_path:
self._config._config_path # ensure _config_path is set self._config._config_path # ensure _config_path is set
setattr(self._config, attr, self._config._relative_path(abs_path)) setattr(self._config, attr, self._config._relative_path(abs_path))
# Sync erase checkbox (may not exist yet) self._config.erase_all = self._erase_cb_var.get()
if hasattr(self, '_erase_cb_var'):
self._config.erase_all = self._erase_cb_var.get()
def _auto_save_config(self) -> None:
"""Auto-save config to disk after UI changes.
Ensures that changes made in the UI (file paths, IP, etc.)
are persisted immediately so that _reload_config() picks them
up on the next step execution.
"""
if not self._config:
return
self._sync_config_from_ui()
user_config_path = self._get_user_config_path()
self._config._config_path = user_config_path
try:
self._config.save()
except Exception:
pass # Silently fail — user can manually save
def _save_config(self) -> None: def _save_config(self) -> None:
"""Save current configuration to user config file (config.yaml). """Save current configuration to user config file (config.yaml).
@@ -317,31 +293,21 @@ class MainWindow(ctk.CTk):
if self._config: if self._config:
self._config.zynq_ip = self._ip_string_var.get() 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.
"""
self._auto_save_config()
# ── UI Construction ──────────────────────────────────────── # ── UI Construction ────────────────────────────────────────
def _build_ui(self) -> None: def _build_ui(self) -> None:
"""Build the complete UI layout.""" """Build the complete UI layout."""
# Main container — regular frame (not scrollable) so we can # Scrollable main container — scrollbar appears when content overflows
# control whether scrollbars appear based on window size. self.main_frame = ctk.CTkScrollableFrame(self)
main_frame = ctk.CTkFrame(self) self.main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE) self.main_frame.grid_rowconfigure(1, weight=1)
main_frame.grid_rowconfigure(1, weight=1) self.main_frame.grid_columnconfigure(0, weight=1)
main_frame.grid_columnconfigure(0, weight=1)
# ── Header ── # ── Header ──
self._build_header(main_frame) self._build_header(self.main_frame)
# ── Left Panel: Workflow Steps ── # ── Left Panel: Workflow Steps ──
left_frame = ctk.CTkFrame(main_frame) left_frame = ctk.CTkFrame(self.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(1, weight=1) left_frame.grid_rowconfigure(1, weight=1)
left_frame.grid_columnconfigure(0, weight=1) left_frame.grid_columnconfigure(0, weight=1)
@@ -350,7 +316,7 @@ class MainWindow(ctk.CTk):
self._build_log_panel(left_frame) self._build_log_panel(left_frame)
# ── Right Panel: Configuration ── # ── Right Panel: Configuration ──
right_frame = ctk.CTkFrame(main_frame) right_frame = ctk.CTkFrame(self.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(3, weight=1) right_frame.grid_rowconfigure(3, weight=1)
right_frame.grid_columnconfigure(0, weight=1) right_frame.grid_columnconfigure(0, weight=1)
@@ -519,6 +485,7 @@ class MainWindow(ctk.CTk):
row=1, column=0, sticky="nsew", row=1, column=0, sticky="nsew",
padx=PADDING, pady=PADDING, padx=PADDING, pady=PADDING,
) )
self._log_panel = panel
panel.grid_rowconfigure(1, weight=1) panel.grid_rowconfigure(1, weight=1)
panel.grid_columnconfigure(0, weight=1) panel.grid_columnconfigure(0, weight=1)
@@ -591,9 +558,7 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="FSBL ELF (.elf):", label_text="FSBL ELF (.elf):",
file_types=[("ELF files", "*.elf"), ("All files", "*")], file_types=[("ELF files", "*.elf"), ("All files", "*")],
callback=self._on_file_selected,
) )
self._fsbl_selector._suppress_callback = True
if self._config.fsbl_elf_path: if self._config.fsbl_elf_path:
resolved = self._config.resolve_path(self._config.fsbl_elf_path) resolved = self._config.resolve_path(self._config.fsbl_elf_path)
self._fsbl_selector.set_relative_path(self._config.fsbl_elf_path) self._fsbl_selector.set_relative_path(self._config.fsbl_elf_path)
@@ -605,9 +570,7 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="Bootloader BIT (.bit):", label_text="Bootloader BIT (.bit):",
file_types=[("BIT files", "*.bit"), ("All files", "*")], file_types=[("BIT files", "*.bit"), ("All files", "*")],
callback=self._on_file_selected,
) )
self._bit_selector._suppress_callback = True
if self._config.bootloader_bit_path: if self._config.bootloader_bit_path:
resolved = self._config.resolve_path(self._config.bootloader_bit_path) resolved = self._config.resolve_path(self._config.bootloader_bit_path)
self._bit_selector.set_relative_path(self._config.bootloader_bit_path) self._bit_selector.set_relative_path(self._config.bootloader_bit_path)
@@ -619,9 +582,7 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="Bootloader ELF (.elf):", label_text="Bootloader ELF (.elf):",
file_types=[("ELF files", "*.elf"), ("All files", "*")], file_types=[("ELF files", "*.elf"), ("All files", "*")],
callback=self._on_file_selected,
) )
self._elf_selector._suppress_callback = True
if self._config.bootloader_elf_path: if self._config.bootloader_elf_path:
resolved = self._config.resolve_path(self._config.bootloader_elf_path) resolved = self._config.resolve_path(self._config.bootloader_elf_path)
self._elf_selector.set_relative_path(self._config.bootloader_elf_path) self._elf_selector.set_relative_path(self._config.bootloader_elf_path)
@@ -633,9 +594,7 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="Bootloader BIN (.bin):", label_text="Bootloader BIN (.bin):",
file_types=[("BIN files", "*.bin"), ("All files", "*")], file_types=[("BIN files", "*.bin"), ("All files", "*")],
callback=self._on_file_selected,
) )
self._bootloader_bin_selector._suppress_callback = True
if self._config.bootloader_bin_path: if self._config.bootloader_bin_path:
resolved = self._config.resolve_path(self._config.bootloader_bin_path) resolved = self._config.resolve_path(self._config.bootloader_bin_path)
self._bootloader_bin_selector.set_relative_path(self._config.bootloader_bin_path) self._bootloader_bin_selector.set_relative_path(self._config.bootloader_bin_path)
@@ -669,23 +628,13 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="Firmware BIN (.bin):", label_text="Firmware BIN (.bin):",
file_types=[("BIN files", "*.bin"), ("All files", "*")], file_types=[("BIN files", "*.bin"), ("All files", "*")],
callback=self._on_file_selected,
) )
self._firmware_bin_selector._suppress_callback = True
if self._config.firmware_bin_path: if self._config.firmware_bin_path:
resolved = self._config.resolve_path(self._config.firmware_bin_path) resolved = self._config.resolve_path(self._config.firmware_bin_path)
self._firmware_bin_selector.set_relative_path(self._config.firmware_bin_path) self._firmware_bin_selector.set_relative_path(self._config.firmware_bin_path)
self._firmware_bin_selector.set_path(str(resolved)) self._firmware_bin_selector.set_path(str(resolved))
self._firmware_bin_selector.grid(row=8, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._firmware_bin_selector.grid(row=8, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# Enable callbacks now that all selectors are created
for sel in (self._fsbl_selector, self._bit_selector, self._elf_selector,
self._bootloader_bin_selector, self._firmware_bin_selector):
sel._suppress_callback = False
# Now sync and save config (all selectors exist)
self._auto_save_config()
# Save button # Save button
save_btn = ctk.CTkButton( save_btn = ctk.CTkButton(
panel, panel,
@@ -989,16 +938,14 @@ class MainWindow(ctk.CTk):
"""Hide the persistent UART warning label.""" """Hide the persistent UART warning label."""
self._uart_warning_label.grid_remove() self._uart_warning_label.grid_remove()
def _on_window_resize(self, event: tk.Event | None = None) -> None: def _on_window_resize(self, event) -> None:
"""Handle window resize / maximize events. """Handle window resize / maximize events.
When the window is maximized, disable the scrollable frame and When the window is maximized, scale content to fit one screen
let all panels fill the available space without scrolling. and hide scrollbars. When restored, use normal sizing with
When the window is restored to a smaller size, re-enable scrollbars on overflow.
scrolling so the content remains accessible.
""" """
try: try:
# Get the current window state
window_state = self.state() window_state = self.state()
is_maximized = window_state == "zoomed" is_maximized = window_state == "zoomed"
@@ -1009,20 +956,89 @@ class MainWindow(ctk.CTk):
pass pass
def _adjust_layout_for_maximize(self) -> None: def _adjust_layout_for_maximize(self) -> None:
"""Adjust layout based on maximized state. """Scale content and toggle scrollbars based on maximized state."""
if not hasattr(self, 'main_frame'):
return
sf = self.main_frame
has_scrollbars = hasattr(sf, '_scrollbar_y')
When maximized: all panels expand to fill the window, no scrolling.
When not maximized: scrollable frame re-enables scrolling for overflow.
"""
if self._maximized: if self._maximized:
# When maximized, we want everything to fit in one screen. # Hide scrollbars when maximized
# Set a reasonable minsize so the layout doesn't collapse too small. if has_scrollbars:
self.minsize(800, 600) sf._scrollbar_y.grid_remove()
sf._scrollbar_x.grid_remove()
# Scale down to fit one screen
self._scale_ui(0.6)
# Hide the log panel when maximized to save space
if hasattr(self, '_log_panel'):
self._log_panel.grid_remove()
else: else:
# When restored to a smaller size, the scrollable frame # Show scrollbars when not maximized
# (if we were using one) would show scrollbars. if has_scrollbars:
# With regular frames, the user can resize the window. sf._scrollbar_y.grid()
pass sf._scrollbar_x.grid()
# Restore normal scale
self._scale_ui(1.0)
# Show the log panel when not maximized
if hasattr(self, '_log_panel'):
self._log_panel.grid()
def _scale_ui(self, factor: float) -> None:
"""Scale UI elements by a factor to fit screen.
Args:
factor: Scaling factor (1.0 = normal, <1.0 = smaller).
"""
# Scale font sizes in key labels
scale_font = lambda orig: (orig[0], max(8, int(orig[1] * factor)), orig[2] if len(orig) > 2 else "")
# Header fonts
if hasattr(self, '_vitis_status'):
self._vitis_status.configure(font=scale_font(FONT_BODY))
if hasattr(self, '_ip_status'):
self._ip_status.configure(font=scale_font(FONT_BODY))
if hasattr(self, '_jtag_status'):
self._jtag_status.configure(font=scale_font(FONT_BODY))
# Workflow panel fonts
if hasattr(self, '_steps'):
for step in self._steps:
if hasattr(step, '_title_label'):
step._title_label.configure(font=scale_font((FONT_BODY[0], 13, "bold")))
if hasattr(step, '_desc_label'):
step._desc_label.configure(font=scale_font(FONT_SMALL))
if hasattr(step, '_action_btn'):
step._action_btn.configure(font=scale_font((FONT_BODY[0], 12)))
# Config panel fonts
if hasattr(self, '_xilinx_entry'):
self._xilinx_entry.configure(font=scale_font(FONT_BODY))
if hasattr(self, '_ip_entry'):
self._ip_entry.configure(font=scale_font(FONT_BODY))
# Log display font
if hasattr(self, '_log_display') and hasattr(self._log_display, '_text_widget'):
self._log_display._text_widget.configure(font=scale_font(FONT_MONO))
# Status display font
if hasattr(self, '_status') and hasattr(self._status, '_status_text'):
self._status._status_text.configure(font=scale_font(FONT_BODY))
# Scale padding/spacing on all frames
if hasattr(self, 'config_frame'):
self.config_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
if hasattr(self, 'workflow_frame'):
self.workflow_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
if hasattr(self, 'log_frame'):
self.log_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
if hasattr(self, 'status_frame'):
self.status_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
# Scale step frame spacing
if hasattr(self, '_steps'):
for step in self._steps:
if hasattr(step, '_inner_frame'):
step._inner_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
def _on_port_changed(self, new_port: str) -> None: def _on_port_changed(self, new_port: str) -> None:
"""Handle serial port selection change — validate immediately. """Handle serial port selection change — validate immediately.
@@ -1722,20 +1738,12 @@ class MainWindow(ctk.CTk):
def _read_serial(self) -> None: def _read_serial(self) -> None:
"""Open the UART Monitor window.""" """Open the UART Monitor window."""
port = self._port_var.get().strip() port = self._port_var.get()
if not port:
self._log_message(" ✗ No serial port selected")
return
baudrate = self._config.serial_baudrate if self._config else 115200 baudrate = self._config.serial_baudrate if self._config else 115200
try: self._uart_window = UartMonitorWindow(
self._uart_window = UartMonitorWindow( self, port=port, baudrate=baudrate,
self, port=port, baudrate=baudrate, on_log=self._log_message,
on_log=self._log_message, )
)
except Exception as e:
self._log_message(f" ✗ Failed to open UART window: {e}")
import traceback
self._log_message(traceback.format_exc())
def _test_serial_version(self) -> None: def _test_serial_version(self) -> None:
"""Test serial port by sending 'ver()' command and parsing response.""" """Test serial port by sending 'ver()' command and parsing response."""
+50 -81
View File
@@ -6,7 +6,6 @@ consistent styling from gui/styles.py.
from __future__ import annotations from __future__ import annotations
import os
import threading import threading
import customtkinter as ctk import customtkinter as ctk
@@ -439,9 +438,7 @@ class ProgressIndicator(ctk.CTkFrame):
class StatusDisplay(ctk.CTkFrame): class StatusDisplay(ctk.CTkFrame):
"""Status display widget for operation results. """Status display widget for operation results.
Shows status messages in a multi-line, expandable text area that Shows a status message with color-coded feedback.
uses the full available space. Messages are color-coded and
automatically wrapped.
""" """
def __init__(self, master, **kwargs): def __init__(self, master, **kwargs):
@@ -455,19 +452,15 @@ class StatusDisplay(ctk.CTkFrame):
self._create_widgets() self._create_widgets()
def _create_widgets(self) -> None: def _create_widgets(self) -> None:
"""Create the status display UI — multi-line expandable area.""" """Create the status display UI."""
self.grid_rowconfigure(0, weight=1) self._status_label = ctk.CTkLabel(
self.grid_columnconfigure(0, weight=1)
self._status_text = ctk.CTkTextbox(
self, self,
text="Ready",
font=FONT_BODY, font=FONT_BODY,
corner_radius=CORNER_RADIUS, anchor="w",
state="disabled",
wrap="word",
) )
self._status_text.grid( self._status_label.pack(
row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL fill="x", padx=PADDING, pady=PADDING_SMALL
) )
def set_status(self, message: str, status: str = "info") -> None: def set_status(self, message: str, status: str = "info") -> None:
@@ -477,32 +470,20 @@ class StatusDisplay(ctk.CTkFrame):
message: Status message text. message: Status message text.
status: Status type for coloring ('info', 'success', 'error', 'warning'). status: Status type for coloring ('info', 'success', 'error', 'warning').
""" """
self._status_text.configure(state="normal") self._status_label.configure(text=message)
self._status_text.delete("1.0", "end")
colors = { colors = {
"info": INFO_COLOR, "info": INFO_COLOR,
"success": SUCCESS_COLOR, "success": SUCCESS_COLOR,
"error": DANGER_COLOR, "error": DANGER_COLOR,
"warning": WARNING_COLOR, "warning": WARNING_COLOR,
} }
color = colors.get(status, INFO_COLOR) self._status_label.configure(text_color=colors.get(status, INFO_COLOR))
self._status_text.insert("1.0", message)
# Tag the entire text with the color (CTkTextbox uses tag_config)
self._status_text.tag_config("status", foreground=color)
self._status_text.tag_add("status", "1.0", "end")
self._status_text.configure(state="disabled")
class FileSelector(ctk.CTkFrame): class FileSelector(ctk.CTkFrame):
"""File selector widget with browse button and path display. """File selector widget with browse button and path display.
Stores the full absolute path internally but displays only the Allows users to browse for files and displays the selected path.
filename in the entry field. Supports relative paths: the
``set_relative_path()`` / ``get_relative_path()`` methods work
with paths relative to a given base directory.
""" """
def __init__( def __init__(
@@ -525,10 +506,7 @@ class FileSelector(ctk.CTkFrame):
self._callback = callback self._callback = callback
self._file_types = file_types or [("All files", "*")] self._file_types = file_types or [("All files", "*")]
self._full_path: str = "" # absolute path (internal) self._selected_path = ctk.StringVar(value="")
self._relative_path: str = "" # relative path (synced to Config)
self._display_text = ctk.StringVar(value="")
self._suppress_callback: bool = False # True during construction
self._create_widgets(label_text) self._create_widgets(label_text)
@@ -546,10 +524,10 @@ class FileSelector(ctk.CTkFrame):
) )
label.grid(row=0, column=0, sticky="w", padx=(PADDING, PADDING_SMALL)) label.grid(row=0, column=0, sticky="w", padx=(PADDING, PADDING_SMALL))
# Path entry — shows only the filename # Path entry
self._entry = ctk.CTkEntry( self._entry = ctk.CTkEntry(
self, self,
textvariable=self._display_text, textvariable=self._selected_path,
font=FONT_MONO, font=FONT_MONO,
state="readonly", state="readonly",
) )
@@ -571,60 +549,33 @@ class FileSelector(ctk.CTkFrame):
def _browse(self) -> None: def _browse(self) -> None:
"""Open file dialog and set selected path.""" """Open file dialog and set selected path."""
import customtkinter
from tkinter import filedialog from tkinter import filedialog
file_path = filedialog.askopenfilename( file_path = filedialog.askopenfilename(
title="Select file", title=f"Select {self._selected_path.get() or 'file'}",
filetypes=self._file_types, filetypes=self._file_types,
) )
if file_path: if file_path:
self.set_path(file_path) self.set_path(file_path)
def set_path(self, path: str) -> None: def set_path(self, path: str) -> None:
"""Set the selected file path (absolute or relative). """Set the selected file path.
Internally stores the absolute path and displays only the
filename.
Args: Args:
path: Absolute or relative path to the selected file. path: Absolute path to the selected file.
""" """
self._full_path = path self._selected_path.set(path)
self._display_text.set(os.path.basename(path) if path else "") if self._callback:
if self._callback and not self._suppress_callback:
self._callback(path)
def set_relative_path(self, path: str) -> None:
"""Set a relative path (from config).
Resolves to absolute for internal storage, shows only filename.
Args:
path: Relative path string.
"""
self._relative_path = path
# Resolve to absolute for internal use
if path:
self._full_path = path # caller should resolve before calling
self._display_text.set(os.path.basename(path) if path else "")
if self._callback and not self._suppress_callback:
self._callback(path) self._callback(path)
def get_path(self) -> str: def get_path(self) -> str:
"""Get the currently selected file path (absolute). """Get the currently selected file path.
Returns: Returns:
Selected file path string. Selected file path string.
""" """
return self._full_path return self._selected_path.get()
def get_relative_path(self) -> str:
"""Get the currently selected relative path.
Returns:
Relative path string, or empty string.
"""
return self._relative_path
class LogDisplay(ctk.CTkFrame): class LogDisplay(ctk.CTkFrame):
@@ -699,15 +650,15 @@ class SubStepFrame(ctk.CTkFrame):
"""Collapsible frame showing flash operation sub-steps with status. """Collapsible frame showing flash operation sub-steps with status.
Colors match StepHeader: gray pending, blue running (+pulse), Colors match StepHeader: gray pending, blue running (+pulse),
green complete, red error. Default expanded. green complete, red error. Default collapsed.
""" """
def __init__(self, parent: ctk.CTkFrame) -> None: def __init__(self, parent: ctk.CTkFrame) -> None:
super().__init__(parent, fg_color="transparent") super().__init__(parent, fg_color="transparent")
self._collapsed = False self._collapsed = True
self._toggle_btn = ctk.CTkButton( self._toggle_btn = ctk.CTkButton(
self, text="", width=22, height=22, self, text="", width=22, height=22,
font=(FONT_SMALL[0], 9), command=self._toggle, font=(FONT_SMALL[0], 9), command=self._toggle,
) )
self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w") self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w")
@@ -732,6 +683,16 @@ class SubStepFrame(ctk.CTkFrame):
self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1)
self._pulse_job: str | None = None self._pulse_job: str | None = None
# Apply initial collapsed state (hide sub-items)
self._hide()
def _hide(self) -> None:
"""Hide sub-items without toggling state."""
for item in self._sub_items:
item["dot"].grid_remove()
item["label"].grid_remove()
item["pct"].grid_remove()
def _toggle(self) -> None: def _toggle(self) -> None:
self._collapsed = not self._collapsed self._collapsed = not self._collapsed
self._toggle_btn.configure(text="" if self._collapsed else "") self._toggle_btn.configure(text="" if self._collapsed else "")
@@ -833,19 +794,19 @@ class TftpSubStepFrame(ctk.CTkFrame):
Sub-steps: Upload, Download Verify, CRC Check, Reboot, Boot Verify. Sub-steps: Upload, Download Verify, CRC Check, Reboot, Boot Verify.
Colors match StepHeader: gray pending, blue running (+pulse), Colors match StepHeader: gray pending, blue running (+pulse),
green complete, red error. Default expanded. green complete, red error. Default collapsed.
""" """
def __init__(self, parent: ctk.CTkFrame, substep_names: list[str] | None = None) -> None: def __init__(self, parent: ctk.CTkFrame, substep_names: list[str] | None = None) -> None:
super().__init__(parent, fg_color="transparent") super().__init__(parent, fg_color="transparent")
self._collapsed = False self._collapsed = True
self._pulse_job: str | None = None self._pulse_job: str | None = None
self._substep_names = substep_names or ["Upload", "Download Verify", "CRC Check", "Reboot", "Boot Verify"] self._substep_names = substep_names or ["Upload", "Download Verify", "CRC Check", "Reboot", "Boot Verify"]
self._num_steps = len(self._substep_names) self._num_steps = len(self._substep_names)
self._toggle_btn = ctk.CTkButton( self._toggle_btn = ctk.CTkButton(
self, text="", width=22, height=22, self, text="", width=22, height=22,
font=(FONT_SMALL[0], 9), command=self._toggle, font=(FONT_SMALL[0], 9), command=self._toggle,
) )
self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w") self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w")
@@ -868,6 +829,16 @@ class TftpSubStepFrame(ctk.CTkFrame):
self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1)
# Apply initial collapsed state (hide sub-items)
self._hide()
def _hide(self) -> None:
"""Hide sub-items without toggling state."""
for item in self._sub_items:
item["dot"].grid_remove()
item["label"].grid_remove()
item["pct"].grid_remove()
def _toggle(self) -> None: def _toggle(self) -> None:
self._collapsed = not self._collapsed self._collapsed = not self._collapsed
self._toggle_btn.configure(text="" if self._collapsed else "") self._toggle_btn.configure(text="" if self._collapsed else "")
@@ -1002,8 +973,6 @@ class UartMonitorWindow(ctk.CTkToplevel):
self.geometry("680x540") self.geometry("680x540")
self.minsize(480, 360) self.minsize(480, 360)
self.protocol("WM_DELETE_WINDOW", self._on_close) self.protocol("WM_DELETE_WINDOW", self._on_close)
self.lift() # ensure visible on Windows
self.focus()
self._on_log = on_log self._on_log = on_log
@@ -1018,7 +987,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
self._build_log_area() self._build_log_area()
self._build_status_bar() self._build_status_bar()
# Try to open serial port after window is drawn # Try to open serial port after window is drawn
self.after(100, self._try_open) self.after(100, self._try_open)
# ── Title Bar ────────────────────────────────────────────── # ── Title Bar ──────────────────────────────────────────────
@@ -1126,8 +1095,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
def _bg_open() -> None: def _bg_open() -> None:
try: try:
import serial import serial
from serial_monitor import _open_serial ser = serial.Serial(self._port, self._baudrate, timeout=1)
ser = _open_serial(self._port, self._baudrate, 1)
ser.close() ser.close()
self.after(0, self._on_open_success) self.after(0, self._on_open_success)
except Exception as e: except Exception as e:
@@ -1253,4 +1221,5 @@ class UartMonitorWindow(ctk.CTkToplevel):
if self._monitor: if self._monitor:
self._monitor.stop() self._monitor.stop()
self._monitor = None self._monitor = None
self.grab_release()
self.destroy() self.destroy()
+2 -3
View File
@@ -125,12 +125,11 @@ def reboot_via_serial(
try: try:
import serial import serial
from serial_monitor import _open_serial
with _open_serial( with serial.Serial(
config.serial_port, config.serial_port,
config.serial_baudrate, config.serial_baudrate,
timeout=5.0, timeout=5,
) as ser: ) as ser:
ser.reset_input_buffer() ser.reset_input_buffer()
# Send Ctrl+C to break out of any running process # Send Ctrl+C to break out of any running process
+4 -32
View File
@@ -8,7 +8,6 @@ Provides UartMonitor for continuous background monitoring.
from __future__ import annotations from __future__ import annotations
import re import re
import sys
import threading import threading
import time import time
from dataclasses import dataclass from dataclasses import dataclass
@@ -18,33 +17,6 @@ import serial
import serial.tools.list_ports import serial.tools.list_ports
def _open_serial(port: str, baudrate: int, timeout: float = 1.0) -> serial.Serial:
"""Open a serial port, trying both name formats on Windows.
pyserial normally adds \\\\.\\COM prefix for COM>=10, but some
versions don't. Try raw name first, then explicit NT namespace.
"""
port = port.strip()
last_err = None
# Try 1: port name as-is
try:
return serial.Serial(port, baudrate, timeout=timeout)
except serial.SerialException as e:
last_err = e
# Try 2: Windows NT namespace prefix
if sys.platform == "win32":
m = re.match(r"^(COM\d+)$", port, re.IGNORECASE)
if m:
try:
return serial.Serial(r"\\.\%s" % port, baudrate, timeout=timeout)
except serial.SerialException as e:
last_err = e
raise last_err or OSError(f"Cannot open serial port: {port}")
@dataclass @dataclass
class BootInfo: class BootInfo:
"""Parsed information extracted from Zynq boot output.""" """Parsed information extracted from Zynq boot output."""
@@ -197,7 +169,7 @@ def check_uart_available(
# 3. Try to open and read # 3. Try to open and read
try: try:
with _open_serial(port, baudrate, timeout) as ser: with serial.Serial(port, baudrate, timeout=timeout) as ser:
ser.reset_input_buffer() ser.reset_input_buffer()
lines: list[str] = [] lines: list[str] = []
while ser.in_waiting: while ser.in_waiting:
@@ -271,7 +243,7 @@ def test_serial_version(
import serial import serial
try: try:
with _open_serial(port, baudrate, timeout) as ser: with serial.Serial(port, baudrate, timeout=timeout) as ser:
ser.reset_input_buffer() ser.reset_input_buffer()
ser.reset_output_buffer() ser.reset_output_buffer()
@@ -412,7 +384,7 @@ class UartMonitor:
# Quick port check before starting thread # Quick port check before starting thread
try: try:
test_ser = _open_serial(self._port, self._baudrate, 1) test_ser = serial.Serial(self._port, self._baudrate, timeout=1)
test_ser.close() test_ser.close()
except serial.SerialException as e: except serial.SerialException as e:
if self.on_error: if self.on_error:
@@ -438,7 +410,7 @@ class UartMonitor:
def _read_loop(self) -> None: def _read_loop(self) -> None:
"""Main read loop running in background thread.""" """Main read loop running in background thread."""
try: try:
ser = _open_serial( ser = serial.Serial(
self._port, self._port,
self._baudrate, self._baudrate,
timeout=self._timeout, timeout=self._timeout,