Compare commits
7
Commits
5f9049a321
...
c9636b2fcd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9636b2fcd | ||
|
|
03bc5d5268 | ||
|
|
ef685d6657 | ||
|
|
8a83794dc5 | ||
|
|
5704ada93c | ||
|
|
63711ffede | ||
|
|
9238cbc63e |
+96
-88
@@ -218,20 +218,15 @@ class MainWindow(ctk.CTk):
|
||||
object's perspective); the FileSelector stores both the
|
||||
absolute path (for internal use) and the relative path
|
||||
(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:
|
||||
return
|
||||
# Sync IP address (may not exist yet during _build_config_panel)
|
||||
# Sync IP address
|
||||
if self._ip_string_var:
|
||||
self._config.zynq_ip = self._ip_string_var.get()
|
||||
# Sync Xilinx Kit path
|
||||
if hasattr(self, '_xilinx_path_var'):
|
||||
self._config.xilinx_path = self._xilinx_path_var.get()
|
||||
# Sync serial port (may not exist yet during _build_config_panel)
|
||||
if hasattr(self, '_port_var'):
|
||||
# Sync serial port
|
||||
self._config.serial_port = self._port_var.get()
|
||||
# Sync file paths from FileSelectors
|
||||
files = self._get_selected_files()
|
||||
@@ -250,27 +245,8 @@ class MainWindow(ctk.CTk):
|
||||
if abs_path:
|
||||
self._config._config_path # ensure _config_path is set
|
||||
setattr(self._config, attr, self._config._relative_path(abs_path))
|
||||
# Sync erase checkbox (may not exist yet)
|
||||
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:
|
||||
"""Save current configuration to user config file (config.yaml).
|
||||
|
||||
@@ -317,31 +293,21 @@ class MainWindow(ctk.CTk):
|
||||
if self._config:
|
||||
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 ────────────────────────────────────────
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
"""Build the complete UI layout."""
|
||||
# Main container — regular frame (not scrollable) so we can
|
||||
# control whether scrollbars appear based on window size.
|
||||
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)
|
||||
# Scrollable main container — scrollbar appears when content overflows
|
||||
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)
|
||||
|
||||
# ── Header ──
|
||||
self._build_header(main_frame)
|
||||
self._build_header(self.main_frame)
|
||||
|
||||
# ── 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_rowconfigure(1, weight=1)
|
||||
left_frame.grid_columnconfigure(0, weight=1)
|
||||
@@ -350,7 +316,7 @@ class MainWindow(ctk.CTk):
|
||||
self._build_log_panel(left_frame)
|
||||
|
||||
# ── 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_rowconfigure(3, weight=1)
|
||||
right_frame.grid_columnconfigure(0, weight=1)
|
||||
@@ -519,6 +485,7 @@ class MainWindow(ctk.CTk):
|
||||
row=1, column=0, sticky="nsew",
|
||||
padx=PADDING, pady=PADDING,
|
||||
)
|
||||
self._log_panel = panel
|
||||
panel.grid_rowconfigure(1, weight=1)
|
||||
panel.grid_columnconfigure(0, weight=1)
|
||||
|
||||
@@ -591,9 +558,7 @@ class MainWindow(ctk.CTk):
|
||||
panel,
|
||||
label_text="FSBL ELF (.elf):",
|
||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||
callback=self._on_file_selected,
|
||||
)
|
||||
self._fsbl_selector._suppress_callback = True
|
||||
if 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)
|
||||
@@ -605,9 +570,7 @@ class MainWindow(ctk.CTk):
|
||||
panel,
|
||||
label_text="Bootloader BIT (.bit):",
|
||||
file_types=[("BIT files", "*.bit"), ("All files", "*")],
|
||||
callback=self._on_file_selected,
|
||||
)
|
||||
self._bit_selector._suppress_callback = True
|
||||
if 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)
|
||||
@@ -619,9 +582,7 @@ class MainWindow(ctk.CTk):
|
||||
panel,
|
||||
label_text="Bootloader ELF (.elf):",
|
||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||
callback=self._on_file_selected,
|
||||
)
|
||||
self._elf_selector._suppress_callback = True
|
||||
if 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)
|
||||
@@ -633,9 +594,7 @@ class MainWindow(ctk.CTk):
|
||||
panel,
|
||||
label_text="Bootloader BIN (.bin):",
|
||||
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:
|
||||
resolved = self._config.resolve_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,
|
||||
label_text="Firmware BIN (.bin):",
|
||||
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:
|
||||
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_path(str(resolved))
|
||||
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_btn = ctk.CTkButton(
|
||||
panel,
|
||||
@@ -989,16 +938,14 @@ class MainWindow(ctk.CTk):
|
||||
"""Hide the persistent UART warning label."""
|
||||
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.
|
||||
|
||||
When the window is maximized, disable the scrollable frame and
|
||||
let all panels fill the available space without scrolling.
|
||||
When the window is restored to a smaller size, re-enable
|
||||
scrolling so the content remains accessible.
|
||||
When the window is maximized, scale content to fit one screen
|
||||
and hide scrollbars. When restored, use normal sizing with
|
||||
scrollbars on overflow.
|
||||
"""
|
||||
try:
|
||||
# Get the current window state
|
||||
window_state = self.state()
|
||||
is_maximized = window_state == "zoomed"
|
||||
|
||||
@@ -1009,20 +956,89 @@ class MainWindow(ctk.CTk):
|
||||
pass
|
||||
|
||||
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:
|
||||
# When maximized, we want everything to fit in one screen.
|
||||
# Set a reasonable minsize so the layout doesn't collapse too small.
|
||||
self.minsize(800, 600)
|
||||
# Hide scrollbars when maximized
|
||||
if has_scrollbars:
|
||||
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:
|
||||
# When restored to a smaller size, the scrollable frame
|
||||
# (if we were using one) would show scrollbars.
|
||||
# With regular frames, the user can resize the window.
|
||||
pass
|
||||
# Show scrollbars when not maximized
|
||||
if has_scrollbars:
|
||||
sf._scrollbar_y.grid()
|
||||
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:
|
||||
"""Handle serial port selection change — validate immediately.
|
||||
@@ -1722,20 +1738,12 @@ class MainWindow(ctk.CTk):
|
||||
|
||||
def _read_serial(self) -> None:
|
||||
"""Open the UART Monitor window."""
|
||||
port = self._port_var.get().strip()
|
||||
if not port:
|
||||
self._log_message(" ✗ No serial port selected")
|
||||
return
|
||||
port = self._port_var.get()
|
||||
baudrate = self._config.serial_baudrate if self._config else 115200
|
||||
try:
|
||||
self._uart_window = UartMonitorWindow(
|
||||
self, port=port, baudrate=baudrate,
|
||||
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:
|
||||
"""Test serial port by sending 'ver()' command and parsing response."""
|
||||
|
||||
+49
-80
@@ -6,7 +6,6 @@ consistent styling from gui/styles.py.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
import customtkinter as ctk
|
||||
@@ -439,9 +438,7 @@ class ProgressIndicator(ctk.CTkFrame):
|
||||
class StatusDisplay(ctk.CTkFrame):
|
||||
"""Status display widget for operation results.
|
||||
|
||||
Shows status messages in a multi-line, expandable text area that
|
||||
uses the full available space. Messages are color-coded and
|
||||
automatically wrapped.
|
||||
Shows a status message with color-coded feedback.
|
||||
"""
|
||||
|
||||
def __init__(self, master, **kwargs):
|
||||
@@ -455,19 +452,15 @@ class StatusDisplay(ctk.CTkFrame):
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self) -> None:
|
||||
"""Create the status display UI — multi-line expandable area."""
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._status_text = ctk.CTkTextbox(
|
||||
"""Create the status display UI."""
|
||||
self._status_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="Ready",
|
||||
font=FONT_BODY,
|
||||
corner_radius=CORNER_RADIUS,
|
||||
state="disabled",
|
||||
wrap="word",
|
||||
anchor="w",
|
||||
)
|
||||
self._status_text.grid(
|
||||
row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL
|
||||
self._status_label.pack(
|
||||
fill="x", padx=PADDING, pady=PADDING_SMALL
|
||||
)
|
||||
|
||||
def set_status(self, message: str, status: str = "info") -> None:
|
||||
@@ -477,32 +470,20 @@ class StatusDisplay(ctk.CTkFrame):
|
||||
message: Status message text.
|
||||
status: Status type for coloring ('info', 'success', 'error', 'warning').
|
||||
"""
|
||||
self._status_text.configure(state="normal")
|
||||
self._status_text.delete("1.0", "end")
|
||||
|
||||
self._status_label.configure(text=message)
|
||||
colors = {
|
||||
"info": INFO_COLOR,
|
||||
"success": SUCCESS_COLOR,
|
||||
"error": DANGER_COLOR,
|
||||
"warning": WARNING_COLOR,
|
||||
}
|
||||
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")
|
||||
self._status_label.configure(text_color=colors.get(status, INFO_COLOR))
|
||||
|
||||
|
||||
class FileSelector(ctk.CTkFrame):
|
||||
"""File selector widget with browse button and path display.
|
||||
|
||||
Stores the full absolute path internally but displays only the
|
||||
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.
|
||||
Allows users to browse for files and displays the selected path.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -525,10 +506,7 @@ class FileSelector(ctk.CTkFrame):
|
||||
|
||||
self._callback = callback
|
||||
self._file_types = file_types or [("All files", "*")]
|
||||
self._full_path: str = "" # absolute path (internal)
|
||||
self._relative_path: str = "" # relative path (synced to Config)
|
||||
self._display_text = ctk.StringVar(value="")
|
||||
self._suppress_callback: bool = False # True during construction
|
||||
self._selected_path = ctk.StringVar(value="")
|
||||
|
||||
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))
|
||||
|
||||
# Path entry — shows only the filename
|
||||
# Path entry
|
||||
self._entry = ctk.CTkEntry(
|
||||
self,
|
||||
textvariable=self._display_text,
|
||||
textvariable=self._selected_path,
|
||||
font=FONT_MONO,
|
||||
state="readonly",
|
||||
)
|
||||
@@ -571,60 +549,33 @@ class FileSelector(ctk.CTkFrame):
|
||||
|
||||
def _browse(self) -> None:
|
||||
"""Open file dialog and set selected path."""
|
||||
import customtkinter
|
||||
from tkinter import filedialog
|
||||
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select file",
|
||||
title=f"Select {self._selected_path.get() or 'file'}",
|
||||
filetypes=self._file_types,
|
||||
)
|
||||
if file_path:
|
||||
self.set_path(file_path)
|
||||
|
||||
def set_path(self, path: str) -> None:
|
||||
"""Set the selected file path (absolute or relative).
|
||||
|
||||
Internally stores the absolute path and displays only the
|
||||
filename.
|
||||
"""Set the selected file path.
|
||||
|
||||
Args:
|
||||
path: Absolute or relative path to the selected file.
|
||||
path: Absolute path to the selected file.
|
||||
"""
|
||||
self._full_path = path
|
||||
self._display_text.set(os.path.basename(path) if path else "")
|
||||
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._selected_path.set(path)
|
||||
if self._callback:
|
||||
self._callback(path)
|
||||
|
||||
def get_path(self) -> str:
|
||||
"""Get the currently selected file path (absolute).
|
||||
"""Get the currently selected file path.
|
||||
|
||||
Returns:
|
||||
Selected file path string.
|
||||
"""
|
||||
return self._full_path
|
||||
|
||||
def get_relative_path(self) -> str:
|
||||
"""Get the currently selected relative path.
|
||||
|
||||
Returns:
|
||||
Relative path string, or empty string.
|
||||
"""
|
||||
return self._relative_path
|
||||
return self._selected_path.get()
|
||||
|
||||
|
||||
class LogDisplay(ctk.CTkFrame):
|
||||
@@ -699,15 +650,15 @@ class SubStepFrame(ctk.CTkFrame):
|
||||
"""Collapsible frame showing flash operation sub-steps with status.
|
||||
|
||||
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:
|
||||
super().__init__(parent, fg_color="transparent")
|
||||
|
||||
self._collapsed = False
|
||||
self._collapsed = True
|
||||
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,
|
||||
)
|
||||
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._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:
|
||||
self._collapsed = not self._collapsed
|
||||
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.
|
||||
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:
|
||||
super().__init__(parent, fg_color="transparent")
|
||||
|
||||
self._collapsed = False
|
||||
self._collapsed = True
|
||||
self._pulse_job: str | None = None
|
||||
self._substep_names = substep_names or ["Upload", "Download Verify", "CRC Check", "Reboot", "Boot Verify"]
|
||||
self._num_steps = len(self._substep_names)
|
||||
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
# 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:
|
||||
self._collapsed = not self._collapsed
|
||||
self._toggle_btn.configure(text="▶" if self._collapsed else "▼")
|
||||
@@ -1002,8 +973,6 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
||||
self.geometry("680x540")
|
||||
self.minsize(480, 360)
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
self.lift() # ensure visible on Windows
|
||||
self.focus()
|
||||
|
||||
self._on_log = on_log
|
||||
|
||||
@@ -1126,8 +1095,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
||||
def _bg_open() -> None:
|
||||
try:
|
||||
import serial
|
||||
from serial_monitor import _open_serial
|
||||
ser = _open_serial(self._port, self._baudrate, 1)
|
||||
ser = serial.Serial(self._port, self._baudrate, timeout=1)
|
||||
ser.close()
|
||||
self.after(0, self._on_open_success)
|
||||
except Exception as e:
|
||||
@@ -1253,4 +1221,5 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
||||
if self._monitor:
|
||||
self._monitor.stop()
|
||||
self._monitor = None
|
||||
self.grab_release()
|
||||
self.destroy()
|
||||
|
||||
@@ -125,12 +125,11 @@ def reboot_via_serial(
|
||||
|
||||
try:
|
||||
import serial
|
||||
from serial_monitor import _open_serial
|
||||
|
||||
with _open_serial(
|
||||
with serial.Serial(
|
||||
config.serial_port,
|
||||
config.serial_baudrate,
|
||||
timeout=5.0,
|
||||
timeout=5,
|
||||
) as ser:
|
||||
ser.reset_input_buffer()
|
||||
# Send Ctrl+C to break out of any running process
|
||||
|
||||
+4
-32
@@ -8,7 +8,6 @@ Provides UartMonitor for continuous background monitoring.
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
@@ -18,33 +17,6 @@ import serial
|
||||
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
|
||||
class BootInfo:
|
||||
"""Parsed information extracted from Zynq boot output."""
|
||||
@@ -197,7 +169,7 @@ def check_uart_available(
|
||||
|
||||
# 3. Try to open and read
|
||||
try:
|
||||
with _open_serial(port, baudrate, timeout) as ser:
|
||||
with serial.Serial(port, baudrate, timeout=timeout) as ser:
|
||||
ser.reset_input_buffer()
|
||||
lines: list[str] = []
|
||||
while ser.in_waiting:
|
||||
@@ -271,7 +243,7 @@ def test_serial_version(
|
||||
import serial
|
||||
|
||||
try:
|
||||
with _open_serial(port, baudrate, timeout) as ser:
|
||||
with serial.Serial(port, baudrate, timeout=timeout) as ser:
|
||||
ser.reset_input_buffer()
|
||||
ser.reset_output_buffer()
|
||||
|
||||
@@ -412,7 +384,7 @@ class UartMonitor:
|
||||
|
||||
# Quick port check before starting thread
|
||||
try:
|
||||
test_ser = _open_serial(self._port, self._baudrate, 1)
|
||||
test_ser = serial.Serial(self._port, self._baudrate, timeout=1)
|
||||
test_ser.close()
|
||||
except serial.SerialException as e:
|
||||
if self.on_error:
|
||||
@@ -438,7 +410,7 @@ class UartMonitor:
|
||||
def _read_loop(self) -> None:
|
||||
"""Main read loop running in background thread."""
|
||||
try:
|
||||
ser = _open_serial(
|
||||
ser = serial.Serial(
|
||||
self._port,
|
||||
self._baudrate,
|
||||
timeout=self._timeout,
|
||||
|
||||
Reference in New Issue
Block a user