健康数据集成¶
学习目标¶
通过本文档的学习,你将能够:
- 理解核心概念和原理
- 掌握实际应用方法
- 了解最佳实践和注意事项
前置知识¶
在学习本文档之前,建议你已经掌握:
- 基础的嵌入式系统知识
- C/C++编程基础
- 相关领域的基本概念
概述¶
健康数据集成是移动医疗应用的核心功能,涉及从多个来源收集、标准化和同步健康数据。本指南介绍主要的健康数据平台、集成方法和最佳实践。
主要健康数据平台¶
1. Apple HealthKit(iOS)¶
数据类型¶
- 生命体征: 心率、血压、体温、血氧
- 身体测量: 体重、身高、BMI、体脂率
- 活动数据: 步数、距离、爬楼层数、活动能量
- 营养数据: 卡路里、蛋白质、碳水化合物
- 睡眠分析: 睡眠时长、睡眠阶段
- 生殖健康: 月经周期、排卵期
集成示例¶
import HealthKit
class HealthKitIntegration {
let healthStore = HKHealthStore()
// 请求权限
func requestAuthorization() async throws {
let readTypes: Set<HKObjectType> = [
HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.quantityType(forIdentifier: .stepCount)!,
HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic)!,
HKObjectType.quantityType(forIdentifier: .bloodGlucose)!,
HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
]
let writeTypes: Set<HKSampleType> = [
HKObjectType.quantityType(forIdentifier: .bodyMass)!,
HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed)!
]
try await healthStore.requestAuthorization(toShare: writeTypes, read: readTypes)
}
// 读取最近7天的步数
func fetchWeeklySteps() async throws -> [(Date, Double)] {
guard let stepsType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
throw HealthKitError.invalidType
}
let calendar = Calendar.current
let endDate = Date()
let startDate = calendar.date(byAdding: .day, value: -7, to: endDate)!
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
var interval = DateComponents()
interval.day = 1
return try await withCheckedThrowingContinuation { continuation in
let query = HKStatisticsCollectionQuery(
quantityType: stepsType,
quantitySamplePredicate: predicate,
options: .cumulativeSum,
anchorDate: startDate,
intervalComponents: interval
)
query.initialResultsHandler = { query, results, error in
if let error = error {
continuation.resume(throwing: error)
return
}
var data: [(Date, Double)] = []
results?.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in
if let sum = statistics.sumQuantity() {
let steps = sum.doubleValue(for: HKUnit.count())
data.append((statistics.startDate, steps))
}
}
continuation.resume(returning: data)
}
healthStore.execute(query)
}
}
// 写入体重数据
func saveWeight(_ weight: Double, date: Date = Date()) async throws {
guard let weightType = HKQuantityType.quantityType(forIdentifier: .bodyMass) else {
throw HealthKitError.invalidType
}
let weightQuantity = HKQuantity(unit: .gramUnit(with: .kilo), doubleValue: weight)
let weightSample = HKQuantitySample(
type: weightType,
quantity: weightQuantity,
start: date,
end: date
)
try await healthStore.save(weightSample)
}
// 实时监听心率变化
func observeHeartRate(handler: @escaping (Double) -> Void) {
guard let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
return
}
let query = HKObserverQuery(sampleType: heartRateType, predicate: nil) { query, completionHandler, error in
if error != nil {
completionHandler()
return
}
// 获取最新心率数据
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let sampleQuery = HKSampleQuery(
sampleType: heartRateType,
predicate: nil,
limit: 1,
sortDescriptors: [sortDescriptor]
) { _, samples, _ in
if let sample = samples?.first as? HKQuantitySample {
let heartRate = sample.quantity.doubleValue(for: HKUnit.count().unitDivided(by: .minute()))
handler(heartRate)
}
completionHandler()
}
self.healthStore.execute(sampleQuery)
}
healthStore.execute(query)
}
}
2. Health Connect(Android)¶
数据类型¶
- 活动记录: 步数、距离、卡路里、运动会话
- 身体测量: 体重、身高、体脂率
- 生命体征: 心率、血压、血氧、体温
- 营养: 水分摄入、营养素
- 睡眠: 睡眠会话、睡眠阶段
- 周期追踪: 月经周期
集成示例¶
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.*
import androidx.health.connect.client.request.ReadRecordsRequest
import androidx.health.connect.client.time.TimeRangeFilter
import java.time.Instant
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
class HealthConnectIntegration(private val context: Context) {
private val healthConnectClient by lazy {
HealthConnectClient.getOrCreate(context)
}
// 检查可用性
suspend fun isAvailable(): Boolean {
return HealthConnectClient.isAvailable(context)
}
// 请求权限
suspend fun requestPermissions(activity: Activity) {
val permissions = setOf(
HealthPermission.READ_HEART_RATE,
HealthPermission.READ_STEPS,
HealthPermission.READ_BLOOD_PRESSURE,
HealthPermission.READ_BLOOD_GLUCOSE,
HealthPermission.READ_SLEEP,
HealthPermission.WRITE_WEIGHT,
HealthPermission.WRITE_NUTRITION
)
healthConnectClient.permissionController.requestPermissions(activity, permissions)
}
// 读取每日步数
suspend fun readDailySteps(date: ZonedDateTime): Long {
val startOfDay = date.truncatedTo(ChronoUnit.DAYS)
val endOfDay = startOfDay.plusDays(1)
val request = ReadRecordsRequest(
recordType = StepsRecord::class,
timeRangeFilter = TimeRangeFilter.between(
startOfDay.toInstant(),
endOfDay.toInstant()
)
)
val response = healthConnectClient.readRecords(request)
return response.records.sumOf { it.count }
}
// 读取心率数据
suspend fun readHeartRateData(
startTime: Instant,
endTime: Instant
): List<HeartRateData> {
val request = ReadRecordsRequest(
recordType = HeartRateRecord::class,
timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
)
val response = healthConnectClient.readRecords(request)
return response.records.map { record ->
HeartRateData(
timestamp = record.time,
bpm = record.samples.firstOrNull()?.beatsPerMinute ?: 0
)
}
}
// 写入体重
suspend fun writeWeight(weight: Double, time: ZonedDateTime) {
val weightRecord = WeightRecord(
weight = Mass.kilograms(weight),
time = time.toInstant(),
zoneOffset = time.offset
)
healthConnectClient.insertRecords(listOf(weightRecord))
}
// 写入营养数据
suspend fun writeNutrition(
calories: Double,
protein: Double,
carbs: Double,
fat: Double,
time: ZonedDateTime
) {
val nutritionRecord = NutritionRecord(
energy = Energy.kilocalories(calories),
protein = Mass.grams(protein),
totalCarbohydrate = Mass.grams(carbs),
totalFat = Mass.grams(fat),
startTime = time.toInstant(),
endTime = time.toInstant(),
startZoneOffset = time.offset,
endZoneOffset = time.offset
)
healthConnectClient.insertRecords(listOf(nutritionRecord))
}
// 读取睡眠数据
suspend fun readSleepData(date: ZonedDateTime): List<SleepSessionRecord> {
val startOfDay = date.truncatedTo(ChronoUnit.DAYS).minusHours(12)
val endOfDay = startOfDay.plusDays(1)
val request = ReadRecordsRequest(
recordType = SleepSessionRecord::class,
timeRangeFilter = TimeRangeFilter.between(
startOfDay.toInstant(),
endOfDay.toInstant()
)
)
val response = healthConnectClient.readRecords(request)
return response.records
}
}
data class HeartRateData(
val timestamp: Instant,
val bpm: Long
)
3. 可穿戴设备集成¶
Apple Watch¶
import WatchConnectivity
class WatchConnectivityManager: NSObject, WCSessionDelegate {
static let shared = WatchConnectivityManager()
private var session: WCSession?
func setupSession() {
if WCSession.isSupported() {
session = WCSession.default
session?.delegate = self
session?.activate()
}
}
// 发送数据到Apple Watch
func sendHealthData(_ data: [String: Any]) {
guard let session = session, session.isReachable else {
return
}
session.sendMessage(data, replyHandler: nil) { error in
print("发送失败: \(error)")
}
}
// 接收来自Apple Watch的数据
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
if let heartRate = message["heartRate"] as? Double {
// 处理心率数据
handleHeartRate(heartRate)
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// 处理激活完成
}
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {}
private func handleHeartRate(_ heartRate: Double) {
// 保存到HealthKit或本地数据库
}
}
Wear OS¶
import com.google.android.gms.wearable.*
class WearableDataManager(private val context: Context) : DataClient.OnDataChangedListener {
private val dataClient: DataClient by lazy {
Wearable.getDataClient(context)
}
fun startListening() {
dataClient.addListener(this)
}
fun stopListening() {
dataClient.removeListener(this)
}
// 发送数据到手表
fun sendHealthData(heartRate: Int, steps: Int) {
val putDataReq = PutDataMapRequest.create("/health_data").apply {
dataMap.putInt("heart_rate", heartRate)
dataMap.putInt("steps", steps)
dataMap.putLong("timestamp", System.currentTimeMillis())
}.asPutDataRequest()
dataClient.putDataItem(putDataReq)
}
// 接收来自手表的数据
override fun onDataChanged(dataEvents: DataEventBuffer) {
dataEvents.forEach { event ->
if (event.type == DataEvent.TYPE_CHANGED) {
val dataItem = event.dataItem
if (dataItem.uri.path == "/health_data") {
val dataMap = DataMapItem.fromDataItem(dataItem).dataMap
val heartRate = dataMap.getInt("heart_rate")
val steps = dataMap.getInt("steps")
// 处理数据
handleHealthData(heartRate, steps)
}
}
}
}
private fun handleHealthData(heartRate: Int, steps: Int) {
// 保存到Health Connect或本地数据库
}
}
数据标准化¶
FHIR(Fast Healthcare Interoperability Resources)¶
// FHIR Observation资源示例
data class FHIRObservation(
val resourceType: String = "Observation",
val id: String,
val status: String,
val category: List<CodeableConcept>,
val code: CodeableConcept,
val subject: Reference,
val effectiveDateTime: String,
val valueQuantity: Quantity?
)
data class CodeableConcept(
val coding: List<Coding>,
val text: String?
)
data class Coding(
val system: String,
val code: String,
val display: String
)
data class Reference(
val reference: String,
val display: String?
)
data class Quantity(
val value: Double,
val unit: String,
val system: String,
val code: String
)
// 转换HealthKit数据到FHIR
class FHIRConverter {
fun convertHeartRateToFHIR(heartRate: Double, patientId: String, date: Date): FHIRObservation {
return FHIRObservation(
id = UUID.randomUUID().toString(),
status = "final",
category = listOf(
CodeableConcept(
coding = listOf(
Coding(
system = "http://terminology.hl7.org/CodeSystem/observation-category",
code = "vital-signs",
display = "Vital Signs"
)
),
text = "Vital Signs"
)
),
code = CodeableConcept(
coding = listOf(
Coding(
system = "http://loinc.org",
code = "8867-4",
display = "Heart rate"
)
),
text = "Heart rate"
),
subject = Reference(
reference = "Patient/$patientId",
display = null
),
effectiveDateTime = ISO8601DateFormatter().string(from: date),
valueQuantity = Quantity(
value = heartRate,
unit = "beats/minute",
system = "http://unitsofmeasure.org",
code = "/min"
)
)
}
}
数据同步策略¶
1. 增量同步¶
class DataSyncManager {
private let lastSyncKey = "lastSyncTimestamp"
func syncHealthData() async throws {
let lastSync = UserDefaults.standard.object(forKey: lastSyncKey) as? Date ?? Date.distantPast
let now = Date()
// 获取自上次同步以来的新数据
let newData = try await fetchHealthDataSince(lastSync)
// 上传到服务器
try await uploadToServer(newData)
// 更新最后同步时间
UserDefaults.standard.set(now, forKey: lastSyncKey)
}
private func fetchHealthDataSince(_ date: Date) async throws -> [HealthData] {
// 从HealthKit获取数据
return []
}
private func uploadToServer(_ data: [HealthData]) async throws {
// 上传数据
}
}
2. 冲突解决¶
class ConflictResolver {
enum class ResolutionStrategy {
SERVER_WINS, // 服务器数据优先
CLIENT_WINS, // 客户端数据优先
LATEST_WINS, // 最新数据优先
MERGE // 合并数据
}
fun resolveConflict(
serverData: HealthRecord,
clientData: HealthRecord,
strategy: ResolutionStrategy
): HealthRecord {
return when (strategy) {
ResolutionStrategy.SERVER_WINS -> serverData
ResolutionStrategy.CLIENT_WINS -> clientData
ResolutionStrategy.LATEST_WINS -> {
if (serverData.timestamp > clientData.timestamp) serverData else clientData
}
ResolutionStrategy.MERGE -> {
mergeRecords(serverData, clientData)
}
}
}
private fun mergeRecords(server: HealthRecord, client: HealthRecord): HealthRecord {
// 合并逻辑
return server.copy(
value = (server.value + client.value) / 2,
timestamp = maxOf(server.timestamp, client.timestamp)
)
}
}
数据验证¶
数据质量检查¶
class DataValidator {
// 验证心率数据
func validateHeartRate(_ heartRate: Double) -> ValidationResult {
switch heartRate {
case 0:
return .invalid("心率不能为0")
case 1..<30:
return .warning("心率异常低")
case 30..<220:
return .valid
case 220...:
return .warning("心率异常高")
default:
return .invalid("无效的心率值")
}
}
// 验证血压数据
func validateBloodPressure(systolic: Int, diastolic: Int) -> ValidationResult {
guard systolic > diastolic else {
return .invalid("收缩压必须大于舒张压")
}
if systolic < 70 || systolic > 250 {
return .warning("收缩压超出正常范围")
}
if diastolic < 40 || diastolic > 150 {
return .warning("舒张压超出正常范围")
}
return .valid
}
}
enum ValidationResult {
case valid
case warning(String)
case invalid(String)
}
最佳实践¶
1. 权限管理¶
- 仅请求必需的权限
- 提供清晰的权限说明
- 支持部分权限授予
- 定期检查权限状态
2. 数据隐私¶
- 最小化数据收集
- 本地处理优先
- 匿名化敏感数据
- 提供数据导出和删除功能
3. 性能优化¶
- 批量读写数据
- 使用后台任务同步
- 实施数据缓存策略
- 限制查询频率
4. 错误处理¶
- 优雅处理权限拒绝
- 网络错误重试机制
- 数据验证失败提示
- 同步冲突解决
相关资源¶
下一步¶
最后更新: 2024年
💬 讨论区
欢迎在这里分享您的想法、提出问题或参与讨论。需要 GitHub 账号登录。