diff --git a/src/gui/widgets.py b/src/gui/widgets.py index 420807b..e624a3c 100644 --- a/src/gui/widgets.py +++ b/src/gui/widgets.py @@ -1075,7 +1075,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 normalize_port + ser = serial.Serial(normalize_port(self._port), self._baudrate, timeout=1) ser.close() self.after(0, self._on_open_success) except Exception as e: diff --git a/src/reboot_manager.py b/src/reboot_manager.py index d30e3a2..df90749 100644 --- a/src/reboot_manager.py +++ b/src/reboot_manager.py @@ -125,9 +125,10 @@ def reboot_via_serial( try: import serial + from serial_monitor import normalize_port with serial.Serial( - config.serial_port, + normalize_port(config.serial_port), config.serial_baudrate, timeout=5, ) as ser: diff --git a/src/serial_monitor.py b/src/serial_monitor.py index 2636b7b..7982dbd 100644 --- a/src/serial_monitor.py +++ b/src/serial_monitor.py @@ -7,8 +7,10 @@ Provides UartMonitor for continuous background monitoring. from __future__ import annotations +import platform import re import threading +import time from dataclasses import dataclass from typing import Callable @@ -16,6 +18,26 @@ import serial import serial.tools.list_ports +def normalize_port(port: str) -> str: + """Normalize a serial port name for the current platform. + + On Windows, COM ports numbered >= 10 require the NT namespace + prefix (\\\\.\\COM11). Without it, CreateFile fails with + 'Access denied' or 'file not found'. + + Args: + port: Raw port name from config or detection. + + Returns: + Normalized port name suitable for serial.Serial(). + """ + if platform.system() == "Windows": + match = re.match(r"^COM(\d+)$", port, re.IGNORECASE) + if match and int(match.group(1)) >= 10: + return rf"\\.\{port}" + return port + + @dataclass class BootInfo: """Parsed information extracted from Zynq boot output.""" @@ -168,7 +190,7 @@ def check_uart_available( # 3. Try to open and read try: - with serial.Serial(port, baudrate, timeout=timeout) as ser: + with serial.Serial(normalize_port(port), baudrate, timeout=timeout) as ser: ser.reset_input_buffer() lines: list[str] = [] while ser.in_waiting: @@ -206,7 +228,7 @@ def read_serial_stream( serial.SerialException: If the port cannot be opened. """ lines: list[str] = [] - with serial.Serial(port, baudrate, timeout=timeout) as ser: + with serial.Serial(normalize_port(port), baudrate, timeout=timeout) as ser: # Clear any pending data ser.reset_input_buffer() while ser.in_waiting: @@ -242,8 +264,7 @@ def test_serial_version( import serial try: - with serial.Serial(port, baudrate, timeout=timeout) as ser: - # Clear buffers + with serial.Serial(normalize_port(port), baudrate, timeout=timeout) as ser: ser.reset_input_buffer() ser.reset_output_buffer() @@ -384,7 +405,7 @@ class UartMonitor: # Quick port check before starting thread try: - test_ser = serial.Serial(self._port, self._baudrate, timeout=1) + test_ser = serial.Serial(normalize_port(self._port), self._baudrate, timeout=1) test_ser.close() except serial.SerialException as e: if self.on_error: @@ -411,7 +432,7 @@ class UartMonitor: """Main read loop running in background thread.""" try: ser = serial.Serial( - self._port, + normalize_port(self._port), self._baudrate, timeout=self._timeout, ) diff --git a/src/zynq_checker.py b/src/zynq_checker.py index d19173e..c07147d 100644 --- a/src/zynq_checker.py +++ b/src/zynq_checker.py @@ -8,17 +8,14 @@ recognition on the JTAG chain. from __future__ import annotations -import os import re import subprocess -import tempfile import time from dataclasses import dataclass, field -from pathlib import Path from typing import Callable from config_manager import Config -from vitis_checker import get_vivado_path, get_hw_server_path, build_tool_command +from vitis_checker import get_xsct_path, get_hw_server_path, build_tool_command @dataclass @@ -102,35 +99,32 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe hw_proc: subprocess.Popen | None = None try: - # Start hw_server in the background (it daemonises own I/O) + # Start hw_server in the background (needed for xsct connect) hw_proc = subprocess.Popen( build_tool_command(hw_server_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - # Give hw_server a moment to bind its port - time.sleep(2) + time.sleep(2) # let hw_server bind its port - # ── 2. Find vivado ──────────────────────────────────── - vivado_path = get_vivado_path(xilinx_root=xilinx_root) - if not vivado_path: + # ── 2. Find xsct (much lighter than Vivado for JTAG ops) ── + xsct_path = get_xsct_path(xilinx_root=xilinx_root) + if not xsct_path: return ZynqCheckResult( devices=[], jtag_chain=[], is_present=False, ps_detected=False, pl_detected=False, hw_server_url=hw_server_url, - error="Vivado not found", + error="xsct not found", ) - # ── 3. Create and run JTAG query TCL ────────────────── + # ── 3. Run JTAG query TCL via xsct ──────────────────── tcl_content = _build_jtag_query_tcl() - tcl_path = Path(tempfile.gettempdir()) / f"zynq_jtag_check_{os.getpid()}.tcl" - tcl_path.write_text(tcl_content) if callback: callback("progress", "Scanning JTAG chain...") cmd = build_tool_command( - vivado_path, "-mode", "batch", "-source", str(tcl_path), + xsct_path, "-eval", tcl_content, ) python_timeout = config.step_timeouts.get("check_env", 120) result = subprocess.run( @@ -139,7 +133,6 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe text=True, timeout=python_timeout, ) - tcl_path.unlink(missing_ok=True) output = result.stdout + result.stderr jtag_chain = _parse_jtag_chain(output) @@ -193,23 +186,25 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe def _build_jtag_query_tcl() -> str: - """Build TCL script for JTAG device query. + """Build TCL script for xsct JTAG device query. + + Uses xsct (not Vivado) because: + - xsct is much lighter (no GUI infrastructure) + - xsct JTAG reboot already works on this system + - connect + targets lists all devices on the JTAG chain Returns: - TCL script content as string. + TCL script content as string (semicolon-delimited for -eval mode). """ - return """ -open_hw_manager -connect_hw_server -open_hw_target -puts "===JTAG_START===" -foreach dev [get_hw_devices] { - set name [get_property NAME $dev] - puts "DEVICE:$name" -} -puts "===JTAG_END===" -quit -""" + return ( + "connect; " + 'puts "===JTAG_START==="; ' + "set all [targets]; " + 'foreach t $all { puts "DEVICE:$t" }; ' + 'puts "===JTAG_END==="; ' + "disconnect; " + "quit" + ) def _parse_jtag_chain(output: str) -> list[JtagDevice]: