From a1b9abd4cb1905fa2c8477c135204def49553e74 Mon Sep 17 00:00:00 2001 From: darken Date: Wed, 10 Jun 2026 10:33:03 +0200 Subject: [PATCH] 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 --- .../apple/ble/protocol/ProximityPairing.kt | 18 ++++++++++++++++++ .../ble/devices/airpods/AirPodsGen4Test.kt | 13 ++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/protocol/ProximityPairing.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/protocol/ProximityPairing.kt index a8a9b591..8f9ecf82 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/ble/protocol/ProximityPairing.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ble/protocol/ProximityPairing.kt @@ -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") } diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/ble/devices/airpods/AirPodsGen4Test.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/ble/devices/airpods/AirPodsGen4Test.kt index acd25181..9968b7ea 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/ble/devices/airpods/AirPodsGen4Test.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/ble/devices/airpods/AirPodsGen4Test.kt @@ -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 } } -} \ No newline at end of file + + @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("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 + } + } +}