Compare commits

...
27 Commits
Author SHA1 Message Date
yuysh c0f7219d30 fix: replace Popen readline loop with subprocess.run for Windows pipe reliability; add close_hw before quit in TCL; revert console=True 2026-06-12 17:46:11 +08:00
yuysh 4da03e97d8 fix: open_hw_manager→open_hw for 2018.3 batch mode compat; revert console=True 2026-06-12 17:37:54 +08:00
yuysh c9eae220f0 Fix naming 2026-06-12 17:24:01 +08:00
yuysh e09f16a3aa fix: Xilinx 2018.3 tool discovery — add SDK dir, .bat PATH search, older versions 2026-06-12 17:20:49 +08:00
yuysh 47ea614ba2 fix: switch to console=False (GUI only, no terminal window) 2026-06-12 16:56:56 +08:00
yuysh 13ed0d89f8 fix: resolve config.yaml path to exe directory when frozen (PyInstaller) 2026-06-12 16:55:35 +08:00
yuysh 0d66f65ef2 feat: add PyInstaller spec + Windows build script (console=True) 2026-06-12 16:51:26 +08:00
yuysh 4c01acdf69 fix: print log messages to terminal in addition to GUI 2026-06-12 16:38:51 +08:00
yuysh c0cac5b2c5 fix: ping_ip bool unpack, skip sub-step visibility, IP 192.168.100.11 after Step 3
- ping_ip returns bool, not tuple — fixed 2 unpack errors
- TftpSubStepFrame: _skipped set prevents set_phase from overwriting Skip
- set_expanded(True) before skip marking so UI shows it immediately
- After Step 3 success, fix IP to 192.168.100.11
2026-06-12 16:35:32 +08:00
yuysh 5ed195e2d6 fix: skip marking, fixed panel ratio, dialog hints, 30s boot, ping+UDP fallback
- TftpSubStepFrame.set_step_skipped(): ◌ Skip in WARNING color
- _reload_config(): suppress callbacks to avoid duplicate log/status wipe
- _log_all_file_info(): removed clear_info(), updates in-place per-key
- Removed duplicate _log_file_info method
- main_frame grid: uniform col group for strict 7:3 ratio
- FileSelector browse title shows expected file type hint
- boot_wait_delay: default 10→30s
- Step 4 IP fallback: ping first, then UDP scan, extra boot_delay retry
2026-06-12 16:10:28 +08:00
yuysh 639f2e3ce6 feat: 5 improvements — file info, download verify, UART warning, status display, COM port reuse
- file_utils: compute_file_info() — CRC32, human-readable size, mtime
- StatusDisplay: simplified to info-only key-value panel; status messages
  removed (log-only); file info shown in status panel at startup/select
- config: added download_verify checkbox (default ON), skips Step 4.2
- UART warning: CTkTextbox with word-wrap and auto-height
- COM port: check_uart_available / test_serial_version accept optional
  UartMonitor to avoid redundant serial opens (Error 13 on Windows)
- layout: left:right 7:3 ratio, config panel stretches full width
2026-06-12 15:47:40 +08:00
yuysh 9502bba636 fix: config save broken (relative_to→relpath), startup validation, auto-create, untrack config.yaml
- _relative_path(): replace Path.relative_to() with os.path.relpath()
  (ValueError crossing filesystems silently returned absolute paths)
- _sync_config_from_ui(): always derive relative from get_path() absolute,
  never trust stale FileSelector._relative_path after Browse
- validate_paths(): check all file paths exist on startup, clear missing
- Clear UI selectors after validation to prevent re-save of stale paths
- _load_config(): auto-create blank config.yaml when missing on disk
- .gitignore: add config.yaml (user-specific, must not be tracked)
2026-06-12 15:02:09 +08:00
yuysh 9238cbc63e fix: correct indentation errors in widgets.py (TftpSubStepFrame, UartMonitorWindow) 2026-06-12 14:14:17 +08:00
yuysh f330f8c276 fix: remove 'uboot'/'u-boot' from fail keywords — too broad
Xilinx program_flash often outputs informational lines containing
'uboot' (mini u-boot image file, U-Boot version banner) which are
not errors. The 'failed' keyword already catches 'U-Boot failed'
and 'uboot failed' in actual failure scenarios.
2026-06-11 18:13:30 +08:00
yuysh 04ad61e694 fix: convert all TCL-embedded paths to forward slashes (cross-platform)
Windows backslashes in TCL strings are interpreted as escape sequences
(\f → form feed, \t → tab). This caused 'dow "Files\fsbl..."' to
fail with garbled paths — TCL couldn't find the file.

Added _tcl_path() helper: replaces \ with / in any path before
embedding in TCL. Forward slashes work on all platforms including
Windows. Applied to all 7 path sites: source, fpga -file, dow.
2026-06-11 18:11:44 +08:00
yuysh 4ca15c1190 fix: detect flash failure from output even when returncode is 0
program_flash can return exit code 0 while outputting errors like
'uboot failed' or 'cannot open'. Previously only returncode was
checked, so these silent failures caused the workflow to continue
past a failed erase/program step.

Now scans combined stdout+stderr for failure keywords
(uboot, failed, cannot, abort, invalid, unsupported, timeout)
and returns success=False. Also checks returncode != 0 as fallback.
2026-06-11 18:00:49 +08:00
yuysh 4453fc8e12 feat: validate JTAG Zynq part matches config.zynq_part (default XC7Z100)
Step 1 now compares detected part (e.g. XC7Z035) against config's
zynq_part. If mismatch: log error, return False to block subsequent
steps. Also returns False when no Zynq detected (was always True).
2026-06-11 17:38:27 +08:00
yuysh 799da5b2f3 fix: Windows common paths try .bat before .exe (Xilinx tools are .bat)
Vitis/Vivado on Windows uses .bat wrappers, not .exe. The hardcoded
fallback paths in _find_executable step 4 only looked for .exe files.
Also expanded to try both 2023.2 and 2022.2 + Vitis/Vivado combos.
2026-06-11 17:34:36 +08:00
yuysh 7e8427d522 fix: redirect Vivado .jou/.log to temp dir with -tempDir
Vivado batch mode creates vivado.jou, vivado.log, and backup files
in cwd by default. Use -tempDir to redirect all temp output to the
system temp directory, and clean up afterwards.
2026-06-11 17:26:41 +08:00
yuysh 66c04da5e0 fix: stream Vivado output live, remove broken normalize_port
JTAG: Start hw_server explicitly (Windows needs it), then run Vivado
TCL with Popen + line-by-line stdout streaming via callback so user
can see Vivado progress in real-time (not just on failure).

