feat: Integrate AAP connection freshness into signal quality indicator

This commit is contained in:
darken
2026-03-31 19:17:09 +02:00
committed by Matthias Urhahn
parent a8593eecd3
commit c860ecc430
5 changed files with 137 additions and 4 deletions
@@ -15,6 +15,7 @@ import eu.darken.capod.pods.core.apple.ble.SingleBlePodSnapshot
import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods
import eu.darken.capod.pods.core.apple.aap.AapPodState
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
import java.time.Duration
import java.time.Instant
/**
@@ -45,9 +46,26 @@ data class PodDevice(
// Signal / timing
val seenLastAt: Instant? get() = ble?.seenLastAt
val seenFirstAt: Instant? get() = ble?.seenFirstAt
val signalQuality: Float get() = ble?.signalQuality ?: 0f
val signalQuality: Float
get() {
val bleQuality = ble?.signalQuality ?: 0f
return (bleQuality + computeAapBoost(Instant.now())).coerceAtMost(1f)
}
val rssi: Int get() = ble?.rssi ?: 0
internal fun computeAapBoost(now: Instant): Float {
val state = aap ?: return 0f
if (state.connectionState != AapPodState.ConnectionState.READY) return 0.05f
val lastMessage = state.lastMessageAt ?: return 0.05f
val ageSeconds = Duration.between(lastMessage, now).seconds
return when {
ageSeconds < 0 -> 0.05f // future timestamp (defensive)
ageSeconds < 10 -> 0.15f // fresh
ageSeconds < 30 -> 0.10f // warm
else -> 0.05f // stale
}
}
// Battery — AAP preferred, BLE fallback
val batteryLeft: Float?
get() = aap?.batteryLeft ?: (ble as? DualBlePodSnapshot)?.batteryLeftPodPercent
@@ -13,6 +13,7 @@ import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceProfile
import eu.darken.capod.pods.core.apple.aap.protocol.AapFramer
import eu.darken.capod.pods.core.apple.aap.protocol.AapMessage
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
import java.time.Instant
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -179,7 +180,7 @@ internal class AapConnection(
// Try battery
profile.decodeBattery(message)?.let { batteries ->
_state.value = _state.value.copy(batteries = batteries)
_state.value = _state.value.copy(batteries = batteries, lastMessageAt = Instant.now())
log(TAG) { "Battery update: ${batteries.entries.map { "${it.key}=${(it.value.percent * 100).toInt()}% ${it.value.charging}" }}" }
return
}
@@ -187,20 +188,21 @@ internal class AapConnection(
// Try private key response
profile.decodePrivateKeyResponse(message)?.let { keys ->
log(TAG) { "Private keys received: IRK=${keys.irk != null}, ENC=${keys.encKey != null}" }
_state.value = _state.value.copy(lastMessageAt = Instant.now())
_keysReceived.tryEmit(keys)
return
}
// Try device info
profile.decodeDeviceInfo(message)?.let { info ->
_state.value = _state.value.copy(deviceInfo = info)
_state.value = _state.value.copy(deviceInfo = info, lastMessageAt = Instant.now())
log(TAG) { "Device info: ${info.name} (${info.modelNumber})" }
return
}
// Try setting update (merge into existing state)
profile.decodeSetting(message)?.let { (key, value) ->
_state.value = _state.value.withSetting(key, value)
_state.value = _state.value.withSetting(key, value).copy(lastMessageAt = Instant.now())
log(TAG) { "Setting: ${key.simpleName} = $value" }
return
}
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.aap
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
import java.time.Instant
import kotlin.reflect.KClass
/**
@@ -12,6 +13,7 @@ data class AapPodState(
val deviceInfo: AapDeviceInfo? = null,
val settings: Map<KClass<out AapSetting>, AapSetting> = emptyMap(),
val batteries: Map<BatteryType, Battery> = emptyMap(),
val lastMessageAt: Instant? = null,
) {
inline fun <reified T : AapSetting> setting(): T? = settings[T::class] as? T