From af12a0e5cb2bfef9d563f859ac5c949ab5dd7b50 Mon Sep 17 00:00:00 2001 From: darken Date: Thu, 27 Jan 2022 22:45:47 +0100 Subject: [PATCH] Improved signal quality heuristic and value caching. --- .../capod/common/bluetooth/BleScanner.kt | 6 +- .../darken/capod/monitor/core/PodMonitor.kt | 56 ++++---- .../eu/darken/capod/pods/core/PodDevice.kt | 16 ++- .../capod/pods/core/apple/AppleFactory.kt | 2 +- .../pods/core/apple/AppleFactoryModule.kt | 22 +-- .../darken/capod/pods/core/apple/ApplePods.kt | 105 -------------- .../capod/pods/core/apple/ApplePodsFactory.kt | 136 ++++++++++++++++++ .../core/apple/BasicSingleApplePodsFactory.kt | 3 + .../capod/pods/core/apple/DualApplePods.kt | 1 + .../pods/core/apple/DualApplePodsFactory.kt | 84 +++++++++++ .../pods/core/apple/SingleApplePodsFactory.kt | 3 + .../pods/core/apple/UnknownAppleDevice.kt | 24 ++-- .../pods/core/apple/airpods/AirPodsGen1.kt | 40 +++--- .../pods/core/apple/airpods/AirPodsGen2.kt | 40 +++--- .../pods/core/apple/airpods/AirPodsGen3.kt | 40 +++--- .../pods/core/apple/airpods/AirPodsMax.kt | 25 ++-- .../pods/core/apple/airpods/AirPodsPro.kt | 51 ++++--- .../capod/pods/core/apple/beats/BeatsFlex.kt | 25 ++-- .../capod/pods/core/apple/beats/BeatsSolo3.kt | 24 ++-- .../pods/core/apple/beats/BeatsStudio3.kt | 22 +-- .../capod/pods/core/apple/beats/BeatsX.kt | 25 ++-- .../pods/core/apple/beats/PowerBeats3.kt | 25 ++-- .../pods/core/apple/beats/PowerBeatsPro.kt | 41 +++--- .../core/apple/protocol/ProximityPairing.kt | 24 +--- .../ic_baseline_settings_input_antenna_24.xml | 10 ++ .../layout/overview_pods_apple_dual_item.xml | 2 +- .../overview_pods_apple_single_basic_item.xml | 2 +- .../overview_pods_apple_single_item.xml | 2 +- .../res/layout/overview_pods_unknown_item.xml | 2 +- 29 files changed, 542 insertions(+), 316 deletions(-) create mode 100644 app/src/main/java/eu/darken/capod/pods/core/apple/ApplePodsFactory.kt create mode 100644 app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePodsFactory.kt create mode 100644 app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePodsFactory.kt create mode 100644 app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePodsFactory.kt create mode 100644 app/src/main/res/drawable/ic_baseline_settings_input_antenna_24.xml diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt index bc1f931d..34091cdb 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt @@ -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) } } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt index b1d40adb..dcfcf3dd 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt @@ -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() private val cacheLock = Mutex() + private suspend fun List.preFilterAndMap( + scannerMode: ScannerMode + ): List = 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> = 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() 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.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.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") diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt index 064ab2a3..394aab8b 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt @@ -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 - 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 + } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt index da157924..8df795aa 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt @@ -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, + private val podFactories: @JvmSuppressWildcards Set>, private val unknownAppleFactory: UnknownAppleDevice.Factory, ) { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt index d9793d23..9002e95a 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt @@ -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 + @Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory - @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 + @Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory - @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 + @Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory + @Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt index 7aaeef20..1e43d3a3 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt @@ -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() - - internal data class KnownDevice( - val identifier: PodDevice.Id, - val scanResult: BleScanResult, - val message: ProximityPairing.Message, - val rssiHistory: List = 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() - - 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 - } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePodsFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePodsFactory.kt new file mode 100644 index 00000000..6009220d --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePodsFactory.kt @@ -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(private val tag: String) { + + data class Markings( + val vendor: UByte, + val length: UByte, + val device: UShort, + val podBatteryData: Set, + 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 + ) { + 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() + + 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 +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePodsFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePodsFactory.kt new file mode 100644 index 00000000..6b2aeb3e --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePodsFactory.kt @@ -0,0 +1,3 @@ +package eu.darken.capod.pods.core.apple + +abstract class BasicSingleApplePodsFactory(private val tag: String) : ApplePodsFactory(tag) \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt index cfc161b7..cea24f71 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt @@ -182,4 +182,5 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase { constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes) } + } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePodsFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePodsFactory.kt new file mode 100644 index 00000000..4e5829c6 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePodsFactory.kt @@ -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(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.findSplitPodsMatch(device: DualApplePods): Collection { + val target = device.getCaseMatchMarkings() + + return filter { known -> + known.history + .filterIsInstance() + .any { it.getCaseMatchMarkings() == target } + } + } + + fun KnownDevice.getLatestCaseBattery(): Float? = history + .filterIsInstance() + .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() + .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 + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePodsFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePodsFactory.kt new file mode 100644 index 00000000..c1824edd --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePodsFactory.kt @@ -0,0 +1,3 @@ +package eu.darken.capod.pods.core.apple + +abstract class SingleApplePodsFactory(private val tag: String) : ApplePodsFactory(tag) \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt index 4d7ff023..c56b218f 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt @@ -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 + 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(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), ) } } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt index 6b231e93..0b1347f2 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt @@ -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 + 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 } } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt index cb2e23ca..7930518d 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt @@ -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, + 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 } } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen3.kt index 7a7a8fc2..d1311642 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen3.kt @@ -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, + 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 } } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt index b01f0cc4..7c80cd81 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt @@ -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, + 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), ) } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt index 8c8adc14..f806e378 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt @@ -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, + 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") + } } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt index be35feb4..28d8072a 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt @@ -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, + 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), ) } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt index dd322ec4..058dba44 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt @@ -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, + 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), ) } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt index f31205f8..a44341ec 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt @@ -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, + 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), ) } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt index 09e4db69..fda2ab87 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt @@ -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, + 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), ) } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt index 7e4c2086..05669c7b 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt @@ -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, + 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), ) } } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt index 28a78a36..a0a5937f 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt @@ -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, + 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 } } diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ProximityPairing.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ProximityPairing.kt index 961703c4..ea7ee515 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ProximityPairing.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/protocol/ProximityPairing.kt @@ -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, - 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 } diff --git a/app/src/main/res/drawable/ic_baseline_settings_input_antenna_24.xml b/app/src/main/res/drawable/ic_baseline_settings_input_antenna_24.xml new file mode 100644 index 00000000..febe5b79 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_settings_input_antenna_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/overview_pods_apple_dual_item.xml b/app/src/main/res/layout/overview_pods_apple_dual_item.xml index 42767d0f..f599d028 100644 --- a/app/src/main/res/layout/overview_pods_apple_dual_item.xml +++ b/app/src/main/res/layout/overview_pods_apple_dual_item.xml @@ -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)" />