Repository layout
core/airkalon-core/ Rust crate: RaptorQ frames, KCF1 container, QR, C ABI
include/airkalon.h The C contract between Rust and Swift
tests/ Golden vectors, container tests, FFI tests
kit/ Swift package AirKalonKit, shared by both apps
Frameworks/ AirKalonCore.xcframework (built by scripts/build-core.sh)
Sources/AirKalonKit/ Engines, receiver policy, shared views
Sources/AirKalonGauntlet Seeded synthetic degradation harness
Benchmarks/ visionbench and recorded device results
apps/macos/ SwiftPM app shell
apps/ios/ XcodeGen project for iPhone and iPad
docs/ MODE2-SPEC.md, KCF1-SPEC.md, benchmarks, the paperRust core
The crate airkalon-core, version 3.0.0, builds as a static library and an rlib. Its modules:
| Module | Contents |
|---|---|
mode2 | The 24-byte mode 2 frame: packing, validation, magic sniffing |
raptorq_engine | RaptorQ encoder and decoder over that framing, with derived parameters and the 2^24 symbol identifier bound |
ack | Acknowledgement frame |
kcf | KCF1 file container: packing, compression policy, bounded inflation, SHA-256 verification |
fnv | FNV-1a, the frame header's payload hash |
qr | QR module matrices through qrcodegen, with standard mask selection |
ffi | The C interface |
Dependencies: qrcodegen 1.8, sha2 0.10, flate2 1 and raptorq 2.0.1, pinned because the mode 2 goldens fix its packet bytes and parameter serialisation. The release profile uses panic = "abort".
C interface
include/airkalon.h declares 42 functions under ABI version 2 (AK_ABI_VERSION). The Swift package checks at first use that the linked library reports the same version. Handles are opaque and none are thread-safe: use each from one queue at a time.
| Group | Functions |
|---|---|
| QR | 1 |
| Mode 2 encoder | 5 |
| Mode 2 decoder | 13 |
| KCF1 container | 19 |
| Acknowledgement | 2 |
| Hash and version | 2 |
Mode 2
AkEncoder2 *ak_encoder2_new(const uint8_t *payload, uint32_t payload_len,
uint16_t symbol_len, uint16_t session_id);
uint32_t ak_encoder2_frame(const AkEncoder2 *enc, uint32_t esi,
uint8_t *out, uint32_t out_cap);
AkDecoder2 *ak_decoder2_new(void);
int32_t ak_decoder2_add(AkDecoder2 *dec, const uint8_t *bytes, uint32_t len);
uint32_t ak_decoder2_assemble(const AkDecoder2 *dec, uint8_t *out, uint32_t out_cap);
int32_t ak_decoder2_hash_ok(const AkDecoder2 *dec);symbol_len must be 8 to 2,928, and a multiple of 8 when 64 or more; session_id must be non-zero. The payload may be 1 byte to 256 MiB, provided K lands between 1 and 56,403; otherwise ak_encoder2_new returns NULL. A decoder configures itself from the first valid frame, and a frame with a different session resets it. ak_decoder2_hash_ok is the FNV-1a transport check only; the file is verified by ak_kcf_unpack.
Results from add
| Value | Name | Meaning |
|---|---|---|
| 0 | AK_ADD_NOT_A_FRAME | Not a valid mode 2 frame for this decoder. Ignore it |
| 1 | AK_ADD_ACCEPTED | New frame absorbed |
| 2 | AK_ADD_DUPLICATE | Already seen for this session |
| 3 | AK_ADD_ALREADY_DONE | Stream complete, frame ignored |
| 4 | AK_ADD_NEW_SESSION | Decoder locked on to a new session and absorbed the frame |
| 5 | AK_ADD_UNSUPPORTED_SUITE | Well-formed frame with a reserved cipher suite |
KCF1 container
AkKcfPacked *ak_kcf_pack(const uint8_t *name, uint32_t name_len,
const uint8_t *mime, uint32_t mime_len,
int32_t kind,
const uint8_t *bytes, uint32_t bytes_len,
int32_t *err);
AkKcfFile *ak_kcf_unpack(const uint8_t *container, uint32_t len,
int32_t *err);kindis anAkKcfKind: 0 file, 1 snippet, 2 bundle.ak_kcf_packsanitises the name and media type, hashes, applies the compression rule and returns a handle with getters for the container bytes, its flags, the original length and the body length. Its only refusal is a file over 256 MiB,AK_KCF_ERR_FILE_TOO_LARGE(20).ak_kcf_unpackparses, inflates and verifies the SHA-256. A handle it returns holds exactly the bytes the sender packed; getters return the re-sanitised name, the media type, the file bytes, the digest, the flags and the body length. On failure it returns NULL and writes anAkKcfErrorfrom 1 to 17 in validation order.ak_kcf_probeis a cheap check for theKCF1magic.- Every handle has a matching
_freefunction.
QR, acknowledgement and helpers
ak_qr_encodeandak_encoder2_qr_frameproduce a module matrix, one byte per module with no quiet zone, at an exact or automatic version.ak_encoder2_qr_frameevaluates all eight masks on an encoder's first call and reuses the chosen mask afterwards.ak_ack_packandak_ack_parsehandle the 19-byte acknowledgement. The only valid mode byte isAK_ACK_MODE, 2.ak_fnv1aandak_abi_versionround out the interface.
Swift package
AirKalonKit targets macOS 14 and iOS 17 and links the core through AirKalonCore.xcframework, whose C module is CAirKalon.
Low-level types
| Type | Purpose |
|---|---|
FountainEncoder2, FountainDecoder2 | The mode 2 RaptorQ encoder and decoder |
HardenedFountainDecoder | The quorum and restart policy over the decoder |
packFile(name:mime:kind:bytes:), PackedFile | Build a KCF1 container |
unpackFile(container:), ReceivedFile, KCFError | Unpack and verify one; the result is a file or the reason it was refused |
PayloadKind, FileCompression | The container's kind and compression flags. Compression labels itself deflate |
AckFrame, packAck, parseAck, AckMatcher | The acknowledgement channel |
sniffFrameMagic, mode2AlignedSymbolLen, fnv1a | Helpers |
Decoders take either exact frame bytes with add(_:) or a Vision payload with add(visionPayload:), which also tries unwrapping the byte-mode segment from raw QR codewords.
import AirKalonKit
let packed = try packFile(name: "photo.png", mime: "image/png", bytes: fileBytes)
let encoder = try FountainEncoder2(payload: packed.container, symbolLen: 1440, sessionId: 0x2A17)
let frame = encoder.packedFrame(esi: 0)
let decoder = FountainDecoder2()
switch decoder.add(frame) {
case .newSession, .accepted:
if let bytes = decoder.assemble(), decoder.hashOK == true,
case .success(let file) = unpackFile(container: bytes) {
// file.name, file.mime, file.bytes: SHA-256 verified
}
default:
break
}Engines and views
| Type | Purpose |
|---|---|
SenderEngine | Packing, fitting, encoder construction, pacing and QR rendering. Publishes image, isRunning, framesSent, actualFps, outgoingStream, payloadNote |
ReceiverEngine | Camera, prefilter, Vision, optics lock, hardened decoding, verification. Publishes phase, stats, progress, receivedFile, errorMessage |
AckWatcher | The sender's camera for auto-stop |
SendView, ReceiveView | The shared screens, with closures for picking and saving |
SnippetComposer | The text composer |
To embed Air Kalon in another app, own a SenderEngine and a ReceiverEngine, present SendView(engine:choosePayload:composeText:brandMark:) and ReceiveView(engine:save:share:), and supply your own file picking and saving. Pass kind: .snippet or kind: .bundle to SenderEngine.setPayload for text and multi-file sends, and gate picks against kcfMaxFileBytes (256 MiB) before reading them. Your app needs a camera usage description.
Building from source
Requirements
- A Mac with Apple silicon. The core is built for
aarch64-apple-darwin,aarch64-apple-iosandaarch64-apple-ios-simonly. - Rust with
rustup. The build script adds the three targets if they are missing. - Xcode, for
xcodebuild -create-xcframeworkand the iOS build. - XcodeGen, for the iOS project.
Steps
scripts/build-core.sh # Rust static libraries for three slices, then the xcframework
scripts/build-macos.sh # builds, assembles and signs build/Air Kalon.app
cd apps/ios && xcodegen # generates AirKalon.xcodeproj; open it in Xcodebuild-macos.sh reads the app version from the repository's VERSION file and signs with a local identity named "Kalon Local Dev" when one exists, otherwise ad hoc. With ad hoc signing, macOS may ask for camera permission again after every rebuild.
For an iPhone or iPad, set your development team in Xcode. The simulator builds without one but has no camera, so it can only send. The simulator starts the demo stream automatically.
XcodeGen rewrites apps/ios/Info.plist from project.yml on every run. Change version numbers in project.yml, never in the plist.
Both devices in a transfer need a build of the current core. Earlier builds used a different frame format and container and do not interoperate with it.
Tests
cd core && cargo test # wire goldens, KCF1 container tests, FFI tests
cd kit && swift test # includes an end-to-end loopback through VisionThe repository holds 89 Rust tests and 162 Swift tests: 153 in the kit and 9 in the gauntlet.
| Suite | Tests |
|---|---|
| Rust unit tests: frame, acknowledgement, hash, QR and RaptorQ engine | 26 |
| Rust golden vectors, including the drift check | 10 |
| Rust KCF1 container: 33 unit tests and 4 fuzz-style property tests | 37 |
| Rust C interface, including a file carried end to end through frames | 16 |
| Swift kit | 153 |
| Swift gauntlet | 9 |
The loopback test encodes a payload, renders every frame to an image, decodes the images with Vision, feeds the RaptorQ decoder and checks the result byte for byte: the whole pipeline except the physical light.
Mode 2 goldens are regenerated in memory on every run and compared with the committed file. Writing them to disk takes an explicit step:
AK_REGEN_GOLDEN_MODE2=1 cargo test -p airkalon-core --test gen_golden_mode2Benchmarks
cargo run --release -p airkalon-core --example rqbench, run incore, measures RaptorQ frames-to-complete and decode cost at the version 27 and 40 symbol sizes, KCF1 pack and unpack throughput, and QR encoding cost with all eight masks against a pinned mask.swift run visionbenchinkitmeasures Vision decode latency on the machine running it.swift run gauntletinkitruns the seeded degradation cells.