Browse Source

feat: sleep data

dev_ios
AnranYus 4 weeks ago
parent
commit
35da66b6b8
  1. 3
      iosApp/iosApp/Libs/Modules/DeviceCenter.m
  2. 1
      shared/src/commonMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.kt
  3. 64
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/sleep/SleepChartPreview.kt
  4. 43
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateViewModel.kt
  5. 7
      shared/src/commonMain/kotlin/com/whitefish/ring/utils/Utils.kt
  6. 71
      shared/src/iosMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.ios.kt

3
iosApp/iosApp/Libs/Modules/DeviceCenter.m

@ -292,8 +292,6 @@ NSString * const CP_NAME = @"BlackShark";
#pragma mark -- 睡眠相关 #pragma mark -- 睡眠相关
-(void)querySleep:(NSDate *)date -(void)querySleep:(NSDate *)date
{ {
self.calcSleepDate = date; self.calcSleepDate = date;
// 0 通知日期变更,其他界面各自查询 // 0 通知日期变更,其他界面各自查询
[[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:NOTI_NAME_SLEEP_DATE_CHANGE object:self.calcSleepDate]; [[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:NOTI_NAME_SLEEP_DATE_CHANGE object:self.calcSleepDate];
@ -331,6 +329,7 @@ NSString * const CP_NAME = @"BlackShark";
// [results addObjectsFromArray:@[napData, napData2]]; // [results addObjectsFromArray:@[napData, napData2]];
#endif #endif
if (results.count) { // if (results.count) { //
strongSelf.currentSelectDataSleepArray = [NSMutableArray new]; strongSelf.currentSelectDataSleepArray = [NSMutableArray new];
strongSelf.currentSelectNapArray = [NSMutableArray new]; strongSelf.currentSelectNapArray = [NSMutableArray new];

1
shared/src/commonMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.kt

@ -3,7 +3,6 @@ package com.whitefish.ring.data
import com.whitefish.ring.bean.ui.HeartRate import com.whitefish.ring.bean.ui.HeartRate
import com.whitefish.ring.bean.ui.SleepState import com.whitefish.ring.bean.ui.SleepState
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
suspend fun mac() = obtainDataStore().data.map { it[address] }.first() suspend fun mac() = obtainDataStore().data.map { it[address] }.first()

64
shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/sleep/SleepChartPreview.kt

@ -22,7 +22,7 @@ fun SleepChartPreview(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// 使用示例睡眠数据 // 使用示例睡眠数据
val sampleData = remember { createSampleSleepData() } // val sampleData = remember { createSampleSleepData() }
Card( Card(
modifier = modifier.padding(16.dp), modifier = modifier.padding(16.dp),
@ -40,16 +40,16 @@ fun SleepChartPreview(
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
) )
// 睡眠图表 // // 睡眠图表
SleepChart( // SleepChart(
sleepData = sampleData, // sleepData = sampleData,
modifier = Modifier // modifier = Modifier
.fillMaxWidth() // .fillMaxWidth()
.height(120.dp), // .height(120.dp),
barWidthMultiplier = 1f, // barWidthMultiplier = 1f,
barHeight = 10f // barHeight = 10f
) // )
//
// 图例 // 图例
SleepChartLegend( SleepChartLegend(
modifier = Modifier modifier = Modifier
@ -57,13 +57,13 @@ fun SleepChartPreview(
.padding(top = 16.dp) .padding(top = 16.dp)
) )
// 睡眠统计信息 // // 睡眠统计信息
SleepStatistics( // SleepStatistics(
sleepData = sampleData, // sleepData = sampleData,
modifier = Modifier // modifier = Modifier
.fillMaxWidth() // .fillMaxWidth()
.padding(top = 16.dp) // .padding(top = 16.dp)
) // )
} }
} }
} }
@ -71,17 +71,17 @@ fun SleepChartPreview(
/** /**
* *
*/ */
fun createSampleSleepData(): List<SleepSegment> { //fun createSampleSleepData(): List<SleepSegment> {
return listOf( // return listOf(
SleepSegment(state = 0, durationMinutes = 30f), // 清醒 // SleepSegment(state = 0, durationMinutes = 30f), // 清醒
SleepSegment(state = 2, durationMinutes = 120f), // 深睡 // SleepSegment(state = 2, durationMinutes = 120f), // 深睡
SleepSegment(state = 1, durationMinutes = 90f), // 浅睡 // SleepSegment(state = 1, durationMinutes = 90f), // 浅睡
SleepSegment(state = 3, durationMinutes = 60f), // REM // SleepSegment(state = 3, durationMinutes = 60f), // REM
SleepSegment(state = 2, durationMinutes = 100f), // 深睡 // SleepSegment(state = 2, durationMinutes = 100f), // 深睡
SleepSegment(state = 1, durationMinutes = 80f), // 浅睡 // SleepSegment(state = 1, durationMinutes = 80f), // 浅睡
SleepSegment(state = 0, durationMinutes = 20f) // 清醒 // SleepSegment(state = 0, durationMinutes = 20f) // 清醒
) // )
} //}
/** /**
* *
@ -150,9 +150,9 @@ private fun SleepStatistics(
// 计算各状态总时长 // 计算各状态总时长
val stateTime = remember(sleepData) { val stateTime = remember(sleepData) {
val times = mutableMapOf<Int, Float>() val times = mutableMapOf<Int, Float>()
sleepData.forEach { segment -> // sleepData.forEach { segment ->
times[segment.state] = (times[segment.state] ?: 0f) + segment.durationMinutes // times[segment.state] = (times[segment.state] ?: 0f) + segment.durationMinutes
} // }
times times
} }

43
shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/StateViewModel.kt

@ -2,8 +2,6 @@ package com.whitefish.ring.ui.home.state
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.whitefish.app.ui.chart.sleep.createSampleSleepData
import com.whitefish.ring.data.MockDataProvider
import com.whitefish.ring.data.getHeartRate import com.whitefish.ring.data.getHeartRate
import com.whitefish.ring.data.getSleep import com.whitefish.ring.data.getSleep
import com.whitefish.ring.obtainDeviceManager import com.whitefish.ring.obtainDeviceManager
@ -12,6 +10,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import com.whitefish.ring.ui.home.state.components.ExerciseGoalData import com.whitefish.ring.ui.home.state.components.ExerciseGoalData
import com.whitefish.ring.ui.home.state.components.RecoveryScoreData import com.whitefish.ring.ui.home.state.components.RecoveryScoreData
import com.whitefish.ring.utils.getPastDayStartMillis
import com.whitefish.ring.utils.getTodayEndMillis import com.whitefish.ring.utils.getTodayEndMillis
import com.whitefish.ring.utils.getTodayStartMillis import com.whitefish.ring.utils.getTodayStartMillis
import com.whitefish.ring.utils.nowMilliseconds import com.whitefish.ring.utils.nowMilliseconds
@ -36,7 +35,7 @@ class StateViewModel : ViewModel() {
val uiState: StateFlow<StateUiState> = _uiState.asStateFlow() val uiState: StateFlow<StateUiState> = _uiState.asStateFlow()
init { init {
loadStateData() loadInitData()
viewModelScope.launch { viewModelScope.launch {
obtainDeviceManager().bleReadyStateFlow.collectLatest { obtainDeviceManager().bleReadyStateFlow.collectLatest {
if (it){ if (it){
@ -44,40 +43,48 @@ class StateViewModel : ViewModel() {
today().atTime(0, 0).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds(), today().atTime(0, 0).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds(),
nowMilliseconds() nowMilliseconds()
) )
Napier.i { "heart rates:${heartRates}" } val sleepState = getSleep(getPastDayStartMillis(1), getTodayEndMillis())
_uiState.value.copy(stateCards = _uiState.value.stateCards.map { card ->
if (card.type is StateCardType.HeartRate) { _uiState.value.copy(stateCards = arrayListOf(
card.copy(type = StateCardType.HeartRate(heartRates.map { it.value })) StateCardData(
} else { title = "心率",
card date = "${today().month}/${today().dayOfMonth}",
} subtitle = "bpm",
}).also { newState -> isFullWidth = false,
type = StateCardType.HeartRate(heartRates.map { it.value })
),
StateCardData(
title = "睡眠",
date = "${today().month}/${today().dayOfMonth}",
subtitle = "昨晚",
isFullWidth = false,
type = StateCardType.SleepState(sleepState.segments)
)
)).let { newState ->
_uiState.value = newState _uiState.value = newState
} }
getSleep(getTodayStartMillis(), getTodayEndMillis())
} }
} }
} }
} }
private fun loadStateData() { private fun loadInitData() {
val mockData = listOf( val mockData = listOf(
StateCardData( StateCardData(
title = "心率", title = "心率",
date = "8/9", date = "${today().month}/${today().dayOfMonth}",
subtitle = "bpm", subtitle = "bpm",
isFullWidth = false, isFullWidth = false,
type = StateCardType.HeartRate(emptyList()) type = StateCardType.HeartRate(emptyList())
), ),
StateCardData( StateCardData(
title = "睡眠", title = "睡眠",
date = "8/9", date = "${today().month}/${today().dayOfMonth}",
subtitle = "昨晚", subtitle = "昨晚",
isFullWidth = false, isFullWidth = false,
type = StateCardType.SleepState(createSampleSleepData()) type = StateCardType.SleepState(emptyList())
) )
) )
_uiState.value = _uiState.value.copy(stateCards = mockData) _uiState.value = _uiState.value.copy(stateCards = mockData)
} }
} }

7
shared/src/commonMain/kotlin/com/whitefish/ring/utils/Utils.kt

@ -1,5 +1,6 @@
package com.whitefish.ring.utils package com.whitefish.ring.utils
import androidx.compose.ui.geometry.Offset
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
@ -24,3 +25,9 @@ fun getTodayStartMillis(): Long {
fun getTodayEndMillis(): Long { fun getTodayEndMillis(): Long {
return today().atTime(23, 59, 59, 999999999).toInstant(UtcOffset.ZERO).toEpochMilliseconds() return today().atTime(23, 59, 59, 999999999).toInstant(UtcOffset.ZERO).toEpochMilliseconds()
} }
fun pastDay(offset: Int) = today().minus(offset, DateTimeUnit.DAY)
fun getPastDayStartMillis(offset: Int): Long {
return pastDay(offset).atTime(0,0).toInstant(UtcOffset.ZERO).toEpochMilliseconds()
}

71
shared/src/iosMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.ios.kt

@ -1,18 +1,31 @@
package com.whitefish.ring.data package com.whitefish.ring.data
import com.whitefish.app.ui.chart.sleep.SleepSegment import com.whitefish.app.ui.chart.sleep.SleepSegment
import com.whitefish.ring.DeviceManager
import com.whitefish.ring.bean.ui.HeartRate import com.whitefish.ring.bean.ui.HeartRate
import com.whitefish.ring.bean.ui.SleepState import com.whitefish.ring.bean.ui.SleepState
import com.whitefish.ring.objc.DBHeartRate import com.whitefish.ring.objc.DBHeartRate
import com.whitefish.ring.objc.DBSleepData import com.whitefish.ring.objc.DBSleepData
import com.whitefish.ring.objc.StagingListObj import com.whitefish.ring.objc.StagingListObj
import com.whitefish.ring.objc.StagingSubObj import com.whitefish.ring.objc.StagingSubObj
import com.whitefish.ring.obtainDeviceManager
import com.whitefish.ring.utils.getCurrentTimestamp import com.whitefish.ring.utils.getCurrentTimestamp
import com.whitefish.ring.utils.timestampToDate import com.whitefish.ring.utils.timestampToDate
import com.whitefish.ring.utils.today
import io.github.aakira.napier.Napier
import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.UtcOffset
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.toNSDateComponents
import platform.Foundation.NSAllocateMemoryPages
import platform.Foundation.timeIntervalSince1970 import platform.Foundation.timeIntervalSince1970
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@ -50,56 +63,52 @@ actual suspend fun getSleep(
return@withContext suspendCoroutine { coroutine -> return@withContext suspendCoroutine { coroutine ->
macAddress?.let { macAddress?.let {
val segments = mutableListOf<SleepSegment>() val segments = mutableListOf<SleepSegment>()
Napier.i { "${start.timestampToDate()} --> ${end.timestampToDate()}" }
DBSleepData.queryDbSleepBy( DBSleepData.queryDbSleepBy(
it, start.timestampToDate().timeIntervalSince1970, it, start.timestampToDate().timeIntervalSince1970,
end.timestampToDate().timeIntervalSince1970 end.timestampToDate().timeIntervalSince1970
) { result -> ) { result ->
val sleepDataList = result?.map { it as DBSleepData }
var totalSeconds = 0 var totalSeconds = 0
result?.let {
it.forEach {
(it as DBSleepData).apply {
totalSeconds += it.duration.intValue
Napier.i { "sleep isNap:${isNap},start:${it.sleepStart},end:${it.sleepEnd},duration:${duration}" }
stagingData.ousideStagingList?.forEach {
(it as StagingSubObj).apply {
Napier.i { "staging type:${type.value}, list size:${list.size} ========>" }
list.forEach { item ->
(item as StagingListObj).apply {
Napier.i { "staging item time:${time.doubleValue}" }
}
}
}
}
}
}
}
val sleepDataList = result?.map { it as DBSleepData }
sleepDataList?.let { dataList -> sleepDataList?.let { dataList ->
dataList.forEach { sleepData -> dataList.forEach { sleepData ->
if (!sleepData.isNap) { if (!sleepData.isNap) {
totalSeconds += sleepData.duration.intValue
// 处理睡眠分期数据 // 处理睡眠分期数据
sleepData.stagingData?.ousideStagingList?.let { stagingList -> sleepData.stagingData.ousideStagingList?.let { stagingList ->
for (i in 0 until stagingList.size) { for (i in 0 until stagingList.size) {
val stagingObj = stagingList[i] as StagingSubObj val stagingObj = stagingList[i] as StagingSubObj
val stageDuration = (stagingObj.list.last() as StagingListObj).time.doubleValue - (stagingObj.list.first() as StagingListObj).time.doubleValue / 60
// 计算该阶段的持续时间
val duration = if (i == 0) {
// 第一个阶段:从开始到结束
stagingObj.list.let { list ->
val firstTime = (list.first() as? StagingListObj)?.time?.doubleValue ?: 0.0
val lastTime = (list.last() as? StagingListObj)?.time?.doubleValue ?: 0.0
lastTime - firstTime
}
} else {
// 后续阶段:从前一个阶段结束到当前阶段结束
val prevStagingObj = stagingList[i - 1] as StagingSubObj
val currentEndTime = (stagingObj.list.first() as? StagingListObj)?.time?.doubleValue ?: 0.0
val prevEndTime = (prevStagingObj.list.last() as? StagingListObj)?.time?.doubleValue ?: 0.0
currentEndTime - prevEndTime
}
// 转换睡眠状态类型到用户定义的状态
val mappedState = mapSleepStageToState(stagingObj.type.value.toInt()) val mappedState = mapSleepStageToState(stagingObj.type.value.toInt())
segments.add(SleepSegment(mappedState, stageDuration.toFloat()))
// 只添加有效的睡眠状态(跳过 NONE 状态)
if (mappedState >= 0) {
val durationMinutes = (duration / 60.0).toFloat()
if (durationMinutes > 0) {
segments.add(SleepSegment(mappedState, durationMinutes))
}
}
} }
} }
} }
} }
} }
coroutine.resume(SleepState(totalSeconds =totalSeconds, segments = segments ))
coroutine.resume(SleepState(totalSeconds =totalSeconds,segments = segments))
} }
} }
} }

Loading…
Cancel
Save