Reverse-engineering my glucose meter's Bluetooth protocol (and building an app around it)
I built badblood, a small iOS/macOS app that connects to my Contour glucose meter over Bluetooth, pulls down every stored reading, and shows them in a clean list with configurable low/high thresholds — no cloud account, no proprietary companion app required.
Here's how it came together.
The starting point: it's just BLE
Most modern glucose meters, including the Contour, speak the standard Bluetooth SIG Glucose Profile. That means the protocol isn't secret — it's a documented GATT service:
1808— Glucose Service2A18— Glucose Measurement (the actual reading)2A34— Glucose Measurement Context (meal tags: fasting, postprandial, bedtime, etc.)2A52— Record Access Control Point (RACP) — used to request "send me everything you've got"
So instead of sniffing traffic or reverse-engineering a binary protocol, the job was really "implement the spec correctly," which turned out to have its own sharp edges.
Prototyping in Python first
Before touching Swift, I wrote a quick prototype with bleak (contour_ble.py) to scan for the meter, subscribe to notifications, and request all stored records via RACP. This was the fast way to validate the decoding logic against a real device without fighting Xcode and CoreBluetooth's more ceremonial API.
The trickiest part was decoding the IEEE-11073 16-bit SFLOAT format the glucose value is packed into — a 12-bit signed mantissa and 4-bit signed exponent, value = mantissa * 10^exponent:
def decode_sfloat(raw: bytes) -> float:
v = int.from_bytes(raw, byteorder="little", signed=False)
mantissa = v & 0x0FFF
if mantissa >= 0x0800:
mantissa -= 0x1000
exponent = (v >> 12) & 0x0F
if exponent >= 0x08:
exponent -= 0x10
return mantissa * (10 ** exponent)
A few other things that weren't obvious from a skim of the spec:
- The "units" bit in the measurement flags tells you whether the value is in mol/L or kg/L — it's easy to conflate with the "type/sample location" bit, which is a different flag that only says whether type/sample data is present at all.
- Glucose measurements and their meal context arrive as separate notifications that share a sequence number, and context doesn't always arrive — you have to buffer pending records and flush them if no context shows up.
- RACP's "success" response comes back in more than one byte layout depending on the device — I saw both
06 01 01(per spec) and a vendor quirk06 00 01 01. Handling only the spec-correct form silently drops the completion signal.
Once the Python script could reliably log correctly-decoded readings with meal tags, I had a known-good reference implementation to port.
Building the real app
The production app is SwiftUI + CoreBluetooth, with the same decoding logic reimplemented in Swift (ContourBLEManager.swift). A few decisions shaped it beyond "just port the Python":
Auto-reconnect, not manual pairing. The manager remembers the last-connected peripheral's UUID and retries on a timer, so opening the app just works without re-scanning every time. It also supports CoreBluetooth's state restoration so a background BLE event can wake the app.
Skip re-downloading everything. The meter can hold a lot of history, and re-fetching all records on every connect is wasteful. Before requesting the full record set, the app first asks for just the record count and compares it to the last-seen count — it only does a full RACP "report all" pull when something's actually changed.
Local persistence with SwiftData, keyed on (seq, timestamp) so re-syncs upsert rather than duplicate.
Threshold-based color coding — user-configurable low/high mmol/L bounds drive a simple traffic-light UI (orange/green/red) per reading, plus a "Meter Connected" local notification so you know a sync happened without keeping the app open.
What's next
Possible follow-ups: trend charts (time-in-range over days/weeks), CSV export, and Apple Health integration so readings show up alongside other health data. The core protocol work is done — everything left is UI and glue.
If you want to try this against your own meter: any device that implements the standard 0x1808 Glucose Service should work with the same decoding logic, since none of it is Contour-specific — it's just the Bluetooth SIG spec.