fix(ble): Drop proximity messages with unsupported payload prefix

AirPods Gen4 emit a one-off type 0x07 frame from their identity address
on connect whose payload prefix is 0x07 instead of 0x01. Its bytes are
not the plaintext status format, but the model bytes happen to match,
so it minted a fresh device tracker and showed as a duplicate device
card until the stale timeout.

Every known plaintext status broadcast (including pairing mode) uses
prefix 0x01; pairing state lives in the suffix byte. Reject anything
else at the decoder, logging the dropped frame's hex.

Fixes #603
This commit is contained in:
darken
2026-06-10 10:44:48 +02:00
committed by Matthias Urhahn
parent 5d30844fa8
commit a1b9abd4cb
2 changed files with 30 additions and 1 deletions
@@ -2,7 +2,9 @@ package eu.darken.capod.pods.core.apple.ble.protocol
import android.bluetooth.le.ScanFilter
import dagger.Reusable
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import javax.inject.Inject
@@ -16,6 +18,17 @@ object ProximityPairing {
return null
}
// Pods also emit type 0x07 frames whose payload does not use the plaintext status
// format, e.g. prefix 0x07 from the identity address on connect (#603). Their bytes
// are garbage at the standard offsets and would create a ghost device.
if (message.data.firstOrNull() != PAIRING_MESSAGE_PREFIX) {
log(TAG, DEBUG) {
val hex = message.data.joinToString(" ") { "%02X".format(it.toInt()) }
"Dropping proximity pairing message with unsupported prefix: [$hex]"
}
return null
}
return ProximityMessage(
type = message.type,
length = message.length,
@@ -49,4 +62,9 @@ object ProximityPairing {
// This is the default message length among official Apple devices, clones may have different length
internal const val PAIRING_MESSAGE_LENGTH = 25
// First payload byte of every known plaintext status broadcast, including pairing mode
internal val PAIRING_MESSAGE_PREFIX = 0x01.toUByte()
private val TAG = logTag("Pod", "Apple", "ProximityPairing")
}
@@ -1,6 +1,7 @@
package eu.darken.capod.pods.core.apple.ble.devices.airpods
import eu.darken.capod.pods.core.apple.PodModel
import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot
import eu.darken.capod.pods.core.apple.ble.devices.BaseBlePodsTest
import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods
import eu.darken.capod.pods.core.apple.ble.devices.HasAppleColor
@@ -43,4 +44,14 @@ class AirPodsGen4Test : BaseBlePodsTest() {
model shouldBe PodModel.AIRPODS_GEN4
}
}
}
@Test
fun `frame with unsupported payload prefix is rejected - #603`() = runTest {
// Captured from AirPods Gen4 on connect: sent from the identity address, payload prefix
// 0x07 instead of 0x01, model bytes match but battery/color bytes are not plaintext.
// Decoding it as a status frame created a duplicate device card.
create<BlePodSnapshot?>("07 19 07 19 20 35 DF DE 46 76 CE 66 5C F4 6E BA B6 AC 1B 69 4C A7 D8 09 BF 9A 04") {
this shouldBe null
}
}
}