Compare commits
6
Commits
master
...
5f9049a321
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f9049a321 | ||
|
|
e2e1fdd2a0 | ||
|
|
a196211708 | ||
|
|
36fbb6a662 | ||
|
|
4ef1ce0c6b | ||
|
|
906f787748 |
+176
-31
@@ -173,14 +173,20 @@ class MainWindow(ctk.CTk):
|
|||||||
self._config._config_path = user_config_path
|
self._config._config_path = user_config_path
|
||||||
|
|
||||||
def _reload_config(self) -> None:
|
def _reload_config(self) -> None:
|
||||||
"""Re-read config.yaml and update UI selectors with latest paths."""
|
"""Re-read config.yaml and update UI selectors with latest paths.
|
||||||
|
|
||||||
|
Reads the file on disk, applies values to the internal Config
|
||||||
|
object, then updates every UI selector so that changes made
|
||||||
|
outside the GUI (or between runs) take effect immediately.
|
||||||
|
"""
|
||||||
user_path = self._get_user_config_path()
|
user_path = self._get_user_config_path()
|
||||||
if not user_path.exists():
|
if not user_path.exists():
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
fresh = Config.from_file(user_path)
|
fresh = Config.from_file(user_path)
|
||||||
self._config = fresh
|
self._config = fresh
|
||||||
# Update UI selectors
|
# Update UI selectors with resolved (absolute) paths — selectors
|
||||||
|
# display only the filename internally.
|
||||||
for attr, selector in [
|
for attr, selector in [
|
||||||
("bootloader_bit_path", self._bit_selector),
|
("bootloader_bit_path", self._bit_selector),
|
||||||
("bootloader_elf_path", self._elf_selector),
|
("bootloader_elf_path", self._elf_selector),
|
||||||
@@ -190,7 +196,9 @@ class MainWindow(ctk.CTk):
|
|||||||
]:
|
]:
|
||||||
val = getattr(fresh, attr, "")
|
val = getattr(fresh, attr, "")
|
||||||
if val:
|
if val:
|
||||||
selector.set_path(str(fresh.resolve_path(val)))
|
resolved = fresh.resolve_path(val)
|
||||||
|
selector.set_relative_path(val)
|
||||||
|
selector.set_path(str(resolved))
|
||||||
# Update IP
|
# Update IP
|
||||||
if self._ip_string_var:
|
if self._ip_string_var:
|
||||||
self._ip_string_var.set(fresh.zynq_ip)
|
self._ip_string_var.set(fresh.zynq_ip)
|
||||||
@@ -205,24 +213,63 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
This must be called before _save_config() to ensure
|
This must be called before _save_config() to ensure
|
||||||
the Config object has the latest UI values.
|
the Config object has the latest UI values.
|
||||||
|
|
||||||
|
File paths are synced as relative paths (from the Config
|
||||||
|
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:
|
if not self._config:
|
||||||
return
|
return
|
||||||
# Sync IP address
|
# Sync IP address (may not exist yet during _build_config_panel)
|
||||||
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
|
||||||
self._config.xilinx_path = self._xilinx_path_var.get()
|
if hasattr(self, '_xilinx_path_var'):
|
||||||
# Sync serial port
|
self._config.xilinx_path = self._xilinx_path_var.get()
|
||||||
self._config.serial_port = self._port_var.get()
|
# Sync serial port (may not exist yet during _build_config_panel)
|
||||||
|
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()
|
||||||
self._config.bootloader_bit_path = files["bootloader_bit_path"]
|
for attr, selector in [
|
||||||
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
("bootloader_bit_path", self._bit_selector),
|
||||||
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
("bootloader_elf_path", self._elf_selector),
|
||||||
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
("bootloader_bin_path", self._bootloader_bin_selector),
|
||||||
self._config.erase_all = self._erase_cb_var.get()
|
("fsbl_elf_path", self._fsbl_selector),
|
||||||
self._config.firmware_bin_path = files["firmware_bin_path"]
|
("firmware_bin_path", self._firmware_bin_selector),
|
||||||
|
]:
|
||||||
|
rel = selector.get_relative_path()
|
||||||
|
if rel:
|
||||||
|
setattr(self._config, attr, rel)
|
||||||
|
else:
|
||||||
|
abs_path = selector.get_path()
|
||||||
|
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:
|
def _save_config(self) -> None:
|
||||||
"""Save current configuration to user config file (config.yaml).
|
"""Save current configuration to user config file (config.yaml).
|
||||||
@@ -256,17 +303,36 @@ class MainWindow(ctk.CTk):
|
|||||||
path = filedialog.askdirectory(title="Select Xilinx Root Directory")
|
path = filedialog.askdirectory(title="Select Xilinx Root Directory")
|
||||||
if path:
|
if path:
|
||||||
self._xilinx_path_var.set(path)
|
self._xilinx_path_var.set(path)
|
||||||
if self._config:
|
self._on_xilinx_path_changed()
|
||||||
self._config.xilinx_path = path
|
# Re-check tools with new path
|
||||||
# Re-check tools with new path
|
self._check_vitis()
|
||||||
self._check_vitis()
|
|
||||||
|
def _on_xilinx_path_changed(self) -> None:
|
||||||
|
"""Handle Xilinx path entry change — sync config immediately."""
|
||||||
|
if self._config:
|
||||||
|
self._config.xilinx_path = self._xilinx_path_var.get()
|
||||||
|
|
||||||
|
def _on_ip_changed(self) -> None:
|
||||||
|
"""Handle IP address entry change — sync config immediately."""
|
||||||
|
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 ────────────────────────────────────────
|
# ── UI Construction ────────────────────────────────────────
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
"""Build the complete UI layout."""
|
"""Build the complete UI layout."""
|
||||||
# Scrollable main container — scrollbar appears when content overflows
|
# Main container — regular frame (not scrollable) so we can
|
||||||
main_frame = ctk.CTkScrollableFrame(self)
|
# 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.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
|
||||||
main_frame.grid_rowconfigure(1, weight=1)
|
main_frame.grid_rowconfigure(1, weight=1)
|
||||||
main_frame.grid_columnconfigure(0, weight=1)
|
main_frame.grid_columnconfigure(0, weight=1)
|
||||||
@@ -294,6 +360,10 @@ class MainWindow(ctk.CTk):
|
|||||||
self._build_utility_panel(right_frame)
|
self._build_utility_panel(right_frame)
|
||||||
self._build_status_panel(right_frame)
|
self._build_status_panel(right_frame)
|
||||||
|
|
||||||
|
# ── Maximize / resize handling ──
|
||||||
|
self.bind("<Configure>", self._on_window_resize)
|
||||||
|
self._maximized = False
|
||||||
|
|
||||||
def _build_header(self, parent: ctk.CTkFrame) -> None:
|
def _build_header(self, parent: ctk.CTkFrame) -> None:
|
||||||
"""Build the application header."""
|
"""Build the application header."""
|
||||||
header = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
header = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
||||||
@@ -335,6 +405,7 @@ class MainWindow(ctk.CTk):
|
|||||||
row=0, column=0, sticky="nsew",
|
row=0, column=0, sticky="nsew",
|
||||||
padx=PADDING, pady=(PADDING, 0),
|
padx=PADDING, pady=(PADDING, 0),
|
||||||
)
|
)
|
||||||
|
panel.grid_rowconfigure(0, weight=1)
|
||||||
panel.grid_columnconfigure(0, weight=1)
|
panel.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
# ── Title ──
|
# ── Title ──
|
||||||
@@ -496,6 +567,8 @@ class MainWindow(ctk.CTk):
|
|||||||
self._xilinx_path_var = ctk.StringVar(value=self._config.xilinx_path)
|
self._xilinx_path_var = ctk.StringVar(value=self._config.xilinx_path)
|
||||||
self._xilinx_entry = ctk.CTkEntry(xilinx_frame, textvariable=self._xilinx_path_var)
|
self._xilinx_entry = ctk.CTkEntry(xilinx_frame, textvariable=self._xilinx_path_var)
|
||||||
self._xilinx_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
self._xilinx_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
||||||
|
# Bind real-time sync so changes take effect immediately
|
||||||
|
self._xilinx_path_var.trace_add("write", lambda *args: self._on_xilinx_path_changed())
|
||||||
ctk.CTkButton(
|
ctk.CTkButton(
|
||||||
xilinx_frame, text="Browse", font=FONT_SMALL, width=70,
|
xilinx_frame, text="Browse", font=FONT_SMALL, width=70,
|
||||||
command=self._browse_xilinx_path,
|
command=self._browse_xilinx_path,
|
||||||
@@ -510,15 +583,20 @@ class MainWindow(ctk.CTk):
|
|||||||
self._ip_string_var = ctk.StringVar(value=self._config.zynq_ip)
|
self._ip_string_var = ctk.StringVar(value=self._config.zynq_ip)
|
||||||
self._ip_entry = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
|
self._ip_entry = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
|
||||||
self._ip_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
self._ip_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
||||||
|
# Bind real-time sync
|
||||||
|
self._ip_string_var.trace_add("write", lambda *args: self._on_ip_changed())
|
||||||
|
|
||||||
# FSBL ELF path (First Stage Bootloader — initialises PS, forces JTAG boot)
|
# FSBL ELF path (First Stage Bootloader — initialises PS, forces JTAG boot)
|
||||||
self._fsbl_selector = FileSelector(
|
self._fsbl_selector = FileSelector(
|
||||||
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_path(str(resolved))
|
self._fsbl_selector.set_path(str(resolved))
|
||||||
self._fsbl_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._fsbl_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
@@ -527,9 +605,12 @@ 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_path(str(resolved))
|
self._bit_selector.set_path(str(resolved))
|
||||||
self._bit_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._bit_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
@@ -538,9 +619,12 @@ 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_path(str(resolved))
|
self._elf_selector.set_path(str(resolved))
|
||||||
self._elf_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._elf_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
@@ -549,9 +633,12 @@ 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_path(str(resolved))
|
self._bootloader_bin_selector.set_path(str(resolved))
|
||||||
self._bootloader_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._bootloader_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
@@ -582,12 +669,23 @@ 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_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,
|
||||||
@@ -702,15 +800,17 @@ class MainWindow(ctk.CTk):
|
|||||||
self._test_version_btn.grid(row=2, column=1, padx=PADDING_SMALL, pady=PADDING_SMALL, sticky="ew")
|
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 — uses full available space."""
|
||||||
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
||||||
panel.grid(
|
panel.grid(
|
||||||
row=3, column=0, sticky="nsew",
|
row=3, column=0, sticky="nsew",
|
||||||
padx=PADDING, pady=PADDING,
|
padx=PADDING, pady=PADDING,
|
||||||
)
|
)
|
||||||
|
panel.grid_rowconfigure((0, 1), weight=1)
|
||||||
|
panel.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self._status = StatusDisplay(panel)
|
self._status = StatusDisplay(panel)
|
||||||
self._status.pack(fill="x", padx=PADDING, pady=PADDING)
|
self._status.grid(row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING)
|
||||||
|
|
||||||
# UART warning label (persistent, not overwritten by other status updates)
|
# UART warning label (persistent, not overwritten by other status updates)
|
||||||
self._uart_warning_label = ctk.CTkLabel(
|
self._uart_warning_label = ctk.CTkLabel(
|
||||||
@@ -720,10 +820,10 @@ class MainWindow(ctk.CTk):
|
|||||||
anchor="w",
|
anchor="w",
|
||||||
text_color=WARNING_COLOR,
|
text_color=WARNING_COLOR,
|
||||||
)
|
)
|
||||||
self._uart_warning_label.pack(
|
self._uart_warning_label.grid(
|
||||||
fill="x", padx=PADDING, pady=(0, PADDING_SMALL)
|
row=1, column=0, sticky="ew", padx=PADDING, pady=(0, PADDING_SMALL)
|
||||||
)
|
)
|
||||||
self._uart_warning_label.pack_forget() # Hide by default
|
self._uart_warning_label.grid_remove() # Hide by default
|
||||||
|
|
||||||
# ── Workflow Steps ─────────────────────────────────────────
|
# ── Workflow Steps ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -744,6 +844,8 @@ class MainWindow(ctk.CTk):
|
|||||||
def _get_selected_files(self) -> dict:
|
def _get_selected_files(self) -> dict:
|
||||||
"""Get selected file paths from the UI.
|
"""Get selected file paths from the UI.
|
||||||
|
|
||||||
|
Returns absolute paths for immediate backend use.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with all file paths from UI selectors.
|
Dictionary with all file paths from UI selectors.
|
||||||
"""
|
"""
|
||||||
@@ -881,11 +983,46 @@ class MainWindow(ctk.CTk):
|
|||||||
msg: Warning message to display.
|
msg: Warning message to display.
|
||||||
"""
|
"""
|
||||||
self._uart_warning_label.configure(text=msg)
|
self._uart_warning_label.configure(text=msg)
|
||||||
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
|
self._uart_warning_label.grid()
|
||||||
|
|
||||||
def _hide_uart_warning(self) -> None:
|
def _hide_uart_warning(self) -> None:
|
||||||
"""Hide the persistent UART warning label."""
|
"""Hide the persistent UART warning label."""
|
||||||
self._uart_warning_label.pack_forget()
|
self._uart_warning_label.grid_remove()
|
||||||
|
|
||||||
|
def _on_window_resize(self, event: tk.Event | None = None) -> 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.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the current window state
|
||||||
|
window_state = self.state()
|
||||||
|
is_maximized = window_state == "zoomed"
|
||||||
|
|
||||||
|
if is_maximized != self._maximized:
|
||||||
|
self._maximized = is_maximized
|
||||||
|
self._adjust_layout_for_maximize()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _adjust_layout_for_maximize(self) -> None:
|
||||||
|
"""Adjust layout based on maximized state.
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
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.
|
||||||
@@ -1585,12 +1722,20 @@ 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()
|
port = self._port_var.get().strip()
|
||||||
|
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
|
||||||
self._uart_window = UartMonitorWindow(
|
try:
|
||||||
self, port=port, baudrate=baudrate,
|
self._uart_window = UartMonitorWindow(
|
||||||
on_log=self._log_message,
|
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:
|
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."""
|
||||||
|
|||||||
+74
-23
@@ -6,6 +6,7 @@ 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
|
||||||
@@ -438,7 +439,9 @@ 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 a status message with color-coded feedback.
|
Shows status messages in a multi-line, expandable text area that
|
||||||
|
uses the full available space. Messages are color-coded and
|
||||||
|
automatically wrapped.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, master, **kwargs):
|
def __init__(self, master, **kwargs):
|
||||||
@@ -452,15 +455,19 @@ 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."""
|
"""Create the status display UI — multi-line expandable area."""
|
||||||
self._status_label = ctk.CTkLabel(
|
self.grid_rowconfigure(0, weight=1)
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self._status_text = ctk.CTkTextbox(
|
||||||
self,
|
self,
|
||||||
text="Ready",
|
|
||||||
font=FONT_BODY,
|
font=FONT_BODY,
|
||||||
anchor="w",
|
corner_radius=CORNER_RADIUS,
|
||||||
|
state="disabled",
|
||||||
|
wrap="word",
|
||||||
)
|
)
|
||||||
self._status_label.pack(
|
self._status_text.grid(
|
||||||
fill="x", padx=PADDING, pady=PADDING_SMALL
|
row=0, column=0, sticky="nsew", 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:
|
||||||
@@ -470,20 +477,32 @@ 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_label.configure(text=message)
|
self._status_text.configure(state="normal")
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
self._status_label.configure(text_color=colors.get(status, INFO_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")
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
Allows users to browse for files and displays the selected path.
|
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -506,7 +525,10 @@ 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._selected_path = ctk.StringVar(value="")
|
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._create_widgets(label_text)
|
self._create_widgets(label_text)
|
||||||
|
|
||||||
@@ -524,10 +546,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
|
# Path entry — shows only the filename
|
||||||
self._entry = ctk.CTkEntry(
|
self._entry = ctk.CTkEntry(
|
||||||
self,
|
self,
|
||||||
textvariable=self._selected_path,
|
textvariable=self._display_text,
|
||||||
font=FONT_MONO,
|
font=FONT_MONO,
|
||||||
state="readonly",
|
state="readonly",
|
||||||
)
|
)
|
||||||
@@ -549,33 +571,60 @@ 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=f"Select {self._selected_path.get() or 'file'}",
|
title="Select 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.
|
"""Set the selected file path (absolute or relative).
|
||||||
|
|
||||||
|
Internally stores the absolute path and displays only the
|
||||||
|
filename.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Absolute path to the selected file.
|
path: Absolute or relative path to the selected file.
|
||||||
"""
|
"""
|
||||||
self._selected_path.set(path)
|
self._full_path = path
|
||||||
if self._callback:
|
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._callback(path)
|
self._callback(path)
|
||||||
|
|
||||||
def get_path(self) -> str:
|
def get_path(self) -> str:
|
||||||
"""Get the currently selected file path.
|
"""Get the currently selected file path (absolute).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Selected file path string.
|
Selected file path string.
|
||||||
"""
|
"""
|
||||||
return self._selected_path.get()
|
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
|
||||||
|
|
||||||
|
|
||||||
class LogDisplay(ctk.CTkFrame):
|
class LogDisplay(ctk.CTkFrame):
|
||||||
@@ -953,6 +1002,8 @@ 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
|
||||||
|
|
||||||
@@ -1075,7 +1126,8 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
|||||||
def _bg_open() -> None:
|
def _bg_open() -> None:
|
||||||
try:
|
try:
|
||||||
import serial
|
import serial
|
||||||
ser = serial.Serial(self._port, self._baudrate, timeout=1)
|
from serial_monitor import _open_serial
|
||||||
|
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:
|
||||||
@@ -1201,5 +1253,4 @@ 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()
|
||||||
|
|||||||
@@ -125,11 +125,12 @@ def reboot_via_serial(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import serial
|
import serial
|
||||||
|
from serial_monitor import _open_serial
|
||||||
|
|
||||||
with serial.Serial(
|
with _open_serial(
|
||||||
config.serial_port,
|
config.serial_port,
|
||||||
config.serial_baudrate,
|
config.serial_baudrate,
|
||||||
timeout=5,
|
timeout=5.0,
|
||||||
) 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
|
||||||
|
|||||||
+32
-4
@@ -8,6 +8,7 @@ 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
|
||||||
@@ -17,6 +18,33 @@ 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."""
|
||||||
@@ -169,7 +197,7 @@ def check_uart_available(
|
|||||||
|
|
||||||
# 3. Try to open and read
|
# 3. Try to open and read
|
||||||
try:
|
try:
|
||||||
with serial.Serial(port, baudrate, timeout=timeout) as ser:
|
with _open_serial(port, baudrate, 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:
|
||||||
@@ -243,7 +271,7 @@ def test_serial_version(
|
|||||||
import serial
|
import serial
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with serial.Serial(port, baudrate, timeout=timeout) as ser:
|
with _open_serial(port, baudrate, timeout) as ser:
|
||||||
ser.reset_input_buffer()
|
ser.reset_input_buffer()
|
||||||
ser.reset_output_buffer()
|
ser.reset_output_buffer()
|
||||||
|
|
||||||
@@ -384,7 +412,7 @@ class UartMonitor:
|
|||||||
|
|
||||||
# Quick port check before starting thread
|
# Quick port check before starting thread
|
||||||
try:
|
try:
|
||||||
test_ser = serial.Serial(self._port, self._baudrate, timeout=1)
|
test_ser = _open_serial(self._port, self._baudrate, 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:
|
||||||
@@ -410,7 +438,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 = serial.Serial(
|
ser = _open_serial(
|
||||||
self._port,
|
self._port,
|
||||||
self._baudrate,
|
self._baudrate,
|
||||||
timeout=self._timeout,
|
timeout=self._timeout,
|
||||||
|
|||||||
Reference in New Issue
Block a user