Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions AAP Definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,56 @@ AirPods send conversational awareness packets when the person wearing them start
| 03 | Person Stopped Speaking; increase volume back to normal |
| Intermediate values | Intermediate volume levels |
| 08/09 | Normal Volume |
### Reading Conversational Awareness State

After requesting notifications, the AirPods send a packet indicating the current state of Conversational Awareness (CA). This packet is only sent once after notifications are requested, not when the CA state is changed.

The packet format is:

```plaintext
04 00 04 00 09 00 28 [status] 00 00 00
```

- `[status]` is a single byte at offset 7 (zero-based), immediately after the header.
- `0x01` — Conversational Awareness is **enabled**
- `0x02` — Conversational Awareness is **disabled**
- Any other value — Unknown/undetermined state

**Example:**
```plaintext
04 00 04 00 09 00 28 01 00 00 00
```
Here, `01` at the 8th byte (offset 7) means CA is enabled.

## Metadata

This packet contains device information like name, model number, etc. The packet format is:

```plaintext
04 00 04 00 1d [strings...]
```

The strings are null-terminated UTF-8 strings in the following order:

1. Bluetooth advertising name (varies in length)
2. Model number
3. Manufacturer
4. Serial number
5. Firmware version
6. Firmware version 2 (the exact same as before??)
7. Software version (1.0.0 why would we need it?)
8. App identifier (com.apple.accessory.updater.app.71 what?)
9. Serial number 1
10. Serial number 2
11. Unknown numeric value
12. Encrypted data
13. Additional encrypted data

Example packet:
```plaintext
040004001d0002d5000400416972506f64732050726f004133303438004170706c6520496e632e0051584e524848595850360036312e313836383034303030323030303030302e323731330036312e313836383034303030323030303030302e3237313300312e302e3000636f6d2e6170706c652e6163636573736f72792e757064617465722e6170702e3731004859394c5432454632364a59004833504c5748444a32364b3000363335373533360089312a6567a5400f84a3ca234947efd40b90d78436ae5946748d70273e66066a2589300035333935303630363400```

The packet contains device identification and version information followed by some encrypted data whose format is not known.


# Writing to the AirPods
Expand Down
164 changes: 164 additions & 0 deletions Proximity Pairing Message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Bluetooth Low Energy (BLE) - Apple Proximity Pairing Message

This document describes how the AirPods BLE "Proximity Pairing Message" is parsed and interpreted in the application. This message is broadcast by Apple devices (such as AirPods) and contains key information about the device's state, battery, and other properties.

## Overview

When scanning for BLE devices, the application looks for manufacturer data with Apple's ID (`0x004C`). If the data starts with `0x07`, it is identified as a Proximity Pairing Message. The message contains various fields, each representing a specific property of the AirPods.

## Proximity Pairing Message Structure

| Byte Index | Field Name | Description | Example Value(s) |
|------------|-------------------------|---------------------------------------------------------|--------------------------|
| 0 | Prefix | Message type (should be `0x07` for proximity pairing) | `0x07` |
| 1 | Length | Length of the message | `0x12` |
| 2 | Pairing Mode | `0x01` = Paired, `0x00` = Pairing mode | `0x01`, `0x00` |
| 3-4 | Device Model | Big-endian: [3]=high, [4]=low | `0x0E20` (AirPods Pro) |
| 5 | Status | Bitfield, see below | `0x62` |
| 6 | Pods Battery Byte | Nibbles for left/right pod battery | `0xA7` |
| 7 | Flags & Case Battery | Upper nibble: case battery, lower: flags | `0xB3` |
| 8 | Lid Indicator | Bits for lid state and open counter | `0x09` |
| 9 | Device Color | Color code | `0x02` |
| 10 | Connection State | Enum, see below | `0x04` |
| 11-26 | Encrypted Payload | 16 bytes, not parsed | |

## Field Details

### Device Model

| Value (hex) | Model Name |
|-------------|--------------------------|
| 0x0220 | AirPods 1st Gen |
| 0x0F20 | AirPods 2nd Gen |
| 0x1320 | AirPods 3rd Gen |
| 0x1920 | AirPods 4th Gen |
| 0x1B20 | AirPods 4th Gen (ANC) |
| 0x0A20 | AirPods Max |
| 0x1F20 | AirPods Max (USB-C) |
| 0x0E20 | AirPods Pro |
| 0x1420 | AirPods Pro 2nd Gen |
| 0x2420 | AirPods Pro 2nd Gen (USB-C) |

### Status Byte (Bitfield)

| Bit | Meaning | Value if Set |
|-----|--------------------------------|-------------|
| 0 | Right Pod In Ear (XOR logic) | true |
| 1 | Right Pod In Ear (XOR logic) | true |
| 2 | Both Pods In Case | true |
| 3 | Left Pod In Ear (XOR logic) | true |
| 4 | One Pod In Case | true |
| 5 | Primary Pod (1=Left, 0=Right) | true/false |
| 6 | This Pod In Case | true |

