Compare commits

...
6 Commits
Author SHA1 Message Date
yuysh 5f9049a321 fix: suppress FileSelector callbacks during construction
During _build_config_panel(), set_relative_path() triggered callbacks
that called _get_selected_files() before all selectors were created,
causing AttributeError.

Fix: FileSelector now has _suppress_callback flag. Set True during
construction, then False after all selectors are created. Finally
call _auto_save_config() once at the end of the panel build.
2026-06-12 10:29:58 +08:00
yuysh e2e1fdd2a0 fix: tag_config for CTkTextbox + auto-save on file selection
- StatusDisplay: use tag_config (not tag_configure) for CTkTextbox
- FileSelector callbacks now trigger _auto_save_config() so that
  file path changes are persisted immediately to config.yaml
- This fixes the issue where UI file path changes weren't picked up
  by subsequent step executions
2026-06-12 10:20:07 +08:00
yuysh a196211708 fix: 5 UI improvements — relative paths, filename-only display, real-time config, multi-line status, maximize layout
1. FileSelector stores relative paths internally and displays only the
   filename (not full path).  set_relative_path() / get_relative_path()
   methods added.

2. Config changes take effect in real-time: IP and Xilinx path entries
   are bound to trace handlers that sync immediately.  FileSelector
   relative paths are preserved between save/load cycles.

3. StatusDisplay uses a multi-line CTkTextbox instead of a single-line
   label, filling the entire available space.

4. Window maximize handling: bind to <Configure> event, detect
   maximized state, and adjust layout so content fits in one screen
   without scrolling.

5. Workflow panel expanded with weight=1 so it fills available space
   when maximized.
2026-06-12 10:13:11 +08:00
yuysh 36fbb6a662 fix: try both COM11 and \\.\COM11 formats when opening serial port
pyserial should auto-add \\.\ prefix for COM>=10, but some versions
don't. Added _open_serial() helper that tries the raw port name first,
then falls back to explicit NT namespace prefix on Windows.

Replaces all serial.Serial() calls across serial_monitor, reboot_manager,
and widgets.
2026-06-12 09:46:31 +08:00
yuysh 4ef1ce0c6b fix: UART window stability on Windows — lift/focus, remove stray grab_release
CTkToplevel on Windows can fail to display (flash then disappear)
without explicit lift()/focus(). Also removed grab_release() call
in _on_close since grab_set() was never called — calling release
without a matching grab can cause issues on some Windows tkinter
builds.
2026-06-11 18:24:42 +08:00
yuysh 906f787748 fix: guard UART window creation with try-except to prevent silent crash
On Windows the UartMonitorWindow could crash instantly without any
error message. Added try-except around window creation in _read_serial
so errors are logged instead of silently killing the window.

