Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce544cd3c | ||
|
|
73b8225a27 | ||
|
|
0568315b83 | ||
|
|
7067ce273f | ||
|
|
9a7a625c46 | ||
|
|
47569a0e43 | ||
|
|
8b2fac931c | ||
|
|
09be20791f | ||
|
|
b294dfe58a | ||
|
|
a47e3b78ae |
+1
-1
@@ -1,6 +1,6 @@
|
||||
bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin
|
||||
bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrapper_hw_platform_0/pl_arm_wrapper.bit
|
||||
bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf
|
||||
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
|
||||
|
||||
+324
-53
@@ -214,7 +214,7 @@ def _program_with_xsct(
|
||||
if callback:
|
||||
callback("progress", "Initializing PS then programming FPGA...")
|
||||
|
||||
# Build TCL: PS init first, then FPGA config
|
||||
# Build TCL: PS init → FPGA config → post-config → halt CPU → disable Flash boot
|
||||
lines = ["connect"]
|
||||
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
||||
lines.append("targets 1")
|
||||
@@ -223,6 +223,17 @@ def _program_with_xsct(
|
||||
if callback:
|
||||
callback("progress", "PS initialized (clocks, DDR, MIO)")
|
||||
lines.append(f'fpga -file "{bit_path}"')
|
||||
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
||||
lines.append("ps7_post_config")
|
||||
if callback:
|
||||
callback("progress", "PS post-config (PL-PS interfaces)")
|
||||
# After FPGA config, CPU may be reset — halt it to prevent Flash code
|
||||
# from enabling MMU before the ELF step can download FSBL
|
||||
lines.append("targets 2")
|
||||
lines.append("after 100")
|
||||
lines.append("stop")
|
||||
# Prevent boot from Flash by disabling PCAP reconfiguration
|
||||
lines.append("mwr 0xF800025C 0x00000001")
|
||||
lines.append("exit")
|
||||
tcl_script = "\n".join(lines)
|
||||
|
||||
@@ -323,7 +334,9 @@ def run_elf(
|
||||
) -> BitstreamResult:
|
||||
"""Download and run a .elf executable on the Zynq PS ARM core.
|
||||
|
||||
Uses xsct's `dow` (download) command, with Vivado as fallback.
|
||||
First downloads FSBL (First Stage Bootloader) to OCM (0x0) to
|
||||
initialize clocks, DDR, MIO and force JTAG boot mode. Then
|
||||
downloads the target ELF to DDR.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
@@ -341,10 +354,15 @@ def run_elf(
|
||||
message=f"ELF file not found: {elf_path}",
|
||||
)
|
||||
|
||||
fsbl_path = config.fsbl_elf_path if hasattr(config, 'fsbl_elf_path') else ""
|
||||
if fsbl_path and not Path(fsbl_path).exists():
|
||||
if callback:
|
||||
callback("warning", f"FSBL not found: {fsbl_path}, will try direct dow")
|
||||
fsbl_path = ""
|
||||
|
||||
if callback:
|
||||
callback("start", f"Running ELF: {elf_path.name}...")
|
||||
|
||||
# Use xsct for CPU operations (Vivado HM cannot program PS CPU)
|
||||
xsct = get_xsct_path(
|
||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
||||
@@ -356,91 +374,168 @@ def run_elf(
|
||||
message="xsct not found in PATH (required for CPU operations)",
|
||||
)
|
||||
|
||||
# Auto-detect ps7_init.tcl from config or BIT file directory
|
||||
ps7_tcl = _find_ps7_init_tcl(config)
|
||||
timeout = config.step_timeouts.get("elf_download", 60)
|
||||
return _run_elf_with_xsct(xsct, elf_path, callback, ps7_tcl, timeout)
|
||||
|
||||
if fsbl_path:
|
||||
return _run_elf_via_fsbl(xsct, elf_path, fsbl_path, callback, timeout)
|
||||
else:
|
||||
return _run_elf_direct(xsct, elf_path, callback, timeout)
|
||||
|
||||
|
||||
def _run_elf_with_xsct(
|
||||
def _run_elf_via_fsbl(
|
||||
xsct_path: str,
|
||||
elf_path: Path,
|
||||
fsbl_path: str,
|
||||
callback: ProgressCallback = None,
|
||||
ps7_init_tcl: str = "",
|
||||
timeout: int = 60,
|
||||
) -> BitstreamResult:
|
||||
"""Download and run ELF using xsct's dow command.
|
||||
"""Download and run ELF via FSBL bootloader.
|
||||
|
||||
Assumes PS was already initialized (by a prior BIT download step).
|
||||
Falls back to PS init if DAP is broken.
|
||||
FSBL loads to OCM (0x0) which is always accessible even when
|
||||
MMU is enabled. FSBL initializes clocks/DDR/MIO and forces
|
||||
JTAG boot mode, making DDR accessible for the app ELF.
|
||||
|
||||
Args:
|
||||
xsct_path: Path to xsct executable.
|
||||
elf_path: Path to .elf file.
|
||||
elf_path: Path to target .elf file.
|
||||
fsbl_path: Path to FSBL .elf file.
|
||||
callback: Optional progress callback.
|
||||
ps7_init_tcl: Optional ps7_init.tcl path for recovery.
|
||||
timeout: Subprocess timeout in seconds.
|
||||
|
||||
Returns:
|
||||
BitstreamResult with status.
|
||||
"""
|
||||
if callback:
|
||||
callback("progress", "Downloading ELF...")
|
||||
|
||||
lines = ["connect"]
|
||||
# Recovery: if DAP is broken, re-init PS
|
||||
if ps7_init_tcl and Path(ps7_init_tcl).exists():
|
||||
lines.append("targets 1")
|
||||
lines.append(f'source "{ps7_init_tcl}"')
|
||||
lines.append("ps7_init")
|
||||
lines.append("targets 2")
|
||||
lines.append(f'dow "{elf_path}"')
|
||||
lines.append("con")
|
||||
lines.append("exit")
|
||||
tcl_script = "\n".join(lines)
|
||||
|
||||
import tempfile, os
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.tcl', delete=False) as f:
|
||||
f.write(tcl_script)
|
||||
tcl_file = f.name
|
||||
|
||||
if callback:
|
||||
callback("progress", "Downloading FSBL to OCM...")
|
||||
|
||||
tcl = "\n".join([
|
||||
"connect",
|
||||
"targets 2",
|
||||
"dow \"%s\"" % fsbl_path,
|
||||
"puts \"FSBL downloaded\"",
|
||||
"con",
|
||||
"after 3000",
|
||||
"stop",
|
||||
"puts \"FSBL done, CPU halted\"",
|
||||
"dow \"%s\"" % elf_path,
|
||||
"mwr 0xF800025C 0x00000001",
|
||||
"con",
|
||||
"puts \"App running\"",
|
||||
"exit",
|
||||
])
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".tcl", delete=False
|
||||
) as tf:
|
||||
tf.write(tcl)
|
||||
tcl_file = tf.name
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[xsct_path, tcl_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
)
|
||||
os.unlink(tcl_file)
|
||||
|
||||
success = result.returncode == 0
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
|
||||
# xsct may return non-zero even if download succeeded
|
||||
if any(phrase in output for phrase in ("Download successful", "Done", "done")):
|
||||
success = True
|
||||
success = result.returncode == 0 or any(
|
||||
p in output for p in (
|
||||
"App running", "Download successful",
|
||||
"Successfully downloaded",
|
||||
)
|
||||
)
|
||||
|
||||
if callback:
|
||||
callback(
|
||||
"complete" if success else "error",
|
||||
"ELF " + ("executed" if success else "failed"),
|
||||
"ELF " + ("executed via FSBL" if success else "failed"),
|
||||
)
|
||||
|
||||
return BitstreamResult(
|
||||
step="elf",
|
||||
success=success,
|
||||
message="ELF executed successfully" if success else "ELF execution failed",
|
||||
message="ELF executed via FSBL" if success else "ELF execution failed",
|
||||
output=output[:4000],
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.unlink(tcl_file)
|
||||
return BitstreamResult(
|
||||
step="elf",
|
||||
success=False,
|
||||
message="xsct ELF download timed out",
|
||||
return BitstreamResult(step="elf", success=False,
|
||||
message="ELF download timed out")
|
||||
|
||||
|
||||
def _run_elf_direct(
|
||||
xsct_path: str,
|
||||
elf_path: Path,
|
||||
callback: ProgressCallback = None,
|
||||
timeout: int = 60,
|
||||
) -> BitstreamResult:
|
||||
"""Download and run ELF directly (no FSBL, assumes DAP alive).
|
||||
|
||||
Used when no FSBL ELF is configured. Attempts direct dow
|
||||
which only works if the CPU is in a clean state with MMU off.
|
||||
|
||||
Args:
|
||||
xsct_path: Path to xsct executable.
|
||||
elf_path: Path to .elf file.
|
||||
callback: Optional progress callback.
|
||||
timeout: Subprocess timeout in seconds.
|
||||
|
||||
Returns:
|
||||
BitstreamResult with status.
|
||||
"""
|
||||
import tempfile, os
|
||||
|
||||
if callback:
|
||||
callback("progress", "Downloading ELF directly...")
|
||||
|
||||
tcl = "\n".join([
|
||||
"connect",
|
||||
"targets 2",
|
||||
"dow \"%s\"" % elf_path,
|
||||
"mwr 0xF800025C 0x00000001",
|
||||
"con",
|
||||
"exit",
|
||||
])
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".tcl", delete=False
|
||||
) as tf:
|
||||
tf.write(tcl)
|
||||
tcl_file = tf.name
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[xsct_path, tcl_file],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
)
|
||||
os.unlink(tcl_file)
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
|
||||
success = result.returncode == 0 or any(
|
||||
p in output for p in (
|
||||
"Download successful", "Successfully downloaded",
|
||||
)
|
||||
)
|
||||
|
||||
if callback:
|
||||
callback("complete" if success else "error",
|
||||
"ELF " + ("executed" if success else "failed"))
|
||||
|
||||
# ── Full Workflow ───────────────────────────────────────────────────
|
||||
return BitstreamResult(
|
||||
step="elf",
|
||||
success=success,
|
||||
message="ELF executed" if success else "ELF execution failed",
|
||||
output=output[:4000],
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.unlink(tcl_file)
|
||||
return BitstreamResult(step="elf", success=False,
|
||||
message="ELF download timed out")
|
||||
|
||||
|
||||
# ── Full Workflow (combined BIT+ELF in one xsct session) ──────────
|
||||
|
||||
|
||||
def full_bitstream_program(
|
||||
@@ -449,7 +544,13 @@ def full_bitstream_program(
|
||||
elf_path: str | Path,
|
||||
callback: ProgressCallback = None,
|
||||
) -> list[BitstreamResult]:
|
||||
"""Execute full bootloader workflow: program bitstream → run ELF.
|
||||
"""Execute full bootloader workflow in one xsct session.
|
||||
|
||||
Flow:
|
||||
1. rst -processor (bypass BootROM, block Flash boot)
|
||||
2. dow fsbl → con → stop (FSBL initializes clocks/DDR/MIO)
|
||||
3. fpga -f bit (configure PL)
|
||||
4. dow app → con (run bootloader)
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
@@ -460,11 +561,181 @@ def full_bitstream_program(
|
||||
Returns:
|
||||
List of BitstreamResult for each step.
|
||||
"""
|
||||
results: list[BitstreamResult] = []
|
||||
import tempfile, os
|
||||
|
||||
results.append(program_bitstream(config, bit_path, callback))
|
||||
if not results[-1].success:
|
||||
return results
|
||||
bit_path = Path(bit_path)
|
||||
elf_path = Path(elf_path)
|
||||
fsbl_path = config.fsbl_elf_path if hasattr(config, 'fsbl_elf_path') else ""
|
||||
|
||||
results.append(run_elf(config, elf_path, callback))
|
||||
return results
|
||||
# Validate files
|
||||
if not bit_path.exists():
|
||||
return [BitstreamResult(step="bitstream", success=False,
|
||||
message=f"BIT not found: {bit_path}")]
|
||||
if not elf_path.exists():
|
||||
return [BitstreamResult(step="elf", success=False,
|
||||
message=f"ELF not found: {elf_path}")]
|
||||
if not fsbl_path or not Path(fsbl_path).exists():
|
||||
return [BitstreamResult(step="elf", success=False,
|
||||
message=f"FSBL not found: {fsbl_path or '(not configured)'}")]
|
||||
|
||||
xsct = get_xsct_path(
|
||||
vitis_path=config.vitis_path if hasattr(config, 'vitis_path') else "",
|
||||
xilinx_root=config.xilinx_path if hasattr(config, 'xilinx_path') else "",
|
||||
)
|
||||
if not xsct:
|
||||
return [BitstreamResult(step="bitstream", success=False,
|
||||
message="xsct not found")]
|
||||
|
||||
if callback:
|
||||
callback("start", "Initializing Zynq via JTAG (FSBL override)...")
|
||||
|
||||
# Build TCL with preemptive system reset + DAP error recovery
|
||||
tcl_lines = [
|
||||
'puts "INFO: Starting Zynq initialization via JTAG override..."',
|
||||
"connect",
|
||||
"after 500",
|
||||
]
|
||||
|
||||
# Preemptive system reset (safe on clean board; may help after flash)
|
||||
tcl_lines.extend([
|
||||
'# Preemptive system reset to clean up any leftover state',
|
||||
'if {[catch {',
|
||||
' targets -set -nocase -filter {name =~ "arm*#0"}',
|
||||
' rst -system',
|
||||
' after 1000',
|
||||
' stop',
|
||||
' puts "INFO: Preemptive system reset OK"',
|
||||
'}]} { puts "INFO: Preemptive system reset skipped (no ARM target)" }',
|
||||
'',
|
||||
])
|
||||
|
||||
# DAP error check — if ARM cores not visible, try recovery via DAP
|
||||
tcl_lines.extend([
|
||||
'# Check for DAP errors and attempt recovery',
|
||||
'set target_list [targets]',
|
||||
'set has_arm 0',
|
||||
'foreach t $target_list { if {[regexp -nocase {arm|cortex} $t]} { set has_arm 1 } }',
|
||||
'if {!$has_arm} {',
|
||||
' puts "WARNING: ARM cores not found — DAP may be in error state"',
|
||||
' # Show DAP status for logging',
|
||||
' foreach t $target_list { puts " TARGET: $t" }',
|
||||
' # Attempt recovery: select DAP (target 1) and issue system reset',
|
||||
' puts "INFO: Attempting DAP recovery via system reset..."',
|
||||
' catch {',
|
||||
' targets 1',
|
||||
' rst -system',
|
||||
' after 1000',
|
||||
' stop',
|
||||
' }',
|
||||
' # Disconnect and reconnect to refresh JTAG chain',
|
||||
' catch { disconnect; after 1000; connect; after 500 }',
|
||||
' # Check if ARM cores reappeared',
|
||||
' set target_list2 [targets]',
|
||||
' set has_arm2 0',
|
||||
' foreach t $target_list2 {',
|
||||
' puts " AFTER RECOVERY: $t"',
|
||||
' if {[regexp -nocase {arm|cortex} $t]} { set has_arm2 1 }',
|
||||
' }',
|
||||
' if {!$has_arm2} {',
|
||||
' puts "FATAL: DAP recovery failed — board requires power cycle"',
|
||||
' puts "FATAL: Unplug/replug power, then retry Step 3"',
|
||||
' exit 1',
|
||||
' }',
|
||||
' puts "INFO: ARM cores recovered after DAP reset"',
|
||||
'}',
|
||||
'',
|
||||
])
|
||||
|
||||
# Core flow
|
||||
tcl_lines.extend([
|
||||
'targets -set -nocase -filter {name =~ "arm*#0"}',
|
||||
'puts "INFO: Resetting processor (Bypassing BootROM Flash boot)..."',
|
||||
"rst -processor",
|
||||
"after 500",
|
||||
'puts "INFO: Downloading FSBL..."',
|
||||
'dow "%s"' % 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,
|
||||
"after 1000",
|
||||
'targets -set -nocase -filter {name =~ "arm*#0"}',
|
||||
'puts "INFO: Downloading Application ELF..."',
|
||||
'dow "%s"' % elf_path,
|
||||
'puts "INFO: Executing Application..."',
|
||||
"con",
|
||||
'puts "SUCCESS: Zynq target is running the specified application."',
|
||||
"exit",
|
||||
])
|
||||
|
||||
tcl = "\n".join(tcl_lines)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".tcl", delete=False
|
||||
) as tf:
|
||||
tf.write(tcl)
|
||||
tcl_file = tf.name
|
||||
|
||||
timeout = config.step_timeouts.get("bitstream", 60) + \
|
||||
config.step_timeouts.get("elf_download", 60)
|
||||
|
||||
try:
|
||||
if callback:
|
||||
callback("progress", "Executing combined JTAG flow...")
|
||||
|
||||
result = subprocess.run(
|
||||
[xsct, tcl_file],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
)
|
||||
os.unlink(tcl_file)
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
|
||||
success = result.returncode == 0 and "SUCCESS" in output
|
||||
|
||||
# Detect DAP errors for logging
|
||||
dap_errors = [l for l in output.split("\n") if "DAP" in l and "error" in l.lower()]
|
||||
if dap_errors:
|
||||
if callback:
|
||||
for e in dap_errors:
|
||||
callback("error", f"DAP: {e.strip()[:120]}")
|
||||
success = False
|
||||
|
||||
if callback:
|
||||
callback(
|
||||
"complete" if success else "error",
|
||||
"Bootloader " + ("running" if success else "failed"),
|
||||
)
|
||||
|
||||
return [
|
||||
BitstreamResult(
|
||||
step="bitstream",
|
||||
success=success,
|
||||
message=(
|
||||
"DAP error — board needs power cycle"
|
||||
if dap_errors else
|
||||
"Bitstream loaded" if success else "Combined flow failed"
|
||||
),
|
||||
output=output[:4000],
|
||||
),
|
||||
BitstreamResult(
|
||||
step="elf",
|
||||
success=success,
|
||||
message=(
|
||||
"DAP error — board needs power cycle"
|
||||
if dap_errors else
|
||||
"ELF executed" if success else "Combined flow failed"
|
||||
),
|
||||
output=output[:4000],
|
||||
),
|
||||
]
|
||||
except subprocess.TimeoutExpired:
|
||||
os.unlink(tcl_file)
|
||||
return [
|
||||
BitstreamResult(step="bitstream", success=False,
|
||||
message="Combined JTAG flow timed out"),
|
||||
BitstreamResult(step="elf", success=False,
|
||||
message="Combined JTAG flow timed out"),
|
||||
]
|
||||
|
||||
+91
-22
@@ -82,6 +82,7 @@ class MainWindow(ctk.CTk):
|
||||
self._zynq_jtag_present: bool = False
|
||||
self._zynq_jtag_info = None
|
||||
self._uart_monitor: UartMonitor | None = None
|
||||
self._uart_window: UartMonitorWindow | None = None
|
||||
self._flash_phase: str = ""
|
||||
|
||||
# StringVar references for config sync
|
||||
@@ -157,21 +158,46 @@ class MainWindow(ctk.CTk):
|
||||
if self._config_path:
|
||||
try:
|
||||
self._config = Config.from_file(self._config_path)
|
||||
# If loaded from default_config.yaml, set save path to config.yaml
|
||||
if self._config_path.name == "default_config.yaml":
|
||||
user_config_path = self._get_user_config_path()
|
||||
self._config._config_path = user_config_path
|
||||
except Exception:
|
||||
self._config = Config.from_default()
|
||||
# Set save path to config.yaml for new configs
|
||||
user_config_path = self._get_user_config_path()
|
||||
self._config._config_path = user_config_path
|
||||
else:
|
||||
self._config = Config.from_default()
|
||||
# Set save path to config.yaml for new configs
|
||||
user_config_path = self._get_user_config_path()
|
||||
self._config._config_path = user_config_path
|
||||
|
||||
def _reload_config(self) -> None:
|
||||
"""Re-read config.yaml and update UI selectors with latest paths."""
|
||||
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
|
||||
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),
|
||||
]:
|
||||
val = getattr(fresh, attr, "")
|
||||
if val:
|
||||
selector.set_path(str(fresh.resolve_path(val)))
|
||||
# Update IP
|
||||
if self._ip_string_var:
|
||||
self._ip_string_var.set(fresh.zynq_ip)
|
||||
# Update xilinx path
|
||||
if hasattr(fresh, 'xilinx_path') and self._xilinx_path_var:
|
||||
self._xilinx_path_var.set(fresh.xilinx_path)
|
||||
except Exception:
|
||||
pass # Keep existing config on error
|
||||
|
||||
def _sync_config_from_ui(self) -> None:
|
||||
"""Read current UI values and update the Config object.
|
||||
|
||||
@@ -432,7 +458,7 @@ class MainWindow(ctk.CTk):
|
||||
row=0, column=0, sticky="nsew",
|
||||
padx=PADDING, pady=PADDING,
|
||||
)
|
||||
panel.grid_rowconfigure(8, weight=1)
|
||||
panel.grid_rowconfigure(9, weight=1)
|
||||
|
||||
title = ctk.CTkLabel(
|
||||
panel,
|
||||
@@ -465,6 +491,17 @@ class MainWindow(ctk.CTk):
|
||||
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)
|
||||
|
||||
# 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", "*")],
|
||||
)
|
||||
if self._config.fsbl_elf_path:
|
||||
resolved = self._config.resolve_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)
|
||||
|
||||
# BIT path (Bootloader)
|
||||
self._bit_selector = FileSelector(
|
||||
panel,
|
||||
@@ -474,7 +511,7 @@ class MainWindow(ctk.CTk):
|
||||
if self._config.bootloader_bit_path:
|
||||
resolved = self._config.resolve_path(self._config.bootloader_bit_path)
|
||||
self._bit_selector.set_path(str(resolved))
|
||||
self._bit_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
self._bit_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
# ELF path (Bootloader)
|
||||
self._elf_selector = FileSelector(
|
||||
@@ -485,7 +522,7 @@ class MainWindow(ctk.CTk):
|
||||
if self._config.bootloader_elf_path:
|
||||
resolved = self._config.resolve_path(self._config.bootloader_elf_path)
|
||||
self._elf_selector.set_path(str(resolved))
|
||||
self._elf_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
self._elf_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
# Bootloader BIN path (Flash programming)
|
||||
self._bootloader_bin_selector = FileSelector(
|
||||
@@ -496,22 +533,11 @@ class MainWindow(ctk.CTk):
|
||||
if self._config.bootloader_bin_path:
|
||||
resolved = self._config.resolve_path(self._config.bootloader_bin_path)
|
||||
self._bootloader_bin_selector.set_path(str(resolved))
|
||||
self._bootloader_bin_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
# FSBL ELF path (required by program_flash for QSPI programming)
|
||||
self._fsbl_selector = FileSelector(
|
||||
panel,
|
||||
label_text="FSBL ELF (.elf):",
|
||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||
)
|
||||
if self._config.fsbl_elf_path:
|
||||
resolved = self._config.resolve_path(self._config.fsbl_elf_path)
|
||||
self._fsbl_selector.set_path(str(resolved))
|
||||
self._fsbl_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
self._bootloader_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
# Flash options
|
||||
flash_frame = ctk.CTkFrame(panel)
|
||||
flash_frame.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
flash_frame.grid(row=7, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
flash_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._erase_cb_var = ctk.BooleanVar(value=self._config.erase_all)
|
||||
@@ -845,9 +871,22 @@ class MainWindow(ctk.CTk):
|
||||
self._bitstream_callback,
|
||||
)
|
||||
|
||||
dap_fail = False
|
||||
for result in results:
|
||||
status = "✓" if result.success else "✗"
|
||||
self._log_message(f" {status} {result.step}: {result.message}")
|
||||
if "DAP" in result.message and not result.success:
|
||||
dap_fail = True
|
||||
|
||||
if dap_fail:
|
||||
self._log_message("")
|
||||
self._log_message(" ╔══════════════════════════════════════════╗")
|
||||
self._log_message(" ║ DAP ERROR — JTAG debug port hung ║")
|
||||
self._log_message(" ║ This is caused by prior flash programming║")
|
||||
self._log_message(" ║ → Power-cycle the board (unplug/replug) ║")
|
||||
self._log_message(" ║ → Then re-run Step 3 ║")
|
||||
self._log_message(" ╚══════════════════════════════════════════╝")
|
||||
self._log_message("")
|
||||
|
||||
return all(r.success for r in results)
|
||||
except Exception as e:
|
||||
@@ -988,6 +1027,9 @@ class MainWindow(ctk.CTk):
|
||||
if index >= len(self._steps):
|
||||
return
|
||||
|
||||
# Always re-read config.yaml before running
|
||||
self._reload_config()
|
||||
|
||||
self._is_running = True
|
||||
self._run_btn.configure(state="disabled", text="Running...")
|
||||
self._progress.reset()
|
||||
@@ -1021,6 +1063,9 @@ class MainWindow(ctk.CTk):
|
||||
if self._is_running:
|
||||
return
|
||||
|
||||
# Always re-read config.yaml before running
|
||||
self._reload_config()
|
||||
|
||||
self._is_running = True
|
||||
self._run_btn.configure(state="disabled", text="Running...")
|
||||
self._progress.reset()
|
||||
@@ -1106,6 +1151,17 @@ class MainWindow(ctk.CTk):
|
||||
|
||||
# Stop on first failure when running all steps
|
||||
if not single_step and not success:
|
||||
# Special case: Step 2 pass → Step 3 DAP fail = board needs power cycle
|
||||
if i == 2 and results[0] and not success:
|
||||
self._log_message("")
|
||||
self._log_message(" ┌─────────────────────────────────────────┐")
|
||||
self._log_message(" │ Step 3 failed after flash programming. │")
|
||||
self._log_message(" │ This is normal — program_flash leaves │")
|
||||
self._log_message(" │ the JTAG DAP in error state. │")
|
||||
self._log_message(" │ → Power-cycle the board, then re-run │")
|
||||
self._log_message(" │ Step 3 (Load Bootloader) alone. │")
|
||||
self._log_message(" └─────────────────────────────────────────┘")
|
||||
self._log_message("")
|
||||
break
|
||||
|
||||
# Inter-step delay (skip after last step)
|
||||
@@ -1148,6 +1204,12 @@ class MainWindow(ctk.CTk):
|
||||
|
||||
def _start_uart_monitor(self) -> None:
|
||||
"""Start background serial monitoring if UART is available."""
|
||||
# If UART window is already monitoring, don't start a second instance
|
||||
if (self._uart_window
|
||||
and self._uart_window.winfo_exists()
|
||||
and self._uart_window.is_monitoring):
|
||||
self._log_message(" [UART] Already monitoring via UART window")
|
||||
return
|
||||
if not self._uart_available or not self._config:
|
||||
return
|
||||
port = self._config.serial_port
|
||||
@@ -1157,8 +1219,9 @@ class MainWindow(ctk.CTk):
|
||||
self._uart_monitor = UartMonitor(port, self._config.serial_baudrate)
|
||||
self._uart_monitor.on_line = lambda line: self._log_message(f" [UART] {line}")
|
||||
self._uart_monitor.on_boot_info = lambda info: (
|
||||
self._log_message(f" [UART] Boot detected: IP={info.ip_address}, Ver={info.version}")
|
||||
if info.ip_address or info.version else None
|
||||
self._log_message(
|
||||
f" [UART] Boot={info.boot_mode}, IP={info.ip_address}, Ver={info.version}"
|
||||
) if any([info.boot_mode, info.ip_address, info.version]) else None
|
||||
)
|
||||
self._uart_monitor.on_error = lambda e: self._log_message(f" [UART] Error: {e}")
|
||||
if self._uart_monitor.start():
|
||||
@@ -1191,6 +1254,9 @@ class MainWindow(ctk.CTk):
|
||||
def _on_close(self) -> None:
|
||||
"""Handle window close event: save config and cleanup."""
|
||||
self._stop_uart_monitor()
|
||||
if self._uart_window and self._uart_window.winfo_exists():
|
||||
self._uart_window.destroy()
|
||||
self._uart_window = None
|
||||
if self._config:
|
||||
self._sync_config_from_ui() # Sync UI values first
|
||||
# Save to user config file (config.yaml)
|
||||
@@ -1206,7 +1272,10 @@ class MainWindow(ctk.CTk):
|
||||
"""Open the UART Monitor window."""
|
||||
port = self._port_var.get()
|
||||
baudrate = self._config.serial_baudrate if self._config else 115200
|
||||
UartMonitorWindow(self, port=port, baudrate=baudrate)
|
||||
self._uart_window = UartMonitorWindow(
|
||||
self, port=port, baudrate=baudrate,
|
||||
on_log=self._log_message,
|
||||
)
|
||||
|
||||
def _test_serial_version(self) -> None:
|
||||
"""Test serial port by sending 'ver()' command and parsing response."""
|
||||
|
||||
+15
-1
@@ -786,6 +786,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
||||
master: ctk.CTk | ctk.CTkToplevel,
|
||||
port: str,
|
||||
baudrate: int = 115200,
|
||||
on_log: Callable[[str], None] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the UART monitor window.
|
||||
|
||||
@@ -793,6 +794,7 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
||||
master: Parent window (MainWindow).
|
||||
port: Serial port device path (e.g., '/dev/ttyUSB0').
|
||||
baudrate: Baud rate for serial communication.
|
||||
on_log: Optional callback to forward parsed events to main log.
|
||||
"""
|
||||
super().__init__(master)
|
||||
self.title("UART Monitor")
|
||||
@@ -800,6 +802,8 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
||||
self.minsize(480, 360)
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
self._on_log = on_log
|
||||
|
||||
self._port = port
|
||||
self._baudrate = baudrate
|
||||
self._monitor: UartMonitor | None = None
|
||||
@@ -991,12 +995,17 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
||||
|
||||
def _on_boot_info(self, info) -> None:
|
||||
parts = []
|
||||
if info.boot_mode:
|
||||
parts.append(f"Boot={info.boot_mode}")
|
||||
if info.ip_address:
|
||||
parts.append(f"IP={info.ip_address}")
|
||||
if info.version:
|
||||
parts.append(f"Ver={info.version}")
|
||||
if parts:
|
||||
self.after(0, lambda: self._log(f"[BOOT] {' '.join(parts)}\n"))
|
||||
# Forward to main log only (not UART window)
|
||||
if self._on_log:
|
||||
msg = f"[UART] {' '.join(parts)}"
|
||||
self.after(0, lambda: self._on_log(msg))
|
||||
|
||||
def _on_error(self, error: str) -> None:
|
||||
self.after(0, lambda: self._log_error(error))
|
||||
@@ -1024,6 +1033,11 @@ class UartMonitorWindow(ctk.CTkToplevel):
|
||||
|
||||
# ── Window Close ───────────────────────────────────────────
|
||||
|
||||
@property
|
||||
def is_monitoring(self) -> bool:
|
||||
"""True if the UART monitor thread is currently running."""
|
||||
return self._monitor is not None and self._monitor.is_running()
|
||||
|
||||
def _on_close(self) -> None:
|
||||
"""Cleanup UartMonitor and destroy window."""
|
||||
if self._monitor:
|
||||
|
||||
+31
-6
@@ -23,6 +23,7 @@ class BootInfo:
|
||||
ip_address: str = ""
|
||||
version: str = ""
|
||||
boot_message: str = ""
|
||||
boot_mode: str = ""
|
||||
raw_lines: list[str] | None = None
|
||||
|
||||
|
||||
@@ -73,10 +74,19 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
||||
ip_pattern = re.compile(r"\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b")
|
||||
version_patterns = [
|
||||
re.compile(r"[Vv]ersion[:\s]+([^\s;,\n]+)", re.IGNORECASE),
|
||||
re.compile(r"[Bb]oot\s+([^\s;,\n]+)", re.IGNORECASE),
|
||||
re.compile(r"[Ff]irmware\s+([^\s;,\n]+)", re.IGNORECASE),
|
||||
re.compile(r"(?:^|[\s:=])(v\d+\.\d+\.\d+)(?:\s|$)", re.IGNORECASE),
|
||||
re.compile(r"[Bb]oot\s+(?:version\s+)?([Vv]?\d+\.\d+[^\s;,\n]*)", re.IGNORECASE),
|
||||
re.compile(r"[Ff]irm[Ww]are:?\s*([^\s;,\n]+)", re.IGNORECASE),
|
||||
re.compile(r"(?:^|[\s:=])([Vv]\d+\.\d+\.\d+[^\s;,\n]*)", re.IGNORECASE),
|
||||
]
|
||||
|
||||
def _is_valid_version(v: str) -> bool:
|
||||
"""Filter out noise like '3.1', 'mode', 'update'."""
|
||||
if len(v) < 4:
|
||||
return False
|
||||
has_digit = any(c.isdigit() for c in v)
|
||||
has_dot = "." in v or "_" in v
|
||||
noise = {"mode", "upate", "update", "done", "error", "loaded", "running"}
|
||||
return has_digit and has_dot and v.lower() not in noise
|
||||
boot_complete_patterns = [
|
||||
re.compile(r"boot\s+complete", re.IGNORECASE),
|
||||
re.compile(r"system\s+ready", re.IGNORECASE),
|
||||
@@ -84,6 +94,10 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
||||
re.compile(r"Linux\s+booted", re.IGNORECASE),
|
||||
re.compile(r"QSYS\s+ready", re.IGNORECASE),
|
||||
]
|
||||
boot_mode_patterns = [
|
||||
re.compile(r"[Bb]oot\s*[Mm]ode\s*(?:is)?\s*[:\[=\s]*(\w+)"),
|
||||
re.compile(r"BootMode\s*[:\[=\s]*(\w+)", re.IGNORECASE),
|
||||
]
|
||||
|
||||
for line in lines:
|
||||
# Extract IP address
|
||||
@@ -96,7 +110,7 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
||||
if not info.version:
|
||||
for pattern in version_patterns:
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
if match and _is_valid_version(match.group(1)):
|
||||
info.version = match.group(1)
|
||||
break
|
||||
|
||||
@@ -107,6 +121,14 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
||||
info.boot_message = line.strip()
|
||||
break
|
||||
|
||||
# Extract boot mode
|
||||
if not info.boot_mode:
|
||||
for pattern in boot_mode_patterns:
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
info.boot_mode = match.group(1)
|
||||
break
|
||||
|
||||
return info
|
||||
|
||||
|
||||
@@ -332,6 +354,7 @@ class UartMonitor:
|
||||
self._lock = threading.Lock()
|
||||
self._all_lines: list[str] = []
|
||||
self._latest_info = BootInfo(raw_lines=[])
|
||||
self._last_emitted: tuple[str, str, str] = ("", "", "")
|
||||
|
||||
# Callbacks — set these before calling start()
|
||||
self.on_line: Callable[[str], None] | None = None
|
||||
@@ -428,8 +451,10 @@ class UartMonitor:
|
||||
with self._lock:
|
||||
self._latest_info = info
|
||||
|
||||
# Notify if we have new boot info
|
||||
if info.boot_message or info.ip_address or info.version:
|
||||
# Notify only when parsed info changes (dedup)
|
||||
current = (info.boot_mode, info.ip_address, info.version)
|
||||
if current != self._last_emitted and any(current):
|
||||
self._last_emitted = current
|
||||
if self.on_boot_info:
|
||||
self.on_boot_info(info)
|
||||
else:
|
||||
|
||||
+104
-154
@@ -1,16 +1,14 @@
|
||||
"""TFTP file transfer manager for Zynq Flasher GUI.
|
||||
|
||||
Handles uploading, downloading, and CRC verification of files
|
||||
via TFTP to the Zynq device.
|
||||
via TFTP to the Zynq device using a pure-Python TFTP client.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import binascii
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import socket
|
||||
import struct
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -35,18 +33,17 @@ class TftpResult:
|
||||
|
||||
ProgressCallback = Callable[[str, str], None] | None
|
||||
|
||||
# TFTP opcodes
|
||||
_TFTP_RRQ = 1
|
||||
_TFTP_WRQ = 2
|
||||
_TFTP_DATA = 3
|
||||
_TFTP_ACK = 4
|
||||
_TFTP_ERROR = 5
|
||||
_TFTP_BLOCK_SIZE = 512
|
||||
|
||||
|
||||
def _compute_crc32(file_path: str | Path) -> int:
|
||||
"""Compute CRC32 checksum of a file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file.
|
||||
|
||||
Returns:
|
||||
CRC32 value as unsigned integer.
|
||||
"""
|
||||
import binascii
|
||||
|
||||
"""Compute CRC32 checksum of a file."""
|
||||
crc = 0
|
||||
with open(file_path, "rb") as f:
|
||||
while True:
|
||||
@@ -57,15 +54,66 @@ def _compute_crc32(file_path: str | Path) -> int:
|
||||
return crc & 0xFFFFFFFF
|
||||
|
||||
|
||||
def _find_tftp_client() -> str | None:
|
||||
"""Find the tftp client executable.
|
||||
def _tftp_put(host: str, file_path: str, remote_name: str,
|
||||
timeout: float = 10.0) -> tuple[bool, str]:
|
||||
"""Upload a file to a TFTP server using pure Python.
|
||||
|
||||
Uses shutil.which() for cross-platform compatibility.
|
||||
Args:
|
||||
host: TFTP server IP address.
|
||||
file_path: Local file path.
|
||||
remote_name: Remote filename.
|
||||
timeout: Socket timeout in seconds.
|
||||
|
||||
Returns:
|
||||
Path to tftp executable, or None if not found.
|
||||
(success, message).
|
||||
"""
|
||||
return shutil.which("tftp")
|
||||
data = Path(file_path).read_bytes()
|
||||
total = len(data)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(timeout)
|
||||
|
||||
try:
|
||||
# ── Send WRQ ──
|
||||
wrq = struct.pack("!H", _TFTP_WRQ)
|
||||
wrq += remote_name.encode("ascii") + b"\x00"
|
||||
wrq += b"octet\x00"
|
||||
sock.sendto(wrq, (host, 69))
|
||||
|
||||
# ── Wait for ACK(0) to get server's data port ──
|
||||
ack_data, server_addr = sock.recvfrom(516)
|
||||
if len(ack_data) < 4:
|
||||
return False, "Invalid ACK from server"
|
||||
opcode = struct.unpack("!H", ack_data[:2])[0]
|
||||
if opcode == _TFTP_ERROR:
|
||||
err_msg = ack_data[4:].decode("ascii", errors="replace").rstrip("\x00")
|
||||
return False, f"TFTP error: {err_msg}"
|
||||
if opcode != _TFTP_ACK:
|
||||
return False, f"Expected ACK, got opcode {opcode}"
|
||||
|
||||
# ── Send data blocks ──
|
||||
block = 1
|
||||
offset = 0
|
||||
while offset < total:
|
||||
chunk = data[offset:offset + _TFTP_BLOCK_SIZE]
|
||||
offset += len(chunk)
|
||||
data_pkt = struct.pack("!HH", _TFTP_DATA, block) + chunk
|
||||
sock.sendto(data_pkt, server_addr)
|
||||
|
||||
# Wait for ACK
|
||||
try:
|
||||
ack_data, _ = sock.recvfrom(516)
|
||||
ack_op = struct.unpack("!H", ack_data[:2])[0]
|
||||
ack_block = struct.unpack("!H", ack_data[2:4])[0]
|
||||
if ack_op != _TFTP_ACK or ack_block != block:
|
||||
return False, f"Bad ACK: op={ack_op} block={ack_block}"
|
||||
except socket.timeout:
|
||||
return False, f"Timeout waiting for ACK block {block}"
|
||||
block += 1
|
||||
|
||||
return True, f"Uploaded {total} bytes in {block - 1} blocks"
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def tftp_upload(
|
||||
@@ -74,7 +122,7 @@ def tftp_upload(
|
||||
remote_name: str | None = None,
|
||||
callback: ProgressCallback = None,
|
||||
) -> TftpResult:
|
||||
"""Upload a file to the Zynq device via TFTP.
|
||||
"""Upload a file to the Zynq device via TFTP (pure Python).
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
@@ -90,83 +138,42 @@ def tftp_upload(
|
||||
|
||||
if not file_path.exists():
|
||||
return TftpResult(
|
||||
step="upload",
|
||||
success=False,
|
||||
step="upload", success=False,
|
||||
message=f"File not found: {file_path}",
|
||||
local_path=str(file_path),
|
||||
remote_path=remote_name,
|
||||
local_path=str(file_path), remote_path=remote_name,
|
||||
)
|
||||
|
||||
if callback:
|
||||
callback("start", f"Uploading {file_path.name} -> {remote_name}")
|
||||
callback("start",
|
||||
f"Uploading {file_path.name} -> {remote_name}")
|
||||
|
||||
# Verify device is reachable first
|
||||
# Verify device is reachable
|
||||
if not ping_ip(config.zynq_ip, config.ping_count, config.ping_timeout):
|
||||
return TftpResult(
|
||||
step="upload",
|
||||
success=False,
|
||||
step="upload", success=False,
|
||||
message=f"Device {config.zynq_ip} not reachable",
|
||||
local_path=str(file_path),
|
||||
remote_path=remote_name,
|
||||
local_path=str(file_path), remote_path=remote_name,
|
||||
)
|
||||
|
||||
tftp_cmd = _find_tftp_client()
|
||||
if not tftp_cmd:
|
||||
try:
|
||||
ok, msg = _tftp_put(config.zynq_ip, str(file_path), remote_name)
|
||||
except OSError as e:
|
||||
return TftpResult(
|
||||
step="upload",
|
||||
success=False,
|
||||
message="tftp client not found",
|
||||
local_path=str(file_path),
|
||||
remote_path=remote_name,
|
||||
step="upload", success=False,
|
||||
message=f"Socket error: {e}",
|
||||
local_path=str(file_path), remote_path=remote_name,
|
||||
)
|
||||
|
||||
if callback:
|
||||
callback("progress", f"Running: {tftp_cmd}")
|
||||
callback("complete" if ok else "error",
|
||||
"Upload " + ("succeeded" if ok else "failed"))
|
||||
|
||||
# Build TFTP command
|
||||
cmd = [tftp_cmd, "-m", "binary", config.zynq_ip, "put", str(file_path), remote_name]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
success = result.returncode == 0
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
|
||||
if callback:
|
||||
callback("complete" if success else "error",
|
||||
"Upload " + ("succeeded" if success else "failed"))
|
||||
|
||||
return TftpResult(
|
||||
step="upload",
|
||||
success=success,
|
||||
message=output or ("Upload succeeded" if success else "Upload failed"),
|
||||
local_path=str(file_path),
|
||||
remote_path=remote_name,
|
||||
crc_local=_compute_crc32(file_path),
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
if callback:
|
||||
callback("error", "Upload timed out")
|
||||
return TftpResult(
|
||||
step="upload",
|
||||
success=False,
|
||||
message="Upload timed out",
|
||||
local_path=str(file_path),
|
||||
remote_path=remote_name,
|
||||
crc_local=_compute_crc32(file_path),
|
||||
)
|
||||
except OSError as e:
|
||||
return TftpResult(
|
||||
step="upload",
|
||||
success=False,
|
||||
message=f"OS error: {e}",
|
||||
local_path=str(file_path),
|
||||
remote_path=remote_name,
|
||||
)
|
||||
return TftpResult(
|
||||
step="upload", success=ok,
|
||||
message=msg,
|
||||
local_path=str(file_path), remote_path=remote_name,
|
||||
crc_local=_compute_crc32(file_path) if ok else 0,
|
||||
)
|
||||
|
||||
|
||||
def tftp_download(
|
||||
@@ -175,85 +182,28 @@ def tftp_download(
|
||||
local_dir: str | Path | None = None,
|
||||
callback: ProgressCallback = None,
|
||||
) -> TftpResult:
|
||||
"""Download a file from the Zynq device via TFTP.
|
||||
"""Download a file from the Zynq device via TFTP (pure Python).
|
||||
|
||||
Note: This requires the Zynq to act as a TFTP client (U-Boot tftpboot),
|
||||
which typically means we need a TFTP server on the PC. For simplicity,
|
||||
this operation is not natively supported — use ping-based verification
|
||||
or UART log analysis instead.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
remote_name: Remote filename on the TFTP server.
|
||||
local_dir: Local directory to save the file (default: temp directory).
|
||||
local_dir: Local directory to save the file.
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
TftpResult with download status.
|
||||
TftpResult with status (always not-implemented).
|
||||
"""
|
||||
local_dir = Path(local_dir or tempfile.gettempdir())
|
||||
local_path = local_dir / remote_name
|
||||
|
||||
if callback:
|
||||
callback("start", f"Downloading {remote_name} -> {local_path}")
|
||||
|
||||
# Verify device is reachable
|
||||
if not ping_ip(config.zynq_ip, config.ping_count, config.ping_timeout):
|
||||
return TftpResult(
|
||||
step="download",
|
||||
success=False,
|
||||
message=f"Device {config.zynq_ip} not reachable",
|
||||
local_path=str(local_path),
|
||||
remote_path=remote_name,
|
||||
)
|
||||
|
||||
tftp_cmd = _find_tftp_client()
|
||||
if not tftp_cmd:
|
||||
return TftpResult(
|
||||
step="download",
|
||||
success=False,
|
||||
message="tftp client not found",
|
||||
local_path=str(local_path),
|
||||
remote_path=remote_name,
|
||||
)
|
||||
|
||||
cmd = [tftp_cmd, "-m", "binary", config.zynq_ip, "get", remote_name, str(local_path)]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
success = result.returncode == 0
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
|
||||
if callback:
|
||||
callback("complete" if success else "error",
|
||||
"Download " + ("succeeded" if success else "failed"))
|
||||
|
||||
crc = _compute_crc32(local_path) if success and local_path.exists() else 0
|
||||
|
||||
return TftpResult(
|
||||
step="download",
|
||||
success=success,
|
||||
message=output or ("Download succeeded" if success else "Download failed"),
|
||||
local_path=str(local_path),
|
||||
remote_path=remote_name,
|
||||
crc_remote=crc,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return TftpResult(
|
||||
step="download",
|
||||
success=False,
|
||||
message="Download timed out",
|
||||
local_path=str(local_path),
|
||||
remote_path=remote_name,
|
||||
)
|
||||
except OSError as e:
|
||||
return TftpResult(
|
||||
step="download",
|
||||
success=False,
|
||||
message=f"OS error: {e}",
|
||||
local_path=str(local_path),
|
||||
remote_path=remote_name,
|
||||
)
|
||||
return TftpResult(
|
||||
step="download",
|
||||
success=False,
|
||||
message="TFTP download requires PC-side server (not implemented)",
|
||||
remote_path=remote_name,
|
||||
)
|
||||
|
||||
|
||||
def tftp_upload_verify(
|
||||
|
||||
Reference in New Issue
Block a user