#!/usr/bin/env python3 """ Receive a file from the ESP32 SD card over serial. The device must be running the sdcard send command; this script sends the command and captures the hex-encoded output, then decodes and saves to a file. Usage: python3 tools/sdcard_recv.py -p /dev/ttyUSB0 -f myfile.txt [-o output.bin] python3 tools/sdcard_recv.py --port /dev/ttyUSB0 --remote myfile.txt Requires: pyserial (pip install pyserial) """ import argparse import re import sys import time try: import serial except ImportError: print("Error: pyserial required. Run: pip install pyserial", file=sys.stderr) sys.exit(1) def main(): ap = argparse.ArgumentParser(description="Receive file from ESP32 SD card over serial") ap.add_argument("-p", "--port", required=True, help="Serial port (e.g. /dev/ttyUSB0)") ap.add_argument("-b", "--baud", type=int, default=115200, help="Baud rate (default 115200)") ap.add_argument("-f", "--remote", "--file", dest="remote", required=True, help="Path of file on the SD card (e.g. myfile.txt or log/data.bin)") ap.add_argument("-o", "--output", help="Local output path (default: basename of remote file)") ap.add_argument("-t", "--timeout", type=float, default=60.0, help="Timeout in seconds for transfer (default 60)") args = ap.parse_args() out_path = args.output if not out_path: out_path = args.remote.split("/")[-1].split("\\")[-1] or "received.bin" ser = serial.Serial(args.port, args.baud, timeout=1.0) # Drain any pending input ser.reset_input_buffer() # Send: sdcard send \r\n cmd = f"sdcard send {args.remote}\r\n" ser.write(cmd.encode("ascii")) ser.flush() # Wait for ---SDFILE--- marker_start = b"---SDFILE---" marker_end = b"---END SDFILE---" line_buf = b"" state = "wait_start" remote_name = None size_val = None hex_buf = [] deadline = time.time() + args.timeout while True: if time.time() > deadline: print("Timeout waiting for transfer", file=sys.stderr) sys.exit(1) c = ser.read(1) if not c: continue line_buf += c if c != b"\n" and c != b"\r": if len(line_buf) > 2048: line_buf = line_buf[-1024:] continue line = line_buf.decode("ascii", errors="ignore").strip() line_buf = b"" if state == "wait_start": if marker_start.decode() in line or line == "---SDFILE---": state = "read_meta" continue if state == "read_meta": if line.startswith("SIZE:"): try: size_val = int(line.split(":", 1)[1].strip()) except ValueError: size_val = 0 state = "wait_hex" elif line and not line.startswith("---"): remote_name = line continue if state == "wait_hex": if "---HEX---" in line: state = "read_hex" continue if state == "read_hex": if marker_end.decode() in line or line == "---END SDFILE---": break # Strip non-hex and decode hex_part = re.sub(r"[^0-9a-fA-F]", "", line) if hex_part: hex_buf.append(hex_part) continue # Decode hex and write raw = bytes.fromhex("".join(hex_buf)) if size_val is not None and len(raw) != size_val: print(f"Warning: size mismatch (expected {size_val}, got {len(raw)})", file=sys.stderr) with open(out_path, "wb") as f: f.write(raw) print(f"Saved {len(raw)} bytes to {out_path}") ser.close() if __name__ == "__main__": main()