How it works

The QR codes are the wire.

There is no network in this picture. A terminal draws frames, a camera reads them, and nothing travels between the two machines except light.

01 — The path

File in, file out

Five stages on the sending side, four on the receiving side, and an air gap in the middle that no packet crosses.

Sender

01

Read

The file is read whole, or piped in on stdin.

02

Chunk

Split to fit one QR code, sized from the terminal height.

03

Encode

Plain text, base64 for binary, gzip, or fountain symbols.

04

Frame

Each chunk gets a pipe-separated header and becomes a QR code.

05

Render

A ratatui slideshow you can scrub, pause and gap-fill.

Receiver

01

Scan

Continuous detection — no shutter, no trigger.

02

Parse

The header identifies the transfer and the frame type.

03

Assemble

A worker isolate peels fountain symbols and writes chunks to disk.

04

Verify

SHA-256 against the CHECKSUM frame, then save.

the whole protocol
Sending machine                      Receiving device
  (Rust binary)                        (Flutter app)

  read file
  chunk it
  encode
  build QR frames
  render slideshow ──── light ────►   camera
                                       decode QR
                                       parse frame
                                       assemble
                                       verify → save

02 — The constraint

A link with no back-channel

The receiver cannot ask for a frame again. Everything interesting about the design follows from that one fact.

What the sender knows

Nothing. It does not know whether anyone is scanning, how much has been caught, or whether the receiver exists. It draws frames until you stop it.

What the receiver can ask for

Nothing. A frame lost to glare, blur or a bad angle is simply gone. There is no retransmit request, because there is no channel to send one on.

Every other design decision follows from this. It is why fountain coding is in the box, why the checksum travels in-band, and why the receiver writes chunks to disk as it goes instead of holding them until the end.

03 — Two encodings

Sequential, or fountain

Sequential frames are file slices in order. Fountain frames are XORs of a random subset, so any sufficient pile of them rebuilds the file.

Sequential

Frame n is byte range n of the file. Simple, and enough for a few hundred chunks. Miss one and you wait for the sender to loop back to exactly that index — or drive it there yourself with gap-fill mode.

Fountain

Each frame is the XOR of a pseudo-random subset of the file's blocks, chosen from the frame's sequence number by a PRNG both sides run independently. Collect enough and peeling recovers everything. No specific frame matters.

fountain frame
F|seq|K|fileSize|id|payload

// seq       which blocks this symbol XORs together
// K         how many source blocks the file has
// fileSize  so the receiver can trim the zero padding
// id        two chars, derived from the file's digest

The catch is that "enough" is more than K. Peeling needs between 1.33× and 1.89× the number of source blocks, so a progress bar scaled to K sits at 99% with a third of the scanning still to do.

04 — Getting it back

Scan, or serve, or join

The camera is the normal path. When you already have the bytes on disk, two subcommands finish the job without one.

Scan

The normal path. Point the app at the terminal; it assembles, verifies and saves, resuming from disk if it is killed partway.

serve

An HTTP receiver, for when a network turns out to exist after all. Takes raw uploads, multipart, and QR-scan JSON alike.

join

Reassembles the .partaa files a receiver wrote, checking them against the recorded digest.

Copyright © 2026