mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
Improved signal quality heuristic and value caching.
This commit is contained in:
@@ -72,8 +72,8 @@ class BleScanner @Inject constructor(
|
||||
)
|
||||
if (adapter.isOffloadedScanBatchingSupported) {
|
||||
when (scannerMode) {
|
||||
ScannerMode.LOW_POWER -> setReportDelay(5000)
|
||||
ScannerMode.BALANCED -> setReportDelay(2500)
|
||||
ScannerMode.LOW_POWER -> setReportDelay(2000)
|
||||
ScannerMode.BALANCED -> setReportDelay(1000)
|
||||
ScannerMode.LOW_LATENCY -> setReportDelay(500)
|
||||
}
|
||||
} else {
|
||||
@@ -87,7 +87,7 @@ class BleScanner @Inject constructor(
|
||||
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
||||
when (scannerMode) {
|
||||
ScannerMode.LOW_POWER -> break
|
||||
ScannerMode.BALANCED -> delay(2500)
|
||||
ScannerMode.BALANCED -> delay(1000)
|
||||
ScannerMode.LOW_LATENCY -> delay(500)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.bluetooth.BleScanner
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
@@ -9,6 +10,7 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.replayingShare
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.ScannerMode
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -33,39 +35,44 @@ class PodMonitor @Inject constructor(
|
||||
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||
private val cacheLock = Mutex()
|
||||
|
||||
private suspend fun List<BleScanResult>.preFilterAndMap(
|
||||
scannerMode: ScannerMode
|
||||
): List<PodDevice> = this
|
||||
.groupBy { it.address }
|
||||
.values
|
||||
.map { sameAdrDevs ->
|
||||
// For each address we only want the newest result, upstream may batch data
|
||||
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
||||
sameAdrDevs.minus(newest).let {
|
||||
if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" }
|
||||
}
|
||||
newest
|
||||
}
|
||||
.mapNotNull { podFactory.createPod(it) }
|
||||
|
||||
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
||||
.flatMapLatest { isBluetoothEnabled ->
|
||||
if (isBluetoothEnabled) {
|
||||
log(TAG) { "Bluetooth is enabled" }
|
||||
generalSettings.scannerMode.flow.flatMapLatest { bleScanner.scan(scannerMode = it) }
|
||||
generalSettings.scannerMode.flow
|
||||
.flatMapLatest { mode ->
|
||||
bleScanner.scan(scannerMode = mode).map { it.preFilterAndMap(mode) }
|
||||
}
|
||||
} else {
|
||||
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
||||
flowOf(null)
|
||||
}
|
||||
}
|
||||
.map { result ->
|
||||
// For each address we only want the newest result, upstream may batch data
|
||||
result?.groupBy { it.address }
|
||||
?.values
|
||||
?.map { sameAdrDevs ->
|
||||
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
||||
sameAdrDevs.minus(newest).let {
|
||||
if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" }
|
||||
}
|
||||
newest
|
||||
}
|
||||
}
|
||||
.map { scanResults ->
|
||||
.map { newPods ->
|
||||
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||
|
||||
cacheLock.withLock {
|
||||
if (scanResults == null) {
|
||||
if (newPods == null) {
|
||||
log(TAG) { "Null result, Bluetooth is disabled." }
|
||||
deviceCache.clear()
|
||||
return@map emptyList()
|
||||
}
|
||||
|
||||
val newPods = scanResults.mapNotNull { podFactory.createPod(it) }
|
||||
val now = Instant.now()
|
||||
deviceCache.toList().forEach { (key, value) ->
|
||||
if (Duration.between(value.lastSeenAt, now) > Duration.ofSeconds(20)) {
|
||||
@@ -119,16 +126,15 @@ class PodMonitor @Inject constructor(
|
||||
.map { it.determineMainDevice() }
|
||||
.replayingShare(appScope)
|
||||
|
||||
private fun List<PodDevice>.determineMainDevice(): PodDevice? =
|
||||
this.firstOrNull()?.let {
|
||||
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
||||
when {
|
||||
it.model == PodDevice.Model.UNKNOWN -> null
|
||||
mainDeviceModel != PodDevice.Model.UNKNOWN && it.model != mainDeviceModel -> null
|
||||
it.signalQuality <= generalSettings.minimumSignalQuality.value -> null
|
||||
else -> it
|
||||
}
|
||||
private fun List<PodDevice>.determineMainDevice(): PodDevice? = this.firstOrNull()?.let {
|
||||
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
||||
when {
|
||||
it.model == PodDevice.Model.UNKNOWN -> null
|
||||
mainDeviceModel != PodDevice.Model.UNKNOWN && it.model != mainDeviceModel -> null
|
||||
it.signalQuality <= generalSettings.minimumSignalQuality.value -> null
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Monitor", "PodMonitor")
|
||||
|
||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
interface PodDevice {
|
||||
|
||||
@@ -16,21 +17,24 @@ interface PodDevice {
|
||||
|
||||
val model: Model
|
||||
|
||||
val address: String
|
||||
get() = scanResult.address
|
||||
|
||||
val lastSeenAt: Instant
|
||||
|
||||
val scanResult: BleScanResult
|
||||
|
||||
val rssiHistory: List<Int>
|
||||
|
||||
val rssi: Int
|
||||
get() = rssiHistory.sum() / rssiHistory.size
|
||||
get() = scanResult.rssi
|
||||
|
||||
val confidence: Float
|
||||
|
||||
/**
|
||||
* This is not correct but it works ¯\_(ツ)_/¯
|
||||
* The range of the RSSI is device specific (ROMs).
|
||||
*/
|
||||
val signalQuality: Float
|
||||
get() = (100 - abs(rssi)) / 100f
|
||||
get() = ((100 - abs(rssi)) / 100f) * (max(BASE_CONFIDENCE, confidence))
|
||||
|
||||
val rawData: ByteArray
|
||||
|
||||
@@ -93,4 +97,8 @@ interface PodDevice {
|
||||
"Unknown"
|
||||
);
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_CONFIDENCE = 0.5f
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import javax.inject.Singleton
|
||||
class AppleFactory @Inject constructor(
|
||||
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
||||
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
||||
private val podFactories: @JvmSuppressWildcards Set<ApplePods.Factory>,
|
||||
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory<out ApplePods>>,
|
||||
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
||||
) {
|
||||
|
||||
|
||||
@@ -12,17 +12,17 @@ import eu.darken.capod.pods.core.apple.beats.*
|
||||
@Module
|
||||
abstract class AppleFactoryModule {
|
||||
|
||||
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||
|
||||
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
|
||||
|
||||
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePods.Factory
|
||||
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.SystemClockWrap
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
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.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Duration
|
||||
|
||||
interface ApplePods : PodDevice {
|
||||
|
||||
@@ -54,104 +49,4 @@ interface ApplePods : PodDevice {
|
||||
get() = proximityMessage.data[8]
|
||||
|
||||
|
||||
abstract class Factory(private val tag: String) {
|
||||
|
||||
internal data class ValueCache(
|
||||
val caseBatteryPercentage: Float?
|
||||
)
|
||||
|
||||
internal val cachedValues = mutableMapOf<PodDevice.Id, ValueCache>()
|
||||
|
||||
internal data class KnownDevice(
|
||||
val identifier: PodDevice.Id,
|
||||
val scanResult: BleScanResult,
|
||||
val message: ProximityPairing.Message,
|
||||
val rssiHistory: List<Int> = emptyList()
|
||||
) {
|
||||
val address: String
|
||||
get() = scanResult.address
|
||||
|
||||
val rssi: Int
|
||||
get() = scanResult.rssi
|
||||
|
||||
val timestampNanos: Duration
|
||||
get() = Duration.ofNanos(scanResult.generatedAtNanos)
|
||||
|
||||
fun isOlderThan(age: Duration): Boolean {
|
||||
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos)
|
||||
return now - timestampNanos > age
|
||||
}
|
||||
}
|
||||
|
||||
private val knownDevs = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||
|
||||
internal fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): KnownDevice {
|
||||
val address = scanResult.address
|
||||
|
||||
var recognizedDevice: KnownDevice? = knownDevs.values
|
||||
.firstOrNull { it.address == address }
|
||||
?.also { log(tag, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" } }
|
||||
|
||||
if (recognizedDevice == null) {
|
||||
val currentMarkers = message.getRecogMarkers()
|
||||
recognizedDevice = knownDevs.values
|
||||
.firstOrNull { it.message.getRecogMarkers() == currentMarkers }
|
||||
?.also {
|
||||
log(tag) { "recognizeDevice: Close match based similarity markers: $currentMarkers" }
|
||||
}
|
||||
if (recognizedDevice == null) {
|
||||
log(tag) { "recognizeDevice: Markers didn't match any previous data: $currentMarkers" }
|
||||
}
|
||||
}
|
||||
|
||||
recognizedDevice = if (recognizedDevice == null) {
|
||||
log(tag, VERBOSE) { "recognizeDevice: Mapping as new device" }
|
||||
KnownDevice(
|
||||
identifier = PodDevice.Id(),
|
||||
scanResult = scanResult,
|
||||
message = message,
|
||||
rssiHistory = listOf(scanResult.rssi)
|
||||
)
|
||||
} else {
|
||||
recognizedDevice.copy(
|
||||
scanResult = scanResult,
|
||||
message = message,
|
||||
rssiHistory = recognizedDevice.rssiHistory
|
||||
.toMutableList()
|
||||
.let {
|
||||
if (it.size > 3) it.removeAt(0)
|
||||
it.plus(scanResult.rssi)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
knownDevs[recognizedDevice.identifier] = recognizedDevice
|
||||
|
||||
knownDevs.values.toList().forEach { knownDevice ->
|
||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
||||
log(tag, VERBOSE) { "recognizeDevice: Removing stale known device: $knownDevice" }
|
||||
knownDevs.remove(knownDevice.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
return recognizedDevice
|
||||
}
|
||||
|
||||
data class ModelInfo(
|
||||
val full: UShort,
|
||||
val dirty: UByte,
|
||||
)
|
||||
|
||||
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
||||
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||
dirty = data[1]
|
||||
)
|
||||
|
||||
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
||||
|
||||
abstract fun create(
|
||||
scanResult: BleScanResult,
|
||||
proximityMessage: ProximityPairing.Message,
|
||||
): ApplePods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.SystemClockWrap
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Duration
|
||||
|
||||
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||
|
||||
data class Markings(
|
||||
val vendor: UByte,
|
||||
val length: UByte,
|
||||
val device: UShort,
|
||||
val podBatteryData: Set<UShort>,
|
||||
val caseBatteryData: UShort,
|
||||
val deviceColor: UByte,
|
||||
)
|
||||
|
||||
fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
||||
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
||||
length = ProximityPairing.PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||
device = (((data[1].toInt() and 255) shl 8) or (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]
|
||||
)
|
||||
|
||||
data class KnownDevice(
|
||||
val id: PodDevice.Id,
|
||||
val history: List<ApplePods>
|
||||
) {
|
||||
val lastMessage: ProximityPairing.Message
|
||||
get() = history.last().proximityMessage
|
||||
|
||||
val lastAddress: String
|
||||
get() = history.last().address
|
||||
|
||||
val confidence: Float
|
||||
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
||||
|
||||
fun averageRssi(latest: Int): Int =
|
||||
history.map { it.rssi }.plus(latest).takeLast(10).average().toInt()
|
||||
|
||||
val timestampNanos: Duration
|
||||
get() = Duration.ofNanos(history.last().scanResult.generatedAtNanos)
|
||||
|
||||
fun isOlderThan(age: Duration): Boolean {
|
||||
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos)
|
||||
return now - timestampNanos > age
|
||||
}
|
||||
|
||||
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
||||
|
||||
companion object {
|
||||
const val MAX_HISTORY = 10
|
||||
}
|
||||
}
|
||||
|
||||
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||
|
||||
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||
val scanResult = current.scanResult
|
||||
val message = current.proximityMessage
|
||||
|
||||
knownDevices.values.toList().forEach { knownDevice ->
|
||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
||||
log(tag, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
|
||||
knownDevices.remove(knownDevice.id)
|
||||
}
|
||||
}
|
||||
|
||||
knownDevices.values
|
||||
.filter { it.history.size > KnownDevice.MAX_HISTORY }
|
||||
.toList()
|
||||
.forEach {
|
||||
knownDevices[it.id] = it.copy(history = it.history.takeLast(KnownDevice.MAX_HISTORY))
|
||||
}
|
||||
|
||||
var recognizedDevice: KnownDevice? = knownDevices.values
|
||||
.firstOrNull { it.lastAddress == scanResult.address }
|
||||
?.also { log(tag, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } }
|
||||
|
||||
if (recognizedDevice == null) {
|
||||
val currentMarkers = message.getApplePodsMarkings()
|
||||
recognizedDevice = knownDevices.values
|
||||
.firstOrNull { it.lastMessage.getApplePodsMarkings() == currentMarkers }
|
||||
?.also { log(tag) { "searchHistory1: Close match based on similarity: $currentMarkers" } }
|
||||
}
|
||||
|
||||
if (recognizedDevice == null) {
|
||||
log(tag) { "searchHistory1: Didn't recognize: $message" }
|
||||
}
|
||||
|
||||
return recognizedDevice
|
||||
}
|
||||
|
||||
fun updateHistory(device: PodType) {
|
||||
val existing = knownDevices[device.identifier]
|
||||
|
||||
knownDevices[device.identifier] = when {
|
||||
existing != null -> {
|
||||
existing.copy(history = existing.history.plus(device))
|
||||
}
|
||||
else -> {
|
||||
log(tag) { "searchHistory1: Creating new history for $device" }
|
||||
KnownDevice(
|
||||
id = device.identifier,
|
||||
history = listOf(device)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ModelInfo(
|
||||
val full: UShort,
|
||||
val dirty: UByte,
|
||||
)
|
||||
|
||||
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
||||
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||
dirty = data[1]
|
||||
)
|
||||
|
||||
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
||||
|
||||
abstract fun create(
|
||||
scanResult: BleScanResult,
|
||||
proximityMessage: ProximityPairing.Message,
|
||||
): ApplePods
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
abstract class BasicSingleApplePodsFactory(private val tag: String) : ApplePodsFactory<BasicSingleApplePods>(tag)
|
||||
@@ -182,4 +182,5 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
||||
|
||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||
|
||||
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
|
||||
|
||||
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
|
||||
leftPodBattery = batteryLeftPodPercent,
|
||||
rightPodBattery = batteryRightPodPercent,
|
||||
microPhoneLeft = isLeftPodMicrophone,
|
||||
microPhoneRight = isRightPodMicrophone,
|
||||
chargingLeft = isLeftPodCharging,
|
||||
chargingRight = isRightPodCharging,
|
||||
color = deviceColor,
|
||||
model = model
|
||||
)
|
||||
|
||||
/**
|
||||
* Split pods, one in case, one not.
|
||||
*/
|
||||
data class SplitPodsMarkings(
|
||||
val leftPodBattery: Float?,
|
||||
val rightPodBattery: Float?,
|
||||
val microPhoneLeft: Boolean,
|
||||
val microPhoneRight: Boolean,
|
||||
val chargingLeft: Boolean,
|
||||
val chargingRight: Boolean,
|
||||
val color: DualApplePods.DeviceColor,
|
||||
val model: PodDevice.Model,
|
||||
)
|
||||
|
||||
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
|
||||
val target = device.getCaseMatchMarkings()
|
||||
|
||||
return filter { known ->
|
||||
known.history
|
||||
.filterIsInstance<DualApplePods>()
|
||||
.any { it.getCaseMatchMarkings() == target }
|
||||
}
|
||||
}
|
||||
|
||||
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
||||
.filterIsInstance<DualApplePods>()
|
||||
.mapNotNull { it.batteryCasePercent }
|
||||
.lastOrNull()
|
||||
|
||||
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||
if (definitive.contains(basic.caseLidState)) return null
|
||||
|
||||
return history
|
||||
.filterIsInstance<AirPodsPro>()
|
||||
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
|
||||
?.caseLidState
|
||||
}
|
||||
|
||||
override fun searchHistory(current: DualApplePods): KnownDevice? {
|
||||
val basicResult = super.searchHistory(current)
|
||||
|
||||
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
||||
|
||||
log(tag, DEBUG) { "searchHistory2: Case ignored matches(${caseIgnored.size}): $caseIgnored" }
|
||||
|
||||
return when (caseIgnored.size) {
|
||||
0 -> basicResult
|
||||
1 -> caseIgnored.single()
|
||||
else -> {
|
||||
log(tag) { "searchHistory2: More than one result when ignoring case markers." }
|
||||
val oldest = caseIgnored.maxByOrNull { it.history.size } ?: return null
|
||||
|
||||
caseIgnored.minus(oldest).forEach {
|
||||
log(tag) { "searchHistory2: Removing outlier: $it" }
|
||||
knownDevices.remove(it.id)
|
||||
}
|
||||
|
||||
oldest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
abstract class SingleApplePodsFactory(private val tag: String) : ApplePodsFactory<SingleApplePods>(tag)
|
||||
@@ -14,27 +14,35 @@ data class UnknownAppleDevice(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>
|
||||
override val confidence: Float = 0f,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : ApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
||||
|
||||
override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label)
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : ApplePodsFactory<ApplePods>(TAG) {
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
proximityMessage: ProximityPairing.Message,
|
||||
): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
return UnknownAppleDevice(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,8 +15,10 @@ data class AirPodsGen1 constructor(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||
@@ -23,27 +26,32 @@ data class AirPodsGen1 constructor(
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = AirPodsGen1(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
val device = AirPodsGen1(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
return device
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,8 +15,10 @@ data class AirPodsGen2 constructor(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||
@@ -23,27 +26,32 @@ data class AirPodsGen2 constructor(
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
val device = AirPodsGen2(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
return device
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,35 +15,42 @@ data class AirPodsGen3 constructor(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
val device = AirPodsGen3(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.SingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,24 +15,32 @@ data class AirPodsMax(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : SingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
return AirPodsMax(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,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.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.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,8 +16,10 @@ data class AirPodsPro(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: LidState? = null
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||
@@ -23,33 +27,38 @@ data class AirPodsPro(
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val caseLidState: LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
val device = AirPodsPro(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x0e20.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro")
|
||||
companion object {
|
||||
private val DEVICE_CODE = 0x0e20.toUShort()
|
||||
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro", "Factory")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,24 +15,32 @@ data class BeatsFlex(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
return BeatsFlex(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,24 +15,31 @@ data class BeatsSolo3(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
return BeatsSolo3(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,24 +15,29 @@ data class BeatsStudio3(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
return BeatsStudio3(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,24 +15,32 @@ data class BeatsX(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
return BeatsX(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,23 +15,31 @@ data class PowerBeats3(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
) : BasicSingleApplePods {
|
||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
val basic = PowerBeats3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
return PowerBeats3(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
@@ -14,8 +15,10 @@ data class PowerBeatsPro(
|
||||
override val lastSeenAt: Instant = Instant.now(),
|
||||
override val scanResult: BleScanResult,
|
||||
override val proximityMessage: ProximityPairing.Message,
|
||||
private val cachedBatteryPercentage: Float?,
|
||||
override val rssiHistory: List<Int>,
|
||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||
private val rssiAverage: Int? = null,
|
||||
private val cachedBatteryPercentage: Float? = null,
|
||||
private val cachedCaseState: DualApplePods.LidState? = null
|
||||
) : DualApplePods {
|
||||
|
||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||
@@ -23,27 +26,33 @@ data class PowerBeatsPro(
|
||||
override val batteryCasePercent: Float?
|
||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||
|
||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
||||
override val caseLidState: DualApplePods.LidState
|
||||
get() = cachedCaseState ?: super.caseLidState
|
||||
|
||||
override val rssi: Int
|
||||
get() = rssiAverage ?: super.rssi
|
||||
|
||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||
|
||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||
|
||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||
val recognized = recognizeDevice(scanResult, proximityMessage)
|
||||
var basic = PowerBeatsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||
val result = searchHistory(basic)
|
||||
|
||||
val device = PowerBeatsPro(
|
||||
identifier = recognized.identifier,
|
||||
scanResult = scanResult,
|
||||
proximityMessage = proximityMessage,
|
||||
cachedBatteryPercentage = cachedValues[recognized.identifier]?.caseBatteryPercentage,
|
||||
rssiHistory = recognized.rssiHistory,
|
||||
if (result != null) basic = basic.copy(identifier = result.id)
|
||||
updateHistory(basic)
|
||||
|
||||
if (result == null) return basic
|
||||
|
||||
return basic.copy(
|
||||
identifier = result.id,
|
||||
confidence = result.confidence,
|
||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||
rssiAverage = result.averageRssi(basic.rssi),
|
||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||
)
|
||||
|
||||
cachedValues[recognized.identifier] = ValueCache(
|
||||
caseBatteryPercentage = device.batteryCasePercent
|
||||
)
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package eu.darken.capod.pods.core.apple.protocol
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import dagger.Reusable
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -15,24 +13,6 @@ object ProximityPairing {
|
||||
val length: Int,
|
||||
val data: UByteArray
|
||||
) {
|
||||
data class Markers(
|
||||
val vendor: UByte,
|
||||
val length: UByte,
|
||||
val device: UShort,
|
||||
val podBatteryData: Set<UShort>,
|
||||
val caseBatteryData: UShort,
|
||||
val deviceColor: UByte,
|
||||
)
|
||||
|
||||
fun getRecogMarkers(): Markers = Markers(
|
||||
vendor = CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
||||
length = PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||
device = (((data[1].toInt() and 255) shl 8) or (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]
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
|
||||
@@ -81,6 +61,6 @@ object ProximityPairing {
|
||||
}
|
||||
|
||||
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
||||
private val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
||||
private const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
||||
internal val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
||||
internal const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,5c-3.87,0 -7,3.13 -7,7h2c0,-2.76 2.24,-5 5,-5s5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM13,14.29c0.88,-0.39 1.5,-1.26 1.5,-2.29 0,-1.38 -1.12,-2.5 -2.5,-2.5S9.5,10.62 9.5,12c0,1.02 0.62,1.9 1.5,2.29v3.3L7.59,21 9,22.41l3,-3 3,3L16.41,21 13,17.59v-3.3zM12,1C5.93,1 1,5.93 1,12h2c0,-4.97 4.03,-9 9,-9s9,4.03 9,9h2c0,-6.07 -4.93,-11 -11,-11z" />
|
||||
</vector>
|
||||
@@ -79,7 +79,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reception"
|
||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reception"
|
||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reception"
|
||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reception"
|
||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
|
||||
Reference in New Issue
Block a user