English · 简体中文
One-way optical file transfer for air-gapped machines. The sender renders a fountain-coded stream of QR codes on screen; any phone camera can record it, and a single zero-dependency executable rebuilds the file — byte-for-byte identical. Or skip the recording entirely: the Android app scans the screen live.
No network. No USB. No Bluetooth. Nothing leaves the room except light.
flowchart TB
subgraph TX["Sender · the air-gapped machine"]
direction LR
A["file<br/>compress<br/>gzip / brotli"] --> B["SMC1<br/>container"] --> C["RaptorQ coding<br/>RFC 6330"] --> D["SMQ frames<br/>self-describing"] --> E["QR codes<br/>full-screen<br/>or dual-lane"]
end
E ==>|one-way, lossy, no ACK| F
subgraph RX["Receiver"]
direction LR
F["phone camera<br/>recording"] --> G["sendmecongo-recv"] --> H["RaptorQ<br/>recovery"] --> I["four layers<br/>of CRC"] --> J["file on disk<br/>verified identical"]
end
Built for getting documents, keys, configuration, logs, and small evidence packages out of a physically isolated environment — in the direction the screen is already allowed to face. See the protocol spec (SMQ1 v0.1) for the container and frame formats.
Three closest open projects were evaluated: deedy/qr-data-transfer (no
LICENSE file at all), its MIT-tagged fork qrferry (inherits the unlicensed
code), and decimen-optical-transfer (conflicting MIT/AGPL claims, LT codes).
Protocol ideas were borrowed; not a single line of their code is used —
this codebase is clean-room and all-Rust.
| Binary | Runs on | What it is |
|---|---|---|
sendmecongo-send |
the isolated machine | GUI + fullscreen player, one exe, two modes |
sendmecongo-recv |
the receiver's machine | standalone recovery tool, zero dependencies |
sendmecongo-bench |
dev box | capacity calibration, frame export, erasure simulation |
sendmecongo-recv needs nothing installed — no Python, no FFmpeg, no codec
pack, no internet. The macOS build is a 2.6 MB single-architecture (arm64)
.app whose only runtime dependencies are the system libraries
(otool -L: CoreFoundation, Cocoa, Metal, libc++, libSystem). It decodes the
HEVC or H.264 recording itself, recognizes the QR stream, and rebuilds the
file; it stops as soon as the fountain code has enough symbols, so you never
need to watch the whole recording.
Both GUIs and the CLI speak Simplified Chinese (default), Traditional
Chinese, and English — switchable live from the menu bar (macOS) or in-window
menu (Windows/Linux), or with --lang / SENDMECONGO_LANG.
The two GUIs draw with OpenGL and need OpenGL 2.0 or newer. The
sendmecongo-recv CLI mode has no such requirement.
- Windows: 10 (1607+) or Server 2016+. On older Windows (7 / 8.1 / Server 2012 R2 and earlier) the exe fails at load time with “The procedure entry point SetProcessDpiAwarenessContext could not be located” — the file is not broken, the OS is simply below the floor; there is no patch for this.
- GPU-less remote sessions (RDP, Citrix): the session's built-in OpenGL is
1.1 (GDI Generic) and the GUIs refuse to start with
egui_glow requires opengl 2.0+. Copy Mesa's software renderer (opengl32.dll+libgallium_wgl.dllfrom mesa-dist-win) into the exe's folder — no admin rights, nothing system-wide. It works, but CPU rendering is slower; shrink the sender window if the symbol rate drops. - macOS: 11+ (Apple Silicon only).
- Linux: X11 or Wayland with OpenGL 2.0+.
- Android: 10+ (API 29), arm64 — see the next section.
Sender (the isolated machine):
cargo build --release -p sendmecongo-send
./target/release/sendmecongo-send # GUI: drag file in, pick a preset, press playReceiver (any other machine, e.g. where you copied the recording):
cargo build --release -p sendmecongo-recv
./target/release/sendmecongo-recv recording.mov --compare original-fileor on macOS just double-click the .app from the DMG and drag the recording
into the window. Progress, decoded frames, and symbol counts are visible the
whole time; the progress denominator is known from the first frame header, not
estimated.
Recommended recording setup (measured, not guessed): turbo60 preset,
--size 1600, 4K60 recording, AE/AF locked, phone on a stand. At the
resulting 100 KB/s: 1 MB ≈ 10 s, 10 MB ≈ 1.7 min, 64 MB ≈ 10 min. Files above
64 MB are rejected outright — the optical link and preparation memory both
make that a bad idea.
The same protocol received live — the app reads the QR stream straight off the camera preview, so there is no recording step and nothing to copy off the phone afterwards by hand.
- Grab
sendmecongo-recv-android-x.y.z.apkfrom the releases page (Android 10+, arm64-v8a) and install it. - On the sender pick an
mp…preset — the phone presets use bigger modules and a longer per-code hold, tuned for a phone camera instead of a 4K recording. - Press play, hold the phone up to the screen (landscape for the
x2dual-lane presets, so both codes fit in frame). Progress and stats show on screen; the finished file can be shared to any other app.
Measured on real hardware (incompressible random data, all runs verified byte-identical):
| Preset | QR | Lanes | sym/s | Nominal | Measured goodput |
|---|---|---|---|---|---|
| mp40 | V40-L | 1 | 10 | 29.3 KB/s | 29 KB/s |
| mp40-30 | V40-L | 1 | 30 | 87.9 KB/s | 80+ KB/s |
| mp40-30x2 | V40-L | 2 | 60 | 175.9 KB/s | 130 KB/s stable |
Under the hood it is the same pure-Rust sendmecongo-core receiver compiled
to an arm64 JNI .so, driven by a one-screen Kotlin shell (CameraX). The
manual exposure, 60fps target, 1440×1080 analysis resolution, and the Y-plane
row-stride contract all live in that shell; the decoder itself is the rxing
fork with encoding_rs enabled (byte-mode QR needs it).
Fountain coding means losses only cost time, never correctness: RaptorQ needs
K' symbols (slightly above K, fixed by RFC 6330 parameters) and the stream
loops until the camera has them. Overhead measured at 1.00x — collect K
symbols, get the file back, IDENTICAL, four CRC layers verified end to end.
| Preset | QR | Lanes | sym/s | Nominal | Effective (w/ 20% repair) |
|---|---|---|---|---|---|
| robust | V15-L | 1 | 10 | 4.9 KB/s | ~4 KB/s |
| balanced | V20-L | 1 | 15 | 12.2 KB/s | ~10 KB/s |
| turbo15 | V30-L | 1 | 15 | 25.0 KB/s | ~21 KB/s |
| turbo30 | V30-L | 1 | 30 | 50.1 KB/s | ~42 KB/s |
| turbo60 | V30-L | 2 | 60 | 100.2 KB/s | ~84 KB/s |
| megabit | V40-L | 2 | 60 | 171.7 KB/s | ~143 KB/s |
Measured on real hardware (incompressible random data, iPhone, AE/AF locked,
zero blackout; all runs IDENTICAL ✓):
| Preset | --size |
Recording | goodput | % of nominal |
|---|---|---|---|---|
| turbo30 | 1200 | 4K30 | 49.9 KB/s | 99% |
| turbo60 | 1600 | 4K60 | 100.2 KB/s | 100% |
| megabit | 1400 | 4K30 | 105.2 KB/s | 61% |
Notes from the measurement campaign:
- turbo30 and turbo60 are saturated — at their theoretical limit, because the nominal "effective" figure assumes a full loop including repair symbols.
- turbo60 is the recommended default: same throughput as megabit with larger modules (137 vs 177 per side), i.e. much more robust.
- Dual-lane presets need high-framerate recording. Camera fps must be ≥ 2×
the per-lane update rate, or a phase beat between camera and lane switching
periodically kills one lane (observed: 74.6 KB/s and
codes/frameoscillating 1.0↔2.0 at 4K30; back to 100.2 KB/s and a steady 2.0 at 4K60). - Erasure simulation (256 KB, turbo30, K=154): 10–40% frame drops finish with overhead 1.00–1.55x and always intact.
recording.mov/mp4
├─ in-house ISO-BMFF demuxer hvc1 / hev1 / avc1, moov at head or tail
├─ pure-Rust decode HEVC → rusty_h265 H.264 → openh264 (vendored, no cmake)
├─ QR recognition rxing (pure-Rust ZXing port), QR only
├─ RaptorQ recovery the shared sendmecongo-core Receiver, protocol untouched
└─ stops when complete the whole recording is never needed
Recordings are split at CRA keyframes (~0.9 s apart on phone HEVC) into independent GOPs decoded in parallel. A GOP boundary is open (RASL frames after it are dropped) — which is exactly what the fountain code's repair symbols exist to absorb.
The GUI shows candidate recordings found next to the app and in the usual places (home, Downloads, Desktop, Movies, mounted volumes), sorted newest first; drag-and-drop into the window works too.
cargo build --release # everything- Playback must be release builds — debug rendering can't keep up and halves the symbol rate (measured).
- The toolchain is pinned in
rust-toolchain.toml(1.98.1;raptorq 2.0.1needs rustc ≥ 1.89 for x86 targets). sendmecongo-recvdefaults to including H.264 via vendoredopenh264C++ (no cmake needed; NASM failure is soft — no SIMD, still works). Machines without a C++ compiler can build--no-default-featuresfor HEVC-only, which is what phones actually record.- Windows builds produce static-CRT single-file exes (see
.cargo/config.toml). For the Windows steps, the two icon paths and what to check before shipping, see docs/BUILD-WINDOWS.md.
Packaging (macOS): ./tools/build-macos.sh builds the .apps and DMGs.
The DMG includes a first-open note and a one-liner quarantine-clearing
.command, because the app is ad-hoc signed and Gatekeeper will challenge it
after download (right-click → Open on older macOS; Settings → Privacy &
Security → Open Anyway on 15+; no challenge when copied from USB).
- docs/PROTOCOL.md — SMC1 container and SMQ frame formats
- docs/TESTING.md — how to test the optical link on your own screen + phone, and how to read the metrics
- docs/BUILD-WINDOWS.md — Windows build guide: toolchains, the two independent icon paths, acceptance checklist
- docs/AGENT.md — the machine contract (CLI flags, exit codes, JSON schema) for driving the binaries from an agent
- macOS builds are Apple Silicon only (intentional — cross-compiling openh264 for x86_64 isn't worth the complexity).
- Night Shift / blue-light filters shift the colors and break decoding; turn them off before playing.
rxing's multi-code detection occasionally reports only one of two side-by-side codes per frame; the receiver compensates with overlapping band rescans, but marginal recordings suffer first.rusty_h265supports Main / Main 10 / Main Still Picture, 4:2:0, ≤ 10-bit — exactly what phones record; anything outside is rejected loudly, never mis-decoded.
- H.264 path end-to-end verification —
recordings/already held two real Androidavc1captures (4K60, both lanes, megabit); both now decode every sample, report zero decode errors, and recover completely. A 1.5 KB synthetic H.264 clip keeps the branch honest incargo test. Re-encoding the HEVC corpus is not a substitute — those B frames defeat openh264. - Real-Windows-machine run of the packaged
.exe - megabit re-measured at
--size 1600+ 4K60 — blocked on the renderer, not on the camera. The play loop re-encodes each symbol's QR on every frame, which caps megabit at 38 sym/s at any window size, and the 105.2 KB/s on record is already 96% of what that renderer can deliver: faster filming cannot move the number. Measurements and a rejected fix are in docs/TESTING.md §7.1. - M2 (v0.5.0): resumable transfer — receiver-side progress manifests merged across recordings, per-block symbol grid, and a type-in resume code so the sender replays only the deficit — plus an agent skill (SKILL.md + CLI/JSON contract) so an AI agent can drive send/receive end to end. See docs/M2-DESIGN.md
- Android real-time receiver (v0.5.2): live camera scanning, phone presets, 130 KB/s measured on the dual-lane mp40-30x2 preset.
- M3: AES-256-GCM encryption envelope + audit log, multi-file batches, automated acceptance matrix
Dual-licensed under MIT or Apache-2.0, at your option — the Rust ecosystem default.
The name is a nod to "send me to the Congo": if you're stuck inside an air gap, this is the boat out.