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
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
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
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
Write-with-response queuing.
Two simultaneous writes-with-response = silently dropped data. The fix is in one obscure WWDC video.
- 5
The
didUpdateValueForcallback 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
didUpdateValueForas 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
- 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 PersonalTeam
- 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 TeamEnterprise
- 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 EnterpriseAdd-ons
You already have your own BLE wrapper but you don't want to write the chunked OTA + CRC handshake from scratch. Drop this in.
A Zoom call where you bring your stuck BLE problem. Code review, architecture call, whatever moves you forward.
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.