Heart Rate Monitoring Implementation via Mobile Application
Heart rate measurable three ways via phone: built-in camera (PPG via flash), Bluetooth GATT with external sensor (chest strap, smartwatch), or reading from platform storage (HealthKit, Health Connect — data from Apple Watch / Wear OS). Approach choice determines accuracy, latency and UX.
PPG via Camera: How it Works and Why Difficult
User places finger on camera with flash. Capillaries pulse — changes reflected red light. Analyze green channel (more hemoglobin sensitive) of each frame.
// iOS: AVCaptureVideoDataOutputSampleBufferDelegate
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
guard let buffer = CVPixelBufferGetBaseAddress(pixelBuffer) else { return }
// Average green channel over center area
var greenSum: Int64 = 0
let startX = width / 3
let startY = height / 3
for y in startY..<(height * 2 / 3) {
for x in startX..<(width * 2 / 3) {
let pixel = buffer.advanced(by: y * bytesPerRow + x * 4)
greenSum += Int64(pixel.load(fromByteOffset: 1, as: UInt8.self))
}
}
let avgGreen = Double(greenSum) / Double((width / 3) * (height / 3))
processPPGSample(avgGreen)
}
Then — FFT or Peak Detection on time series. At 30 fps, HR range 40–200 bpm → frequency 0.67–3.33 Hz. FFT on 5-second window (150 samples) gives sufficient resolution.
PPG problems:
- Finger movement = artifacts 10× larger than pulse signal
- Ambient light brightness affects baseline (needs normalization)
- Accuracy ±5–10 bpm under ideal conditions, ±20+ on movement
- iOS limits flash brightness — weak signal on some models
PPG suitable for "measure at rest, hold finger 30 seconds". For real-time monitoring during movement — need external sensor.
Bluetooth GATT: Heart Rate Profile
Standard BLE profile for pulse sensors (Polar H10, Wahoo TICKR, Garmin HRM):
- Service UUID:
0x180D(Heart Rate) - Characteristic UUID:
0x2A37(Heart Rate Measurement)
Data arrives via Notification. First byte — flags: bit 0 determines format (UINT8 or UINT16), bit 4 — RR-intervals presence.
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
val flag = value[0].toInt()
val isUint16 = flag and 0x01 != 0
val heartRate = if (isUint16) {
((value[2].toInt() and 0xFF) shl 8) or (value[1].toInt() and 0xFF)
} else {
value[1].toInt() and 0xFF
}
// RR-intervals (if present) — for HRV
if (flag and 0x10 != 0) {
var offset = if (flag and 0x08 != 0) 4 else 3 // skip energy if present
while (offset + 1 < value.size) {
val rrRaw = ((value[offset + 1].toInt() and 0xFF) shl 8) or (value[offset].toInt() and 0xFF)
val rrMs = rrRaw * 1000 / 1024 // convert from 1/1024 s to milliseconds
rrIntervals.add(rrMs)
offset += 2
}
}
}
RR-intervals — basis for HRV (Heart Rate Variability) calculation. If product claims stress monitoring or recovery — can't do without RR.
Reading from HealthKit / Health Connect
For apps not doing direct measurement, only displaying:
iOS:
let query = HKAnchoredObjectQuery(
type: HKQuantityType(.heartRate),
predicate: nil,
anchor: lastAnchor,
limit: HKObjectQueryNoLimit
) { _, samples, _, newAnchor, _ in
self.lastAnchor = newAnchor
let bpmValues = (samples as? [HKQuantitySample])?.map {
$0.quantity.doubleValue(for: HKUnit(from: "count/min"))
} ?? []
}
healthStore.execute(query)
Data arrives with sources: Apple Watch Series 4+ gives heart rate every 5–15 minutes at rest, every second during workout.
Visualization
For realtime HR graph: ring buffer of last N values, update once per second. On iOS — Charts (DanielGindi) or Swift Charts (iOS 16+). On Android — MPAndroidChart or Compose Canvas with custom drawing.
Heart rate zones calculated from max heart rate (220 minus age or Karvonen formula with resting heart rate):
| Zone | % of Max | Color |
|---|---|---|
| 1 — recovery | 50–60% | Gray |
| 2 — aerobic base | 60–70% | Blue |
| 3 — aerobic | 70–80% | Green |
| 4 — anaerobic threshold | 80–90% | Orange |
| 5 — maximum | 90–100% | Red |
Timeframes
PPG measurement via camera — 2–3 weeks (with algorithm part). Bluetooth GATT integration — 1–2 weeks. Reading from HealthKit/Health Connect with visualization — 5–8 days.







