Skip to content
Closed
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 @@ -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

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Int>) {
logD("Presenting user choice dialog with existing scale slots: $indicesList and 'Create new'")
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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 Ω")
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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) {
Expand Down