Реалізація мониторингу пульсу через мобільне приложення
Пульс можна вимірювати трьома способами через телефон: вбудована камера (PPG через вспишку), Bluetooth GATT з зовнішнім датчиком (нагрудний пояс, смарт-часи), або читання з платформе сховища (HealthKit, Health Connect — дані від Apple Watch / Wear OS). Вибір підходу визначає точність, затримку та UX.
PPG через камеру: як це працює та чому складно
Користувач прикладає палець до камери зі вспішкою. Капіляри в пальці пульсують — змінюється кількість відбитого червоного світла. Аналізуємо зелений канал (чутливіший до гемоглобіну) кожного кадра.
// 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 }
// Середнє значення зеленого каналу по центральній області
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)
}
Далі — FFT або Peak Detection на часовому ряді значень. При частоті 30 fps діапазон ЧСС 40–200 уд/мин → частота 0.67–3.33 Гц. FFT на вікні 5 секунд (150 семплів) дає достатнє розрізнення.
Проблеми PPG:
- Рух пальця = артефакти в 10× більше пульсового сигналу
- Яскравість зовнішнього освітлення впливає на baseline (потребує нормалізації)
- Точність ±5–10 уд/мин при ідеальних умовах, ±20+ при русі
- iOS ограничує яскравість вспішки — на деяких моделях сигнал слабкий
PPG підходить для «вимірювання в спокої, прикладивши палець на 30 секунд». Для мониторингу в реальному часі під час руху — потребується зовнішній датчик.
Bluetooth GATT: Heart Rate Profile
Стандартний профіль Bluetooth LE для датчиків пульсу (Polar H10, Wahoo TICKR, Garmin HRM):
- Service UUID:
0x180D(Heart Rate) - Characteristic UUID:
0x2A37(Heart Rate Measurement)
Дані приходять через Notification. Перший байт — флаги: біт 0 визначає формат (UINT8 або UINT16), біт 4 — наявність RR-інтервалів.
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-інтервали (якщо є) — для HRV
if (flag and 0x10 != 0) {
var offset = if (flag and 0x08 != 0) 4 else 3 // пропускаємо енергозатрати якщо є
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 // переведення з 1/1024 с в мс
rrIntervals.add(rrMs)
offset += 2
}
}
}
RR-інтервали — основа для розрахунку HRV (Heart Rate Variability). Якщо продукт претендує на стрес-мониторинг або восстановление — без RR не обійтися.
Читання з HealthKit / Health Connect
Для приложень, які не займаються прямим вимірюванням, а тільки показують дані:
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)
Дані приходять з джерелами: Apple Watch Series 4+ дає ЧСС кожні 5–15 хвилин у спокої та кожну секунду під час тренування.
Візуалізація
Для графіка ЧСС в реальному часі: кільцевий буфер останніх N значень, оновлення раз в секунду. На iOS — Charts (DanielGindi) або Swift Charts (iOS 16+). На Android — MPAndroidChart або Compose Canvas з кастомним рисуванням.
Зони ЧСС розраховуються від максимального пульсу (220 мінус вік або за формулою Кервонена з урахуванням пульсу в спокої):
| Зона | % від макс | Колір |
|---|---|---|
| 1 — відновлення | 50–60% | Сірий |
| 2 — аеробна база | 60–70% | Синій |
| 3 — аеробна | 70–80% | Зелений |
| 4 — анаеробний поріг | 80–90% | Помаранчевий |
| 5 — максимальна | 90–100% | Червоний |
Терміни
PPG-вимірювання через камеру — 2–3 тижні (з алгоритмічною частиною). Bluetooth GATT-інтеграція — 1–2 тижні. Читання з HealthKit/Health Connect з візуалізацією — 5–8 днів.







