‹ All Air Kalon docs

Air Kalon

Integration and building

The Rust core, its 42-function C interface, the AirKalonKit Swift package, and how to build and test everything from source.

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 paper

Rust core

The crate airkalon-core, version 3.0.0, builds as a static library and an rlib. Its modules:

ModuleContents
mode2The 24-byte mode 2 frame: packing, validation, magic sniffing
raptorq_engineRaptorQ encoder and decoder over that framing, with derived parameters and the 2^24 symbol identifier bound
ackAcknowledgement frame
kcfKCF1 file container: packing, compression policy, bounded inflation, SHA-256 verification
fnvFNV-1a, the frame header's payload hash
qrQR module matrices through qrcodegen, with standard mask selection
ffiThe 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.

GroupFunctions
QR1
Mode 2 encoder5
Mode 2 decoder13
KCF1 container19
Acknowledgement2
Hash and version2

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

ValueNameMeaning
0AK_ADD_NOT_A_FRAMENot a valid mode 2 frame for this decoder. Ignore it
1AK_ADD_ACCEPTEDNew frame absorbed
2AK_ADD_DUPLICATEAlready seen for this session
3AK_ADD_ALREADY_DONEStream complete, frame ignored
4AK_ADD_NEW_SESSIONDecoder locked on to a new session and absorbed the frame
5AK_ADD_UNSUPPORTED_SUITEWell-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);
  • kind is an AkKcfKind: 0 file, 1 snippet, 2 bundle.
  • ak_kcf_pack sanitises 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_unpack parses, 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 an AkKcfError from 1 to 17 in validation order.
  • ak_kcf_probe is a cheap check for the KCF1 magic.
  • Every handle has a matching _free function.

QR, acknowledgement and helpers

  • ak_qr_encode and ak_encoder2_qr_frame produce a module matrix, one byte per module with no quiet zone, at an exact or automatic version. ak_encoder2_qr_frame evaluates all eight masks on an encoder's first call and reuses the chosen mask afterwards.
  • ak_ack_pack and ak_ack_parse handle the 19-byte acknowledgement. The only valid mode byte is AK_ACK_MODE, 2.
  • ak_fnv1a and ak_abi_version round 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

TypePurpose
FountainEncoder2, FountainDecoder2The mode 2 RaptorQ encoder and decoder
HardenedFountainDecoderThe quorum and restart policy over the decoder
packFile(name:mime:kind:bytes:), PackedFileBuild a KCF1 container
unpackFile(container:), ReceivedFile, KCFErrorUnpack and verify one; the result is a file or the reason it was refused
PayloadKind, FileCompressionThe container's kind and compression flags. Compression labels itself deflate
AckFrame, packAck, parseAck, AckMatcherThe acknowledgement channel
sniffFrameMagic, mode2AlignedSymbolLen, fnv1aHelpers

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

TypePurpose
SenderEnginePacking, fitting, encoder construction, pacing and QR rendering. Publishes image, isRunning, framesSent, actualFps, outgoingStream, payloadNote
ReceiverEngineCamera, prefilter, Vision, optics lock, hardened decoding, verification. Publishes phase, stats, progress, receivedFile, errorMessage
AckWatcherThe sender's camera for auto-stop
SendView, ReceiveViewThe shared screens, with closures for picking and saving
SnippetComposerThe 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-ios and aarch64-apple-ios-sim only.
  • Rust with rustup. The build script adds the three targets if they are missing.
  • Xcode, for xcodebuild -create-xcframework and 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 Xcode

build-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 Vision

The repository holds 89 Rust tests and 162 Swift tests: 153 in the kit and 9 in the gauntlet.

SuiteTests
Rust unit tests: frame, acknowledgement, hash, QR and RaptorQ engine26
Rust golden vectors, including the drift check10
Rust KCF1 container: 33 unit tests and 4 fuzz-style property tests37
Rust C interface, including a file carried end to end through frames16
Swift kit153
Swift gauntlet9

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_mode2

Benchmarks

  • cargo run --release -p airkalon-core --example rqbench, run in core, 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 visionbench in kit measures Vision decode latency on the machine running it.
  • swift run gauntlet in kit runs the seeded degradation cells.