115 lines
3.7 KiB
Python
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()
|