ESP32/tools/sdcard_recv.py

115 lines
3.7 KiB
Python

#!/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 <path>\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()