Apple AirPods, and most modern Bluetooth headphones, can be paired to multiple devices at once. This is great because Bluetooth pairing is often a lengthy and involved process. However, when multiple paired devices are powered on and in the proximity of the headphone, it is not clear which device should, and will, take over the connection.

Apple OSes try to be smart about this by switching the connection automatically, for example from listening to music on your laptop to answering a call on your phone. This works most of the time but it is unreliable enough that I do not trust it and have therefore turned it off.

I prefer simple explicit solutions in my workflows and Apple’s unreliable switching does not exist on Linux anyway. So I came up with my own solution.

I had two objectives:

  • I wanted to be in charge of when my computer connects to the headphones and takes over the connection. I use AirPods with my phone most of the time and wanted to occasionally and intentionally use them with my computer.
  • I wanted to be able to connect/disconnect quickly and easily. My arch installation is pretty bare bone, so I had to come up with a minimal interface.

I made a simple Polybar module that shows the connection status of my headphones. Clicking on it toggles the connection:

polybar module for connecting to airpods

The important part is making sure that any disconnect event (either from an intentional click on the Polybar module, or because of any other reason) is immediately followed by putting that device into “blocked” mode. This stops the Bluetooth controller from trying to connect later.

Initial Paring

I followed the generic pairing guide on Arch wiki for the one-time initial pairing. However, Apple AirPods do not seem to support Bluetooth LE pairing, so you have to set the pairing mode to classic br/edr by setting ControllerMode in /etc/bluetooth/main.conf:

ControllerMode = bredr

After making that change and restarting the Bluetooth service you should be able to follow the steps in the wiki and pair your headphones.

The main script

This is the top-level script that deals with all the Bluetooth operations. The first argument is the mode of operation. The second argument is the Bluetooth address of the device. It has three modes of operation:

  • return the current status of the connection (used by the Polybar module)
  • toggle the connection (used when clicking on the Polybar module)
  • block the device (triggered by any device disconnect event)
#!/usr/bin/env bash

# Dependencies: Materials Icon font for the glyphs

device=$2

grey=#928374
green=#55aa55
yellow=#fabd2f

service_running=$(systemctl is-active "bluetooth.service")
controller_on=$(bluetoothctl show | grep "Powered: yes")
device_paired=$(bluetoothctl devices Paired | grep $device)
device_connected=$(bluetoothctl info $device | grep "Connected: yes")
device_blocked=$(bluetoothctl info $device | grep "Blocked: yes")

status_str() {
    if [[ $service_running ]] && [[ $controller_on ]] && [[ $device_paired ]]; then
        if [[ $device_connected ]]; then
            echo %{T4}%{F"$green"}%{T-}%{F-}
        elif [[ $controller_on ]] && [[ ! $device_blocked ]];then
            echo %{T4}%{F"$yellow"}%{T-}%{F-}
        else
            echo %{T4}%{T-}
        fi
    else
        echo %{T4}%{F"$grey"}%{T-}%{F-}
    fi
}

toggle_state() {
    if [[ ! $device_paired ]];then
      return 1
    fi

    if [[ $device_connected ]]; then
        bluetoothctl disconnect $device
        bluetoothctl block $device
    elif [[ $controller_on ]] && [[ ! $device_blocked ]];then
        bluetoothctl block $device
    elif [[ $controller_on ]] && [[ $device_blocked ]];then
        bluetoothctl unblock $device
        bluetoothctl connect $device
    else
        bluetoothctl power on
        bluetoothctl unblock $device
        bluetoothctl connect $device
    fi
}

maybe_block() {
    if [[ $controller_on ]] && [[ $device_paired ]] && [[ ! $device_connected ]] && [[ ! $device_blocked ]];then
        bluetoothctl block $device
    fi
}

case "$1" in
    --toggle) 
        toggle_state
        ;;
    --status)
        status_str
        ;;
    --block-if-not-connected)
        maybe_block
        ;;
esac

The Polybar module

The Polybar module is quite simple and just calls the script above in two modes. It’s tied to a specific device (i.e. has the Bluetooth address hard-coded) so I don’t have to think about it at all. Normally it’s just displaying the current status of the connection: green if connected, yellow if attempting to connect, or greyed out if not connected or if the Bluetooth controller is off. Clicking the module toggles the connection and lets you explicitly connect or disconnect:

[module/airpods]
type = custom/script
exec = ~/.scripts/bluetooth-actions --status AA:BB:CC:DD:EE:FF
tail = false
interval = 2
click-left = ~/.scripts/bluetooth-actions --toggle AA:BB:CC:DD:EE:FF &

Auto block after disconnect

If I disconnect through the Polybar module the script makes sure the device is blocked but there are other scenarios where a connection can drop: I can just put the headphones back in their case, I can walk away from my computer, etc. In these scenarios I want my computer to immediately go back to the blocked mode. This can be achieved by the udev subsystem.

udev rules allow you to register callbacks for events relating to peripheral devices. I never remember how to write them and find easier to monitor the events:

udevadm monitor --property --udev

After monitoring the events after a Bluetooth connection drop I created a new file at /etc/udev/rules.d/99-bluetooth.rules with a simple rule:

ACTION=="remove" SUBSYSTEM=="bluetooth", NAME="*AirPods*", RUN+="/home/milad/.local/bin/bluetooth-actions --block-if-not-connected AA:BB:CC:DD:EE:FF"

Every time the AirPods are disconnected udev subsystem calls a script as a fallback mechanism that blocks the device. (Unfortunately I couldn’t find BT addr when monitoring udev events. Instead, I leveraged the NAME field to limit the rule to only AirPods.