diff --git a/app/src/main/java/eu/darken/capod/monitor/core/BatteryReading.kt b/app/src/main/java/eu/darken/capod/monitor/core/BatteryReading.kt new file mode 100644 index 00000000..c2339b89 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/monitor/core/BatteryReading.kt @@ -0,0 +1,29 @@ +package eu.darken.capod.monitor.core + +import eu.darken.capod.pods.core.apple.ble.BATTERY_RESOLUTION_DECILE +import eu.darken.capod.pods.core.apple.ble.BATTERY_RESOLUTION_PERCENT +import eu.darken.capod.pods.core.apple.ble.devices.HasCase + +/** + * A battery percentage together with the step size of the source it came from. A reading of 20% + * from a decile source means "somewhere in [20%, 30%)", which is the difference between a claim we + * can make and one we can't. + */ +data class BatteryReading( + val percent: Float, + val resolution: Float, +) + +/** + * [PodDevice.batteryCase] resolved through the same precedence, but keeping the resolution of + * whichever source won. Cache entries store no resolution, so they report the coarsest one. + */ +val PodDevice.batteryCaseReading: BatteryReading? + get() { + aap?.batteryCase?.let { return BatteryReading(it, BATTERY_RESOLUTION_PERCENT) } + (ble as? HasCase)?.let { snapshot -> + snapshot.batteryCasePercent?.let { return BatteryReading(it, snapshot.batteryCaseResolution) } + } + cached?.case?.percent?.let { return BatteryReading(it, BATTERY_RESOLUTION_DECILE) } + return null + } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/PodDeviceExtensions.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/PodDeviceExtensions.kt index 3c45ef37..ea2d9d3e 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/PodDeviceExtensions.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/PodDeviceExtensions.kt @@ -21,6 +21,12 @@ import kotlin.math.roundToInt const val BATTERY_UNKNOWN = -1f +/** Step size of a percent-granularity reading: AAP battery notifications and the encrypted payload. */ +const val BATTERY_RESOLUTION_PERCENT = 0.01f + +/** Step size of the public advertisement's battery nibble, which only carries deciles. */ +const val BATTERY_RESOLUTION_DECILE = 0.10f + fun isKnownBattery(percent: Float): Boolean = percent.isFinite() && percent >= 0f fun batteryProgress(percent: Float): Float = diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/devices/DualApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/devices/DualApplePods.kt index 07fd298c..4a982bfa 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/devices/DualApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/devices/DualApplePods.kt @@ -4,6 +4,8 @@ import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.isBitSet import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.upperNibble +import eu.darken.capod.pods.core.apple.ble.BATTERY_RESOLUTION_DECILE +import eu.darken.capod.pods.core.apple.ble.BATTERY_RESOLUTION_PERCENT import eu.darken.capod.pods.core.apple.ble.DualBlePodSnapshot import eu.darken.capod.pods.core.apple.ble.DualBlePodSnapshot.Pod @@ -137,6 +139,13 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualBlePodSnapshot, } } + /** The encrypted payload carries whole percents; the public nibble only carries deciles. */ + override val batteryCaseResolution: Float + get() = when { + payload.private?.asBatteryState(3) != null -> BATTERY_RESOLUTION_PERCENT + else -> BATTERY_RESOLUTION_DECILE + } + override val isCaseCharging: Boolean get() { payload.private?.asBatteryState(3)?.let { return it.isCharging } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/devices/HasCase.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/devices/HasCase.kt index a196d909..c9695823 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/devices/HasCase.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/devices/HasCase.kt @@ -2,14 +2,23 @@ package eu.darken.capod.pods.core.apple.ble.devices import androidx.annotation.DrawableRes import eu.darken.capod.R +import eu.darken.capod.pods.core.apple.ble.BATTERY_RESOLUTION_DECILE interface HasCase { val batteryCasePercent: Float? + /** + * Step size of [batteryCasePercent]. Deciles unless a decoder knows it read a finer source, so + * a caller reasoning about how much the true value may exceed the reading errs on the coarse + * side. + */ + val batteryCaseResolution: Float + get() = BATTERY_RESOLUTION_DECILE + val isCaseCharging: Boolean @get:DrawableRes val caseIcon: Int get() = R.drawable.device_airpods_gen1_case -} \ No newline at end of file +}