Roles
A transfer has two roles and no negotiation between them. The sender decides everything about the stream: the frame size, the error correction level and the session. The receiver learns all of it from the first frame it reads.
Sender
States
| State | Meaning |
|---|---|
| Empty | No payload loaded |
| Packing | The payload is being packed into a KCF1 container off the main thread |
| Ready | A payload is loaded and the stream is stopped |
| Preparing | Start was pressed and the encoder is being built |
| Streaming | Frames are on air |
A file the container refuses, one over 256 MiB, never reaches Ready: the sender clears it and shows why, and Start repeats the same sentence.
Starting a stream
- Choose a fresh random session identifier from 1 to 65,535.
- Fit the symbol length to the container under the frame size ceiling, rounding it up to a multiple of 8 when it is 64 or more, without changing K.
- Check that the header plus the fitted symbol fits a version 40 QR code at the chosen error correction level. If not, refuse to start with a message.
- Build the encoder on a background task. For a large payload this is real work, so the interface stays responsive. If K would exceed 56,403, the encoder refuses and the sender asks you to raise the frame size.
- Publish the stream's identity (mode, session, K, payload hash) for the acknowledgement watcher.
- Emit frames from the display link at the chosen rate, with at most one catch-up frame after any stall.
What the sender emits
- Symbol identifiers count up from 0. Identifiers 0 to K − 1 are the source blocks; from K on, every frame is a new repair symbol. The sender never emits an identifier of 2^24 or more.
- Grid. Two frames per display tick, side by side, from the same session.
- QR geometry. The first frame chooses the QR version and the mask; every later frame of the stream reuses both.
A stream never ends on its own. It stops when the user presses Stop, when auto-stop sees a matching done acknowledgement, or when a setting changes. A setting change restarts the stream with a new session identifier.
Receiver
States
| State | Meaning |
|---|---|
| Idle | Camera off |
| Denied | Camera permission refused |
| Searching | Camera running, no stream locked |
| Receiving | A stream is locked and frames are being absorbed |
| Done | The container unpacked and its SHA-256 verified |
| Failed | The stream completed but the payload was refused |
Transitions
Idle --Start Receiving----------------------> Searching
Idle --camera refused-----------------------> Denied
Searching --first valid frame--------------------> Receiving
Receiving --three frames of a new identity-------> Receiving (new stream)
Receiving --decoder complete, verified-----------> Done
Receiving --decoder complete, payload refused----> Failed
Done --Receive Another----------------------> Searching
Failed --Try Again----------------------------> Searching
any --Stop---------------------------------> IdleHandling each frame
For every decoded code:
- Sniff. Read the magic. Mode 2 frames go to the decoder. Acknowledgement frames and anything else are dropped.
- Parse. Apply the validation rules. A frame that fails is ignored with no change of state.
- Match the identity.
- If no stream is locked, lock on to this one.
- If the frame belongs to the live stream, feed it to the decoder.
- If it carries a different identity, buffer it. When three distinct frames of the same new identity arrive within two seconds, switch to the new stream and replay the buffered frames into it. Candidates are kept per identity, up to eight at once, so a second code in view cannot starve a real new sender.
- If it carries the live session number but different parameters, treat it the same way: a quorum-gated restart. More than three restarts in sixty seconds latches a conflict error instead of looping.
- Absorb. The decoder reports the frame as new, a duplicate, or arriving after completion. Duplicates are counted but cost nothing.
Decoder outcomes
| Result | Meaning |
|---|---|
| Not a frame | Rejected by the parser or the identity rules. No state change |
| Accepted | A new symbol was absorbed |
| Duplicate | This symbol identifier was already seen |
| Already done | The stream is complete; the frame is ignored |
| New session | The decoder locked on to a new stream and absorbed the frame |
| Unsupported suite | A well-formed frame with a reserved cipher suite. No state change |
Completion and verification
- Check the FNV-1a hash of the assembled bytes against
payloadFnv. A mismatch fails the transfer as damaged in transit. - Unpack the bytes as a KCF1 container, applying every rule in order: header, flags, lengths, text encoding, and, for a compressed body, bounded inflation to exactly the declared length.
- Check the SHA-256 of the recovered file against the header.
- If every step passes, offer the file with its name, type and kind. Otherwise fail the transfer, show the reason and offer nothing.
The same rules apply at every size. There is no raw fallback and no size above which only the cheaper hash is checked.
Progress
Progress is distinct frames received divided by K, because RaptorQ completes at K. It is capped at 99 percent until the file is complete.
Acknowledgement loop
The acknowledgement channel is strictly fail-open: if it never works, the transfer is unaffected.
Receiver
- While decoding, show a small acknowledgement code in a corner of the preview, away from the centre where the sender's code sits.
- Regenerate it only when
framesNewordonechanges, at most four times a second. A change todoneis shown at once. - On a verified completion, show the done acknowledgement full size with Delivered and hold it until the user taps Done. A failed transfer shows no done acknowledgement.
Sender
- With auto-stop on, run the camera only while a stream is on air.
- Honour an acknowledgement only if its mode, session and
identityFnvall equal the live stream's. Anything else is counted and ignored. - Ignore a
framesNewlower than one already seen, since captures can arrive out of order. - Stop at most once per stream on a matching
done = 1. - Permission denied or no camera means the watcher never runs, and the sender streams until it is stopped by hand.
Coexistence and compatibility
- The forward parser rejects acknowledgement frames, and the acknowledgement parser rejects forward frames.
- Any other QR code in the camera's view sniffs as unknown and changes nothing.
- Earlier builds of Air Kalon used a different frame format and container. They do not interoperate with the current build in either direction: a current receiver ignores their frames or refuses their payload, and both devices need the current build.
- Any future change to the mode 2 or acknowledgement layout is a new version nibble with new golden vectors, and the current receiver ignores a frame with an unknown version.