feat: UART Monitor standalone window

Replace 'Read Serial' button with 'Open UART' that opens a new
UART Monitor window (CTkToplevel) with:

- Title bar with status indicator (● connecting/ready/monitoring/error)
- Port info display + Start/Stop/Clear buttons
- Large scrollable log area (CTkTextbox) with monospace font
- Auto-scroll to bottom, line counter in status bar
- Boot info parsing (IP, version) shown as [BOOT] prefix
- Background serial monitoring via UartMonitor class

UART unavailable handling:
- On init, tries to open serial port in background thread
- If port can't open → shows 'UART 不可用' state with error message
- If port opens → shows 'Ready', user clicks Start to begin monitoring

Window features:
- Modal (grab_set) — focus stays on monitor window
- Clean up UartMonitor on close (WM_DELETE_WINDOW handler)
- Thread-safe UI updates via after(0, ...)
- Reuses existing UartMonitor from serial_monitor.py
This commit is contained in:
Jeremy Shen
2026-06-10 14:51:13 +08:00
parent 93ab224d6d
commit d5fd816ff9
3 changed files with 280 additions and 33 deletions
+2 -3
View File
@@ -1,7 +1,7 @@
bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin
bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrapper_hw_platform_0/pl_arm_wrapper.bit bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrapper_hw_platform_0/pl_arm_wrapper.bit
bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf
erase_all: false erase_all: true
firmware_bin_path: /home/ly0kos/work/Verify/FW/V1.3.1.3.3_06_01_pmu_cpld_check/V1.3.1.3.3_06_01_pmu_cpld_check.bin firmware_bin_path: /home/ly0kos/work/Verify/FW/V1.3.1.3.3_06_01_pmu_cpld_check/V1.3.1.3.3_06_01_pmu_cpld_check.bin
flash_model: s25fl256s1 flash_model: s25fl256s1
flash_type: qspi-x4-single flash_type: qspi-x4-single
@@ -21,6 +21,5 @@ step_timeouts:
tftp_reboot: 120 tftp_reboot: 120
tftp_upload_name: z7bin tftp_upload_name: z7bin
uart_delay: 3 uart_delay: 3
xilinx_path: "" xilinx_path: /data/xilinx
vitis_path: ''
zynq_ip: 192.168.100.11 zynq_ip: 192.168.100.11
+8 -30
View File
@@ -53,6 +53,7 @@ from gui.widgets import (
FileSelector, FileSelector,
LogDisplay, LogDisplay,
SubStepFrame, SubStepFrame,
UartMonitorWindow,
) )
@@ -591,14 +592,14 @@ class MainWindow(ctk.CTk):
) )
refresh_btn.grid(row=0, column=2, padx=PADDING_SMALL) refresh_btn.grid(row=0, column=2, padx=PADDING_SMALL)
# Read button # Open UART button
read_btn = ctk.CTkButton( uart_btn = ctk.CTkButton(
panel, panel,
text="Read Serial", text="Open UART",
font=FONT_BODY, font=FONT_BODY,
command=self._read_serial, command=self._read_serial,
) )
read_btn.grid(row=2, column=0, padx=PADDING, pady=PADDING_SMALL) uart_btn.grid(row=2, column=0, padx=PADDING, pady=PADDING_SMALL)
# Test version button # Test version button
self._test_version_btn = ctk.CTkButton( self._test_version_btn = ctk.CTkButton(
@@ -1202,33 +1203,10 @@ class MainWindow(ctk.CTk):
self.destroy() self.destroy()
def _read_serial(self) -> None: def _read_serial(self) -> None:
"""Read and display serial output.""" """Open the UART Monitor window."""
port = self._port_var.get() port = self._port_var.get()
if not port: baudrate = self._config.serial_baudrate if self._config else 115200
self._log_message("No serial port selected") UartMonitorWindow(self, port=port, baudrate=baudrate)
return
self._log_message(f"Reading from {port}...")
try:
import serial
with serial.Serial(port, self._config.serial_baudrate if self._config else 115200, timeout=5) as ser:
ser.reset_input_buffer()
lines = []
while ser.in_waiting:
line = ser.readline().decode("utf-8", errors="replace").strip()
if line:
lines.append(line)
if lines:
info = parse_boot_output(lines)
self._log_message(f" Parsed: IP={info.ip_address}, Ver={info.version}")
for line in lines[-10:]:
self._log_message(f" > {line}")
else:
self._log_message(" No data received")
except Exception as e:
self._log_message(f" Error: {e}")
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."""
+270
View File
@@ -6,9 +6,13 @@ consistent styling from gui/styles.py.
from __future__ import annotations from __future__ import annotations
import threading
import customtkinter as ctk import customtkinter as ctk
from typing import Callable from typing import Callable
from serial_monitor import UartMonitor
from gui.styles import ( from gui.styles import (
FONT_BODY, FONT_BODY,
FONT_MONO, FONT_MONO,
@@ -761,3 +765,269 @@ class SubStepFrame(ctk.CTkFrame):
self._toggle() self._toggle()
self._toggle() # collapse then expand to reset self._toggle() # collapse then expand to reset
self._toggle() self._toggle()
# ════════════════════════════════════════════════════════════════════
# UartMonitorWindow — standalone UART monitoring window
# ════════════════════════════════════════════════════════════════════
class UartMonitorWindow(ctk.CTkToplevel):
"""Standalone UART monitor window with real-time serial output.
Opens a Toplevel window that connects to the selected serial port,
displays live output in a scrollable log, and provides Start/Stop/Clear
controls. Reuses UartMonitor from serial_monitor.
"""
def __init__(
self,
master: ctk.CTk | ctk.CTkToplevel,
port: str,
baudrate: int = 115200,
) -> None:
"""Initialize the UART monitor window.
Args:
master: Parent window (MainWindow).
port: Serial port device path (e.g., '/dev/ttyUSB0').
baudrate: Baud rate for serial communication.
"""
super().__init__(master)
self.title("UART Monitor")
self.geometry("680x540")
self.minsize(480, 360)
self.grab_set()
self.protocol("WM_DELETE_WINDOW", self._on_close)
self._port = port
self._baudrate = baudrate
self._monitor: UartMonitor | None = None
self._line_count = 0
self._is_opening = False
self._build_title_bar()
self._build_controls()
self._build_log_area()
self._build_status_bar()
# Try to open serial port after window is drawn
self.after(100, self._try_open)
# ── Title Bar ──────────────────────────────────────────────
def _build_title_bar(self) -> None:
bar = ctk.CTkFrame(self, fg_color=DARK_CARD)
bar.pack(fill="x", padx=PADDING, pady=(PADDING, PADDING_SMALL))
self._title_label = ctk.CTkLabel(
bar, text="UART Monitor", font=_font(14, "bold"),
)
self._title_label.pack(side="left", padx=(PADDING_SMALL, 0), pady=PADDING_SMALL)
self._status_indicator = ctk.CTkLabel(
bar, text="", font=_font(12),
text_color=ACCENT_PENDING,
)
self._status_indicator.pack(side="right", padx=(PADDING_SMALL, PADDING_SMALL))
self._status_text = ctk.CTkLabel(
bar, text="Connecting...", font=FONT_SMALL,
text_color=DESC_TEXT_COLOR,
)
self._status_text.pack(side="right")
# ── Controls ───────────────────────────────────────────────
def _build_controls(self) -> None:
frame = ctk.CTkFrame(self, fg_color="transparent")
frame.pack(fill="x", padx=PADDING, pady=PADDING_SMALL)
self._port_info = ctk.CTkLabel(
frame, text=f"Port: {self._port} @ {self._baudrate} baud",
font=FONT_SMALL,
)
self._port_info.pack(side="left")
btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
btn_frame.pack(side="right")
self._start_btn = ctk.CTkButton(
btn_frame, text="Start", font=FONT_SMALL, width=64,
fg_color=PRIMARY_COLOR, hover_color=STEP_RUN_BG_HOVER,
command=self._on_start,
)
self._start_btn.pack(side="left", padx=(0, PADDING_SMALL))
self._stop_btn = ctk.CTkButton(
btn_frame, text="Stop", font=FONT_SMALL, width=64,
fg_color=DANGER_COLOR, hover_color="#B92B2F",
command=self._on_stop, state="disabled",
)
self._stop_btn.pack(side="left", padx=(0, PADDING_SMALL))
self._clear_btn = ctk.CTkButton(
btn_frame, text="Clear", font=FONT_SMALL, width=64,
fg_color=STEP_RUNNER_BG, hover_color=STEP_RUNNER_BG_HOVER,
command=self._on_clear, state="disabled",
)
self._clear_btn.pack(side="left")
# ── Log Area ───────────────────────────────────────────────
def _build_log_area(self) -> None:
self._log_frame = ctk.CTkFrame(self)
self._log_frame.pack(fill="both", expand=True, padx=PADDING, pady=PADDING_SMALL)
self._log_text = ctk.CTkTextbox(
self._log_frame,
font=FONT_MONO,
corner_radius=CORNER_RADIUS,
state="disabled",
)
self._log_text.pack(fill="both", expand=True, padx=PADDING, pady=PADDING)
# ── Status Bar ─────────────────────────────────────────────
def _build_status_bar(self) -> None:
bar = ctk.CTkFrame(self, fg_color=DARK_CARD)
bar.pack(fill="x", padx=PADDING, pady=(0, PADDING))
self._line_label = ctk.CTkLabel(
bar, text="Lines: 0", font=FONT_SMALL,
text_color=DESC_TEXT_COLOR,
)
self._line_label.pack(side="left", padx=PADDING_SMALL)
self._error_label = ctk.CTkLabel(
bar, text="", font=FONT_SMALL,
text_color=DANGER_COLOR,
)
self._error_label.pack(side="right", padx=PADDING_SMALL)
# ── Serial Port Open / Close ───────────────────────────────
def _try_open(self) -> None:
"""Attempt to open the serial port on init."""
if not self._port:
self._show_unavailable("未选择串口")
return
self._is_opening = True
self._update_status(ACCENT_PENDING, "Connecting...")
def _bg_open() -> None:
try:
import serial
ser = serial.Serial(self._port, self._baudrate, timeout=1)
ser.close()
self.after(0, self._on_open_success)
except Exception as e:
self.after(0, lambda: self._show_unavailable(str(e)))
threading.Thread(target=_bg_open, daemon=True).start()
def _on_open_success(self) -> None:
"""Serial port opened — enable Start button."""
self._is_opening = False
self._start_btn.configure(state="normal")
self._update_status(ACCENT_PENDING, "Ready")
self._log(f"[{self._port}] Port ready. Click Start to begin monitoring.\n")
def _show_unavailable(self, reason: str) -> None:
"""Show UART unavailable state."""
self._is_opening = False
self._status_indicator.configure(text_color=DANGER_COLOR)
self._status_text.configure(text="UART 不可用")
self._port_info.configure(text=f"Port: {self._port} (Unavailable)")
self._start_btn.configure(state="disabled")
self._log(f"[UART] 不可用: {reason}\n")
# ── Start / Stop / Clear ───────────────────────────────────
def _on_start(self) -> None:
if self._monitor and self._monitor.is_running():
return
self._start_btn.configure(state="disabled")
self._stop_btn.configure(state="normal")
self._clear_btn.configure(state="normal")
self._update_status(ACCENT_RUNNING, "Monitoring...")
self._log(f"[*] Starting monitor on {self._port} @ {self._baudrate} baud\n")
self._monitor = UartMonitor(self._port, self._baudrate)
self._monitor.on_line = self._on_line
self._monitor.on_boot_info = self._on_boot_info
self._monitor.on_error = self._on_error
if not self._monitor.start():
self._log("[!] Failed to start monitor\n")
self._update_status(ACCENT_ERROR, "Start failed")
self._start_btn.configure(state="normal")
self._stop_btn.configure(state="disabled")
self._clear_btn.configure(state="disabled")
def _on_stop(self) -> None:
if self._monitor:
self._monitor.stop()
self._monitor = None
self._start_btn.configure(state="normal")
self._stop_btn.configure(state="disabled")
self._clear_btn.configure(state="disabled")
self._update_status(ACCENT_PENDING, "Stopped")
self._log("[*] Monitor stopped\n")
def _on_clear(self) -> None:
self._log_text.configure(state="normal")
self._log_text.delete("1.0", "end")
self._log_text.configure(state="disabled")
self._line_count = 0
self._line_label.configure(text="Lines: 0")
# ── Callbacks (called from UartMonitor background thread) ──
def _on_line(self, line: str) -> None:
self.after(0, lambda: self._log_line(line))
def _on_boot_info(self, info) -> None:
parts = []
if info.ip_address:
parts.append(f"IP={info.ip_address}")
if info.version:
parts.append(f"Ver={info.version}")
if parts:
self.after(0, lambda: self._log(f"[BOOT] {' '.join(parts)}\n"))
def _on_error(self, error: str) -> None:
self.after(0, lambda: self._log_error(error))
# ── UI Update Helpers ──────────────────────────────────────
def _log(self, text: str) -> None:
"""Append text to the log (thread-safe via after)."""
self._log_text.configure(state="normal")
self._log_text.insert("end", text)
self._log_text.see("end")
self._log_text.configure(state="disabled")
def _log_line(self, line: str) -> None:
self._line_count += 1
self._log(f"{line}\n")
self._line_label.configure(text=f"Lines: {self._line_count}")
def _log_error(self, error: str) -> None:
self._error_label.configure(text=error)
def _update_status(self, color: str, text: str) -> None:
self._status_indicator.configure(text_color=color)
self._status_text.configure(text=text)
# ── Window Close ───────────────────────────────────────────
def _on_close(self) -> None:
"""Cleanup UartMonitor and destroy window."""
if self._monitor:
self._monitor.stop()
self._monitor = None
self.grab_release()
self.destroy()