{"id":51069231,"url":"https://github.com/rrecio/bluetooth-audio-watchdog","last_synced_at":"2026-06-23T09:02:09.758Z","repository":{"id":361848200,"uuid":"1256109772","full_name":"rrecio/bluetooth-audio-watchdog","owner":"rrecio","description":"Watchdog that detects Bluetooth audio devices stuck in Connected-without-MediaTransport1 (the silent AirPods-on-Linux failure).","archived":false,"fork":false,"pushed_at":"2026-06-01T13:24:06.000Z","size":7,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T15:18:38.786Z","etag":null,"topics":["airpods","bash","bluetooth","bluez","linux","pipewire","systemd","watchdog","wireplumber"],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rrecio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-01T13:21:59.000Z","updated_at":"2026-06-01T13:25:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rrecio/bluetooth-audio-watchdog","commit_stats":null,"previous_names":["rrecio/bluetooth-audio-watchdog"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/rrecio/bluetooth-audio-watchdog","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrecio%2Fbluetooth-audio-watchdog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrecio%2Fbluetooth-audio-watchdog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrecio%2Fbluetooth-audio-watchdog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrecio%2Fbluetooth-audio-watchdog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rrecio","download_url":"https://codeload.github.com/rrecio/bluetooth-audio-watchdog/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rrecio%2Fbluetooth-audio-watchdog/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34682633,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["airpods","bash","bluetooth","bluez","linux","pipewire","systemd","watchdog","wireplumber"],"created_at":"2026-06-23T09:02:09.159Z","updated_at":"2026-06-23T09:02:09.753Z","avatar_url":"https://github.com/rrecio.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# bluetooth-audio-watchdog\n\nA small user-level systemd service that detects a silent failure mode in the\nLinux Bluetooth audio stack and tells you (and you alone — there's no fleet,\nno telemetry) what's actually wrong.\n\n## The failure it catches\n\nBlueZ can mark a Bluetooth headset as `Connected: true` while the audio\ntransport (`org.bluez.MediaTransport1`, i.e. A2DP/HFP) never gets established.\nWhen that happens:\n\n- AVRCP (media control) comes up, so the device looks \"connected\" in GNOME.\n- No PipeWire sink appears, so there is no audio output to pick.\n- Nothing in BlueZ, WirePlumber, or GNOME notices or surfaces the gap.\n\nAirPods on Linux land in this state regularly (paired in Find-My BLE mode,\nheld by another iCloud-bonded device, asleep in beacon mode, …). This\nwatchdog polls BlueZ and, when a device sits in that state past a threshold,\nclassifies *why* and fires a desktop notification + journald entry with a\nremediation that matches the actual cause.\n\n## Install\n\n```sh\n# clone wherever you like\ngit clone \u003crepo-url\u003e ~/src/bluetooth-audio-watchdog\ncd ~/src/bluetooth-audio-watchdog\n\n# symlink the binary and the user unit into the standard locations\nmkdir -p ~/.local/bin ~/.config/systemd/user\nln -s \"$PWD/bin/bluetooth-audio-watchdog\"           ~/.local/bin/bluetooth-audio-watchdog\nln -s \"$PWD/systemd/user/bluetooth-audio-watchdog.service\" \\\n                                                    ~/.config/systemd/user/bluetooth-audio-watchdog.service\n\nsystemctl --user daemon-reload\nsystemctl --user enable --now bluetooth-audio-watchdog\n```\n\nRequires `busctl`, `journalctl`, `notify-send`, `systemd-cat` — all present\non a default Ubuntu/GNOME desktop.\n\n## Configuration\n\nSet via `Environment=` in the unit, or by editing the unit override\n(`systemctl --user edit bluetooth-audio-watchdog`):\n\n| Variable | Default | Meaning |\n| --- | --- | --- |\n| `BT_WATCHDOG_THRESHOLD` | `15` | Seconds a device must sit in \"connected, no transport\" before alerting. |\n| `BT_WATCHDOG_POLL`      | `3`  | Poll interval in seconds. |\n| `BT_WATCHDOG_AUTOFIX`   | `0`  | If `1`, also call `Device1.Disconnect` on a stuck device after alerting. Off by default — alert-only is safer because legit transient states (AirPods returning to case briefly) can look identical for a few seconds. |\n\n## How it classifies\n\nAfter threshold the watchdog reads:\n\n- `BREDR.Paired` / `LE.Paired` from `busctl introspect`\n- `journalctl -u bluetooth` in the last 2 min, filtered by MAC, counting\n  AVDTP \"Connection reset by peer\" lines (`avdtp_err`) and any AVDTP\n  activity (`avdtp_any`)\n\nand picks one of four branches:\n\n| Branch | Trigger | Diagnosis | Suggested fix |\n| --- | --- | --- | --- |\n| **A** | `BREDR.Paired = false` and `LE.Paired = true` | Only an LE/Find-My bond exists; A2DP can never come up. | `bluetoothctl remove \u003cmac\u003e` then re-pair with the AirPods in your ears and the case button held until the LED is solid white. |\n| **B** | `avdtp_err \u003e 0` | Peer is rejecting the A2DP channel. | Turn Bluetooth off on other Apple devices on the same iCloud (Settings, not Control Center), take AirPods out of case, reconnect. |\n| **C** | `avdtp_any = 0` (no AVDTP attempt at all) | AirPods are asleep in BLE beacon mode and not responding to BR/EDR. | Make sure no other iCloud-bonded device is claiming them, put them in ears (in-ear sensor keeps them awake), then `bluetoothctl connect \u003cmac\u003e`. |\n| **D** | otherwise | A2DP/HFP didn't come up despite a clean pair. | `bluetoothctl disconnect \u003cmac\u003e; sleep 3; bluetoothctl connect \u003cmac\u003e`; check `journalctl -u bluetooth` for the AVDTP error. |\n\nThe `- Find My` suffix in the BlueZ device alias is intentionally **not** used\nas a signal — it just reflects whatever the AirPods are currently\nbroadcasting on BLE, which idle AirPods always do. The pairing record is the\nauthoritative source.\n\n## Subcommands\n\n```sh\nbluetooth-audio-watchdog                       # poll loop (what the unit runs)\nbluetooth-audio-watchdog --self-test           # 8 classify() cases, exit 0/1\nbluetooth-audio-watchdog --show-all            # print all 4 branch texts, no popups\nbluetooth-audio-watchdog --show-all notify     # also fire 4 real desktop popups\nbluetooth-audio-watchdog --alert B \u003cmac\u003e [name]  # emit one branch end-to-end\n```\n\n`--self-test` runs the pure `classify()` function against canned inputs and\nis the right thing to wire into CI or a pre-commit hook if you edit the\nheuristic.\n\n## Logs\n\n```sh\njournalctl --user -u bluetooth-audio-watchdog -f    # service stdout/stderr\njournalctl -t bt-audio-watchdog                     # structured warning entries\n```\n\n## Limitations\n\n- The classifier reads BR/EDR/LE pair state via `busctl introspect` parsing.\n  If BlueZ changes its bearer-interface shape, the awk extraction can fail\n  silently — `classify()` then treats both as unpaired and falls into branch\n  C, which is the least-bad default.\n- Only the user-session DBus is monitored; system-level bluetoothd state is\n  what's actually read, but `notify-send` requires a graphical session.\n- AVDTP log heuristics depend on bluez writing those exact debug lines,\n  which it does in BlueZ 5.85 (Ubuntu 26.04); other versions may need the\n  grep patterns adjusted.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frrecio%2Fbluetooth-audio-watchdog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frrecio%2Fbluetooth-audio-watchdog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frrecio%2Fbluetooth-audio-watchdog/lists"}