Implementing Fitness Tracker Data Transfer via BLE in Mobile Applications
Most fitness trackers on the market are closed devices with proprietary GATT profiles. Xiaomi Mi Band, Huawei Band, Fitbit — each has its own "magic" over standard Bluetooth LE. If the tracker is custom (developed for your product on Nordic nRF52840 or Dialog DA14531), situation is simpler: you have GATT documentation. Below — work with both scenarios.
Standard GATT Profiles: What Trackers Actually Support
Bluetooth SIG defined profiles for wearables: Heart Rate Profile (HRP), Cycling Speed and Cadence (CSC), Running Speed and Cadence (RSC). Certified trackers implement these predictably.
Heart Rate Measurement characteristic (UUID 0x2A37):
// Heart Rate Measurement packet format
fun parseHeartRate(data: ByteArray): HeartRateMeasurement {
val flags = data[0].toInt()
val hrFormat16bit = (flags and 0x01) != 0
val energyExpended = (flags and 0x08) != 0
val rrIntervalPresent = (flags and 0x10) != 0
var offset = 1
val bpm = if (hrFormat16bit) {
val value = ((data[offset + 1].toInt() and 0xFF) shl 8) or (data[offset].toInt() and 0xFF)
offset += 2
value
} else {
data[offset++].toInt() and 0xFF
}
val rrIntervals = mutableListOf<Double>()
if (rrIntervalPresent) {
while (offset + 1 < data.size) {
val raw = ((data[offset + 1].toInt() and 0xFF) shl 8) or (data[offset].toInt() and 0xFF)
rrIntervals.add(raw / 1024.0 * 1000.0) // in milliseconds
offset += 2
}
}
return HeartRateMeasurement(bpm, rrIntervals)
}
RR intervals — time between heartbeats — are needed for HRV (Heart Rate Variability). Many apps ignore them and lose valuable data for stress analysis.
Scanning and Filtering
Don't scan "everything" — only needed devices. On Android:
fun startScan(onDevice: (BluetoothDevice) -> Unit) {
val filters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(HEART_RATE_SERVICE_UUID))
.build(),
)
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
scanner.startScan(filters, settings, object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
onDevice(result.device)
}
})
// Forcibly stop after 10 seconds — endless scan kills battery
Handler(Looper.getMainLooper()).postDelayed({ scanner.stopScan(this) }, 10_000)
}
On iOS background scanning requires bluetooth-central background mode and serviceUUIDs filter — without filter CoreBluetooth doesn't deliver events in background.
Working with Proprietary Tracker Without Documentation
If tracker is commercial and no docs — reverse-engineer via nRF Connect and Wireshark (Bluetooth HCI snoop log on Android, PacketLogger on Mac for iOS). Enable Settings → Developer Options → Enable Bluetooth HCI snoop log, reproduce sync via official app, analyze log in Wireshark.
Usually find: service UUID, init command sequence, data format (often without docs — "guess" from values: first 4 bytes — Unix timestamp, next 2 — steps, etc.).
Reliable Reconnection
Trackers disconnect. Phone backgrounds. BluetoothGatt references stale. Reconnect strategy:
private fun scheduleReconnect(device: BluetoothDevice) {
reconnectJob?.cancel()
reconnectJob = scope.launch {
var attempt = 0
while (isActive) {
delay(minOf(1000L * (1 shl attempt), 30_000L)) // exponential backoff to 30 sec
val result = connect(device)
if (result.isSuccess) break
attempt++
}
}
}
Exponential backoff with 30-second ceiling — balance between recovery speed and BLE stack load.
Implementing fitness tracker data sync via BLE (steps, heart rate, sleep, HRV): 3–5 weeks. Reverse-engineering proprietary protocol adds 1–2 weeks. Cost calculated individually.







