Skip to content

Reconnect AirPods after a day away (0.3.2) - #874

Closed
DaBarkle wants to merge 134 commits into
librepods-org:mainfrom
DaBarkle:claude/airpods-reconnect-after-away
Closed

DaBarkle wants to merge 134 commits into
librepods-org:mainfrom
DaBarkle:claude/airpods-reconnect-after-away

Conversation

@DaBarkle

Copy link
Copy Markdown

Requested by David

Problem

David came home after a workday and his AirPods didn't reconnect to the PC. Logs on the PC showed the AirPods dropped at 05:56 and the app then made zero connection attempts for 18 hours.

Cause: drops were only retried eagerly when the AirPods went into the case or handed over to an iPhone. Walking out of range while wearing them fell through to the advert path, which needs the PC to recognise the AirPods' BLE adverts (they often only send Find My beacons) and gives up after AUTO_RECONNECT_WINDOW (4 h).

Fix

  • Every AirPods drop now gets the "until they come back" retry schedule (20 s, then 1 min, then every 3 min), like a case drop.
  • To keep "disconnect here means stay disconnected", the new disconnect_watch module listens for BlueZ's org.bluez.Device1.Disconnected(reason, message) signal (BlueZ 5.87 on Bazzite has it). Reason.Local means this PC closed the link (KDE menu, bluetoothctl), so we stop retrying. Timeout is range loss and always retries until back. Peer hand-offs keep their 3-minute window.
  • The decision is a pure function, disconnect_watch::plan, run when the drop is noticed and again when the reason arrives, in either order.
  • Release 0.3.2: version bump, CHANGELOG, AppStream release notes.

Tests

Unit tests for reason parsing, object path to address, and the plan for each kind of drop (including the regression: Timeout/no reason, not in case, not peer, gives UntilBack). Not built locally because David was gaming; CI runs fmt, clippy and tests.

Notes

Also found the installed Flatpak is still 0.2.2, so 0.3.x was never installed on the PC.

🤖 Generated with Claude Code

should probably have an indication when not connected
tbh, I did not want to get results right away; batching would have been fine. But, BlueR seems to give the first scan result only after 10s if I set sampling period.
finally! only copying all ui from android to first release
claude and others added 28 commits October 25, 2026 16:21
When the iPhone takes the AirPods, BazzitePods pauses Brave. For a moment
afterwards Brave still reports playing (and its stream stays uncorked), so
both the route watcher and the playback listener saw "media playing here"
and took the AirPods straight back, three times in seven seconds. Ignore
playback for a few seconds after pausing players for another device.

Also only pass ear detection to the media controller when the status
changed: the AirPods repeat it, and each repeat paused the players again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nw15uJa3wJ9BwDHoBunZoh
A take-back within 20 s of the previous rebuild didn't rebuild the output,
so after a quick switch back the AirPods stayed silent until the stall
check caught it 20 s later. The long limit was there to stop the PC and
iPhone bouncing, which is now prevented where it starts, so keep only a
3 s window that merges duplicates. Also skip the rebuild for a take-back
in the first 10 s after the link comes up: the output was just built, and
that rebuild only cost a speaker mute and a slot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nw15uJa3wJ9BwDHoBunZoh
Picking the AirPods in Control Center only shows up as OwnsConnection 00:
no Smart Routing media message, and the Audio Source report that follows
found the route already marked as away, so the switch was never recorded.
The PC then ignored the iPhone letting go, and only the stall check
brought the audio back, 11 s later, followed by a second rebuild.

Record the loss and pause the stall checks on OwnsConnection 00 too, treat
any switch as a borrow until the other device says it is playing media,
pause stall checks for every switch, and let a stall rebuild count towards
the duplicate window so a take-back right after it doesn't rebuild again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nw15uJa3wJ9BwDHoBunZoh
When an iPhone that wasn't connected joins the AirPods, they can drop the
PC's link entirely. With no link the PC never hears the iPhone let go, so
the AirPods stayed away. Two fixes:

- The AirPods task ends by itself when the link drops, usually before
  BlueZ reports the disconnect, and the supervisor pruned it as "ended on
  its own" without recording the lost link. Record it, so the existing
  reconnect-on-advert path works for any drop.
- When the drop follows another device joining the AirPods within 10 s,
  retry the connection every 20 s for 3 minutes without waiting for
  adverts. Connecting doesn't take the audio from the iPhone. Other drops,
  such as disconnecting here, don't get these retries.

Reconnect attempts and failures are now logged at info level.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nw15uJa3wJ9BwDHoBunZoh
…dverts

Out of the case and busy with an iPhone, the AirPods advertise only Find My
beacons from static random addresses, which the IRK can never resolve, so
the reconnect-on-advert path never fired and the AirPods stayed off the PC.

