Device: Add groundwork for AirPods Custom EQ - #689
Draft
d4rken wants to merge 1 commit into
Draft
Conversation
Adds decode and encode for AAP opcode 0x63, Apple's iOS 27 "Custom EQ" (three bands plus a Recommended/Custom mode selector, H2 models). The wire format comes from librepods commit 7341e41 and has never been confirmed on hardware, so the decoder validates every field and returns null on any mismatch -- including a strict payload-length equality, so a frame carrying trailing unknown bytes is surfaced verbatim in the unknown-message hex log rather than mis-parsed into plausible values. Nothing optimistically assumes the write lands: SetCustomEq deliberately has no optimistic state update and no verification predicate, because an unacknowledged write must not render as success, and the device is not known to report this setting at all. Also recovers two bytes the 0x53 decoder was discarding. Opcode 0x53 is the Headphone Accommodations configuration -- an iOS Accessibility feature that the iOS 18.1+ hearing-aid feature reuses -- and payload bytes 4 and 5 are its "Apply To: Phone / Media" flags. They are plain 0x01 flags, not the Apple-bool 0x01/0x02 encoding used by the 0x09 control settings. They describe scope only and say nothing about whether a profile exists, so isAllZero stays a pure band-data predicate. A debug-only card (BuildConfig.DEBUG, mirroring the existing PME chart) drives the opcode for hardware evaluation. It edits local draft state and sends exactly one packet per Apply tap; slider-driven sends would flood the link and make a logcat observation window unreadable. Apply is disabled while no pod is worn, because AapOutboundController queues non-exempt commands in that state and the queue collapses repeats, surfacing them later at an unrelated moment. That guard reads the AAP EarDetection setting rather than PodDevice's BLE-fallback property, so it matches the controller's gate in every state -- notably, neither gates when the AAP setting is absent. On-device evaluation, both legs negative: AirPods Pro 2 USB-C (A3048, fw 81.2675000075000000.6814) and AirPods Pro 3 (A3064, fw ...6503) never advertise 0x63 across settings bursts of 33 and 26 frames, never echo a write, never persist one across a case-cycle reconnect, and produce no audible change. Both accept the write at the socket and discard it. This does not establish that the byte format is wrong -- a device ignoring an unimplemented opcode is indistinguishable from one rejecting a malformed frame -- and it is not evidence that privileged access is required, since neither device advertises the feature at all.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
No user-facing behavior change. Adds AAP protocol support (decode and encode) for Apple's iOS 27 "Custom EQ", recovers two flags the existing Headphone Accommodations decoder was throwing away, and adds a debug-only control used to test the new opcode against real hardware.
Custom EQ does not currently work on any hardware available for testing. The fourth bullet below has the results.
Technical Context
The
0x63wire format comes from a third-party project (librepods commit7341e41) which never verified it either, so the decoder validates every field and returns null on any mismatch. The payload length check is a strict equality rather than a minimum, so a frame carrying trailing unknown bytes surfaces verbatim in the unknown-message hex log instead of being mis-parsed into plausible values.SetCustomEqdeliberately gets no optimistic state update and no verification predicate: an unacknowledged write must not render as success, and no device is known to report this setting at all.Opcode
0x53is the Headphone Accommodations configuration, which the iOS 18.1+ hearing-aid feature reuses, rather than solely the hearing-aid audiogram as the previous comments claimed. Payload bytes 4 and 5 are its "Apply To: Phone / Media" flags, encoded as plain0x01flags and not the Apple-bool0x01/0x02used by the0x09control settings, sodecodeAppleBoolmust not be used on them. They describe scope only and say nothing about whether a profile exists, soisAllZerostays a pure band-data predicate.The debug card's Apply button is disabled while no pod is worn.
AapOutboundControllerqueues non-exempt commands in that state and the queue collapses repeats, so a write would otherwise surface minutes later at an unrelated moment and poison the log window the card exists to produce. ExemptingSetCustomEqfrom ear-gating was considered and rejected: it would mutate shared production code for a debug tool, and it would allow EQ writes while the pods are unworn, which confounds the hardware test. The guard reads the AAPEarDetectionsetting rather thanPodDevice.isEitherPodInEar, whose BLE fallback disagrees with the controller exactly when AAP ear data is absent.On-device results, both legs negative. AirPods Pro 2 USB-C (A3048, firmware
81.2675000075000000.6814) and AirPods Pro 3 (A3064, firmware...6503) never advertise0x63across connect bursts of 33 and 26 settings frames, never echo a write, never persist one across a case-cycle reconnect, and produce no audible change. Both accept the write at the socket and discard it. Worth noting the comparison is confounded: the Pro 3 is on older firmware than the Pro 2 on the same train, so this pair cannot separate a model line from a firmware line.What that negative does not establish: that the byte format is wrong (a device ignoring an unimplemented opcode is indistinguishable from one rejecting a malformed frame), or that privileged access such as VendorID spoofing is required, since neither device advertises the feature at all. The cheapest decisive follow-up is checking whether Apple's own settings offer an Equalizer entry for these pods on an iOS 27 host.
Review checklist
0x53change is additive only: the 4x8 Float32 decode keeps offset 6, andisAllZerois unchanged.0x63decode is intentional. Any dialect mismatch must log verbatim rather than decode.src/mainbehindBuildConfig.DEBUG, mirroring the existing PME chart, with hardcoded English text so throwaway instrumentation labels never reach Crowdin.