Documentation Index
Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Synheart Wear supports direct Bluetooth LE connections to any standard heart rate monitor. This enables real-time HR streaming without requiring a cloud API or vendor-specific SDK.
Supported devices include:
- WHOOP (Broadcast Heart Rate mode)
- Polar sensors (H10 chest strap, OH1 / Verity Sense optical arm bands)
- Wahoo TICKR, TICKR X
- Garmin HRM-Pro, HRM-Dual
- Gym equipment with BLE HR broadcast
- Any device implementing the standard BLE Heart Rate Profile (0x180D)
How It Works
The BLE HRM provider connects to devices advertising the standard Bluetooth Heart Rate Service (0x180D), subscribes to the Heart Rate Measurement characteristic (0x2A37), and parses incoming data per the Bluetooth SIG specification.
Phone (iOS/Android)
│
│── BLE Scan (filter: 0x180D service)
│
▼
BLE Heart Rate Monitor
│
│── Subscribe to HR Measurement (0x2A37)
│
▼
Real-time HR samples → HeartRateSample stream
│
│── bpm, rr_intervals_ms, device_id, timestamp
│
▼
Your App
iOS
Add to Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to heart rate monitors.</string>
Android
Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
Note: Android 12+ requires runtime permission requests for BLUETOOTH_SCAN and BLUETOOTH_CONNECT.
Usage
Flutter/Dart
Swift
Kotlin
import 'package:synheart_wear/synheart_wear.dart';
final bleHrm = BleHrmProvider();
// Scan for nearby HR monitors
final devices = await bleHrm.scan(
timeoutMs: 10000,
namePrefix: 'WHOOP', // optional filter
);
// Connect to a device
await bleHrm.connect(
deviceId: devices.first.deviceId,
sessionId: 'my-session', // optional
);
// Listen to heart rate samples
bleHrm.onHeartRate.listen((sample) {
print('BPM: ${sample.bpm}');
if (sample.rrIntervalsMs != null) {
print('RR: ${sample.rrIntervalsMs}');
}
});
// Disconnect when done
await bleHrm.disconnect();
import SynheartWear
let config = SynheartWearConfig(enabledAdapters: [.bleHrm])
let synheartWear = SynheartWear(config: config)
guard let bleHrm = synheartWear.bleHrm else { return }
// Scan
let devices = try await bleHrm.scan(timeoutMs: 10000, namePrefix: "WHOOP")
// Connect
try await bleHrm.connect(deviceId: devices.first.deviceId)
// Stream
for await sample in bleHrm.onHeartRate {
print("BPM: \(sample.bpm)")
}
// Disconnect
try await bleHrm.disconnect()
import ai.synheart.wear.SynheartWear
import ai.synheart.wear.models.DeviceAdapter
val synheartWear = SynheartWear(
context = this,
config = SynheartWearConfig(
enabledAdapters = setOf(DeviceAdapter.BLE_HRM)
)
)
val bleHrm = synheartWear.bleHrm ?: return
// Scan
val devices = bleHrm.scan(timeoutMs = 10000, namePrefix = "WHOOP")
// Connect
bleHrm.connect(deviceId = devices.first().deviceId)
// Collect
bleHrm.heartRateFlow.collect { sample ->
Log.d(TAG, "BPM: ${sample.bpm}")
}
// Disconnect
bleHrm.disconnect()
API Reference
Methods
| Method | Description | Returns |
|---|
scan(timeoutMs, namePrefix?) | Scan for nearby BLE HR monitors | List<BleHrmDevice> |
connect(deviceId, sessionId?, enableBattery?) | Connect to a device and start HR streaming | void |
disconnect() | Disconnect from the current device | void |
isConnected() | Check if a device is currently connected | bool |
BleHrmDevice
Returned from scan():
| Field | Type | Description |
|---|
deviceId | String | BLE device UUID |
name | String? | Device advertised name |
rssi | int | Signal strength (dBm) |
HeartRateSample
Emitted on the heart rate stream:
| Field | Type | Description |
|---|
tsMs | int | Phone receipt timestamp (ms since epoch) |
bpm | int | Heart rate in beats per minute |
source | String | Always "ble_hrm" |
deviceId | String | BLE device UUID |
deviceName | String? | Device advertised name |
sessionId | String? | Session tag (passed during connect) |
rrIntervalsMs | List<double>? | RR intervals in milliseconds |
Note: rrIntervalsMs availability depends on the device. Polar chest straps typically include RR intervals; WHOOP Broadcast typically does not.
Error Codes
| Code | Meaning |
|---|
PERMISSION_DENIED | Bluetooth permission not granted |
BLUETOOTH_OFF | Bluetooth adapter is disabled |
DEVICE_NOT_FOUND | Device not found or connection timed out |
SUBSCRIBE_FAILED | Failed to subscribe to HR characteristic |
DISCONNECTED | Device disconnected unexpectedly |
WHOOP Broadcast Setup
To use WHOOP as a BLE heart rate monitor:
- Open the WHOOP app on your phone
- Go to Device Settings (tap your WHOOP device)
- Enable Broadcast Heart Rate
- Your WHOOP will now appear in BLE scans as a standard HR monitor
Note: WHOOP Broadcast HR provides BPM only (no RR intervals). For full WHOOP data (recovery, sleep, strain), use the WHOOP cloud provider instead.
Reconnection
The BLE HRM provider automatically handles disconnections:
- 3 retry attempts with exponential backoff (1s, 2s, 4s)
- After retries are exhausted, a
DISCONNECTED error is emitted on the stream
- Your app can then prompt the user to reconnect
Limitations
- Single device: Only one BLE HRM device can be connected at a time (v1)
- Phone-side only: Requires the phone to be in BLE range of the HR monitor
- No HRV calculation: Raw RR intervals are provided when available, but HRV calculation is not performed by the provider
- Battery dependent: Battery level monitoring is optional (
enableBattery: true during connect)
Architecture
The BLE HRM provider is implemented natively on each platform:
| Platform | Implementation | Streaming Pattern |
|---|
| iOS | CoreBluetooth (CBCentralManager) | AsyncStream<HeartRateSample> |
| Android | Android BLE (BluetoothLeScanner, BluetoothGatt) | SharedFlow<HeartRateSample> |
| Flutter | Platform Channels (MethodChannel + EventChannel) | Stream<HeartRateSample> |
All platforms parse HR data identically per the Bluetooth SIG Heart Rate Profile specification (flags byte, uint8/uint16 BPM, optional RR intervals at 1/1024s resolution).