COM: Remove normalize_port — pyserial >= 3.0 handles COM>=10 prefix
internally. The manual \\.\ prefix was causing PermissionError(13)
on Windows where pyserial's own handling conflicted.
2026-06-11 17:24:31 +08:00
yuysh 954aed7c0d chore: expand .gitignore for all Vivado/Xilinx temp files
Added: vivado.jou, vivado.log, vivado_*.backup.log, vivado_*.str,
hs_err_pid*.log (Java crash), webtalk*, usage_statistics_*
2026-06-11 17:03:18 +08:00
yuysh 474e3f122b fix: use Vivado TCL for JTAG check — xsct 'targets' has broken TCL list parsing
xsct's [targets] returns unquoted TCL lists where multi-word names
(e.g. 'ARM Cortex-A9 MPCore #0') are split into individual words
by foreach, making it impossible to reconstruct device names.

Vivado TCL with open_hw_manager works reliably:
- Cold start (no hw_server): 7.1s on Linux
- hw_server already running: 6.8s on Linux
- Provides IDCODE (converted from Vivado binary to hex), device names

Smart hw_server lifecycle:
- Check port 3121 before starting — reuse if already running
- Start hw_server only when needed (poll port up to 3s)
- Don't kill hw_server that we didn't start

Output format parsed: DEVICE:arm_dap_0 IDCODE:010010111010...0111
→ arm_dap_0 idcode=4BA00477 PS detected
→ xc7z100_1 idcode=03736093 PL detected → XC7Z100

Tested on Linux 2023.2, cold + warm start, confirmed working.
2026-06-11 17:02:23 +08:00
yuysh dd35ab0846 fix: use xsct 'jtag targets' for raw JTAG chain with IDCODE
The previous TCL used 'targets' which returns XSCT target IDs (numbers
like 1, 2, APU, xc7z100) — each word was parsed as a separate device,
producing '1 (Other)', 'APU (Other)', etc.

Switch to 'jtag targets' which outputs raw JTAG chain devices:
  1  xc7z100 (idcode 03736093 irlen 4)
  2  arm_dap (idcode 4BA00477 irlen 4)

Changes:
- New _JTAG_LINE_RE pattern for parsing jtag targets output
- Zynq pattern relaxed for xsct names (xc7z100 not xc7z100_1)
- ARM DAP pattern: arm_dap (not arm_dap_N)
- JtagDevice now carries idcode field
- ZynqDevice propagates idcode + is_programmable=True
- Removed unused tempfile/Path/os imports
2026-06-11 16:45:37 +08:00
yuysh 86d65641cc chore: gitignore Vivado/JTAG temp files, delete existing ones 2026-06-11 16:37:21 +08:00
yuysh 7716171aeb fix: use xsct (not Vivado) for JTAG check + normalize COM>=10 ports on Windows
**JTAG check**: Switch from Vivado to xsct
- xsct is already proven to work (JTAG reboot succeeds)
- Much lighter than Vivado (no GUI infrastructure / open_hw_manager)
- Uses -eval inline TCL: connect; targets; disconnect; quit
- Removes dead tempfile/Path/os imports

**Windows COM port**: Add normalize_port() to serial_monitor.py
- COM ports >= 10 require NT namespace prefix (\.\COM11)
- Without it: PermissionError(13, '拒绝访问。')
- Applied at all 7 call sites: serial_monitor (5), reboot_manager, widgets
2026-06-11 16:29:32 +08:00
yuysh 91417f609e fix: start hw_server explicitly before Vivado JTAG scan
Previously the TCL script relied on Vivado's connect_hw_server to
auto-start hw_server. On Windows this either never worked (hw_server
never shows in Task Manager) or hung indefinitely during Vivado's
slow batch-mode initialisation.

Root cause: hw_server was never started.

Changes:
- vitis_checker.py: add 'hw_server' to TOOL_ALIASES + get_hw_server_path()
- zynq_checker.py: start hw_server via Popen before Vivado, kill in finally
  - 2s sleep for port binding after start
  - timeout from config.step_timeouts.check_env (default bumped to 120s)
  - clean terminate/kill sequence in finally
- config_manager.py: check_env default 30 → 120s
2026-06-11 15:57:53 +08:00
yuysh 72d0eceb8d fix: don't restart Boot Verify pulse after step 4 already completed it
Step 4's _run_step_4_load_main_program() already marks each sub-step
individually via set_step_complete/set_step_error. But the outer
_run_steps_worker() called set_phase('Boot Verify') on success, which
re-entered the 'running' phase and started a fresh pulse animation
(because _stop_pulse cleared _pulse_job).

