Wireless file transfer for retro handhelds.
retsend (retro + sending) is a Rust LocalSend
client for retro handhelds: send and receive files with your phone
or PC over Wi-Fi, no cable or SSH. Compatible with the official LocalSend apps.
It targets PortMaster-compatible Linux handhelds and the Miyoo Mini Plus and Flip running OnionOS or Allium, all of which are gamepad-only systems without a compositor. It also runs on regular desktop Linux and on Android handhelds and phones too.
| Receive | Request | Save | Transfer |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Send | History | Settings | Save routes |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Getting files onto a handheld usually means pulling the SD card or setting up SSH/SMB. Phones and desktops already have LocalSend — this is the missing end: a client built for a gamepad and screen.
- Discovery — a live radar of nearby devices (LocalSend protocol v2.1), with a manually typed IP address for the networks that block multicast.
- Receive — accept/decline dialog with countdown, speed/ETA, and cancel from either side; X picks a folder for that one transfer; quick-save mode auto-accepts.
- Send — gamepad file browser with multi-select and per-file progress; pin the folders and files you send often and they lead every listing.
- Save routes — received ROMs land in the console folder they belong to, detected from the card; per-extension overrides on top.
- Folders — a folder sent from the official app arrives as a folder, its tree rebuilt under the save folder.
- Encryption — the protocol's HTTPS mode, on by default; works with the official app's default settings both ways.
- Settings on device — alias, save folder, and port, applied live.
- Headless —
retsend --receiveruns with no screen, for SSH and scripting. - Small and simple — no async runtime, a minimal HTTP server, and the system's own SDL2 wherever one can drive the screen.
Grab retsend-portmaster.zip from
Releases and unpack it
into your ports folder (e.g. /roms/ports/).
Grab retsend-onionos.zip from
Releases and unzip it at the
root of the SD card, so the app lands in App/Retsend/. It shows up under Apps, and MENU quits it.
The zip carries an SDL2 built for the Miyoo's panel and a launcher that asks for
the software renderer, since the SSD202D has no GPU at all. What ships inside it,
in full: onionos/App/Retsend/lib/README.md.
Grab retsend-allium.zip from
Releases and unzip it at the root
of the SD card, so the app lands in Apps/Retsend.pak/. It shows up on the Apps
tab, and MENU quits it — pressed on its own, since Allium keeps MENU held with
a pad for brightness and volume.
Same device and same bundled SDL2 as the OnionOS package; what differs is the launcher, and that the app closes its own transfers on MENU rather than being killed from outside. What ships inside it: allium/README.md.
Grab retsend-spruceos.zip from
Releases and unzip it at the root
of the SD card, so the app lands in App/Retsend/. It shows up under Apps, and
MENU quits it.
Turn wifi on in spruce's settings first — it brings the radio up at boot and only when that setting is on, so an empty radar is otherwise hard to read. The same device and the same bundled SDL2 again, which matters more here: spruce ships a Miyoo SDL2 of its own whose drivers answer to other names. What ships inside it: spruce/README.md.
Grab retsend-android-arm64.apk from
Releases and sideload it. Same
app, driven by touch or by a pad, with the system Back button as B: tap a device
to send to it, a file to pick it, a row to open it, and the button hints along
the bottom are themselves the buttons.
Grant All files access when it asks and received files land in Download/,
where an emulator or file manager can reach them; deny it and the app is confined
to its own folder. See android/README.md.
System SDL2 is the only native dependency. On Debian/Ubuntu:
sudo apt-get install -y build-essential pkg-config libsdl2-dev
cargo runFiles passed as arguments (cargo run -- file1 file2) come pre-selected in the
send browser.
Two instances on one machine discover each other (multicast loopback) — handy for trying both sides of a transfer without a second device:
RETSEND_DATA_DIR=/tmp/ls-a cargo run &
RETSEND_DATA_DIR=/tmp/ls-b cargo runTests are headless (no SDL, no network setup needed):
cargo testWith the Android SDK and an NDK installed:
rustup target add aarch64-linux-android
cargo install cargo-ndk --locked
./android/scripts/build.sh release # android/app/build/outputs/apk/release/app-release.apkIt builds libSDL2.so on the first run, cross-compiles the Rust cdylib SDL loads,
and assembles the APK — see android/README.md for how the
port fits together.
Touch works too, where there is a touchscreen: tap a row to act on it, or tap a hint along the bottom to press the button it names.
| Pad | Keyboard | Action |
|---|---|---|
| D-pad / stick | Arrows | Navigate · left/right switch tabs |
| A | Enter | Send to device · select file · accept · type |
| B | Esc | Back · decline · cancel · leave the keyboard |
| X | X / Bksp | Add a device by IP · pick where an incoming transfer lands · erase a character · take every file in the folder |
| Y | Y | Pin / unpin the row under the cursor |
| Start | F1 | Confirm send · OK (keyboard) |
| Select | Tab / F5 | Refresh radar · switch roots · layer (keyboard) |
| L1 / R1 | PgUp / PgDn | Switch tabs · page the file browser |
config.toml lives in the data dir (created with defaults on first run). The
Settings screen edits everything in it except:
[network] https = false— fall back to the protocol's plain-http mode[network] announce_interval_secs— multicast announce cadence[transfer] browser_roots— extra mount points for the file browser[transfer] history_limit— max transfers kept in the History tab (default 200)[transfer] pinned_paths,last_send_dir— written by Y and by sending
Environment variables override paths and control logging at launch:
RETSEND_DATA_DIR, RETSEND_CONFIG, RETSEND_SAVE_DIR, RETSEND_BROWSER_ROOTS
(:-separated), RETSEND_ALIAS, RETSEND_SCALE, RETSEND_GLES=0|1,
RETSEND_SOFTWARE=1, RETSEND_BLIT=1, RETSEND_KEYMAP=miyoo|desktop,
RETSEND_LOG_LEVEL, RETSEND_LOG_FILE, RETSEND_PANIC_FILE.
RETSEND_BLIT=1 rasterizes the UI offscreen and presents it as a single texture
copy per frame, for drivers that show nothing else — the Miyoo Mini's panel driver
drops geometry and window-surface updates without reporting an error.
Received files land in save_dir, except ROMs: those are routed by extension
into the console folders already present in save_dir — .gba into gba,
.sfc into snes. Nothing is created, so a missing folder gets no route, and ambiguous
extensions (.zip, .iso, .bin, .md) never take part. Off via
Settings → Auto save routes or [transfer] auto_routes = false.
Per-extension overrides win over the detected routes — Settings → Save routes on the device, or:
[transfer.routes]
gbc = "gb" # relative → <save_dir>/gb
gba = "/roms/gba" # absolute → used as-is
png = "/roms/screenshots"Extensions match case-insensitively, folders are created on demand, and anything
unrouted lands in save_dir.
Protocol v2 carries a folder transfer as ordinary files whose names hold the
relative path, so a folder sent from the official app is rebuilt under
save_dir: Zelda/saves/Zelda.sav arrives at <save_dir>/Zelda/saves/Zelda.sav.
Files inside a folder skip the save routes — a folder arrives whole rather than
split across console folders — while loose files route as usual. Sender-supplied
paths are sanitized component by component and can never leave save_dir.
Switch it off with Settings → Received folders or [transfer] keep_folders = false, and every file lands flat, placed by the routes as before.








