Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Currently supported models:
* Beats Studio Buds
* Beats Studio Buds+
* Beats Studio Pro
* Beats 360
* Beats X
* Beats Flex
* Beats Fit Pro
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,14 @@ enum class PodModel(
modelNumbers = setOf("A2924"), // headphones
),

@SerialName("beats.360")
BEATS_360(
"Beats 360",
R.drawable.device_beats_headphones,
Features(hasAncControl = true),
modelNumbers = setOf("A3577"), // headphones
),

@SerialName("beats.x")
BEATS_X(
"Beats X",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import eu.darken.capod.pods.core.apple.ble.devices.airpods.AirPodsPro
import eu.darken.capod.pods.core.apple.ble.devices.airpods.AirPodsPro2
import eu.darken.capod.pods.core.apple.ble.devices.airpods.AirPodsPro2Usbc
import eu.darken.capod.pods.core.apple.ble.devices.airpods.AirPodsPro3
import eu.darken.capod.pods.core.apple.ble.devices.beats.Beats360
import eu.darken.capod.pods.core.apple.ble.devices.beats.BeatsFitPro
import eu.darken.capod.pods.core.apple.ble.devices.beats.BeatsFlex
import eu.darken.capod.pods.core.apple.ble.devices.beats.BeatsSolo3
Expand Down Expand Up @@ -76,6 +77,7 @@ abstract class AppleFactoryModule {
@Binds @IntoSet abstract fun beatsStudioBuds(factory: BeatsStudioBuds.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun beatsStudioBudsPlus(factory: BeatsStudioBudsPlus.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun beatsStudioPro(factory: BeatsStudioPro.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun beats360(factory: Beats360.Factory): ApplePodsFactory

@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory
@Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats

import eu.darken.capod.common.SystemTimeSource
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.ble.devices.ApplePods
import eu.darken.capod.pods.core.apple.ble.devices.ApplePodsFactory
import eu.darken.capod.pods.core.apple.ble.devices.SingleApplePods
import eu.darken.capod.pods.core.apple.ble.history.PodHistoryRepo
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.ble.protocol.ProximityPayload
import java.time.Instant
import javax.inject.Inject

data class Beats360(
override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id(),
override val seenLastAt: Instant = SystemTimeSource.now(),
override val seenFirstAt: Instant = SystemTimeSource.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val payload: ProximityPayload,
override val meta: ApplePods.AppleMeta,
override val reliability: Float = BlePodSnapshot.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
) : SingleApplePods {

override val model: PodModel = PodModel.BEATS_360
override val rssi: Int
get() = rssiAverage ?: super.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 suspend fun create(
scanResult: BleScanResult,
payload: ProximityPayload,
meta: ApplePods.AppleMeta
): ApplePods {
var basic = Beats360(scanResult = scanResult, payload = payload, meta = meta)
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,
rssiAverage = result.rssiSmoothed(basic.rssi),
)
}
}

companion object {
private val DEVICE_CODE = 0x3820.toUShort()
private val TAG = logTag("PodDevice", "Beats", "360")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ModelFeaturesTest : BaseTest() {
PodModel.BEATS_STUDIO_BUDS,
PodModel.BEATS_STUDIO_BUDS_PLUS,
PodModel.BEATS_STUDIO_PRO,
PodModel.BEATS_360,
PodModel.BEATS_X,
PodModel.POWERBEATS_3,
PodModel.POWERBEATS_4,
Expand Down Expand Up @@ -265,6 +266,7 @@ class ModelFeaturesTest : BaseTest() {
PodModel.BEATS_STUDIO_BUDS,
PodModel.BEATS_STUDIO_BUDS_PLUS,
PodModel.BEATS_STUDIO_PRO,
PodModel.BEATS_360,
PodModel.POWERBEATS_PRO2,
PodModel.BEATS_FIT_PRO,
PodModel.FAKE_AIRPODS_PRO,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package eu.darken.capod.pods.core.apple.ble.devices.beats

import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.ble.devices.BaseBlePodsTest
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test

class Beats360Test : BaseBlePodsTest() {

@Test
fun `default Beats360`() = runTest {
create<Beats360>("07 19 01 38 20 00 07 80 04 02 00 01 7D B1 76 1F 5C D6 93 D7 8B 71 77 0A 81 7B AD") {
pubDeviceModel shouldBe 0x3820.toUShort()
batteryHeadsetPercent shouldBe 0.7f
model shouldBe PodModel.BEATS_360
}
}

@Test
fun `model number maps to Beats 360`() {
PodModel.fromModelNumber("A3577") shouldBe PodModel.BEATS_360
}
}