Реализация мониторинга пульса через мобильное приложение
Пульс можно измерять тремя способами через телефон: встроенная камера (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 дней.







