SXXXXXXX_PyDownloadFwViaSRIO/pydownloadfwviasrio/tftp_client.py
2026-01-22 17:10:05 +01:00

311 lines
11 KiB
Python

"""TFTP client for SRIO-over-TFTP communication.
This module implements the sender side: it constructs special TFTP filenames
that encode SRIO read/write commands and uses a simple TFTP implementation
to communicate with the target (Windows-compatible).
Protocol format (compatible with Vecchia_app):
- Write: PUT "$SRIO:<slot>/<address>+<len>" with binary payload
- Read: GET "$SRIO:<slot>/<address>+<len>" returns binary data
Reference: fgpaprogrammer.cpp rtgWrite/rtgRead functions.
Note: This is a minimal TFTP client implementation for Windows compatibility.
For production, consider using a full-featured TFTP library.
"""
from __future__ import annotations
import socket
import struct
import time
from typing import Callable, Optional
class SimpleTFTPClient:
"""Minimal TFTP client (RFC 1350) for Windows compatibility.
Implements only write (WRQ) and read (RRQ) operations needed for SRIO protocol.
"""
# TFTP opcodes
OP_RRQ = 1 # Read request
OP_WRQ = 2 # Write request
OP_DATA = 3 # Data
OP_ACK = 4 # Acknowledgment
OP_ERROR = 5 # Error
def __init__(self, server_ip: str, server_port: int = 69, timeout: float = 5.0) -> None:
self.server_ip = server_ip
self.server_port = server_port
self.timeout = timeout
self.block_size = 512 # TFTP default
def _send_rrq(self, filename: str, sock: socket.socket) -> None:
"""Send Read Request."""
mode = b"octet"
packet = struct.pack("!H", self.OP_RRQ) + filename.encode() + b"\x00" + mode + b"\x00"
sock.sendto(packet, (self.server_ip, self.server_port))
def _send_wrq(self, filename: str, sock: socket.socket) -> None:
"""Send Write Request."""
mode = b"octet"
packet = struct.pack("!H", self.OP_WRQ) + filename.encode() + b"\x00" + mode + b"\x00"
sock.sendto(packet, (self.server_ip, self.server_port))
def _send_ack(self, block_num: int, sock: socket.socket, addr: tuple) -> None:
"""Send ACK."""
packet = struct.pack("!HH", self.OP_ACK, block_num)
sock.sendto(packet, addr)
def _send_data(self, block_num: int, data: bytes, sock: socket.socket, addr: tuple) -> None:
"""Send DATA."""
packet = struct.pack("!HH", self.OP_DATA, block_num) + data
sock.sendto(packet, addr)
def download(self, filename: str) -> Optional[bytes]:
"""Download file via TFTP GET (RRQ)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(self.timeout)
try:
self._send_rrq(filename, sock)
data_buffer = bytearray()
expected_block = 1
while True:
packet, server_addr = sock.recvfrom(4096)
opcode = struct.unpack("!H", packet[:2])[0]
if opcode == self.OP_DATA:
block_num = struct.unpack("!H", packet[2:4])[0]
data = packet[4:]
if block_num == expected_block:
data_buffer.extend(data)
self._send_ack(block_num, sock, server_addr)
expected_block += 1
# Last packet (less than 512 bytes)
if len(data) < self.block_size:
break
elif opcode == self.OP_ERROR:
error_code = struct.unpack("!H", packet[2:4])[0]
error_msg = packet[4:].decode('ascii', errors='ignore').rstrip('\x00')
raise Exception(f"TFTP Error {error_code}: {error_msg}")
return bytes(data_buffer)
except socket.timeout:
raise Exception("TFTP timeout")
finally:
sock.close()
def upload(self, filename: str, data: bytes) -> bool:
"""Upload file via TFTP PUT (WRQ)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(self.timeout)
try:
self._send_wrq(filename, sock)
# Wait for initial ACK (block 0)
packet, server_addr = sock.recvfrom(4096)
opcode = struct.unpack("!H", packet[:2])[0]
if opcode == self.OP_ERROR:
error_code = struct.unpack("!H", packet[2:4])[0]
error_msg = packet[4:].decode('ascii', errors='ignore').rstrip('\x00')
raise Exception(f"TFTP Error {error_code}: {error_msg}")
if opcode != self.OP_ACK:
raise Exception("Expected ACK after WRQ")
# Send data blocks
block_num = 1
offset = 0
while offset < len(data):
chunk = data[offset : offset + self.block_size]
self._send_data(block_num, chunk, sock, server_addr)
# Wait for ACK
packet, _ = sock.recvfrom(4096)
opcode = struct.unpack("!H", packet[:2])[0]
if opcode == self.OP_ERROR:
error_code = struct.unpack("!H", packet[2:4])[0]
error_msg = packet[4:].decode('ascii', errors='ignore').rstrip('\x00')
raise Exception(f"TFTP Error {error_code}: {error_msg}")
if opcode != self.OP_ACK:
raise Exception(f"Expected ACK for block {block_num}")
ack_block = struct.unpack("!H", packet[2:4])[0]
if ack_block != block_num:
raise Exception(f"ACK mismatch: expected {block_num}, got {ack_block}")
offset += len(chunk)
block_num += 1
return True
except socket.timeout:
raise Exception("TFTP timeout")
finally:
sock.close()
class SRIOTFTPClient:
"""TFTP client that sends SRIO commands via filename encoding.
Includes simplified retry logic with exponential backoff.
Reference: fgpaprogrammer.cpp NUM_REPEAT_WRITE/NUM_REPEAT_READ.
"""
def __init__(self, server_ip: str, server_port: int = 69, max_retries: int = 3) -> None:
"""Initialize SRIO TFTP client.
Args:
server_ip: TFTP server IP address.
server_port: TFTP server port (default 69).
max_retries: Maximum retry attempts for TFTP operations (configurable).
"""
self.server_ip = server_ip
self.server_port = server_port
self.max_retries = max_retries
self.client = SimpleTFTPClient(server_ip, server_port)
def srio_write(
self,
slot: str,
address: int,
data: bytes,
log: Optional[Callable[[str], None]] = None,
description: str = "",
abort_check: Optional[Callable[[], bool]] = None,
) -> bool:
"""Write data to SRIO address via TFTP PUT with retry.
Args:
slot: SRIO endpoint (e.g., "0x10").
address: Target memory address.
data: Binary payload to write.
log: Optional logging callback.
description: Optional description of operation.
abort_check: Optional callback that returns True if operation should abort.
Returns:
True on success, False on failure after retries.
"""
length = len(data)
remote_file = f"$SRIO:{slot}/0x{address:X}+{length}"
for attempt in range(self.max_retries):
# Check for abort before each retry attempt
if abort_check and abort_check():
if log:
log(f"TFTP operation aborted: {remote_file}")
return False
try:
if log and attempt == 0:
desc_str = f"{description}" if description else ""
log(f"TFTP PUT {remote_file}{desc_str}")
elif log:
log(f"Retry {attempt}/{self.max_retries - 1}: {remote_file}")
# Use simple TFTP client instead of BytesIO
self.client.upload(remote_file, data)
return True
except Exception as e:
# Check abort even after errors - don't waste time retrying if user aborted
if abort_check and abort_check():
if log:
log(f"TFTP operation aborted: {remote_file}")
return False
if attempt < self.max_retries - 1:
# Exponential backoff: 0.1s, 0.2s, 0.4s
delay = 0.1 * (2 ** attempt)
if log:
log(f"Write error: {e}, retrying in {delay}s...")
time.sleep(delay)
else:
if log:
log(f"Write FAILED after {self.max_retries} attempts: {remote_file} - {e}")
return False
return False
def srio_read(
self,
slot: str,
address: int,
length: int,
log: Optional[Callable[[str], None]] = None,
description: str = "",
abort_check: Optional[Callable[[], bool]] = None,
) -> Optional[bytes]:
"""Read data from SRIO address via TFTP GET with retry.
Args:
slot: SRIO endpoint (e.g., "0x10").
address: Target memory address.
length: Number of bytes to read.
log: Optional logging callback.
description: Optional description of operation.
Returns:
Binary data on success, None on failure after retries.
"""
remote_file = f"$SRIO:{slot}/0x{address:X}+{length}"
for attempt in range(self.max_retries):
# Check for abort before each retry attempt
if abort_check and abort_check():
if log:
log(f"TFTP operation aborted: {remote_file}")
return None
try:
if log and attempt == 0:
desc_str = f"{description}" if description else ""
log(f"TFTP GET {remote_file}{desc_str}")
elif log:
log(f"Retry {attempt}/{self.max_retries - 1}: {remote_file}")
# Use simple TFTP client
data = self.client.download(remote_file)
return data
except Exception as e:
# Check abort even after errors - don't waste time retrying if user aborted
if abort_check and abort_check():
if log:
log(f"TFTP operation aborted: {remote_file}")
return None
if attempt < self.max_retries - 1:
# Exponential backoff: 0.1s, 0.2s, 0.4s
delay = 0.1 * (2 ** attempt)
if log:
log(f"Read error: {e}, retrying in {delay}s...")
time.sleep(delay)
else:
if log:
log(f"Read FAILED after {self.max_retries} attempts: {remote_file} - {e}")
return None
return None
def format_srio_filename(slot: str, address: int, length: int) -> str:
"""Format SRIO command as TFTP filename (Vecchia_app compatible).
Examples:
>>> format_srio_filename("0x10", 0x1000, 256)
'$SRIO:0x10/0x1000+256'
"""
return f"$SRIO:{slot}/0x{address:X}+{length}"