Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,14 @@ class WhoopRecordingService : Service() {
recordCollectorJob = launch { collectRecords() }
unknownCollectorJob?.cancel()
unknownCollectorJob = launch { collectUnknownObservations() }
connectWithRetry(currentMode)
whoopRuntimeCoordinator.setState(WhoopRuntimeState.HISTORY)
gattManager.awaitHistoricalSyncCompletion()
connectWithRetry(currentMode) {
whoopRuntimeCoordinator.setState(WhoopRuntimeState.HISTORY)
gattManager.awaitHistoricalSyncCompletion()
}
val repairedHrSamples = recordingRepo.repairMissingHrSamplesFromRawImu()
if (repairedHrSamples > 0) {
Timber.i("Repaired $repairedHrSamples missing WHOOP HR samples from raw IMU")
}
delay(1_000L)
stopRecording()
} catch (e: Exception) {
Expand Down Expand Up @@ -302,7 +307,10 @@ class WhoopRecordingService : Service() {

// ── BLE connection with exponential backoff ───────────────────────────────

private suspend fun connectWithRetry(mode: WhoopSessionMode) {
private suspend fun connectWithRetry(
mode: WhoopSessionMode,
afterConnected: suspend () -> Unit = {},
) {
var backoffMs = 2_000L

repeat(6) { attempt ->
Expand All @@ -322,9 +330,11 @@ class WhoopRecordingService : Service() {
"WHOOP connected — recording"
}
)
afterConnected()
return
} catch (e: Exception) {
Timber.w(e, "Connect attempt $attempt failed, retry in ${backoffMs}ms")
gattManager.disconnect()
if (attempt < 5) {
delay(backoffMs)
backoffMs = (backoffMs * 2).coerceAtMost(30_000L)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,16 @@ class DashboardViewModel @Inject constructor(

private fun refreshWhoopHistoryState() {
viewModelScope.launch {
val repairedHrSamples = recordingRepo.repairMissingHrSamplesFromRawImu()
val latestWhoopTimestamp = recordingRepo.getLatestStoredTimestamp()
_uiState.update {
it.copy(
latestWhoopDataTimestampSeconds = latestWhoopTimestamp,
)
}
if (repairedHrSamples > 0) {
refreshAnalytics()
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions data/src/main/kotlin/com/sploot/data/dao/RawImuDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,29 @@ interface RawImuDao {

@Query("SELECT MAX(tsSeconds) FROM raw_imu")
suspend fun getLatestTimestamp(): Long?

@Query(
"""
INSERT OR IGNORE INTO hr_samples(sessionId, tsSeconds, hrBpm)
SELECT sessionId, tsSeconds, hrBpm
FROM raw_imu
WHERE hrBpm BETWEEN 30 AND 240
"""
)
suspend fun backfillMissingHrSamples()

@Query(
"""
SELECT COUNT(*)
FROM raw_imu
WHERE hrBpm BETWEEN 30 AND 240
AND NOT EXISTS (
SELECT 1
FROM hr_samples
WHERE hr_samples.sessionId = raw_imu.sessionId
AND hr_samples.tsSeconds = raw_imu.tsSeconds
)
"""
)
suspend fun countMissingHrSamples(): Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ class RecordingRepository @Inject constructor(
eventDao.getLatestTimestamp(),
).maxOrNull()

suspend fun repairMissingHrSamplesFromRawImu(): Int {
val missingCount = imuDao.countMissingHrSamples()
if (missingCount > 0) {
imuDao.backfillMissingHrSamples()
}
return missingCount
}

suspend fun sessionHasAnyData(sessionId: Long): Boolean =
imuDao.countForSession(sessionId) > 0 ||
ppgDao.countForSession(sessionId) > 0 ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ class WhoopGattManager @Inject constructor(
}
BluetoothProfile.STATE_DISCONNECTED -> {
_state.value = ConnectionState.DISCONNECTED
val disconnectException = IOException("GATT disconnected during WHOOP operation (status=$status)")
if (!connectionDeferred.isCompleted) {
connectionDeferred.completeExceptionally(disconnectException)
}
if (!liveReadyDeferred.isCompleted) {
liveReadyDeferred.completeExceptionally(disconnectException)
}
if (!historicalSyncDeferred.isCompleted) {
historicalSyncDeferred.completeExceptionally(disconnectException)
}
dataAssembler.reset()
eventAssembler.reset()
cmdAssembler.reset()
Expand Down
Loading