diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/StandardBeurerSanitasHandler.kt b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/StandardBeurerSanitasHandler.kt index ec81bfcd1..a8694e95d 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/StandardBeurerSanitasHandler.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/StandardBeurerSanitasHandler.kt @@ -17,16 +17,14 @@ */ package com.health.openscale.core.bluetooth.scales -import androidx.datastore.preferences.core.PreferencesSerializer.writeTo import com.health.openscale.core.bluetooth.data.ScaleUser -import com.health.openscale.core.bluetooth.scales.SanitasSbf72Handler.Companion.CHR_SBF72_USER_LIST -import com.health.openscale.core.bluetooth.scales.SanitasSbf72Handler.Companion.SVC_SBF72_CUSTOM import com.health.openscale.core.data.ActivityLevel import com.health.openscale.core.data.GenderType import com.health.openscale.core.service.ScannedDeviceInfo import com.welie.blessed.BluetoothBytesParser import java.nio.ByteBuffer import java.util.GregorianCalendar +import java.util.Locale import java.util.UUID /** @@ -97,8 +95,7 @@ class StandardBeurerSanitasHandler : StandardWeightProfileHandler() { // Model detection; constructor stays empty. override fun supportFor(device: ScannedDeviceInfo): DeviceSupport? { - val name = device.name?.lowercase().orEmpty() - + val name = device.name.lowercase(Locale.ROOT) val model = when { "bf105" in name || "bf720" in name -> Model.BEURER_BF105 "bf950" in name || "sbf77" in name || "sbf76" in name -> Model.BEURER_BF950 @@ -181,6 +178,15 @@ class StandardBeurerSanitasHandler : StandardWeightProfileHandler() { } } + override fun onAutoConsentFailed(user: ScaleUser) { + if (activeModel == Model.BEURER_BF500) { + logD("BF 500: Auto-consent failed, registering new user") + registerScaleNewUser(user.id) + } else { + super.onAutoConsentFailed(user) + } + } + override fun onRequestMeasurement() { profile?.let { logD("Requesting measurement: writing 0x00 to chrTakeMeasurement=${it.chrTakeMeasurement}") @@ -260,7 +266,7 @@ class StandardBeurerSanitasHandler : StandardWeightProfileHandler() { } private fun writeInitials(user: ScaleUser) { - val raw = user.userName?.uppercase()?.replace(Regex("[^A-Z0-9]"), "").orEmpty() + val raw = user.userName.uppercase().replace(Regex("[^A-Z0-9]"), "") val initials = raw.take(3) if (initials.isNotEmpty()) { profile?.chrInitials?.let { chr -> diff --git a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/StandardWeightProfileHandler.kt b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/StandardWeightProfileHandler.kt index c4e9bd751..e04fe45db 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/StandardWeightProfileHandler.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/StandardWeightProfileHandler.kt @@ -91,6 +91,7 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { private var pendingAppUserId: Int? = null private var pendingConsentForNewUser: Int? = null private var awaitingReferenceAfterRegister = false + private var pendingSoftLeanMass: Float = 0.0f /** * Identify devices that expose any of the standard scale services. @@ -138,8 +139,7 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { // 1) Try auto-consent from persisted mapping/consent if (!tryAutoConsent(user)) { - // 2) No mapping → try to list users via UDS (may be unsupported) - requestUdsListAllUsers() + onAutoConsentFailed(user) } // Generic hint @@ -253,6 +253,11 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { logD("UserInteraction requested: CHOOSE_USER with items=${items.joinToString()} indices=${indices.joinToString()}") } + protected open fun onAutoConsentFailed(user: ScaleUser) { + logD("onAutoConsentFailed => requesting via UDS a list of all users") + requestUdsListAllUsers() + } + /** Show a CHOOSE_USER dialog built from simple slot indices (+ "Create new"). */ protected fun presentChooseFromIndices(indicesList: List) { logD("Presenting user choice dialog with existing scale slots: $indicesList and 'Create new'") @@ -294,6 +299,7 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { // When we already have weight (or stabilized) → publish if (prev.hasWeight()) { + transformMeasurement(prev) publishTransformed(prev) pendingMeasurement = null } else { @@ -302,11 +308,42 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { } } else { // Different userId → publish old, start new + transformMeasurement(prev) publishTransformed(prev) pendingMeasurement = newM } } + protected open fun transformMeasurement(m: ScaleMeasurement) { + if (m.weight <= 0f) { + logD("transformMeasurement: skipping (no weight)") + return + } + + // Water: convert from mass in weight unit to percentage + val waterPct = (m.water / m.weight) * 100f + logD("transformMeasurement: water ${m.water} kg → $waterPct%") + m.water = waterPct + + // Bone/LBM: Calculate from soft lean mass if available + if (pendingSoftLeanMass > 0f) { + val fatMass = m.weight * (m.fat / 100f) + val leanBodyMass = m.weight - fatMass + val boneMass = leanBodyMass - pendingSoftLeanMass + m.lbm = leanBodyMass + m.bone = boneMass + logD("transformMeasurement: calculated LBM=$leanBodyMass kg, bone=$boneMass kg from softLean=$pendingSoftLeanMass kg") + } else if (pendingSoftLeanMass == 0f) { + // Reference measurement or user stepped from scale while measuring - no BIA data + m.bone = 0f + m.lbm = 0f + logD("transformMeasurement: reference measurement (softLean=0) - bone/LBM set to 0") + } + + // Reset for next measurement + pendingSoftLeanMass = 0f + } + private fun parseWeightToMeasurement(value: ByteArray): ScaleMeasurement? { if (value.isEmpty()) return null var offset = 0 @@ -399,15 +436,16 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { if (bmrPresent) { val bmrJ = u16le(value, offset); offset += 2 val bmrKcal = ((bmrJ / 4.1868f) * 10f).toInt() / 10f + m.bmr = bmrKcal logD("BMR ≈ $bmrKcal kcal") } if (musclePctPresent) { val musclePct = u16le(value, offset) * 0.1f; offset += 2 m.muscle = musclePct + logD("Muscle %=$musclePct%") } - var softLean = 0.0f if (muscleMassPresent) { val muscleMass = u16le(value, offset) * massMultiplier; offset += 2 logD("Muscle mass=$muscleMass kg") @@ -419,17 +457,20 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { } if (softLeanPresent) { - softLean = u16le(value, offset) * massMultiplier; offset += 2 + val softLean = u16le(value, offset) * massMultiplier; offset += 2 + pendingSoftLeanMass = softLean logD("Soft lean mass=$softLean kg") } if (waterMassPresent) { val bodyWaterMass = u16le(value, offset) * massMultiplier; offset += 2 m.water = bodyWaterMass + logD("Body water mass=$bodyWaterMass kg") } if (impedancePresent) { val z = u16le(value, offset) * 0.1f; offset += 2 + m.impedance = z.toDouble() logD("Impedance=$z Ω") } @@ -447,16 +488,6 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { if (multiPacket) logW("Body Composition: multi-packet measurement not supported") - // Derive LBM & bone if we have soft-lean and weight - val w2 = m.weight - if (w2 > 0f && softLeanPresent) { - val fatMass = w2 * (m.fat / 100f) - val leanBodyMass = w2 - fatMass - val boneMass = leanBodyMass - softLean - m.lbm = leanBodyMass - m.bone = boneMass - } - return m } @@ -650,38 +681,11 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { return } - pendingAppUserId = appUserId logD("CHOOSE_USER selected: scaleIndex=$scaleIndex for appUserId=$appUserId") - if (scaleIndex == -1) { - // Create/register new user on the scale - val consent = randomConsent().also { pendingConsentForNewUser = it } - registeringNewUser = true - logD("Starting registration of new user with appUserId=$appUserId and generated consent=$consent") - userInfo(R.string.bt_info_register_new_user_started) - sendRegisterNewUser(consent) + registerScaleNewUser(appUserId) } else { - // Existing slot selected: persist mapping; use stored consent if available - logD("Linking existing scale slot $scaleIndex to appUserId=$appUserId") - for (i in 0..255) { - if (i != scaleIndex && loadUserIdForScaleIndex(i) == appUserId) { - saveUserIdForScaleIndex(i, -1) - logD("Cleared previous mapping for appUserId=$appUserId at scaleIndex=$i") - } - } - - saveUserIdForScaleIndex(scaleIndex, appUserId) - userInfo(R.string.bt_info_linked_app_user_to_slot, appUserId, scaleIndex) - - val consent = loadConsentForScaleIndex(scaleIndex) - if (consent == -1) { - logD("No consent found for scaleIndex=$scaleIndex, requesting consent") - userInfo(R.string.bt_info_consent_needed, scaleIndex) - requestScaleUserConsent(appUserId, scaleIndex) - } else { - logD("Found existing consent=$consent for scaleIndex=$scaleIndex, sending to scale") - sendConsent(scaleIndex, consent) - } + registerScaleExistingUser(appUserId, scaleIndex) } } @@ -729,6 +733,42 @@ open class StandardWeightProfileHandler : ScaleDeviceHandler() { } } + protected fun registerScaleNewUser(appUserId: Int) { + pendingAppUserId = appUserId + registeringNewUser = true + + val consent = randomConsent().also { pendingConsentForNewUser = it } + logD("Starting registration of new user with appUserId=$appUserId and generated consent=$consent") + userInfo(R.string.bt_info_register_new_user_started) + sendRegisterNewUser(consent) + } + + protected fun registerScaleExistingUser(appUserId: Int, scaleIndex: Int) { + pendingAppUserId = appUserId + + // Existing slot selected: persist mapping; use stored consent if available + logD("Linking existing scale slot $scaleIndex to appUserId=$appUserId") + for (i in 0..255) { + if (i != scaleIndex && loadUserIdForScaleIndex(i) == appUserId) { + saveUserIdForScaleIndex(i, -1) + logD("Cleared previous mapping for appUserId=$appUserId at scaleIndex=$i") + } + } + + saveUserIdForScaleIndex(scaleIndex, appUserId) + userInfo(R.string.bt_info_linked_app_user_to_slot, appUserId, scaleIndex) + + val consent = loadConsentForScaleIndex(scaleIndex) + if (consent == -1) { + logD("No consent found for scaleIndex=$scaleIndex, requesting consent") + userInfo(R.string.bt_info_consent_needed, scaleIndex) + requestScaleUserConsent(appUserId, scaleIndex) + } else { + logD("Found existing consent=$consent for scaleIndex=$scaleIndex, sending to scale") + sendConsent(scaleIndex, consent) + } + } + // ---- UDS command builders ------------------------------------------------- private fun sendRegisterNewUser(consentCode: Int) {