mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Refactor proximity message handling, introduce "Payload" that splits the data in public and private part.
This commit is contained in:
@@ -19,7 +19,6 @@ import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodFactory
|
||||
import eu.darken.capod.monitor.core.RPAChecker
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -202,7 +201,7 @@ class PodMonitor @Inject constructor(
|
||||
private fun determineMainDevice(pods: List<PodDevice>): PodDevice? {
|
||||
generalSettings.mainDeviceIdentityKey.value
|
||||
.also { log(TAG) { "Identity-Resolving-Key (IRK): ${it?.toByteString()}" } }
|
||||
?.let { irkKey -> pods.firstOrNull { pod -> rpaChecker.verify(pod, irkKey) } }
|
||||
?.let { irkKey -> pods.firstOrNull { pod -> rpaChecker.verify(pod.address, irkKey) } }
|
||||
?.let {
|
||||
log(TAG) { "Main device determined via IRK: $it" }
|
||||
return it
|
||||
|
||||
@@ -5,7 +5,6 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
@@ -14,14 +13,14 @@ import javax.inject.Inject
|
||||
class RPAChecker @Inject constructor() {
|
||||
|
||||
// Resolvable-Private-Address
|
||||
fun verify(dev: PodDevice, irk: ByteArray): Boolean = try {
|
||||
val rpa = dev.address.split(":").map { it.toInt(16).toByte() }.reversed().toByteArray()
|
||||
fun verify(address: String, irk: ByteArray): Boolean = try {
|
||||
val rpa = address.split(":").map { it.toInt(16).toByte() }.reversed().toByteArray()
|
||||
val prand = rpa.copyOfRange(3, 6)
|
||||
val hash = rpa.copyOfRange(0, 3)
|
||||
val computedHash = ah(irk, prand)
|
||||
hash.contentEquals(computedHash)
|
||||
} catch (e: Exception) {
|
||||
log(TAG, ERROR) { "Failed to verify RPA\nPod=${dev}\nIRK=${irk.toByteString()}\n${e.asLog()}" }
|
||||
log(TAG, ERROR) { "Failed to verify RPA\naddress=${address}\nIRK=${irk.toByteString()}\n${e.asLog()}" }
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.monitor.core.RPAChecker
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.MessageDecrypter
|
||||
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 kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
@@ -20,15 +22,16 @@ import javax.inject.Singleton
|
||||
class AppleFactory @Inject constructor(
|
||||
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
||||
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
||||
private val proximityMessageDecrypter: ProximityMessage.Decrypter,
|
||||
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory<out ApplePods>>,
|
||||
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val messageDecrypter: MessageDecrypter,
|
||||
private val rpaChecker: RPAChecker,
|
||||
) {
|
||||
|
||||
private val lock = Mutex()
|
||||
|
||||
private fun getMessage(scanResult: BleScanResult): ProximityPairing.Message? {
|
||||
private fun getMessage(scanResult: BleScanResult): ProximityMessage? {
|
||||
val messages = try {
|
||||
continuityProtocolDecoder.decode(scanResult)
|
||||
} catch (e: Exception) {
|
||||
@@ -54,20 +57,40 @@ class AppleFactory @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
|
||||
val pm = getMessage(scanResult) ?: return@withLock null
|
||||
val proximityMessage = getMessage(scanResult) ?: return@withLock null
|
||||
|
||||
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
||||
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) }
|
||||
|
||||
val decryptedData = generalSettings.mainDeviceEncryptionKey.value
|
||||
?.let { messageDecrypter.decrypt(pm, it) }
|
||||
val payload = getPayload(scanResult, proximityMessage)
|
||||
|
||||
return@withLock (factory ?: unknownAppleFactory).create(
|
||||
scanResult = scanResult,
|
||||
message = pm,
|
||||
decrypted = decryptedData,
|
||||
payload = payload,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPayload(
|
||||
scanResult: BleScanResult,
|
||||
proximityMessage: ProximityMessage
|
||||
) = ProximityPayload(
|
||||
public = ProximityPayload.Public(
|
||||
proximityMessage.data.take(16).toUByteArray()
|
||||
),
|
||||
private = run {
|
||||
val irkKey = generalSettings.mainDeviceIdentityKey.value
|
||||
if (irkKey == null) return@run null
|
||||
val encKey = generalSettings.mainDeviceEncryptionKey.value
|
||||
if (encKey == null) return@run null
|
||||
|
||||
if (!rpaChecker.verify(scanResult.address, irkKey)) return@run null
|
||||
|
||||
val encrypted = proximityMessage.data.takeLast(16).toUByteArray().toByteArray()
|
||||
proximityMessageDecrypter.decrypt(encrypted, encKey)?.let {
|
||||
ProximityPayload.Private(data = it)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Pod", "Apple", "Factory")
|
||||
}
|
||||
|
||||
@@ -1,51 +1,39 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.toHex
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
|
||||
interface ApplePods : PodDevice {
|
||||
|
||||
val proximityMessage: ProximityPairing.Message
|
||||
|
||||
val decryptedPayload: UByteArray?
|
||||
get() = null
|
||||
val payload: ProximityPayload
|
||||
|
||||
// We start counting at the airpods prefix byte
|
||||
val rawPrefix: UByte
|
||||
get() = proximityMessage.data[0]
|
||||
val pubPrefix: UByte
|
||||
get() = payload.public.data[0]
|
||||
|
||||
val rawDeviceModel: UShort
|
||||
get() = (((proximityMessage.data[1].toInt() and 255) shl 8) or (proximityMessage.data[2].toInt() and 255)).toUShort()
|
||||
val pubDeviceModel: UShort
|
||||
get() = (((payload.public.data[1].toInt() and 255) shl 8) or (payload.public.data[2].toInt() and 255)).toUShort()
|
||||
|
||||
val rawStatus: UByte
|
||||
get() = proximityMessage.data[3]
|
||||
val pubStatus: UByte
|
||||
get() = payload.public.data[3]
|
||||
|
||||
val rawStatusHex: String
|
||||
get() = rawStatus.toHex()
|
||||
val pubPodsBattery: UByte
|
||||
get() = payload.public.data[4]
|
||||
|
||||
val rawPodsBattery: UByte
|
||||
get() = proximityMessage.data[4]
|
||||
val pubFlags: UShort
|
||||
get() = payload.public.data[5].upperNibble
|
||||
|
||||
val rawPodsBatteryHex: String
|
||||
get() = rawPodsBattery.toHex()
|
||||
val pubCaseBattery: UShort
|
||||
get() = payload.public.data[5].lowerNibble
|
||||
|
||||
val rawFlags: UShort
|
||||
get() = proximityMessage.data[5].upperNibble
|
||||
|
||||
val rawCaseBattery: UShort
|
||||
get() = proximityMessage.data[5].lowerNibble
|
||||
|
||||
val rawCaseLidState: UByte
|
||||
get() = proximityMessage.data[6]
|
||||
|
||||
val rawDeviceColor: UByte
|
||||
get() = proximityMessage.data[7]
|
||||
|
||||
val rawSuffix: UByte
|
||||
get() = proximityMessage.data[8]
|
||||
val pubCaseLidState: UByte
|
||||
get() = payload.public.data[6]
|
||||
|
||||
val pubDeviceColor: UByte
|
||||
get() = payload.public.data[7]
|
||||
|
||||
val pubSuffix: UByte
|
||||
get() = payload.public.data[8]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import android.R.id.message
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.collections.median
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
@@ -9,30 +10,27 @@ import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.max
|
||||
|
||||
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
|
||||
data class Markings(
|
||||
val vendor: UByte,
|
||||
val length: UByte,
|
||||
data class Identifier(
|
||||
val device: UShort,
|
||||
val podBatteryData: Set<UShort>,
|
||||
val caseBatteryData: UShort,
|
||||
val deviceColor: UByte,
|
||||
)
|
||||
|
||||
private fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
||||
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
||||
length = ProximityPairing.PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||
private fun ProximityPayload.getFuzzyIdentifier(): Identifier = Identifier(
|
||||
device = (((public.data[1].toInt() and 255) shl 8) or (public.data[2].toInt() and 255)).toUShort(),
|
||||
// Make comparison order independent
|
||||
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
||||
caseBatteryData = data[5].lowerNibble,
|
||||
deviceColor = data[7]
|
||||
podBatteryData = setOf(public.data[4].upperNibble, public.data[4].lowerNibble),
|
||||
caseBatteryData = public.data[5].lowerNibble,
|
||||
deviceColor = public.data[7]
|
||||
)
|
||||
|
||||
data class KnownDevice(
|
||||
@@ -42,8 +40,8 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
val history: List<ApplePods>,
|
||||
val lastCaseBattery: Float?,
|
||||
) {
|
||||
val lastMessage: ProximityPairing.Message
|
||||
get() = history.last().proximityMessage
|
||||
val lastPayload: ProximityPayload
|
||||
get() = history.last().payload
|
||||
|
||||
val lastAddress: String
|
||||
get() = history.last().address
|
||||
@@ -127,7 +125,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
|
||||
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||
val scanResult = current.scanResult
|
||||
val message = current.proximityMessage
|
||||
val payload = current.payload
|
||||
|
||||
knownDevices.values.toList().forEach { knownDevice ->
|
||||
if (knownDevice.isOlderThan(Duration.ofSeconds(30))) {
|
||||
@@ -148,9 +146,9 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
?.also { log(tag, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } }
|
||||
|
||||
if (recognizedDevice == null) {
|
||||
val currentMarkers = message.getApplePodsMarkings()
|
||||
val currentMarkers = payload.getFuzzyIdentifier()
|
||||
recognizedDevice = knownDevices.values
|
||||
.firstOrNull { it.lastMessage.getApplePodsMarkings() == currentMarkers }
|
||||
.firstOrNull { it.lastPayload.getFuzzyIdentifier() == currentMarkers }
|
||||
?.also { log(tag) { "searchHistory1: Close match based on similarity: $currentMarkers" } }
|
||||
}
|
||||
|
||||
@@ -173,6 +171,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
lastCaseBattery = history.determineLatestCaseBattery() ?: existing.lastCaseBattery
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
log(tag) { "searchHistory1: Creating new history for $device" }
|
||||
val history = listOf(device)
|
||||
@@ -192,16 +191,15 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
val dirty: UByte,
|
||||
)
|
||||
|
||||
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
||||
fun ProximityMessage.getModelInfo(): ModelInfo = ModelInfo(
|
||||
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||
dirty = data[1]
|
||||
)
|
||||
|
||||
abstract fun isResponsible(message: ProximityPairing.Message): Boolean
|
||||
abstract fun isResponsible(message: ProximityMessage): Boolean
|
||||
|
||||
abstract fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?,
|
||||
payload: ProximityPayload,
|
||||
): ApplePods
|
||||
}
|
||||
@@ -15,7 +15,7 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
|
||||
HasDualMicrophone, HasAppleColor {
|
||||
|
||||
val primaryPod: Pod
|
||||
get() = when (rawStatus.isBitSet(5)) {
|
||||
get() = when (pubStatus.isBitSet(5)) {
|
||||
true -> Pod.LEFT
|
||||
false -> Pod.RIGHT
|
||||
}
|
||||
@@ -25,17 +25,17 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
|
||||
* If the right pod is the primary, the values are flipped.
|
||||
*/
|
||||
val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
get() = !pubStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
decryptedPayload?.get(if (areValuesFlipped) 2 else 1)?.let { raw ->
|
||||
payload.private?.data?.get(if (areValuesFlipped) 2 else 1)?.let { raw ->
|
||||
val level = (raw and 0x7Fu).toInt() / 100f
|
||||
if (level <= 1.0f) return level
|
||||
}
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
true -> pubPodsBattery.upperNibble.toInt()
|
||||
false -> pubPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
@@ -50,13 +50,13 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
decryptedPayload?.get(if (areValuesFlipped) 1 else 2)?.let { raw ->
|
||||
payload.private?.data?.get(if (areValuesFlipped) 1 else 2)?.let { raw ->
|
||||
val level = (raw and 0x7Fu).toInt() / 100f
|
||||
if (level <= 1.0f) return level
|
||||
}
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
true -> pubPodsBattery.lowerNibble.toInt()
|
||||
false -> pubPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
@@ -70,24 +70,24 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
|
||||
}
|
||||
|
||||
val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
get() = pubStatus.isBitSet(6)
|
||||
|
||||
val isOnePodInCase: Boolean
|
||||
get() = rawStatus.isBitSet(4)
|
||||
get() = pubStatus.isBitSet(4)
|
||||
|
||||
val areBothPodsInCase: Boolean
|
||||
get() = rawStatus.isBitSet(2)
|
||||
get() = pubStatus.isBitSet(2)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
true -> pubStatus.isBitSet(3)
|
||||
false -> pubStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
true -> pubStatus.isBitSet(1)
|
||||
false -> pubStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,46 +95,46 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isLeftPodMicrophone: Boolean
|
||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
get() = pubStatus.isBitSet(5) xor isThisPodInThecase
|
||||
|
||||
/**
|
||||
* The data flip bit is UNset if the right pod is primary.
|
||||
* For the pod that is in the case, this is flipped again though.
|
||||
*/
|
||||
override val isRightPodMicrophone: Boolean
|
||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||
get() = !pubStatus.isBitSet(5) xor isThisPodInThecase
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() {
|
||||
decryptedPayload?.get(if (areValuesFlipped) 2 else 1)?.let { raw ->
|
||||
payload.private?.data?.get(if (areValuesFlipped) 2 else 1)?.let { raw ->
|
||||
val isCharging = (raw and 0x80u).toInt() != 0
|
||||
return isCharging
|
||||
}
|
||||
return when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
false -> pubFlags.isBitSet(0)
|
||||
true -> pubFlags.isBitSet(1)
|
||||
}
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() {
|
||||
decryptedPayload?.get(if (areValuesFlipped) 1 else 2)?.let { raw ->
|
||||
payload.private?.data?.get(if (areValuesFlipped) 1 else 2)?.let { raw ->
|
||||
val isCharging = (raw and 0x80u).toInt() != 0
|
||||
return isCharging
|
||||
}
|
||||
return when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
false -> pubFlags.isBitSet(1)
|
||||
true -> pubFlags.isBitSet(0)
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() {
|
||||
decryptedPayload?.get(3)?.let { raw ->
|
||||
payload.private?.data?.get(3)?.let { raw ->
|
||||
val level = (raw and 0x7Fu).toInt() / 100f
|
||||
if (level <= 1.0f) return level
|
||||
}
|
||||
return when (val value = rawCaseBattery.toInt()) {
|
||||
return when (val value = pubCaseBattery.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
@@ -147,18 +147,18 @@ interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasE
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() {
|
||||
decryptedPayload?.get(3)?.let { raw ->
|
||||
payload.private?.data?.get(3)?.let { raw ->
|
||||
val isCharging = (raw and 0x80u).toInt() != 0
|
||||
if (batteryCasePercent != null) return isCharging
|
||||
}
|
||||
|
||||
return rawFlags.isBitSet(2)
|
||||
return pubFlags.isBitSet(2)
|
||||
}
|
||||
|
||||
val caseLidState: LidState
|
||||
get() {
|
||||
val rawstate = rawCaseLidState
|
||||
return LidState.values().firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
|
||||
val rawstate = pubCaseLidState
|
||||
return LidState.entries.firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<
|
||||
microPhoneRight = isRightPodMicrophone,
|
||||
chargingLeft = isLeftPodCharging,
|
||||
chargingRight = isRightPodCharging,
|
||||
color = rawDeviceColor,
|
||||
color = pubDeviceColor,
|
||||
model = model
|
||||
)
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import eu.darken.capod.pods.core.HasPodStyle
|
||||
interface HasAppleColor : ApplePods, HasPodStyle {
|
||||
|
||||
override val podStyle: HasPodStyle.PodStyle
|
||||
get() = DeviceColor.values()
|
||||
.firstOrNull { it.raw == rawDeviceColor }
|
||||
get() = DeviceColor.entries
|
||||
.firstOrNull { it.raw == pubDeviceColor }
|
||||
?: DeviceColor.UNKNOWN
|
||||
|
||||
enum class DeviceColor(val raw: UByte?) : HasPodStyle.PodStyle {
|
||||
|
||||
@@ -10,7 +10,7 @@ import eu.darken.capod.pods.core.SinglePodDevice
|
||||
interface SingleApplePods : ApplePods, SinglePodDevice, HasAppleColor {
|
||||
|
||||
override val batteryHeadsetPercent: Float?
|
||||
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
|
||||
get() = when (val value = pubPodsBattery.lowerNibble.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Headset above 100% battery: $value" }
|
||||
|
||||
@@ -6,17 +6,19 @@ 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.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 AirPodsGen1 constructor(
|
||||
data class AirPodsGen1(
|
||||
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 payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -36,17 +38,16 @@ data class AirPodsGen1 constructor(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsGen1(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsGen1(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,17 +6,19 @@ 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.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 AirPodsGen2 constructor(
|
||||
data class AirPodsGen2(
|
||||
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 payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -36,16 +38,15 @@ data class AirPodsGen2 constructor(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsGen2(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,17 +6,19 @@ 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.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 AirPodsGen3 constructor(
|
||||
data class AirPodsGen3(
|
||||
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 payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -36,16 +38,15 @@ data class AirPodsGen3 constructor(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsGen3(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,7 +6,9 @@ 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.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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class AirPodsGen4(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -36,16 +38,15 @@ data class AirPodsGen4(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsGen4(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsGen4(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,7 +6,9 @@ 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.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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class AirPodsGen4Anc(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -36,16 +38,15 @@ data class AirPodsGen4Anc(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsGen4Anc(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsGen4Anc(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -10,7 +10,9 @@ import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -20,7 +22,7 @@ data class AirPodsMax(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
|
||||
@@ -31,23 +33,22 @@ data class AirPodsMax(
|
||||
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
||||
|
||||
override val isHeadsetBeingCharged: Boolean
|
||||
get() = rawFlags.isBitSet(0)
|
||||
get() = pubFlags.isBitSet(0)
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = rawStatus.isBitSet(5)
|
||||
get() = pubStatus.isBitSet(5)
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsMax(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -10,7 +10,9 @@ import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -20,7 +22,7 @@ data class AirPodsMaxUsbc(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasEarDetection, HasChargeDetection, HasAppleColor {
|
||||
@@ -31,23 +33,22 @@ data class AirPodsMaxUsbc(
|
||||
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
||||
|
||||
override val isHeadsetBeingCharged: Boolean
|
||||
get() = rawFlags.isBitSet(0)
|
||||
get() = pubFlags.isBitSet(0)
|
||||
|
||||
override val isBeingWorn: Boolean
|
||||
get() = rawStatus.isBitSet(5)
|
||||
get() = pubStatus.isBitSet(5)
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsMaxUsbc(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsMaxUsbc(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -9,7 +9,9 @@ import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -19,8 +21,7 @@ data class AirPodsPro(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val decryptedPayload: UByteArray?,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -56,19 +57,17 @@ data class AirPodsPro(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsPro(
|
||||
scanResult = scanResult,
|
||||
proximityMessage = message,
|
||||
decryptedPayload = decrypted
|
||||
payload = payload,
|
||||
)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -19,7 +21,7 @@ data class AirPodsPro2(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -55,16 +57,15 @@ data class AirPodsPro2(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsPro2(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsPro2(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -9,7 +9,9 @@ import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -19,7 +21,7 @@ data class AirPodsPro2Usbc(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -55,16 +57,15 @@ data class AirPodsPro2Usbc(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = AirPodsPro2Usbc(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = AirPodsPro2Usbc(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import eu.darken.capod.pods.core.apple.ApplePods
|
||||
interface HasStateDetectionAirPods : HasStateDetection, ApplePods {
|
||||
|
||||
override val state: ConnectionState
|
||||
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
||||
get() = ConnectionState.entries.firstOrNull { pubSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
||||
|
||||
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
|
||||
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
|
||||
|
||||
@@ -6,7 +6,9 @@ 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.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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class BeatsFitPro(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -36,16 +38,15 @@ data class BeatsFitPro(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = BeatsFitPro(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = BeatsFitPro(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,7 +6,9 @@ import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class BeatsFlex(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
@@ -28,16 +30,15 @@ data class BeatsFlex(
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = BeatsFlex(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = BeatsFlex(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,7 +6,9 @@ import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class BeatsSolo3(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
@@ -27,16 +29,15 @@ data class BeatsSolo3(
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = BeatsSolo3(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,7 +6,9 @@ import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class BeatsStudio3(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
@@ -25,16 +27,15 @@ data class BeatsStudio3(
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = BeatsStudio3(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,7 +6,9 @@ import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class BeatsX(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
@@ -28,16 +30,15 @@ data class BeatsX(
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = BeatsX(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = BeatsX(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,7 +6,9 @@ import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class PowerBeats3(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
@@ -28,16 +30,15 @@ data class PowerBeats3(
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = PowerBeats3(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = PowerBeats3(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -7,7 +7,9 @@ import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
|
||||
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
|
||||
|
||||
@@ -17,7 +19,7 @@ data class PowerBeats4(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods, HasStateDetectionAirPods {
|
||||
@@ -29,16 +31,15 @@ data class PowerBeats4(
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = PowerBeats4(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = PowerBeats4(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -6,7 +6,9 @@ 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.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
|
||||
|
||||
@@ -16,7 +18,7 @@ data class PowerBeatsPro(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
@@ -36,16 +38,15 @@ data class PowerBeatsPro(
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = PowerBeatsPro(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = PowerBeatsPro(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
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.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -18,118 +19,38 @@ import javax.inject.Inject
|
||||
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||
* Marketed as TWS i99999
|
||||
*/
|
||||
data class FakeAirPodsGen1 constructor(
|
||||
data class FakeAirPodsGen1(
|
||||
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 payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||
|
||||
/**
|
||||
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||
* If the right pod is the primary, the values are flipped.
|
||||
*/
|
||||
val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Right pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
15 -> cachedBatteryPercentage
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawFlags.isBitSet(2)
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen1>(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = FakeAirPodsGen1(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = FakeAirPodsGen1(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -1,133 +1,54 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
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.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||
*/
|
||||
data class FakeAirPodsGen2 constructor(
|
||||
data class FakeAirPodsGen2(
|
||||
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 payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||
|
||||
/**
|
||||
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||
* If the right pod is the primary, the values are flipped.
|
||||
*/
|
||||
val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Right pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
15 -> cachedBatteryPercentage
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawFlags.isBitSet(2)
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen2>(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = FakeAirPodsGen2(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = FakeAirPodsGen2(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
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.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -18,118 +19,38 @@ import javax.inject.Inject
|
||||
* Shorter data structure.
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/1054404630118924308
|
||||
*/
|
||||
data class FakeAirPodsGen3 constructor(
|
||||
data class FakeAirPodsGen3(
|
||||
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 payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
) : DualApplePods, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||
|
||||
/**
|
||||
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||
* If the right pod is the primary, the values are flipped.
|
||||
*/
|
||||
val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Right pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
15 -> cachedBatteryPercentage
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawFlags.isBitSet(2)
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen3>(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = FakeAirPodsGen3(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = FakeAirPodsGen3(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -3,15 +3,16 @@ package eu.darken.capod.pods.core.apple.misc
|
||||
import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.common.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
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.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -20,17 +21,17 @@ import javax.inject.Inject
|
||||
* Shorter data structure.
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/962068944196358234
|
||||
*/
|
||||
data class FakeAirPodsPro constructor(
|
||||
data class FakeAirPodsPro(
|
||||
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 payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||
) : DualApplePods, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
|
||||
|
||||
@@ -51,103 +52,23 @@ data class FakeAirPodsPro constructor(
|
||||
get() = R.drawable.devic_airpods_pro2_right
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||
|
||||
/**
|
||||
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||
* If the right pod is the primary, the values are flipped.
|
||||
*/
|
||||
val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Right pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
15 -> cachedBatteryPercentage
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawFlags.isBitSet(2)
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro>(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = FakeAirPodsPro(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = FakeAirPodsPro(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -3,16 +3,18 @@ package eu.darken.capod.pods.core.apple.misc
|
||||
import androidx.annotation.DrawableRes
|
||||
import eu.darken.capod.common.R
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.*
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.HasChargeDetectionDual
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
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.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -20,18 +22,18 @@ import javax.inject.Inject
|
||||
* AirPods Pro2 clone
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
|
||||
*/
|
||||
data class FakeAirPodsPro2 constructor(
|
||||
data class FakeAirPodsPro2(
|
||||
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 payload: ProximityPayload,
|
||||
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null,
|
||||
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
|
||||
private val cachedCaseState: LidState? = null,
|
||||
) : DualApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||
|
||||
@@ -52,99 +54,25 @@ data class FakeAirPodsPro2 constructor(
|
||||
get() = R.drawable.devic_airpods_pro2_right
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: scanResult.rssi
|
||||
|
||||
private val areValuesFlipped: Boolean
|
||||
get() = !rawStatus.isBitSet(5)
|
||||
|
||||
override val batteryLeftPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.upperNibble.toInt()
|
||||
false -> rawPodsBattery.lowerNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Left pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
(value / 10f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val batteryRightPodPercent: Float?
|
||||
get() {
|
||||
val value = when (areValuesFlipped) {
|
||||
true -> rawPodsBattery.lowerNibble.toInt()
|
||||
false -> rawPodsBattery.upperNibble.toInt()
|
||||
}
|
||||
return when (value) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
log { "Right pod: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val isThisPodInThecase: Boolean
|
||||
get() = rawStatus.isBitSet(6)
|
||||
|
||||
override val isLeftPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(3)
|
||||
false -> rawStatus.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodInEar: Boolean
|
||||
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||
true -> rawStatus.isBitSet(1)
|
||||
false -> rawStatus.isBitSet(3)
|
||||
}
|
||||
|
||||
override val isLeftPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(0)
|
||||
true -> rawFlags.isBitSet(1)
|
||||
}
|
||||
|
||||
override val isRightPodCharging: Boolean
|
||||
get() = when (areValuesFlipped) {
|
||||
false -> rawFlags.isBitSet(1)
|
||||
true -> rawFlags.isBitSet(0)
|
||||
}
|
||||
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||
override val caseLidState: LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = when (val value = rawCaseBattery.toInt()) {
|
||||
15 -> cachedBatteryPercentage
|
||||
else -> if (value > 10) {
|
||||
log { "Case: Above 100% battery: $value" }
|
||||
1.0f
|
||||
} else {
|
||||
value / 10f
|
||||
}
|
||||
}
|
||||
|
||||
override val isCaseCharging: Boolean
|
||||
get() = rawFlags.isBitSet(2)
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro2>(TAG) {
|
||||
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = message.run {
|
||||
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?
|
||||
payload: ProximityPayload
|
||||
): ApplePods {
|
||||
var basic = FakeAirPodsPro2(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = FakeAirPodsPro2(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -7,7 +7,8 @@ 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.protocol.ProximityPairing
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -17,7 +18,7 @@ data class UnknownAppleDevice(
|
||||
override val seenFirstAt: Instant = Instant.now(),
|
||||
override val seenCounter: Int = 1,
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val payload: ProximityPayload,
|
||||
override val reliability: Float = 0f,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : ApplePods {
|
||||
@@ -30,14 +31,13 @@ data class UnknownAppleDevice(
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<ApplePods>(TAG) {
|
||||
override fun isResponsible(message: ProximityPairing.Message): Boolean = true
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = true
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
message: ProximityPairing.Message,
|
||||
decrypted: UByteArray?,
|
||||
payload: ProximityPayload,
|
||||
): ApplePods {
|
||||
var basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = message)
|
||||
var basic = UnknownAppleDevice(scanResult = scanResult, payload = payload)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.protocol
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessageDecrypter @Inject constructor() {
|
||||
|
||||
@SuppressLint("GetInstance")
|
||||
fun decrypt(message: ProximityPairing.Message, key: ByteArray): UByteArray? {
|
||||
val data = message.data.toByteArray()
|
||||
if (data.size < 16) return null
|
||||
|
||||
val decryptedData = try {
|
||||
val cipher = Cipher.getInstance("AES/ECB/NoPadding").apply {
|
||||
val secretKey = SecretKeySpec(key, "AES")
|
||||
init(Cipher.DECRYPT_MODE, secretKey)
|
||||
}
|
||||
cipher.doFinal(data.copyOfRange(data.size - 16, data.size))
|
||||
} catch (e: Exception) {
|
||||
log(TAG, Logging.Priority.ERROR) { "Failed to decrypt $message with ${key.toByteString()}\n${e.asLog()}" }
|
||||
null
|
||||
}
|
||||
|
||||
log(
|
||||
TAG,
|
||||
Logging.Priority.VERBOSE
|
||||
) { "Decrypted $message with ${key.toByteString()} to ${decryptedData?.toByteString()}" }
|
||||
|
||||
if (decryptedData == null || decryptedData.size != 16) return null
|
||||
|
||||
return decryptedData.toUByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Pod", "Factory", "Decrypter")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package eu.darken.capod.pods.core.apple.protocol
|
||||
|
||||
import android.R.id.message
|
||||
import android.annotation.SuppressLint
|
||||
import dagger.Reusable
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.inject.Inject
|
||||
|
||||
data class ProximityMessage(
|
||||
val type: UByte,
|
||||
val length: Int,
|
||||
val data: UByteArray
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
|
||||
return "ProximityPairing.Message(type=$type, length=$length, data=$dataHex)"
|
||||
}
|
||||
|
||||
@Reusable
|
||||
class Decrypter @Inject constructor() {
|
||||
@SuppressLint("GetInstance")
|
||||
fun decrypt(data: ByteArray, key: ByteArray): UByteArray? {
|
||||
val decryptedData = try {
|
||||
val cipher = Cipher.getInstance("AES/ECB/NoPadding").apply {
|
||||
val secretKey = SecretKeySpec(key, "AES")
|
||||
init(Cipher.DECRYPT_MODE, secretKey)
|
||||
}
|
||||
cipher.doFinal(data.copyOfRange(data.size - 16, data.size))
|
||||
} catch (e: Exception) {
|
||||
log(
|
||||
TAG,
|
||||
Logging.Priority.ERROR
|
||||
) { "Failed to decrypt $message with ${key.toByteString()}\n${e.asLog()}" }
|
||||
null
|
||||
}
|
||||
|
||||
log(
|
||||
TAG,
|
||||
Logging.Priority.VERBOSE
|
||||
) { "Decrypted $message with ${key.toByteString()} to ${decryptedData?.toByteString()}" }
|
||||
|
||||
if (decryptedData == null || decryptedData.size != 16) return null
|
||||
|
||||
return decryptedData.toUByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Pod", "Factory", "Decrypter")
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-14
@@ -8,27 +8,15 @@ import javax.inject.Inject
|
||||
|
||||
object ProximityPairing {
|
||||
|
||||
data class Message(
|
||||
val type: UByte,
|
||||
val length: Int,
|
||||
val data: UByteArray
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
|
||||
return "ProximityPairing.Message(type=$type, length=$length, data=$dataHex)"
|
||||
}
|
||||
}
|
||||
|
||||
@Reusable
|
||||
class Decoder @Inject constructor() {
|
||||
fun decode(message: ContinuityProtocol.Message): Message? {
|
||||
fun decode(message: ContinuityProtocol.Message): ProximityMessage? {
|
||||
if (message.type != CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING) {
|
||||
log { "Not a proximity pairing message: $this" }
|
||||
return null
|
||||
}
|
||||
|
||||
return Message(
|
||||
return ProximityMessage(
|
||||
type = message.type,
|
||||
length = message.length,
|
||||
data = message.data
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package eu.darken.capod.pods.core.apple.protocol
|
||||
|
||||
data class ProximityPayload(
|
||||
val public: Public,
|
||||
val private: Private?,
|
||||
) {
|
||||
|
||||
data class Public(
|
||||
val data: UByteArray,
|
||||
)
|
||||
|
||||
data class Private(
|
||||
val data: UByteArray,
|
||||
)
|
||||
}
|
||||
@@ -51,8 +51,8 @@ class PopUpReaction @Inject constructor(
|
||||
return@mapNotNull null
|
||||
}
|
||||
log(TAG, VERBOSE) {
|
||||
val prev = previous?.rawCaseLidState?.let { String.format("%02X", it.toByte()) }
|
||||
val cur = current.rawCaseLidState.let { String.format("%02X", it.toByte()) }
|
||||
val prev = previous?.pubCaseLidState?.let { String.format("%02X", it.toByte()) }
|
||||
val cur = current.pubCaseLidState.let { String.format("%02X", it.toByte()) }
|
||||
"previous=$prev (${previous?.caseLidState}), current=$cur (${current.caseLidState})"
|
||||
}
|
||||
log(TAG, VERBOSE) { "previous-id=${previous?.identifier}, current-id=${current.identifier}" }
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import eu.darken.capod.common.SystemClockWrap
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.serialization.SerializationModule
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.preferences.mockFlowPreference
|
||||
import java.time.Instant
|
||||
import javax.inject.Singleton
|
||||
|
||||
abstract class BaseAirPodsTest : BaseTest() {
|
||||
@Singleton
|
||||
@Component(modules = [AppleFactoryModule::class])
|
||||
@Component(modules = [AppleFactoryModule::class, SerializationModule::class])
|
||||
interface AppleFactoryTestComponent {
|
||||
|
||||
val appleFactory: AppleFactory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(): AppleFactoryTestComponent
|
||||
fun create(
|
||||
@BindsInstance generalSettings: GeneralSettings,
|
||||
): AppleFactoryTestComponent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +41,14 @@ abstract class BaseAirPodsTest : BaseTest() {
|
||||
manufacturerSpecificData = emptyMap()
|
||||
)
|
||||
|
||||
val factory: AppleFactory = DaggerBaseAirPodsTest_AppleFactoryTestComponent.factory().create().appleFactory
|
||||
val generalSettings = mockk<GeneralSettings>().apply {
|
||||
every { mainDeviceIdentityKey } returns mockFlowPreference(null)
|
||||
every { mainDeviceEncryptionKey } returns mockFlowPreference(null)
|
||||
}
|
||||
|
||||
val factory: AppleFactory = DaggerBaseAirPodsTest_AppleFactoryTestComponent.factory().create(
|
||||
generalSettings = generalSettings
|
||||
).appleFactory
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
|
||||
+9
-9
@@ -9,15 +9,15 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `test mapping`() = runTest {
|
||||
create<SingleApplePods>("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()
|
||||
rawFlags shouldBe 0x0.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0520.toUShort()
|
||||
pubStatus shouldBe 0x00.toUByte()
|
||||
pubPodsBattery shouldBe 0xF5.toUByte()
|
||||
pubFlags shouldBe 0x0.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x01.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,15 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
||||
fun `test bit mapping`() = runTest {
|
||||
create<DualApplePods>("07 19 01 0E 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 0x0e20.toUShort()
|
||||
rawStatus shouldBe 0x54.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x5.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0e20.toUShort()
|
||||
pubStatus shouldBe 0x54.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0xB.toUShort()
|
||||
pubCaseBattery shouldBe 0x5.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@ class SingleApplePodsTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default bit mapping Max`() = runTest {
|
||||
create<SingleApplePods>("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()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0A20.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,15 @@ class AirPodsGen1Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `fake airpods`() = runTest {
|
||||
create<AirPodsGen1>("07 19 01 02 20 55 AF 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||
rawStatus shouldBe 0x55.toUByte()
|
||||
rawPodsBattery shouldBe 0xAF.toUByte()
|
||||
rawFlags shouldBe 0x5.toUShort()
|
||||
rawCaseBattery shouldBe 0x6.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0220.toUShort()
|
||||
pubStatus shouldBe 0x55.toUByte()
|
||||
pubPodsBattery shouldBe 0xAF.toUByte()
|
||||
pubFlags shouldBe 0x5.toUShort()
|
||||
pubCaseBattery shouldBe 0x6.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe null
|
||||
|
||||
@@ -13,15 +13,15 @@ class AirPodsGen2Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `random Neighbor AirPodsGen2`() = runTest {
|
||||
create<AirPodsGen2>("07 19 01 0F 20 02 F9 8F 01 00 05 F2 7E 14 E0 54 0A 53 69 5B 7D F2 15 1F D7 B1 12") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0F20.toUShort()
|
||||
rawStatus shouldBe 0x02.toUByte()
|
||||
rawPodsBattery shouldBe 0xF9.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x05.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0F20.toUShort()
|
||||
pubStatus shouldBe 0x02.toUByte()
|
||||
pubPodsBattery shouldBe 0xF9.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x05.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe null
|
||||
batteryRightPodPercent shouldBe 0.9f
|
||||
|
||||
@@ -13,15 +13,15 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `AirPods Gen3`() = runTest {
|
||||
create<AirPodsGen3>("07 19 01 13 20 75 AA B9 31 00 04 67 9A 57 DF BE F6 90 52 B0 04 1F 8D 89 DA F4 9E") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1320.toUShort()
|
||||
rawStatus shouldBe 0x75.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x9.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1320.toUShort()
|
||||
pubStatus shouldBe 0x75.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0xB.toUShort()
|
||||
pubCaseBattery shouldBe 0x9.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
+9
-9
@@ -13,15 +13,15 @@ class AirPodsGen4AncTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `AirPods Gen4 with ANC via log from #226`() = runTest {
|
||||
create<AirPodsGen4Anc>("07 19 01 1B 20 0B 9A 8F 10 00 04 43 DF EC 1D D3 F1 C3 F4 A1 9B 29 26 B9 E7 3A A0") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1B20.toUShort()
|
||||
rawStatus shouldBe 0x0b.toUByte()
|
||||
rawPodsBattery shouldBe 0x9A.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x10.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1B20.toUShort()
|
||||
pubStatus shouldBe 0x0b.toUByte()
|
||||
pubPodsBattery shouldBe 0x9A.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x10.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
@@ -13,15 +13,15 @@ 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()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1920.toUShort()
|
||||
pubStatus shouldBe 0x2B.toUByte()
|
||||
pubPodsBattery shouldBe 0x33.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x11.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 0.3f
|
||||
batteryRightPodPercent shouldBe 0.3f
|
||||
|
||||
+32
-32
@@ -14,15 +14,15 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default AirPods Max`() = runTest {
|
||||
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()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0A20.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
@@ -36,15 +36,15 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default AirPods Max flipped values`() = runTest {
|
||||
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()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x04.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x44.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0A20.toUShort()
|
||||
pubStatus shouldBe 0x02.toUByte()
|
||||
pubPodsBattery shouldBe 0x05.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x04.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x44.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
@@ -55,21 +55,21 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `some dude at the gym`() = runTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
rawStatus shouldBe 0x23.toUByte()
|
||||
rawPodsBattery shouldBe 0x07.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x03.toUByte()
|
||||
rawDeviceColor shouldBe 0x03.toUByte()
|
||||
rawSuffix shouldBe 0x65.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0A20.toUShort()
|
||||
pubStatus shouldBe 0x23.toUByte()
|
||||
pubPodsBattery shouldBe 0x07.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x03.toUByte()
|
||||
pubDeviceColor shouldBe 0x03.toUByte()
|
||||
pubSuffix shouldBe 0x65.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.7f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
rawStatus.isBitSet(5) shouldBe true
|
||||
pubStatus.isBitSet(5) shouldBe true
|
||||
|
||||
podStyle shouldBe HasAppleColor.DeviceColor.BLUE
|
||||
}
|
||||
@@ -78,15 +78,15 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `wear status`() = runTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 03 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
rawStatus shouldBe 0x03.toUByte()
|
||||
pubStatus shouldBe 0x03.toUByte()
|
||||
|
||||
rawStatus.isBitSet(5) shouldBe false
|
||||
pubStatus.isBitSet(5) shouldBe false
|
||||
isBeingWorn shouldBe false
|
||||
}
|
||||
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
rawStatus shouldBe 0x23.toUByte()
|
||||
pubStatus shouldBe 0x23.toUByte()
|
||||
|
||||
rawStatus.isBitSet(5) shouldBe true
|
||||
pubStatus.isBitSet(5) shouldBe true
|
||||
isBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -12,15 +12,15 @@ class AirPodsMaxUsbcTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default AirPods Max`() = runTest {
|
||||
create<AirPodsMaxUsbc>("07 19 01 1F 20 2B 05 80 03 12 C5 2E 8B F9 9A 7E 19 7B 63 0F 30 6E D7 3B E2 EC 32") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1F20.toUShort()
|
||||
rawStatus shouldBe 0x2B.toUByte()
|
||||
rawPodsBattery shouldBe 0x05.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x03.toUByte()
|
||||
rawDeviceColor shouldBe 0x12.toUByte()
|
||||
rawSuffix shouldBe 0xC5.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1F20.toUShort()
|
||||
pubStatus shouldBe 0x2B.toUByte()
|
||||
pubPodsBattery shouldBe 0x05.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x03.toUByte()
|
||||
pubDeviceColor shouldBe 0x12.toUByte()
|
||||
pubSuffix shouldBe 0xC5.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
|
||||
+18
-18
@@ -16,15 +16,15 @@ class AirPodsPro2Test : BaseAirPodsTest() {
|
||||
fun `test AirPods Pro 2 - unknown setup from #31`() = runTest {
|
||||
create<AirPodsPro2>("07 19 01 14 20 55 88 F9 51 00 04 20 50 03 CA D5 C9 AC 0F FA 84 78 94 5A 4D DF F5") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||
rawStatus shouldBe 0x55.toUByte()
|
||||
rawPodsBattery shouldBe 0x88.toUByte()
|
||||
rawFlags shouldBe 0xF.toUShort()
|
||||
rawCaseBattery shouldBe 0x9.toUShort()
|
||||
rawCaseLidState shouldBe 0x51.toUByte()
|
||||
rawDeviceColor shouldBe 0x0.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1420.toUShort()
|
||||
pubStatus shouldBe 0x55.toUByte()
|
||||
pubPodsBattery shouldBe 0x88.toUByte()
|
||||
pubFlags shouldBe 0xF.toUShort()
|
||||
pubCaseBattery shouldBe 0x9.toUShort()
|
||||
pubCaseLidState shouldBe 0x51.toUByte()
|
||||
pubDeviceColor shouldBe 0x0.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
@@ -53,15 +53,15 @@ class AirPodsPro2Test : BaseAirPodsTest() {
|
||||
fun `test AirPods Pro 2 - unknown setup from reddit user`() = runTest {
|
||||
create<AirPodsPro2>("07 19 01 14 20 2B 9A 8F 01 00 04 0F 26 1A C4 2B FA 2F B9 B6 08 CD 60 CB DF 75 AB") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||
rawStatus shouldBe 0x2B.toUByte()
|
||||
rawPodsBattery shouldBe 0x9A.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1420.toUShort()
|
||||
pubStatus shouldBe 0x2B.toUByte()
|
||||
pubPodsBattery shouldBe 0x9A.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
+9
-9
@@ -16,15 +16,15 @@ class AirPodsPro2UsbcTest : BaseAirPodsTest() {
|
||||
fun `AirPods Pro 2 with USB-C - via #164`() = runTest {
|
||||
create<AirPodsPro2Usbc>("07 19 01 24 20 0B 99 8F 11 00 04 BD A7 3B FF 2D 8A 3C AF 9B 1A 7C 74 B7 A9 D1 C3") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x2420.toUShort()
|
||||
rawStatus shouldBe 0x0B.toUByte()
|
||||
rawPodsBattery shouldBe 0x99.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x11.toUByte()
|
||||
rawDeviceColor shouldBe 0x0.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x2420.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
|
||||
|
||||
+10
-10
@@ -13,15 +13,15 @@ class AirPodsProTest : BaseAirPodsTest() {
|
||||
fun `test AirPods Pro - default changed and in case`() = runTest {
|
||||
create<AirPodsPro>("07 19 01 0E 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 0x0e20.toUShort()
|
||||
rawStatus shouldBe 0x54.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x5.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0e20.toUShort()
|
||||
pubStatus shouldBe 0x54.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0xB.toUShort()
|
||||
pubCaseBattery shouldBe 0x5.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
@@ -177,7 +177,7 @@ class AirPodsProTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `left pod has no data`() = runTest {
|
||||
create<AirPodsPro>("07 19 01 0E 20 0B F9 8F 03 00 05 5B 59 67 4C F7 F3 EF 01 BA F4 92 1B 26 E4 90 40") {
|
||||
rawPodsBattery shouldBe 0xF9.toUByte()
|
||||
pubPodsBattery shouldBe 0xF9.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe null
|
||||
batteryRightPodPercent shouldBe 0.9f
|
||||
|
||||
@@ -15,15 +15,15 @@ class BeatsFitProTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `test basics`() = runTest {
|
||||
create<BeatsFitPro>("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()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1220.toUShort()
|
||||
pubStatus shouldBe 0x20.toUByte()
|
||||
pubPodsBattery shouldBe 0xFA.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x11.toUByte()
|
||||
pubSuffix shouldBe 0x24.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
@@ -44,15 +44,15 @@ class BeatsFitProTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `extra rl test case`() = runTest {
|
||||
create<BeatsFitPro>("07 19 01 12 20 04 FA 92 54 11 24 CE B1 DF 9D 8D F5 E3 37 60 B1 23 8B 90 3B 63 3F") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1220.toUShort()
|
||||
rawStatus shouldBe 0x04.toUByte()
|
||||
rawPodsBattery shouldBe 0xFA.toUByte()
|
||||
rawFlags shouldBe 0x9.toUShort()
|
||||
rawCaseBattery shouldBe 0x2.toUShort()
|
||||
rawCaseLidState shouldBe 0x54.toUByte()
|
||||
rawDeviceColor shouldBe 0x11.toUByte()
|
||||
rawSuffix shouldBe 0x24.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1220.toUShort()
|
||||
pubStatus shouldBe 0x04.toUByte()
|
||||
pubPodsBattery shouldBe 0xFA.toUByte()
|
||||
pubFlags shouldBe 0x9.toUShort()
|
||||
pubCaseBattery shouldBe 0x2.toUShort()
|
||||
pubCaseLidState shouldBe 0x54.toUByte()
|
||||
pubDeviceColor shouldBe 0x11.toUByte()
|
||||
pubSuffix shouldBe 0x24.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
|
||||
@@ -12,15 +12,15 @@ class BeatsFlexText : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default BeatsFlex`() = runTest {
|
||||
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()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x00.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1020.toUShort()
|
||||
pubStatus shouldBe 0x0A.toUByte()
|
||||
pubPodsBattery shouldBe 0xF4.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x00.toUByte()
|
||||
pubDeviceColor shouldBe 0x01.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
|
||||
@@ -12,15 +12,15 @@ class BeatsSolo3Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default BeatsSolo3`() = runTest {
|
||||
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()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0620.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
|
||||
@@ -12,15 +12,15 @@ class BeatsStudio3Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default BeatsStudio3`() = runTest {
|
||||
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()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0920.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
|
||||
@@ -12,15 +12,15 @@ class BeatsXTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default BeatsX`() = runTest {
|
||||
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()
|
||||
rawFlags shouldBe 0x0.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0520.toUShort()
|
||||
pubStatus shouldBe 0x00.toUByte()
|
||||
pubPodsBattery shouldBe 0xF5.toUByte()
|
||||
pubFlags shouldBe 0x0.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x01.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
|
||||
@@ -12,15 +12,15 @@ class PowerBeats3Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `default PowerBeats3`() = runTest {
|
||||
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()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0320.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
|
||||
@@ -12,15 +12,15 @@ class PowerBeats4Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `playing music`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 05 07 C5 E1 9C BD 9A 05 0E AE 9E 56 53 2F F9 75 4A") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0D20.toUShort()
|
||||
rawStatus shouldBe 0x0.toUByte()
|
||||
rawPodsBattery shouldBe 0x02.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x05.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0D20.toUShort()
|
||||
pubStatus shouldBe 0x0.toUByte()
|
||||
pubPodsBattery shouldBe 0x02.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x05.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
@@ -33,15 +33,15 @@ class PowerBeats4Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `extra test case`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 02 00 04 80 90 60 77 32 C2 0C 75 7A 3D 7E D7 1C 0E B8 43") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0D20.toUShort()
|
||||
rawStatus shouldBe 0x0.toUByte()
|
||||
rawPodsBattery shouldBe 0x02.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x02.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0D20.toUShort()
|
||||
pubStatus shouldBe 0x0.toUByte()
|
||||
pubPodsBattery shouldBe 0x02.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x02.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
@@ -55,15 +55,15 @@ class PowerBeats4Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `disconnected state`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 00 25 DF 66 40 AF 44 7B 77 95 8F D1 92 50 26 11 74") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0D20.toUShort()
|
||||
rawStatus shouldBe 0x0.toUByte()
|
||||
rawPodsBattery shouldBe 0x02.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0D20.toUShort()
|
||||
pubStatus shouldBe 0x0.toUByte()
|
||||
pubPodsBattery shouldBe 0x02.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ class PowerBeatsProTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `test PowerBeatsPro`() = runTest {
|
||||
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()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x5.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0B20.toUShort()
|
||||
pubStatus shouldBe 0x54.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0xB.toUShort()
|
||||
pubCaseBattery shouldBe 0x5.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
+9
-9
@@ -11,15 +11,15 @@ class FakeAirPodsGen1Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `charging in box`() = runTest {
|
||||
create<FakeAirPodsGen2>("07 13 01 0F 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0F20.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x3.toUShort()
|
||||
rawCaseBattery shouldBe 0x7.toUShort()
|
||||
rawCaseLidState shouldBe 0x32.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0F20.toUShort()
|
||||
pubStatus shouldBe 0x71.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x3.toUShort()
|
||||
pubCaseBattery shouldBe 0x7.toUShort()
|
||||
pubCaseLidState shouldBe 0x32.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
+9
-9
@@ -11,15 +11,15 @@ class FakeAirPodsGen2Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `charging in box`() = runTest {
|
||||
create<FakeAirPodsGen1>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x3.toUShort()
|
||||
rawCaseBattery shouldBe 0x7.toUShort()
|
||||
rawCaseLidState shouldBe 0x32.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0220.toUShort()
|
||||
pubStatus shouldBe 0x71.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x3.toUShort()
|
||||
pubCaseBattery shouldBe 0x7.toUShort()
|
||||
pubCaseLidState shouldBe 0x32.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
+9
-9
@@ -11,15 +11,15 @@ class FakeAirPodsGen3Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `charging in case`() = runTest {
|
||||
create<FakeAirPodsGen3>("07 13 01 13 20 75 AA 37 34 00 10 00 E4 E4 64 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1320.toUShort()
|
||||
rawStatus shouldBe 0x75.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x3.toUShort()
|
||||
rawCaseBattery shouldBe 0x7.toUShort()
|
||||
rawCaseLidState shouldBe 0x34.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1320.toUShort()
|
||||
pubStatus shouldBe 0x75.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x3.toUShort()
|
||||
pubCaseBattery shouldBe 0x7.toUShort()
|
||||
pubCaseLidState shouldBe 0x34.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1f
|
||||
batteryRightPodPercent shouldBe 1f
|
||||
|
||||
+18
-18
@@ -11,15 +11,15 @@ class FakeAirPodsPro2Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `guessed data`() = runTest {
|
||||
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 58 35 00 10 00 E4 E4 26 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||
rawStatus shouldBe 0x75.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x5.toUShort()
|
||||
rawCaseBattery shouldBe 0x8.toUShort()
|
||||
rawCaseLidState shouldBe 0x35.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1420.toUShort()
|
||||
pubStatus shouldBe 0x75.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x5.toUShort()
|
||||
pubCaseBattery shouldBe 0x8.toUShort()
|
||||
pubCaseLidState shouldBe 0x35.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
@@ -38,15 +38,15 @@ class FakeAirPodsPro2Test : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `user supplied`() = runTest {
|
||||
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 72 39 00 00 6F E4 E4 93 30 00 30 30 30 30") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||
rawStatus shouldBe 0x75.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x7.toUShort()
|
||||
rawCaseBattery shouldBe 0x2.toUShort()
|
||||
rawCaseLidState shouldBe 0x39.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x0.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1420.toUShort()
|
||||
pubStatus shouldBe 0x75.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x7.toUShort()
|
||||
pubCaseBattery shouldBe 0x2.toUShort()
|
||||
pubCaseLidState shouldBe 0x39.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x0.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
@@ -11,15 +11,15 @@ class FakeAirPodsProTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `guessed data`() = runTest {
|
||||
create<FakeAirPodsPro>("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0E20.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x3.toUShort()
|
||||
rawCaseBattery shouldBe 0x7.toUShort()
|
||||
rawCaseLidState shouldBe 0x36.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0E20.toUShort()
|
||||
pubStatus shouldBe 0x71.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x3.toUShort()
|
||||
pubCaseBattery shouldBe 0x7.toUShort()
|
||||
pubCaseLidState shouldBe 0x36.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
Reference in New Issue
Block a user