Compare commits
35
Commits
21acc16c8c
..
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0f7219d30 | ||
|
|
4da03e97d8 | ||
|
|
c9eae220f0 | ||
|
|
e09f16a3aa | ||
|
|
47ea614ba2 | ||
|
|
13ed0d89f8 | ||
|
|
0d66f65ef2 | ||
|
|
4c01acdf69 | ||
|
|
c0cac5b2c5 | ||
|
|
5ed195e2d6 | ||
|
|
639f2e3ce6 | ||
|
|
9502bba636 | ||
|
|
9238cbc63e | ||
|
|
f330f8c276 | ||
|
|
04ad61e694 | ||
|
|
4ca15c1190 | ||
|
|
4453fc8e12 | ||
|
|
799da5b2f3 | ||
|
|
7e8427d522 | ||
|
|
66c04da5e0 | ||
|
|
954aed7c0d | ||
|
|
474e3f122b | ||
|
|
dd35ab0846 | ||
|
|
86d65641cc | ||
|
|
7716171aeb | ||
|
|
91417f609e | ||
|
|
72d0eceb8d | ||
|
|
1619dc194f | ||
|
|
267cae93b6 | ||
|
|
17c8f2d68b | ||
|
|
61f0e6cd2b | ||
|
|
6d686964e2 | ||
|
|
da94cc6cdc | ||
|
|
70c88815cc | ||
|
|
32b201cf8e |
+15
-1
@@ -33,8 +33,22 @@ htmlcov/
|
|||||||
# ── Opencode (internal tooling) ──────────────────────────────
|
# ── Opencode (internal tooling) ──────────────────────────────
|
||||||
.opencode/
|
.opencode/
|
||||||
|
|
||||||
# ── Config overrides (user-specific) ─────────────────────────
|
# ── Config (user-specific, auto-generated if missing) ─────────
|
||||||
|
config.yaml
|
||||||
config/zynq_flasher.yaml
|
config/zynq_flasher.yaml
|
||||||
|
|
||||||
# ── Logs ─────────────────────────────────────────────────────
|
# ── Logs ─────────────────────────────────────────────────────
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# ── Vivado / Xilinx temp files ────────────────────────────────
|
||||||
|
vivado.jou
|
||||||
|
vivado.log
|
||||||
|
vivado_*.backup.jou
|
||||||
|
vivado_*.backup.log
|
||||||
|
vivado_*.str
|
||||||
|
hs_err_pid*.log
|
||||||
|
webtalk*.jou
|
||||||
|
webtalk*.log
|
||||||
|
usage_statistics_*
|
||||||
|
xicom_zynq_bin*
|
||||||
|
*.pcapng
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
"""PyInstaller spec for Zynq Flasher — Windows x64 single-exe build.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
pyinstaller --clean --noconfirm ZynqFlasher.spec
|
||||||
|
→ output: dist/ZynqFlasher.exe
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['app.py'],
|
||||||
|
pathex=['src'],
|
||||||
|
binaries=[],
|
||||||
|
datas=[
|
||||||
|
# CustomTkinter themes, fonts, icons — auto-collected
|
||||||
|
*collect_data_files('customtkinter'),
|
||||||
|
# Default config shipped as template
|
||||||
|
('config/default_config.yaml', 'config'),
|
||||||
|
],
|
||||||
|
hiddenimports=[
|
||||||
|
*collect_submodules('customtkinter'),
|
||||||
|
'darkdetect',
|
||||||
|
'yaml',
|
||||||
|
'serial',
|
||||||
|
'serial.tools',
|
||||||
|
'serial.tools.list_ports',
|
||||||
|
],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher,
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='ZynqFlasher',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True, # show console for terminal log output
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo Zynq Flasher — Windows x64 Build Script
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM ── 1. Check Python ─────────────────────────────────────────
|
||||||
|
python --version >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [ERROR] Python not found. Install Python 3.9+ first.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo [1/4] Python: OK
|
||||||
|
|
||||||
|
REM ── 2. Install dependencies ─────────────────────────────────
|
||||||
|
echo [2/4] Installing Python packages...
|
||||||
|
pip install -r requirements.txt --quiet
|
||||||
|
pip install pyinstaller --quiet
|
||||||
|
echo Done.
|
||||||
|
|
||||||
|
REM ── 3. Clean previous builds ────────────────────────────────
|
||||||
|
echo [3/4] Cleaning previous builds...
|
||||||
|
if exist build rmdir /s /q build
|
||||||
|
if exist dist\ZynqFlasher.exe del /q dist\ZynqFlasher.exe
|
||||||
|
if not exist dist mkdir dist
|
||||||
|
|
||||||
|
REM ── 4. Build ────────────────────────────────────────────────
|
||||||
|
echo [4/4] Building ZynqFlasher.exe...
|
||||||
|
pyinstaller --clean --noconfirm ZynqFlasher.spec
|
||||||
|
|
||||||
|
echo.
|
||||||
|
if exist dist\ZynqFlasher.exe (
|
||||||
|
echo ============================================================
|
||||||
|
echo Build successful!
|
||||||
|
echo Output: dist\ZynqFlasher.exe
|
||||||
|
echo ============================================================
|
||||||
|
) else (
|
||||||
|
echo [ERROR] Build failed — dist\ZynqFlasher.exe not found.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
endlocal
|
||||||
|
pause
|
||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
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/tftp_app/tftp.elf
|
|
||||||
erase_all: false
|
|
||||||
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
|
|
||||||
fsbl_elf_path: /home/ly0kos/work/Verify/FPGA/Xilinx/Z100/fsbl_qspi_bypass.elf
|
|
||||||
inter_step_delay: 2
|
|
||||||
ping_count: 3
|
|
||||||
ping_timeout: 5
|
|
||||||
reboot_timeout: 30
|
|
||||||
serial_baudrate: 115200
|
|
||||||
serial_port: /dev/ttyUSB0
|
|
||||||
step_timeouts:
|
|
||||||
bitstream: 60
|
|
||||||
check_env: 30
|
|
||||||
elf_download: 60
|
|
||||||
flash_erase: 120
|
|
||||||
flash_program: 600
|
|
||||||
tftp_reboot: 120
|
|
||||||
tftp_upload_name: z7bin
|
|
||||||
uart_delay: 3
|
|
||||||
boot_wait_delay: 10
|
|
||||||
xilinx_path: /data/xilinx
|
|
||||||
zynq_ip: 192.168.100.11
|
|
||||||
+22
-44
@@ -11,14 +11,13 @@ Compatible with Xilinx 2018.3 — 2023.2.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from config_manager import Config
|
from config_manager import Config
|
||||||
from vitis_checker import get_xsct_path
|
from vitis_checker import get_xsct_path, get_vivado_path
|
||||||
|
|
||||||
# ── Types ───────────────────────────────────────────────────────────
|
# ── Types ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -38,33 +37,13 @@ ProgressCallback = Callable[[str, str], None] | None
|
|||||||
# ── Helpers ─────────────────────────────────────────────────────────
|
# ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def _get_vivado_path(vitis_path: str) -> str | None:
|
def _tcl_path(path: str | Path) -> str:
|
||||||
"""Find the Vivado executable.
|
"""Convert a filesystem path to TCL-safe format.
|
||||||
|
|
||||||
Uses shutil.which() for PATH resolution, then common install paths.
|
TCL interprets backslashes as escape sequences (\\f, \\t, \\n).
|
||||||
|
Forward slashes work on all platforms including Windows.
|
||||||
Args:
|
|
||||||
vitis_path: Optional Vitis installation directory.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to vivado executable, or None.
|
|
||||||
"""
|
"""
|
||||||
found = shutil.which("vivado")
|
return str(path).replace("\\", "/")
|
||||||
if found:
|
|
||||||
return found
|
|
||||||
if vitis_path:
|
|
||||||
candidate = Path(vitis_path) / "bin" / "vivado"
|
|
||||||
if candidate.is_file():
|
|
||||||
return str(candidate)
|
|
||||||
common = [
|
|
||||||
"/data/xilinx/Vivado/2023.2/bin/vivado",
|
|
||||||
"/opt/xilinx/bin/vivado",
|
|
||||||
os.path.expanduser("~/Xilinx/Vivado/2023.2/bin/vivado"),
|
|
||||||
]
|
|
||||||
for p in common:
|
|
||||||
if os.path.isfile(p):
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _find_ps7_init_tcl(config: Config) -> str:
|
def _find_ps7_init_tcl(config: Config) -> str:
|
||||||
@@ -167,16 +146,15 @@ def program_bitstream(
|
|||||||
|
|
||||||
# Try xsct first (preferred)
|
# Try xsct first (preferred)
|
||||||
xsct = get_xsct_path(
|
xsct = get_xsct_path(
|
||||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
xilinx_root=getattr(config, "xilinx_path", ""),
|
||||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
|
||||||
)
|
)
|
||||||
if xsct:
|
if xsct:
|
||||||
timeout = config.step_timeouts.get("bitstream", 60)
|
timeout = config.step_timeouts.get("bitstream", 60)
|
||||||
return _program_with_xsct(xsct, bit_path, callback, ps7_tcl, timeout)
|
return _program_with_xsct(xsct, bit_path, callback, ps7_tcl, timeout)
|
||||||
|
|
||||||
# Fallback to Vivado
|
# Fallback to Vivado
|
||||||
vivado = _get_vivado_path(
|
vivado = get_vivado_path(
|
||||||
config.vitis_path if hasattr(config, 'vitis_path') else ""
|
xilinx_root=getattr(config, "xilinx_path", ""),
|
||||||
)
|
)
|
||||||
if vivado:
|
if vivado:
|
||||||
return _program_with_vivado(vivado, bit_path, callback)
|
return _program_with_vivado(vivado, bit_path, callback)
|
||||||
@@ -218,11 +196,11 @@ def _program_with_xsct(
|
|||||||
lines = ["connect"]
|
lines = ["connect"]
|
||||||
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
||||||
lines.append("targets 1")
|
lines.append("targets 1")
|
||||||
lines.append(f'source "{ps7_init_tcl}"')
|
lines.append(f'source "{_tcl_path(ps7_init_tcl)}"')
|
||||||
lines.append("ps7_init")
|
lines.append("ps7_init")
|
||||||
if callback:
|
if callback:
|
||||||
callback("progress", "PS initialized (clocks, DDR, MIO)")
|
callback("progress", "PS initialized (clocks, DDR, MIO)")
|
||||||
lines.append(f'fpga -file "{bit_path}"')
|
lines.append(f'fpga -file {{{_tcl_path(bit_path)}}}')
|
||||||
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
||||||
lines.append("ps7_post_config")
|
lines.append("ps7_post_config")
|
||||||
if callback:
|
if callback:
|
||||||
@@ -293,12 +271,14 @@ def _program_with_vivado(
|
|||||||
callback("progress", "Using Vivado Hardware Manager...")
|
callback("progress", "Using Vivado Hardware Manager...")
|
||||||
|
|
||||||
tcl_content = f"""
|
tcl_content = f"""
|
||||||
open_hw_manager
|
open_hw
|
||||||
connect_hw_server
|
connect_hw_server
|
||||||
open_hw_target
|
open_hw_target
|
||||||
current_hw_device [lindex [get_hw_devices] 0]
|
current_hw_device [lindex [get_hw_devices] 0]
|
||||||
program_hw_device -file "{bit_path}"
|
program_hw_device -file "{bit_path}"
|
||||||
refresh_hw_device
|
refresh_hw_device
|
||||||
|
close_hw
|
||||||
|
disconnect_hw_server
|
||||||
quit
|
quit
|
||||||
"""
|
"""
|
||||||
tcl_path = bit_path.parent / "temp_program.tcl"
|
tcl_path = bit_path.parent / "temp_program.tcl"
|
||||||
@@ -364,8 +344,7 @@ def run_elf(
|
|||||||
callback("start", f"Running ELF: {elf_path.name}...")
|
callback("start", f"Running ELF: {elf_path.name}...")
|
||||||
|
|
||||||
xsct = get_xsct_path(
|
xsct = get_xsct_path(
|
||||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
xilinx_root=getattr(config, "xilinx_path", ""),
|
||||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
|
||||||
)
|
)
|
||||||
if not xsct:
|
if not xsct:
|
||||||
return BitstreamResult(
|
return BitstreamResult(
|
||||||
@@ -413,13 +392,13 @@ def _run_elf_via_fsbl(
|
|||||||
tcl = "\n".join([
|
tcl = "\n".join([
|
||||||
"connect",
|
"connect",
|
||||||
"targets 2",
|
"targets 2",
|
||||||
"dow \"%s\"" % fsbl_path,
|
"dow {%s}" % _tcl_path(fsbl_path),
|
||||||
"puts \"FSBL downloaded\"",
|
"puts \"FSBL downloaded\"",
|
||||||
"con",
|
"con",
|
||||||
"after 3000",
|
"after 3000",
|
||||||
"stop",
|
"stop",
|
||||||
"puts \"FSBL done, CPU halted\"",
|
"puts \"FSBL done, CPU halted\"",
|
||||||
"dow \"%s\"" % elf_path,
|
"dow {%s}" % _tcl_path(elf_path),
|
||||||
"mwr 0xF800025C 0x00000001",
|
"mwr 0xF800025C 0x00000001",
|
||||||
"con",
|
"con",
|
||||||
"puts \"App running\"",
|
"puts \"App running\"",
|
||||||
@@ -493,7 +472,7 @@ def _run_elf_direct(
|
|||||||
tcl = "\n".join([
|
tcl = "\n".join([
|
||||||
"connect",
|
"connect",
|
||||||
"targets 2",
|
"targets 2",
|
||||||
"dow \"%s\"" % elf_path,
|
"dow {%s}" % _tcl_path(elf_path),
|
||||||
"mwr 0xF800025C 0x00000001",
|
"mwr 0xF800025C 0x00000001",
|
||||||
"con",
|
"con",
|
||||||
"exit",
|
"exit",
|
||||||
@@ -579,8 +558,7 @@ def full_bitstream_program(
|
|||||||
message=f"FSBL not found: {fsbl_path or '(not configured)'}")]
|
message=f"FSBL not found: {fsbl_path or '(not configured)'}")]
|
||||||
|
|
||||||
xsct = get_xsct_path(
|
xsct = get_xsct_path(
|
||||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
xilinx_root=getattr(config, "xilinx_path", ""),
|
||||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
|
||||||
)
|
)
|
||||||
if not xsct:
|
if not xsct:
|
||||||
return [BitstreamResult(step="bitstream", success=False,
|
return [BitstreamResult(step="bitstream", success=False,
|
||||||
@@ -653,18 +631,18 @@ def full_bitstream_program(
|
|||||||
"rst -processor",
|
"rst -processor",
|
||||||
"after 500",
|
"after 500",
|
||||||
'puts "INFO: Downloading FSBL..."',
|
'puts "INFO: Downloading FSBL..."',
|
||||||
'dow "%s"' % fsbl_path,
|
'dow {%s}' % _tcl_path(fsbl_path),
|
||||||
'puts "INFO: Running FSBL for hardware initialization..."',
|
'puts "INFO: Running FSBL for hardware initialization..."',
|
||||||
"con",
|
"con",
|
||||||
"after 2000",
|
"after 2000",
|
||||||
"stop",
|
"stop",
|
||||||
'puts "INFO: Hardware initialization completed."',
|
'puts "INFO: Hardware initialization completed."',
|
||||||
'puts "INFO: Downloading Bitstream..."',
|
'puts "INFO: Downloading Bitstream..."',
|
||||||
'fpga -f "%s"' % bit_path,
|
'fpga -f {%s}' % _tcl_path(bit_path),
|
||||||
"after 1000",
|
"after 1000",
|
||||||
'targets -set -nocase -filter {name =~ "arm*#0"}',
|
'targets -set -nocase -filter {name =~ "arm*#0"}',
|
||||||
'puts "INFO: Downloading Application ELF..."',
|
'puts "INFO: Downloading Application ELF..."',
|
||||||
'dow "%s"' % elf_path,
|
'dow {%s}' % _tcl_path(elf_path),
|
||||||
'puts "INFO: Executing Application..."',
|
'puts "INFO: Executing Application..."',
|
||||||
"con",
|
"con",
|
||||||
'puts "SUCCESS: Zynq target is running the specified application."',
|
'puts "SUCCESS: Zynq target is running the specified application."',
|
||||||
|
|||||||
+42
-5
@@ -17,6 +17,7 @@ import yaml
|
|||||||
DEFAULT_CONFIG: dict[str, Any] = {
|
DEFAULT_CONFIG: dict[str, Any] = {
|
||||||
"xilinx_path": "",
|
"xilinx_path": "",
|
||||||
"zynq_ip": "192.168.100.11",
|
"zynq_ip": "192.168.100.11",
|
||||||
|
"zynq_part": "XC7Z100",
|
||||||
"tftp_upload_name": "z7bin",
|
"tftp_upload_name": "z7bin",
|
||||||
"serial_port": "",
|
"serial_port": "",
|
||||||
"serial_baudrate": 115200,
|
"serial_baudrate": 115200,
|
||||||
@@ -27,15 +28,16 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||||||
"flash_type": "qspi-x4-single",
|
"flash_type": "qspi-x4-single",
|
||||||
"flash_model": "s25fl256s1",
|
"flash_model": "s25fl256s1",
|
||||||
"erase_all": False,
|
"erase_all": False,
|
||||||
|
"download_verify": True,
|
||||||
"firmware_bin_path": "",
|
"firmware_bin_path": "",
|
||||||
"reboot_timeout": 30,
|
"reboot_timeout": 30,
|
||||||
"ping_timeout": 5,
|
"ping_timeout": 5,
|
||||||
"ping_count": 3,
|
"ping_count": 3,
|
||||||
"uart_delay": 3,
|
"uart_delay": 3,
|
||||||
"inter_step_delay": 2,
|
"inter_step_delay": 2,
|
||||||
"boot_wait_delay": 10,
|
"boot_wait_delay": 30,
|
||||||
"step_timeouts": {
|
"step_timeouts": {
|
||||||
"check_env": 30,
|
"check_env": 120,
|
||||||
"flash_erase": 120,
|
"flash_erase": 120,
|
||||||
"flash_program": 600,
|
"flash_program": 600,
|
||||||
"bitstream": 60,
|
"bitstream": 60,
|
||||||
@@ -55,6 +57,7 @@ class Config:
|
|||||||
|
|
||||||
xilinx_path: str = ""
|
xilinx_path: str = ""
|
||||||
zynq_ip: str = "192.168.100.11"
|
zynq_ip: str = "192.168.100.11"
|
||||||
|
zynq_part: str = "XC7Z100"
|
||||||
tftp_upload_name: str = "z7bin"
|
tftp_upload_name: str = "z7bin"
|
||||||
serial_port: str = ""
|
serial_port: str = ""
|
||||||
serial_baudrate: int = 115200
|
serial_baudrate: int = 115200
|
||||||
@@ -65,13 +68,14 @@ class Config:
|
|||||||
flash_type: str = "qspi-x4-single"
|
flash_type: str = "qspi-x4-single"
|
||||||
flash_model: str = "s25fl256s1"
|
flash_model: str = "s25fl256s1"
|
||||||
erase_all: bool = False
|
erase_all: bool = False
|
||||||
|
download_verify: bool = True
|
||||||
firmware_bin_path: str = ""
|
firmware_bin_path: str = ""
|
||||||
reboot_timeout: int = 30
|
reboot_timeout: int = 30
|
||||||
ping_timeout: int = 5
|
ping_timeout: int = 5
|
||||||
ping_count: int = 3
|
ping_count: int = 3
|
||||||
uart_delay: int = 3
|
uart_delay: int = 3
|
||||||
inter_step_delay: int = 2
|
inter_step_delay: int = 2
|
||||||
boot_wait_delay: int = 10
|
boot_wait_delay: int = 30
|
||||||
step_timeouts: dict[str, int] = field(default_factory=lambda: {})
|
step_timeouts: dict[str, int] = field(default_factory=lambda: {})
|
||||||
|
|
||||||
# Internal: path to the config file on disk
|
# Internal: path to the config file on disk
|
||||||
@@ -174,6 +178,7 @@ class Config:
|
|||||||
"flash_type": self.flash_type,
|
"flash_type": self.flash_type,
|
||||||
"flash_model": self.flash_model,
|
"flash_model": self.flash_model,
|
||||||
"erase_all": self.erase_all,
|
"erase_all": self.erase_all,
|
||||||
|
"download_verify": self.download_verify,
|
||||||
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
||||||
"reboot_timeout": self.reboot_timeout,
|
"reboot_timeout": self.reboot_timeout,
|
||||||
"ping_timeout": self.ping_timeout,
|
"ping_timeout": self.ping_timeout,
|
||||||
@@ -188,8 +193,11 @@ class Config:
|
|||||||
def _relative_path(self, path: str) -> str:
|
def _relative_path(self, path: str) -> str:
|
||||||
"""Convert an absolute path to a relative path from the config file.
|
"""Convert an absolute path to a relative path from the config file.
|
||||||
|
|
||||||
|
Uses :func:`os.path.relpath` which works across filesystem
|
||||||
|
boundaries (unlike :meth:`Path.relative_to`).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Absolute path string.
|
path: Absolute or relative path string.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Relative path string, or the original if conversion fails.
|
Relative path string, or the original if conversion fails.
|
||||||
@@ -199,7 +207,8 @@ class Config:
|
|||||||
try:
|
try:
|
||||||
target = Path(path)
|
target = Path(path)
|
||||||
if target.is_absolute():
|
if target.is_absolute():
|
||||||
return str(target.relative_to(self._config_path.parent))
|
base = str(self._config_path.parent)
|
||||||
|
return os.path.relpath(str(target), base)
|
||||||
except (ValueError, OSError):
|
except (ValueError, OSError):
|
||||||
pass
|
pass
|
||||||
return path
|
return path
|
||||||
@@ -217,6 +226,34 @@ class Config:
|
|||||||
return Path()
|
return Path()
|
||||||
return (self._config_path.parent / relative_path).resolve()
|
return (self._config_path.parent / relative_path).resolve()
|
||||||
|
|
||||||
|
def validate_paths(self) -> list[str]:
|
||||||
|
"""Validate file paths in config and clear those that don't exist.
|
||||||
|
|
||||||
|
Checks each file path field, resolves it against the config
|
||||||
|
directory, and clears it if the target file does not exist
|
||||||
|
on the current filesystem.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of field names that were cleared.
|
||||||
|
"""
|
||||||
|
cleared: list[str] = []
|
||||||
|
path_fields = [
|
||||||
|
"bootloader_bit_path",
|
||||||
|
"bootloader_elf_path",
|
||||||
|
"bootloader_bin_path",
|
||||||
|
"fsbl_elf_path",
|
||||||
|
"firmware_bin_path",
|
||||||
|
]
|
||||||
|
for attr in path_fields:
|
||||||
|
val = getattr(self, attr, "")
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
resolved = self.resolve_path(val)
|
||||||
|
if not resolved.is_file():
|
||||||
|
setattr(self, attr, "")
|
||||||
|
cleared.append(attr)
|
||||||
|
return cleared
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_path(self) -> Path | None:
|
def config_path(self) -> Path | None:
|
||||||
"""Return the path to the config file on disk."""
|
"""Return the path to the config file on disk."""
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""File metadata utilities — CRC32, human-readable size, timestamps."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import zlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FileInfo:
|
||||||
|
"""Computed metadata for a single file."""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
exists: bool = False
|
||||||
|
size: int = 0
|
||||||
|
crc32: int = 0
|
||||||
|
mtime: float = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size_human(self) -> str:
|
||||||
|
"""Human-readable file size (KiB / MiB / GiB)."""
|
||||||
|
if self.size >= 1_073_741_824:
|
||||||
|
return f"{self.size / 1_073_741_824:.2f} GiB"
|
||||||
|
if self.size >= 1_048_576:
|
||||||
|
return f"{self.size / 1_048_576:.2f} MiB"
|
||||||
|
if self.size >= 1_024:
|
||||||
|
return f"{self.size / 1_024:.2f} KiB"
|
||||||
|
return f"{self.size} B"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mtime_iso(self) -> str:
|
||||||
|
"""ISO-formatted modification time, or empty string."""
|
||||||
|
if not self.mtime:
|
||||||
|
return ""
|
||||||
|
return datetime.datetime.fromtimestamp(self.mtime).strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def crc32_hex(self) -> str:
|
||||||
|
"""8-character hex CRC32 representation."""
|
||||||
|
return f"{self.crc32 & 0xFFFFFFFF:08X}"
|
||||||
|
|
||||||
|
|
||||||
|
def compute_file_info(file_path: str | Path) -> FileInfo:
|
||||||
|
"""Compute size, CRC32, and modification time for a file.
|
||||||
|
|
||||||
|
If the file does not exist or cannot be read, the returned
|
||||||
|
``FileInfo`` will have ``exists=False`` and all numeric fields
|
||||||
|
set to zero.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Absolute or relative path to the file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FileInfo dataclass with computed metadata.
|
||||||
|
"""
|
||||||
|
path = Path(file_path)
|
||||||
|
info = FileInfo(path=str(path))
|
||||||
|
if not path.is_file():
|
||||||
|
return info
|
||||||
|
info.exists = True
|
||||||
|
try:
|
||||||
|
stat = path.stat()
|
||||||
|
info.size = stat.st_size
|
||||||
|
info.mtime = stat.st_mtime
|
||||||
|
except OSError:
|
||||||
|
return info
|
||||||
|
|
||||||
|
# CRC32
|
||||||
|
try:
|
||||||
|
value = 0
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
while chunk := f.read(1_048_576): # 1 MiB chunks
|
||||||
|
value = zlib.crc32(chunk, value)
|
||||||
|
info.crc32 = value
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return info
|
||||||
+62
-12
@@ -10,6 +10,7 @@ Compatible with Xilinx 2018.3 — 2023.2.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -35,6 +36,27 @@ ProgressCallback = Callable[[str, str], None] | None
|
|||||||
|
|
||||||
# ── Helpers ─────────────────────────────────────────────────────────
|
# ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Keywords in program_flash output that indicate failure even with rc=0
|
||||||
|
_FAIL_KEYWORDS = [
|
||||||
|
"failed", "cannot", "abort",
|
||||||
|
"invalid", "unsupported", "timeout",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _output_has_error(output: str) -> str:
|
||||||
|
"""Check combined stdout+stderr for known failure keywords.
|
||||||
|
|
||||||
|
Returns the first matching line, or empty string if clean.
|
||||||
|
"""
|
||||||
|
lowered = output.lower()
|
||||||
|
for kw in _FAIL_KEYWORDS:
|
||||||
|
if kw in lowered:
|
||||||
|
# Find the actual line
|
||||||
|
for line in output.splitlines():
|
||||||
|
if kw in line.lower():
|
||||||
|
return line.strip()[:200]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _run_command(
|
def _run_command(
|
||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
@@ -113,8 +135,7 @@ def wipe_flash(
|
|||||||
FlashResult with status.
|
FlashResult with status.
|
||||||
"""
|
"""
|
||||||
flash_tool = get_program_flash_path(
|
flash_tool = get_program_flash_path(
|
||||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
xilinx_root=getattr(config, "xilinx_path", ""),
|
||||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
|
||||||
)
|
)
|
||||||
if not flash_tool:
|
if not flash_tool:
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
@@ -168,12 +189,27 @@ def wipe_flash(
|
|||||||
callback=callback,
|
callback=callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
success = result.returncode == 0
|
combined = result.stdout + result.stderr
|
||||||
|
err_line = _output_has_error(combined)
|
||||||
|
if err_line:
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
step="wipe",
|
step="wipe",
|
||||||
success=success,
|
success=False,
|
||||||
message="Flash erased successfully" if success else "Flash erase failed",
|
message=f"Flash erase failed: {err_line}",
|
||||||
output=(result.stdout + result.stderr)[:4000],
|
output=combined[:4000],
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return FlashResult(
|
||||||
|
step="wipe",
|
||||||
|
success=False,
|
||||||
|
message=f"Flash erase failed (exit {result.returncode})",
|
||||||
|
output=combined[:4000],
|
||||||
|
)
|
||||||
|
return FlashResult(
|
||||||
|
step="wipe",
|
||||||
|
success=True,
|
||||||
|
message="Flash erased successfully",
|
||||||
|
output=combined[:4000],
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
return FlashResult(step="wipe", success=False, message="Flash erase timed out")
|
return FlashResult(step="wipe", success=False, message="Flash erase timed out")
|
||||||
@@ -229,8 +265,7 @@ def program_flash_bin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
flash_tool = get_program_flash_path(
|
flash_tool = get_program_flash_path(
|
||||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
xilinx_root=getattr(config, "xilinx_path", ""),
|
||||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
|
||||||
)
|
)
|
||||||
if not flash_tool:
|
if not flash_tool:
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
@@ -254,12 +289,27 @@ def program_flash_bin(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = _run_command(cmd, timeout=config.step_timeouts.get("flash_program", 600), callback=callback)
|
result = _run_command(cmd, timeout=config.step_timeouts.get("flash_program", 600), callback=callback)
|
||||||
success = result.returncode == 0
|
combined = result.stdout + result.stderr
|
||||||
|
err_line = _output_has_error(combined)
|
||||||
|
if err_line:
|
||||||
return FlashResult(
|
return FlashResult(
|
||||||
step="program",
|
step="program",
|
||||||
success=success,
|
success=False,
|
||||||
message="Flash programmed successfully" if success else "Flash programming failed",
|
message=f"Flash programming failed: {err_line}",
|
||||||
output=(result.stdout + result.stderr)[:4000],
|
output=combined[:4000],
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return FlashResult(
|
||||||
|
step="program",
|
||||||
|
success=False,
|
||||||
|
message=f"Flash programming failed (exit {result.returncode})",
|
||||||
|
output=combined[:4000],
|
||||||
|
)
|
||||||
|
return FlashResult(
|
||||||
|
step="program",
|
||||||
|
success=True,
|
||||||
|
message="Flash programmed successfully",
|
||||||
|
output=combined[:4000],
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
return FlashResult(step="program", success=False, message="Flash programming timed out (600s)")
|
return FlashResult(step="program", success=False, message="Flash programming timed out (600s)")
|
||||||
|
|||||||
+554
-65
@@ -7,6 +7,7 @@ built with CustomTkinter.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
@@ -25,6 +26,7 @@ from tftp_manager import tftp_upload, tftp_download, tftp_upload_verify, TftpRes
|
|||||||
from reboot_manager import reboot_zynq, RebootResult
|
from reboot_manager import reboot_zynq, RebootResult
|
||||||
from boot_verifier import verify_boot, verify_zynq_status, BootVerificationResult
|
from boot_verifier import verify_boot, verify_zynq_status, BootVerificationResult
|
||||||
from serial_monitor import detect_serial_ports, parse_boot_output, check_uart_available, UartMonitor
|
from serial_monitor import detect_serial_ports, parse_boot_output, check_uart_available, UartMonitor
|
||||||
|
from file_utils import compute_file_info, FileInfo
|
||||||
from gui.styles import (
|
from gui.styles import (
|
||||||
WINDOW_WIDTH,
|
WINDOW_WIDTH,
|
||||||
WINDOW_HEIGHT,
|
WINDOW_HEIGHT,
|
||||||
@@ -92,6 +94,35 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
self._load_config()
|
self._load_config()
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
|
# Validate file paths — clear any that don't exist on this machine
|
||||||
|
if self._config and self._config._config_path:
|
||||||
|
try:
|
||||||
|
cleared = self._config.validate_paths()
|
||||||
|
if cleared:
|
||||||
|
joined = ", ".join(cleared)
|
||||||
|
self._log_message(f" ⚠ Cleared stale paths from config: {joined}")
|
||||||
|
# Also clear the UI selectors so stale paths don't
|
||||||
|
# get re-saved by subsequent _auto_save_config calls
|
||||||
|
_selector_map = {
|
||||||
|
"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,
|
||||||
|
}
|
||||||
|
for attr in cleared:
|
||||||
|
sel = _selector_map.get(attr)
|
||||||
|
if sel:
|
||||||
|
sel.set_path("")
|
||||||
|
self._config.save()
|
||||||
|
self._log_message(f" ✓ Config saved after clearing stale paths")
|
||||||
|
except Exception as e:
|
||||||
|
self._log_message(f" ✗ Config validation failed: {e}")
|
||||||
|
|
||||||
|
# Log file metadata for all configured paths
|
||||||
|
self._log_all_file_info()
|
||||||
|
|
||||||
self._update_step_dependencies()
|
self._update_step_dependencies()
|
||||||
|
|
||||||
# Handle window close
|
# Handle window close
|
||||||
@@ -109,6 +140,18 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
# ── Config ─────────────────────────────────────────────────
|
# ── Config ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_app_dir() -> Path:
|
||||||
|
"""Return the application directory (where config.yaml lives).
|
||||||
|
|
||||||
|
When running as a PyInstaller bundle (frozen), uses the
|
||||||
|
directory of the .exe file. Otherwise navigates from
|
||||||
|
``__file__`` up to the project root.
|
||||||
|
"""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
return Path(sys.executable).parent
|
||||||
|
return Path(__file__).parent.parent.parent
|
||||||
|
|
||||||
def _find_config(self) -> Path | None:
|
def _find_config(self) -> Path | None:
|
||||||
"""Find the user configuration file.
|
"""Find the user configuration file.
|
||||||
|
|
||||||
@@ -120,8 +163,7 @@ class MainWindow(ctk.CTk):
|
|||||||
Returns:
|
Returns:
|
||||||
Path to user config file, or None if not found.
|
Path to user config file, or None if not found.
|
||||||
"""
|
"""
|
||||||
# Navigate from src/gui/main_window.py -> project root
|
app_dir = self._get_app_dir()
|
||||||
app_dir = Path(__file__).parent.parent.parent
|
|
||||||
|
|
||||||
# 1. Check for user config at project root
|
# 1. Check for user config at project root
|
||||||
user_config = app_dir / "config.yaml"
|
user_config = app_dir / "config.yaml"
|
||||||
@@ -144,43 +186,62 @@ class MainWindow(ctk.CTk):
|
|||||||
def _get_user_config_path(self) -> Path:
|
def _get_user_config_path(self) -> Path:
|
||||||
"""Get the path to the user config file (config.yaml).
|
"""Get the path to the user config file (config.yaml).
|
||||||
|
|
||||||
Creates config.yaml in project root if it doesn't exist.
|
Creates config.yaml in app directory if it doesn't exist.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to user config file.
|
Path to user config file.
|
||||||
"""
|
"""
|
||||||
# Navigate from src/gui/main_window.py -> project root
|
app_dir = self._get_app_dir()
|
||||||
app_dir = Path(__file__).parent.parent.parent
|
|
||||||
config_path = app_dir / "config.yaml"
|
config_path = app_dir / "config.yaml"
|
||||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
return config_path
|
return config_path
|
||||||
|
|
||||||
def _load_config(self) -> None:
|
def _load_config(self) -> None:
|
||||||
"""Load configuration from file or create default."""
|
"""Load configuration from file or create a blank one on disk.
|
||||||
|
|
||||||
|
If config.yaml does not exist or cannot be read, a new one
|
||||||
|
is created with default values and written to disk immediately.
|
||||||
|
"""
|
||||||
|
user_config_path = self._get_user_config_path()
|
||||||
if self._config_path:
|
if self._config_path:
|
||||||
try:
|
try:
|
||||||
self._config = Config.from_file(self._config_path)
|
self._config = Config.from_file(self._config_path)
|
||||||
if self._config_path.name == "default_config.yaml":
|
if self._config_path.name == "default_config.yaml":
|
||||||
user_config_path = self._get_user_config_path()
|
|
||||||
self._config._config_path = user_config_path
|
self._config._config_path = user_config_path
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
|
pass # Fall through and create default
|
||||||
|
|
||||||
|
# Config file missing or unreadable — create a blank one
|
||||||
self._config = Config.from_default()
|
self._config = Config.from_default()
|
||||||
user_config_path = self._get_user_config_path()
|
|
||||||
self._config._config_path = user_config_path
|
|
||||||
else:
|
|
||||||
self._config = Config.from_default()
|
|
||||||
user_config_path = self._get_user_config_path()
|
|
||||||
self._config._config_path = user_config_path
|
self._config._config_path = user_config_path
|
||||||
|
try:
|
||||||
|
self._config.save()
|
||||||
|
except Exception:
|
||||||
|
pass # Will be saved later by _auto_save_config
|
||||||
|
|
||||||
def _reload_config(self) -> None:
|
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()
|
user_path = self._get_user_config_path()
|
||||||
if not user_path.exists():
|
if not user_path.exists():
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
fresh = Config.from_file(user_path)
|
fresh = Config.from_file(user_path)
|
||||||
self._config = fresh
|
self._config = fresh
|
||||||
# Update UI selectors
|
# Update UI selectors — suppress callbacks to avoid
|
||||||
|
# redundant file-info logging and status-panel rewrites.
|
||||||
|
_sels = [
|
||||||
|
self._bit_selector, self._elf_selector,
|
||||||
|
self._bootloader_bin_selector, self._fsbl_selector,
|
||||||
|
self._firmware_bin_selector,
|
||||||
|
]
|
||||||
|
for s in _sels:
|
||||||
|
s._suppress_callback = True
|
||||||
for attr, selector in [
|
for attr, selector in [
|
||||||
("bootloader_bit_path", self._bit_selector),
|
("bootloader_bit_path", self._bit_selector),
|
||||||
("bootloader_elf_path", self._elf_selector),
|
("bootloader_elf_path", self._elf_selector),
|
||||||
@@ -190,7 +251,11 @@ class MainWindow(ctk.CTk):
|
|||||||
]:
|
]:
|
||||||
val = getattr(fresh, attr, "")
|
val = getattr(fresh, attr, "")
|
||||||
if val:
|
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))
|
||||||
|
for s in _sels:
|
||||||
|
s._suppress_callback = False
|
||||||
# Update IP
|
# Update IP
|
||||||
if self._ip_string_var:
|
if self._ip_string_var:
|
||||||
self._ip_string_var.set(fresh.zynq_ip)
|
self._ip_string_var.set(fresh.zynq_ip)
|
||||||
@@ -205,24 +270,64 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
This must be called before _save_config() to ensure
|
This must be called before _save_config() to ensure
|
||||||
the Config object has the latest UI values.
|
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:
|
if not self._config:
|
||||||
return
|
return
|
||||||
# Sync IP address
|
# Sync IP address (may not exist yet during _build_config_panel)
|
||||||
if self._ip_string_var:
|
if self._ip_string_var:
|
||||||
self._config.zynq_ip = self._ip_string_var.get()
|
self._config.zynq_ip = self._ip_string_var.get()
|
||||||
# Sync Xilinx Kit path
|
# Sync Xilinx Kit path
|
||||||
|
if hasattr(self, '_xilinx_path_var'):
|
||||||
self._config.xilinx_path = self._xilinx_path_var.get()
|
self._config.xilinx_path = self._xilinx_path_var.get()
|
||||||
# Sync serial port
|
# Sync serial port (may not exist yet during _build_config_panel)
|
||||||
|
if hasattr(self, '_port_var'):
|
||||||
self._config.serial_port = self._port_var.get()
|
self._config.serial_port = self._port_var.get()
|
||||||
# Sync file paths from FileSelectors
|
# Sync file paths from FileSelectors
|
||||||
files = self._get_selected_files()
|
# Always derive relative from absolute — the _relative_path
|
||||||
self._config.bootloader_bit_path = files["bootloader_bit_path"]
|
# stored in FileSelector can be stale after Browse.
|
||||||
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
for attr, selector in [
|
||||||
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
("bootloader_bit_path", self._bit_selector),
|
||||||
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
("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),
|
||||||
|
]:
|
||||||
|
abs_path = selector.get_path()
|
||||||
|
if abs_path:
|
||||||
|
setattr(self._config, attr, self._config._relative_path(abs_path))
|
||||||
|
else:
|
||||||
|
setattr(self._config, attr, "")
|
||||||
|
# Sync erase checkbox (may not exist yet)
|
||||||
|
if hasattr(self, '_erase_cb_var'):
|
||||||
self._config.erase_all = self._erase_cb_var.get()
|
self._config.erase_all = self._erase_cb_var.get()
|
||||||
self._config.firmware_bin_path = files["firmware_bin_path"]
|
# Sync download verify checkbox
|
||||||
|
if hasattr(self, '_download_verify_var'):
|
||||||
|
self._config.download_verify = self._download_verify_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:
|
def _save_config(self) -> None:
|
||||||
"""Save current configuration to user config file (config.yaml).
|
"""Save current configuration to user config file (config.yaml).
|
||||||
@@ -250,32 +355,101 @@ class MainWindow(ctk.CTk):
|
|||||||
if self._config:
|
if self._config:
|
||||||
self._config.erase_all = self._erase_cb_var.get()
|
self._config.erase_all = self._erase_cb_var.get()
|
||||||
|
|
||||||
|
def _on_download_verify_toggle(self) -> None:
|
||||||
|
"""Update config.download_verify immediately when checkbox toggles."""
|
||||||
|
if self._config:
|
||||||
|
self._config.download_verify = self._download_verify_var.get()
|
||||||
|
|
||||||
def _browse_xilinx_path(self) -> None:
|
def _browse_xilinx_path(self) -> None:
|
||||||
"""Open directory dialog to select Xilinx root folder."""
|
"""Open directory dialog to select Xilinx root folder."""
|
||||||
from tkinter import filedialog
|
from tkinter import filedialog
|
||||||
path = filedialog.askdirectory(title="Select Xilinx Root Directory")
|
path = filedialog.askdirectory(title="Select Xilinx Root Directory")
|
||||||
if path:
|
if path:
|
||||||
self._xilinx_path_var.set(path)
|
self._xilinx_path_var.set(path)
|
||||||
if self._config:
|
self._on_xilinx_path_changed()
|
||||||
self._config.xilinx_path = path
|
|
||||||
# Re-check tools with new path
|
# Re-check tools with new path
|
||||||
self._check_vitis()
|
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, auto-save, and log file info."""
|
||||||
|
from file_utils import compute_file_info
|
||||||
|
self._auto_save_config()
|
||||||
|
if path:
|
||||||
|
info = compute_file_info(path)
|
||||||
|
self._log_file_info(info)
|
||||||
|
# Update status info panel
|
||||||
|
self._log_all_file_info()
|
||||||
|
|
||||||
|
def _log_file_info(self, info: FileInfo) -> None:
|
||||||
|
"""Log file metadata in the log window.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
info: FileInfo dataclass with computed metadata.
|
||||||
|
"""
|
||||||
|
filename = os.path.basename(info.path) if info.path else "(unknown)"
|
||||||
|
if not info.exists:
|
||||||
|
self._log_message(f" ⚠ {filename}: file not found")
|
||||||
|
return
|
||||||
|
self._log_message(
|
||||||
|
f" ✓ {filename}: "
|
||||||
|
f"{info.size_human} | CRC {info.crc32_hex} | {info.mtime_iso}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _log_all_file_info(self) -> None:
|
||||||
|
"""Update file metadata in the status info panel (preserves other info).
|
||||||
|
|
||||||
|
Each file line is updated in-place via ``set_info()`` — existing
|
||||||
|
Zynq info, tool versions, etc. are left untouched.
|
||||||
|
"""
|
||||||
|
if not self._config:
|
||||||
|
return
|
||||||
|
paths = [
|
||||||
|
("FSBL ELF", self._config.fsbl_elf_path),
|
||||||
|
("Bitstream", self._config.bootloader_bit_path),
|
||||||
|
("Bootloader ELF", self._config.bootloader_elf_path),
|
||||||
|
("Bootloader BIN", self._config.bootloader_bin_path),
|
||||||
|
("Firmware BIN", self._config.firmware_bin_path),
|
||||||
|
]
|
||||||
|
for label, path in paths:
|
||||||
|
if not path:
|
||||||
|
self._status.set_info(label, "(not set)")
|
||||||
|
continue
|
||||||
|
resolved = self._config.resolve_path(path)
|
||||||
|
info = compute_file_info(resolved)
|
||||||
|
if info.exists:
|
||||||
|
self._status.set_info(
|
||||||
|
label,
|
||||||
|
f"{info.size_human} CRC {info.crc32_hex} {info.mtime_iso}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._status.set_info(label, "(not found)")
|
||||||
|
|
||||||
# ── UI Construction ────────────────────────────────────────
|
# ── UI Construction ────────────────────────────────────────
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
"""Build the complete UI layout."""
|
"""Build the complete UI layout."""
|
||||||
# Scrollable main container — scrollbar appears when content overflows
|
# Scrollable main container — scrollbar appears when content overflows
|
||||||
main_frame = ctk.CTkScrollableFrame(self)
|
self.main_frame = ctk.CTkScrollableFrame(self)
|
||||||
main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
|
self.main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
|
||||||
main_frame.grid_rowconfigure(1, weight=1)
|
self.main_frame.grid_rowconfigure(1, weight=1)
|
||||||
main_frame.grid_columnconfigure(0, weight=1)
|
self.main_frame.grid_columnconfigure(0, weight=7, uniform="main_col")
|
||||||
|
self.main_frame.grid_columnconfigure(1, weight=3, uniform="main_col")
|
||||||
|
|
||||||
# ── Header ──
|
# ── Header ──
|
||||||
self._build_header(main_frame)
|
self._build_header(self.main_frame)
|
||||||
|
|
||||||
# ── Left Panel: Workflow Steps ──
|
# ── Left Panel: Workflow Steps ──
|
||||||
left_frame = ctk.CTkFrame(main_frame)
|
left_frame = ctk.CTkFrame(self.main_frame)
|
||||||
left_frame.grid(row=1, column=0, sticky="nsew", padx=(0, PADDING))
|
left_frame.grid(row=1, column=0, sticky="nsew", padx=(0, PADDING))
|
||||||
left_frame.grid_rowconfigure(1, weight=1)
|
left_frame.grid_rowconfigure(1, weight=1)
|
||||||
left_frame.grid_columnconfigure(0, weight=1)
|
left_frame.grid_columnconfigure(0, weight=1)
|
||||||
@@ -284,9 +458,9 @@ class MainWindow(ctk.CTk):
|
|||||||
self._build_log_panel(left_frame)
|
self._build_log_panel(left_frame)
|
||||||
|
|
||||||
# ── Right Panel: Configuration ──
|
# ── Right Panel: Configuration ──
|
||||||
right_frame = ctk.CTkFrame(main_frame)
|
right_frame = ctk.CTkFrame(self.main_frame)
|
||||||
right_frame.grid(row=1, column=1, sticky="nsew", padx=(PADDING, 0))
|
right_frame.grid(row=1, column=1, sticky="nsew", padx=(PADDING, 0))
|
||||||
right_frame.grid_rowconfigure(3, weight=1)
|
right_frame.grid_rowconfigure(3, weight=3) # status panel — main info area
|
||||||
right_frame.grid_columnconfigure(0, weight=1)
|
right_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self._build_config_panel(right_frame)
|
self._build_config_panel(right_frame)
|
||||||
@@ -294,6 +468,10 @@ class MainWindow(ctk.CTk):
|
|||||||
self._build_utility_panel(right_frame)
|
self._build_utility_panel(right_frame)
|
||||||
self._build_status_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:
|
def _build_header(self, parent: ctk.CTkFrame) -> None:
|
||||||
"""Build the application header."""
|
"""Build the application header."""
|
||||||
header = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
header = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
||||||
@@ -335,6 +513,7 @@ class MainWindow(ctk.CTk):
|
|||||||
row=0, column=0, sticky="nsew",
|
row=0, column=0, sticky="nsew",
|
||||||
padx=PADDING, pady=(PADDING, 0),
|
padx=PADDING, pady=(PADDING, 0),
|
||||||
)
|
)
|
||||||
|
panel.grid_rowconfigure(0, weight=1)
|
||||||
panel.grid_columnconfigure(0, weight=1)
|
panel.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
# ── Title ──
|
# ── Title ──
|
||||||
@@ -448,6 +627,7 @@ class MainWindow(ctk.CTk):
|
|||||||
row=1, column=0, sticky="nsew",
|
row=1, column=0, sticky="nsew",
|
||||||
padx=PADDING, pady=PADDING,
|
padx=PADDING, pady=PADDING,
|
||||||
)
|
)
|
||||||
|
self._log_panel = panel
|
||||||
panel.grid_rowconfigure(1, weight=1)
|
panel.grid_rowconfigure(1, weight=1)
|
||||||
panel.grid_columnconfigure(0, weight=1)
|
panel.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
@@ -479,6 +659,7 @@ class MainWindow(ctk.CTk):
|
|||||||
padx=PADDING, pady=PADDING,
|
padx=PADDING, pady=PADDING,
|
||||||
)
|
)
|
||||||
panel.grid_rowconfigure(9, weight=1)
|
panel.grid_rowconfigure(9, weight=1)
|
||||||
|
panel.grid_columnconfigure(0, weight=1) # let content stretch
|
||||||
|
|
||||||
title = ctk.CTkLabel(
|
title = ctk.CTkLabel(
|
||||||
panel,
|
panel,
|
||||||
@@ -496,6 +677,8 @@ class MainWindow(ctk.CTk):
|
|||||||
self._xilinx_path_var = ctk.StringVar(value=self._config.xilinx_path)
|
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 = ctk.CTkEntry(xilinx_frame, textvariable=self._xilinx_path_var)
|
||||||
self._xilinx_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
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(
|
ctk.CTkButton(
|
||||||
xilinx_frame, text="Browse", font=FONT_SMALL, width=70,
|
xilinx_frame, text="Browse", font=FONT_SMALL, width=70,
|
||||||
command=self._browse_xilinx_path,
|
command=self._browse_xilinx_path,
|
||||||
@@ -510,15 +693,20 @@ class MainWindow(ctk.CTk):
|
|||||||
self._ip_string_var = ctk.StringVar(value=self._config.zynq_ip)
|
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 = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
|
||||||
self._ip_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
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)
|
# FSBL ELF path (First Stage Bootloader — initialises PS, forces JTAG boot)
|
||||||
self._fsbl_selector = FileSelector(
|
self._fsbl_selector = FileSelector(
|
||||||
panel,
|
panel,
|
||||||
label_text="FSBL ELF (.elf):",
|
label_text="FSBL ELF (.elf):",
|
||||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||||
|
callback=self._on_file_selected,
|
||||||
)
|
)
|
||||||
|
self._fsbl_selector._suppress_callback = True
|
||||||
if self._config.fsbl_elf_path:
|
if self._config.fsbl_elf_path:
|
||||||
resolved = self._config.resolve_path(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.set_path(str(resolved))
|
||||||
self._fsbl_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._fsbl_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
@@ -527,9 +715,12 @@ class MainWindow(ctk.CTk):
|
|||||||
panel,
|
panel,
|
||||||
label_text="Bootloader BIT (.bit):",
|
label_text="Bootloader BIT (.bit):",
|
||||||
file_types=[("BIT files", "*.bit"), ("All files", "*")],
|
file_types=[("BIT files", "*.bit"), ("All files", "*")],
|
||||||
|
callback=self._on_file_selected,
|
||||||
)
|
)
|
||||||
|
self._bit_selector._suppress_callback = True
|
||||||
if self._config.bootloader_bit_path:
|
if self._config.bootloader_bit_path:
|
||||||
resolved = self._config.resolve_path(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.set_path(str(resolved))
|
||||||
self._bit_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._bit_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
@@ -538,9 +729,12 @@ class MainWindow(ctk.CTk):
|
|||||||
panel,
|
panel,
|
||||||
label_text="Bootloader ELF (.elf):",
|
label_text="Bootloader ELF (.elf):",
|
||||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||||
|
callback=self._on_file_selected,
|
||||||
)
|
)
|
||||||
|
self._elf_selector._suppress_callback = True
|
||||||
if self._config.bootloader_elf_path:
|
if self._config.bootloader_elf_path:
|
||||||
resolved = self._config.resolve_path(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.set_path(str(resolved))
|
||||||
self._elf_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._elf_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
@@ -549,9 +743,12 @@ class MainWindow(ctk.CTk):
|
|||||||
panel,
|
panel,
|
||||||
label_text="Bootloader BIN (.bin):",
|
label_text="Bootloader BIN (.bin):",
|
||||||
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
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:
|
if self._config.bootloader_bin_path:
|
||||||
resolved = self._config.resolve_path(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.set_path(str(resolved))
|
||||||
self._bootloader_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
self._bootloader_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
|
|
||||||
@@ -577,17 +774,39 @@ class MainWindow(ctk.CTk):
|
|||||||
)
|
)
|
||||||
flash_model_label.grid(row=1, column=0, sticky="w", padx=PADDING_SMALL, pady=(2, 0))
|
flash_model_label.grid(row=1, column=0, sticky="w", padx=PADDING_SMALL, pady=(2, 0))
|
||||||
|
|
||||||
|
# Download verify checkbox (Step 4.2)
|
||||||
|
self._download_verify_var = ctk.BooleanVar(value=self._config.download_verify)
|
||||||
|
self._download_verify_cb = ctk.CTkCheckBox(
|
||||||
|
flash_frame,
|
||||||
|
text="下载校验 + CRC (Step 4.2)",
|
||||||
|
variable=self._download_verify_var,
|
||||||
|
font=FONT_SMALL,
|
||||||
|
command=self._on_download_verify_toggle,
|
||||||
|
)
|
||||||
|
self._download_verify_cb.grid(row=2, column=0, sticky="w", padx=PADDING_SMALL, pady=(2, 0))
|
||||||
|
|
||||||
# Firmware BIN path (TFTP upload)
|
# Firmware BIN path (TFTP upload)
|
||||||
self._firmware_bin_selector = FileSelector(
|
self._firmware_bin_selector = FileSelector(
|
||||||
panel,
|
panel,
|
||||||
label_text="Firmware BIN (.bin):",
|
label_text="Firmware BIN (.bin):",
|
||||||
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
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:
|
if self._config.firmware_bin_path:
|
||||||
resolved = self._config.resolve_path(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.set_path(str(resolved))
|
||||||
self._firmware_bin_selector.grid(row=8, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
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 button
|
||||||
save_btn = ctk.CTkButton(
|
save_btn = ctk.CTkButton(
|
||||||
panel,
|
panel,
|
||||||
@@ -702,33 +921,39 @@ class MainWindow(ctk.CTk):
|
|||||||
self._test_version_btn.grid(row=2, column=1, padx=PADDING_SMALL, pady=PADDING_SMALL, sticky="ew")
|
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:
|
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 = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
||||||
panel.grid(
|
panel.grid(
|
||||||
row=3, column=0, sticky="nsew",
|
row=3, column=0, sticky="nsew",
|
||||||
padx=PADDING, pady=PADDING,
|
padx=PADDING, pady=PADDING,
|
||||||
)
|
)
|
||||||
|
panel.grid_rowconfigure((0, 1), weight=1)
|
||||||
|
panel.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self._status = StatusDisplay(panel)
|
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)
|
# UART warning — multi-line textbox, wraps long messages instead
|
||||||
self._uart_warning_label = ctk.CTkLabel(
|
# of expanding the right panel horizontally
|
||||||
|
self._uart_warning = ctk.CTkTextbox(
|
||||||
panel,
|
panel,
|
||||||
text="",
|
|
||||||
font=FONT_BODY,
|
font=FONT_BODY,
|
||||||
anchor="w",
|
height=40,
|
||||||
text_color=WARNING_COLOR,
|
corner_radius=4,
|
||||||
|
fg_color="transparent",
|
||||||
|
state="disabled",
|
||||||
|
wrap="word",
|
||||||
)
|
)
|
||||||
self._uart_warning_label.pack(
|
self._uart_warning.grid(
|
||||||
fill="x", padx=PADDING, pady=(0, PADDING_SMALL)
|
row=1, column=0, sticky="ew", padx=PADDING, pady=(0, PADDING_SMALL)
|
||||||
)
|
)
|
||||||
self._uart_warning_label.pack_forget() # Hide by default
|
self._uart_warning.grid_remove() # Hide by default
|
||||||
|
|
||||||
# ── Workflow Steps ─────────────────────────────────────────
|
# ── Workflow Steps ─────────────────────────────────────────
|
||||||
|
|
||||||
def _log_message(self, message: str) -> None:
|
def _log_message(self, message: str) -> None:
|
||||||
"""Append a message to the log display (thread-safe)."""
|
"""Append a message to the log display and terminal (thread-safe)."""
|
||||||
|
print(message, flush=True)
|
||||||
self.after(0, lambda: self._log_display.append(message))
|
self.after(0, lambda: self._log_display.append(message))
|
||||||
|
|
||||||
def _set_step_status(self, index: int, status: str) -> None:
|
def _set_step_status(self, index: int, status: str) -> None:
|
||||||
@@ -744,6 +969,8 @@ class MainWindow(ctk.CTk):
|
|||||||
def _get_selected_files(self) -> dict:
|
def _get_selected_files(self) -> dict:
|
||||||
"""Get selected file paths from the UI.
|
"""Get selected file paths from the UI.
|
||||||
|
|
||||||
|
Returns absolute paths for immediate backend use.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with all file paths from UI selectors.
|
Dictionary with all file paths from UI selectors.
|
||||||
"""
|
"""
|
||||||
@@ -783,6 +1010,10 @@ class MainWindow(ctk.CTk):
|
|||||||
if result.is_ready:
|
if result.is_ready:
|
||||||
self._vitis_status.configure(text="Vitis: Ready", text_color=SUCCESS_COLOR)
|
self._vitis_status.configure(text="Vitis: Ready", text_color=SUCCESS_COLOR)
|
||||||
self._log_message(" Vitis/Vivado tools available")
|
self._log_message(" Vitis/Vivado tools available")
|
||||||
|
# Show tool versions in status display
|
||||||
|
for tool_name, tool_info in status_dict["tools"].items():
|
||||||
|
if tool_info["found"] and tool_info.get("version"):
|
||||||
|
self._status.set_info(tool_name, f"v{tool_info['version']}")
|
||||||
# Report all found versions for diagnostics
|
# Report all found versions for diagnostics
|
||||||
if self._config.xilinx_path:
|
if self._config.xilinx_path:
|
||||||
try:
|
try:
|
||||||
@@ -811,9 +1042,22 @@ class MainWindow(ctk.CTk):
|
|||||||
self._log_message(f" - {jtag_dev.name} ({dev_type})")
|
self._log_message(f" - {jtag_dev.name} ({dev_type})")
|
||||||
|
|
||||||
if jtag_result.is_present:
|
if jtag_result.is_present:
|
||||||
|
detected_part = jtag_result.devices[0].part_number
|
||||||
|
expected_part = self._config.zynq_part.upper()
|
||||||
|
|
||||||
|
# Validate part number matches expected
|
||||||
|
if detected_part != expected_part:
|
||||||
|
self._log_message(
|
||||||
|
f" ✗ Wrong Zynq part: detected {detected_part}, expected {expected_part}"
|
||||||
|
)
|
||||||
|
self._jtag_status.configure(
|
||||||
|
text=f"JTAG: {detected_part} ≠ {expected_part}", text_color=ERROR_COLOR
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
self._zynq_jtag_present = True
|
self._zynq_jtag_present = True
|
||||||
self._zynq_jtag_info = jtag_result.devices[0]
|
self._zynq_jtag_info = jtag_result.devices[0]
|
||||||
part = jtag_result.devices[0].part_number
|
part = detected_part
|
||||||
|
|
||||||
# Log PS and PL detection status
|
# Log PS and PL detection status
|
||||||
ps_status = "✓" if jtag_result.ps_detected else "✗"
|
ps_status = "✓" if jtag_result.ps_detected else "✗"
|
||||||
@@ -823,17 +1067,20 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
self._log_message(f" ✓ Zynq {part} detected on JTAG chain")
|
self._log_message(f" ✓ Zynq {part} detected on JTAG chain")
|
||||||
self._jtag_status.configure(text=f"JTAG: {part} ✓", text_color=SUCCESS_COLOR)
|
self._jtag_status.configure(text=f"JTAG: {part} ✓", text_color=SUCCESS_COLOR)
|
||||||
|
self._status.set_info("Zynq", part)
|
||||||
else:
|
else:
|
||||||
self._zynq_jtag_present = False
|
self._zynq_jtag_present = False
|
||||||
self._zynq_jtag_info = None
|
self._zynq_jtag_info = None
|
||||||
self._log_message(f" ✗ Zynq not detected on JTAG: {jtag_result.error}")
|
self._log_message(f" ✗ Zynq not detected on JTAG: {jtag_result.error}")
|
||||||
self._jtag_status.configure(text="JTAG: Not detected", text_color=WARNING_COLOR)
|
self._jtag_status.configure(text="JTAG: Not detected", text_color=WARNING_COLOR)
|
||||||
|
return False
|
||||||
|
|
||||||
# Check UART availability
|
# Check UART availability
|
||||||
self._log_message(" Checking UART serial port...")
|
self._log_message(" Checking UART serial port...")
|
||||||
port = self._config.serial_port
|
port = self._config.serial_port
|
||||||
if port:
|
if port:
|
||||||
available, reason = check_uart_available(port, self._config.serial_baudrate)
|
available, reason = check_uart_available(port, self._config.serial_baudrate,
|
||||||
|
monitor=self._uart_monitor)
|
||||||
self._uart_available = available
|
self._uart_available = available
|
||||||
self._uart_message = reason
|
self._uart_message = reason
|
||||||
if available:
|
if available:
|
||||||
@@ -861,17 +1108,128 @@ class MainWindow(ctk.CTk):
|
|||||||
self.after(0, lambda: self._show_uart_warning(msg))
|
self.after(0, lambda: self._show_uart_warning(msg))
|
||||||
|
|
||||||
def _show_uart_warning(self, msg: str) -> None:
|
def _show_uart_warning(self, msg: str) -> None:
|
||||||
"""Display the persistent UART warning message.
|
"""Display the persistent UART warning message (multi-line).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
msg: Warning message to display.
|
msg: Warning message to display.
|
||||||
"""
|
"""
|
||||||
self._uart_warning_label.configure(text=msg)
|
self._uart_warning.configure(state="normal")
|
||||||
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
|
self._uart_warning.delete("1.0", "end")
|
||||||
|
self._uart_warning.insert("1.0", msg)
|
||||||
|
self._uart_warning.tag_config("warn", foreground=WARNING_COLOR)
|
||||||
|
self._uart_warning.tag_add("warn", "1.0", "end")
|
||||||
|
self._uart_warning.configure(state="disabled")
|
||||||
|
self._uart_warning.grid()
|
||||||
|
|
||||||
|
# Auto-size height based on line count
|
||||||
|
line_count = msg.count("\n") + 1
|
||||||
|
self._uart_warning.configure(height=max(2, line_count))
|
||||||
|
|
||||||
def _hide_uart_warning(self) -> None:
|
def _hide_uart_warning(self) -> None:
|
||||||
"""Hide the persistent UART warning label."""
|
"""Hide the persistent UART warning."""
|
||||||
self._uart_warning_label.pack_forget()
|
self._uart_warning.grid_remove()
|
||||||
|
|
||||||
|
def _on_window_resize(self, event) -> None:
|
||||||
|
"""Handle window resize / maximize events.
|
||||||
|
|
||||||
|
When the window is maximized, scale content to fit one screen
|
||||||
|
and hide scrollbars. When restored, use normal sizing with
|
||||||
|
scrollbars on overflow.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
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:
|
||||||
|
"""Scale content and toggle scrollbars based on maximized state."""
|
||||||
|
if not hasattr(self, 'main_frame'):
|
||||||
|
return
|
||||||
|
sf = self.main_frame
|
||||||
|
has_scrollbars = hasattr(sf, '_scrollbar_y')
|
||||||
|
|
||||||
|
if self._maximized:
|
||||||
|
# Hide scrollbars when maximized
|
||||||
|
if has_scrollbars:
|
||||||
|
sf._scrollbar_y.grid_remove()
|
||||||
|
sf._scrollbar_x.grid_remove()
|
||||||
|
# Scale down to fit one screen
|
||||||
|
self._scale_ui(0.6)
|
||||||
|
# Hide the log panel when maximized to save space
|
||||||
|
if hasattr(self, '_log_panel'):
|
||||||
|
self._log_panel.grid_remove()
|
||||||
|
else:
|
||||||
|
# Show scrollbars when not maximized
|
||||||
|
if has_scrollbars:
|
||||||
|
sf._scrollbar_y.grid()
|
||||||
|
sf._scrollbar_x.grid()
|
||||||
|
# Restore normal scale
|
||||||
|
self._scale_ui(1.0)
|
||||||
|
# Show the log panel when not maximized
|
||||||
|
if hasattr(self, '_log_panel'):
|
||||||
|
self._log_panel.grid()
|
||||||
|
|
||||||
|
def _scale_ui(self, factor: float) -> None:
|
||||||
|
"""Scale UI elements by a factor to fit screen.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
factor: Scaling factor (1.0 = normal, <1.0 = smaller).
|
||||||
|
"""
|
||||||
|
# Scale font sizes in key labels
|
||||||
|
scale_font = lambda orig: (orig[0], max(8, int(orig[1] * factor)), orig[2] if len(orig) > 2 else "")
|
||||||
|
|
||||||
|
# Header fonts
|
||||||
|
if hasattr(self, '_vitis_status'):
|
||||||
|
self._vitis_status.configure(font=scale_font(FONT_BODY))
|
||||||
|
if hasattr(self, '_ip_status'):
|
||||||
|
self._ip_status.configure(font=scale_font(FONT_BODY))
|
||||||
|
if hasattr(self, '_jtag_status'):
|
||||||
|
self._jtag_status.configure(font=scale_font(FONT_BODY))
|
||||||
|
|
||||||
|
# Workflow panel fonts
|
||||||
|
if hasattr(self, '_steps'):
|
||||||
|
for step in self._steps:
|
||||||
|
if hasattr(step, '_title_label'):
|
||||||
|
step._title_label.configure(font=scale_font((FONT_BODY[0], 13, "bold")))
|
||||||
|
if hasattr(step, '_desc_label'):
|
||||||
|
step._desc_label.configure(font=scale_font(FONT_SMALL))
|
||||||
|
if hasattr(step, '_action_btn'):
|
||||||
|
step._action_btn.configure(font=scale_font((FONT_BODY[0], 12)))
|
||||||
|
|
||||||
|
# Config panel fonts
|
||||||
|
if hasattr(self, '_xilinx_entry'):
|
||||||
|
self._xilinx_entry.configure(font=scale_font(FONT_BODY))
|
||||||
|
if hasattr(self, '_ip_entry'):
|
||||||
|
self._ip_entry.configure(font=scale_font(FONT_BODY))
|
||||||
|
|
||||||
|
# Log display font
|
||||||
|
if hasattr(self, '_log_display') and hasattr(self._log_display, '_text_widget'):
|
||||||
|
self._log_display._text_widget.configure(font=scale_font(FONT_MONO))
|
||||||
|
|
||||||
|
# Status display font
|
||||||
|
if hasattr(self, '_status') and hasattr(self._status, '_status_text'):
|
||||||
|
self._status._status_text.configure(font=scale_font(FONT_BODY))
|
||||||
|
|
||||||
|
# Scale padding/spacing on all frames
|
||||||
|
if hasattr(self, 'config_frame'):
|
||||||
|
self.config_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||||
|
if hasattr(self, 'workflow_frame'):
|
||||||
|
self.workflow_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||||
|
if hasattr(self, 'log_frame'):
|
||||||
|
self.log_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||||
|
if hasattr(self, 'status_frame'):
|
||||||
|
self.status_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||||
|
|
||||||
|
# Scale step frame spacing
|
||||||
|
if hasattr(self, '_steps'):
|
||||||
|
for step in self._steps:
|
||||||
|
if hasattr(step, '_inner_frame'):
|
||||||
|
step._inner_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||||
|
|
||||||
def _on_port_changed(self, new_port: str) -> None:
|
def _on_port_changed(self, new_port: str) -> None:
|
||||||
"""Handle serial port selection change — validate immediately.
|
"""Handle serial port selection change — validate immediately.
|
||||||
@@ -1067,7 +1425,19 @@ class MainWindow(ctk.CTk):
|
|||||||
self._log_message(" ⏳ Waiting 2s before download...")
|
self._log_message(" ⏳ Waiting 2s before download...")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# 4b: TFTP Download Verify
|
# 4b+4c: TFTP Download Verify + CRC Check
|
||||||
|
if not self._config.download_verify:
|
||||||
|
self._log_message("Step 4b+4c: Download verify & CRC — skipped (disabled in config)")
|
||||||
|
all_results.append(True) # 4b
|
||||||
|
all_results.append(True) # 4c
|
||||||
|
if hasattr(self, '_tftp_sub_step_frame'):
|
||||||
|
self._tftp_sub_step_frame.set_expanded(True)
|
||||||
|
self._tftp_sub_step_frame.set_step_skipped("Download Verify")
|
||||||
|
self._tftp_sub_step_frame.set_step_skipped("CRC Check")
|
||||||
|
self._log_message(" ◌ Download Verify: Skip")
|
||||||
|
self._log_message(" ◌ CRC Check: Skip")
|
||||||
|
else:
|
||||||
|
# 4b: Download Verify
|
||||||
self._log_message("Step 4b: TFTP download verify...")
|
self._log_message("Step 4b: TFTP download verify...")
|
||||||
self._tftp_phase = "Download Verify"
|
self._tftp_phase = "Download Verify"
|
||||||
if hasattr(self, '_tftp_sub_step_frame'):
|
if hasattr(self, '_tftp_sub_step_frame'):
|
||||||
@@ -1091,8 +1461,7 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
if not all_results[-1]:
|
if not all_results[-1]:
|
||||||
self._log_message(" Skipping CRC check (download failed)")
|
self._log_message(" Skipping CRC check (download failed)")
|
||||||
return False
|
else:
|
||||||
|
|
||||||
# 4c: CRC Check
|
# 4c: CRC Check
|
||||||
self._log_message("Step 4c: CRC check...")
|
self._log_message("Step 4c: CRC check...")
|
||||||
if hasattr(self, '_tftp_sub_step_frame'):
|
if hasattr(self, '_tftp_sub_step_frame'):
|
||||||
@@ -1114,7 +1483,7 @@ class MainWindow(ctk.CTk):
|
|||||||
all_results.append(False)
|
all_results.append(False)
|
||||||
|
|
||||||
if not all_results[-1]:
|
if not all_results[-1]:
|
||||||
self._log_message(" Skipping reboot (CRC mismatch)")
|
self._log_message(" Skipping reboot (download/CRC failed)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 4d: Reboot
|
# 4d: Reboot
|
||||||
@@ -1144,27 +1513,114 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
# Wait for board to boot — UART if available, else config delay
|
# Wait for board to boot — UART if available, else config delay
|
||||||
boot_delay = self._config.boot_wait_delay
|
boot_delay = self._config.boot_wait_delay
|
||||||
uart_alive = (self._uart_monitor is not None
|
# Check both main monitor and UART window monitor
|
||||||
and self._uart_monitor.is_running())
|
uart_alive = False
|
||||||
if uart_alive:
|
_uart_src = None # source of latest_info
|
||||||
|
if self._uart_monitor and self._uart_monitor.is_running():
|
||||||
|
uart_alive = True
|
||||||
|
_uart_src = self._uart_monitor
|
||||||
|
elif (self._uart_window and self._uart_window.winfo_exists()
|
||||||
|
and self._uart_window.is_monitoring):
|
||||||
|
uart_alive = True
|
||||||
|
_uart_src = self._uart_window
|
||||||
|
discovered_ip: str = ""
|
||||||
|
if uart_alive and _uart_src:
|
||||||
self._log_message(f" ⏳ Waiting for UART boot signal (max {boot_delay}s)...")
|
self._log_message(f" ⏳ Waiting for UART boot signal (max {boot_delay}s)...")
|
||||||
elapsed = 0.0
|
elapsed = 0.0
|
||||||
|
boot_seen = False # Phase 1 guard — log once, keep waiting for IP
|
||||||
while elapsed < boot_delay:
|
while elapsed < boot_delay:
|
||||||
info = self._uart_monitor.latest_info
|
info = (
|
||||||
if info.ip_address or info.version:
|
_uart_src.latest_info
|
||||||
|
if hasattr(_uart_src, "latest_info")
|
||||||
|
else _uart_src.get_latest_info()
|
||||||
|
)
|
||||||
|
# Phase 2: IP address found — definitive boot confirmation
|
||||||
|
if info and info.ip_address:
|
||||||
self._log_message(
|
self._log_message(
|
||||||
f" ✓ Boot detected after {elapsed:.1f}s"
|
f" ✓ Boot detected after {elapsed:.1f}s"
|
||||||
f" (IP={info.ip_address}, Ver={info.version})"
|
f" (IP={info.ip_address}, Ver={info.version})"
|
||||||
)
|
)
|
||||||
|
discovered_ip = info.ip_address
|
||||||
break
|
break
|
||||||
|
# Phase 1: boot signal (version) seen but no IP yet — keep polling
|
||||||
|
if info and info.version and not boot_seen:
|
||||||
|
boot_seen = True
|
||||||
|
self._log_message(
|
||||||
|
f" ✓ Boot signal after {elapsed:.1f}s"
|
||||||
|
f" (Ver={info.version}), waiting for IP..."
|
||||||
|
)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
elapsed += 0.5
|
elapsed += 0.5
|
||||||
else:
|
else:
|
||||||
self._log_message(f" ⏱ UART timeout — no boot signal in {boot_delay}s")
|
self._log_message(f" ⏱ UART timeout — no IP in {boot_delay}s")
|
||||||
|
# Final check: IP may have arrived after the last poll
|
||||||
|
info = (
|
||||||
|
_uart_src.latest_info
|
||||||
|
if hasattr(_uart_src, "latest_info")
|
||||||
|
else _uart_src.get_latest_info()
|
||||||
|
)
|
||||||
|
if info and info.ip_address:
|
||||||
|
self._log_message(f" IP recovered from latest UART info: {info.ip_address}")
|
||||||
|
discovered_ip = info.ip_address
|
||||||
else:
|
else:
|
||||||
self._log_message(f" ⏳ UART unavailable — sleeping {boot_delay}s for boot...")
|
self._log_message(f" ⏳ UART unavailable — sleeping {boot_delay}s for boot...")
|
||||||
time.sleep(boot_delay)
|
time.sleep(boot_delay)
|
||||||
|
|
||||||
|
# Fallback: parse IP from accumulated UART buffer (lines may arrive
|
||||||
|
# after boot wait loop exited — e.g. "Board IP: 192.168.100.13")
|
||||||
|
if not discovered_ip and _uart_src:
|
||||||
|
all_lines = (
|
||||||
|
_uart_src.all_lines
|
||||||
|
if hasattr(_uart_src, "all_lines")
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
if all_lines:
|
||||||
|
parsed = parse_boot_output(all_lines)
|
||||||
|
if parsed.ip_address:
|
||||||
|
discovered_ip = parsed.ip_address
|
||||||
|
self._log_message(f" IP from UART buffer: {discovered_ip}")
|
||||||
|
|
||||||
|
# Fallback: if no IP from UART, try ping → UDP scan, then extra delay
|
||||||
|
if not discovered_ip:
|
||||||
|
self._log_message(" ⏳ Network stack — waiting 3s...")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Try ping configured IP first (fast, reliable)
|
||||||
|
self._log_message(f" Pinging {self._config.zynq_ip}...")
|
||||||
|
ok = ping_ip(self._config.zynq_ip, timeout=2)
|
||||||
|
if ok:
|
||||||
|
discovered_ip = self._config.zynq_ip
|
||||||
|
self._log_message(f" ✓ Zynq responds to ping at {discovered_ip}")
|
||||||
|
else:
|
||||||
|
# Fallback: UDP broadcast scan
|
||||||
|
self._log_message(" Scanning for Zynq IP (192.168.100.11–30)...")
|
||||||
|
discovered_ip = self._discover_zynq_ip()
|
||||||
|
if discovered_ip:
|
||||||
|
self._log_message(f" ✓ Found Zynq at {discovered_ip}")
|
||||||
|
else:
|
||||||
|
self._log_message(" ⚠ Zynq not found — extra boot delay...")
|
||||||
|
self._log_message(f" ⏳ Waiting {boot_delay}s for late boot...")
|
||||||
|
time.sleep(boot_delay)
|
||||||
|
# Retry ping + scan after extra delay
|
||||||
|
ok2 = ping_ip(self._config.zynq_ip, timeout=2)
|
||||||
|
if ok2:
|
||||||
|
discovered_ip = self._config.zynq_ip
|
||||||
|
self._log_message(f" ✓ Zynq responds to ping at {discovered_ip}")
|
||||||
|
else:
|
||||||
|
discovered_ip = self._discover_zynq_ip()
|
||||||
|
if discovered_ip:
|
||||||
|
self._log_message(f" ✓ Found Zynq at {discovered_ip}")
|
||||||
|
else:
|
||||||
|
self._log_message(" ⚠ Zynq not found on any scanned IP")
|
||||||
|
|
||||||
|
if discovered_ip and discovered_ip != self._config.zynq_ip:
|
||||||
|
self._log_message(
|
||||||
|
f" IP changed: {self._config.zynq_ip} → {discovered_ip}"
|
||||||
|
)
|
||||||
|
self._config.zynq_ip = discovered_ip
|
||||||
|
if self._ip_string_var:
|
||||||
|
self._ip_string_var.set(discovered_ip)
|
||||||
|
|
||||||
# 4e: Verify boot
|
# 4e: Verify boot
|
||||||
self._log_message("Step 4e: Verifying boot...")
|
self._log_message("Step 4e: Verifying boot...")
|
||||||
if hasattr(self, '_tftp_sub_step_frame'):
|
if hasattr(self, '_tftp_sub_step_frame'):
|
||||||
@@ -1364,6 +1820,13 @@ class MainWindow(ctk.CTk):
|
|||||||
elapsed = time.time() - t_step_start
|
elapsed = time.time() - t_step_start
|
||||||
self._log_message(f" ⏱ Step {i+1} completed in {elapsed:.0f}s")
|
self._log_message(f" ⏱ Step {i+1} completed in {elapsed:.0f}s")
|
||||||
|
|
||||||
|
# Step 3: After bootloader load, fix IP to 192.168.100.11
|
||||||
|
if i == 2 and success:
|
||||||
|
self._config.zynq_ip = "192.168.100.11"
|
||||||
|
if self._ip_string_var:
|
||||||
|
self._ip_string_var.set("192.168.100.11")
|
||||||
|
self._log_message(" IP set to 192.168.100.11")
|
||||||
|
|
||||||
# Step 2: mark sub-steps based on result
|
# Step 2: mark sub-steps based on result
|
||||||
if i == 1 and hasattr(self, '_sub_step_frame'):
|
if i == 1 and hasattr(self, '_sub_step_frame'):
|
||||||
if success:
|
if success:
|
||||||
@@ -1371,16 +1834,20 @@ class MainWindow(ctk.CTk):
|
|||||||
else:
|
else:
|
||||||
self._sub_step_frame.set_phase("error")
|
self._sub_step_frame.set_phase("error")
|
||||||
|
|
||||||
# Step 4: mark sub-steps based on result
|
# Step 4: sub-steps already marked individually inside _run_step_4;
|
||||||
|
# avoid set_phase which would restart a pulse on Boot Verify.
|
||||||
if i == 3 and hasattr(self, '_tftp_sub_step_frame'):
|
if i == 3 and hasattr(self, '_tftp_sub_step_frame'):
|
||||||
if success:
|
if not success:
|
||||||
self._tftp_sub_step_frame.set_phase("Boot Verify")
|
|
||||||
else:
|
|
||||||
self._tftp_sub_step_frame.set_step_error("Boot Verify")
|
self._tftp_sub_step_frame.set_step_error("Boot Verify")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results.append(False)
|
results.append(False)
|
||||||
self._set_step_status(i, "error")
|
self._set_step_status(i, "error")
|
||||||
self._log_message(f" Exception in step {i+1}: {e}")
|
self._log_message(f" Exception in step {i+1}: {e}")
|
||||||
|
# Mark sub-step frames as error too
|
||||||
|
if i == 1 and hasattr(self, '_sub_step_frame'):
|
||||||
|
self._sub_step_frame.set_phase("error")
|
||||||
|
if i == 3 and hasattr(self, '_tftp_sub_step_frame'):
|
||||||
|
self._tftp_sub_step_frame.set_step_error("Boot Verify")
|
||||||
|
|
||||||
self._progress.set_value((step_idx + 1) / total)
|
self._progress.set_value((step_idx + 1) / total)
|
||||||
|
|
||||||
@@ -1532,6 +1999,7 @@ class MainWindow(ctk.CTk):
|
|||||||
success, message, version = test_serial_version(
|
success, message, version = test_serial_version(
|
||||||
port,
|
port,
|
||||||
self._config.serial_baudrate if self._config else 115200,
|
self._config.serial_baudrate if self._config else 115200,
|
||||||
|
monitor=self._uart_monitor,
|
||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
@@ -1655,6 +2123,27 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
threading.Thread(target=_thread, daemon=True).start()
|
threading.Thread(target=_thread, daemon=True).start()
|
||||||
|
|
||||||
|
def _discover_zynq_ip(self) -> str:
|
||||||
|
"""Scan IP range to find the Zynq after reboot.
|
||||||
|
|
||||||
|
Sends UDP ver() to each candidate IP (port = last_octet - 3)
|
||||||
|
and returns the first IP that responds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Discovered IP address, or empty string if not found.
|
||||||
|
"""
|
||||||
|
subnet = "192.168.100"
|
||||||
|
for last in range(11, 31):
|
||||||
|
ip = f"{subnet}.{last}"
|
||||||
|
try:
|
||||||
|
from reboot_manager import _send_udp_command
|
||||||
|
ok, _resp = _send_udp_command(ip, b"ver()", timeout=0.5)
|
||||||
|
if ok:
|
||||||
|
return ip
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return ""
|
||||||
|
|
||||||
# ── Vitis Check ────────────────────────────────────────────
|
# ── Vitis Check ────────────────────────────────────────────
|
||||||
|
|
||||||
def _check_vitis(self) -> None:
|
def _check_vitis(self) -> None:
|
||||||
|
|||||||
+155
-44
@@ -6,6 +6,7 @@ consistent styling from gui/styles.py.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
@@ -436,54 +437,91 @@ class ProgressIndicator(ctk.CTkFrame):
|
|||||||
|
|
||||||
|
|
||||||
class StatusDisplay(ctk.CTkFrame):
|
class StatusDisplay(ctk.CTkFrame):
|
||||||
"""Status display widget for operation results.
|
"""Info display widget — persistent key-value file/device info.
|
||||||
|
|
||||||
Shows a status message with color-coded feedback.
|
Shows aligned ``key : value`` lines for file metadata, Zynq
|
||||||
|
part info, tool versions, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, master, **kwargs):
|
def __init__(self, master, **kwargs):
|
||||||
"""Initialize the status display.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
master: Parent widget.
|
|
||||||
"""
|
|
||||||
super().__init__(master, **kwargs)
|
super().__init__(master, **kwargs)
|
||||||
|
self._info_items: list[tuple[str, str]] = []
|
||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
|
|
||||||
def _create_widgets(self) -> None:
|
def _create_widgets(self) -> None:
|
||||||
"""Create the status display UI."""
|
self.grid_rowconfigure(0, weight=1)
|
||||||
self._status_label = ctk.CTkLabel(
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self._info_text = ctk.CTkTextbox(
|
||||||
self,
|
self,
|
||||||
text="Ready",
|
|
||||||
font=FONT_BODY,
|
font=FONT_BODY,
|
||||||
anchor="w",
|
corner_radius=CORNER_RADIUS,
|
||||||
|
state="disabled",
|
||||||
|
wrap="word",
|
||||||
)
|
)
|
||||||
self._status_label.pack(
|
self._info_text.grid(
|
||||||
fill="x", padx=PADDING, pady=PADDING_SMALL
|
row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Pre-configure tags
|
||||||
|
for name, color in [
|
||||||
|
("info_label", PRIMARY_COLOR),
|
||||||
|
("info_value", DESC_TEXT_COLOR if _is_dark() else DESC_TEXT_COLOR_LIGHT),
|
||||||
|
]:
|
||||||
|
self._info_text.tag_config(name, foreground=color)
|
||||||
|
|
||||||
def set_status(self, message: str, status: str = "info") -> None:
|
def set_status(self, message: str, status: str = "info") -> None:
|
||||||
"""Update the status message and color.
|
"""Compatibility stub — status messages go to log now."""
|
||||||
|
|
||||||
|
def set_info(self, key: str, value: str) -> None:
|
||||||
|
"""Set or update a persistent info line.
|
||||||
|
|
||||||
|
Displays as ``key : value`` in the info block.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message: Status message text.
|
key: Label for the info line.
|
||||||
status: Status type for coloring ('info', 'success', 'error', 'warning').
|
value: Value string.
|
||||||
"""
|
"""
|
||||||
self._status_label.configure(text=message)
|
for i, (k, _) in enumerate(self._info_items):
|
||||||
colors = {
|
if k == key:
|
||||||
"info": INFO_COLOR,
|
self._info_items[i] = (key, value)
|
||||||
"success": SUCCESS_COLOR,
|
break
|
||||||
"error": DANGER_COLOR,
|
else:
|
||||||
"warning": WARNING_COLOR,
|
self._info_items.append((key, value))
|
||||||
}
|
self._refresh_info()
|
||||||
self._status_label.configure(text_color=colors.get(status, INFO_COLOR))
|
|
||||||
|
def clear_info(self) -> None:
|
||||||
|
"""Remove all persistent info lines."""
|
||||||
|
self._info_items.clear()
|
||||||
|
self._info_text.configure(state="normal")
|
||||||
|
self._info_text.delete("1.0", "end")
|
||||||
|
self._info_text.configure(state="disabled")
|
||||||
|
|
||||||
|
def _refresh_info(self) -> None:
|
||||||
|
"""Rebuild the info text block from current items."""
|
||||||
|
self._info_text.configure(state="normal")
|
||||||
|
self._info_text.delete("1.0", "end")
|
||||||
|
if not self._info_items:
|
||||||
|
self._info_text.configure(state="disabled")
|
||||||
|
return
|
||||||
|
max_key = max(len(k) for k, _ in self._info_items)
|
||||||
|
for key, value in self._info_items:
|
||||||
|
line = f"{key.ljust(max_key + 2)} {value}\n"
|
||||||
|
start = self._info_text.index("end-1c")
|
||||||
|
self._info_text.insert("end", line)
|
||||||
|
end = self._info_text.index("end-1c")
|
||||||
|
self._info_text.tag_add("info_label", start, f"{start}+{len(key)}c")
|
||||||
|
self._info_text.tag_add("info_value", f"{start}+{len(key)}c", end)
|
||||||
|
self._info_text.configure(state="disabled")
|
||||||
|
|
||||||
|
|
||||||
class FileSelector(ctk.CTkFrame):
|
class FileSelector(ctk.CTkFrame):
|
||||||
"""File selector widget with browse button and path display.
|
"""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__(
|
def __init__(
|
||||||
@@ -506,7 +544,11 @@ class FileSelector(ctk.CTkFrame):
|
|||||||
|
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
self._file_types = file_types or [("All files", "*")]
|
self._file_types = file_types or [("All files", "*")]
|
||||||
self._selected_path = ctk.StringVar(value="")
|
self._hint_text = label_text.rstrip(":") # e.g. "FSBL ELF (.elf)"
|
||||||
|
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)
|
self._create_widgets(label_text)
|
||||||
|
|
||||||
@@ -524,10 +566,10 @@ class FileSelector(ctk.CTkFrame):
|
|||||||
)
|
)
|
||||||
label.grid(row=0, column=0, sticky="w", padx=(PADDING, PADDING_SMALL))
|
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._entry = ctk.CTkEntry(
|
||||||
self,
|
self,
|
||||||
textvariable=self._selected_path,
|
textvariable=self._display_text,
|
||||||
font=FONT_MONO,
|
font=FONT_MONO,
|
||||||
state="readonly",
|
state="readonly",
|
||||||
)
|
)
|
||||||
@@ -549,33 +591,60 @@ class FileSelector(ctk.CTkFrame):
|
|||||||
|
|
||||||
def _browse(self) -> None:
|
def _browse(self) -> None:
|
||||||
"""Open file dialog and set selected path."""
|
"""Open file dialog and set selected path."""
|
||||||
import customtkinter
|
|
||||||
from tkinter import filedialog
|
from tkinter import filedialog
|
||||||
|
|
||||||
file_path = filedialog.askopenfilename(
|
file_path = filedialog.askopenfilename(
|
||||||
title=f"Select {self._selected_path.get() or 'file'}",
|
title=f"Select {self._hint_text}",
|
||||||
filetypes=self._file_types,
|
filetypes=self._file_types,
|
||||||
)
|
)
|
||||||
if file_path:
|
if file_path:
|
||||||
self.set_path(file_path)
|
self.set_path(file_path)
|
||||||
|
|
||||||
def set_path(self, path: str) -> None:
|
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:
|
Args:
|
||||||
path: Absolute path to the selected file.
|
path: Absolute or relative path to the selected file.
|
||||||
"""
|
"""
|
||||||
self._selected_path.set(path)
|
self._full_path = path
|
||||||
if self._callback:
|
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)
|
self._callback(path)
|
||||||
|
|
||||||
def get_path(self) -> str:
|
def get_path(self) -> str:
|
||||||
"""Get the currently selected file path.
|
"""Get the currently selected file path (absolute).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Selected file path string.
|
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):
|
class LogDisplay(ctk.CTkFrame):
|
||||||
@@ -650,15 +719,15 @@ class SubStepFrame(ctk.CTkFrame):
|
|||||||
"""Collapsible frame showing flash operation sub-steps with status.
|
"""Collapsible frame showing flash operation sub-steps with status.
|
||||||
|
|
||||||
Colors match StepHeader: gray pending, blue running (+pulse),
|
Colors match StepHeader: gray pending, blue running (+pulse),
|
||||||
green complete, red error. Default expanded.
|
green complete, red error. Default collapsed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent: ctk.CTkFrame) -> None:
|
def __init__(self, parent: ctk.CTkFrame) -> None:
|
||||||
super().__init__(parent, fg_color="transparent")
|
super().__init__(parent, fg_color="transparent")
|
||||||
|
|
||||||
self._collapsed = False
|
self._collapsed = True
|
||||||
self._toggle_btn = ctk.CTkButton(
|
self._toggle_btn = ctk.CTkButton(
|
||||||
self, text="▼", width=22, height=22,
|
self, text="▶", width=22, height=22,
|
||||||
font=(FONT_SMALL[0], 9), command=self._toggle,
|
font=(FONT_SMALL[0], 9), command=self._toggle,
|
||||||
)
|
)
|
||||||
self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w")
|
self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w")
|
||||||
@@ -683,6 +752,16 @@ class SubStepFrame(ctk.CTkFrame):
|
|||||||
self.grid_columnconfigure(1, weight=1)
|
self.grid_columnconfigure(1, weight=1)
|
||||||
self._pulse_job: str | None = None
|
self._pulse_job: str | None = None
|
||||||
|
|
||||||
|
# Apply initial collapsed state (hide sub-items)
|
||||||
|
self._hide()
|
||||||
|
|
||||||
|
def _hide(self) -> None:
|
||||||
|
"""Hide sub-items without toggling state."""
|
||||||
|
for item in self._sub_items:
|
||||||
|
item["dot"].grid_remove()
|
||||||
|
item["label"].grid_remove()
|
||||||
|
item["pct"].grid_remove()
|
||||||
|
|
||||||
def _toggle(self) -> None:
|
def _toggle(self) -> None:
|
||||||
self._collapsed = not self._collapsed
|
self._collapsed = not self._collapsed
|
||||||
self._toggle_btn.configure(text="▶" if self._collapsed else "▼")
|
self._toggle_btn.configure(text="▶" if self._collapsed else "▼")
|
||||||
@@ -784,19 +863,20 @@ class TftpSubStepFrame(ctk.CTkFrame):
|
|||||||
|
|
||||||
Sub-steps: Upload, Download Verify, CRC Check, Reboot, Boot Verify.
|
Sub-steps: Upload, Download Verify, CRC Check, Reboot, Boot Verify.
|
||||||
Colors match StepHeader: gray pending, blue running (+pulse),
|
Colors match StepHeader: gray pending, blue running (+pulse),
|
||||||
green complete, red error. Default expanded.
|
green complete, red error. Default collapsed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent: ctk.CTkFrame, substep_names: list[str] | None = None) -> None:
|
def __init__(self, parent: ctk.CTkFrame, substep_names: list[str] | None = None) -> None:
|
||||||
super().__init__(parent, fg_color="transparent")
|
super().__init__(parent, fg_color="transparent")
|
||||||
|
|
||||||
self._collapsed = False
|
self._collapsed = True
|
||||||
self._pulse_job: str | None = None
|
self._pulse_job: str | None = None
|
||||||
|
self._skipped: set[int] = set()
|
||||||
self._substep_names = substep_names or ["Upload", "Download Verify", "CRC Check", "Reboot", "Boot Verify"]
|
self._substep_names = substep_names or ["Upload", "Download Verify", "CRC Check", "Reboot", "Boot Verify"]
|
||||||
self._num_steps = len(self._substep_names)
|
self._num_steps = len(self._substep_names)
|
||||||
|
|
||||||
self._toggle_btn = ctk.CTkButton(
|
self._toggle_btn = ctk.CTkButton(
|
||||||
self, text="▼", width=22, height=22,
|
self, text="▶", width=22, height=22,
|
||||||
font=(FONT_SMALL[0], 9), command=self._toggle,
|
font=(FONT_SMALL[0], 9), command=self._toggle,
|
||||||
)
|
)
|
||||||
self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w")
|
self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w")
|
||||||
@@ -819,6 +899,16 @@ class TftpSubStepFrame(ctk.CTkFrame):
|
|||||||
|
|
||||||
self.grid_columnconfigure(1, weight=1)
|
self.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Apply initial collapsed state (hide sub-items)
|
||||||
|
self._hide()
|
||||||
|
|
||||||
|
def _hide(self) -> None:
|
||||||
|
"""Hide sub-items without toggling state."""
|
||||||
|
for item in self._sub_items:
|
||||||
|
item["dot"].grid_remove()
|
||||||
|
item["label"].grid_remove()
|
||||||
|
item["pct"].grid_remove()
|
||||||
|
|
||||||
def _toggle(self) -> None:
|
def _toggle(self) -> None:
|
||||||
self._collapsed = not self._collapsed
|
self._collapsed = not self._collapsed
|
||||||
self._toggle_btn.configure(text="▶" if self._collapsed else "▼")
|
self._toggle_btn.configure(text="▶" if self._collapsed else "▼")
|
||||||
@@ -847,6 +937,8 @@ class TftpSubStepFrame(ctk.CTkFrame):
|
|||||||
|
|
||||||
pct_text = f"{pct}%" if pct >= 0 else ""
|
pct_text = f"{pct}%" if pct >= 0 else ""
|
||||||
for i, item in enumerate(self._sub_items):
|
for i, item in enumerate(self._sub_items):
|
||||||
|
if i in self._skipped:
|
||||||
|
continue # leave skipped items untouched
|
||||||
if i < idx:
|
if i < idx:
|
||||||
item["dot"].configure(text="●", text_color=_C_DONE)
|
item["dot"].configure(text="●", text_color=_C_DONE)
|
||||||
item["label"].configure(text_color=_C_DONE)
|
item["label"].configure(text_color=_C_DONE)
|
||||||
@@ -886,6 +978,18 @@ class TftpSubStepFrame(ctk.CTkFrame):
|
|||||||
item["label"].configure(text_color=_C_ERROR)
|
item["label"].configure(text_color=_C_ERROR)
|
||||||
self._stop_pulse()
|
self._stop_pulse()
|
||||||
|
|
||||||
|
def set_step_skipped(self, phase: str) -> None:
|
||||||
|
"""Mark a specific step as skipped (warning color, Skip text)."""
|
||||||
|
idx = self._substep_names.index(phase) if phase in self._substep_names else -1
|
||||||
|
if idx < 0:
|
||||||
|
return
|
||||||
|
self._skipped.add(idx)
|
||||||
|
item = self._sub_items[idx]
|
||||||
|
item["dot"].configure(text="◌", text_color=WARNING_COLOR)
|
||||||
|
item["label"].configure(text_color=WARNING_COLOR)
|
||||||
|
item["pct"].configure(text="Skip", text_color=WARNING_COLOR)
|
||||||
|
self._stop_pulse()
|
||||||
|
|
||||||
def _start_pulse(self, idx: int) -> None:
|
def _start_pulse(self, idx: int) -> None:
|
||||||
"""Start pulsing the running sub-step."""
|
"""Start pulsing the running sub-step."""
|
||||||
_LIGHT = RUNNING_FADE_LIGHT
|
_LIGHT = RUNNING_FADE_LIGHT
|
||||||
@@ -910,6 +1014,7 @@ class TftpSubStepFrame(ctk.CTkFrame):
|
|||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
self._stop_pulse()
|
self._stop_pulse()
|
||||||
|
self._skipped.clear()
|
||||||
_C = ACCENT_PENDING
|
_C = ACCENT_PENDING
|
||||||
for i, item in enumerate(self._sub_items):
|
for i, item in enumerate(self._sub_items):
|
||||||
item["dot"].configure(text="○", text_color=_C)
|
item["dot"].configure(text="○", text_color=_C)
|
||||||
@@ -1079,7 +1184,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
|||||||
ser.close()
|
ser.close()
|
||||||
self.after(0, self._on_open_success)
|
self.after(0, self._on_open_success)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.after(0, lambda: self._show_unavailable(str(e)))
|
self.after(0, lambda e=e: self._show_unavailable(str(e)))
|
||||||
|
|
||||||
threading.Thread(target=_bg_open, daemon=True).start()
|
threading.Thread(target=_bg_open, daemon=True).start()
|
||||||
|
|
||||||
@@ -1190,6 +1295,12 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
|||||||
"""True if the UART monitor thread is currently running."""
|
"""True if the UART monitor thread is currently running."""
|
||||||
return self._monitor is not None and self._monitor.is_running()
|
return self._monitor is not None and self._monitor.is_running()
|
||||||
|
|
||||||
|
def get_latest_info(self):
|
||||||
|
"""Return latest parsed BootInfo from the UART monitor, or None."""
|
||||||
|
if self._monitor:
|
||||||
|
return self._monitor.latest_info
|
||||||
|
return None
|
||||||
|
|
||||||
def _on_close(self) -> None:
|
def _on_close(self) -> None:
|
||||||
"""Cleanup UartMonitor and destroy window."""
|
"""Cleanup UartMonitor and destroy window."""
|
||||||
if self._monitor:
|
if self._monitor:
|
||||||
|
|||||||
@@ -186,8 +186,7 @@ def reboot_via_jtag(
|
|||||||
from vitis_checker import get_xsct_path
|
from vitis_checker import get_xsct_path
|
||||||
|
|
||||||
xsct = get_xsct_path(
|
xsct = get_xsct_path(
|
||||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
xilinx_root=getattr(config, "xilinx_path", ""),
|
||||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
|
||||||
)
|
)
|
||||||
if not xsct:
|
if not xsct:
|
||||||
return RebootResult(
|
return RebootResult(
|
||||||
@@ -231,8 +230,9 @@ exit
|
|||||||
if callback:
|
if callback:
|
||||||
callback("progress", "Running JTAG system reset...")
|
callback("progress", "Running JTAG system reset...")
|
||||||
|
|
||||||
|
from vitis_checker import build_tool_command
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[xsct, tcl_path],
|
build_tool_command(xsct, tcl_path),
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
|
|||||||
+23
-11
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ def check_uart_available(
|
|||||||
port: str,
|
port: str,
|
||||||
baudrate: int = 115200,
|
baudrate: int = 115200,
|
||||||
timeout: float = 3.0,
|
timeout: float = 3.0,
|
||||||
|
monitor: UartMonitor | None = None,
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
"""Check whether the UART serial port is available and readable.
|
"""Check whether the UART serial port is available and readable.
|
||||||
|
|
||||||
@@ -144,19 +146,24 @@ def check_uart_available(
|
|||||||
considered available (the device may simply not be outputting yet).
|
considered available (the device may simply not be outputting yet).
|
||||||
If the port cannot be opened at all, it is unavailable.
|
If the port cannot be opened at all, it is unavailable.
|
||||||
|
|
||||||
|
If *monitor* is provided and currently running, the function
|
||||||
|
skips the port-open test (which would fail on Windows with
|
||||||
|
Error 13 due to exclusive access) and returns True immediately.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
port: Serial port device path (e.g., '/dev/ttyUSB0').
|
port: Serial port device path (e.g., '/dev/ttyUSB0').
|
||||||
baudrate: Baud rate for serial communication.
|
baudrate: Baud rate for serial communication.
|
||||||
timeout: Seconds to wait for data after opening.
|
timeout: Seconds to wait for data after opening.
|
||||||
|
monitor: Optional running UartMonitor — if active, reuse it.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (is_available, reason_string).
|
Tuple of (is_available, reason_string).
|
||||||
- (True, "Port open") — port is usable
|
|
||||||
- (True, "Port open, no data yet") — port open, no data currently
|
|
||||||
- (False, "No serial ports detected") — system has no serial ports
|
|
||||||
- (False, "Port not in detected ports") — configured port missing
|
|
||||||
- (False, "Failed to open: <error>") — port open error
|
|
||||||
"""
|
"""
|
||||||
|
# Reuse existing monitor when available (avoids Error 13 on Windows)
|
||||||
|
if monitor is not None and monitor.is_running():
|
||||||
|
return True, "Already monitoring"
|
||||||
|
|
||||||
|
# 1. Quick port list check
|
||||||
# 1. Quick port list check
|
# 1. Quick port list check
|
||||||
ports = detect_serial_ports()
|
ports = detect_serial_ports()
|
||||||
if not ports:
|
if not ports:
|
||||||
@@ -222,28 +229,33 @@ def test_serial_version(
|
|||||||
port: str,
|
port: str,
|
||||||
baudrate: int = 115200,
|
baudrate: int = 115200,
|
||||||
timeout: float = 5.0,
|
timeout: float = 5.0,
|
||||||
|
monitor: UartMonitor | None = None,
|
||||||
) -> tuple[bool, str, str]:
|
) -> tuple[bool, str, str]:
|
||||||
"""Test serial port by sending 'ver()' command and parsing response.
|
"""Test serial port by sending 'ver()' command and parsing response.
|
||||||
|
|
||||||
Sends 'ver\\r\\n' to the serial port and reads the response.
|
If *monitor* is running, skips opening a new connection and
|
||||||
Attempts to parse version string from the response.
|
returns the latest parsed version from the monitor instead.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
port: Serial port device path (e.g., '/dev/ttyUSB0').
|
port: Serial port device path (e.g., '/dev/ttyUSB0').
|
||||||
baudrate: Baud rate for serial communication.
|
baudrate: Baud rate for serial communication.
|
||||||
timeout: Read timeout in seconds.
|
timeout: Read timeout in seconds.
|
||||||
|
monitor: Optional running UartMonitor — if active, reuse it.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (success, message, version_string).
|
Tuple of (success, message, version_string).
|
||||||
- (True, "Version: x.y.z", "x.y.z") — version detected
|
|
||||||
- (True, "Port responded", "") — port responded but no version
|
|
||||||
- (False, "Error message", "") — error occurred
|
|
||||||
"""
|
"""
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
|
# Reuse existing monitor when available (avoids Error 13 on Windows)
|
||||||
|
if monitor is not None and monitor.is_running():
|
||||||
|
info = monitor.latest_info
|
||||||
|
if info and info.version:
|
||||||
|
return True, f"Version: {info.version}", info.version
|
||||||
|
return True, "Port monitored, no version parsed yet", ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with serial.Serial(port, baudrate, timeout=timeout) as ser:
|
with serial.Serial(port, baudrate, timeout=timeout) as ser:
|
||||||
# Clear buffers
|
|
||||||
ser.reset_input_buffer()
|
ser.reset_input_buffer()
|
||||||
ser.reset_output_buffer()
|
ser.reset_output_buffer()
|
||||||
|
|
||||||
|
|||||||
+138
-19
@@ -56,10 +56,12 @@ TOOL_ALIASES: dict[str, list[str]] = {
|
|||||||
"xsct": ["xsct"],
|
"xsct": ["xsct"],
|
||||||
"program_flash": ["program_flash"],
|
"program_flash": ["program_flash"],
|
||||||
"bootgen": ["bootgen"],
|
"bootgen": ["bootgen"],
|
||||||
|
"hw_server": ["hw_server"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Product subdirectories under xilinx_root to scan (in preference order)
|
# Product subdirectories under xilinx_root to scan (in preference order)
|
||||||
_PRODUCT_DIRS = ("Vitis", "Vivado")
|
# SDK covers pre-2019 installations where xsct lived in SDK/*/bin/
|
||||||
|
_PRODUCT_DIRS = ("Vitis", "Vivado", "SDK")
|
||||||
|
|
||||||
|
|
||||||
# ── Version helpers ──────────────────────────────────────────────────
|
# ── Version helpers ──────────────────────────────────────────────────
|
||||||
@@ -110,6 +112,7 @@ def _scan_xilinx_root(
|
|||||||
"""Scan xilinx_root for all installed versions of a tool.
|
"""Scan xilinx_root for all installed versions of a tool.
|
||||||
|
|
||||||
Searches under Vitis/*/bin/ and Vivado/*/bin/ for the executable.
|
Searches under Vitis/*/bin/ and Vivado/*/bin/ for the executable.
|
||||||
|
On Windows also checks .bat wrappers in addition to .exe.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
xilinx_root: Xilinx root directory (e.g., /data/xilinx, C:/Xilinx).
|
xilinx_root: Xilinx root directory (e.g., /data/xilinx, C:/Xilinx).
|
||||||
@@ -124,6 +127,11 @@ def _scan_xilinx_root(
|
|||||||
if not xr.is_dir():
|
if not xr.is_dir():
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
# On Windows, also try .bat extension (Xilinx tools use .bat wrappers)
|
||||||
|
suffixes = [exec_suffix]
|
||||||
|
if exec_suffix == ".exe":
|
||||||
|
suffixes.append(".bat")
|
||||||
|
|
||||||
for product in _PRODUCT_DIRS:
|
for product in _PRODUCT_DIRS:
|
||||||
pdir = xr / product
|
pdir = xr / product
|
||||||
if not pdir.is_dir():
|
if not pdir.is_dir():
|
||||||
@@ -135,11 +143,13 @@ def _scan_xilinx_root(
|
|||||||
for ver_dir in entries:
|
for ver_dir in entries:
|
||||||
if not ver_dir.is_dir():
|
if not ver_dir.is_dir():
|
||||||
continue
|
continue
|
||||||
candidate = ver_dir / "bin" / f"{exec_name}{exec_suffix}"
|
for sfx in suffixes:
|
||||||
|
candidate = ver_dir / "bin" / f"{exec_name}{sfx}"
|
||||||
if candidate.is_file():
|
if candidate.is_file():
|
||||||
v = _parse_version_tuple(ver_dir.name)
|
v = _parse_version_tuple(ver_dir.name)
|
||||||
if v != (0,):
|
if v != (0,):
|
||||||
results.append((v, candidate, product))
|
results.append((v, candidate, product))
|
||||||
|
break # Found for this version dir
|
||||||
# Sort newest first (descending version tuple)
|
# Sort newest first (descending version tuple)
|
||||||
results.sort(key=lambda x: x[0], reverse=True)
|
results.sort(key=lambda x: x[0], reverse=True)
|
||||||
return results
|
return results
|
||||||
@@ -154,9 +164,9 @@ def _find_executable(
|
|||||||
"""Find an executable by name using PATH, Vitis, and Xilinx root.
|
"""Find an executable by name using PATH, Vitis, and Xilinx root.
|
||||||
|
|
||||||
Search order:
|
Search order:
|
||||||
1. System PATH (shutil.which)
|
1. System PATH (shutil.which) — tries bare name, .exe, .bat on Windows
|
||||||
2. vitis_path/bin (legacy config)
|
2. vitis_path/bin (legacy config)
|
||||||
3. xilinx_root/Vitis/*/bin and Vivado/*/bin — pick NEWEST version
|
3. xilinx_root/Vitis/*/bin, Vivado/*/bin, SDK/*/bin — pick NEWEST version
|
||||||
4. Common install paths (cross-platform)
|
4. Common install paths (cross-platform)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -170,14 +180,20 @@ def _find_executable(
|
|||||||
"""
|
"""
|
||||||
exec_suffix = ".exe" if system == "windows" else ""
|
exec_suffix = ".exe" if system == "windows" else ""
|
||||||
|
|
||||||
# 1. System PATH
|
# 1. System PATH — search without explicit extension first
|
||||||
|
# so Windows PATHEXT resolves .bat/.exe/.cmd automatically
|
||||||
|
found = shutil.which(exec_name)
|
||||||
|
if not found and exec_suffix:
|
||||||
found = shutil.which(f"{exec_name}{exec_suffix}")
|
found = shutil.which(f"{exec_name}{exec_suffix}")
|
||||||
|
if not found:
|
||||||
|
found = shutil.which(f"{exec_name}.bat")
|
||||||
if found:
|
if found:
|
||||||
return found
|
return found
|
||||||
|
|
||||||
# 2. Legacy vitis_path/bin
|
# 2. Legacy vitis_path/bin
|
||||||
if vitis_path:
|
if vitis_path:
|
||||||
candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}"
|
for sfx in (".bat", ".exe") if system == "windows" else (exec_suffix,):
|
||||||
|
candidate = Path(vitis_path) / "bin" / f"{exec_name}{sfx}"
|
||||||
if candidate.is_file():
|
if candidate.is_file():
|
||||||
return str(candidate)
|
return str(candidate)
|
||||||
|
|
||||||
@@ -192,12 +208,11 @@ def _find_executable(
|
|||||||
|
|
||||||
# 4. Common install paths (cross-platform fallback)
|
# 4. Common install paths (cross-platform fallback)
|
||||||
if system == "windows":
|
if system == "windows":
|
||||||
common = [
|
common: list[str] = []
|
||||||
f"C:/Xilinx/Vitis/2023.2/bin/{exec_name}.exe",
|
for sfx in (".bat", ".exe"):
|
||||||
f"C:/Xilinx/Vivado/2023.2/bin/{exec_name}.exe",
|
for ver in ("2023.2", "2022.2", "2018.3"):
|
||||||
f"C:/Xilinx/Vitis/2022.2/bin/{exec_name}.exe",
|
for prod in ("Vitis", "Vivado", "SDK"):
|
||||||
f"C:/Xilinx/Vivado/2022.2/bin/{exec_name}.exe",
|
common.append(f"C:/Xilinx/{prod}/{ver}/bin/{exec_name}{sfx}")
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
common = [
|
common = [
|
||||||
f"/data/xilinx/Vitis/2023.2/bin/{exec_name}",
|
f"/data/xilinx/Vitis/2023.2/bin/{exec_name}",
|
||||||
@@ -237,6 +252,60 @@ def get_all_found_versions(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Environment setup ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _find_settings_script(tool_path: str) -> str | None:
|
||||||
|
"""Find the settings64 script for a Xilinx tool's version directory.
|
||||||
|
|
||||||
|
Walks up from the tool's bin/ directory looking for
|
||||||
|
settings64.sh (Linux) or settings64.bat (Windows).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_path: Absolute path to a Xilinx tool executable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to settings64 script, or None if not found.
|
||||||
|
"""
|
||||||
|
p = Path(tool_path).resolve()
|
||||||
|
for parent in p.parents:
|
||||||
|
for name in ("settings64.sh", "settings64.bat"):
|
||||||
|
candidate = parent / name
|
||||||
|
if candidate.is_file():
|
||||||
|
return str(candidate)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_tool_command(tool_path: str, *args: str) -> list[str]:
|
||||||
|
"""Build a subprocess-safe command with Xilinx environment setup.
|
||||||
|
|
||||||
|
On Linux: sources settings64.sh before running the tool.
|
||||||
|
On Windows: .bat wrappers handle environment internally.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_path: Path to the Xilinx tool executable.
|
||||||
|
*args: Arguments to pass to the tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Command list suitable for subprocess.run().
|
||||||
|
"""
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
system = platform.system().lower()
|
||||||
|
|
||||||
|
# Windows: .bat wrappers set up their own environment
|
||||||
|
if system == "windows":
|
||||||
|
return [tool_path, *args]
|
||||||
|
|
||||||
|
# Linux: find settings64.sh and source it before running
|
||||||
|
settings = _find_settings_script(tool_path)
|
||||||
|
if not settings:
|
||||||
|
return [tool_path, *args]
|
||||||
|
|
||||||
|
quoted = " ".join(shlex.quote(a) for a in [tool_path, *args])
|
||||||
|
return ["bash", "-c", f"source {shlex.quote(settings)} && {quoted}"]
|
||||||
|
|
||||||
|
|
||||||
# ── Public path getters ──────────────────────────────────────────────
|
# ── Public path getters ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -255,6 +324,37 @@ def get_bootgen_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
|
|||||||
return _find_executable("bootgen", vitis_path, platform.system().lower(), xilinx_root)
|
return _find_executable("bootgen", vitis_path, platform.system().lower(), xilinx_root)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vivado_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
|
||||||
|
"""Find the Vivado executable (JTAG operations via hw_server).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vitis_path: Legacy Vitis installation directory.
|
||||||
|
xilinx_root: Xilinx root directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to vivado executable, or None.
|
||||||
|
"""
|
||||||
|
return _find_executable("vivado", vitis_path, platform.system().lower(), xilinx_root)
|
||||||
|
|
||||||
|
|
||||||
|
def get_hw_server_path(vitis_path: str = "", xilinx_root: str = "") -> str | None:
|
||||||
|
"""Find the hw_server executable (Xilinx hardware server).
|
||||||
|
|
||||||
|
hw_server must be running before Vivado/xsct can connect to the
|
||||||
|
JTAG chain. This finder searches the same Vitis/Vivado bin dirs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vitis_path: Legacy Vitis installation directory.
|
||||||
|
xilinx_root: Xilinx root directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to hw_server executable, or None.
|
||||||
|
"""
|
||||||
|
return _find_executable(
|
||||||
|
"hw_server", vitis_path, platform.system().lower(), xilinx_root
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Tool version detection ───────────────────────────────────────────
|
# ── Tool version detection ───────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -262,7 +362,7 @@ def _get_tool_version(tool_path: str) -> str:
|
|||||||
"""Get the version string from a tool executable.
|
"""Get the version string from a tool executable.
|
||||||
|
|
||||||
Tries (in order):
|
Tries (in order):
|
||||||
1. tool -version
|
1. tool -version → parse version from output
|
||||||
2. Extract from path (e.g., .../Vitis/2023.2/bin/xsct → 2023.2)
|
2. Extract from path (e.g., .../Vitis/2023.2/bin/xsct → 2023.2)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -281,14 +381,17 @@ def _get_tool_version(tool_path: str) -> str:
|
|||||||
)
|
)
|
||||||
output = (result.stdout + result.stderr).strip()
|
output = (result.stdout + result.stderr).strip()
|
||||||
if output:
|
if output:
|
||||||
# Look for a version pattern in output
|
# Scan all lines for a version pattern (skip batch echo noise)
|
||||||
m = re.search(r"(\d{4}\.\d+(?:\.\d+)?)", output)
|
for raw_line in output.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
# Skip Windows batch echo noise
|
||||||
|
if _is_batch_noise(line):
|
||||||
|
continue
|
||||||
|
m = re.search(r"(\d{4}\.\d+(?:\.\d+)?)", line)
|
||||||
if m:
|
if m:
|
||||||
return m.group(1)
|
return m.group(1)
|
||||||
# Return first meaningful line (truncated)
|
|
||||||
line = output.split("\n")[0][:80]
|
|
||||||
if line and "usage" not in line.lower():
|
|
||||||
return line
|
|
||||||
except (subprocess.TimeoutExpired, OSError):
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -296,6 +399,22 @@ def _get_tool_version(tool_path: str) -> str:
|
|||||||
return _version_from_path(tool_path)
|
return _version_from_path(tool_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_batch_noise(line: str) -> bool:
|
||||||
|
"""Check if a line is Windows batch script noise.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line: A line of output text.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the line looks like batch echo/status noise.
|
||||||
|
"""
|
||||||
|
noise_patterns = [
|
||||||
|
r"^ECHO\s+(is\s+(on|off)|处于)", # ECHO is on/off (en+zh)
|
||||||
|
r"^[A-Z]:\\[^>]*>", # Prompt line like C:\path>
|
||||||
|
]
|
||||||
|
return any(re.search(p, line, re.IGNORECASE) for p in noise_patterns)
|
||||||
|
|
||||||
|
|
||||||
# ── Main check function ──────────────────────────────────────────────
|
# ── Main check function ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+217
-244
@@ -1,78 +1,84 @@
|
|||||||
"""Zynq JTAG presence checker for Zynq Flasher GUI.
|
"""Zynq JTAG presence checker for Zynq Flasher GUI.
|
||||||
|
|
||||||
Connects to hw_server via Vivado and scans the JTAG chain to detect
|
Starts hw_server explicitly, then uses Vivado batch-mode TCL to scan
|
||||||
Zynq devices, returning chip model and presence information.
|
the JTAG chain. On failure, Vivado's stdout/stderr is returned in the
|
||||||
Checks for both PS (Processing System) and PL (Programmable Logic)
|
error message for debugging.
|
||||||
recognition on the JTAG chain.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from config_manager import Config
|
from config_manager import Config
|
||||||
|
from vitis_checker import get_vivado_path, get_hw_server_path, build_tool_command
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class JtagDevice:
|
class JtagDevice:
|
||||||
"""Information about a device on the JTAG chain."""
|
"""Information about a device on the JTAG chain."""
|
||||||
|
|
||||||
name: str # Device name from JTAG chain (e.g. "xc7z100_1")
|
name: str
|
||||||
is_zynq: bool # Whether this is a Zynq device
|
idcode: str = ""
|
||||||
is_arm_dap: bool # Whether this is an ARM DAP (PS side)
|
is_zynq: bool = False
|
||||||
is_pl_device: bool # Whether this is a PL device
|
is_arm_dap: bool = False
|
||||||
|
is_pl_device: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ZynqDevice:
|
class ZynqDevice:
|
||||||
"""Information about a detected Zynq device on the JTAG chain."""
|
"""Information about a detected Zynq device on the JTAG chain."""
|
||||||
|
|
||||||
name: str # Device name from JTAG chain (e.g. "xc7z100_1")
|
name: str
|
||||||
part_number: str # Extracted part number (e.g. "XC7Z100")
|
part_number: str
|
||||||
family: str # Family (e.g. "zynq7")
|
family: str
|
||||||
speed: str # Speed grade (e.g. "1", "2", "-1", "-2")
|
speed: str
|
||||||
idcode: str = "" # JTAG IDCODE if available
|
idcode: str = ""
|
||||||
is_programmable: bool = False # Whether the device is programmable
|
is_programmable: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ZynqCheckResult:
|
class ZynqCheckResult:
|
||||||
"""Result of Zynq device detection on the JTAG chain."""
|
"""Result of Zynq device detection on the JTAG chain."""
|
||||||
|
|
||||||
devices: list[ZynqDevice] # All detected Zynq devices
|
devices: list[ZynqDevice]
|
||||||
jtag_chain: list[JtagDevice] # All devices on JTAG chain
|
jtag_chain: list[JtagDevice]
|
||||||
is_present: bool # Whether any Zynq device was found
|
is_present: bool
|
||||||
ps_detected: bool # Whether PS (ARM DAP) is detected
|
ps_detected: bool
|
||||||
pl_detected: bool # Whether PL is detected
|
pl_detected: bool
|
||||||
hw_server_url: str # hw_server connection URL used
|
hw_server_url: str
|
||||||
error: str = "" # Error message if detection failed
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
# Zynq part number patterns from JTAG device names
|
# ── Patterns ──────────────────────────────────────────────────────────
|
||||||
# Examples: xc7z010_1, xc7z020_1, xc7z100_1, xc7z030_1
|
_DEVICE_LINE_RE = re.compile(r"^DEVICE:(\S+)\s+IDCODE:([01]+)")
|
||||||
ZYNQ_PART_PATTERN = re.compile(
|
_ZYNQ_RE = re.compile(r"^xc\d+z\d+[a-z]?\d+", re.IGNORECASE)
|
||||||
r"^(xc\d+z\d+[a-z]?\d+)_\d+$", re.IGNORECASE
|
_ARM_DAP_RE = re.compile(r"^arm_dap", re.IGNORECASE)
|
||||||
)
|
|
||||||
|
|
||||||
# ARM DAP pattern (PS side)
|
|
||||||
ARM_DAP_PATTERN = re.compile(r"^arm_dap_\d+$", re.IGNORECASE)
|
|
||||||
|
|
||||||
# hw_server default port
|
|
||||||
HW_SERVER_PORT = 3121
|
HW_SERVER_PORT = 3121
|
||||||
|
|
||||||
|
|
||||||
def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqCheckResult:
|
def _idcode_to_hex(binary_str: str) -> str:
|
||||||
"""Check for Zynq devices on the JTAG chain via hw_server.
|
try:
|
||||||
|
return f"{int(binary_str, 2):08X}"
|
||||||
|
except ValueError:
|
||||||
|
return binary_str
|
||||||
|
|
||||||
Connects to hw_server using Vivado batch mode, scans the JTAG chain,
|
|
||||||
and identifies Zynq devices by their JTAG device names. Also checks
|
# ── Main entry ────────────────────────────────────────────────────────
|
||||||
for PS (ARM DAP) and PL (Programmable Logic) recognition.
|
|
||||||
|
def check_zynq_jtag(
|
||||||
|
config: Config, callback: Callable | None = None
|
||||||
|
) -> ZynqCheckResult:
|
||||||
|
"""Check for Zynq devices on the JTAG chain.
|
||||||
|
|
||||||
|
1. Start hw_server (Vivado may not auto-start on Windows)
|
||||||
|
2. Run Vivado batch TCL to enumerate JTAG devices
|
||||||
|
3. On failure, include Vivado output in error for debugging
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
@@ -82,178 +88,221 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
|
|||||||
ZynqCheckResult with detected devices and status.
|
ZynqCheckResult with detected devices and status.
|
||||||
"""
|
"""
|
||||||
hw_server_url = f"localhost:{HW_SERVER_PORT}"
|
hw_server_url = f"localhost:{HW_SERVER_PORT}"
|
||||||
|
xilinx_root = getattr(config, "xilinx_path", "")
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback("start", "Connecting to hw_server...")
|
callback("start", "Connecting to hw_server...")
|
||||||
|
|
||||||
# Find vivado executable
|
# ── 1. Find tools ──────────────────────────────────────────
|
||||||
vivado_path = _get_vivado_path(
|
vivado_path = get_vivado_path(xilinx_root=xilinx_root)
|
||||||
config.vitis_path if hasattr(config, 'vitis_path') else ""
|
|
||||||
)
|
|
||||||
if not vivado_path:
|
if not vivado_path:
|
||||||
return ZynqCheckResult(
|
return _fail("Vivado not found", hw_server_url)
|
||||||
devices=[],
|
|
||||||
jtag_chain=[],
|
|
||||||
is_present=False,
|
|
||||||
ps_detected=False,
|
|
||||||
pl_detected=False,
|
|
||||||
hw_server_url=hw_server_url,
|
|
||||||
error="Vivado not found",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create temporary TCL script
|
hw_server_path = get_hw_server_path(xilinx_root=xilinx_root)
|
||||||
tcl_content = _build_jtag_query_tcl()
|
if not hw_server_path:
|
||||||
tcl_path = Path("/tmp/zynq_jtag_check_{}.tcl".format(os.getpid()))
|
return _fail("hw_server not found", hw_server_url)
|
||||||
|
|
||||||
|
# ── 2. Start hw_server ─────────────────────────────────────
|
||||||
|
hw_proc: subprocess.Popen | None = None
|
||||||
try:
|
try:
|
||||||
tcl_path.write_text(tcl_content)
|
hw_proc = subprocess.Popen(
|
||||||
|
build_tool_command(hw_server_path),
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
# Wait up to 10s for hw_server to bind
|
||||||
|
started = False
|
||||||
|
for _ in range(100):
|
||||||
|
time.sleep(0.1)
|
||||||
|
if _port_listening():
|
||||||
|
started = True
|
||||||
|
break
|
||||||
|
if not started:
|
||||||
|
return _fail("hw_server did not bind port 3121 within 10s", hw_server_url)
|
||||||
|
except OSError as e:
|
||||||
|
return _fail(f"Failed to start hw_server: {e}", hw_server_url)
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback("progress", "Scanning JTAG chain...")
|
callback("progress", "Scanning JTAG chain...")
|
||||||
|
|
||||||
# Run Vivado in batch mode
|
# ── 3. Run Vivado TCL ──────────────────────────────────────
|
||||||
cmd = [vivado_path, "-mode", "batch", "-source", str(tcl_path)]
|
tcl = _build_jtag_query_tcl()
|
||||||
|
python_timeout = config.step_timeouts.get("check_env", 120)
|
||||||
|
|
||||||
|
# Redirect Vivado journal/log to temp dir so we don't litter the cwd
|
||||||
|
tmp = tempfile.gettempdir()
|
||||||
|
vivado_args = [
|
||||||
|
"-mode", "batch",
|
||||||
|
"-tempDir", tmp,
|
||||||
|
"-journal", str(Path(tmp) / "vivado_jtag.jou"),
|
||||||
|
"-log", str(Path(tmp) / "vivado_jtag.log"),
|
||||||
|
"-source", tcl,
|
||||||
|
]
|
||||||
|
|
||||||
|
output_lines: list[str] = []
|
||||||
|
try:
|
||||||
|
cmd = build_tool_command(vivado_path, *vivado_args)
|
||||||
|
# Use run() with communicate() under the hood — reliably
|
||||||
|
# drains pipes on Windows even when .bat wrappers are involved
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=30,
|
timeout=python_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
output = result.stdout + result.stderr
|
output = result.stdout + result.stderr
|
||||||
|
for line in output.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped:
|
||||||
|
output_lines.append(stripped)
|
||||||
|
if callback:
|
||||||
|
callback("progress", stripped)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return _fail(
|
||||||
|
f"JTAG scan timed out after {python_timeout}s",
|
||||||
|
hw_server_url,
|
||||||
|
)
|
||||||
|
except OSError as e:
|
||||||
|
return _fail(f"Failed to run Vivado: {e}", hw_server_url)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
Path(tcl).unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for f in ("vivado_jtag.jou", "vivado_jtag.log"):
|
||||||
|
try:
|
||||||
|
(Path(tmp) / f).unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Parse JTAG device information from output
|
output = "\n".join(output_lines)
|
||||||
|
|
||||||
|
# Parse results
|
||||||
jtag_chain = _parse_jtag_chain(output)
|
jtag_chain = _parse_jtag_chain(output)
|
||||||
devices = _filter_zynq_devices(jtag_chain)
|
devices = _filter_zynq_devices(jtag_chain)
|
||||||
|
|
||||||
if callback:
|
|
||||||
if devices:
|
if devices:
|
||||||
|
if callback:
|
||||||
ps_note = "PS" if any(d.is_arm_dap for d in jtag_chain) else ""
|
ps_note = "PS" if any(d.is_arm_dap for d in jtag_chain) else ""
|
||||||
pl_note = "PL" if any(d.is_pl_device for d in jtag_chain) else ""
|
pl_note = "PL" if any(d.is_pl_device for d in jtag_chain) else ""
|
||||||
notes = ", ".join(filter(None, [ps_note, pl_note]))
|
notes = ", ".join(filter(None, [ps_note, pl_note]))
|
||||||
callback("complete", f"Found {len(devices)} Zynq device(s) [{notes}]")
|
callback("complete", f"Found {len(devices)} Zynq device(s) [{notes}]")
|
||||||
else:
|
else:
|
||||||
callback("error", "No Zynq device found on JTAG chain")
|
# Include Vivado output so user can debug
|
||||||
|
err_detail = _extract_relevant(output)
|
||||||
|
msg = "No Zynq device found on JTAG chain"
|
||||||
|
if err_detail:
|
||||||
|
msg += f" | Vivado: {err_detail[:300]}"
|
||||||
|
if callback:
|
||||||
|
callback("error", msg)
|
||||||
|
return ZynqCheckResult(
|
||||||
|
devices=[], jtag_chain=jtag_chain,
|
||||||
|
is_present=False,
|
||||||
|
ps_detected=False, pl_detected=False,
|
||||||
|
hw_server_url=hw_server_url,
|
||||||
|
error=msg,
|
||||||
|
)
|
||||||
|
|
||||||
# Check PS and PL detection
|
|
||||||
ps_detected = any(d.is_arm_dap for d in jtag_chain)
|
ps_detected = any(d.is_arm_dap for d in jtag_chain)
|
||||||
pl_detected = any(d.is_pl_device for d in jtag_chain)
|
pl_detected = any(d.is_pl_device for d in jtag_chain)
|
||||||
|
|
||||||
return ZynqCheckResult(
|
return ZynqCheckResult(
|
||||||
devices=devices,
|
devices=devices, jtag_chain=jtag_chain,
|
||||||
jtag_chain=jtag_chain,
|
is_present=True,
|
||||||
is_present=len(devices) > 0,
|
ps_detected=ps_detected, pl_detected=pl_detected,
|
||||||
ps_detected=ps_detected,
|
|
||||||
pl_detected=pl_detected,
|
|
||||||
hw_server_url=hw_server_url,
|
hw_server_url=hw_server_url,
|
||||||
error="" if devices else "No Zynq device found on JTAG chain",
|
error="",
|
||||||
)
|
)
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return ZynqCheckResult(
|
|
||||||
devices=[],
|
|
||||||
jtag_chain=[],
|
|
||||||
is_present=False,
|
|
||||||
ps_detected=False,
|
|
||||||
pl_detected=False,
|
|
||||||
hw_server_url=hw_server_url,
|
|
||||||
error="JTAG scan timed out after 30s",
|
|
||||||
)
|
|
||||||
except OSError as e:
|
|
||||||
return ZynqCheckResult(
|
|
||||||
devices=[],
|
|
||||||
jtag_chain=[],
|
|
||||||
is_present=False,
|
|
||||||
ps_detected=False,
|
|
||||||
pl_detected=False,
|
|
||||||
hw_server_url=hw_server_url,
|
|
||||||
error=f"OS error: {e}",
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
tcl_path.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _fail(error: str, url: str) -> ZynqCheckResult:
|
||||||
|
return ZynqCheckResult(
|
||||||
|
devices=[], jtag_chain=[], is_present=False,
|
||||||
|
ps_detected=False, pl_detected=False,
|
||||||
|
hw_server_url=url, error=error,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _port_listening(port: int = HW_SERVER_PORT) -> bool:
|
||||||
|
"""Check if hw_server is listening on its TCP port."""
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
with socket.create_connection(("localhost", port), timeout=1):
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_relevant(output: str) -> str:
|
||||||
|
"""Extract error/warning lines from Vivado output for user display."""
|
||||||
|
lines = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if any(kw in line for kw in ("ERROR", "WARNING", "CRITICAL", "FAIL", "cannot", "No")):
|
||||||
|
lines.append(line)
|
||||||
|
return " | ".join(lines[-5:]) # Last 5 relevant lines
|
||||||
|
|
||||||
|
|
||||||
|
# ── TCL builder ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_jtag_query_tcl() -> str:
|
def _build_jtag_query_tcl() -> str:
|
||||||
"""Build TCL script for JTAG device query.
|
"""Build Vivado TCL script file for JTAG device enumeration."""
|
||||||
|
content = """\
|
||||||
Returns:
|
open_hw
|
||||||
TCL script content as string.
|
|
||||||
"""
|
|
||||||
return """
|
|
||||||
open_hw_manager
|
|
||||||
connect_hw_server
|
connect_hw_server
|
||||||
open_hw_target
|
open_hw_target
|
||||||
puts "===JTAG_START==="
|
puts "===JTAG_START==="
|
||||||
foreach dev [get_hw_devices] {
|
foreach dev [get_hw_devices] {
|
||||||
set name [get_property NAME $dev]
|
set name [get_property NAME $dev]
|
||||||
puts "DEVICE:$name"
|
set idcode [get_property IDCODE $dev]
|
||||||
|
puts "DEVICE:$name IDCODE:$idcode"
|
||||||
}
|
}
|
||||||
puts "===JTAG_END==="
|
puts "===JTAG_END==="
|
||||||
|
close_hw
|
||||||
|
disconnect_hw_server
|
||||||
quit
|
quit
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
|
path = Path(tempfile.gettempdir()) / f"zynq_jtag_{os.getpid()}.tcl"
|
||||||
|
path.write_text(content)
|
||||||
|
return str(path)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Parsers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _parse_jtag_chain(output: str) -> list[JtagDevice]:
|
def _parse_jtag_chain(output: str) -> list[JtagDevice]:
|
||||||
"""Parse JTAG chain information from Vivado output.
|
"""Parse Vivado TCL output into JtagDevice list."""
|
||||||
|
|
||||||
Args:
|
|
||||||
output: Combined stdout/stderr from Vivado batch execution.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of JtagDevice objects for all devices on the JTAG chain.
|
|
||||||
"""
|
|
||||||
devices: list[JtagDevice] = []
|
devices: list[JtagDevice] = []
|
||||||
|
section = _extract_section(output, "===JTAG_START===", "===JTAG_END===")
|
||||||
# Extract device list from output
|
if not section:
|
||||||
devices_section = _extract_section(output, "===JTAG_START===", "===JTAG_END===")
|
|
||||||
if not devices_section:
|
|
||||||
return devices
|
return devices
|
||||||
|
for line in section.splitlines():
|
||||||
for line in devices_section.splitlines():
|
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line.startswith("DEVICE:"):
|
if not line:
|
||||||
device_name = line[len("DEVICE:"):]
|
continue
|
||||||
device = _classify_jtag_device(device_name)
|
match = _DEVICE_LINE_RE.match(line)
|
||||||
devices.append(device)
|
if match:
|
||||||
|
name = match.group(1)
|
||||||
|
idcode_hex = _idcode_to_hex(match.group(2))
|
||||||
|
devices.append(_classify_jtag_device(name, idcode_hex))
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
|
||||||
def _filter_zynq_devices(jtag_chain: list[JtagDevice]) -> list[ZynqDevice]:
|
def _filter_zynq_devices(jtag_chain: list[JtagDevice]) -> list[ZynqDevice]:
|
||||||
"""Filter Zynq devices from JTAG chain.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
jtag_chain: List of all JTAG devices.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of ZynqDevice objects.
|
|
||||||
"""
|
|
||||||
zynq_devices: list[ZynqDevice] = []
|
zynq_devices: list[ZynqDevice] = []
|
||||||
for jtag_dev in jtag_chain:
|
for jtag_dev in jtag_chain:
|
||||||
if jtag_dev.is_zynq:
|
if jtag_dev.is_zynq:
|
||||||
zynq_dev = _parse_zynq_device(jtag_dev.name)
|
zynq_dev = _parse_zynq_device(jtag_dev.name, jtag_dev.idcode)
|
||||||
if zynq_dev:
|
if zynq_dev:
|
||||||
zynq_devices.append(zynq_dev)
|
zynq_devices.append(zynq_dev)
|
||||||
return zynq_devices
|
return zynq_devices
|
||||||
|
|
||||||
|
|
||||||
def _extract_section(
|
def _extract_section(text: str, start_marker: str, end_marker: str) -> str | None:
|
||||||
text: str, start_marker: str, end_marker: str
|
|
||||||
) -> str | None:
|
|
||||||
"""Extract text between two markers, handling duplicate markers.
|
|
||||||
|
|
||||||
Finds the last occurrence of each marker to avoid matching
|
|
||||||
markers inside TCL script source code.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: Full output text.
|
|
||||||
start_marker: Starting marker string.
|
|
||||||
end_marker: Ending marker string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Text between markers, or None if not found.
|
|
||||||
"""
|
|
||||||
# Find last occurrence of each marker to avoid matching
|
|
||||||
# markers inside TCL script source code
|
|
||||||
start_idx = text.rfind(start_marker)
|
start_idx = text.rfind(start_marker)
|
||||||
end_idx = text.rfind(end_marker)
|
end_idx = text.rfind(end_marker)
|
||||||
if start_idx == -1 or end_idx == -1 or start_idx >= end_idx:
|
if start_idx == -1 or end_idx == -1 or start_idx >= end_idx:
|
||||||
@@ -261,105 +310,37 @@ def _extract_section(
|
|||||||
return text[start_idx + len(start_marker):end_idx].strip()
|
return text[start_idx + len(start_marker):end_idx].strip()
|
||||||
|
|
||||||
|
|
||||||
def _classify_jtag_device(device_name: str) -> JtagDevice:
|
def _classify_jtag_device(device_name: str, idcode: str = "") -> JtagDevice:
|
||||||
"""Classify a JTAG device by its name.
|
is_zynq = bool(_ZYNQ_RE.match(device_name))
|
||||||
|
is_arm_dap = bool(_ARM_DAP_RE.match(device_name))
|
||||||
Args:
|
|
||||||
device_name: Device name from JTAG chain.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JtagDevice with classification information.
|
|
||||||
"""
|
|
||||||
is_zynq = bool(ZYNQ_PART_PATTERN.match(device_name))
|
|
||||||
is_arm_dap = bool(ARM_DAP_PATTERN.match(device_name))
|
|
||||||
is_pl_device = is_zynq and not is_arm_dap
|
|
||||||
|
|
||||||
return JtagDevice(
|
return JtagDevice(
|
||||||
name=device_name,
|
name=device_name,
|
||||||
|
idcode=idcode,
|
||||||
is_zynq=is_zynq,
|
is_zynq=is_zynq,
|
||||||
is_arm_dap=is_arm_dap,
|
is_arm_dap=is_arm_dap,
|
||||||
is_pl_device=is_pl_device,
|
is_pl_device=is_zynq and not is_arm_dap,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_zynq_device(device_name: str) -> ZynqDevice | None:
|
def _parse_zynq_device(device_name: str, idcode: str = "") -> ZynqDevice | None:
|
||||||
"""Parse a Zynq device from its JTAG chain name.
|
match = _ZYNQ_RE.match(device_name)
|
||||||
|
|
||||||
Args:
|
|
||||||
device_name: Device name from JTAG chain (e.g. "xc7z100_1").
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ZynqDevice if it's a Zynq chip, None otherwise.
|
|
||||||
"""
|
|
||||||
match = ZYNQ_PART_PATTERN.match(device_name)
|
|
||||||
if not match:
|
if not match:
|
||||||
return None
|
return None
|
||||||
|
base = device_name.rsplit("_", 1)[0] if "_" in device_name else device_name
|
||||||
part_base = match.group(1)
|
part_number = base.upper()
|
||||||
# Convert to uppercase for part number
|
|
||||||
part_number = part_base.upper()
|
|
||||||
|
|
||||||
# Extract family and speed from part number
|
|
||||||
# Examples: XC7Z010 -> family=zynq7, speed=1; XC7Z100 -> family=zynq7, speed=1
|
|
||||||
family = "zynq7"
|
family = "zynq7"
|
||||||
speed = "1" # Default speed grade
|
|
||||||
|
|
||||||
# Parse speed grade from the numeric suffix
|
|
||||||
speed_match = re.search(r"(\d+)$", part_base)
|
|
||||||
if speed_match:
|
|
||||||
speed_num = int(speed_match.group(1))
|
|
||||||
# Map numeric speed to speed grade string
|
|
||||||
if speed_num >= 2:
|
|
||||||
speed = "-2"
|
|
||||||
elif speed_num >= 1:
|
|
||||||
speed = "-1"
|
speed = "-1"
|
||||||
|
speed_match = re.search(r"(\d+)$", part_number)
|
||||||
|
if speed_match:
|
||||||
|
speed = "-2" if int(speed_match.group(1)) >= 2 else "-1"
|
||||||
return ZynqDevice(
|
return ZynqDevice(
|
||||||
name=device_name,
|
name=device_name, part_number=part_number,
|
||||||
part_number=part_number,
|
family=family, speed=speed,
|
||||||
family=family,
|
idcode=idcode, is_programmable=True,
|
||||||
speed=speed,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_vivado_path(vitis_path: str) -> str | None:
|
|
||||||
"""Find the Vivado executable path.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
vitis_path: Vitis installation path.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to Vivado executable, or None if not found.
|
|
||||||
"""
|
|
||||||
path = shutil.which("vivado")
|
|
||||||
if path:
|
|
||||||
return path
|
|
||||||
if vitis_path:
|
|
||||||
candidate = Path(vitis_path) / "bin" / "vivado"
|
|
||||||
if candidate.exists():
|
|
||||||
return str(candidate)
|
|
||||||
candidate = Path(vitis_path) / "bin" / "vivado.exe"
|
|
||||||
if candidate.exists():
|
|
||||||
return str(candidate)
|
|
||||||
common_paths = [
|
|
||||||
"/opt/xilinx/bin/vivado",
|
|
||||||
os.path.expanduser("~/Xilinx/Vivado/bin/vivado"),
|
|
||||||
]
|
|
||||||
for p in common_paths:
|
|
||||||
if os.path.isfile(p):
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_zynq_status_dict(result: ZynqCheckResult) -> dict:
|
def get_zynq_status_dict(result: ZynqCheckResult) -> dict:
|
||||||
"""Convert a ZynqCheckResult to a serializable dictionary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
result: ZynqCheckResult from check_zynq_jtag().
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary suitable for JSON serialization or GUI display.
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
"is_present": result.is_present,
|
"is_present": result.is_present,
|
||||||
"ps_detected": result.ps_detected,
|
"ps_detected": result.ps_detected,
|
||||||
@@ -367,23 +348,15 @@ def get_zynq_status_dict(result: ZynqCheckResult) -> dict:
|
|||||||
"hw_server_url": result.hw_server_url,
|
"hw_server_url": result.hw_server_url,
|
||||||
"error": result.error,
|
"error": result.error,
|
||||||
"jtag_chain": [
|
"jtag_chain": [
|
||||||
{
|
{"name": d.name, "idcode": d.idcode,
|
||||||
"name": dev.name,
|
"is_zynq": d.is_zynq, "is_arm_dap": d.is_arm_dap,
|
||||||
"is_zynq": dev.is_zynq,
|
"is_pl_device": d.is_pl_device}
|
||||||
"is_arm_dap": dev.is_arm_dap,
|
for d in result.jtag_chain
|
||||||
"is_pl_device": dev.is_pl_device,
|
|
||||||
}
|
|
||||||
for dev in result.jtag_chain
|
|
||||||
],
|
],
|
||||||
"devices": [
|
"devices": [
|
||||||
{
|
{"name": d.name, "part_number": d.part_number,
|
||||||
"name": dev.name,
|
"family": d.family, "speed": d.speed,
|
||||||
"part_number": dev.part_number,
|
"idcode": d.idcode, "is_programmable": d.is_programmable}
|
||||||
"family": dev.family,
|
for d in result.devices
|
||||||
"speed": dev.speed,
|
|
||||||
"idcode": dev.idcode,
|
|
||||||
"is_programmable": dev.is_programmable,
|
|
||||||
}
|
|
||||||
for dev in result.devices
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#-----------------------------------------------------------
|
|
||||||
# Vivado v2023.2 (64-bit)
|
|
||||||
# SW Build 4029153 on Fri Oct 13 20:13:54 MDT 2023
|
|
||||||
# IP Build 4028589 on Sat Oct 14 00:45:43 MDT 2023
|
|
||||||
# SharedData Build 4025554 on Tue Oct 10 17:18:54 MDT 2023
|
|
||||||
# Start of session at: Wed Jun 10 15:01:29 2026
|
|
||||||
# Process ID: 1790122
|
|
||||||
# Current directory: /data/work/prj/Zynq_Flasher
|
|
||||||
# Command line: vivado -mode batch -source /tmp/zynq_jtag_check_1789776.tcl
|
|
||||||
# Log file: /data/work/prj/Zynq_Flasher/vivado.log
|
|
||||||
# Journal file: /data/work/prj/Zynq_Flasher/vivado.jou
|
|
||||||
# Running On: mkb, OS: Linux, CPU Frequency: 5069.987 MHz, CPU Physical cores: 32, Host memory: 134146 MB
|
|
||||||
#-----------------------------------------------------------
|
|
||||||
source /tmp/zynq_jtag_check_1789776.tcl
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#-----------------------------------------------------------
|
|
||||||
# Vivado v2023.2 (64-bit)
|
|
||||||
# SW Build 4029153 on Fri Oct 13 20:13:54 MDT 2023
|
|
||||||
# IP Build 4028589 on Sat Oct 14 00:45:43 MDT 2023
|
|
||||||
# SharedData Build 4025554 on Tue Oct 10 17:18:54 MDT 2023
|
|
||||||
# Start of session at: Wed Jun 10 15:02:25 2026
|
|
||||||
# Process ID: 1790665
|
|
||||||
# Current directory: /data/work/prj/Zynq_Flasher
|
|
||||||
# Command line: vivado -mode batch -source /tmp/zynq_jtag_check_1789776.tcl
|
|
||||||
# Log file: /data/work/prj/Zynq_Flasher/vivado.log
|
|
||||||
# Journal file: /data/work/prj/Zynq_Flasher/vivado.jou
|
|
||||||
# Running On: mkb, OS: Linux, CPU Frequency: 5073.251 MHz, CPU Physical cores: 32, Host memory: 134146 MB
|
|
||||||
#-----------------------------------------------------------
|
|
||||||
source /tmp/zynq_jtag_check_1789776.tcl
|
|
||||||
Binary file not shown.
Reference in New Issue
Block a user