Support for more pods + tests

This commit is contained in:
darken
2022-01-03 23:02:54 +01:00
parent e23304d8d7
commit 840c1c1a50
46 changed files with 615 additions and 398 deletions
@@ -1,70 +1,15 @@
package eu.darken.capod.pods.core.airpods
package eu.darken.capod.pods.core.apple
import android.bluetooth.BluetoothDevice
import android.bluetooth.le.ScanRecord
import android.bluetooth.le.ScanResult
import eu.darken.capod.pods.core.DualPods
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.airpods.models.AirPodsGen1
import eu.darken.capod.pods.core.airpods.models.AirPodsPro
import eu.darken.capod.pods.core.airpods.models.UnknownAppleDevice
import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.instanceOf
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import testhelper.BaseTest
class AirPodsFactoryTest : BaseTest() {
@MockK lateinit var scanResult: ScanResult
@MockK lateinit var scanRecord: ScanRecord
@MockK lateinit var device: BluetoothDevice
val factory = AirPodsFactory(
proximityPairingDecoder = ProximityPairing.Decoder(),
continuityProtocolDecoder = ContinuityProtocol.Decoder(),
)
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { scanResult.scanRecord } returns scanRecord
every { scanResult.rssi } returns -66
every { scanResult.timestampNanos } returns 136136027721826
every { scanResult.device } returns device
every { device.address } returns "77:49:4C:D8:25:0C"
}
private suspend inline fun <reified T : PodDevice?> create(hex: String, block: T.() -> Unit) {
val trimmed = hex
.replace(" ", "")
.replace(">", "")
.replace("<", "")
require(trimmed.length % 2 == 0) { "Not a HEX string" }
val bytes = trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
mockData(bytes)
block.invoke(factory.create(scanResult) as T)
}
private fun mockData(hex: String) {
val trimmed = hex
.replace(" ", "")
.replace(">", "")
.replace("<", "")
require(trimmed.length % 2 == 0) { "Not a HEX string" }
val bytes = trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
return mockData(bytes)
}
private fun mockData(data: ByteArray) {
every { scanRecord.getManufacturerSpecificData(ContinuityProtocol.APPLE_COMPANY_IDENTIFIER) } returns data
}
class AirPodsFactoryTest : BaseAirPodsTest() {
@Test
fun `test AirPodDevice - active microphone`() = runBlockingTest {
@@ -1,14 +1,16 @@
package eu.darken.capod.pods.core.airpods
package eu.darken.capod.pods.core.apple
import android.bluetooth.BluetoothDevice
import android.bluetooth.le.ScanRecord
import android.bluetooth.le.ScanResult
import eu.darken.capod.common.SystemClockWrap
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol
import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockkObject
import org.junit.jupiter.api.BeforeEach
import testhelper.BaseTest
@@ -18,7 +20,7 @@ abstract class BaseAirPodsTest : BaseTest() {
@MockK lateinit var scanRecord: ScanRecord
@MockK lateinit var device: BluetoothDevice
val factory = AirPodsFactory(
val factory = AppleFactory(
proximityPairingDecoder = ProximityPairing.Decoder(),
continuityProtocolDecoder = ContinuityProtocol.Decoder(),
)
@@ -31,6 +33,9 @@ abstract class BaseAirPodsTest : BaseTest() {
every { scanResult.timestampNanos } returns 136136027721826
every { scanResult.device } returns device
every { device.address } returns "77:49:4C:D8:25:0C"
mockkObject(SystemClockWrap)
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
}
suspend inline fun <reified T : PodDevice?> create(hex: String, block: T.() -> Unit) {
@@ -1,7 +1,7 @@
package eu.darken.capod.pods.core.airpods.models
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.airpods.BaseAirPodsTest
import eu.darken.capod.pods.core.airpods.DualApplePods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
@@ -1,7 +1,7 @@
package eu.darken.capod.pods.core.airpods.models
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.airpods.BaseAirPodsTest
import eu.darken.capod.pods.core.airpods.DualApplePods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
@@ -0,0 +1,55 @@
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class AirPodsMaxTest : BaseAirPodsTest() {
// Test data from https://github.com/adolfintel/OpenPods/issues/124
@Test
fun `default AirPods Max`() = runBlockingTest {
create<AirPodsMax>("07 19 01 0A 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0A20.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
batteryHeadsetPercent shouldBe 0.4f
isCaseCharging shouldBe false
isHeadsetBeingCharged shouldBe false
isHeadsetBeingWorn shouldBe true
batteryCasePercent shouldBe 0.0f
}
}
// Test data from https://github.com/adolfintel/OpenPods/issues/124
@Test
fun `default AirPods Max flipped values`() = runBlockingTest {
create<AirPodsMax>("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0A20.toUShort()
rawStatus shouldBe 0x02.toUByte()
rawPodsBattery shouldBe 0x05.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawCaseLidState shouldBe 0x04.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x44.toUByte()
batteryHeadsetPercent shouldBe 0.5f
isCaseCharging shouldBe false
isHeadsetBeingCharged shouldBe false
isHeadsetBeingWorn shouldBe true
batteryCasePercent shouldBe 0.0f
}
}
}
@@ -1,8 +1,8 @@
package eu.darken.capod.pods.core.airpods.models
package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.DualPods
import eu.darken.capod.pods.core.airpods.BaseAirPodsTest
import eu.darken.capod.pods.core.airpods.DualApplePods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
@@ -0,0 +1,26 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class BeatsFlexText : BaseAirPodsTest() {
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
@Test
fun `default BeatsFlex`() = runBlockingTest {
create<BeatsFlex>("07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x1020.toUShort()
rawStatus shouldBe 0x0A.toUByte()
rawPodsBattery shouldBe 0xF4.toUByte()
rawCaseBattery shouldBe 0x8F.toUByte()
rawCaseLidState shouldBe 0x00.toUByte()
rawDeviceColor shouldBe 0x01.toUByte()
rawSuffix shouldBe 0x00.toUByte()
batteryHeadsetPercent shouldBe 0.4f
}
}
}
@@ -0,0 +1,26 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class BeatsSolo3Test : BaseAirPodsTest() {
// TODO This is handcrafted data, get actual data for tests
@Test
fun `default BeatsSolo3`() = runBlockingTest {
create<BeatsSolo3>("07 19 01 06 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0620.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
batteryHeadsetPercent shouldBe 0.4f
}
}
}
@@ -0,0 +1,26 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class BeatsStudio3Test : BaseAirPodsTest() {
// TODO This is handcrafted data, get actual data for tests
@Test
fun `default BeatsStudio3`() = runBlockingTest {
create<BeatsStudio3>("07 19 01 09 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0920.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
batteryHeadsetPercent shouldBe 0.4f
}
}
}
@@ -0,0 +1,26 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class BeatsXTest : BaseAirPodsTest() {
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
@Test
fun `default BeatsX`() = runBlockingTest {
create<BeatsX>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0520.toUShort()
rawStatus shouldBe 0x00.toUByte()
rawPodsBattery shouldBe 0xF5.toUByte()
rawCaseBattery shouldBe 0x0F.toUByte()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x01.toUByte()
rawSuffix shouldBe 0x00.toUByte()
batteryHeadsetPercent shouldBe 0.5f
}
}
}
@@ -0,0 +1,26 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class PowerBeats3Test : BaseAirPodsTest() {
// TODO This is handcrafted data, get actual data for tests
@Test
fun `default PowerBeats3`() = runBlockingTest {
create<PowerBeats3>("07 19 01 03 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0320.toUShort()
rawStatus shouldBe 0x62.toUByte()
rawPodsBattery shouldBe 0x04.toUByte()
rawCaseBattery shouldBe 0x80.toUByte()
rawCaseLidState shouldBe 0x01.toUByte()
rawDeviceColor shouldBe 0x0F.toUByte()
rawSuffix shouldBe 0x40.toUByte()
batteryHeadsetPercent shouldBe 0.4f
}
}
}
@@ -0,0 +1,38 @@
package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.DualPods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class PowerBeatsProTest : BaseAirPodsTest() {
// TODO This is handcrafted data, get actual data for tests
@Test
fun `test PowerBeatsPro`() = runBlockingTest {
create<PowerBeatsPro>("07 19 01 0B 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0B20.toUShort()
rawStatus shouldBe 0x54.toUByte()
rawPodsBattery shouldBe 0xAA.toUByte()
rawCaseBattery shouldBe 0xB5.toUByte()
rawCaseLidState shouldBe 0x31.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x00.toUByte()
microPhonePod shouldBe DualPods.Pod.RIGHT
batteryLeftPodPercent shouldBe 1.0f
batteryRightPodPercent shouldBe 1.0f
isCaseCharging shouldBe false
isRightPodCharging shouldBe true
isLeftPodCharging shouldBe true
batteryCasePercent shouldBe 0.5f
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
}
}
}