Skip to content

Recover Apple Silicon Bluetooth after resume - #21

Open
malik-na wants to merge 3 commits into
mainfrom
port/498-bluetooth-resume-recovery
Open

malik-na wants to merge 3 commits into
mainfrom
port/498-bluetooth-resume-recovery

Conversation

@malik-na

@malik-na malik-na commented Oct 8, 2026

Copy link
Copy Markdown
Member

Port of omacom/omarchy-mac#498 by @n0mahd, with the recovery moved into omarchy-mac/. Co-authored commit.

After s2idle the Broadcom Bluetooth firmware can stop answering HCI commands (hci0: command 0x0c01 tx timeout), and only rebinding hci_bcm4377 brings it back. omarchy-bluetooth-resume-fix.service, a vendor unit ordered after the sleep targets, rebinds the Bluetooth PCI function only when that signature appears after the latest suspend-entry journal cursor, and leaves healthy controllers and a radio you turned off alone.

Changes from the source PR:

  • Gated by lib/bluetooth-supported on omarchy-hw-platform and the PCI ID, and enabled once by omarchy-mac-setup-system, which keeps administrator overrides, masks and later disables. The quattro migration is dropped.
  • A second commit adds BCM4388 (14e4:5f72): @oliverlukschander saw the same wedge on an M2 Pro in the source PR's review. @oliverlukschander, could you check this branch on that machine?
  • omarchy-mac goes to 0.1.1.

Tested: CI's scope, architecture-gate and fixture-drift checks pass locally; the package and integration suites run in CI. The source PR verified the recovery on an M1 MacBook Air (BCM4378). Still needed before release: suspend/resume on a BCM4387 and a BCM4388 Mac.

🤖 Generated with Claude Code

malik-na and others added 2 commits October 9, 2026 02:07
Port omacom/omarchy-mac#498 into the hardware package, with controller-specific journal recovery and setup that preserves administrator choices.

Co-authored-by: n0mahd <39080654+n0mahd@users.noreply.github.com>
On an M2 Pro (t6020, 14e4:5f72) the controller wedged across 2 of 7
suspends with the same HCI tx timeout signature before PM: suspend exit,
and reloading hci_bcm4377 brought it back, as reported in the review of
omacom/omarchy-mac#498. The source PR left BCM4388 out only because its
one report then was the rfkill path; this adds it to the gate, the setup
tests, the README and the manual.

Co-authored-by: Oliver Lukschander <33756270+oliverlukschander@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@maralcbr

maralcbr commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Thanks @malik-na, this is a useful port. A few things need changing before merge:

  1. Reach Macs that upgrade. setup-system is only run at install, a hardware rerun or first boot, and the migration from the source PR was dropped, so existing installs never get the unit enabled. Either enable it on upgrade or document the manual step.
  2. Don't let a Bluetooth failure stop the rest of setup. Under set -e, a failed systemctl enable (setup-system:75) or detection error (:78-80) aborts the speaker and launcher setup that follows. Finish the unrelated setup, then exit nonzero with the marker still pending so it retries.
  3. Tie the journal check to the current suspend cycle (omarchy-bluetooth-resume-fix:85-98). As written, a timeout from an earlier cycle can trigger a rebind. Also either handle the hibernation path properly or drop the hibernate targets from the unit (.service:3,13).
  4. Soften the docs. A tx timeout shows a timeout happened, not that the controller is still stuck, so "healthy controllers are left alone" (README, manual) and "wedged controller confirmed" overclaim.
  5. Add tests: timeouts before the current suspend but after a saved cursor; hci0 disappearing on unbind and returning on bind; setup continuing after a Bluetooth failure; a second suspend during the 20-pass watch.

For release (not the source merge): a real lid-close resume through the unit on both BCM4387 and BCM4388.

Review of #21:

- A wants link under /usr/lib/systemd/system/suspend.target.wants runs the
  unit after every suspend on installs and upgrades alike, as the audio
  watchdog's link does, so Macs that only update get it. System setup no
  longer enables it, so a Bluetooth detection failure can no longer stop
  the speaker and launcher setup after it. Turning it off is a mask.
- The watch starts from this suspend's entry: the kernel counts suspend
  attempts in /sys/power/suspend_stats, and the command waits up to 10 s
  for journald to hold that entry before taking its cursor, so an earlier
  cycle's timeouts are never read as this one's. Without it, it watches a
  bounded window from now. Journal reads stay within this boot.
- A suspend during the 20 s watch moves it to the newer resume and restarts
  the watch: systemd will not start the unit again while it runs.
- Hibernation is dropped from the unit; Apple Silicon Macs don't hibernate.
- The README, manual and messages say what was seen (an HCI command
  timeout) instead of claiming a confirmed wedge or a healthy controller.
