✨ 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_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
@@ -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."""
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user