✨ feat: add TFTP download verify, Step 4 sub-steps, and fix UI layout
- Implement _tftp_get() for downloading files from Zynq via TFTP - Add TftpSubStepFrame widget for Step 4 progress display - Refactor Step 4 into 5 sub-steps: Upload, Download Verify, CRC Check, Reboot, Boot Verify - Fix SubStepFrame.reset() bug that caused collapse after execution - Fix layout overlap: Step 4 sub-step and progress bar now use separate rows - Improve ProgressIndicator appearance with better styling and spacing - Fix UART monitor to start regardless of _uart_available flag - Add CRC32 verification between uploaded and downloaded files
This commit is contained in:
+139
-16
@@ -69,18 +69,21 @@ def _tftp_put(host: str, file_path: str, remote_name: str,
|
||||
"""
|
||||
data = Path(file_path).read_bytes()
|
||||
total = len(data)
|
||||
block_size = _TFTP_BLOCK_SIZE
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(timeout)
|
||||
# Allow reuse of local port (prevents "Address already in use")
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
try:
|
||||
# ── Send WRQ ──
|
||||
# ── Send WRQ to port 69 ──
|
||||
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 ──
|
||||
# ── Wait for ACK(0) — discover server's data port ──
|
||||
ack_data, server_addr = sock.recvfrom(516)
|
||||
if len(ack_data) < 4:
|
||||
return False, "Invalid ACK from server"
|
||||
@@ -89,24 +92,34 @@ def _tftp_put(host: str, file_path: str, remote_name: str,
|
||||
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}"
|
||||
return False, f"Expected ACK(0), got opcode {opcode} from {server_addr}"
|
||||
|
||||
srv_ip, srv_port = server_addr
|
||||
import sys
|
||||
print(f"[TFTP] ACK(0) received from {srv_ip}:{srv_port} "
|
||||
f"(file={total}B, blocks={-(-total // block_size)})", file=sys.stderr)
|
||||
|
||||
# ── Send data blocks ──
|
||||
block = 1
|
||||
offset = 0
|
||||
while offset < total:
|
||||
chunk = data[offset:offset + _TFTP_BLOCK_SIZE]
|
||||
chunk = data[offset:offset + block_size]
|
||||
offset += len(chunk)
|
||||
data_pkt = struct.pack("!HH", _TFTP_DATA, block) + chunk
|
||||
sock.sendto(data_pkt, server_addr)
|
||||
|
||||
if block % 100 == 0 or block == -(-total // block_size):
|
||||
pct = offset * 100 // total
|
||||
print(f"[TFTP] Block {block}/{-(-total // block_size)} ({pct}%) sent to {srv_ip}:{srv_port}",
|
||||
file=sys.stderr)
|
||||
|
||||
# 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}"
|
||||
return False, f"Bad ACK: op={ack_op} block={ack_block} (expected {block})"
|
||||
except socket.timeout:
|
||||
return False, f"Timeout waiting for ACK block {block}"
|
||||
block += 1
|
||||
@@ -116,6 +129,91 @@ def _tftp_put(host: str, file_path: str, remote_name: str,
|
||||
sock.close()
|
||||
|
||||
|
||||
def _tftp_get(host: str, remote_name: str, local_dir: str | Path,
|
||||
timeout: float = 30.0) -> tuple[bool, str, Path]:
|
||||
"""Download a file from a TFTP server using pure Python.
|
||||
|
||||
Args:
|
||||
host: TFTP server IP address.
|
||||
remote_name: Remote filename.
|
||||
local_dir: Local directory to save the downloaded file.
|
||||
timeout: Socket timeout in seconds.
|
||||
|
||||
Returns:
|
||||
(success, message, local_path).
|
||||
"""
|
||||
local_dir = Path(local_dir)
|
||||
local_dir.mkdir(parents=True, exist_ok=True)
|
||||
local_path = local_dir / remote_name
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(timeout)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
try:
|
||||
# ── Send RRQ to port 69 ──
|
||||
rrq = struct.pack("!H", _TFTP_RRQ)
|
||||
rrq += remote_name.encode("ascii") + b"\x00"
|
||||
rrq += b"octet\x00"
|
||||
sock.sendto(rrq, (host, 69))
|
||||
|
||||
# ── Wait for first DATA block — discover server's data port ──
|
||||
data_pkt, server_addr = sock.recvfrom(516)
|
||||
if len(data_pkt) < 4:
|
||||
return False, "Invalid DATA from server", local_path
|
||||
opcode = struct.unpack("!H", data_pkt[:2])[0]
|
||||
if opcode == _TFTP_ERROR:
|
||||
err_msg = data_pkt[4:].decode("ascii", errors="replace").rstrip("\x00")
|
||||
return False, f"TFTP error: {err_msg}", local_path
|
||||
if opcode != _TFTP_DATA:
|
||||
return False, f"Expected DATA, got opcode {opcode} from {server_addr}", local_path
|
||||
|
||||
srv_ip, srv_port = server_addr
|
||||
block = struct.unpack("!H", data_pkt[2:4])[0]
|
||||
file_data = data_pkt[4:]
|
||||
import sys
|
||||
print(f"[TFTP] DATA block 1 received from {srv_ip}:{srv_port} "
|
||||
f"({len(file_data)} bytes)", file=sys.stderr)
|
||||
|
||||
# ── ACK block 0 and receive subsequent blocks ──
|
||||
ack = struct.pack("!HH", _TFTP_ACK, 0)
|
||||
sock.sendto(ack, server_addr)
|
||||
|
||||
total = len(file_data)
|
||||
while len(file_data) == _TFTP_BLOCK_SIZE:
|
||||
# Wait for next DATA block
|
||||
try:
|
||||
data_pkt, _ = sock.recvfrom(516)
|
||||
if len(data_pkt) < 4:
|
||||
return False, "Incomplete DATA packet", local_path
|
||||
opcode = struct.unpack("!H", data_pkt[:2])[0]
|
||||
if opcode != _TFTP_DATA:
|
||||
return False, f"Expected DATA, got opcode {opcode}", local_path
|
||||
|
||||
new_block = struct.unpack("!H", data_pkt[2:4])[0]
|
||||
chunk = data_pkt[4:]
|
||||
file_data += chunk
|
||||
total = len(file_data)
|
||||
block = new_block
|
||||
|
||||
if block % 100 == 0:
|
||||
pct = total * 100 // max(total, 1)
|
||||
print(f"[TFTP] Block {block}/{block} ({pct}%) received from {srv_ip}:{srv_port}",
|
||||
file=sys.stderr)
|
||||
|
||||
# ACK this block
|
||||
ack = struct.pack("!HH", _TFTP_ACK, block)
|
||||
sock.sendto(ack, server_addr)
|
||||
except socket.timeout:
|
||||
return False, f"Timeout waiting for DATA block {block}", local_path
|
||||
|
||||
# ── Write file ──
|
||||
local_path.write_bytes(file_data)
|
||||
return True, f"Downloaded {total} bytes to {local_path}", local_path
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def tftp_upload(
|
||||
config: Config,
|
||||
file_path: str | Path,
|
||||
@@ -184,25 +282,50 @@ def tftp_download(
|
||||
) -> TftpResult:
|
||||
"""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.
|
||||
local_dir: Local directory to save the file (default: temp directory).
|
||||
callback: Optional progress callback.
|
||||
|
||||
Returns:
|
||||
TftpResult with status (always not-implemented).
|
||||
TftpResult with download status and CRC.
|
||||
"""
|
||||
if local_dir is None:
|
||||
local_dir = tempfile.gettempdir()
|
||||
local_dir = Path(local_dir)
|
||||
|
||||
if callback:
|
||||
callback("start", f"Downloading {remote_name} from {config.zynq_ip}")
|
||||
|
||||
# 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",
|
||||
remote_path=remote_name,
|
||||
)
|
||||
|
||||
try:
|
||||
ok, msg, local_path = _tftp_get(config.zynq_ip, remote_name, local_dir)
|
||||
except OSError as e:
|
||||
return TftpResult(
|
||||
step="download", success=False,
|
||||
message=f"Socket error: {e}",
|
||||
remote_path=remote_name,
|
||||
)
|
||||
|
||||
crc = _compute_crc32(local_path) if ok else 0
|
||||
|
||||
if callback:
|
||||
callback("complete" if ok else "error",
|
||||
"Download " + ("succeeded" if ok else "failed"))
|
||||
|
||||
return TftpResult(
|
||||
step="download",
|
||||
success=False,
|
||||
message="TFTP download requires PC-side server (not implemented)",
|
||||
remote_path=remote_name,
|
||||
step="download", success=ok,
|
||||
message=msg,
|
||||
local_path=str(local_path), remote_path=remote_name,
|
||||
crc_local=crc,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user