Handle ear detection and microphone detection for dual pods better.

This commit is contained in:
darken
2022-01-25 22:54:53 +01:00
parent 2de2369600
commit 8f609ca989
21 changed files with 140 additions and 60 deletions
@@ -5,10 +5,16 @@ import java.util.*
fun Byte.toHex(): String = String.format("%02X", this)
fun UByte.toHex(): String = this.toByte().toHex()
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toByte()
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toByte()
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUByte()
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUByte()
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toShort()
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toShort()
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUShort()
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUShort()
fun Byte.isBitSet(pos: Int): Boolean = BitSet.valueOf(arrayOf(this).toByteArray()).get(pos)
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
fun Short.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
fun UShort.isBitSet(pos: Int): Boolean = this.toShort().isBitSet(pos)
fun UShort.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(4, '0')
fun UByte.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(8, '0')
@@ -7,7 +7,6 @@ import eu.darken.capod.R
import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.HasDualPods.Pod
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
@@ -31,6 +30,9 @@ class DualApplePodsCardVH(parent: ViewGroup) :
text = sb
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
if (item.showDebug) {
append(" [${device.primaryPod.name}]")
}
}
deviceIcon.setImageResource(device.iconRes)
@@ -49,8 +51,8 @@ class DualApplePodsCardVH(parent: ViewGroup) :
podLeftChargingIcon.isInvisible = !isLeftPodCharging
podLeftChargingLabel.isInvisible = !isLeftPodCharging
podLeftMicrophoneIcon.isInvisible = microPhonePod != Pod.LEFT
podLeftMicrophoneLabel.isInvisible = microPhonePod != Pod.LEFT
podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone
podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone
podLeftWearIcon.isInvisible = !isLeftPodInEar
podLeftWearLabel.isInvisible = !isLeftPodInEar
@@ -64,8 +66,8 @@ class DualApplePodsCardVH(parent: ViewGroup) :
podRightChargingIcon.isInvisible = !isRightPodCharging
podRightChargingLabel.isInvisible = !isRightPodCharging
podRightMicrophoneIcon.isInvisible = microPhonePod != Pod.RIGHT
podRightMicrophoneLabel.isInvisible = microPhonePod != Pod.RIGHT
podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone
podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone
podRightWearIcon.isInvisible = !isRightPodInEar
podRightWearLabel.isInvisible = !isRightPodInEar
@@ -6,6 +6,9 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.*
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.toBinaryString
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.airpods.*
import eu.darken.capod.pods.core.apple.beats.*
@@ -63,6 +66,14 @@ class AppleFactory @Inject constructor(
scanResult = scanResult,
proximityMessage = pm,
)
if (device is DualApplePods) {
device.apply {
log(
TAG,
WARN
) { "${rssi} ${scanResult.address} ${rawStatus.upperNibble.toBinaryString()} ${rawStatus.lowerNibble.toBinaryString()} primary=$primaryPod isCasePod=$isThisPodInThecase" }
}
}
if (factory == null && !SILENCED_PMS.contains(device.rawDeviceModel) && scanResult.address != "6E:9E:D1:49:D2:6D") {
SILENCED_PMS.add(device.rawDeviceModel)
@@ -5,6 +5,9 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.toHex
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
@@ -27,11 +30,20 @@ interface ApplePods : PodDevice {
val rawStatus: UByte
get() = proximityMessage.data[3]
val rawStatusHex: String
get() = rawStatus.toHex()
val rawPodsBattery: UByte
get() = proximityMessage.data[4]
val rawCaseBattery: UByte
get() = proximityMessage.data[5]
val rawPodsBatteryHex: String
get() = rawPodsBattery.toHex()
val rawFlags: UShort
get() = proximityMessage.data[5].upperNibble
val rawCaseBattery: UShort
get() = proximityMessage.data[5].lowerNibble
val rawCaseLidState: UByte
get() = proximityMessage.data[6]
@@ -14,17 +14,24 @@ import eu.darken.capod.pods.core.HasDualPods.Pod
interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
val microPhonePod: Pod
val primaryPod: Pod
get() = when (rawStatus.isBitSet(5)) {
true -> Pod.LEFT
false -> Pod.RIGHT
}
/**
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
* If the right pod is the primary, the values are flipped.
*/
val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
val value = when (microPhonePod) {
Pod.LEFT -> rawPodsBattery.lowerNibble.toInt()
Pod.RIGHT -> rawPodsBattery.upperNibble.toInt()
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
@@ -39,9 +46,9 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
override val batteryRightPodPercent: Float?
get() {
val value = when (microPhonePod) {
Pod.LEFT -> rawPodsBattery.upperNibble.toInt()
Pod.RIGHT -> rawPodsBattery.lowerNibble.toInt()
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
@@ -53,32 +60,56 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
}
}
}
val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
val isOnePodInCase: Boolean
get() = rawStatus.isBitSet(4)
val areBothPodsInCase: Boolean
get() = rawStatus.isBitSet(2)
override val isLeftPodInEar: Boolean
get() = when (microPhonePod) {
Pod.LEFT -> rawStatus.isBitSet(1)
Pod.RIGHT -> rawStatus.isBitSet(3)
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(3)
false -> rawStatus.isBitSet(1)
}
override val isRightPodInEar: Boolean
get() = when (microPhonePod) {
Pod.LEFT -> rawStatus.isBitSet(3)
Pod.RIGHT -> rawStatus.isBitSet(1)
get() = when (areValuesFlipped xor isThisPodInThecase) {
true -> rawStatus.isBitSet(1)
false -> rawStatus.isBitSet(3)
}
/**
* The data flip bit is set if the left pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
val isLeftPodMicrophone: Boolean
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
/**
* The data flip bit is UNset if the right pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
val isRightPodMicrophone: Boolean
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
val isLeftPodCharging: Boolean
get() = when (microPhonePod) {
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0)
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(1)
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(0)
true -> rawFlags.isBitSet(1)
}
val isRightPodCharging: Boolean
get() = when (microPhonePod) {
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(1)
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0)
get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(1)
true -> rawFlags.isBitSet(0)
}
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
get() = when (val value = rawCaseBattery.toInt()) {
15 -> null
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
@@ -89,7 +120,7 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
}
override val isCaseCharging: Boolean
get() = rawCaseBattery.upperNibble.isBitSet(2)
get() = rawFlags.isBitSet(2)
val caseLidState: LidState
get() {
@@ -1,7 +1,6 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.HasSinglePod
@@ -11,7 +10,7 @@ interface SingleApplePods : BasicSingleApplePods, HasEarDetection, HasSinglePod
get() = rawStatus.isBitSet(1)
val isHeadsetBeingCharged: Boolean
get() = rawCaseBattery.upperNibble.isBitSet(0)
get() = rawFlags.isBitSet(0)
override val isBeingWorn: Boolean
get() = isHeadphonesBeingWorn
@@ -19,7 +19,8 @@ object ProximityPairing {
val vendor: UByte,
val length: UByte,
val device: UShort,
val batteryData: Set<UByte>,
val podBatteryData: Set<UShort>,
val caseBatteryData: UShort,
val deviceColor: UByte,
)
@@ -28,7 +29,8 @@ object ProximityPairing {
length = PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
// Make comparison order independent
batteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
caseBatteryData = data[5].lowerNibble,
deviceColor = data[7]
)
@@ -13,7 +13,8 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0520.toUShort()
rawStatus shouldBe 0x00.toUByte()
rawPodsBattery shouldBe 0xF5.toUByte()
rawCaseBattery shouldBe 0x0F.toUByte()
rawFlags shouldBe 0x0.toUShort()
rawCaseBattery shouldBe 0xF.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x01.toUByte()
rawSuffix shouldBe 0x00.toUByte()
@@ -1,6 +1,5 @@
package eu.darken.capod.pods.core.apple
import eu.darken.capod.pods.core.HasDualPods
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
@@ -15,7 +14,8 @@ class DualApplePodsTest : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0e20.toUShort()
rawStatus shouldBe 0x54.toUByte()
rawPodsBattery shouldBe 0xAA.toUByte()
rawCaseBattery shouldBe 0xB5.toUByte()
rawFlags shouldBe 0xB.toUShort()
rawCaseBattery shouldBe 0x5.toUShort()
rawCaseLidState shouldBe 0x31.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
@@ -27,12 +27,14 @@ class DualApplePodsTest : BaseAirPodsTest() {
create<DualApplePods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00101011
// --^-----
microPhonePod shouldBe HasDualPods.Pod.LEFT
isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false
}
create<DualApplePods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00001011
// --^-----
microPhonePod shouldBe HasDualPods.Pod.RIGHT
isLeftPodMicrophone shouldBe false
isRightPodMicrophone shouldBe true
}
}
@@ -14,7 +14,8 @@ class SingleApplePodsTest : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0A20.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
@@ -16,7 +16,8 @@ class AirPodsGen1Test : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0220.toUShort()
rawStatus shouldBe 0x55.toUByte()
rawPodsBattery shouldBe 0xAF.toUByte()
rawCaseBattery shouldBe 0x56.toUByte()
rawFlags shouldBe 0x5.toUShort()
rawCaseBattery shouldBe 0x6.toUShort()
rawCaseLidState shouldBe 0x31.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
@@ -15,7 +15,8 @@ class AirPodsGen2Test : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0F20.toUShort()
rawStatus shouldBe 0x02.toUByte()
rawPodsBattery shouldBe 0xF9.toUByte()
rawCaseBattery shouldBe 0x8F.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0xF.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x05.toUByte()
@@ -15,7 +15,8 @@ class AirPodsGen3Test : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x1320.toUShort()
rawStatus shouldBe 0x75.toUByte()
rawPodsBattery shouldBe 0xAA.toUByte()
rawCaseBattery shouldBe 0xB9.toUByte()
rawFlags shouldBe 0xB.toUShort()
rawCaseBattery shouldBe 0x9.toUShort()
rawCaseLidState shouldBe 0x31.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x04.toUByte()
@@ -15,7 +15,8 @@ class AirPodsMaxTest : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0A20.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
@@ -36,7 +37,8 @@ class AirPodsMaxTest : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0A20.toUShort()
rawStatus shouldBe 0x02.toUByte()
rawPodsBattery shouldBe 0x05.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x04.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x44.toUByte()
@@ -1,6 +1,5 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.HasDualPods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe
@@ -17,12 +16,14 @@ class AirPodsProTest : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0e20.toUShort()
rawStatus shouldBe 0x54.toUByte()
rawPodsBattery shouldBe 0xAA.toUByte()
rawCaseBattery shouldBe 0xB5.toUByte()
rawFlags shouldBe 0xB.toUShort()
rawCaseBattery shouldBe 0x5.toUShort()
rawCaseLidState shouldBe 0x31.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
microPhonePod shouldBe HasDualPods.Pod.RIGHT
isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false
batteryLeftPodPercent shouldBe 1.0f
batteryRightPodPercent shouldBe 1.0f
@@ -39,7 +40,8 @@ class AirPodsProTest : BaseAirPodsTest() {
@Test
fun `test AirPods from my downstairs neighbour`() = runBlockingTest {
create<AirPodsPro>("07 19 01 0E 20 00 F3 8F 02 00 04 79 C6 3F F9 C3 15 D9 11 A1 3C B1 58 66 B9 8B 67") {
microPhonePod shouldBe HasDualPods.Pod.RIGHT
isLeftPodMicrophone shouldBe false
isRightPodMicrophone shouldBe true
batteryLeftPodPercent shouldBe null
batteryRightPodPercent shouldBe 0.3f
@@ -15,7 +15,8 @@ class BeatsFlexText : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x1020.toUShort()
rawStatus shouldBe 0x0A.toUByte()
rawPodsBattery shouldBe 0xF4.toUByte()
rawCaseBattery shouldBe 0x8F.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0xF.toUShort()
rawCaseLidState shouldBe 0x00.toUByte()
rawDeviceColor shouldBe 0x01.toUByte()
rawSuffix shouldBe 0x00.toUByte()
@@ -15,7 +15,8 @@ class BeatsSolo3Test : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0620.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
@@ -15,7 +15,8 @@ class BeatsStudio3Test : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0920.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
@@ -15,7 +15,8 @@ class BeatsXTest : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0520.toUShort()
rawStatus shouldBe 0x00.toUByte()
rawPodsBattery shouldBe 0xF5.toUByte()
rawCaseBattery shouldBe 0x0F.toUByte()
rawFlags shouldBe 0x0.toUShort()
rawCaseBattery shouldBe 0xF.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x01.toUByte()
rawSuffix shouldBe 0x00.toUByte()
@@ -15,7 +15,8 @@ class PowerBeats3Test : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0320.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
@@ -1,6 +1,5 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.HasDualPods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe
@@ -17,12 +16,14 @@ class PowerBeatsProTest : BaseAirPodsTest() {
rawDeviceModel shouldBe 0x0B20.toUShort()
rawStatus shouldBe 0x54.toUByte()
rawPodsBattery shouldBe 0xAA.toUByte()
rawCaseBattery shouldBe 0xB5.toUByte()
rawFlags shouldBe 0xB.toUShort()
rawCaseBattery shouldBe 0x5.toUShort()
rawCaseLidState shouldBe 0x31.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
microPhonePod shouldBe HasDualPods.Pod.RIGHT
isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false
batteryLeftPodPercent shouldBe 1.0f
batteryRightPodPercent shouldBe 1.0f