#!/usr/bin/env python3 """Copy a UF2 artifact to a detected RP2040 mass-storage mount.""" from __future__ import annotations import argparse import os import sys import time from pathlib import Path import shutil INFO_FILE = "INFO_UF2.TXT" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source", type=Path, required=True, help="Path to the UF2 file to copy") parser.add_argument("--timeout", type=float, default=10.0, help="Seconds to wait for the mount") parser.add_argument( "--mount", type=str, default=os.environ.get("MOUNT", ""), help="Explicit mount point (default: auto-detect)", ) return parser.parse_args() def candidate_paths(explicit: str, user: str) -> list[Path]: paths: list[Path] = [] if explicit: paths.append(Path(explicit)) roots = [ Path("/Volumes"), Path("/media"), Path(f"/media/{user}"), Path("/run/media"), Path(f"/run/media/{user}"), ] for root in roots: if not root.exists() or not root.is_dir(): continue # Many systems mount the UF2 volume directly as a child of the root directory. try: for child in root.iterdir(): if child.is_dir(): paths.append(child) except PermissionError: # Skip directories we can't read continue return paths def choose_mount(explicit: str, user: str) -> Path | None: candidates = candidate_paths(explicit, user) if explicit: # For an explicit mount we only care whether it exists. path = Path(explicit) return path if path.exists() and path.is_dir() else None # Only accept candidates containing INFO_UF2.TXT (real RP2040 bootloader mounts) info_candidates = [path for path in candidates if (path / INFO_FILE).exists()] if info_candidates: return info_candidates[0] # Don't fall back to random directories - only accept real RP2040 mounts return None def main() -> int: args = parse_args() source = args.source if not source.exists(): print(f"UF2 source file not found: {source}", file=sys.stderr) return 1 explicit_mount = args.mount.strip() user = os.environ.get("USER", "") deadline = time.time() + float(args.timeout) while time.time() <= deadline: mount = choose_mount(explicit_mount, user) if mount is not None: if not mount.exists() or not mount.is_dir(): time.sleep(1) continue destination = mount / source.name try: shutil.copy2(source, destination) try: if hasattr(os, "sync"): os.sync() except Exception: pass # Give the device a moment to process the copied file before we exit. time.sleep(0.5) except Exception as exc: # noqa: BLE001 print(f"Failed to copy UF2 to {destination}: {exc}", file=sys.stderr) return 1 print(f"Copied {source} to {destination}") return 0 time.sleep(1) if explicit_mount: print( f"Mount point '{explicit_mount}' not found within {args.timeout} seconds", file=sys.stderr, ) else: print( "Unable to detect RP2040 UF2 mount. Make sure device is in bootloader mode:", file=sys.stderr, ) print("1. Press reset/boot button on rp2040 Zero board", file=sys.stderr) print("2. Press upper left corner key when connecting USB", file=sys.stderr) print("3. Press Fn+Fn+Shift+Shift+Ctrl chord", file=sys.stderr) print("Then try again or pass explicit mount via mount=/run/media/$USER/RPI-RP2", file=sys.stderr) return 1 if __name__ == "__main__": # pragma: no cover sys.exit(main())