mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
fix: Correct wear detection on AirPods Max 2
Bit 5 of pubStatus is always set on A3454 and no longer carries the wear flag (unlike Max gen 1). Read bits 1 and 3 instead — the per-earcup sensors. OR rather than AND so phones that only see one bit reliably still report worn correctly. Closes #548
This commit is contained in:
+11
-1
@@ -44,8 +44,18 @@ data class AirPodsMax2(
|
||||
return pubFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
// Aggregate "any wear sensor active" — NOT "both earcups worn".
|
||||
// Observed on A3454: bit 5 of pubStatus is always set while advertising and
|
||||
// no longer carries the wear flag (unlike Max gen 1). Bits 1 and 3 of
|
||||
// pubStatus reflect the two earcup sensors (same byte positions used by
|
||||
// DualApplePods, but without the primary/flip semantics).
|
||||
//
|
||||
// We OR them because some Android pairings only see one of the two bits
|
||||
// reliably (issue #548: "both worn" advertises as 0x23 — bit 1 only —
|
||||
// while macOS sees 0x2B with both bits set). AND-ing would falsely report
|
||||
// "not worn" during normal use on those phones.
|
||||
override val isBeingWorn: Boolean
|
||||
get() = pubStatus.isBitSet(5)
|
||||
get() = pubStatus.isBitSet(1) || pubStatus.isBitSet(3)
|
||||
|
||||
class Factory @Inject constructor(
|
||||
private val repo: PodHistoryRepo,
|
||||
|
||||
@@ -12,6 +12,7 @@ import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityPayload
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
@@ -200,6 +201,23 @@ class PodDeviceTest : BaseTest() {
|
||||
device.isEitherPodInEar shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BLE isBeingWorn delegates to HasEarDetection on single-pod devices`() {
|
||||
val mock = mockk<SingleApplePods>(relaxed = true, moreInterfaces = arrayOf(HasEarDetection::class)) {
|
||||
every { model } returns PodModel.AIRPODS_MAX2
|
||||
every { (this@mockk as HasEarDetection).isBeingWorn } returns true
|
||||
}
|
||||
val device = PodDevice(profileId = null, ble = mock, aap = null)
|
||||
device.isBeingWorn shouldBe true
|
||||
|
||||
val mockNotWorn = mockk<SingleApplePods>(relaxed = true, moreInterfaces = arrayOf(HasEarDetection::class)) {
|
||||
every { model } returns PodModel.AIRPODS_MAX2
|
||||
every { (this@mockk as HasEarDetection).isBeingWorn } returns false
|
||||
}
|
||||
val deviceNotWorn = PodDevice(profileId = null, ble = mockNotWorn, aap = null)
|
||||
deviceNotWorn.isBeingWorn shouldBe false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AAP ear detection preferred over BLE`() {
|
||||
val mock = mockk<DualApplePods>(relaxed = true) {
|
||||
|
||||
+14
@@ -24,6 +24,7 @@ class AirPodsMax2Test : BaseBlePodsTest() {
|
||||
batteryHeadsetPercent shouldBe 0.6f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
isBeingWorn shouldBe true
|
||||
|
||||
model shouldBe PodModel.AIRPODS_MAX2
|
||||
}
|
||||
@@ -35,6 +36,7 @@ class AirPodsMax2Test : BaseBlePodsTest() {
|
||||
pubStatus shouldBe 0x20.toUByte()
|
||||
batteryHeadsetPercent shouldBe 0.6f
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
isBeingWorn shouldBe false
|
||||
model shouldBe PodModel.AIRPODS_MAX2
|
||||
}
|
||||
|
||||
@@ -42,6 +44,7 @@ class AirPodsMax2Test : BaseBlePodsTest() {
|
||||
pubStatus shouldBe 0x21.toUByte()
|
||||
batteryHeadsetPercent shouldBe 0.6f
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
isBeingWorn shouldBe false
|
||||
model shouldBe PodModel.AIRPODS_MAX2
|
||||
}
|
||||
|
||||
@@ -49,6 +52,17 @@ class AirPodsMax2Test : BaseBlePodsTest() {
|
||||
pubStatus shouldBe 0x23.toUByte()
|
||||
batteryHeadsetPercent shouldBe 0.6f
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
isBeingWorn shouldBe true
|
||||
model shouldBe PodModel.AIRPODS_MAX2
|
||||
}
|
||||
|
||||
// Synthetic: one earcup off, the other on (issue #548 sees 0x29 in this state on
|
||||
// both macOS and Android; we don't claim left vs right side semantics here).
|
||||
create<AirPodsMax2>("07 19 01 2D 20 29 F6 8F 03 14 04 E1 8B 99 98 20 0B C3 C1 12 2D B0 43 98 94 D6 A0") {
|
||||
pubStatus shouldBe 0x29.toUByte()
|
||||
batteryHeadsetPercent shouldBe 0.6f
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
isBeingWorn shouldBe true
|
||||
model shouldBe PodModel.AIRPODS_MAX2
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user