Research: protocol map for the missing AirPods features (head tracking solved, heart-rate pipeline mapped)
I reverse-engineered the wire-level protocol for several of the features listed as missing, using the iOS 26.5 dyld_shared_cache and a cross-check against PodsLink's AAP implementation. Sharing the map so it can be built in LibrePods. No hardware was used.
1. Head-tracked spatial audio — fully mapped (AAP opcode 0x17)
The head-orientation stream is the same AAP/AACP transport LibrePods already speaks (L2CAP PSM 0x1001, frame prefix 04 00 04 00, little-endian uint16 opcode).
- Start head tracking — send opcode
0x17 control packet:
04 00 04 00 17 00 00 00 10 00 10 00 08 A1 02 42 0B 08 0E 10 02 1A 05 01 40 9C 00 00
- Stop — the paired stop packet (same shape, disable body).
- Sensor packets arrive with opcode
0x17, length >= 70, byte[8]==0x10, an orientation marker at byte 10.
- Decode: three int16 orientation values
o1/o2/o3 at payload byte offsets 43 / 45 / 47.
pitch = ((o2 + o3) / 2 / 32000) * 180, clamped to [-90, 90]
yaw = ((o2 - o3) / 2 / 32000) * 180, clamped to [-70, 70]
- per-marker axis mapping / sign / swap applies (see PodsLink
HeadTrackingOrientationDecoder).
This is enough to expose a TYPE_HEAD_TRACKER-style quaternion. Delivering it into the Android system spatializer needs a HAL/HID shim (root); a per-app OpenTrack/UDP feed needs no root.
2. Heart rate (AirPods Pro 3) — iOS pipeline mapped
On iOS the AirPods heart-rate sample is modeled as HRCHeartRateData and re-emitted as an IOHIDFamily HIDBiometricEvent. The construction selector is:
-[HRCHeartRateData initWithHeartRate:confidence:confidenceLevel:arbitrationStatus:context:hrContext:timestamp:sampleUuid:sourceType:streamingThrottleStatus:deviceUuid:device:sensorLocation:flags:] (impl in HeartRateCoordinator.framework)
Consumers/pipeline: HeartRateCoordinator -> HealthHeartRateStream / SeymourSessionServices (Apple's "Seymour" = AirPods sensor stack, e.g. makeHeartRateMetricStream) -> HealthKit. Standard BLE HR (HealthLEHeartRate) is a separate path and is not what AirPods use.
The remaining piece is the numeric AAP opcode + payload byte offset that carries the bpm. It is parsed by a cache-resident daemon in the Seymour/accessory-sensor stack. I'm tracing that now.
3. Others
- Find My network: OpenBubbles'
rustpush already solves report decrypt/auth; the blocker is extracting the AirPods offline-finding seed (distinct from the IRK/ENC proximity keys LibrePods reads today).
- HQ two-way audio: iOS keeps A2DP for playback while receiving the mic stream over AACP (per LibrePods README). This is AACP-mic-RE feasible rather than an LE-Audio requirement.
I can open follow-up issues/PRs per feature. Head tracking is ready to implement from the spec above.
Research: protocol map for the missing AirPods features (head tracking solved, heart-rate pipeline mapped)
I reverse-engineered the wire-level protocol for several of the features listed as missing, using the iOS 26.5
dyld_shared_cacheand a cross-check against PodsLink's AAP implementation. Sharing the map so it can be built in LibrePods. No hardware was used.1. Head-tracked spatial audio — fully mapped (AAP opcode 0x17)
The head-orientation stream is the same AAP/AACP transport LibrePods already speaks (L2CAP PSM
0x1001, frame prefix04 00 04 00, little-endian uint16 opcode).0x17control packet:04 00 04 00 17 00 00 00 10 00 10 00 08 A1 02 42 0B 08 0E 10 02 1A 05 01 40 9C 00 000x17, length >= 70, byte[8]==0x10, an orientation marker at byte 10.o1/o2/o3at payload byte offsets 43 / 45 / 47.pitch = ((o2 + o3) / 2 / 32000) * 180, clamped to [-90, 90]yaw = ((o2 - o3) / 2 / 32000) * 180, clamped to [-70, 70]HeadTrackingOrientationDecoder).This is enough to expose a
TYPE_HEAD_TRACKER-style quaternion. Delivering it into the Android system spatializer needs a HAL/HID shim (root); a per-app OpenTrack/UDP feed needs no root.2. Heart rate (AirPods Pro 3) — iOS pipeline mapped
On iOS the AirPods heart-rate sample is modeled as
HRCHeartRateDataand re-emitted as an IOHIDFamilyHIDBiometricEvent. The construction selector is:-[HRCHeartRateData initWithHeartRate:confidence:confidenceLevel:arbitrationStatus:context:hrContext:timestamp:sampleUuid:sourceType:streamingThrottleStatus:deviceUuid:device:sensorLocation:flags:](impl inHeartRateCoordinator.framework)Consumers/pipeline:
HeartRateCoordinator->HealthHeartRateStream/SeymourSessionServices(Apple's "Seymour" = AirPods sensor stack, e.g.makeHeartRateMetricStream) -> HealthKit. Standard BLE HR (HealthLEHeartRate) is a separate path and is not what AirPods use.The remaining piece is the numeric AAP opcode + payload byte offset that carries the bpm. It is parsed by a cache-resident daemon in the Seymour/accessory-sensor stack. I'm tracing that now.
3. Others
rustpushalready solves report decrypt/auth; the blocker is extracting the AirPods offline-finding seed (distinct from the IRK/ENC proximity keys LibrePods reads today).I can open follow-up issues/PRs per feature. Head tracking is ready to implement from the spec above.