Fix: on success, do nothing (sub-steps are already green). Only set
error on failure.
2026-06-11 15:49:10 +08:00
16 changed files with 1286 additions and 488 deletions
+15 -1
View File
@@ -33,8 +33,22 @@ htmlcov/
# ── Opencode (internal tooling) ──────────────────────────────
.opencode/
# ── Config overrides (user-specific) ─────────────────────────
# ── Config (user-specific, auto-generated if missing) ─────────
config.yaml
config/zynq_flasher.yaml
# ── Logs ─────────────────────────────────────────────────────
*.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
+63
View File
@@ -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,
)
+47
View File
@@ -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
View File
@@ -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
+20 -9
View File
@@ -37,6 +37,15 @@ ProgressCallback = Callable[[str, str], None] | None
# ── Helpers ─────────────────────────────────────────────────────────
def _tcl_path(path: str | Path) -> str:
"""Convert a filesystem path to TCL-safe format.
TCL interprets backslashes as escape sequences (\\f, \\t, \\n).
Forward slashes work on all platforms including Windows.
"""
return str(path).replace("\\", "/")
def _find_ps7_init_tcl(config: Config) -> str:
"""Find the ps7_init.tcl file for PS initialization.
@@ -187,11 +196,11 @@ def _program_with_xsct(
lines = ["connect"]
if ps7_init_tcl and Path(ps7_init_tcl).exists():
lines.append("targets 1")
lines.append(f'source "{ps7_init_tcl}"')
lines.append(f'source "{_tcl_path(ps7_init_tcl)}"')
lines.append("ps7_init")
if callback:
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():
lines.append("ps7_post_config")
if callback:
@@ -262,12 +271,14 @@ def _program_with_vivado(
callback("progress", "Using Vivado Hardware Manager...")
tcl_content = f"""
open_hw_manager
open_hw
connect_hw_server
open_hw_target
current_hw_device [lindex [get_hw_devices] 0]
program_hw_device -file "{bit_path}"
refresh_hw_device
close_hw
disconnect_hw_server
quit
"""
tcl_path = bit_path.parent / "temp_program.tcl"
@@ -381,13 +392,13 @@ def _run_elf_via_fsbl(
tcl = "\n".join([
"connect",
"targets 2",
"dow \"%s\"" % fsbl_path,
"dow {%s}" % _tcl_path(fsbl_path),
"puts \"FSBL downloaded\"",
"con",
"after 3000",
"stop",
"puts \"FSBL done, CPU halted\"",
"dow \"%s\"" % elf_path,
"dow {%s}" % _tcl_path(elf_path),
"mwr 0xF800025C 0x00000001",
"con",
"puts \"App running\"",
@@ -461,7 +472,7 @@ def _run_elf_direct(
tcl = "\n".join([
"connect",
"targets 2",
"dow \"%s\"" % elf_path,
"dow {%s}" % _tcl_path(elf_path),
"mwr 0xF800025C 0x00000001",
"con",
"exit",
@@ -620,18 +631,18 @@ def full_bitstream_program(
"rst -processor",
"after 500",
'puts "INFO: Downloading FSBL..."',
'dow "%s"' % fsbl_path,
'dow {%s}' % _tcl_path(fsbl_path),
'puts "INFO: Running FSBL for hardware initialization..."',
"con",
"after 2000",
"stop",
'puts "INFO: Hardware initialization completed."',
'puts "INFO: Downloading Bitstream..."',
'fpga -f "%s"' % bit_path,
'fpga -f {%s}' % _tcl_path(bit_path),
"after 1000",
'targets -set -nocase -filter {name =~ "arm*#0"}',
'puts "INFO: Downloading Application ELF..."',
'dow "%s"' % elf_path,
'dow {%s}' % _tcl_path(elf_path),
'puts "INFO: Executing Application..."',
"con",
'puts "SUCCESS: Zynq target is running the specified application."',
+42 -5
View File
@@ -17,6 +17,7 @@ import yaml
DEFAULT_CONFIG: dict[str, Any] = {
"xilinx_path": "",
"zynq_ip": "192.168.100.11",
"zynq_part": "XC7Z100",
"tftp_upload_name": "z7bin",
"serial_port": "",
"serial_baudrate": 115200,
@@ -27,15 +28,16 @@ DEFAULT_CONFIG: dict[str, Any] = {
"flash_type": "qspi-x4-single",
"flash_model": "s25fl256s1",
"erase_all": False,
"download_verify": True,
"firmware_bin_path": "",
"reboot_timeout": 30,
"ping_timeout": 5,
"ping_count": 3,
"uart_delay": 3,
"inter_step_delay": 2,
"boot_wait_delay": 10,
"boot_wait_delay": 30,
"step_timeouts": {
"check_env": 30,
"check_env": 120,
"flash_erase": 120,
"flash_program": 600,
"bitstream": 60,
@@ -55,6 +57,7 @@ class Config:
xilinx_path: str = ""
zynq_ip: str = "192.168.100.11"
zynq_part: str = "XC7Z100"
tftp_upload_name: str = "z7bin"
serial_port: str = ""
serial_baudrate: int = 115200
@@ -65,13 +68,14 @@ class Config:
flash_type: str = "qspi-x4-single"
flash_model: str = "s25fl256s1"
erase_all: bool = False
download_verify: bool = True
firmware_bin_path: str = ""
reboot_timeout: int = 30
ping_timeout: int = 5
ping_count: int = 3
uart_delay: int = 3
inter_step_delay: int = 2
boot_wait_delay: int = 10
boot_wait_delay: int = 30
step_timeouts: dict[str, int] = field(default_factory=lambda: {})
# Internal: path to the config file on disk
@@ -174,6 +178,7 @@ class Config:
"flash_type": self.flash_type,
"flash_model": self.flash_model,
"erase_all": self.erase_all,
"download_verify": self.download_verify,
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
"reboot_timeout": self.reboot_timeout,
"ping_timeout": self.ping_timeout,
@@ -188,8 +193,11 @@ class Config:
def _relative_path(self, path: str) -> str:
"""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:
path: Absolute path string.
path: Absolute or relative path string.
Returns:
Relative path string, or the original if conversion fails.
@@ -199,7 +207,8 @@ class Config:
try:
target = Path(path)
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):
pass
return path
@@ -217,6 +226,34 @@ class Config:
return Path()
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
def config_path(self) -> Path | None:
"""Return the path to the config file on disk."""
+82
View File
@@ -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
+60 -8
View File
@@ -10,6 +10,7 @@ Compatible with Xilinx 2018.3 — 2023.2.
from __future__ import annotations
import binascii
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
@@ -35,6 +36,27 @@ ProgressCallback = Callable[[str, str], None] | None
# ── 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(
cmd: list[str],
@@ -167,12 +189,27 @@ def wipe_flash(
callback=callback,
)
success = result.returncode == 0
combined = result.stdout + result.stderr
err_line = _output_has_error(combined)
if err_line:
return FlashResult(
step="wipe",
success=False,
message=f"Flash erase failed: {err_line}",
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=success,
message="Flash erased successfully" if success else "Flash erase failed",
output=(result.stdout + result.stderr)[:4000],
success=True,
message="Flash erased successfully",
output=combined[:4000],
)
except subprocess.TimeoutExpired:
return FlashResult(step="wipe", success=False, message="Flash erase timed out")
@@ -252,12 +289,27 @@ def program_flash_bin(
try:
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(
step="program",
success=False,
message=f"Flash programming failed: {err_line}",
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=success,
message="Flash programmed successfully" if success else "Flash programming failed",
output=(result.stdout + result.stderr)[:4000],
success=True,
message="Flash programmed successfully",
output=combined[:4000],
)
except subprocess.TimeoutExpired:
return FlashResult(step="program", success=False, message="Flash programming timed out (600s)")
+514 -111
View File
@@ -7,6 +7,7 @@ built with CustomTkinter.
from __future__ import annotations
import os
import sys
import threading
import time
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 boot_verifier import verify_boot, verify_zynq_status, BootVerificationResult
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 (
WINDOW_WIDTH,
WINDOW_HEIGHT,
@@ -92,6 +94,35 @@ class MainWindow(ctk.CTk):
self._load_config()
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()
# Handle window close
@@ -109,6 +140,18 @@ class MainWindow(ctk.CTk):
# ── 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:
"""Find the user configuration file.
@@ -120,8 +163,7 @@ class MainWindow(ctk.CTk):
Returns:
Path to user config file, or None if not found.
"""
# Navigate from src/gui/main_window.py -> project root
app_dir = Path(__file__).parent.parent.parent
app_dir = self._get_app_dir()
# 1. Check for user config at project root
user_config = app_dir / "config.yaml"
@@ -144,43 +186,62 @@ class MainWindow(ctk.CTk):
def _get_user_config_path(self) -> Path:
"""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:
Path to user config file.
"""
# Navigate from src/gui/main_window.py -> project root
app_dir = Path(__file__).parent.parent.parent
app_dir = self._get_app_dir()
config_path = app_dir / "config.yaml"
config_path.parent.mkdir(parents=True, exist_ok=True)
return config_path
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:
try:
self._config = Config.from_file(self._config_path)
if self._config_path.name == "default_config.yaml":
user_config_path = self._get_user_config_path()
self._config._config_path = user_config_path
return
except Exception:
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
pass # Fall through and create default
# Config file missing or unreadable — create a blank one
self._config = Config.from_default()
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:
"""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()
if not user_path.exists():
return
try:
fresh = Config.from_file(user_path)
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 [
("bootloader_bit_path", self._bit_selector),
("bootloader_elf_path", self._elf_selector),
@@ -190,7 +251,11 @@ class MainWindow(ctk.CTk):
]:
val = getattr(fresh, attr, "")
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
if self._ip_string_var:
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
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:
return
# Sync IP address
# Sync IP address (may not exist yet during _build_config_panel)
if self._ip_string_var:
self._config.zynq_ip = self._ip_string_var.get()
# Sync Xilinx Kit path
self._config.xilinx_path = self._xilinx_path_var.get()
# Sync serial port
self._config.serial_port = self._port_var.get()
if hasattr(self, '_xilinx_path_var'):
self._config.xilinx_path = self._xilinx_path_var.get()
# Sync serial port (may not exist yet during _build_config_panel)
if hasattr(self, '_port_var'):
self._config.serial_port = self._port_var.get()
# Sync file paths from FileSelectors
files = self._get_selected_files()
self._config.bootloader_bit_path = files["bootloader_bit_path"]
self._config.bootloader_elf_path = files["bootloader_elf_path"]
self._config.bootloader_bin_path = files["bootloader_bin_path"]
self._config.fsbl_elf_path = files["fsbl_elf_path"]
self._config.erase_all = self._erase_cb_var.get()
self._config.firmware_bin_path = files["firmware_bin_path"]
# Always derive relative from absolute — the _relative_path
# stored in FileSelector can be stale after Browse.
for attr, selector in [
("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),
]:
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()
# 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:
"""Save current configuration to user config file (config.yaml).
@@ -250,32 +355,101 @@ class MainWindow(ctk.CTk):
if self._config:
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:
"""Open directory dialog to select Xilinx root folder."""
from tkinter import filedialog
path = filedialog.askdirectory(title="Select Xilinx Root Directory")
if path:
self._xilinx_path_var.set(path)
if self._config:
self._config.xilinx_path = path
# Re-check tools with new path
self._check_vitis()
self._on_xilinx_path_changed()
# Re-check tools with new path
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 ────────────────────────────────────────
def _build_ui(self) -> None:
"""Build the complete UI layout."""
# Scrollable main container — scrollbar appears when content overflows
main_frame = ctk.CTkScrollableFrame(self)
main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
main_frame.grid_rowconfigure(1, weight=1)
main_frame.grid_columnconfigure(0, weight=1)
self.main_frame = ctk.CTkScrollableFrame(self)
self.main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
self.main_frame.grid_rowconfigure(1, 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 ──
self._build_header(main_frame)
self._build_header(self.main_frame)
# ── 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_rowconfigure(1, weight=1)
left_frame.grid_columnconfigure(0, weight=1)
@@ -284,9 +458,9 @@ class MainWindow(ctk.CTk):
self._build_log_panel(left_frame)
# ── 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_rowconfigure(3, weight=1)
right_frame.grid_rowconfigure(3, weight=3) # status panel — main info area
right_frame.grid_columnconfigure(0, weight=1)
self._build_config_panel(right_frame)
@@ -294,6 +468,10 @@ class MainWindow(ctk.CTk):
self._build_utility_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:
"""Build the application header."""
header = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
@@ -335,6 +513,7 @@ class MainWindow(ctk.CTk):
row=0, column=0, sticky="nsew",
padx=PADDING, pady=(PADDING, 0),
)
panel.grid_rowconfigure(0, weight=1)
panel.grid_columnconfigure(0, weight=1)
# ── Title ──
@@ -448,6 +627,7 @@ class MainWindow(ctk.CTk):
row=1, column=0, sticky="nsew",
padx=PADDING, pady=PADDING,
)
self._log_panel = panel
panel.grid_rowconfigure(1, weight=1)
panel.grid_columnconfigure(0, weight=1)
@@ -479,6 +659,7 @@ class MainWindow(ctk.CTk):
padx=PADDING, pady=PADDING,
)
panel.grid_rowconfigure(9, weight=1)
panel.grid_columnconfigure(0, weight=1) # let content stretch
title = ctk.CTkLabel(
panel,
@@ -496,6 +677,8 @@ class MainWindow(ctk.CTk):
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.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(
xilinx_frame, text="Browse", font=FONT_SMALL, width=70,
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_entry = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
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)
self._fsbl_selector = FileSelector(
panel,
label_text="FSBL ELF (.elf):",
file_types=[("ELF files", "*.elf"), ("All files", "*")],
callback=self._on_file_selected,
)
self._fsbl_selector._suppress_callback = True
if 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.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
@@ -527,9 +715,12 @@ class MainWindow(ctk.CTk):
panel,
label_text="Bootloader BIT (.bit):",
file_types=[("BIT files", "*.bit"), ("All files", "*")],
callback=self._on_file_selected,
)
self._bit_selector._suppress_callback = True
if 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.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
@@ -538,9 +729,12 @@ class MainWindow(ctk.CTk):
panel,
label_text="Bootloader ELF (.elf):",
file_types=[("ELF files", "*.elf"), ("All files", "*")],
callback=self._on_file_selected,
)
self._elf_selector._suppress_callback = True
if 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.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
@@ -549,9 +743,12 @@ class MainWindow(ctk.CTk):
panel,
label_text="Bootloader BIN (.bin):",
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:
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.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))
# 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)
self._firmware_bin_selector = FileSelector(
panel,
label_text="Firmware BIN (.bin):",
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:
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.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_btn = ctk.CTkButton(
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")
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.grid(
row=3, column=0, sticky="nsew",
padx=PADDING, pady=PADDING,
)
panel.grid_rowconfigure((0, 1), weight=1)
panel.grid_columnconfigure(0, weight=1)
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)
self._uart_warning_label = ctk.CTkLabel(
# UART warning — multi-line textbox, wraps long messages instead
# of expanding the right panel horizontally
self._uart_warning = ctk.CTkTextbox(
panel,
text="",
font=FONT_BODY,
anchor="w",
text_color=WARNING_COLOR,
height=40,
corner_radius=4,
fg_color="transparent",
state="disabled",
wrap="word",
)
self._uart_warning_label.pack(
fill="x", padx=PADDING, pady=(0, PADDING_SMALL)
self._uart_warning.grid(
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 ─────────────────────────────────────────
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))
def _set_step_status(self, index: int, status: str) -> None:
@@ -744,6 +969,8 @@ class MainWindow(ctk.CTk):
def _get_selected_files(self) -> dict:
"""Get selected file paths from the UI.
Returns absolute paths for immediate backend use.
Returns:
Dictionary with all file paths from UI selectors.
"""
@@ -783,6 +1010,10 @@ class MainWindow(ctk.CTk):
if result.is_ready:
self._vitis_status.configure(text="Vitis: Ready", text_color=SUCCESS_COLOR)
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
if self._config.xilinx_path:
try:
@@ -811,9 +1042,22 @@ class MainWindow(ctk.CTk):
self._log_message(f" - {jtag_dev.name} ({dev_type})")
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_info = jtag_result.devices[0]
part = jtag_result.devices[0].part_number
part = detected_part
# Log PS and PL detection status
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._jtag_status.configure(text=f"JTAG: {part}", text_color=SUCCESS_COLOR)
self._status.set_info("Zynq", part)
else:
self._zynq_jtag_present = False
self._zynq_jtag_info = None
self._log_message(f" ✗ Zynq not detected on JTAG: {jtag_result.error}")
self._jtag_status.configure(text="JTAG: Not detected", text_color=WARNING_COLOR)
return False
# Check UART availability
self._log_message(" Checking UART serial port...")
port = self._config.serial_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_message = reason
if available:
@@ -861,17 +1108,128 @@ class MainWindow(ctk.CTk):
self.after(0, lambda: self._show_uart_warning(msg))
def _show_uart_warning(self, msg: str) -> None:
"""Display the persistent UART warning message.
"""Display the persistent UART warning message (multi-line).
Args:
msg: Warning message to display.
"""
self._uart_warning_label.configure(text=msg)
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
self._uart_warning.configure(state="normal")
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:
"""Hide the persistent UART warning label."""
self._uart_warning_label.pack_forget()
"""Hide the persistent UART warning."""
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:
"""Handle serial port selection change — validate immediately.
@@ -1067,54 +1425,65 @@ class MainWindow(ctk.CTk):
self._log_message(" ⏳ Waiting 2s before download...")
time.sleep(2)
# 4b: TFTP Download Verify
self._log_message("Step 4b: TFTP download verify...")
self._tftp_phase = "Download Verify"
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("Download Verify")
try:
download_result = tftp_download(
self._config, remote_name,
callback=self._tftp_sub_callback,
expected_size=bin_path.stat().st_size,
)
status = "" if download_result.success else ""
self._log_message(f" {status} {download_result.message}")
# 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_step_complete("Download Verify") if download_result.success else self._tftp_sub_step_frame.set_step_error("Download Verify")
all_results.append(download_result.success)
except Exception as e:
self._log_message(f" ✗ TFTP download error: {e}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_error("Download Verify")
all_results.append(False)
if not all_results[-1]:
self._log_message(" Skipping CRC check (download failed)")
return False
# 4c: CRC Check
self._log_message("Step 4c: CRC check...")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("CRC Check")
original_crc = _compute_crc32(bin_path)
downloaded_crc = download_result.crc_local
crc_match = original_crc == downloaded_crc
self._log_message(f" Original CRC: {original_crc:#010x}")
self._log_message(f" Downloaded CRC: {downloaded_crc:#010x}")
if crc_match:
self._log_message(" ✓ CRC match")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_complete("CRC Check")
all_results.append(True)
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:
self._log_message(" ✗ CRC mismatch")
# 4b: Download Verify
self._log_message("Step 4b: TFTP download verify...")
self._tftp_phase = "Download Verify"
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_error("CRC Check")
all_results.append(False)
self._tftp_sub_step_frame.set_phase("Download Verify")
try:
download_result = tftp_download(
self._config, remote_name,
callback=self._tftp_sub_callback,
expected_size=bin_path.stat().st_size,
)
status = "" if download_result.success else ""
self._log_message(f" {status} {download_result.message}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_complete("Download Verify") if download_result.success else self._tftp_sub_step_frame.set_step_error("Download Verify")
all_results.append(download_result.success)
except Exception as e:
self._log_message(f" ✗ TFTP download error: {e}")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_error("Download Verify")
all_results.append(False)
if not all_results[-1]:
self._log_message(" Skipping CRC check (download failed)")
else:
# 4c: CRC Check
self._log_message("Step 4c: CRC check...")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_phase("CRC Check")
original_crc = _compute_crc32(bin_path)
downloaded_crc = download_result.crc_local
crc_match = original_crc == downloaded_crc
self._log_message(f" Original CRC: {original_crc:#010x}")
self._log_message(f" Downloaded CRC: {downloaded_crc:#010x}")
if crc_match:
self._log_message(" ✓ CRC match")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_complete("CRC Check")
all_results.append(True)
else:
self._log_message(" ✗ CRC mismatch")
if hasattr(self, '_tftp_sub_step_frame'):
self._tftp_sub_step_frame.set_step_error("CRC Check")
all_results.append(False)
if not all_results[-1]:
self._log_message(" Skipping reboot (CRC mismatch)")
self._log_message(" Skipping reboot (download/CRC failed)")
return False
# 4d: Reboot
@@ -1211,16 +1580,38 @@ class MainWindow(ctk.CTk):
discovered_ip = parsed.ip_address
self._log_message(f" IP from UART buffer: {discovered_ip}")
# Discover actual IP after reboot (may have changed via DHCP)
# 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)
self._log_message(" Scanning for Zynq IP (192.168.100.1130)...")
discovered_ip = self._discover_zynq_ip()
if discovered_ip:
self._log_message(f" ✓ Found Zynq at {discovered_ip}")
# 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:
self._log_message(" ⚠ Zynq not found on any scanned IP")
# Fallback: UDP broadcast scan
self._log_message(" Scanning for Zynq IP (192.168.100.1130)...")
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(
@@ -1429,6 +1820,13 @@ class MainWindow(ctk.CTk):
elapsed = time.time() - t_step_start
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
if i == 1 and hasattr(self, '_sub_step_frame'):
if success:
@@ -1436,16 +1834,20 @@ class MainWindow(ctk.CTk):
else:
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 success:
self._tftp_sub_step_frame.set_phase("Boot Verify")
else:
if not success:
self._tftp_sub_step_frame.set_step_error("Boot Verify")
except Exception as e:
results.append(False)
self._set_step_status(i, "error")
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)
@@ -1597,6 +1999,7 @@ class MainWindow(ctk.CTk):
success, message, version = test_serial_version(
port,
self._config.serial_baudrate if self._config else 115200,
monitor=self._uart_monitor,
)
if success:
+149 -44
View File
@@ -6,6 +6,7 @@ consistent styling from gui/styles.py.
from __future__ import annotations
import os
import threading
import customtkinter as ctk
@@ -436,54 +437,91 @@ class ProgressIndicator(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):
"""Initialize the status display.
Args:
master: Parent widget.
"""
super().__init__(master, **kwargs)
self._info_items: list[tuple[str, str]] = []
self._create_widgets()
def _create_widgets(self) -> None:
"""Create the status display UI."""
self._status_label = ctk.CTkLabel(
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self._info_text = ctk.CTkTextbox(
self,
text="Ready",
font=FONT_BODY,
anchor="w",
corner_radius=CORNER_RADIUS,
state="disabled",
wrap="word",
)
self._status_label.pack(
fill="x", padx=PADDING, pady=PADDING_SMALL
self._info_text.grid(
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:
"""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:
message: Status message text.
status: Status type for coloring ('info', 'success', 'error', 'warning').
key: Label for the info line.
value: Value string.
"""
self._status_label.configure(text=message)
colors = {
"info": INFO_COLOR,
"success": SUCCESS_COLOR,
"error": DANGER_COLOR,
"warning": WARNING_COLOR,
}
self._status_label.configure(text_color=colors.get(status, INFO_COLOR))
for i, (k, _) in enumerate(self._info_items):
if k == key:
self._info_items[i] = (key, value)
break
else:
self._info_items.append((key, value))
self._refresh_info()
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):
"""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__(
@@ -506,7 +544,11 @@ class FileSelector(ctk.CTkFrame):
self._callback = callback
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)
@@ -524,10 +566,10 @@ class FileSelector(ctk.CTkFrame):
)
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,
textvariable=self._selected_path,
textvariable=self._display_text,
font=FONT_MONO,
state="readonly",
)
@@ -549,33 +591,60 @@ class FileSelector(ctk.CTkFrame):
def _browse(self) -> None:
"""Open file dialog and set selected path."""
import customtkinter
from tkinter import filedialog
file_path = filedialog.askopenfilename(
title=f"Select {self._selected_path.get() or 'file'}",
title=f"Select {self._hint_text}",
filetypes=self._file_types,
)
if file_path:
self.set_path(file_path)
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:
path: Absolute path to the selected file.
path: Absolute or relative path to the selected file.
"""
self._selected_path.set(path)
if self._callback:
self._full_path = path
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)
def get_path(self) -> str:
"""Get the currently selected file path.
"""Get the currently selected file path (absolute).
Returns:
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):
@@ -650,15 +719,15 @@ class SubStepFrame(ctk.CTkFrame):
"""Collapsible frame showing flash operation sub-steps with status.
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:
super().__init__(parent, fg_color="transparent")
self._collapsed = False
self._collapsed = True
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,
)
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._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:
self._collapsed = not self._collapsed
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.
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:
super().__init__(parent, fg_color="transparent")
self._collapsed = False
self._collapsed = True
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._num_steps = len(self._substep_names)
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,
)
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)
# 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:
self._collapsed = not self._collapsed
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 ""
for i, item in enumerate(self._sub_items):
if i in self._skipped:
continue # leave skipped items untouched
if i < idx:
item["dot"].configure(text="", 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)
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:
"""Start pulsing the running sub-step."""
_LIGHT = RUNNING_FADE_LIGHT
@@ -910,6 +1014,7 @@ class TftpSubStepFrame(ctk.CTkFrame):
def reset(self) -> None:
self._stop_pulse()
self._skipped.clear()
_C = ACCENT_PENDING
for i, item in enumerate(self._sub_items):
item["dot"].configure(text="", text_color=_C)
@@ -967,7 +1072,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
self._build_log_area()
self._build_status_bar()
# Try to open serial port after window is drawn
# Try to open serial port after window is drawn
self.after(100, self._try_open)
# ── Title Bar ──────────────────────────────────────────────
+23 -11
View File
@@ -9,6 +9,7 @@ from __future__ import annotations
import re
import threading
import time
from dataclasses import dataclass
from typing import Callable
@@ -136,6 +137,7 @@ def check_uart_available(
port: str,
baudrate: int = 115200,
timeout: float = 3.0,
monitor: UartMonitor | None = None,
) -> tuple[bool, str]:
"""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).
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:
port: Serial port device path (e.g., '/dev/ttyUSB0').
baudrate: Baud rate for serial communication.
timeout: Seconds to wait for data after opening.
monitor: Optional running UartMonitor — if active, reuse it.
Returns:
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
ports = detect_serial_ports()
if not ports:
@@ -222,28 +229,33 @@ def test_serial_version(
port: str,
baudrate: int = 115200,
timeout: float = 5.0,
monitor: UartMonitor | None = None,
) -> tuple[bool, str, str]:
"""Test serial port by sending 'ver()' command and parsing response.
Sends 'ver\\r\\n' to the serial port and reads the response.
Attempts to parse version string from the response.
If *monitor* is running, skips opening a new connection and
returns the latest parsed version from the monitor instead.
Args:
port: Serial port device path (e.g., '/dev/ttyUSB0').
baudrate: Baud rate for serial communication.
timeout: Read timeout in seconds.
monitor: Optional running UartMonitor — if active, reuse it.
Returns:
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
# 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:
with serial.Serial(port, baudrate, timeout=timeout) as ser:
# Clear buffers
ser.reset_input_buffer()
ser.reset_output_buffer()
+39 -14
View File
@@ -56,10 +56,12 @@ TOOL_ALIASES: dict[str, list[str]] = {
"xsct": ["xsct"],
"program_flash": ["program_flash"],
"bootgen": ["bootgen"],
"hw_server": ["hw_server"],
}
# 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 ──────────────────────────────────────────────────
@@ -162,9 +164,9 @@ def _find_executable(
"""Find an executable by name using PATH, Vitis, and Xilinx root.
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)
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)
Args:
@@ -178,16 +180,22 @@ def _find_executable(
"""
exec_suffix = ".exe" if system == "windows" else ""
# 1. System PATH
found = shutil.which(f"{exec_name}{exec_suffix}")
# 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}")
if not found:
found = shutil.which(f"{exec_name}.bat")
if found:
return found
# 2. Legacy vitis_path/bin
if vitis_path:
candidate = Path(vitis_path) / "bin" / f"{exec_name}{exec_suffix}"
if candidate.is_file():
return str(candidate)
for sfx in (".bat", ".exe") if system == "windows" else (exec_suffix,):
candidate = Path(vitis_path) / "bin" / f"{exec_name}{sfx}"
if candidate.is_file():
return str(candidate)
# 3. Scan xilinx_root — pick newest version across Vitis + Vivado
if xilinx_root:
@@ -200,12 +208,11 @@ def _find_executable(
# 4. Common install paths (cross-platform fallback)
if system == "windows":
common = [
f"C:/Xilinx/Vitis/2023.2/bin/{exec_name}.exe",
f"C:/Xilinx/Vivado/2023.2/bin/{exec_name}.exe",
f"C:/Xilinx/Vitis/2022.2/bin/{exec_name}.exe",
f"C:/Xilinx/Vivado/2022.2/bin/{exec_name}.exe",
]
common: list[str] = []
for sfx in (".bat", ".exe"):
for ver in ("2023.2", "2022.2", "2018.3"):
for prod in ("Vitis", "Vivado", "SDK"):
common.append(f"C:/Xilinx/{prod}/{ver}/bin/{exec_name}{sfx}")
else:
common = [
f"/data/xilinx/Vitis/2023.2/bin/{exec_name}",
@@ -330,6 +337,24 @@ def get_vivado_path(vitis_path: str = "", xilinx_root: str = "") -> str | 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 ───────────────────────────────────────────
+232 -231
View File
@@ -1,79 +1,84 @@
"""Zynq JTAG presence checker for Zynq Flasher GUI.
Connects to hw_server via Vivado and scans the JTAG chain to detect
Zynq devices, returning chip model and presence information.
Checks for both PS (Processing System) and PL (Programmable Logic)
recognition on the JTAG chain.
Starts hw_server explicitly, then uses Vivado batch-mode TCL to scan
the JTAG chain. On failure, Vivado's stdout/stderr is returned in the
error message for debugging.
"""
from __future__ import annotations
import os
import re
import subprocess
import tempfile
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
from config_manager import Config
from vitis_checker import get_vivado_path, build_tool_command
from vitis_checker import get_vivado_path, get_hw_server_path, build_tool_command
@dataclass
class JtagDevice:
"""Information about a device on the JTAG chain."""
name: str # Device name from JTAG chain (e.g. "xc7z100_1")
is_zynq: bool # Whether this is a Zynq device
is_arm_dap: bool # Whether this is an ARM DAP (PS side)
is_pl_device: bool # Whether this is a PL device
name: str
idcode: str = ""
is_zynq: bool = False
is_arm_dap: bool = False
is_pl_device: bool = False
@dataclass
class ZynqDevice:
"""Information about a detected Zynq device on the JTAG chain."""
name: str # Device name from JTAG chain (e.g. "xc7z100_1")
part_number: str # Extracted part number (e.g. "XC7Z100")
family: str # Family (e.g. "zynq7")
speed: str # Speed grade (e.g. "1", "2", "-1", "-2")
idcode: str = "" # JTAG IDCODE if available
is_programmable: bool = False # Whether the device is programmable
name: str
part_number: str
family: str
speed: str
idcode: str = ""
is_programmable: bool = False
@dataclass
class ZynqCheckResult:
"""Result of Zynq device detection on the JTAG chain."""
devices: list[ZynqDevice] # All detected Zynq devices
jtag_chain: list[JtagDevice] # All devices on JTAG chain
is_present: bool # Whether any Zynq device was found
ps_detected: bool # Whether PS (ARM DAP) is detected
pl_detected: bool # Whether PL is detected
hw_server_url: str # hw_server connection URL used
error: str = "" # Error message if detection failed
devices: list[ZynqDevice]
jtag_chain: list[JtagDevice]
is_present: bool
ps_detected: bool
pl_detected: bool
hw_server_url: str
error: str = ""
# Zynq part number patterns from JTAG device names
# Examples: xc7z010_1, xc7z020_1, xc7z100_1, xc7z030_1
ZYNQ_PART_PATTERN = re.compile(
r"^(xc\d+z\d+[a-z]?\d+)_\d+$", re.IGNORECASE
)
# ARM DAP pattern (PS side)
ARM_DAP_PATTERN = re.compile(r"^arm_dap_\d+$", re.IGNORECASE)
# hw_server default port
# ── Patterns ──────────────────────────────────────────────────────────
_DEVICE_LINE_RE = re.compile(r"^DEVICE:(\S+)\s+IDCODE:([01]+)")
_ZYNQ_RE = re.compile(r"^xc\d+z\d+[a-z]?\d+", re.IGNORECASE)
_ARM_DAP_RE = re.compile(r"^arm_dap", re.IGNORECASE)
HW_SERVER_PORT = 3121
def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqCheckResult:
"""Check for Zynq devices on the JTAG chain via hw_server.
def _idcode_to_hex(binary_str: str) -> str:
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
for PS (ARM DAP) and PL (Programmable Logic) recognition.
# ── Main entry ────────────────────────────────────────────────────────
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:
config: Application configuration.
@@ -83,178 +88,221 @@ def check_zynq_jtag(config: Config, callback: Callable | None = None) -> ZynqChe
ZynqCheckResult with detected devices and status.
"""
hw_server_url = f"localhost:{HW_SERVER_PORT}"
xilinx_root = getattr(config, "xilinx_path", "")
if callback:
callback("start", "Connecting to hw_server...")
# Find vivado executable via Xilinx root scan
vivado_path = get_vivado_path(
xilinx_root=getattr(config, "xilinx_path", ""),
)
# ── 1. Find tools ──────────────────────────────────────────
vivado_path = get_vivado_path(xilinx_root=xilinx_root)
if not vivado_path:
return ZynqCheckResult(
devices=[],
jtag_chain=[],
is_present=False,
ps_detected=False,
pl_detected=False,
hw_server_url=hw_server_url,
error="Vivado not found",
)
return _fail("Vivado not found", hw_server_url)
# Create temporary TCL script
tcl_content = _build_jtag_query_tcl()
tcl_path = Path(tempfile.gettempdir()) / f"zynq_jtag_check_{os.getpid()}.tcl"
hw_server_path = get_hw_server_path(xilinx_root=xilinx_root)
if not hw_server_path:
return _fail("hw_server not found", hw_server_url)
# ── 2. Start hw_server ─────────────────────────────────────
hw_proc: subprocess.Popen | None = None
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:
callback("progress", "Scanning JTAG chain...")
if callback:
callback("progress", "Scanning JTAG chain...")
# Run Vivado in batch mode (with environment setup)
cmd = build_tool_command(vivado_path, "-mode", "batch", "-source", str(tcl_path))
# ── 3. Run Vivado TCL ──────────────────────────────────────
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(
cmd,
capture_output=True,
text=True,
timeout=30,
timeout=python_timeout,
)
output = result.stdout + result.stderr
# Parse JTAG device information from output
jtag_chain = _parse_jtag_chain(output)
devices = _filter_zynq_devices(jtag_chain)
if callback:
if devices:
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 ""
notes = ", ".join(filter(None, [ps_note, pl_note]))
callback("complete", f"Found {len(devices)} Zynq device(s) [{notes}]")
else:
callback("error", "No Zynq device found on JTAG chain")
# Check PS and PL detection
ps_detected = any(d.is_arm_dap for d in jtag_chain)
pl_detected = any(d.is_pl_device for d in jtag_chain)
return ZynqCheckResult(
devices=devices,
jtag_chain=jtag_chain,
is_present=len(devices) > 0,
ps_detected=ps_detected,
pl_detected=pl_detected,
hw_server_url=hw_server_url,
error="" if devices else "No Zynq device found on JTAG chain",
)
for line in output.splitlines():
stripped = line.strip()
if stripped:
output_lines.append(stripped)
if callback:
callback("progress", stripped)
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",
return _fail(
f"JTAG scan timed out after {python_timeout}s",
hw_server_url,
)
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}",
)
return _fail(f"Failed to run Vivado: {e}", hw_server_url)
finally:
tcl_path.unlink(missing_ok=True)
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
output = "\n".join(output_lines)
# Parse results
jtag_chain = _parse_jtag_chain(output)
devices = _filter_zynq_devices(jtag_chain)
if devices:
if callback:
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 ""
notes = ", ".join(filter(None, [ps_note, pl_note]))
callback("complete", f"Found {len(devices)} Zynq device(s) [{notes}]")
else:
# 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,
)
ps_detected = any(d.is_arm_dap for d in jtag_chain)
pl_detected = any(d.is_pl_device for d in jtag_chain)
return ZynqCheckResult(
devices=devices, jtag_chain=jtag_chain,
is_present=True,
ps_detected=ps_detected, pl_detected=pl_detected,
hw_server_url=hw_server_url,
error="",
)
# ── 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:
"""Build TCL script for JTAG device query.
Returns:
TCL script content as string.
"""
return """
open_hw_manager
"""Build Vivado TCL script file for JTAG device enumeration."""
content = """\
open_hw
connect_hw_server
open_hw_target
puts "===JTAG_START==="
foreach dev [get_hw_devices] {
set name [get_property NAME $dev]
puts "DEVICE:$name"
set idcode [get_property IDCODE $dev]
puts "DEVICE:$name IDCODE:$idcode"
}
puts "===JTAG_END==="
close_hw
disconnect_hw_server
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]:
"""Parse JTAG chain information from Vivado output.
Args:
output: Combined stdout/stderr from Vivado batch execution.
Returns:
List of JtagDevice objects for all devices on the JTAG chain.
"""
"""Parse Vivado TCL output into JtagDevice list."""
devices: list[JtagDevice] = []
# Extract device list from output
devices_section = _extract_section(output, "===JTAG_START===", "===JTAG_END===")
if not devices_section:
section = _extract_section(output, "===JTAG_START===", "===JTAG_END===")
if not section:
return devices
for line in devices_section.splitlines():
for line in section.splitlines():
line = line.strip()
if line.startswith("DEVICE:"):
device_name = line[len("DEVICE:"):]
device = _classify_jtag_device(device_name)
devices.append(device)
if not line:
continue
match = _DEVICE_LINE_RE.match(line)
if match:
name = match.group(1)
idcode_hex = _idcode_to_hex(match.group(2))
devices.append(_classify_jtag_device(name, idcode_hex))
return devices
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] = []
for jtag_dev in jtag_chain:
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:
zynq_devices.append(zynq_dev)
return zynq_devices
def _extract_section(
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
def _extract_section(text: str, start_marker: str, end_marker: str) -> str | None:
start_idx = text.rfind(start_marker)
end_idx = text.rfind(end_marker)
if start_idx == -1 or end_idx == -1 or start_idx >= end_idx:
@@ -262,76 +310,37 @@ def _extract_section(
return text[start_idx + len(start_marker):end_idx].strip()
def _classify_jtag_device(device_name: str) -> JtagDevice:
"""Classify a JTAG device by its 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
def _classify_jtag_device(device_name: str, idcode: str = "") -> JtagDevice:
is_zynq = bool(_ZYNQ_RE.match(device_name))
is_arm_dap = bool(_ARM_DAP_RE.match(device_name))
return JtagDevice(
name=device_name,
idcode=idcode,
is_zynq=is_zynq,
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:
"""Parse a Zynq device from its JTAG chain 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)
def _parse_zynq_device(device_name: str, idcode: str = "") -> ZynqDevice | None:
match = _ZYNQ_RE.match(device_name)
if not match:
return None
part_base = match.group(1)
# 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
base = device_name.rsplit("_", 1)[0] if "_" in device_name else device_name
part_number = base.upper()
family = "zynq7"
speed = "1" # Default speed grade
# Parse speed grade from the numeric suffix
speed_match = re.search(r"(\d+)$", part_base)
speed = "-1"
speed_match = re.search(r"(\d+)$", part_number)
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 = "-2" if int(speed_match.group(1)) >= 2 else "-1"
return ZynqDevice(
name=device_name,
part_number=part_number,
family=family,
speed=speed,
name=device_name, part_number=part_number,
family=family, speed=speed,
idcode=idcode, is_programmable=True,
)
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 {
"is_present": result.is_present,
"ps_detected": result.ps_detected,
@@ -339,23 +348,15 @@ def get_zynq_status_dict(result: ZynqCheckResult) -> dict:
"hw_server_url": result.hw_server_url,
"error": result.error,
"jtag_chain": [
{
"name": dev.name,
"is_zynq": dev.is_zynq,
"is_arm_dap": dev.is_arm_dap,
"is_pl_device": dev.is_pl_device,
}
for dev in result.jtag_chain
{"name": d.name, "idcode": d.idcode,
"is_zynq": d.is_zynq, "is_arm_dap": d.is_arm_dap,
"is_pl_device": d.is_pl_device}
for d in result.jtag_chain
],
"devices": [
{
"name": dev.name,
"part_number": dev.part_number,
"family": dev.family,
"speed": dev.speed,
"idcode": dev.idcode,
"is_programmable": dev.is_programmable,
}
for dev in result.devices
{"name": d.name, "part_number": d.part_number,
"family": d.family, "speed": d.speed,
"idcode": d.idcode, "is_programmable": d.is_programmable}
for d in result.devices
],
}
-14
View File
@@ -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
-14
View File
@@ -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.