DEV Community

Cover image for Inside Apple AirPods Auto-Switching: Bluetooth Continuity, AAP & Python Packet Sniffing
Alex Kanunnikov
Alex Kanunnikov

Posted on

Inside Apple AirPods Auto-Switching: Bluetooth Continuity, AAP & Python Packet Sniffing

tl;dr — AirPods jump between your iPhone, iPad & Mac by juggling two Bluetooth links (Classic for audio, BLE for control), iCloud-synced keys, Continuity advertisements and a secret L2CAP channel (AAP). This post explains every layer and shows how to watch the magic with Python & Wireshark.


1 · Architecture in 90 seconds

Layer What AirPods use Why it matters
Bluetooth Classic A2DP (music) · HFP (calls) Actual audio
Bluetooth LE Custom service ➜ L2CAP PSM 0x1001 (AAP) Battery, sensors, ANC, control
Continuity BLE ads “Proximity Pairing” & device-activity beacons Lets every Apple device know who’s doing what
iCloud Keychain Shares pairing keys across your Apple ID Zero-config pairing on all devices
H1/H2 chip Fast link setup & encrypted state Switch in ~1 s instead of normal 5–10 s

The result: only one audio stream at a time, but the OS hands off ownership so quickly that it feels like multipoint.


2 · Continuity BLE: Finding Your Pods in the Ether

Apple devices broadcast & consume proprietary BLE packets (doc-type 0x0220 for AirPods).

Open a terminal and run:

sudo timeout 15s btmon | grep -i -A2 -B2 0x004c
Enter fullscreen mode Exit fullscreen mode

You’ll catch lines like:

> ADV_IND, Apple, Inc. (0x004C), RSSI -43
  22 20 19 aa bb cc dd ee ...   # 0x2220 = AirPods advert
Enter fullscreen mode Exit fullscreen mode

Fields decoded by Martin et al. (PETS 2020):

Byte(s) Meaning
+0-+1 Message type 0x0220
+4-+6 Serial hash
+7 Lid-open counter
+8 Battery L
+9 Battery R
+10 Battery Case + flags

Apple devices listen; when your Mac starts media playback it yells “I need the Pods!” and your iPhone politely disconnects.


3 · AAP: The Secret L2CAP Channel (0x1001)

Reverse-engineered by LibrePods.

3.1 Handshake

import socket, bluetooth  # PyBluez
ADDR = "XX:XX:XX:XX:XX:XX"   # AirPods MAC from `bluetoothctl`
PSM  = 0x1001                # Apple Audio Protocol
sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
sock.connect((ADDR, PSM))

# Stage-1: 4-byte magic
sock.send(b'\x01\x00\x00\x00')
resp = sock.recv(64)
print("Handshake-1:", resp.hex())
Enter fullscreen mode Exit fullscreen mode

Full spec: (see “AAP Definitions”).

3.2 Subscribing for notifications

# Tell Pods we want battery/in-ear updates
sock.send(bytes.fromhex("07 00 00 00 00 00 0A 00 01 00"))
while True:
    pkt = sock.recv(128)
    print("Notify:", pkt.hex())
Enter fullscreen mode Exit fullscreen mode

A battery frame looks like:

08 00 07 00 <left%> <right%> <case%> <flags>.


4 · What Actually Triggers a Switch?

Trigger BLE/Wi-Fi Message Priority
Incoming phone call Telephony Continuity ad 🚨 High
Media play on other device “Activity-level” Active=YES Medium
User taps device in AirPlay menu UI event → OS forces takeover Guaranteed

Current host drops AAP & A2DP → target host connects in ~1 s.

No real “multipoint”: just lightning-fast hand-offs.


5 · Watch It Live: Python + Bleak

import asyncio, struct
from bleak import BleakScanner

APPLE_MFG = 0x004C
TYPE_AIRPODS = b'\x22\x20'

def parse_airpods(data: bytes):
    serial = data[3:7].hex()
    lid    = data[7]
    left, right, case = data[8], data[9], data[10]
    return serial, lid, left, right, case

async def main():
    async with BleakScanner() as scanner:
        while True:
            dev, adv = await scanner.get_discovered_device()
            if adv and adv.manufacturer_data.get(APPLE_MFG, b'').startswith(TYPE_AIRPODS):
                serial, lid, l, r, c = parse_airpods(adv.manufacturer_data[APPLE_MFG])
                print(f"AirPods {serial} lid={lid} L={l}% R={r}% Case={c}%")

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Hit Play on your Mac → watch the adverts stop (Pods connected), then resume from iPhone when call ends.


6 · Opening AAP on Linux (BlueZ ≥ 5.66)

# kernel must allow PSM 0x1001
echo 1 | sudo tee /sys/kernel/debug/bluetooth/allowed_psm/0x1001
python3 aap_client.py  # from LibrePods
Enter fullscreen mode Exit fullscreen mode

aap_client.py dumps live events: ANC mode, in-ear detect, etc.

Great starting point if you’re building multipoint for Android (CAPod is doing exactly that).


7 · FAQ

Q · Can I keep two devices connected at once?

Not today. Only one A2DP stream. Apple optimises switching speed, not true multipoint.

Q · Why don’t W1 (1st-gen) Pods auto-switch?

The W1 lacks BLE packet-routing logic & enough RAM for Continuity states. H1/H2 only.

Q · Will clones work?

Many fakes replay the same Continuity advert, so the pop-up appears, but they can’t speak AAP or fast-switch.


8 · Further Reading & Credits

Resource What you’ll find
“Handoff All Your Privacy” (PETS 2020) https://petsymposium.org/2020/files/papers/issue4/popets-2020-0067.pdf Continuity advert dissections
LibrePods https://github.com/kavishdevar/librepods AAP definitions, sample clients
CAPod https://github.com/d4rken-org/capod A companion app for AirPods on Android.
SoundGuys: H1 vs H2 deep dive https://www.soundguys.com/how-does-apple-h1-chip-work-21049/ Chipset capabilities
9to5Mac guide https://9to5mac.com/2020/09/27/disable-airpods-automatic-switching/ How to disable auto-switch per device

Disclaimer: Educational purposes only. Respect Apple’s terms and local laws when experimenting.

Top comments (0)