fix(popup): decode lid state from case context

This commit is contained in:
darken
2026-04-14 11:28:41 +02:00
committed by Matthias Urhahn
parent 46fcf5a4cb
commit 1f7bc7cfd8
2 changed files with 66 additions and 11 deletions
@@ -144,11 +144,11 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualBlePodSnapshot,
return pubFlags.isBitSet(2)
}
val hasCaseContext: Boolean
get() = isThisPodInThecase || isOnePodInCase || areBothPodsInCase
val caseLidState: LidState
get() {
val rawstate = pubCaseLidState
return LidState.entries.firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
}
get() = LidState.fromRaw(pubCaseLidState, hasCaseContext)
/**
* TODO this is glitchy
@@ -158,11 +158,23 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualBlePodSnapshot,
* They reset after some time to their start values.
* The upper limits are not the maximums but are only reached if playing with the case.
*/
enum class LidState(val rawRange: IntRange) {
OPEN(0x30..0x37),
CLOSED(0x38..0x3F),
NOT_IN_CASE(0x00..0x03),
UNKNOWN(0xFF..0xFF);
enum class LidState {
OPEN,
CLOSED,
NOT_IN_CASE,
UNKNOWN;
companion object {
fun fromRaw(raw: UByte, hasCaseContext: Boolean): LidState {
if (!hasCaseContext) return NOT_IN_CASE
return when ((raw.toInt() shr 3) and 0x01) {
0 -> OPEN
1 -> CLOSED
else -> UNKNOWN
}
}
}
}
}
}
@@ -1,9 +1,13 @@
package eu.darken.capod.pods.core.apple.ble.devices
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.pods.core.apple.ble.devices.airpods.AirPodsPro
import eu.darken.capod.pods.core.apple.ble.devices.airpods.HasStateDetectionAirPods
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityPayload
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import java.time.Instant
class DualApplePodsTest : BaseBlePodsTest() {
@@ -210,6 +214,45 @@ class DualApplePodsTest : BaseBlePodsTest() {
}
}
private fun directAirPodsPro(
status: Int,
rawCaseLidState: Int,
) = AirPodsPro(
scanResult = BleScanResult(
receivedAt = Instant.parse("2026-01-01T00:00:00Z"),
address = "77:49:4C:D8:25:0C",
rssi = -66,
generatedAtNanos = 136136027721826,
manufacturerSpecificData = mutableMapOf(),
),
payload = ProximityPayload(
public = ProximityPayload.Public(
ubyteArrayOf(
0x01u,
0x0Eu,
0x20u,
status.toUByte(),
0x89u,
0xB3u,
rawCaseLidState.toUByte(),
0x00u,
0x00u,
)
),
private = null,
),
meta = ApplePods.AppleMeta(),
)
@Test
fun `test AirPodDevice - case lid uses status derived case context`() {
directAirPodsPro(status = 0x10, rawCaseLidState = 0x51).caseLidState shouldBe DualApplePods.LidState.OPEN
directAirPodsPro(status = 0x04, rawCaseLidState = 0x5A).caseLidState shouldBe DualApplePods.LidState.CLOSED
directAirPodsPro(status = 0x40, rawCaseLidState = 0x51).caseLidState shouldBe DualApplePods.LidState.OPEN
directAirPodsPro(status = 0x2B, rawCaseLidState = 0x11).caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE
directAirPodsPro(status = 0x20, rawCaseLidState = 0x5A).caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE
}
@Test
fun `test AirPodDevice - connection state`() = runTest {
// Disconnected
@@ -244,4 +287,4 @@ class DualApplePodsTest : BaseBlePodsTest() {
}
}
}
}