From 46611b5606bb5a7d9ca1df0a8ccb046fa259a8f6 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn Date: Sat, 14 Jan 2023 16:35:04 +0100 Subject: [PATCH] Add preliminary support for Beats Fit Pro (#75) --- .../eu/darken/capod/pods/core/PodDevice.kt | 3 + .../pods/core/apple/AppleFactoryModule.kt | 1 + .../pods/core/apple/beats/BeatsFitPro.kt | 70 +++++++++++++++++++ .../pods/core/apple/beats/BeatsFitProTest.kt | 43 ++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 app-common/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFitPro.kt create mode 100644 app-common/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsFitProTest.kt diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt b/app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt index fef9b6d9..69a51d42 100644 --- a/app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt +++ b/app-common/src/main/java/eu/darken/capod/pods/core/PodDevice.kt @@ -104,6 +104,9 @@ interface PodDevice { @Json(name = "beats.powerbeats.pro") POWERBEATS_PRO( "Power Beats Pro" ), + @Json(name = "beats.fit.pro") BEATS_FIT_PRO( + "Beats Fit Pro" + ), @Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1( "AirPods (Gen 1)? \uD83C\uDFAD", R.drawable.devic_airpods_gen1_both, diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt b/app-common/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt index dd447e51..ab5ea20f 100644 --- a/app-common/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt +++ b/app-common/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt @@ -27,6 +27,7 @@ abstract class AppleFactoryModule { @Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory @Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory @Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory @Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory @Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFitPro.kt b/app-common/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFitPro.kt new file mode 100644 index 00000000..14e102dd --- /dev/null +++ b/app-common/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFitPro.kt @@ -0,0 +1,70 @@ +package eu.darken.capod.pods.core.apple.beats + +import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods +import eu.darken.capod.pods.core.apple.DualAirPods +import eu.darken.capod.pods.core.apple.DualApplePodsFactory +import eu.darken.capod.pods.core.apple.protocol.ProximityPairing +import java.time.Instant +import javax.inject.Inject + +data class BeatsFitPro( + override val identifier: PodDevice.Id = PodDevice.Id(), + override val seenLastAt: Instant = Instant.now(), + override val seenFirstAt: Instant = Instant.now(), + override val seenCounter: Int = 1, + override val scanResult: BleScanResult, + override val proximityMessage: ProximityPairing.Message, + override val confidence: Float = PodDevice.BASE_CONFIDENCE, + private val rssiAverage: Int? = null, + private val cachedBatteryPercentage: Float? = null, + private val cachedCaseState: DualAirPods.LidState? = null +) : DualAirPods { + + override val model: PodDevice.Model = PodDevice.Model.BEATS_FIT_PRO + + override val batteryCasePercent: Float? + get() = super.batteryCasePercent ?: cachedBatteryPercentage + + override val caseLidState: DualAirPods.LidState + get() = cachedCaseState ?: super.caseLidState + + override val rssi: Int + get() = rssiAverage ?: super.rssi + + class Factory @Inject constructor() : DualApplePodsFactory(TAG) { + + override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { + getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH + } + + override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods { + var basic = BeatsFitPro(scanResult = scanResult, proximityMessage = message) + val result = searchHistory(basic) + + if (result != null) basic = basic.copy(identifier = result.id) + updateHistory(basic) + + if (result == null) return basic + + return basic.copy( + identifier = result.id, + seenFirstAt = result.seenFirstAt, + seenLastAt = scanResult.receivedAt, + seenCounter = result.seenCounter, + confidence = result.confidence, + cachedBatteryPercentage = result.getLatestCaseBattery(), + rssiAverage = result.averageRssi(basic.rssi), + cachedCaseState = result.getLatestCaseLidState(basic) + ) + } + + } + + companion object { + private val DEVICE_CODE = 0x1220.toUShort() + private val TAG = logTag("PodDevice", "Beats", "Fit", "Pro") + } +} \ No newline at end of file diff --git a/app-common/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsFitProTest.kt b/app-common/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsFitProTest.kt new file mode 100644 index 00000000..ffee6a3c --- /dev/null +++ b/app-common/src/test/java/eu/darken/capod/pods/core/apple/beats/BeatsFitProTest.kt @@ -0,0 +1,43 @@ +package eu.darken.capod.pods.core.apple.beats + +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.BaseAirPodsTest +import eu.darken.capod.pods.core.apple.HasAppleColor +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +class BeatsFitProTest : BaseAirPodsTest() { + + /** + * From https://github.com/d4rken-org/capod/issues/33#issuecomment-1256235651 + */ + @Test + fun `test basics`() = runTest { + create("07 19 01 12 20 20 FA 8F 01 11 24 9B 9B 23 52 60 5A 8C 32 1C A5 C2 81 51 82 AF C8") { + rawPrefix shouldBe 0x01.toUByte() + rawDeviceModel shouldBe 0x1220.toUShort() + rawStatus shouldBe 0x20.toUByte() + rawPodsBattery shouldBe 0xFA.toUByte() + rawFlags shouldBe 0x8.toUShort() + rawCaseBattery shouldBe 0xF.toUShort() + rawCaseLidState shouldBe 0x01.toUByte() + rawDeviceColor shouldBe 0x11.toUByte() + rawSuffix shouldBe 0x24.toUByte() + + isLeftPodMicrophone shouldBe true + isRightPodMicrophone shouldBe false + + batteryLeftPodPercent shouldBe 1.0f + batteryRightPodPercent shouldBe null + + isCaseCharging shouldBe false + isRightPodCharging shouldBe false + isLeftPodCharging shouldBe false + batteryCasePercent shouldBe null + podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name + + model shouldBe PodDevice.Model.BEATS_FIT_PRO + } + } +} \ No newline at end of file