Repository navigation
fix: automatic ear detection on AirPods Pro 2 below the RSSI floor - #237
Open
akhmadqasim wants to merge 2 commits into
Open
akhmadqasim wants to merge 2 commits into
akhmadqasim wants to merge 2 commits into
Conversation
Automatic ear detection never paused or resumed media on AirPods Pro 2 (USB-C) sitting a few metres from the PC. A --trace log showed the in-ear bits changing correctly in the advertisement, but `StateManager` rejected every advertisement below `rssi_min` before checking whether it came from the address already being tracked. The weak advertisements carrying the in-ear change were dropped, the lost timer starved, and 10 s later the cached state was wiped. The next accepted advertisement then arrived with `oldState == nullopt`, so `Manager::OnStateChanged` had nothing to compare against and never called `OnBothInEar`. - `StateManager::IsPossibleDesiredAdv` skips the RSSI floor when the advertisement address matches the cached advertisement of either side. Unknown addresses still have to clear the floor and the existing model/battery/rssiDiff heuristics. - New `Details::EarDetectionTracker` remembers the last "both in ear" value independently of `StateManager`'s cached state. `Manager` feeds it from `OnStateChanged` and resets it only when the classic Bluetooth link to the bound device drops or the device is rebound, not on the BLE "device is lost" path. - Log the in-ear transition on the enabled path of `OnBothInEar`. Tests: `ParsesInEarBitsPerBroadcastSide`, `AcceptsWeakAdvertisementFromTrackedAddress`, `EarDetectionTrackerSurvivesStateLoss` and `TraceReplayPausesOnceAndResumesOnce` (replays the captured Pro 2 sequence). `FiltersDuplicateAndWeakAdvertisements` now sends its weak advertisement from an unknown address, since a weak advertisement from the tracked address is accepted by design. Refs SpriteOvO#86, SpriteOvO#132
Hardware testing of the previous fix showed two remaining gaps when the
pods alternate which one broadcasts:
- `DoStateReset` drops a side's cached advertisement after 10 s of
silence, and the same-address bypass read its address from that cache.
When the quiet pod started broadcasting again it was treated as an
unknown address and rejected until a strong advertisement arrived, so
the in-ear change it carried was delayed or missed.
- Windows sometimes reports RSSI -127 ("unavailable") on the very
advertisement that carries an in-ear change. It was rejected by the
floor, and it also poisoned the rssiDiff heuristics (|-127 - x| > 50).
`StateManager` now keeps a per-side `_knownAddress` that is written in
`UpdateAdv`, survives `DoStateReset`/`DoLost`/`ResetAll`, and is cleared
only in `Disconnect()` (classic Bluetooth drop or rebind). The RSSI floor
consults it instead of the advertisement cache. RSSI -127 is treated as
unknown: it never participates in the rssiDiff checks, passes the floor
for a known address, and is rejected with a distinct log reason for an
unknown one.
`StateManager` takes an `Intervals` struct so tests can shorten the lost
and state-reset timers and exercise the real timer paths.
Tests: `RemembersSideAddressAcrossStateReset`,
`AcceptsUnknownRssiFromTrackedAddress`, and
`TraceReplayFollowsAlternatingBroadcaster` (replays the 08:53:25-08:54:20
hardware log). `TraceReplayPausesOnceAndResumesOnce` now reproduces the
"Device is lost" gaps with the real lost timer instead of `Disconnect()`.
Refs SpriteOvO#86, SpriteOvO#132
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #86 (automatic ear detection never pausing/resuming on AirPods Pro 2).
What was happening
A
--tracelog from AirPods Pro 2 (USB-C, model 0x2024) sitting a few metres from the PC showed the in-ear bits in the advertisement changing correctly (0x2b → 0x23 → 0x29 → 0x2b), but none of those changes ever reachedManager::OnBothInEar:StateManager::IsPossibleDesiredAdvrejects every advertisement belowrssi_min(default -80) before checking whether it comes from the address already being tracked. 26 of 38 advertisements in the log were dropped this way, including every one carrying an in-ear change (-82 / -88 dBm).DoStateReset/DoLostwipe the cached state.oldState == nullopt, soOnStateChangedhas nothing to compare against and never pauses or resumes.Two follow-up gaps showed up on hardware once that was fixed: the pods alternate which one broadcasts, and a side's cached advertisement (where the address was read from) is dropped after 10 s of silence; and Windows occasionally reports RSSI -127 ("unavailable") on exactly the advertisement that carries the change, which both failed the floor and poisoned the
rssiDiffheuristics.Changes
StateManagerkeeps a per-side_knownAddress, written inUpdateAdv, that survivesDoStateReset/DoLost/ResetAlland is cleared only inDisconnect()(classic Bluetooth drop or rebind). The RSSI floor is skipped for a known address; unknown addresses still have to clear the floor and the existing model/battery/rssiDiff heuristics.rssiDiffchecks, passes the floor for a known address, and is rejected with its own log reason for an unknown one.Details::EarDetectionTrackerremembers the last "both in ear" value independently ofStateManager's cached state.Managerfeeds it fromOnStateChangedand resets it only when the classic link to the bound device drops or the device is rebound — not on the BLE "device is lost" path.OnBothInEarnow logs the transition (only the disabled path did).StateManagertakes anIntervalsstruct so tests can shorten the lost/state-reset timers and exercise the real timer paths.Tests
Tests/AirPodsDomainTests.cpp:ParsesInEarBitsPerBroadcastSide,AcceptsWeakAdvertisementFromTrackedAddress,EarDetectionTrackerSurvivesStateLoss,RemembersSideAddressAcrossStateReset,AcceptsUnknownRssiFromTrackedAddress, and two trace replays of the captured Pro 2 sequences (TraceReplayPausesOnceAndResumesOnce,TraceReplayFollowsAlternatingBroadcaster) that expect exactly one pause and one resume.FiltersDuplicateAndWeakAdvertisementsnow sends its weak advertisement from an unknown address.Verified on hardware (AirPods Pro 2 USB-C, Windows 11): taking one pod out pauses within the next advertisement, putting it back resumes. Possibly related to #132 as well, since the state no longer flaps on weak signal.