iOS 16+ · Swift 6 strict concurrency · BLE 5 LE

CoreBluetooth that
actually works in production.

Async/await scan, connect, read, write, notify. Chunked OTA firmware update with CRC32. Exponential-backoff reconnects. State restoration done right. A MockPeripheral you can unit-test against. One Swift Package; UIKit and SwiftUI both work.

Six years of shipping BLE for hardware companies, burned into a kit you can drop in this afternoon.

The 5 bugs that break every BLE project

CoreBluetooth has been Apple's worst-documented framework for a decade. Same five bugs in every hardware-company iOS codebase I've worked in. Each one quietly burns 1–3 weeks of engineering time before someone figures it out.

  1. 1

    State restoration is silently misconfigured.

    Forget the CBCentralManagerOptionRestoreIdentifierKey, and your background scanning quietly dies after the first suspend cycle. Users report "the app doesn't reconnect" and you can't repro it on your dev iPhone.

  2. 2

    Background scans drop without a service-UUID filter.

    It's in the docs if you know where to look. Most devs don't, and ship a feature that works in foreground only.

  3. 3

    OTA firmware update has no first-party example.

    Every hardware company reimplements chunked-transfer-with-CRC from scratch. Most get the MTU math wrong.

  4. 4

    Write-with-response queuing.

    Two simultaneous writes-with-response = silently dropped data. The fix is in one obscure WWDC video.

  5. 5

    The didUpdateValueFor callback fires for both reads and notifications.

    Get the routing wrong and you'll spend a week chasing phantom "extra" reads.

Antenna fixes all five. The fixes have shipped to App Store apps and stayed there. 111 unit tests so you can verify before you trust.

What's inside

Nine modules. One import:

import Antenna
Module What it does Why you'd use it
BLEManager Async/await wrapper around CBCentralManager with private serial queue and Swift 6 strict-concurrency compliance Stop fighting the delegate pattern. for await peripheral in stream instead of 20 lines of state tracking.
ConnectedPeripheral Handle exposing read / write / notify against any characteristic Same shape as a real BLE handle, swap in MockPeripheral for tests
WriteQueue MTU-aware chunked writes with progress reporting Send firmware blobs without manually splitting at the right byte count
OTAUpdater Chunked firmware-over-BLE transfer with IEEE-802.3 CRC32 Apple ships zero examples. Every hardware company reimplements this, usually wrong. This module is the one your last hardware lead wishes they had.
ConnectionStateMachine Auto-retry with exponential backoff + jitter Reconnects survive elevators, lock screens, and the customer who walks two rooms away
BackgroundModeHandler State restoration + launch-options detection Background BLE that keeps working after app suspend
RSSIMonitor Rolling-window smoothing + proximity zone classification Unlock-on-approach, fitness proximity, indoor positioning
GATTExplorer One-shot discovery + decoders for standard BLE values Skip the spec lookup for Battery Level, Heart Rate, Device Info
MockPeripheral In-memory fake conforming to the same Peripheral protocol as the real handle Unit-test your BLE feature code without hardware

The whole API in five snippets

Copy-pasteable from the actual kit. Compiles today.

Scan + connect

let manager = BLEManager(restoreIdentifier: "tech.acme.central")
try await manager.waitForReady()

let stream = await manager.startScanning(serviceUUIDs: [CBUUID(string: "180D")])
for await peripheral in stream {
    print(peripheral.name, peripheral.rssi)
}

let connected = try await manager.connect(to: peripheral)

Read / write / notify

let services = try await connected.discoverServices()
let chars = try await connected.discoverCharacteristics(for: services[0].id)

let heartRate = try await connected.read(chars[0])
try await connected.write(Data([0x01]), to: chars[1], withResponse: false)

let notifications = try await connected.notifications(for: chars[2])
for await data in notifications {
    print("BPM:", GATTExplorer.decodeHeartRate(data) ?? 0)
}

Firmware OTA with progress

let updater = OTAUpdater(
    peripheral: connected,
    writeCharacteristic: otaChar,
    mtu: 182
)

let crc = try await updater.transfer(firmwareData) { progress in
    print("OTA \(Int(progress.fraction * 100))% — chunk \(progress.chunkIndex)/\(progress.totalChunks)")
}
// Hand crc to the peripheral's verify endpoint

Auto-retry with backoff

let sm = ConnectionStateMachine(policy: .default)
// 5 attempts, 1s → 2s → 4s → 8s → 16s, ±20% jitter
let connected = try await sm.connect(to: peripheral, via: manager)

Test without hardware

let mock = MockPeripheral(name: "Test HR Monitor")
await mock.addCharacteristic(
    CBUUID(string: "2A37"), to: CBUUID(string: "180D"),
    properties: [.read, .notify],
    initialValue: Data([0x00, 72])
)

let bpm = try await yourHeartRateReader(from: mock)
XCTAssertEqual(bpm, 72)

The same function signature takes a real ConnectedPeripheral in production and a MockPeripheral in tests. Plain protocol polymorphism, so you skip Sourcery, Mockingbird, and the rest of the mocking-framework tax.

"Just ask AI to write it"

Already tried. So has every other engineer at every hardware company. Cursor, Claude, Copilot all output CoreBluetooth code that:

  • Compiles cleanly
  • Runs in the simulator (where BLE doesn't actually work)
  • Skips state restoration entirely
  • Botches background-mode scan filters
  • Treats didUpdateValueFor as if it's only for reads
  • Forgets NSBluetoothAlwaysUsageDescription (app crashes on first scan)
  • Implements OTA as "send the bytes", with no MTU chunking, CRC, or resume

Not AI's fault. The public CoreBluetooth corpus is Apple's toy-grade samples plus a decade of broken Stack Overflow threads. Garbage in, confident garbage out.

Antenna is the part that doesn't exist on the public internet: decisions you only learn by shipping this stuff to the App Store. Hand the kit to your AI tool and it'll happily extend it for you. That's the right way to use both.

Pricing

Pay once. Keep the source forever. 12 months of free updates with every tier. Gumroad handles VAT, GST, and US sales tax, and you get a proper invoice at checkout.

Personal

$99 one-time
  • 1 developer
  • Unlimited personal projects
  • Email support, 30-day priority response
  • 12 months of free updates

Best for: Solo devs shipping their own hardware companion app

Buy Personal
most popular

Team

$299 one-time
  • Up to 5 developers
  • Unlimited commercial apps
  • Email support, 90-day priority response
  • 12 months of free updates

Best for: A 2–5 person iOS team at a hardware startup

Buy Team

Enterprise

$999 one-time
  • Unlimited developers
  • White-label permitted (ship Antenna source inside your closed-source app)
  • 1-hour onboarding call
  • 12 months of priority updates + bug fixes

Best for: Funded hardware companies that ship at scale and need an invoice

Buy Enterprise

Add-ons

OTA module only
$79

You already have your own BLE wrapper but you don't want to write the chunked OTA + CRC handshake from scratch. Drop this in.

1-hour consult
$250

A Zoom call where you bring your stuck BLE problem. Code review, architecture call, whatever moves you forward.

Custom integration sprint
from $2,500

I sit with your codebase, get Antenna integrated, and stay on the bugs until your specific peripheral works in production.

30-day money-back guarantee. If Antenna doesn't compile cleanly into your project, email me and I'll refund + try to help.

Who made this

I'm Shailendra Kumar Ram. Six years on production iOS. Three of those years were inside hardware companies whose app reviews lived or died on whether BLE worked:

  • Barsys - A smart cocktail machine driven over a custom GATT service. Disconnects mid-pour were unacceptable.
  • Tuya - The IoT platform behind a long tail of smart bulbs, plugs, and climate gear. Every quirk of cheap BLE silicon eventually walked through that codebase.
  • TTLock - Smart locks. Proximity unlock that had to work every time, and OTA firmware updates rolling out to millions of locks in the field.

Every fix in Antenna is a fix I've shipped in production at one of those three. Nothing here is theoretical. This is the kit I wish someone had handed me on day one of each of those jobs.

Things people ask before buying

If yours isn't here: hello@antennakit.dev. I read every email.

Does it work in a UIKit app?

Yes. The public API is plain Swift: async/await, AsyncStream, structs, actors. No SwiftUI imports anywhere. Drop it into a UIViewController and you're done.

Does it support Android?

iOS only for v1. An Android companion is on the roadmap, probably via a partner. If you need Android today, the OTA spec and state-machine logic are documented well enough to port. For the BLE wrapping itself you'll want a different tool.

Why pay $99 if Bluejay / RxBluetoothKit exist?

Bluejay (Steamclock) is the closest OSS comparison, and it's been effectively unmaintained for years. It never got an OTA module, a MockPeripheral, or Swift 6 concurrency support. RxBluetoothKit is going down with RxSwift. Antenna is actively maintained, has a real test suite, and ships OTA out of the box. Different category.

Why pay $99 when I could ask Claude to write it?

Already tried that; it's literally the AI-era section above. The kit is the production-tested decisions AI doesn't have data for. If your dev time is $50–200/hour, you'll spend more debugging the AI's output than you would buying the kit.

What's the refund policy?

30 days, no questions asked. If it doesn't compile cleanly into your project, email me and I'll refund + try to help debug. I'd rather sell to people who actually use it.

What's NOT in v1?

Android (deferred). iOS background auto-connect (the AutoConnect module) lands in v1.1 within ~2 weeks of launch. BLE Mesh (different protocol stack, separate kit if ever). React Native / Flutter bridges welcome as community contributions.

Will updates be free?

12 months of updates included with every tier. After year 1, $49/year keeps you on the latest. Skip the renewal and your existing copy keeps working forever, just without new modules.

Do you offer consulting?

Yes. One-hour calls at $250 for code review or a deep BLE debugging session. Longer engagements at $2,500–$6,000 per sprint when you want me to integrate Antenna into your codebase and ship it with you.

Tax + invoicing?

Gumroad is the Merchant of Record, so they handle EU VAT, US sales tax, GST, etc. and issue a tax-compliant invoice to your company at checkout. Indian buyers: GST is handled automatically. If you need a custom VAT-registration line on the invoice, email and I will sort it out.