When they were put in the case before the drop, keep asking BlueZ to
connect: every 20 s for 3 minutes, then every minute for up to 30 minutes.
A failed attempt costs nothing and connecting never takes the audio from an
iPhone that is playing. Drops after a local disconnect while worn still get
no retries, so disconnecting the AirPods here keeps them disconnected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nw15uJa3wJ9BwDHoBunZoh
Merges Dependabot PRs by itself only when they are patch/minor Cargo
updates under linux-rust/ and the Flatpak build of that exact commit
passed. Major bumps, other ecosystems and failed builds stay open for
review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GafSgAbeHTKRmrNro1JnrS
Retries after the AirPods went into the case stopped after 30 minutes, so
after a day out they stayed off the PC. Keep retrying, every 3 minutes after
the first half hour, until they connect. AirPods that are away when the app
starts or the PC wakes from sleep are retried the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nw15uJa3wJ9BwDHoBunZoh
Auto-merge safe Dependabot security updates
Bumps the cargo group with 1 update in the /linux-rust directory: [rand](https://github.com/rust-random/rand).


Updates `rand` from 0.9.2 to 0.9.5
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.9.5/CHANGELOG.md)
- [Commits](rust-random/rand@rand_core-0.9.2...0.9.5)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.9.5
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
…un them in CI

- New test files beside each module (aacp, battery, le, overlay_feed, hub, settings,
  tray, pulse, utils): about 100 new tests on top of the existing 124, none needing
  Bluetooth, a sound server or a display.
- overlay_feed: the notification fallback now returns its notifications and the overlay
  settings parse from a string, so both are testable without D-Bus or disk.
- tests.yml runs the suite with a coverage report on every PR and on main/bazzite, with a
  floor so coverage can't quietly drop.
- Dependabot auto-merge now waits for the tests as well as the Flatpak build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HkZQiV1RZAkdtPGA9VxDw
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HkZQiV1RZAkdtPGA9VxDw
The offline Flatpak build vendors crates from cargo-sources.json, which
still listed rand 0.9.2 after the Cargo.lock bump.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GafSgAbeHTKRmrNro1JnrS
Grouped security updates (the only kind enabled) report no update-type,
so the first real Dependabot PR (#8, rand 0.9.2 -> 0.9.5) was skipped.
Compare Cargo.lock at base and head instead: merge only when every
changed crate stays within its semver-compatible range, no crate is
added, and only linux-rust Cargo files changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GafSgAbeHTKRmrNro1JnrS
Add automated tests for the app logic and run them on every PR
0.2.2: every switching scenario covered, overlay switch feedback, release prep
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kak6d8qzQLuZMFxXsJNCfz
…-a1ec0912cc

build(deps): bump rand from 0.9.2 to 0.9.5 in /linux-rust in the cargo group across 1 directory
Fix Dependabot auto-merge skipping grouped security updates
The overlay becomes an island modelled on the iPhone's Dynamic Island:
a card on connect, a slim island at rest (Compact), and one-line alerts
for charging, low battery, noise control changes, the audio moving to or
from another device, and disconnecting.

- spring.rs: damped springs with SwiftUI's duration/bounce mapping; a
  retarget keeps velocity, so interrupted transitions bend smoothly.
- island.rs: content model, shapes, cross-fade, ring sweep, rolling
  numbers and caption, all pure and unit tested.
- logic.rs: event detection with priorities, mode handling, exact
  deadlines instead of 200 ms polling.
- view.rs/glyphs.rs: island drawing; phone, monitor, battery and noise
  control glyphs. Contents are clipped to the island and revealed as it
  grows.
- Frames are paced by compositor frame callbacks and only requested while
  something moves; at rest the overlay uses no CPU and no wakeups. The
  surface has a fixed size so it is never resized while shown.
- Protocol: optional listening_mode. Default position is top center;
  Glass 0% is pure black.
- --demo-story plays a scripted tour for recordings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJov27xWCsRzbVRci4CYKk
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJov27xWCsRzbVRci4CYKk
… this PC

Gauges no longer grey out every level just because the classic link is down: a fresh
reading from the AirPods' adverts (pods in the case on a charger, case lid open) is shown
live, charging included. When the link drops, what the control channel last said turns
into "last seen" at once instead of looking live for up to a minute.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186cCkmreRwvodd6Q6cZDj3
The control channel reports every setting a model has. The window now keeps those values in
the hub and offers automatic ear detection, Adaptive Audio strength, noise cancellation
with one AirPod, microphone choice and automatic connection, each only when the AirPods
report it. Personalized volume now shows the AirPods' real state instead of a local guess.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186cCkmreRwvodd6Q6cZDj3
… case

AirPods only report the case while it is open with a pod inside; with both pods in a closed
case the link drops and nothing reports. The window and tray now say "In case" instead of
"Not connected" and hint "open the lid to update" under the case's last reading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186cCkmreRwvodd6Q6cZDj3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186cCkmreRwvodd6Q6cZDj3
Overlay: redesign as a Dynamic Island (v0.3.0)
Bring #12 to main: battery in the case, charging while away, more AirPods settings (0.3.1)
AirPods worn out of range only came back if the PC matched their adverts,
and only for 4 hours. Treat every drop like a case drop (keep retrying),
except when BlueZ says this PC closed the link on purpose: listen for
org.bluez.Device1.Disconnected and stay disconnected on Reason.Local.

Release 0.3.2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@DaBarkle

Copy link
Copy Markdown
Author

Opened here by mistake, sorry; this was meant for a fork.

@DaBarkle DaBarkle closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants