mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
Merge pull request #328 from d4rken-org/airpods-pro-3-merged
AirPods Pro 3 support
This commit is contained in:
@@ -32,6 +32,7 @@ Currently supported models:
|
|||||||
* AirPods Pro 1. Generation
|
* AirPods Pro 1. Generation
|
||||||
* AirPods Pro 2. Generation
|
* AirPods Pro 2. Generation
|
||||||
* AirPods Pro 2. Generation (USB-C)
|
* AirPods Pro 2. Generation (USB-C)
|
||||||
|
* AirPods Pro 3. Generation
|
||||||
* AirPods Max
|
* AirPods Max
|
||||||
* Power Beats Pro
|
* Power Beats Pro
|
||||||
* Power Beats 3
|
* Power Beats 3
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ interface PodDevice {
|
|||||||
"AirPods Pro 2 USB-C",
|
"AirPods Pro 2 USB-C",
|
||||||
R.drawable.devic_airpods_pro2_both
|
R.drawable.devic_airpods_pro2_both
|
||||||
),
|
),
|
||||||
|
@Json(name = "airpods.pro3") AIRPODS_PRO3(
|
||||||
|
"AirPods Pro 3",
|
||||||
|
R.drawable.devic_airpods_pro2_both
|
||||||
|
),
|
||||||
@Json(name = "airpods.max") AIRPODS_MAX(
|
@Json(name = "airpods.max") AIRPODS_MAX(
|
||||||
"AirPods Max",
|
"AirPods Max",
|
||||||
R.drawable.devic_headphones_generic
|
R.drawable.devic_headphones_generic
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import eu.darken.capod.pods.core.apple.airpods.AirPodsMaxUsbc
|
|||||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro2
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro2
|
||||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro2Usbc
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro2Usbc
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro3
|
||||||
import eu.darken.capod.pods.core.apple.beats.BeatsFitPro
|
import eu.darken.capod.pods.core.apple.beats.BeatsFitPro
|
||||||
import eu.darken.capod.pods.core.apple.beats.BeatsFlex
|
import eu.darken.capod.pods.core.apple.beats.BeatsFlex
|
||||||
import eu.darken.capod.pods.core.apple.beats.BeatsSolo3
|
import eu.darken.capod.pods.core.apple.beats.BeatsSolo3
|
||||||
@@ -43,6 +44,7 @@ abstract class AppleFactoryModule {
|
|||||||
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory
|
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory
|
||||||
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory
|
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory
|
||||||
@Binds @IntoSet abstract fun airPodsPro2Usbc(factory: AirPodsPro2Usbc.Factory): ApplePodsFactory
|
@Binds @IntoSet abstract fun airPodsPro2Usbc(factory: AirPodsPro2Usbc.Factory): ApplePodsFactory
|
||||||
|
@Binds @IntoSet abstract fun airPodsPro3(factory: AirPodsPro3.Factory): ApplePodsFactory
|
||||||
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory
|
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory
|
||||||
@Binds @IntoSet abstract fun airPodsMax2(factory: AirPodsMaxUsbc.Factory): ApplePodsFactory
|
@Binds @IntoSet abstract fun airPodsMax2(factory: AirPodsMaxUsbc.Factory): ApplePodsFactory
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
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.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
|
import eu.darken.capod.pods.core.apple.history.PodHistoryRepo
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class AirPodsPro3(
|
||||||
|
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 payload: ProximityPayload,
|
||||||
|
override val flags: ApplePods.Flags,
|
||||||
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: LidState? = null
|
||||||
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO3
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
|
override val caseLidState: LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor(
|
||||||
|
private val repo: PodHistoryRepo,
|
||||||
|
) : ApplePodsFactory {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(
|
||||||
|
scanResult: BleScanResult,
|
||||||
|
payload: ProximityPayload,
|
||||||
|
flags: ApplePods.Flags
|
||||||
|
): ApplePods {
|
||||||
|
var basic = AirPodsPro3(scanResult = scanResult, payload = payload, flags = flags)
|
||||||
|
val result = repo.search(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
repo.updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
reliability = result.reliability,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Device code for AirPods Pro 3
|
||||||
|
// Following the pattern of other AirPods models (ends with 0x20 for Apple vendor)
|
||||||
|
private val DEVICE_CODE = 0x2720.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro3")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
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 AirPodsPro3Test : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test case for AirPods Pro 3 - placeholder with correct device code
|
||||||
|
* Following the pattern of other AirPods models (0x2720)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `AirPods Pro 3 - placeholder test`() = runTest {
|
||||||
|
// This test uses a placeholder hex string with the correct device code 0x2720
|
||||||
|
// The actual proximity pairing data will need to be captured from real AirPods Pro 3
|
||||||
|
create<AirPodsPro3>("07 19 01 27 20 0B 99 8F 11 00 04 BD A7 3B FF 2D 8A 3C AF 9B 1A 7C 74 B7 A9 D1 C3") {
|
||||||
|
|
||||||
|
pubPrefix shouldBe 0x01.toUByte()
|
||||||
|
pubDeviceModel shouldBe 0x2720.toUShort()
|
||||||
|
pubStatus shouldBe 0x0B.toUByte()
|
||||||
|
pubPodsBattery shouldBe 0x99.toUByte()
|
||||||
|
pubFlags shouldBe 0x8.toUShort()
|
||||||
|
pubCaseBattery shouldBe 0xF.toUShort()
|
||||||
|
pubCaseLidState shouldBe 0x11.toUByte()
|
||||||
|
pubDeviceColor shouldBe 0x0.toUByte()
|
||||||
|
pubSuffix shouldBe 0x04.toUByte()
|
||||||
|
|
||||||
|
isLeftPodMicrophone shouldBe false
|
||||||
|
isRightPodMicrophone shouldBe true
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe true
|
||||||
|
isRightPodInEar shouldBe true
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 0.9f
|
||||||
|
batteryRightPodPercent shouldBe 0.9f
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
isRightPodCharging shouldBe false
|
||||||
|
isLeftPodCharging shouldBe false
|
||||||
|
batteryCasePercent shouldBe null
|
||||||
|
|
||||||
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_PRO3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AirPods Pro 3 - placeholder test - in case`() = runTest {
|
||||||
|
// This test uses a placeholder hex string with the correct device code 0x2720
|
||||||
|
// The actual proximity pairing data will need to be captured from real AirPods Pro 3
|
||||||
|
create<AirPodsPro3>("07 19 01 27 20 53 AA 98 32 00 05 49 0A B8 BF 8E 29 D8 70 12 0D A7 0C CE 77 56 00") {
|
||||||
|
|
||||||
|
isLeftPodMicrophone shouldBe true
|
||||||
|
isRightPodMicrophone shouldBe false
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe true
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1f
|
||||||
|
batteryRightPodPercent shouldBe 1f
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
isLeftPodCharging shouldBe false
|
||||||
|
batteryCasePercent shouldBe 0.8f
|
||||||
|
|
||||||
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_PRO3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AirPods Pro 3`() = runTest {
|
||||||
|
create<AirPodsPro3>("07 19 01 27 20 0B 99 8F 11 00 05 43 A8 85 17 07 FF 41 62 2C FE 5E 20 08 1A 52 40") {
|
||||||
|
isLeftPodInEar shouldBe true
|
||||||
|
isRightPodInEar shouldBe true
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
isRightPodCharging shouldBe false
|
||||||
|
isLeftPodCharging shouldBe false
|
||||||
|
batteryCasePercent shouldBe null
|
||||||
|
|
||||||
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_PRO3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user