From 1f7bc7cfd8af1c22ba40a53d0d9b44b07a3c8037 Mon Sep 17 00:00:00 2001 From: darken Date: Mon, 13 Apr 2026 18:07:26 +0200 Subject: [PATCH] fix(popup): decode lid state from case context --- .../core/apple/ble/devices/DualApplePods.kt | 32 ++++++++----- .../apple/ble/devices/DualApplePodsTest.kt | 45 ++++++++++++++++++- 2 files changed, 66 insertions(+), 11 deletions(-) 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 671af13f..8d141150 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 @@ -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 + } + } + } } -} \ No newline at end of file +} diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/ble/devices/DualApplePodsTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/ble/devices/DualApplePodsTest.kt index 2916ba4b..0fd7ccc2 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/ble/devices/DualApplePodsTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/ble/devices/DualApplePodsTest.kt @@ -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() { } } -} \ No newline at end of file +}