feat: user TCL flow + parser dedup + Python TFTP
1. bitstream_programmer — combined BIT+ELF flow (user's TCL):
- targets -set -nocase -filter {name =~ "arm*#0"}
- rst -processor (bypass BootROM, block Flash boot)
- dow fsbl → con → stop (FSBL hardware init)
- fpga -f bit (configure PL)
- dow app → con (run bootloader)
- Single xsct session, no ps7_init dependency
2. serial_monitor — parser fixes:
- Boot mode parsing: 'Boot mode is JTAG' → Boot=JTAG
- Version noise filter: rejects '3.1','update','mode','loaded'
- Deduplication: UartMonitor only emits when (boot_mode, IP, ver) changes
- Fixed false match: 'Boot mode' no longer captured as version
3. tftp_manager — pure Python TFTP client:
- _tftp_put(): RFC 1350 WRQ/DATA/ACK via UDP socket
- No external tftp command dependency
- tftp_download(): documented as not-implemented (needs PC server)
This commit is contained in:
+117
-8
@@ -535,7 +535,7 @@ def _run_elf_direct(
|
|||||||
message="ELF download timed out")
|
message="ELF download timed out")
|
||||||
|
|
||||||
|
|
||||||
# ── Full Workflow ───────────────────────────────────────────────────
|
# ── Full Workflow (combined BIT+ELF in one xsct session) ──────────
|
||||||
|
|
||||||
|
|
||||||
def full_bitstream_program(
|
def full_bitstream_program(
|
||||||
@@ -544,7 +544,13 @@ def full_bitstream_program(
|
|||||||
elf_path: str | Path,
|
elf_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> list[BitstreamResult]:
|
) -> 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:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
@@ -555,11 +561,114 @@ def full_bitstream_program(
|
|||||||
Returns:
|
Returns:
|
||||||
List of BitstreamResult for each step.
|
List of BitstreamResult for each step.
|
||||||
"""
|
"""
|
||||||
results: list[BitstreamResult] = []
|
import tempfile, os
|
||||||
|
|
||||||
results.append(program_bitstream(config, bit_path, callback))
|
bit_path = Path(bit_path)
|
||||||
if not results[-1].success:
|
elf_path = Path(elf_path)
|
||||||
return results
|
fsbl_path = config.fsbl_elf_path if hasattr(config, 'fsbl_elf_path') else ""
|
||||||
|
|
||||||
results.append(run_elf(config, elf_path, callback))
|
# Validate files
|
||||||
return results
|
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)...")
|
||||||
|
|
||||||
|
tcl = "\n".join([
|
||||||
|
'puts "INFO: Starting Zynq initialization via JTAG override..."',
|
||||||
|
"connect",
|
||||||
|
"after 500",
|
||||||
|
# 1. Select ARM Core 0
|
||||||
|
'targets -set -nocase -filter {name =~ "arm*#0"}',
|
||||||
|
# 2. Reset processor (bypass BootROM Flash boot)
|
||||||
|
'puts "INFO: Resetting processor (Bypassing BootROM Flash boot)..."',
|
||||||
|
"rst -processor",
|
||||||
|
"after 500",
|
||||||
|
# 3. Download FSBL to OCM
|
||||||
|
'puts "INFO: Downloading FSBL..."',
|
||||||
|
'dow "%s"' % fsbl_path,
|
||||||
|
# 4. Run FSBL for hardware init (clocks, DDR, MIO)
|
||||||
|
'puts "INFO: Running FSBL for hardware initialization..."',
|
||||||
|
"con",
|
||||||
|
"after 2000",
|
||||||
|
"stop",
|
||||||
|
'puts "INFO: Hardware initialization completed."',
|
||||||
|
# 5. Program PL bitstream
|
||||||
|
'puts "INFO: Downloading Bitstream..."',
|
||||||
|
'fpga -f "%s"' % bit_path,
|
||||||
|
"after 1000",
|
||||||
|
# 6. Back to ARM Core 0, download and run app
|
||||||
|
'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",
|
||||||
|
])
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if callback:
|
||||||
|
callback(
|
||||||
|
"complete" if success else "error",
|
||||||
|
"Bootloader " + ("running" if success else "failed"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
BitstreamResult(
|
||||||
|
step="bitstream",
|
||||||
|
success=success,
|
||||||
|
message="Bitstream loaded" if success else "Combined flow failed",
|
||||||
|
output=output[:4000],
|
||||||
|
),
|
||||||
|
BitstreamResult(
|
||||||
|
step="elf",
|
||||||
|
success=success,
|
||||||
|
message="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"),
|
||||||
|
]
|
||||||
|
|||||||
+15
-3
@@ -78,6 +78,15 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
|||||||
re.compile(r"[Ff]irm[Ww]are:?\s*([^\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),
|
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 = [
|
boot_complete_patterns = [
|
||||||
re.compile(r"boot\s+complete", re.IGNORECASE),
|
re.compile(r"boot\s+complete", re.IGNORECASE),
|
||||||
re.compile(r"system\s+ready", re.IGNORECASE),
|
re.compile(r"system\s+ready", re.IGNORECASE),
|
||||||
@@ -101,7 +110,7 @@ def parse_boot_output(lines: list[str]) -> BootInfo:
|
|||||||
if not info.version:
|
if not info.version:
|
||||||
for pattern in version_patterns:
|
for pattern in version_patterns:
|
||||||
match = pattern.search(line)
|
match = pattern.search(line)
|
||||||
if match:
|
if match and _is_valid_version(match.group(1)):
|
||||||
info.version = match.group(1)
|
info.version = match.group(1)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -345,6 +354,7 @@ class UartMonitor:
|
|||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._all_lines: list[str] = []
|
self._all_lines: list[str] = []
|
||||||
self._latest_info = BootInfo(raw_lines=[])
|
self._latest_info = BootInfo(raw_lines=[])
|
||||||
|
self._last_emitted: tuple[str, str, str] = ("", "", "")
|
||||||
|
|
||||||
# Callbacks — set these before calling start()
|
# Callbacks — set these before calling start()
|
||||||
self.on_line: Callable[[str], None] | None = None
|
self.on_line: Callable[[str], None] | None = None
|
||||||
@@ -441,8 +451,10 @@ class UartMonitor:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
self._latest_info = info
|
self._latest_info = info
|
||||||
|
|
||||||
# Notify if we have new boot info
|
# Notify only when parsed info changes (dedup)
|
||||||
if info.boot_message or info.ip_address or info.version:
|
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:
|
if self.on_boot_info:
|
||||||
self.on_boot_info(info)
|
self.on_boot_info(info)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+104
-154
@@ -1,16 +1,14 @@
|
|||||||
"""TFTP file transfer manager for Zynq Flasher GUI.
|
"""TFTP file transfer manager for Zynq Flasher GUI.
|
||||||
|
|
||||||
Handles uploading, downloading, and CRC verification of files
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import os
|
import socket
|
||||||
import platform
|
import struct
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -35,18 +33,17 @@ class TftpResult:
|
|||||||
|
|
||||||
ProgressCallback = Callable[[str, str], None] | None
|
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:
|
def _compute_crc32(file_path: str | Path) -> int:
|
||||||
"""Compute CRC32 checksum of a file.
|
"""Compute CRC32 checksum of a file."""
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Path to the file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
CRC32 value as unsigned integer.
|
|
||||||
"""
|
|
||||||
import binascii
|
|
||||||
|
|
||||||
crc = 0
|
crc = 0
|
||||||
with open(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
while True:
|
while True:
|
||||||
@@ -57,15 +54,66 @@ def _compute_crc32(file_path: str | Path) -> int:
|
|||||||
return crc & 0xFFFFFFFF
|
return crc & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
def _find_tftp_client() -> str | None:
|
def _tftp_put(host: str, file_path: str, remote_name: str,
|
||||||
"""Find the tftp client executable.
|
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:
|
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(
|
def tftp_upload(
|
||||||
@@ -74,7 +122,7 @@ def tftp_upload(
|
|||||||
remote_name: str | None = None,
|
remote_name: str | None = None,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> TftpResult:
|
) -> TftpResult:
|
||||||
"""Upload a file to the Zynq device via TFTP.
|
"""Upload a file to the Zynq device via TFTP (pure Python).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
@@ -90,83 +138,42 @@ def tftp_upload(
|
|||||||
|
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return TftpResult(
|
return TftpResult(
|
||||||
step="upload",
|
step="upload", success=False,
|
||||||
success=False,
|
|
||||||
message=f"File not found: {file_path}",
|
message=f"File not found: {file_path}",
|
||||||
local_path=str(file_path),
|
local_path=str(file_path), remote_path=remote_name,
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if callback:
|
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):
|
if not ping_ip(config.zynq_ip, config.ping_count, config.ping_timeout):
|
||||||
return TftpResult(
|
return TftpResult(
|
||||||
step="upload",
|
step="upload", success=False,
|
||||||
success=False,
|
|
||||||
message=f"Device {config.zynq_ip} not reachable",
|
message=f"Device {config.zynq_ip} not reachable",
|
||||||
local_path=str(file_path),
|
local_path=str(file_path), remote_path=remote_name,
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tftp_cmd = _find_tftp_client()
|
try:
|
||||||
if not tftp_cmd:
|
ok, msg = _tftp_put(config.zynq_ip, str(file_path), remote_name)
|
||||||
|
except OSError as e:
|
||||||
return TftpResult(
|
return TftpResult(
|
||||||
step="upload",
|
step="upload", success=False,
|
||||||
success=False,
|
message=f"Socket error: {e}",
|
||||||
message="tftp client not found",
|
local_path=str(file_path), remote_path=remote_name,
|
||||||
local_path=str(file_path),
|
|
||||||
remote_path=remote_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if callback:
|
if callback:
|
||||||
callback("progress", f"Running: {tftp_cmd}")
|
callback("complete" if ok else "error",
|
||||||
|
"Upload " + ("succeeded" if ok else "failed"))
|
||||||
|
|
||||||
# Build TFTP command
|
return TftpResult(
|
||||||
cmd = [tftp_cmd, "-m", "binary", config.zynq_ip, "put", str(file_path), remote_name]
|
step="upload", success=ok,
|
||||||
|
message=msg,
|
||||||
try:
|
local_path=str(file_path), remote_path=remote_name,
|
||||||
result = subprocess.run(
|
crc_local=_compute_crc32(file_path) if ok else 0,
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def tftp_download(
|
def tftp_download(
|
||||||
@@ -175,85 +182,28 @@ def tftp_download(
|
|||||||
local_dir: str | Path | None = None,
|
local_dir: str | Path | None = None,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> TftpResult:
|
) -> 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:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
remote_name: Remote filename on the TFTP server.
|
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.
|
callback: Optional progress callback.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
TftpResult with download status.
|
TftpResult with status (always not-implemented).
|
||||||
"""
|
"""
|
||||||
local_dir = Path(local_dir or tempfile.gettempdir())
|
return TftpResult(
|
||||||
local_path = local_dir / remote_name
|
step="download",
|
||||||
|
success=False,
|
||||||
if callback:
|
message="TFTP download requires PC-side server (not implemented)",
|
||||||
callback("start", f"Downloading {remote_name} -> {local_path}")
|
remote_path=remote_name,
|
||||||
|
)
|
||||||
# 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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def tftp_upload_verify(
|
def tftp_upload_verify(
|
||||||
|
|||||||
Reference in New Issue
Block a user