mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
feat(monitor): Expose how coarse a case battery reading is
PodDevice.batteryCase merges an AAP notification, the encrypted advertisement payload, the public advertisement nibble and the cache into one Float, and the provenance is gone by the time anyone reads it. The public nibble only carries deciles, so 20% there can mean anything up to 29%. batteryCaseReading walks the same precedence and keeps the step size of the source that won, leaving batteryCase untouched for every existing caller.
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user