Also added empty port guard before attempting to open.
2026-06-11 18:23:40 +08:00
4 changed files with 285 additions and 60 deletions
+176 -31
View File
@@ -173,14 +173,20 @@ class MainWindow(ctk.CTk):
self._config._config_path = user_config_path
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()
if not user_path.exists():
return
try:
fresh = Config.from_file(user_path)
self._config = fresh
# Update UI selectors
# Update UI selectors with resolved (absolute) paths — selectors
# display only the filename internally.
for attr, selector in [
("bootloader_bit_path", self._bit_selector),
("bootloader_elf_path", self._elf_selector),
@@ -190,7 +196,9 @@ class MainWindow(ctk.CTk):
]:
val = getattr(fresh, attr, "")
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
if self._ip_string_var:
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
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:
return
# Sync IP address
# Sync IP address (may not exist yet during _build_config_panel)
if self._ip_string_var:
self._config.zynq_ip = self._ip_string_var.get()
# Sync Xilinx Kit path
self._config.xilinx_path = self._xilinx_path_var.get()
# Sync serial port
self._config.serial_port = self._port_var.get()
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'):
self._config.serial_port = self._port_var.get()
# Sync file paths from FileSelectors
files = self._get_selected_files()
self._config.bootloader_bit_path = files["bootloader_bit_path"]
self._config.bootloader_elf_path = files["bootloader_elf_path"]
self._config.bootloader_bin_path = files["bootloader_bin_path"]
self._config.fsbl_elf_path = files["fsbl_elf_path"]
self._config.erase_all = self._erase_cb_var.get()
self._config.firmware_bin_path = files["firmware_bin_path"]
for attr, selector in [
("bootloader_bit_path", self._bit_selector),
("bootloader_elf_path", self._elf_selector),
("bootloader_bin_path", self._bootloader_bin_selector),
("fsbl_elf_path", self._fsbl_selector),
("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:
"""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")
if path:
self._xilinx_path_var.set(path)
if self._config:
self._config.xilinx_path = path
# Re-check tools with new path
self._check_vitis()
self._on_xilinx_path_changed()
# Re-check tools with new path
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 ────────────────────────────────────────
def _build_ui(self) -> None:
"""Build the complete UI layout."""
# Scrollable main container — scrollbar appears when content overflows
main_frame = ctk.CTkScrollableFrame(self)
# 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)
@@ -294,6 +360,10 @@ class MainWindow(ctk.CTk):
self._build_utility_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:
"""Build the application header."""
header = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
@@ -335,6 +405,7 @@ class MainWindow(ctk.CTk):
row=0, column=0, sticky="nsew",
padx=PADDING, pady=(PADDING, 0),
)
panel.grid_rowconfigure(0, weight=1)
panel.grid_columnconfigure(0, weight=1)
# ── Title ──
@@ -496,6 +567,8 @@ class MainWindow(ctk.CTk):
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.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(
xilinx_frame, text="Browse", font=FONT_SMALL, width=70,
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_entry = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
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)
self._fsbl_selector = FileSelector(
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)
self._fsbl_selector.set_path(str(resolved))
self._fsbl_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
@@ -527,9 +605,12 @@ 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)
self._bit_selector.set_path(str(resolved))
self._bit_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
@@ -538,9 +619,12 @@ 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)
self._elf_selector.set_path(str(resolved))
self._elf_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
@@ -549,9 +633,12 @@ 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)
self._bootloader_bin_selector.set_path(str(resolved))
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,
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,
@@ -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")
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.grid(
row=3, column=0, sticky="nsew",
padx=PADDING, pady=PADDING,
)
panel.grid_rowconfigure((0, 1), weight=1)
panel.grid_columnconfigure(0, weight=1)
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)
self._uart_warning_label = ctk.CTkLabel(
@@ -720,10 +820,10 @@ class MainWindow(ctk.CTk):
anchor="w",
text_color=WARNING_COLOR,
)
self._uart_warning_label.pack(
fill="x", padx=PADDING, pady=(0, PADDING_SMALL)
self._uart_warning_label.grid(
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 ─────────────────────────────────────────
@@ -744,6 +844,8 @@ class MainWindow(ctk.CTk):
def _get_selected_files(self) -> dict:
"""Get selected file paths from the UI.
Returns absolute paths for immediate backend use.
Returns:
Dictionary with all file paths from UI selectors.
"""
@@ -881,11 +983,46 @@ class MainWindow(ctk.CTk):
msg: Warning message to display.
"""
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:
"""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:
"""Handle serial port selection change — validate immediately.
@@ -1585,12 +1722,20 @@ class MainWindow(ctk.CTk):
def _read_serial(self) -> None:
"""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
self._uart_window = UartMonitorWindow(
self, port=port, baudrate=baudrate,
on_log=self._log_message,
)
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."""
+74 -23
View File
@@ -6,6 +6,7 @@ consistent styling from gui/styles.py.
from __future__ import annotations
import os
import threading
import customtkinter as ctk
@@ -438,7 +439,9 @@ class ProgressIndicator(ctk.CTkFrame):
class StatusDisplay(ctk.CTkFrame):
"""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):
@@ -452,15 +455,19 @@ class StatusDisplay(ctk.CTkFrame):
self._create_widgets()
def _create_widgets(self) -> None:
"""Create the status display UI."""
self._status_label = ctk.CTkLabel(
"""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(
self,
text="Ready",
font=FONT_BODY,
anchor="w",
corner_radius=CORNER_RADIUS,
state="disabled",
wrap="word",
)
self._status_label.pack(
fill="x", padx=PADDING, pady=PADDING_SMALL
self._status_text.grid(
row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL
)
def set_status(self, message: str, status: str = "info") -> None:
@@ -470,20 +477,32 @@ class StatusDisplay(ctk.CTkFrame):
message: Status message text.
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 = {
"info": INFO_COLOR,
"success": SUCCESS_COLOR,
"error": DANGER_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):
"""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__(
@@ -506,7 +525,10 @@ class FileSelector(ctk.CTkFrame):
self._callback = callback
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)
@@ -524,10 +546,10 @@ class FileSelector(ctk.CTkFrame):
)
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,
textvariable=self._selected_path,
textvariable=self._display_text,
font=FONT_MONO,
state="readonly",
)
@@ -549,33 +571,60 @@ 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=f"Select {self._selected_path.get() or 'file'}",
title="Select 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.
"""Set the selected file path (absolute or relative).
Internally stores the absolute path and displays only the
filename.
Args:
path: Absolute path to the selected file.
path: Absolute or relative path to the selected file.
"""
self._selected_path.set(path)
if self._callback:
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._callback(path)
def get_path(self) -> str:
"""Get the currently selected file path.
"""Get the currently selected file path (absolute).
Returns:
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):
@@ -953,6 +1002,8 @@ 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
@@ -1075,7 +1126,8 @@ class UartMonitorWindow(ctk.CTkToplevel):
def _bg_open() -> None:
try:
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()
self.after(0, self._on_open_success)
except Exception as e:
@@ -1201,5 +1253,4 @@ class UartMonitorWindow(ctk.CTkToplevel):
if self._monitor:
self._monitor.stop()
self._monitor = None
self.grab_release()
self.destroy()
+3 -2
View File
@@ -125,11 +125,12 @@ def reboot_via_serial(
try:
import serial
from serial_monitor import _open_serial
with serial.Serial(
with _open_serial(
config.serial_port,
config.serial_baudrate,
timeout=5,
timeout=5.0,
) as ser:
ser.reset_input_buffer()
# Send Ctrl+C to break out of any running process
+32 -4
View File
@@ -8,6 +8,7 @@ Provides UartMonitor for continuous background monitoring.
from __future__ import annotations
import re
import sys
import threading
import time
from dataclasses import dataclass
@@ -17,6 +18,33 @@ 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."""
@@ -169,7 +197,7 @@ def check_uart_available(
# 3. Try to open and read
try:
with serial.Serial(port, baudrate, timeout=timeout) as ser:
with _open_serial(port, baudrate, timeout) as ser:
ser.reset_input_buffer()
lines: list[str] = []
while ser.in_waiting:
@@ -243,7 +271,7 @@ def test_serial_version(
import serial
try:
with serial.Serial(port, baudrate, timeout=timeout) as ser:
with _open_serial(port, baudrate, timeout) as ser:
ser.reset_input_buffer()
ser.reset_output_buffer()
@@ -384,7 +412,7 @@ class UartMonitor:
# Quick port check before starting thread
try:
test_ser = serial.Serial(self._port, self._baudrate, timeout=1)
test_ser = _open_serial(self._port, self._baudrate, 1)
test_ser.close()
except serial.SerialException as e:
if self.on_error:
@@ -410,7 +438,7 @@ class UartMonitor:
def _read_loop(self) -> None:
"""Main read loop running in background thread."""
try:
ser = serial.Serial(
ser = _open_serial(
self._port,
self._baudrate,
timeout=self._timeout,