- Tests: a fixture journal that changes over time covers timeouts after an
  earlier cycle's entry, a lagging journal, an entry that never arrives, a
  second suspend during the watch, hci0 leaving on unbind and returning on
  bind (or not), and setup finishing after a Bluetooth detection failure.
@malik-na

malik-na commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Thanks @maralcbr. Changes in 1a499ff:

  1. Upgrades: the package ships a wants link under /usr/lib/systemd/system/suspend.target.wants, as the audio watchdog does, so installs and upgrades both get the unit. setup-system no longer enables it; the runtime only runs setup from the install leaves. Turning it off is a mask, and the README and manual say so.
  2. Setup: with no Bluetooth step left, setup-system is back to main's. A test checks that setup finishes, speaker safety included, when lspci fails.
  3. Current cycle: the watch waits up to 10 s for journald to hold the suspend entry the kernel counted in /sys/power/suspend_stats before taking its cursor. Otherwise it falls back to a window from now, and it reads only this boot. A suspend during the watch moves it to the newer entry and restarts the 20 passes. Hibernate targets dropped.
  4. Docs and messages say "HCI command timeout", and note that a rebind may hit a controller that would have recovered.
  5. Tests: a fixture journal that changes over time covers your four cases, plus a lagging journal and an entry that never arrives.

Tested:

  • omarchy-mac/test/all and test/integration/all (runtime 8614160) pass natively on an M1 Air.
  • On the Air (J313, BCM4378), the package built from this commit was installed without running setup, as an update would be. The unit was wanted by suspend.target and ran after 3 pm_test=devices suspends.
  • A second suspend during the watch logged "Suspended again while watching" and watched 20 s from the newer resume.

No wedge occurred, so the rebind itself is covered only by the tests. Still needed for release: lid-close resumes on BCM4387 and BCM4388.

@scottjones

Copy link
Copy Markdown
Collaborator

Thanks @malik-na, this is really careful work. You covered all five of @maralcbr's points, and the logic holds up. Anchoring the watch on the PM: suspend entry count from /sys/power/suspend_stats ties it to this cycle and nothing earlier, and the rebind only touches Bluetooth's PCI function, so Wi-Fi on the same chip stays out of it.

It can't land as is, though, now that #26 has merged and moved main underneath it. Notes against 1a499ff:

Blocking

  1. omarchy-mac/lib/bluetooth-supported:5 compares against the literal apple-silicon. On quattro's runtime (omacom/omarchy@e1b0e5e9, main's CI pin since Follow Omarchy's x86 / aarch64 / aarch64-apple platform names #26), omarchy-hw-platform prints aarch64-apple, so the gate exits 1 and the unit's ExecCondition skips the fix. I ran e1b0e5e9's detector on my 14" M2 Max to confirm. The pattern Follow Omarchy's x86 / aarch64 / aarch64-apple platform names #26 uses in omarchy-mac-setup-system works here and keeps your exit codes:
    omarchy-hw-platform >/dev/null || exit 2
    omarchy-hw-apple-silicon || exit 1
  2. The tests stub the detector as apple-silicon (omarchy-mac/test/bluetooth-test.sh:9-10, :171 and :293, and test/integration/bluetooth-setup-test.sh:13), so they pass while the real runtime skips the fix. Switch them to aarch64-apple and stub the omarchy-hw-apple-silicon predicate. Both base-test.sh helpers on main have stub_apple_predicate for that.
  3. Version: omarchy-mac/version and omarchy-mac/README.md:3 set 0.1.1, which Skip the Electron wrapping on a runtime without its helpers #23 already shipped, and Follow Omarchy's x86 / aarch64 / aarch64-apple platform names #26 took 0.1.2. After the rebase this needs 0.1.3.

Smaller

  • The PR body is out of date. It still says setup enables the unit and honours later disables, but the package now ships a vendor suspend.target.wants link, so only systemctl mask turns it off.
  • The bare command tx timeout form, with no opcode, isn't matched (omarchy-bluetooth-resume-fix:69). That's only ever a missed recovery, never a false one, so it's optional.

Hardware

I can be one of the two test machines you still need. My MacBook Pro 14" M2 Max (J414c) has a BCM4388: Bluetooth at 01:00.1 is 14e4:5f72 (BCM4388_DEVICE_ID in hci_bcm4377) and Wi-Fi at 01:00.0 is brcmfmac's 14e4:4434. So it covers BCM4388, not BCM4387. Once the rebase is in, I'll run a lid-close resume through the unit, plus a manual unbind/bind of 01:00.1 with Wi-Fi up, to check Wi-Fi survives the reset without waiting for a real wedge.

If you'd rather I push the rebase myself, I'm happy to. The branch is in this repo, so just say the word.

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.

3 participants