From 6fe1a32687888cebb512053688f4cabf68741b61 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 23 Aug 2026 15:40:59 +0200 Subject: [PATCH] =?UTF-8?q?V=C3=A4g-fl=C3=B6de:=20v=C3=A4nta=20in=20och=20?= =?UTF-8?q?merga=20sammans=C3=A4ttningsramen=20(muskler/fett/vatten)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exingtech Y1/VScale skickar vikten först och kroppssammansättningen i en senare ram — tidigare låste skärmen resultatet och kopplade ner vid första publicerade mätningen, så bara vikten registrerades. Nu mergas ramarna (ScaleMeasurement.mergeWith) och anslutningen hålls öppen tills en ram med sammansättning kommit, max 20 s. UI visar 'väntar på muskler/fett/vatten' under tiden och en förklaring om sammansättningen uteblir (kroppsdata saknas / vägde med strumpor). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg --- .../fitnessdroid/ui/scale/ScaleScreens.kt | 105 +++++++++++++++--- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt index c77a815..a4c6ddd 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt @@ -306,10 +306,19 @@ private fun DeviceRow(device: ScannedDeviceInfo, onClick: (() -> Unit)?) { sealed class WeighState { data object Idle : WeighState() data class Working(val status: String) : WeighState() - data class Done(val measurement: ScaleMeasurement) : WeighState() + + /** + * Vikten är mottagen. [complete] = false medan vi fortfarande lyssnar efter + * en senare ram med kroppssammansättning (vågen skickar ofta vikten först + * och muskler/fett/vatten någon sekund senare). + */ + data class Done(val measurement: ScaleMeasurement, val complete: Boolean) : WeighState() data class Failed(val message: String) : WeighState() } +private fun ScaleMeasurement.hasComposition(): Boolean = + fat > 0f || water > 0f || muscle > 0f + class WeighViewModel( private val scaleManager: ScaleManager, private val repo: GymRepository, @@ -319,31 +328,37 @@ class WeighViewModel( val saveError = MutableStateFlow(null) val missingBodyData = MutableStateFlow(false) + private var compositionTimeout: kotlinx.coroutines.Job? = null + init { viewModelScope.launch { scaleManager.lastEvent.collect { event -> - // En färdig mätning ska inte skrivas över av senare events (t.ex. Disconnected) - if (state.value is WeighState.Done) return@collect + val current = state.value when (event) { null -> {} + is BluetoothEvent.MeasurementReceived -> onMeasurement(event.measurement) is BluetoothEvent.Listening -> - state.value = WeighState.Working("Lyssnar efter vågen — kliv på den…") + if (current !is WeighState.Done) + state.value = WeighState.Working("Lyssnar efter vågen — kliv på den…") is BluetoothEvent.Connected -> - state.value = WeighState.Working("Ansluten — ställ dig barfota på vågen") + if (current !is WeighState.Done) + state.value = WeighState.Working("Ansluten — ställ dig barfota på vågen") is BluetoothEvent.DeviceMessage -> - state.value = WeighState.Working(event.message) - is BluetoothEvent.MeasurementReceived -> { - state.value = WeighState.Done(event.measurement) - scaleManager.disconnect() - } + if (current !is WeighState.Done) + state.value = WeighState.Working(event.message) is BluetoothEvent.ConnectionFailed -> - state.value = WeighState.Failed("Kunde inte ansluta: ${event.error}") + if (current !is WeighState.Done) + state.value = WeighState.Failed("Kunde inte ansluta: ${event.error}") is BluetoothEvent.Error -> - state.value = WeighState.Failed(event.error) - is BluetoothEvent.Disconnected -> { - if (state.value is WeighState.Working) { + if (current !is WeighState.Done) + state.value = WeighState.Failed(event.error) + is BluetoothEvent.Disconnected -> when (current) { + is WeighState.Working -> state.value = WeighState.Failed("Vågen kopplade från innan mätningen blev klar") - } + // Tappade vi länken medan vi väntade på sammansättningen + // behåller vi vikten och avslutar mätningen. + is WeighState.Done -> finishMeasurement() + else -> {} } is BluetoothEvent.BroadcastComplete, is BluetoothEvent.UserInteractionRequired -> {} } @@ -351,7 +366,44 @@ class WeighViewModel( } } + /** + * Vågen kan publicera flera ramar: först bara vikt, sen en med + * muskler/fett/vatten. Vi mergar och blir klara först när sammansättningen + * kommit — eller efter en väntetid om den uteblir. + */ + private fun onMeasurement(measurement: ScaleMeasurement) { + val previous = (state.value as? WeighState.Done)?.measurement + val merged = previous?.let { measurement.mergeWith(it) } ?: measurement + + if (merged.hasComposition()) { + compositionTimeout?.cancel() + compositionTimeout = null + state.value = WeighState.Done(merged, complete = true) + scaleManager.disconnect() + } else { + state.value = WeighState.Done(merged, complete = false) + if (compositionTimeout == null) { + compositionTimeout = viewModelScope.launch { + kotlinx.coroutines.delay(20_000) + finishMeasurement() + } + } + } + } + + /** Avsluta med det vi har (vikt utan sammansättning). */ + private fun finishMeasurement() { + compositionTimeout?.cancel() + compositionTimeout = null + (state.value as? WeighState.Done)?.let { d -> + if (!d.complete) state.value = d.copy(complete = true) + } + scaleManager.disconnect() + } + fun start() { + compositionTimeout?.cancel() + compositionTimeout = null state.value = WeighState.Working("Söker efter vågen…") saved.value = false saveError.value = null @@ -523,6 +575,29 @@ fun WeighScreen( ResultRow("Benmassa", m.bone, " kg") ResultRow("Bukfett (index)", m.visceralFat, "") + if (!s.complete) { + Row(verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + Text( + "Väntar på muskler/fett/vatten — stå kvar på vågen…", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 10.dp), + ) + } + } else if (!m.hasComposition()) { + Text( + "Ingen kroppssammansättning togs emot. Kontrollera att " + + "längd/födelseår/kön är ifyllda i profilen och väg dig " + + "barfota med torra fötter.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + saveError?.let { Text( it,