### Ear Detection Logic

The in-ear detection uses XOR logic based on:
- Whether the right pod is primary (`areValuesFlipped`)
- Whether this pod is in the case (`isThisPodInTheCase`)

```cpp
bool xorFactor = areValuesFlipped ^ deviceInfo.isThisPodInTheCase;
deviceInfo.isLeftPodInEar = xorFactor ? (status & 0x08) != 0 : (status & 0x02) != 0; // Bit 3 or 1
deviceInfo.isRightPodInEar = xorFactor ? (status & 0x02) != 0 : (status & 0x08) != 0; // Bit 1 or 3
```

### Primary Pod

Determined by bit 5 of the status byte:
- `1` = Left pod is primary
- `0` = Right pod is primary

This affects:
1. Battery level interpretation (which nibble corresponds to which pod)
2. Microphone assignment
3. Ear detection logic

### Microphone Status

The active microphone is determined by:
```cpp
deviceInfo.isLeftPodMicrophone = primaryLeft ^ deviceInfo.isThisPodInTheCase;
deviceInfo.isRightPodMicrophone = !primaryLeft ^ deviceInfo.isThisPodInTheCase;
```

### Pods Battery Byte

- Upper nibble: one pod battery (depends on primary)
- Lower nibble: other pod battery

| Value | Meaning |
|-------|----------------|
| 0x0-0x9 | 0-90% (x10) |
| 0xA-0xE | 100% |
| 0xF | Not available|

### Flags & Case Battery Byte

- Upper nibble: case battery (same encoding as pods)
- Lower nibble: flags

#### Flags (Lower Nibble)

| Bit | Meaning |
|-----|--------------------------|
| 0 | Right Pod Charging (XOR) |
| 1 | Left Pod Charging (XOR) |
| 2 | Case Charging |

### Lid Indicator

| Bits | Meaning |
|------|------------------------|
| 0-2 | Lid Open Counter |
| 3 | Lid State (0=Open, 1=Closed) |

### Device Color

| Value | Color |
|-------|-------------|
| 0x00 | White |
| 0x01 | Black |
| 0x02 | Red |
| 0x03 | Blue |
| 0x04 | Pink |
| 0x05 | Gray |
| 0x06 | Silver |
| 0x07 | Gold |
| 0x08 | Rose Gold |
| 0x09 | Space Gray |
| 0x0A | Dark Blue |
| 0x0B | Light Blue |
| 0x0C | Yellow |
| 0x0D+ | Unknown |

### Connection State

| Value | State |
|-------|--------------|
| 0x00 | Disconnected |
| 0x04 | Idle |
| 0x05 | Music |
| 0x06 | Call |
| 0x07 | Ringing |
| 0x09 | Hanging Up |
| 0xFF | Unknown |

## Example Message

| Byte Index | Example Value | Description |
|------------|--------------|----------------------------|
| 0 | 0x07 | Proximity Pairing Message |
| 1 | 0x12 | Length |
| 2 | 0x01 | Paired |
| 3-4 | 0x0E 0x20 | AirPods Pro |
| 5 | 0x62 | Status |
| 6 | 0xA7 | Pods Battery |
| 7 | 0xB3 | Flags & Case Battery |
| 8 | 0x09 | Lid Indicator |
| 9 | 0x02 | Device Color |
| 10 | 0x04 | Connection State (Idle) |

---

For further details, see [`BleManager`](linux/ble/blemanager.cpp) and [`BleScanner`](linux/ble/blescanner.cpp).
6 changes: 5 additions & 1 deletion linux/ble/blemanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
if (info.manufacturerData().contains(0x004C))
{
QByteArray data = info.manufacturerData().value(0x004C);
// Ensure data is long enough and starts with prefix 0x07
// Ensure data is long enough and starts with prefix 0x07 (indicates Proximity Pairing Message)
if (data.size() >= 10 && data[0] == 0x07)
{
QString address = info.address().toString();
Expand All @@ -60,6 +60,8 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
deviceInfo.address = address;
deviceInfo.rawData = data;

// data[1] is the length of the data, so we can skip it

// Check if pairing mode is paired (0x01) or pairing (0x00)
if (data[2] == 0x00)
{
Expand All @@ -85,6 +87,8 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)

deviceInfo.connectionState = static_cast<DeviceInfo::ConnectionState>(data[10]);

// Next: Encrypted Payload: 16 bytes

// Determine primary pod (bit 5 of status) and value flipping
bool primaryLeft = (status & 0x20) != 0; // Bit 5: 1 = left primary, 0 = right primary
bool areValuesFlipped = !primaryLeft; // Flipped when right pod is primary
Expand Down