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:
Jeremy Shen
2026-06-10 16:57:54 +08:00
parent 9a7a625c46
commit 7067ce273f
3 changed files with 236 additions and 165 deletions
+104 -154
View File
@@ -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(