← Back to blog
Blog
2026-07-28

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 Service
  • 2A18 — 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 quirk 06 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.