✨ 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:
+2
-3
@@ -1,7 +1,7 @@
|
||||
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_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
|
||||
flash_model: s25fl256s1
|
||||
flash_type: qspi-x4-single
|
||||
@@ -21,6 +21,5 @@ step_timeouts:
|
||||
tftp_reboot: 120
|
||||
tftp_upload_name: z7bin
|
||||
uart_delay: 3
|
||||
xilinx_path: ""
|
||||
vitis_path: ''
|
||||
xilinx_path: /data/xilinx
|
||||
zynq_ip: 192.168.100.11
|
||||
|
||||
+8
-30
@@ -53,6 +53,7 @@ from gui.widgets import (
|
||||
FileSelector,
|
||||
LogDisplay,
|
||||
SubStepFrame,
|
||||
UartMonitorWindow,
|
||||
)
|
||||
|
||||
|
||||
@@ -591,14 +592,14 @@ class MainWindow(ctk.CTk):
|
||||
)
|
||||
refresh_btn.grid(row=0, column=2, padx=PADDING_SMALL)
|
||||
|
||||
# Read button
|
||||
read_btn = ctk.CTkButton(
|
||||
# Open UART button
|
||||
uart_btn = ctk.CTkButton(
|
||||
panel,
|
||||
text="Read Serial",
|
||||
text="Open UART",
|
||||
font=FONT_BODY,
|
||||
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
|
||||
self._test_version_btn = ctk.CTkButton(
|
||||
@@ -1202,33 +1203,10 @@ class MainWindow(ctk.CTk):
|
||||
self.destroy()
|
||||
|
||||
def _read_serial(self) -> None:
|
||||
"""Read and display serial output."""
|
||||
"""Open the UART Monitor window."""
|
||||
port = self._port_var.get()
|
||||
if not port:
|
||||
self._log_message("No serial port selected")
|
||||
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}")
|
||||
baudrate = self._config.serial_baudrate if self._config else 115200
|
||||
UartMonitorWindow(self, port=port, baudrate=baudrate)
|
||||
|
||||
def _test_serial_version(self) -> None:
|
||||
"""Test serial port by sending 'ver()' command and parsing response."""
|
||||
|
||||
@@ -6,9 +6,13 @@ consistent styling from gui/styles.py.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
|
||||
import customtkinter as ctk
|
||||
from typing import Callable
|
||||
|
||||
from serial_monitor import UartMonitor
|
||||
|
||||
from gui.styles import (
|
||||
FONT_BODY,
|
||||
FONT_MONO,
|
||||
@@ -761,3 +765,269 @@ class SubStepFrame(ctk.CTkFrame):
|
||||
self._toggle()
|
||||
self._toggle() # collapse then expand to reset
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user