mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 19:26:12 -04:00
Merge pull request #227 from d4rken-org/airpods-gen4
Add support for AirPods (Gen 4 - no ANC)
This commit is contained in:
@@ -83,6 +83,10 @@ interface PodDevice {
|
|||||||
"AirPods (Gen 3)",
|
"AirPods (Gen 3)",
|
||||||
R.drawable.devic_airpods_gen2_both,
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
|
@Json(name = "airpods.gen4") AIRPODS_GEN4(
|
||||||
|
"AirPods (Gen 4)",
|
||||||
|
R.drawable.devic_airpods_gen2_both,
|
||||||
|
),
|
||||||
@Json(name = "airpods.pro") AIRPODS_PRO(
|
@Json(name = "airpods.pro") AIRPODS_PRO(
|
||||||
"AirPods Pro",
|
"AirPods Pro",
|
||||||
R.drawable.devic_airpods_pro2_both
|
R.drawable.devic_airpods_pro2_both
|
||||||
|
|||||||
@@ -5,9 +5,27 @@ import dagger.Module
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import eu.darken.capod.pods.core.apple.airpods.*
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
|
||||||
import eu.darken.capod.pods.core.apple.beats.*
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen2
|
||||||
import eu.darken.capod.pods.core.apple.misc.*
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen3
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen4
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
|
||||||
|
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.AirPodsPro2Usbc
|
||||||
|
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.BeatsSolo3
|
||||||
|
import eu.darken.capod.pods.core.apple.beats.BeatsStudio3
|
||||||
|
import eu.darken.capod.pods.core.apple.beats.BeatsX
|
||||||
|
import eu.darken.capod.pods.core.apple.beats.PowerBeats3
|
||||||
|
import eu.darken.capod.pods.core.apple.beats.PowerBeats4
|
||||||
|
import eu.darken.capod.pods.core.apple.beats.PowerBeatsPro
|
||||||
|
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen1
|
||||||
|
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen2
|
||||||
|
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen3
|
||||||
|
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro
|
||||||
|
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro2
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@Module
|
@Module
|
||||||
@@ -16,6 +34,7 @@ abstract class AppleFactoryModule {
|
|||||||
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun airPodsGen4(factory: AirPodsGen4.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun airPodsPro2(factory: AirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
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.DualApplePods
|
||||||
|
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 AirPodsGen4(
|
||||||
|
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 reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN4
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super<DualApplePods>.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 = AirPodsGen4(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,
|
||||||
|
reliability = result.reliability,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x1920.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Gen4")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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.DualApplePods
|
||||||
|
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 AirPodsGen4Test : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AirPods Gen4 via log from #225`() = runTest {
|
||||||
|
create<AirPodsGen4>("07 19 01 19 20 2B 33 8F 11 00 04 59 D4 57 20 0F 1C 13 38 B2 00 74 E9 DD 70 D7 A5") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x1920.toUShort()
|
||||||
|
rawStatus shouldBe 0x2B.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0x33.toUByte()
|
||||||
|
rawFlags shouldBe 0x8.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0xF.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x11.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
|
rawSuffix shouldBe 0x04.toUByte()
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 0.3f
|
||||||
|
batteryRightPodPercent shouldBe 0.3f
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
isLeftPodCharging shouldBe false
|
||||||
|
isRightPodCharging shouldBe false
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe true
|
||||||
|
isRightPodInEar shouldBe true
|
||||||
|
batteryCasePercent shouldBe null
|
||||||
|
|
||||||
|
caseLidState shouldBe DualApplePods.LidState.UNKNOWN
|
||||||
|
|
||||||
|
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||||
|
|
||||||
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.AIRPODS_GEN4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user