#!/usr/bin/env python3 """ Reliable file transfer over serial console. Bypasses screen to avoid escaping issues. """ import serial import base64 import hashlib import time import sys import re def send_command(ser, cmd, wait=0.3): """Send a command and wait for output.""" ser.write(f"{cmd}\n".encode()) time.sleep(wait) response = ser.read(ser.in_waiting or 1).decode('utf-8', errors='replace') return response def wait_for_prompt(ser, timeout=5): """Wait for shell prompt.""" start = time.time() buffer = "" while time.time() - start < timeout: if ser.in_waiting: buffer += ser.read(ser.in_waiting).decode('utf-8', errors='replace') if buffer.rstrip().endswith('$') or buffer.rstrip().endswith('#'): return buffer time.sleep(0.1) return buffer def transfer_file(serial_port, local_file, remote_path, baud=115200): """Transfer a file reliably over serial.""" # Read local file with open(local_file, 'rb') as f: content = f.read() # Compute checksums local_md5 = hashlib.md5(content).hexdigest() b64_content = base64.b64encode(content).decode() b64_md5 = hashlib.md5(b64_content.encode()).hexdigest() print(f"File size: {len(content)} bytes") print(f"Base64 size: {len(b64_content)} bytes") print(f"File MD5: {local_md5}") print(f"Base64 MD5: {b64_md5}") # Open serial port print(f"\nOpening {serial_port}...") ser = serial.Serial(serial_port, baud, timeout=1) time.sleep(0.5) # Send initial enter to get prompt ser.write(b'\n') time.sleep(0.5) ser.read(ser.in_waiting) # Clear buffer # Clear target file print("Initializing remote file...") send_command(ser, f"> /tmp/recv.b64", wait=0.5) # Use very small chunks to avoid corruption chunk_size = 60 chunks = [b64_content[i:i+chunk_size] for i in range(0, len(b64_content), chunk_size)] print(f"Sending {len(chunks)} chunks...") errors = 0 for i, chunk in enumerate(chunks): # Use printf for better escaping than echo cmd = f"printf '%s' '{chunk}' >> /tmp/recv.b64" ser.write(f"{cmd}\n".encode()) time.sleep(0.15) # Small delay between chunks # Read any response if ser.in_waiting: ser.read(ser.in_waiting) if (i + 1) % 100 == 0: print(f" Sent {i+1}/{len(chunks)} chunks ({100*(i+1)//len(chunks)}%)") # Give Pi time to process time.sleep(0.5) print("All chunks sent. Waiting for completion...") time.sleep(2) # Verify base64 file checksum print("\nVerifying base64 checksum...") ser.read(ser.in_waiting) # Clear buffer send_command(ser, f"md5sum /tmp/recv.b64", wait=1) response = wait_for_prompt(ser, timeout=5) print(f"Response: {response}") if b64_md5 in response: print("✓ Base64 checksum matches!") else: print("✗ Base64 checksum mismatch!") # Extract the md5 from response match = re.search(r'([a-f0-9]{32})', response) if match: print(f" Expected: {b64_md5}") print(f" Got: {match.group(1)}") return False # Decode to target location print(f"\nDecoding to {remote_path}...") send_command(ser, f"base64 -d /tmp/recv.b64 > {remote_path}", wait=1) time.sleep(1) # Verify final file print("Verifying final file...") ser.read(ser.in_waiting) send_command(ser, f"md5sum {remote_path}", wait=1) response = wait_for_prompt(ser, timeout=5) print(f"Response: {response}") if local_md5 in response: print("✓ File transfer successful!") return True else: print("✗ Final file checksum mismatch!") return False if __name__ == "__main__": if len(sys.argv) < 3: print("Usage: serial_transfer.py [serial_port]") print("Example: serial_transfer.py wifi_manager.py /opt/dangerous-pi/app/backend/managers/wifi_manager.py") sys.exit(1) local_file = sys.argv[1] remote_path = sys.argv[2] serial_port = sys.argv[3] if len(sys.argv) > 3 else "/dev/ttyUSB1" success = transfer_file(serial_port, local_file, remote_path) sys.exit(0 if success else 1)