Skip to content

Latest commit

 

History

History
1852 lines (1627 loc) · 191 KB

File metadata and controls

1852 lines (1627 loc) · 191 KB

CAPTURE_BLUETOOTH_HCI_SNOOP.md — Bluetooth HCI Snoop Capture Guide

Purpose: step-by-step procedure to capture, extract, and analyze Bluetooth HCI traffic between a phone and the Pixel Buds Pro 2, in order to fill in the confidence-rated placeholders in PROTOCOL.md (magic bytes, checksum, opcode table, battery approach). This document is the how — the relevant CAP-NNN-FINDINGS.md and PROTOCOL.md are the what we found.

Two devices are used, for two different purposes:

Device Role Why
Pixel 7a, Android 17 (with Google Play Services), official Pixel Buds app Primary capture Only source of actual libmaestro command frames (ANC toggle, EQ write) triggered on demand
Pixel 9a, GrapheneOS, no Pixel Buds app Secondary / validation capture Confirms pairing/bonding behavior, GATT service discovery, and any spontaneous/OS-level traffic on the actual target OS, without app-driven commands

Do the Pixel 7a capture first — it's the one that produces frames you can actually match against pbpctrl's documented commands. The Pixel 9a capture is a validation/baseline step, not a substitute.


1. Prerequisites

1.1 On your computer

  • Wireshark installed (has a built-in Bluetooth HCI dissector — no plugins needed).
  • Android platform-tools (adb) installed and on your PATH.
  • A few hundred MB of free disk space — adb bugreport output can be large.
  • A USB cable for each phone (data-capable, not charge-only).

1.2 On each phone

  • Developer options enabled.
  • USB debugging enabled.
  • Bluetooth HCI snoop logging enabled (see §2).
  • The Pixel Buds Pro 2 already known to pair reliably with that phone (test this once before your real capture session, so pairing issues don't eat into your capture time or contaminate your log with retries).

1.3 A way to log timestamps of your own actions

A notes app, a voice memo, or simply writing down wall-clock times as you go. This is what lets you match a Wireshark frame to "the moment I tapped ANC → Off" later. Don't skip this — without it, correlating frames to actions is much harder after the fact.


2. Enabling HCI Snoop Logging

Applies to both phones; steps are identical regardless of GrapheneOS vs. stock Android, since HCI snoop logging is an AOSP-level feature.

  1. Enable Developer options (skip if already enabled): Settings → About phone → Software information → tap Build number 7 times.
  2. Go to Settings → System → Developer options.
  3. Enable "Enable Bluetooth HCI snoop log".
  4. Enable "USB debugging".
  5. Restart Bluetooth — reboot the phone (recommended default). A toggle off/on (Settings → Bluetooth) is the officially documented way to make the logger pick up the setting, and normally works. A reboot is recommended here anyway, for a cost-based reason rather than a confirmed reliability problem: on the rare occasion the toggle doesn't take, the failure is silent — you only discover it after doing a full round of actions and pulling an empty bugreport (see the FAQ in §7). A phone reboot costs about a minute; redoing a capture session doesn't. If you've already verified on your specific phone that a plain toggle reliably starts logging, that's a fine, faster alternative — just confirm it once (§7 FAQ has the recovery steps if a session comes back empty either way).
  6. Connect the phone to your computer via USB and accept the "Allow USB debugging?" dialog on the phone (approve your computer's RSA key fingerprint).
  7. Verify the connection: adb devices should list the phone as device (not unauthorized or offline).

Note on log rotation: the on-device snoop log has a size cap and will eventually overwrite old data during long sessions. Keep individual capture sessions short and focused (a handful of isolated actions, see §4) rather than one long open-ended session, so nothing rotates out before you extract it.


3. Extracting the Log (identical method for stock Android and GrapheneOS)

Do not attempt adb root or a direct adb pull of /data/misc/bluetooth/logs/btsnoop_hci.log — on a normal production/non-rooted build, including stock Pixel firmware and GrapheneOS, this will fail with a permission error, since adbd cannot run as root on such a build. This is expected and not specific to GrapheneOS; it has nothing to do with whether the device is capable of being rooted, only with the fact that neither phone is rooted here.

The supported, working method on both phones:

  1. Run on your computer:

    adb bugreport buds_capture
    

    This produces buds_capture.zip (or a similarly named file/folder depending on platform-tools version) in your current directory. No root required. A GrapheneOS community forum report documents adb bugreport producing a report containing the BTSnoop log on a stock, non-rooted GrapheneOS build on Android 16 — this is a community observation, not an official GrapheneOS platform guarantee, but it's consistent with adb bugreport being the standard, documented AOSP mechanism (below) rather than anything GrapheneOS-specific.

  2. Unzip the result. Depending on your unzip tool, this may spill its contents directly into your current directory (an FS/ folder, various .txt/.zip files, etc.) rather than into a named subfolder — that's normal, not a sign anything went wrong.

  3. Check the raw log path first — simplest, no tooling needed if it's there: look for either of these inside the unzipped FS/ tree:

    • FS/data/log/bt/btsnoop_hci.log
    • FS/data/misc/bluetooth/logs/btsnoop_hci.log

    If one of these exists, you're done — it's already a raw BTSnoop file, open it directly in Wireshark (§5). No need to run btsnooz.py at all. The exact path varies by Android version, which is why step 4 below exists as a version-resilient alternative — but check here first, since it's usually simpler when present.

  4. If neither raw path exists — extract via btsnooz.py, per current AOSP documentation:

    # Get btsnooz.py from the AOSP source tree if you don't already have it:
    # https://cs.android.com/android/platform/superproject/+/android-latest-release:packages/modules/Bluetooth/system/tools/scripts/btsnooz.py
    
    # Find the actual bugreport text file first — do NOT assume it's named after
    # whatever you passed to `adb bugreport`. That name only applies to the .zip;
    # the .txt inside always keeps Android's own generated name:
    ls bugreport-*.txt
    # e.g. bugreport-lynx-CP2A.260705.006-2026-08-09-08-52-28.txt
    
    btsnooz.py bugreport-<device>-<build>-<timestamp>.txt > buds_capture_btsnoop.log
    

    This doesn't depend on guessing an internal zip path that has already moved between Android releases (see step 3 above) — btsnooz.py extracts the BTSnoop data directly from the bugreport's text dump, which is a more stable interface across versions, at the cost of an extra tool to set up. If neither the raw path (step 3) nor a bugreport-*.txt file is found at all, search the extracted contents by filename:

    find . -iname "*btsnoop*"
    
  5. Copy/rename the resulting log somewhere memorable, e.g. captures/2026-08-02_pixel7a_anc-toggle.log, so repeated sessions don't overwrite each other.

  6. Once you've confirmed extraction worked, you can disable the HCI snoop toggle again (§2 step 3) to avoid unnecessary background logging and disk usage between sessions.

PROPOSAL — pending maintainer approval: step 3 (raw file) and step 4 (btsnooz.py fallback) are not interchangeable in practice — always check step 3 first and prefer it whenever the raw file is present, and always record which path a given capture actually used in that capture's own CAP-NNN-FINDINGS.md (per PROJECT_RULES.md rule 11's reproducibility requirement) — a capture's usability for anything beyond short control-frame sequencing may depend on this choice, not just on what happened during the session itself. See DESKRESEARCH_FINDINGS.md's 2026-08-28 entry for the full 5-session comparison and evidence (still 🟡 HYPOTHESIS, not a controlled test).


4. Capture Procedure — Isolate Every Action

Groups A–Q below are the main run-through capture scenarios — efficient bundles of related actions to run in one session — not the project's official test record; Z, R, S, T, U, V, W, X, and Y are additional special-purpose groups added later (pipeline validation, occasional/targeted follow-ups), each documented in its own subsection below. Each numbered item is annotated with its permanent Test-ID (e.g. ANC-001) from TESTPLAN_BLUETOOTH_HCI_SNOOP.md, which is where the actual catalog of investigated behaviors, their existence-confidence, and their evidence status live. A Group can bundle Test-IDs from more than one feature area for capture efficiency (e.g. Group C bundles CONV-001 and MULTI-001, which are unrelated features) — the Group is about how to run the session, the Test-ID is what's actually being investigated. See TESTPLAN_BLUETOOTH_HCI_SNOOP.md §0.1 for the full reasoning.

The single most important rule: one user-level action per capture window, with a pause before and after. A capture full of overlapping actions is very hard to attribute correctly in Wireshark; a capture of ten cleanly isolated actions is straightforward. Note that "one action" means one thing you deliberately triggered — not one frame. Some actions (pairing/bonding above all, see Group A below) legitimately produce a burst of related frames as their own multi-step protocol exchange runs to completion; that whole burst still counts as the result of a single isolated action, not as several actions to capture separately.

Recommended rhythm per action: wait ~5s → note the exact time → perform the action → wait ~5–10s → move to the next action.

This timing is a practical heuristic, not a guarantee that every response arrives within it. Since this guide deliberately avoids live capture (§7 FAQ — the live-capture port usually isn't available on modern Android, so you only see the traffic after extraction), there's no way to watch responses arrive in real time and wait until they settle; a fixed interval is the only thing that's actually practical to follow. The known failure mode: an occasional slow/delayed response can land after you've already moved on, and get misattributed to whatever the next action was. Mitigation is in the analysis phase, not the capture phase — see §5 step 3.

4.1 Pixel 7a (official app) — primary session

Source: the action list below is the validated inventory from TESTPLAN_BLUETOOTH_HCI_SNOOP.md sections 1 (User Actions from the Pixel Buds App) and 3 (Automatic Actions Initiated by the App) — checked against your own app screenshots and official Google support documentation. It supersedes the older, shorter action list this section used to contain.

This is a comprehensive list (~40 actions). You do not need to do it all in one sitting — per the FAQ in §7 ("Do I need to do a full capture session every time?"), you can pull a fresh bugreport for any subset at any time. Splitting this into 3–4 shorter sessions by group (below) is often more manageable than one long one, and keeps each adb bugreport capture focused (see the log rotation note in §6).

Run one continuous Bluetooth HCI snoop logging session per group — the on-device logger you enabled in §2 keeps recording throughout, there's nothing you need to re-invoke to "keep it going" — and collect a single adb bugreport (§3) once, at the end of the group. Log each action with its own timestamp, following the usual rhythm: wait ~5s → note the exact time → perform the action → wait ~5–10s → move to the next action.

⚠️ Priority tip (superseded 2026-08-14 — original text kept below, not deleted, per PROJECT_RULES.md §3): the original advice below was to prioritize Group K to confirm/refute the RFCOMM framing hypothesis. That hypothesis question is now resolved for ANC specifically (PROTOCOL.md §4.1 — the official Fast Pair Message Stream, Group 0x08, byte- and timing-verified against CAP-001), which is what Group K's own frame would have helped confirm. The new top priority is Group T (EQ command isolation, below) — EQ's command channel is the one major control feature this project still needs to attribute, now that the earlier assumption "EQ probably shares ANC's channel" no longer holds (PROTOCOL.md §2.3's 2026-08-14 addendum). The same caution applies to Group T's result as applied here to Group K: a superficial byte-pattern match on a single capture is a HYPOTHESIS, not a FACT — see Group T's own cross-command check.

Original tip (2026-08-08 or earlier, kept for the record): this document's own verification methodology flags the "Play sound on Left earbud" action (group K below) as a specifically valuable, low-risk target — its frame can be directly compared against the Fast Pair Message Stream spec's own worked example (0x04 0x01 ... for a ring action) to confirm or refute the framing hypothesis in PROTOCOL.md §2.1. If you only have time for a short session, prioritize group K. Caution when you get there: a superficial resemblance to the worked example (e.g. a plausible-looking group/code byte pair) is not by itself confirmation — don't promote the framing hypothesis to FACT off one frame that merely looks compatible. See the cross-command check in Group K below.

Group Z — Pipeline validation (do this first, before anything else)

Before spending your pairing baseline (Group A) or any real reverse-engineering capture, confirm the whole tooling chain actually works end to end, using the cheapest, most trivial, already-connected action you have — this catches a broken pipeline (logging didn't start, bugreport came back empty, extraction script misconfigured, wrong Wireshark filters) on a throwaway capture instead of on your pairing baseline or Find My Buds, both of which are more valuable and, in the pairing case, mildly disruptive to redo.

  1. With the Buds already connected (any existing pairing is fine — this step doesn't need a fresh bond), work through §2 (enable HCI snoop, restart Bluetooth) and confirm adb devices lists the phone.
  2. Perform one trivial, already-familiar action — e.g. a single ANC mode toggle. Note the timestamp.
  3. Pull a bugreport (§3), extract via btsnooz.py, and open the result in Wireshark.
  4. Confirm you can actually see, before doing any protocol interpretation: general HCI traffic, an RFCOMM stream, frames addressed to/from the Buds' Bluetooth address, and that your noted timestamp lines up with visible activity in the log.
  5. If any of the above is missing, fix the tooling (§2, §3, §6 Notes & Gotchas) and repeat this step — don't move on to Group A until this passes. Log this as your first entry in the Capture Index (§9); mark it discarded afterward if you'd rather not keep a throwaway capture around, but it still counts as a real, worthwhile session.

Group A — Connection / bonding baseline

  1. Pairing / bonding baseline [PAIR-001] — capture this as its own isolated session, ideally before the command groups below (B–P). While connecting, this session also incidentally covers [BATT-004] (battery via RFCOMM on connect) — no separate action needed for that, just note it happened here if relevant. If the Buds are already paired, "forget" the device on the phone side first (Bluetooth settings → the paired device → Forget), then re-pair through Bluetooth settings — this captures a real bonding handshake instead of skipping it because "it's already paired." This is a lightweight, safely repeatable action: it does not touch the Buds' own memory or the Find My Device link, unlike the full factory reset in Group P #16 below — do not confuse the two. Expect a burst, not a single frame: pairing is one user action (tapping the device in the picker) that triggers an automatic multi-step exchange (inquiry, authentication, link-key exchange, SDP, profile connect) — this entire burst is the result of that one action, not several actions to isolate individually; there is nothing to click separately for each sub-step. Wait for the connection to settle before moving on to Group B.

Group B — Active Noise Control

  1. ANC → Off [ANC-001]. Wait. Note time.
  2. ANC → Noise Cancellation (active) [ANC-002]. Wait. Note time.
  3. ANC → Adaptive [ANC-003] (if your firmware exposes it — confirmed present in release_5.203 per PROTOCOL.md §4.1). Wait. Note time.
  4. ANC → Transparency [ANC-004]. Wait. Note time.

Repeat recommendation (2026-08-14), lower priority than Group T: despite ANC's opcode reaching 🟢 FACT, one sub-question stays open — CAP-001's first two taps (Transparency, Off) have no corresponding 0x12 "Set ANC state" frame anywhere in that log (PROTOCOL.md §4.1, CAP-001-FINDINGS.md §5), possibly because the ANC row was still visibly greyed out (UI-state realization, not a genuine command) rather than a missed capture. A clean repeat of this Group with the same isolation requirements as before — but starting only once the ANC row is already fully active/enabled on screen, and doing a genuine single tap per window rather than the six-in-a-row CAP-001 did — would confirm whether every real tap produces a 0x12 frame, and re-confirm the bit-mapping (PROTOCOL.md §4.1's table) on a clean, single-tap capture before it's relied on for implementation.

Group C — Conversation Detection & Multipoint

  1. Toggle 'Conversation Detection' on/off [CONV-001]. Wait. Note time.
  2. Toggle 'Multipoint' on/off [MULTI-001]. Wait. Note time.

Group D — Equalizer: presets

Note, added 2026-08-30 (audit finding): neither Group D nor Group E has its own row in §9's Capture Index — EQP-001/003008 and EQS-001003/005's actual wire capture happened under Group T (CAP-005/CAP-015), which superseded these as isolated, one-action-at-a-time sessions rather than the bundled run-through described below. Treat "D"/"E" in TESTPLAN_BLUETOOTH_HCI_SNOOP.md's Capture-scenario column as historical/superseded by "T," not as a still-open, separately-runnable scenario.

  1. Select EQ preset: Standard [EQP-001]. Wait. Note time.
  2. Select EQ preset: Bass Boost [EQP-002]. Wait. Note time.
  3. Select EQ preset: Bass Reduction [EQP-003]. Wait. Note time.
  4. Select EQ preset: Balanced [EQP-004]. Wait. Note time.
  5. Select EQ preset: Vocal Boost [EQP-005]. Wait. Note time.
  6. Select EQ preset: Clarity [EQP-006]. Wait. Note time.
  7. Select EQ preset: Last saved [EQP-007]. Wait. Note time.
  8. Save current EQ as a new preset ('Save') [EQP-008] — a distinct write action from preset selection. Wait. Note time.

Group E — Equalizer: individual sliders

Change one band at a time by a clearly visible amount — not all bands in one gesture. 16. Adjust EQ slider: High treble [EQS-001]. Wait. Note time. 17. Adjust EQ slider: Treble [EQS-002]. Wait. Note time. 18. Adjust EQ slider: Mid [EQS-003]. Wait. Note time. 19. Adjust EQ slider: Bass [EQS-004]. Wait. Note time. 20. Adjust EQ slider: Low bass [EQS-005]. Wait. Note time.

Group F — Touch & head gesture toggles

  1. Toggle 'Touch controls' fully on/off [TOUCH-001]. Wait. Note time.
  2. Toggle 'Head gestures' fully on/off [HEAD-001]. Wait. Note time.

Group G — Press-and-hold configuration

  1. Set 'Press and hold' Left → Toggle ANC [HOLD-001]. Wait. Note time.
  2. Set 'Press and hold' Left → Digital assistant [HOLD-002]. Wait. Note time.
  3. Set 'Press and hold' Right → Toggle ANC [HOLD-003]. Wait. Note time.
  4. Set 'Press and hold' Right → Digital assistant [HOLD-004]. Wait. Note time.
  5. Check/uncheck one ANC mode in the press-and-hold rotation list [HOLD-005] (e.g. remove 'Off' from the cycle). Wait. Note time.

Group H — Audio & volume settings

  1. Toggle 'Mono audio' on/off [AUDIO-001]. Wait. Note time.
  2. Toggle 'Volume EQ' on/off [AUDIO-002]. Wait. Note time.
  3. Shift the 'Volume balance' slider [AUDIO-003]. Wait. Note time. (Per TESTPLAN_BLUETOOTH_HCI_SNOOP.md §1 this is stored locally on the earbuds themselves — a good candidate for a confirmable persistent write.)

Group I — Firmware & device info

  1. Tap the 'Firmware up to date' check (manual) [FW-001]. Wait. Note time.
  2. Open 'More settings' to view firmware version per component [FW-002] — may trigger a status query. Wait. Note time.
  3. View serial numbers per component (same screen) [FW-003]. Wait. Note time.
  4. View connection status ("Earbud status: Connected") [FW-004]. Wait. Note time.

Group J — In-ear detection & case sounds

  1. Toggle 'In-ear detection' on/off [INEAR-001]. Wait. Note time.
  2. Toggle case sound 'Earbuds replaced' on/off [CASE-001]. Wait. Note time.
  3. Toggle case sound 'Other notifications' on/off [CASE-002]. Wait. Note time.

Group K — Find My Buds (high-priority, see tip above)

  1. Play sound on Left earbud [FIND-001]. Wait. Note time.
  2. Play sound on Right earbud [FIND-002]. Wait. Note time.
  3. Play sound on Case [FIND-003]. Wait. Note time.
  4. Play sound on both earbuds simultaneously [FIND-004]. Wait. Note time.

Before treating the framing hypothesis as confirmed from #38 alone: cross-check against 2–3 semantically different commands — e.g. an ANC mode change (Group B) and one EQ preset or slider write (Group D/E). One matching frame is a promising HYPOTHESIS, not a FACT (PROJECT_RULES.md §1: FACT requires repeated confirmation, not a single observation). For each of these frames, check specifically whether the same structural elements line up across all of them: magic/group byte(s), length-field semantics, channel/message-ID position, where the protobuf payload boundary falls, whether a checksum is present, and whether a Message Group/Code reading (§2 Hypothesis A) explains the leading bytes as consistently as a magic-byte reading (§2 Hypothesis B) would. If all sampled commands share the same structure, that's what actually raises confidence toward FACT — not the Left-earbud frame resembling the spec's worked example on its own.

Group L — Passive/automatic observation windows

These aren't taps — they're deliberate waiting periods to catch background/automatic app traffic per TESTPLAN_BLUETOOTH_HCI_SNOOP.md §3. Log explicit boundaries for each window, not just a single timestamp — otherwise settling traffic from whatever you did right before the window starts is hard to distinguish from genuinely spontaneous traffic during it. For each item below, note: observation start (when you stopped touching anything), any event of interest during the window, observation end, the Bluetooth connection state (connected/reconnecting/idle), and the app's foreground/background state. 42. Idle wait with the app open [BATT-001], ~60s right after connecting, without touching anything — intended to catch the "battery status notification on every reconnect" behavior. Note the start time, and leave a clean ~10s gap after the preceding connect action before this window starts, so reconnect-settling traffic doesn't bleed into what you're trying to observe as spontaneous. 43. Force-close and reopen the app [OBS-001] — intended to catch any status query the app sends on launch. Note the exact time of reopening as the window start, and note when you consider the window over (e.g. ~30–60s after reopening, or once traffic visibly settles).

After finishing a group, pull the bugreport once (§3) — you don't need a separate bugreport per action, just clean timestamps to slice the single log into segments afterward.

Group R — Forced GATT re-discovery (occasional, not part of the normal run-through)

Correction, 2026-08-14 (does not invalidate running this Group, but changes what to expect): removing the bond does not reliably clear Android's cached GATT service/characteristic database. CAP-003-FINDINGS.md §1 found the cache survived bond removal (zero live Read By Group Type discovery traffic), and CAP-004-FINDINGS.md §6 independently reconfirmed the same negative result — two for two attempts using this method. Group W below is the only method that has an untried, stronger chance of working (pm clear com.android.bluetooth, or discovery from a phone that has never connected to this device before) — see also TESTPLAN_BLUETOOTH_HCI_SNOOP.md's GATT-001 row. Group R is kept in this guide because it is still useful for its bonus data (a fresh classic pairing exchange, step 4 below) even though it is no longer expected to force live GATT discovery on its own; if your goal is specifically the GATT handle→UUID mapping, run Group W instead, not this Group.

Android caches the GATT service/characteristic database per bonded device and does not rediscover it on a normal reconnect — every other group in this guide runs against an already-discovered device, so none of them exercise this. GATT-001 in TESTPLAN_BLUETOOTH_HCI_SNOOP.md needs Group W, not this Group, for its Pixel-7a-specific resolution (see the correction above).

Why not just reconnect normally: there is no non-hacky way to force a GATT refresh from outside an app — the only programmatic option (BluetoothGatt.refresh()) is a hidden @hide API requiring reflection, which AGENTS.md §3 bans for this project's own code. Removing the bond was the first thing tried to force it, but per the correction above, it is not reliable for that purpose — it remains useful only as described above.

  1. Remove the bond via system Bluetooth settings — Settings → Connected devices → Pixel Buds Pro 2 → Forget. Use the system settings, not the Pixel Buds app's own "Forget" button — CAP-001's CAP-001-FINDINGS.md §6 found the app-level Forget did not fully clear a BLE-level association, so it isn't reliable for this purpose. Confirm the device no longer appears in the paired-devices list.
  2. Work through §2 (enable HCI snoop, restart Bluetooth/reboot) as usual.
  3. Reconnect using a generic BLE tool (e.g. nRF Connect), not the official Pixel Buds app — install it on the Pixel 7a if not already present. This is a deliberate exception to this guide's usual preference for the official app: here the goal is a human-readable view of the discovered GATT structure on screen, not attributing a specific proprietary command, so a generic scanner is more useful, not less rigorous. The HCI snoop log captures everything at the system level regardless of which app initiates the connection. Do not expect this to actually trigger live discovery (see the correction above) — treat any live discovery traffic that does appear as a bonus, not the expected outcome.
  4. Isolate the whole connect-and-discover sequence as one action window: note the exact time you tap Connect in the BLE tool, and the time the service/characteristic list finishes populating on screen (nRF Connect's UI may still show a service list here even without live discovery traffic — it can be serving its own cached view; do not assume an on-screen list means fresh wire traffic occurred, check the log). Expect a full classic SSP pairing exchange to also appear in this capture (removing the bond clears both classic and BLE state for a dual-mode device) — this is this Group's actual, reliable bonus data point, per the correction above: a PAIR-001 capture (bond removal via system settings only — no factory reset happens in this Group, so this can never be genuine PAIR-002 data), not a GATT-001 one.
  5. If the tool supports it, manually read or subscribe to specific characteristics of interest once they're identified on screen (e.g. anything near handle 0x0f2a or the 0x0c0X cluster flagged in CAP-002's CAP-002-FINDINGS.md §4/§7) — each such action is its own isolated event, noted with its own timestamp, the same way a UI tap is treated elsewhere in this guide.
  6. Extract and analyze as usual (§3, §5). In Wireshark, filter specifically for the ATT opcodes that perform GATT discovery: btatt.opcode == 0x10 / 0x11 (Read By Group Type Request/Response — primary service discovery) and btatt.opcode == 0x08 / 0x09 (Read By Type Request/Response — characteristic discovery). Their responses contain the handle-to-UUID mapping this Group exists to capture.

Group S — Google Play Services disabled, no Pixel Buds app (occasional, not part of the normal run-through)

Purpose: isolate whether the Fast Pair Message Stream traffic identified in CAP-002 (CAP-002-FINDINGS.md §3 — the [Group][Code][Length][Value]-framed channel carrying, among other things, the "Revision 6" string) is Buds-initiated (would still appear identically) or driven by Google Play Services' phone-side Fast Pair/Nearby logic (would disappear or change). This is directly relevant to this project's Zero-GMS goal (AGENTS.md §1, PROJECT.md): if the Buds only send this data because GMS asks for it in a specific way, this project's own app will need to replicate that GMS-side behavior, not just listen passively.

Setup (validated manually before capturing, per the maintainer's own testing): with the Pixel Buds app uninstalled and Google Play Services disabled (Settings → Apps → see all apps → Google Play Services → Disable; adb shell pm disable-user --user 0 com.google.android.gms is the scriptable equivalent), pairing the Buds via system Bluetooth settings still succeeds, but the Fast Pair "Connect" half-sheet (the purple dialog with the Buds product image, normally shown when GMS is enabled) does not appear — confirming the half-sheet itself is GMS-driven UI, not app- or Buds-driven. Whether the underlying RFCOMM Message Stream traffic is also GMS-driven, or independent of it, is what this capture is for — it is not assumed by the setup validation above.

  1. Confirm the Pixel Buds app is uninstalled and Google Play Services is disabled, per the setup note above.
  2. Work through §2 (enable HCI snoop, restart Bluetooth/reboot) as usual.
  3. Pair via system Bluetooth settings [GFPS-001] — there is no app and no Fast Pair half-sheet to use here, so this is the only pairing path available. Note whether the device was already unpaired (e.g. left over from a prior Group R session) or whether this capture also includes a fresh bonding handshake — record this explicitly in this session's CAP-004-EVENT-NOTES.md, since it changes what else can be read from the same capture (see Group R step 4 for the equivalent bonus-PAIR-001 note — Group S is also bond-removal-only, never a factory reset, so this is PAIR-001 data too, not PAIR-002).
  4. Isolate the whole pair-and-settle sequence as one action window: note the exact connect-tap time and when the connection visibly settles.
  5. Extract and analyze as usual (§3, §5). Specifically check whether a channel/DLCI carrying the same [Group][Code][Length][Value] framing as CAP-002 §3 appears at all, and if so, whether the same fields (e.g. Code 0x09's value) match. Do not assume an outcome before analyzing — either result (present or absent) is a real, useful finding for the open question above, not a "pass" or "fail" of this Group.

Group T — EQ command isolation (occasional, not part of the normal run-through; current top priority, added 2026-08-14)

Why this replaces the earlier ANC-first priority tip: ANC's command channel is now confirmed (PROTOCOL.md §4.1 — the official Fast Pair Message Stream, Group 0x08, DLCI 0x04), which also retires the earlier assumption that EQ "probably shares ANC's channel." EQ's command channel is therefore still completely open, and is now the single highest-priority capture target for this project's original implementation goal (see the corrected note at the end of §4.1's intro, below Group S).

  1. Change EQ preset: Bass Boost [EQP-002], as a single isolated action — same rhythm as Group D (≥10s silence before, ≥10s after) — but run alone this time, not bundled with the other seven preset taps back-to-back the way Group D does it, so the capture has only one candidate command frame to attribute.
  2. After the window settles, run a second, independent, isolated window with a structurally different EQ action — Adjust EQ slider: Bass [EQS-004] — before drawing any conclusion, matching Group K's own "don't promote off one matching frame" discipline.

Analysis instructions for this Group specifically:

  1. First check whether any official Fast Pair extension page not yet checked (beyond Hearable Controls, Device Action, SASS, Device Information — see PROTOCOL.md §2.3/§4.1) documents an EQ-shaped message group — the same kind of check that resolved ANC.
  2. If no official page covers it, inspect every open channel's traffic in a tight window around the tap: DLCI 0x04 (Message Stream, watch for any Group not already accounted for), DLCI 0x02 (libmaestro's confirmed Pigweed-HDLC framing, PROTOCOL.md §2.2a — decode the Address/Control fields of every "Sent" sub-frame near the tap and check whether one appears specifically at that moment, distinct from its otherwise-undecoded baseline traffic), and DLCI 0x08 (the private [Group][Code][Length][Value] envelope, CAP-004-FINDINGS.md §5a).
  3. Apply Group K's cross-command check: a single matching frame from step 1's two actions is a HYPOTHESIS, not a FACT — the two actions exist specifically so there's a second, structurally different sample to check against.

Group U — DLCI 0x08 Group 0x04 Code 0x12 liveness/event bracket (occasional, added 2026-08-14)

Purpose: CAP-004-FINDINGS.md §5a's Task 5 found Code 0x12 alternates its value on an irregular interval — 🟡 HYPOTHESIS of a free-running liveness/sequence-parity bit, not yet tested against a real physical event. This Group also closes INEAR-004's existing gap (no capture scenario exists yet for "bud removed from ear, not placed back in case" — flagged in TESTPLAN_BLUETOOTH_HCI_SNOOP.md §9).

  1. Remove one worn earbud and hold it in hand (not placing it back in the case) [INEAR-004], with the connection otherwise idle. Note the exact time. Keep logging Code 0x12 occurrences on DLCI 0x08 for at least 60s before and 60s after this moment.
  2. Close the case lid while the connection is still active (buds elsewhere, e.g. still worn or in hand — not the normal "buds back in case" CASE-006 sequence) [OBS-003]. Note the exact time.
  3. A deliberate, multi-minute idle wait (≥3 minutes, connection active, nothing touched) [OBS-003]. Note the start and end time.

Analysis: for each bracketed moment, check whether the Code 0x12 field1 2↔3 alternation breaks, skips, or pauses at that moment (supports event-driven) or continues unperturbed on its own irregular cadence (supports a free-running counter) — per CAP-004-FINDINGS.md §5a's own framing of this open question.

Group V — In-call HFP/SCO audio behavior (occasional, added 2026-08-14)

Purpose: CAP-002-FINDINGS.md §5 found zero AT+ traffic anywhere outside CAP-001's own pairing-time handshake across a full 8+ hour log, and CAP-001-FINDINGS.md §6 Task 6 ruled out any SCO/eSCO HCI event in all four captures to date — both findings converge on the same missing scenario: none of the four captures so far ever contains an actual phone call, the one trigger that would exercise HFP's Service Level Connection setup and channel 5/DLCI 0x0a's audio path.

  1. Place or receive an actual phone call while connected to the Buds [CALL-001]. Note the exact start and end time of the call.
  2. Optionally, during the call, trigger a deliberate audio-routing action (e.g. switch the audio output device) as a bonus data point — note its time separately.

Analysis: check whether a full HFP AT-command SLC handshake reappears (matching CAP-001's shape) and whether channel 5/DLCI 0x0a carries any payload this time, or whether an HCI-level Setup Synchronous Connection (0x0428) / Enhanced Setup Synchronous Connection (0x043D) / Synchronous Connection Complete (0x2C) event appears at all (none has, in any capture to date — CAP-001-FINDINGS.md §6 Task 6).

Group W — Stronger GATT cache-busting for live service discovery (occasional, added 2026-08-14)

✅ RESOLVED 2026-09-01 (CAP-034, maintainer sign-off obtained per AGENTS.md §6). The 0x0c0X/0x0f2X handle↔UUID mapping question this Group exists for is now closed — CAP-034 combined Option (a) (pm clear com.android.bluetooth) with Option (b)'s underlying goal (a phone never before connected to this Buds unit, the Pixel 9a) for the first time, and the resulting unlimited-snaplen discovery burst resolved the full 15-primary-service GATT profile. See the CAP-034 row in the Capture Index above and CAP-034-FINDINGS.md/PROTOCOL.md §6 for the full result. This Group's own text below is left as historical context for how the resolution was reached — a future session normally has no remaining reason to repeat this Group for GATT-001 specifically, unless a new GATT question arises (e.g. the still-open name of FE2C1238… or "Unknown Service"'s purpose, CAP-034-FINDINGS.md §8).

Purpose: CAP-002, CAP-003, and CAP-004 all failed to trigger a live Read By Group Type GATT discovery against the Buds — Android's cached GATT database survived bond removal in every attempt so far (CAP-003-FINDINGS.md §1, re-confirmed in CAP-004-FINDINGS.md §6). Group R's "remove the bond" method is not a reliable trigger, contrary to what this document and TESTPLAN_BLUETOOTH_HCI_SNOOP.md's GATT-001 row previously stated (corrected here and there — see the 2026-08-14 note on GATT-001 below and in TESTPLAN_BLUETOOTH_HCI_SNOOP.md §5).

  1. Option (a) — clear the Bluetooth system app's cache directly: adb shell pm clear com.android.bluetooth [GATT-001]. Risk, note before running: this clears all of the phone's Bluetooth pairings and state, not just the Buds' — expect to have to re-pair every other Bluetooth device on that phone afterward. Confirm this is acceptable before running it. Also clear nRF Connect's own app cache/data (adb shell pm clear no.nordicsemi.android.mcp — check the actual package name installed; Settings → Apps → nRF Connect → Storage → Clear storage/cache works too) if using it to reconnectCAP-004-FINDINGS.md §6 found nRF Connect's on-screen service list may be served from the app's own cached data rather than fresh wire discovery, which would make the UI look like discovery succeeded even when the Android Bluetooth stack itself never emitted a live Read By Group Type exchange. Clearing only com.android.bluetooth without also clearing nRF Connect's own cache risks a false-positive "it worked" read from the UI that the HCI snoop log won't actually back up — always confirm against the log (§5), never the tool's UI alone.
  2. Option (b) — discover from a phone that has never connected to this device before: run the discovery from the Pixel 9a (GrapheneOS), which has not previously run any app or tool against this specific Buds unit [GATT-001]. If using nRF Connect (or any BLE scanner) on this phone too, use a fresh install or clear its cache first, same reasoning as option (a) — "never connected before" should mean the tool's cache is clean, not just the device's pairing state.
  3. Isolate the connect-and-discover sequence as its own window, same as Group R step 4.

Group X — Battery-level discrepancy bracket (occasional, added 2026-08-14)

Purpose: CAP-001-FINDINGS.md §3 found AT+CIND?'s battchg=3 (≈60%) and AT+BIEV=2,100 (100%) disagreeing at the same moment — unresolved whether either indicator actually tracks a real battery-level change over time.

  1. Start logging well before an expected, natural battery-level decline (e.g. at the start of a normal day of use), keeping the connection active/idle rather than disconnecting between checks.
  2. Periodically (e.g. every 15–30 minutes) note the wall-clock time — no action needed, both indicators are expected to update on their own per their respective triggers.
  3. End the session after a natural, visible battery-percentage drop has occurred on screen. Can be combined with Group V's session if a phone call happens to occur naturally within the same window — no need to force this, note it if it happens.

Analysis: extract every AT+CIND? (battchg) and AT+BIEV=2,... value across the session with its timestamp, and compare both trends against the on-screen battery percentage over the same window.

Note on Group S's repeatability (added 2026-08-14): CAP-004-FINDINGS.md §8 item 4 flags that Group S's Cross-Transport-Key-Derivation bonding result (§2 there) might be an artifact of nRF Connect's early BLE connection rather than of GMS being disabled — CAP-004 used nRF Connect first, deviating from this Group's own system-settings-only procedure (see the "⚠️ Procedure deviation" note in CAP-004-EVENT-NOTES.md). A repeat of Group S exactly as described above (system Bluetooth settings only, no BLE tool at any point) would isolate this confound. The core GFPS-001 result (§4a/§4b there) is not expected to change, only the §2 CTKD-vs-classic-SSP bonding-mechanism finding.

Resolved 2026-08-26 (CAP-012): the repeat was captured, exactly as described above (no BLE tool at any point, independently verified both on screen and on the wire — CAP-012-FINDINGS.md §2). Confirmed: classic SSP, not CTKD — this isolates the confound cleanly, as predicted. As also predicted, the GFPS-001 channel-topology result (DLCI 0x04 never opens) reproduces CAP-004's; however, CAP-012's own log turned out to be severely ACL-truncated (btsnooz-fallback extraction), so the payload-content half of GFPS-001 — which CAP-004's untruncated log could answer — is inconclusive here, not a second confirming data point (CAP-012-FINDINGS.md §1/§4). A further repeat with a working, non-truncated snoop log would still be worthwhile for that reason alone.

Note on Group A's repeatability, optional (added 2026-08-14). CAP-001-FINDINGS.md §6 found a BLE link and a still-valid link key both existing before the on-screen "Forget" tap and before the case was reopened — unresolved whether "Forget" fully clears prior association state. A repeat of Group A that starts HCI snoop logging before any association with the device exists at all (e.g. immediately after a phone restart, before ever opening the case or any Buds app) would isolate this.

Update (2026-08-26): CAP-013 attempted this repeat — did not achieve it, primary question still open. Logging did not actually start before the clearing action (which itself was a phone-wide "Reset Bluetooth & Wi-Fi," not a single-device "Forget") — the log's first frame lands 2m21s after that action, and after the case-open/pair-button/device-list-tap sequence too (CAP-013-FINDINGS.md §0). The primary question above remains 🔴 OPEN. CAP-013 did resolve the secondary PAIR-004 question (fresh SSP, no key reuse, for whatever bonding state was active when its own logging window began) — see TESTPLAN_BLUETOOTH_HCI_SNOOP.md's PAIR-004 row. PROPOSAL — pending maintainer approval: a further repeat is still needed, this time with logging verifiably started before the clearing action itself (e.g. enable HCI snoop logging immediately after a phone restart, before touching Bluetooth settings at all) — proposed as CAP-031 (next free ID per id_registry.csv, not yet assigned/registered).

Update (2026-08-27), CAP-031, PROPOSAL — pending maintainer approval: second consecutive attempt, still did not achieve it — primary question still open. This session used a genuine narrow, per-device "Forget" (screenshot-confirmed, unlike CAP-013's broader reset) and added a live snoop-log file-size-polling check during recording specifically to catch CAP-013's failure mode — but the log's first frame (06:06:37.16) still lands 66s after the on-screen Forget tap (06:05:31), and after the case-open/pair-button/first-scan-attempt sequence too (CAP-031-FINDINGS.md §0). The primary question remains 🔴 OPEN, untested a third time. CAP-031 did reconfirm the secondary PAIR-004 question (fresh SSP, no key reuse — a sixth confirming instance) and, as bonus negative results, found that neither of CAP-013's two other anomalies (DLCI 0x02's ~61s-delayed open, the unattributed second BLE link) reproduce this session (CAP-031-FINDINGS.md §5/§6) — both look like single-session artifacts, not recurring behavior. A fourth attempt is still needed for the primary question, this time verifying the snoop log's own content freshness (last-frame timestamp against a live wall clock) immediately before the Forget tap, not just the log file's size (CAP-031-FINDINGS.md §8's proposed root cause and method).

Update (2026-08-27), CAP-032, PROPOSAL — pending maintainer approval: fourth attempt succeeded. This session was extracted via the raw BTSnoop file path (§3 step 3) instead of the btsnooz.py fallback used for the three prior attempts — the resulting log is genuinely untruncated (frame.cap_len == frame.len for all 2,455 frames) and its first frame (18:29:45.72) lands ~58s before the on-screen Forget tap (18:30:42), and ~30s before the video itself starts. The primary question is now answered for this session: 🟢 no BLE link or valid classic link key existed for the Buds anywhere in the covered pre-Forget window (CAP-032-FINDINGS.md §0.3) — zero classic connection events, exactly one LE connection (to an unrelated random-address device, not the Buds), and a Delete Stored Link Key issued at the Forget tap's own moment reporting Num_Keys_Deleted = 0. This does not reproduce CAP-001's original finding — it shows the opposite, a clean counter-example rather than a confirmation or a refutation of that session's own result; CAP-001's session-specific puzzle (why that session had residual state) remains independently open. PAIR-004's secondary question is reconfirmed a seventh time (fresh SSP, no key reuse). This also supports (one data point, not yet independently isolated) the hypothesis that the btsnooz-fallback extraction path itself — not the individual session — was responsible for CAP-012/CAP-013/CAP-031's truncation; see the proposed §3 addendum below.

Group Y — BLE-only connection isolation for the 0x0044 notification burst (occasional, added 2026-08-20)

Purpose: CAP-016-FINDINGS.md §11 found a 73-frame Handle Value Notification burst on BLE ATT handle 0x0044 (connection handle 0x0002, 23 of the 73 frames containing a recurring 0xfea9 byte-pair marker), confined to a ~29s window right after the BLE link forms and before the classic link exists. Not yet isolated from a physical trigger: every capture that shows this burst to date also has a bud removal/insertion happening nearby in the same session, so it's unconfirmed whether the burst is caused by the BLE link forming at all, or by the bud/case action that happened to coincide with it in those captures.

  1. Enable Bluetooth and let the phone's BLE link to the already-paired Buds form on its own [GATT-002] — do not touch the buds or the case at any point before, during, or for at least 60s after the link forms. The buds/case stay exactly wherever they already are (worn, in hand, or already sitting open/closed) — this is the opposite of Group M's procedure, which deliberately triggers bud/case events; here the goal is a clean BLE-connect with zero physical events nearby to correlate against. Note the exact time Bluetooth was (re-)enabled or the BLE link began forming.
  2. Keep the observation window open and logging for at least 60s past that point, per the usual observation-window discipline (§4.1 Group L's boundary-logging convention: note observation start, any event of interest, and observation end explicitly).

Analysis: filter the resulting log for btatt.opcode==0x1b and btatt.handle==0x0044. If the burst still appears despite no bud/case action anywhere in or near the window, that's a clean positive result narrowing 0x0044's trigger to "BLE link establishment alone" (PROTOCOL.md §6). If it does not appear, that's an equally useful negative result, pointing back toward a bud/case physical action as the real trigger after all — either outcome closes this open question, per the three-way outcome guidance already used for Group Q's items 19–20.

Group AA — SDP UUID branch isolation for gbm.a()'s "default internal rfcomm socket" path (occasional, added 2026-08-30)

Purpose: REVERSE_ENGINEERING.md's gbm/fzd entries and DECISIONS.md ADR-018 found the companion app's own decompiled code (gbm.java:35-43) picks between two internal RFCOMM sockets depending on which of two SDP UUIDs is present in the discovered set: "pigweed" (25e97ff7-..., confirmed = DLCI 0x02 in every capture so far) or "default" (3a046f6d-..., never observed on the wire anywhere — a raw-byte scan of all 26 capture files this project has, in every format (*btsnoop_hci.log, *btsnooz_hci.log, both nRF Connect logs), found zero occurrences in either byte order; see REVERSE_ENGINEERING.md's gbm entry Open questions). This Group covers the two hypotheses that are safely testable with a single Pixel Buds Pro 2 unit on its current firmware (release_5.203):

  • SDP-001: gbm.a()'s discovered-UUID set may come from an SDP browse whose content depends on who triggers it — the OS's own default pairing flow, vs. the companion app's own fetchUuidsWithSdp() call (fxm.java:110, which only fires when the HID UUID 0x1124 is absent). Every existing capture has the app already open, so this hasn't been isolated.
  • SDP-002 (opportunistic): a firmware update might be what changes which UUID gets advertised at all — the "pigweed"/"default" pair reads plausibly as a pre-/post-migration artifact. Only testable the next time an actual OTA update becomes available, same caveat as FWUPD-001/ FWUPD-002.

Explicitly out of scope for this Group — not safely or practically testable, recorded here so they aren't silently retried: deliberately downgrading firmware (unsupported by Google, real bricking risk — see WORKSTATION_PREPARATIONS.md's Disaster Recovery section; never attempt this), and testing against a different physical unit or hardware generation (the maintainer owns one Pixel Buds Pro 2 — PROJECT_RULES.md §8's own-hardware scope). If SDP-001/SDP-002 both come back negative, the leading remaining explanation is that the "default" branch is unreachable on any currently-shipping release_5.203+ unit — a static-analysis question (checking for a firmware-version gate elsewhere in the APK, APK_REVERSE_ENGINEERING_PROCEDURE.md §4), not a capture question, and out of this Group's scope.

  1. [SDP-001] Force-stop the Pixel Buds companion app first (Settings → Apps → Pixel Buds → Force stop), so it cannot react to the pairing at all. Start Bluetooth HCI snoop logging (§2). "Forget" the Buds via system Bluetooth settings only — the same safe, repeatable action already used for PAIR-001 (Group A #1), not CASE-007's factory reset. Re-pair via system Bluetooth settings' "Pair new device" flow only. Note the exact time pairing completes.
  2. Keep observing for at least 60s after bonding completes, without opening the companion app or touching the buds/case — this is the window where the pre-app-fetch UUID set (if one exists and differs from the baseline) would show up.
  3. Still SDP-001, second half of the same session: now open the companion app normally and let it connect as usual — this reproduces every prior capture's baseline in the same log, for a direct in-session before/after comparison rather than relying on a separate session.
  4. Pull the bugreport (§3) once, at the end.
  5. [SDP-002], opportunistic, separate session whenever a firmware update becomes available: repeat the SDP-observation half of steps 1–2 (a fresh SDP browse doesn't require a fresh bond — simply reconnecting is enough if already bonded) immediately before installing the update, and again immediately after it completes and the Buds reconnect.

Analysis: pre-filter by address first, per §13's CLI-hygiene rule, then to btsdp: tshark -r CAP-NNN-btsnoop_hci.log -Y "bluetooth.addr == 04:00:6e:cf:6e:07 and btsdp" -T fields -e frame.number -e frame.time_relative -e btsdp.data_element.value.uuid_128 -e btsdp.protocol.channel (exact command already validated against CAP-001/CAP-002/CAP-032 in REVERSE_ENGINEERING.md's gbm entry). Three-way outcome, matching Group Y's own guidance above:

  • Positive: the pre-app-open window (step 2) shows the "default" UUID (3a046f6d-..., either byte order) instead of or alongside "pigweed" — closes the open question; write it up per the usual FACT/HYPOTHESIS discipline (PROJECT_RULES.md §1) before touching PROTOCOL.md.
  • Negative: still only "pigweed" (or no btsdp traffic at all, e.g. if the OS doesn't run a full SDP browse without the app prompting it) — consistent with every capture to date; narrows the explanation toward SDP-002 or a static-analysis-only dead-code question rather than a UI-timing artifact.
  • SDP-002 positive: a before/after firmware comparison shows the advertised UUID changing — directly explains the two-UUID code as a migration artifact.

Group AB — DLCI 0x08/0x0a/0x06/0x12 GMS-independence check on GrapheneOS (occasional, added 2026-09-02)

🟡 PARTIALLY ADVANCED 2026-09-02 (CAP-035, maintainer sign-off obtained per AGENTS.md §6). DLCI 0x08's content reproduces byte-identical with Google Play Services present but dumpsys-verified disabled (com.google.android.gms enabled=3) — a rigorously confirmed strengthening of CAP-004-FINDINGS.md §4a's existing finding, not the fully conclusive genuinely-GMS-absent result this Group was designed to reach (the Pixel 9a used still has GMS installed, merely disabled, not absent as this Group's own Purpose below assumed it typically would be). DLCI 0x0a opens but stays silent both times; DLCI 0x06/0x12 never open at all — first-time clean negatives for both. See the CAP-035 row in the Capture Index and CAP-035-FINDINGS.md for the full result. Still open: a repeat with GMS genuinely uninstalled, if full closure of the OS-stack-vs-GMS question is wanted.

Purpose: CAP-033-FINDINGS.md §3 found the Buds' own SDP database names DLCI 0x08 as "GSND CONTROL", DLCI 0x0a as "GSND AUDIO", DLCI 0x06 as "DEBUG APP", and DLCI 0x12 as "BTIS" — but a full, mechanical companion-APK search (per ADR-017's boundary) found zero references to any of these names/UUIDs anywhere in the app's own decompiled code, meaning the app never registers or looks up these services itself. Two explanations remain open: (1) Android's own Bluetooth stack connects to these services at the OS/vendor level, independent of any app, or (2) Google Play Services' Nearby module does. CAP-004-FINDINGS.md §4a already found DLCI 0x08's content (the google-pixel-buds-pro-v1 capability blob, Europe/Amsterdam, firmware string) reappears unchanged with GMS disabled and the Pixel Buds app uninstalled — a single data point, and a weaker test than this Group runs, since "disabled" is not the same as "absent." This Group repeats that isolation on the Pixel 9a (GrapheneOS), where Google Play Services is not merely disabled but typically not installed at all — the strongest available version of this test without decompiling any OS-level binary (a materially different, not-yet-authorized scope — see this session's own conversation record for the reasoning). Same underlying logic as Group S, applied to a cleaner GMS-absent environment and to four channels instead of one.

Runs exclusively on the Pixel 9a (GrapheneOS) — no app-driven commands are possible on this device regardless (§4.2), which is a feature here, not a limitation: it keeps the phone-side software surface minimal (system Bluetooth stack only, no Pixel Buds app, no third-party GATT tool).

Test-ID: GSND-001 (new — added to TESTPLAN_BLUETOOTH_HCI_SNOOP.md and id_registry.csv alongside this Group).

Setup — verify, don't assume, per PROJECT_RULES.md §1 (record the actual findings in CAP-035-EVENT-NOTES.md, not just the intended state):

  1. Confirm no Google Play Services package is present at all (GrapheneOS's optional sandboxed Play compatibility layer is a separate, explicitly-installed app — check it isn't present either):
    adb shell pm list packages | grep -i "google\|gms\|play"
    
    If any GMS-related package is present, additionally confirm/record its disabled state (adb shell dumpsys package <package> | grep -i enabled) — "absent" and "present but disabled" are different conditions and change how strong this session's result is.
  2. Confirm the Pixel Buds Companion App is not installed: adb shell pm list packages | grep -i pixelbuds (or check the app drawer).
  3. Confirm nRF Connect (or any other BLE/GATT tool) is not used this session — system Bluetooth settings only, the same constraint as Group S.
  4. If the Buds are currently bonded to this phone (e.g. left over from CAP-034), "Forget" them via Settings → Connected devices → the Buds → Forget — a narrow, per-device action, not the broader "Reset Wi-Fi, mobile & Bluetooth" system option (that wipes every other paired device and all Wi-Fi networks on the phone for no benefit this session actually needs — this Group isn't chasing a GATT-cache question, so Group W's heavier cache-busting isn't relevant here). A plain per-device Forget is enough to get a clean fresh-pairing handshake as a bonus PAIR-001 data point; it is not strictly required for the DLCI-content question itself (CAP-010-FINDINGS.md §5 already showed DLCI 0x08's handshake reproduces on a reconnect to an already-bonded device too), but keeps this session's own artifacts simple to read.
  5. Enable Bluetooth HCI snoop logging (§2) and reboot, same as every other session.

Procedure:

  1. Start video recording (wall-clock overlay) and confirm HCI snoop logging is active.
  2. Open the case, press the pairing button. [PAIR-001]
  3. On the phone: Settings → Connected devices → Pair new device → select the Buds from the list. Confirm the system pairing dialog. Note the exact tap time. [PAIR-001, GSND-001]
  4. Once connected, do not open any app or tool — leave the connection idle for at least 90–120 seconds. This is deliberately longer than Group S's window: DLCI 0x06/0x0a's only prior payload-bearing occurrence (CAP-021-FINDINGS.md §4a) did not appear immediately on connect, so a short window risks a false negative for those two channels specifically. [GSND-001, BATT-003]
  5. Disconnect and reconnect once, as an isolated pair of actions — the one condition under which DLCI 0x0a has ever carried content before. [PAIR-003, GSND-001]
  6. Idle again for ~30–60s after the reconnect settles.
  7. Stop recording and logging. Extract via the raw btsnoop path first (§3), and verify snaplen integrity immediately (capinfos / frame.cap_len == frame.len) before any analysis, per every prior session's practice.

Analysis: filter on the Buds' address first (§13 CLI-hygiene), then check, per channel:

tshark -r CAP-035-btsnoop_hci.log -Y "bluetooth.addr == 04:00:6e:cf:6e:07 and btrfcomm.dlci in {6,8,10,18}" \
  -T fields -e frame.number -e frame.time_relative -e btrfcomm.dlci -e _ws.col.Info

(DLCI values: 0x06=6, 0x08=8, 0x0a=10, 0x12=18 — decimal, since tshark fields compare numerically.) For DLCI 0x08 specifically, if it opens, reproduce CAP-001/CAP-004's exact content check:

tshark -r CAP-035-btsnoop_hci.log -Y 'frame contains "google-pixel-buds-pro-v1"' -T fields -e frame.number -e frame.time

Three-way outcome, stated in advance so the result isn't read selectively (same discipline as Group S/Y/AA):

  • DLCI 0x08's content appears, byte-identical to prior captures, with no GMS present at all — strengthens CAP-004's single data point into a 2nd, stronger confirmation that this channel is OS/vendor-Bluetooth-stack-level, not GMS/Nearby-dependent. Write up as a CAP-035-FINDINGS.md proposal per the usual FACT/HYPOTHESIS discipline — this session alone does not authorize self-promoting anything to 🟢 FACT in PROTOCOL.md (AGENTS.md §6).
  • DLCI 0x08's content is absent or differs — contradicts CAP-004, and becomes a new, actively interesting open question in its own right (not a "failed" session — an absence here is exactly as informative as a presence, same principle as Group S).
  • DLCI 0x0a/0x06/0x12 show any payload at all (previously unobserved outside CAP-021's single DLCI 0x0a burst) — first data point on whether these depend on GMS presence; record whatever is found, don't force an interpretation beyond what the bytes show.

Group AC — Settings-state read-back on (re)connect and on settings-screen open (occasional, added 2026-09-04)

Purpose: ARCHITECTURE.md §3.1 (State Reconciliation) requires this project's own app to query the hardware's actual current state on every (re)connection — ANC mode, battery, EQ, touch-control config — before trusting or displaying any locally cached value. Whether the official app does the same thing, and over which channel, has never been isolated. What the evidence looks like today:

  • DLCI 0x04 (Fast Pair Message Stream): a read direction is documented for ANC — Group 0x08 Code 0x11 "Get ANC state", Seeker → Provider (PROTOCOL.md §4.1's own table, straight from the official Hearable Controls extension). But this project has never observed a 0x11 frame in any capture: only 0x12 (Set) and 0x13 (Notify) appear on the wire (CAP-001-FINDINGS.md §5). Documented-but-never-observed is not the same as absent — nobody has looked for it in a window where it would be expected to fire.
  • DLCI 0x02 (libmaestro's Pigweed HDLC channel): no read direction is documented at all. Every entry in PROTOCOL.md §4.2 (EQ) and §4.5 (Conversation Detection, Multipoint, touch & press-and-hold, head gestures, in-ear detection, mono audio, Volume EQ, volume balance, case sounds) is evidenced exclusively by a write: a UI tap producing a Sent frame, whose only observed response is an Rcvd-direction echo of the same field/prefix shape — "no distinct ACK opcode observed," in §4.5.1's own wording, repeated verbatim across the §4.5 subsections. Not one of those entries rests on a frame that reads as "give me the current value."

So the question this Group exists to answer is genuinely open, and deliberately narrow: does the official app ever issue a state query — as opposed to a state write — and if so, on what trigger? The two candidate triggers this Group brackets are the two that matter for ARCHITECTURE.md §3.1's design: connection establishment, and opening a settings screen.

Runs on the Pixel 7a with the official Pixel Buds companion app and Google Play Services enabled — the normal baseline, deliberately. This is the opposite setup from Groups S/AB, which strip GMS and the app away to isolate what the Buds do on their own. Here the official app's own behavior is the object of study, so it must be present, connected, and behaving normally.

Test-ID: OBS-004 (new — added to TESTPLAN_BLUETOOTH_HCI_SNOOP.md and id_registry.csv alongside this Group). Deliberately distinct from OBS-001 (app launch after a force-close, Group L) and FW-002 (the firmware/serial info screen, Group I) — see that row's Note for why neither covers this.

A clean negative is a real result here, not a failed session. If the log shows no query traffic anywhere in the three windows below, that is citable evidence that the official app does not read settings state back before a user change — which is itself directly load-bearing for ARCHITECTURE.md §3.1 (this project would then be doing something the official app doesn't, and needs to justify how). This project already has precedent for treating a clean negative as real evidence rather than an empty result: DESKRESEARCH_FINDINGS.md's 2026-08-28 cross-capture pass records "the best available negative-result test for this question … and it comes back clean" as a finding in its own right, with the same FACT/HYPOTHESIS labelling discipline as any positive one. Same discipline applies here — record the absence explicitly, with the filters that were run and the windows they covered, per PROJECT_RULES.md §1 rule 4a.

Procedure — three isolated observation windows. No setting is touched at any point in this session. Not a slider, not a toggle, not an ANC mode, not a preset — the entire value of this capture depends on every frame in it being app- or connection-initiated rather than user-initiated. If a setting is touched by accident, note it explicitly in CAP-036-EVENT-NOTES.md and treat the affected window as contaminated rather than quietly keeping it. Leave a clean few-second buffer before each window, per §4's usual rhythm, so the previous window's settling traffic doesn't bleed into the next one.

  1. Reconnect — not a fresh pair [OBS-004, incidental PAIR-003]. The Buds must already be bonded to this phone; do not "Forget"/re-pair (a bonding handshake would flood the window with pairing traffic and defeat the isolation). Trigger a reconnect the way a user normally would — e.g. take the buds out of the case, or toggle the connection from system Bluetooth settings. Note the exact time the connection is established on screen, then idle ~10–15s without navigating anywhere at all, app included.
  2. Open the EQ screen [OBS-004]. Note the exact time the EQ screen finishes rendering, then idle ~15–20s without touching any slider or preset. Do not scroll into a drag by accident — the sliders are the whole risk on this screen.
  3. Navigate away (back out to the device details screen), leave a clean few-second buffer, then open the touch-controls screen [OBS-004]. Note the exact open time, then idle ~15–20s without touching any toggle.

Optional bonus, time permitting — same idle-only pattern, still purely observational, still nothing touched: repeat window 3's shape for the in-ear detection and multipoint settings screens. These are cheap extra data points on whether any per-screen query exists, and they cost nothing but session time; skip them freely if the session is running long, and note in the event notes whether they were run at all.

  1. Stop recording and logging. Extract via the raw btsnoop path first (§3 step 3), and verify snaplen integrity immediately (capinfos / frame.cap_len == frame.len) before any analysis — a truncated log would silently hide exactly the short frames this Group is looking for.

Analysis: pre-filter by the Buds' address first, per §13's CLI-hygiene rule, before layering on any protocol filter — a shared, non-restarted snoop log can contain unrelated devices' traffic. Then, per window:

tshark -r CAP-036-btsnoop_hci.log -Y "bluetooth.addr == 04:00:6e:cf:6e:07 and btrfcomm.dlci in {2,4,8}" \
  -T fields -e frame.number -e frame.time -e frame.p2p_dir -e btrfcomm.dlci -e btrfcomm.len -e data.data

(DLCI values decimal: 0x02=2, 0x04=4, 0x08=8.) Specifically:

  • DLCI 0x04: does a 08 11 … frame ("Get ANC state") appear anywhere — in any of the three windows, or at connection setup? This is the one read-direction opcode this project knows to exist by name, and has never seen.
  • DLCI 0x02: is there any Sent-direction HDLC frame inside a window where nothing was touched? Split each RFCOMM payload on the 0x7e flag byte before decoding (multiple complete HDLC sub-frames routinely pack into one RFCOMM I-frame — DESKRESEARCH_FINDINGS.md's 2026-08-28 pass documents this exact trap). Compare any such frame's shape against §4.5's known field5{field4{…}} write envelope: a write-shaped frame with no user action behind it means something different from an unrecognized, differently-shaped frame — say which was found.
  • DLCI 0x08: the private [Group][Code][Length][Value] envelope already pushes unsolicited content (battery, capabilities, firmware string) on connect — expect traffic here in window 1 and do not mistake a known push for a query. Attribute it against PROTOCOL.md §4.3 Option E before reading anything new into it.

Three-way outcome, stated in advance so the result isn't read selectively (same discipline as Groups S/Y/AA/AB):

  • A query frame appears in one or more windows — identify which channel and which trigger, record it in this session's own CAP-NNN-FINDINGS.md with frame numbers and raw hex per PROJECT_RULES.md §1 rule 4a, and treat the opcode itself as 🟡 HYPOTHESIS until it replicates. Do not self-promote it to 🟢 FACT in PROTOCOL.md, and do not write a DECISIONS.md ADR for it — both require explicit maintainer sign-off (AGENTS.md §6).
  • No query traffic in any window, with clean, uncontaminated windows — a positive finding in its own right (see the negative-result note above): the official app does not read settings state back before a user change, at least not on these two triggers, on this firmware. Record it as such, with the exact filters run and windows covered, and copy the resulting open item into PROTOCOL.md §6 per §8's mandatory rule.
  • Inconclusive — a setting was touched by accident, a window was contaminated by an unrelated action, or the log came back truncated. This supports neither conclusion above; note it as 🔴 unconfirmed and re-run rather than reading a weak result either way.

Related open question this Group also informs (do not conflate it with the above): PROTOCOL.md §6 Behavior carries an item from CAP-024-FINDINGS.md §4 — does opening the "Case sounds" screen itself trigger a state-sync write on DLCI 0x02, or does a write only register on an explicit tap? Windows 2 and 3 here are the same experimental shape (a settings screen opened with nothing touched) applied to different screens, so whatever they show is directly relevant to that question too — but a write on screen-open and a read on screen-open are different findings, and this Group's own question is the read. Keep them labelled separately in the findings.

Group AD — "Get ANC state" reconnect-reliability + dock-state transition, purpose-built repeat (occasional, added 2026-09-05)

Purpose: DECISIONS.md ADR-022 promoted DLCI 0x04's "Get ANC state" (0x11) trigger- reliability to 🟢 FACT and ADR-024 promoted the "Notify ANC state" Settable-toggles byte as a dock-state indicator (0x00 = both earbuds docked, 0xe8 otherwise) — but both promotions rest on retrospective analysis of captures never designed for either question (settings-toggle tests, a pairing repeat, an ANC repeat), and no single session has yet captured the dock-state transition itself within one continuous recording. This Group is the first purpose-built session for both: 5 isolated reconnects in one sitting, deliberately alternating dock state.

Runs on the Pixel 7a with the official Pixel Buds companion app and Google Play Services enabled — the same baseline as Group AC, so nothing about the phone/app/GMS condition is a new variable here; only the repeat structure and deliberate dock-state alternation are new.

Test-ID: OBS-004 (existing, CAP-036's own Test-ID — this is a deeper replication of the same underlying question, not a new one) plus incidental PAIR-003.

Procedure — 5 reconnects, alternating dock state, buffered ~3–5s apart: buds start docked; repeat 1 reconnect from docked; repeat 2 reconnect from undocked (both buds removed from the case, not worn, held/on the table); repeat 3 reconnect from docked again; repeat 4 reconnect from undocked; repeat 5 reconnect from docked. Toggle Bluetooth off between every repeat for a clean disconnect. See CAP-037-EVENT-NOTES.md for the full step-by-step procedure and preparation checklist.

Analysis: for each of the 5 repeats, confirm 08 11 00 00/08 13 fires and record its Settable-toggles byte against that repeat's known dock state. A miss on any repeat is a counter-example to ADR-022; a Settable-toggles value that doesn't match dock state on any repeat is a counter-example to ADR-024 — either must be reported plainly, not reconciled away. See CAP-037-EVENT-NOTES.md's Decode/Analysis checklist for the full three-way outcome.

Group AE — Realistic physical reconnect trigger vs. a system-Bluetooth-toggle reconnect (occasional, added 2026-09-05)

Purpose: every reconnect this project has captured to date on the Pixel 7a with the official app (CAP-036, CAP-037) was triggered by toggling Bluetooth in system settings while the Buds sat in the open case, never removed, never worn — not the normal user flow of taking the Buds out of the case and inserting them into the ears. That normal flow could exercise wire traffic a pure OS-level toggle never touches: an in-ear-detection event, A2DP audio-profile setup, or a different sequencing of the DLCI 0x02/0x04/0x08 connection burst. This Group tests that directly.

Runs on the Pixel 7a with the official Pixel Buds companion app and Google Play Services enabled, same baseline as Group AC/AD.

Test-ID: OBS-005 (new), incidental PAIR-003 and the INEAR family.

Procedure — two windows: (1) with Bluetooth already on, physically remove both earbuds from the case and insert them into the ears, the normal user flow, and idle ~15–20s; (2) optional/time permitting — with the Buds back in the case and Bluetooth on, close the lid, then reopen it without touching the Bluetooth toggle, to see whether the lid alone triggers a differently- shaped reconnect than an OS-level toggle. See CAP-038-EVENT-NOTES.md for the full procedure.

Analysis: confirm the DLCI 0x04 Get/Notify pair still fires (expected, per ADR-022's channel-(re)establishment-scoped trigger) and its Settable-toggles byte reads "undocked" (0xe8, per ADR-024, now genuinely worn rather than merely "not in the case"). The core question is whether any traffic appears here that has no counterpart in CAP-036/CAP-037 — an in-ear-detection push, A2DP setup, or a structurally different connection burst. See CAP-038-EVENT-NOTES.md's Decode/Analysis checklist.

Group AF — Settable-toggles byte: Set-tap vs. reconnect-Get, fixed dock state (occasional, added 2026-09-05)

Purpose: CAP-036-FINDINGS.md §3 originally framed this open question as "does a Set- triggered Notify differ from a Get-triggered Notify?" DECISIONS.md ADR-024 reframed it as tracking dock state instead — but that reframing has never been tested by comparing a Set and a Get within one session, at a fixed, known dock state. This Group closes that gap directly: one ANC tap (a Set) and one forced reconnect (a Get), both with the Buds worn (undocked) throughout, so dock state cannot explain any difference found.

Runs on the Pixel 7a with the official Pixel Buds companion app and Google Play Services enabled, same baseline. Buds stay in the ears (undocked) for the entire session, including during the forced reconnect.

Test-ID: OBS-006 (new), incidental ANC-family and PAIR-003.

Procedure: one ANC mode tap (any mode different from the current one), then a clean buffer, then force a reconnect via the Bluetooth toggle without removing the Buds from the ears. See CAP-039-EVENT-NOTES.md for the full procedure.

Analysis: decode both the Set-triggered and the Get-triggered Notify frames' Settable-toggles byte and compare directly. Identical values (both 0xe8, matching the constant undocked state) support ADR-024's dock-state reading as trigger-independent; a difference despite identical dock state is a genuine counter-example to ADR-024 and must be flagged prominently, not reconciled quietly. See CAP-039-EVENT-NOTES.md's Decode/Analysis checklist.

Group AG — DLCI 0x08's unmapped Get-shaped codes vs. a known-changing value (occasional, added 2026-09-05)

Purpose: CAP-036-FINDINGS.md §5 found DLCI 0x08's connect-time burst contains several zero-length [Group][Code][00 00]-shaped Sent frames (05 0c, 04 02, 04 04, 04 11, 04 13, 04 15, 0e 04) structurally identical to DLCI 0x04's confirmed "Get" pattern — but none of these Group/Code pairs is mapped to any known setting, and their Rcvd responses have never been examined. Per AGENTS.md §13.6's zero-creativity rule, these can only be decoded via correlation against a known, independently-verifiable value, not by guessing.

Runs on the Pixel 7a with the official Pixel Buds companion app and Google Play Services enabled, same baseline, with the app open on Device details so on-screen battery/case values are visible for cross-check.

Test-ID: PRIV-001 (new), incidental PAIR-003 and the BATT family.

Procedure: bracket a known-changing value across 4 reconnects — either an opportunistic battery-discharge gap (if a convenient one exists), or the recommended default: vary which component (Left/Right/both/neither) is docked at each of 4 reconnects, reading the resulting on-screen Left/Right/Case values each time. See CAP-040-EVENT-NOTES.md for the full procedure and both bracketing options.

Analysis: for each unmapped code, find its Sent Get frame and the immediately-following Rcvd response(s) on the same Group, decode any numeric fields, and check whether any field tracks the bracketed value across all 4 repeats. A field that stays constant is not a match; only report a semantic reading for a field that visibly tracks the known value repeat after repeat. See CAP-040-EVENT-NOTES.md's Decode/Analysis checklist for the full three-way outcome.

Group AH — DLCI 0x02's connect-time RPC burst vs. non-default settings state (occasional, added 2026-09-05)

Purpose: CAP-036-FINDINGS.md §4 found a dense, undecoded RPC-shaped burst on DLCI 0x02 immediately after it opens on reconnect, captured under a session where every setting was at its default (EQ centered, touch controls on). This leaves open whether the burst's content reflects current settings state at all — directly relevant to ARCHITECTURE.md §3.1 (State Reconciliation): if libmaestro's own channel carries a settings-state read-back, that would be a second, independent mechanism alongside DLCI 0x04's confirmed ANC read (ADR-021/ADR-022). This Group tests it by deliberately setting non-default EQ and touch-controls values before reconnecting, then comparing the resulting burst byte-for-byte against CAP-036's own default-settings burst.

Runs on the Pixel 7a with the official Pixel Buds companion app and Google Play Services enabled, same baseline as CAP-036 — a clean single-variable comparison (settings state only).

Test-ID: OBS-007 (new), incidental PAIR-003.

Procedure: set EQ bands well off-center and touch controls off before recording starts (the fixed starting condition for this session), confirm on screen, then reconnect once and idle ~15s. See CAP-041-EVENT-NOTES.md for the full procedure and preparation checklist.

Analysis: extract this session's own connect-time DLCI 0x02 burst using the same method as CAP-036-FINDINGS.md §4, and diff it byte-for-byte against CAP-036's own burst. Any payload that differs and also plausibly matches a known settings-value shape (e.g. a 5-band float quintet, PROTOCOL.md §4.2) is a strong candidate for a libmaestro-side state read-back; a byte-for-byte match despite genuinely different settings is a clean negative. See CAP-041-EVENT-NOTES.md's Decode/Analysis checklist.

Group AI — Long pure-idle bracket for the periodic DLCI 0x02/0x04/0x08/HFP push cadence (occasional, added 2026-09-05)

Purpose: CAP-036-FINDINGS.md §12.5 found a periodic push recurring on DLCI 0x02, 0x04, 0x08, and HFP's AT+BIEV all within 7–18ms of each other, starting ~3.5 minutes after connection — but CAP-036's own session only ran ~7 minutes, too short to characterize the cadence's longer-run behavior the way CAP-009-FINDINGS.md §2's 101-minute HFP-only session already does for AT+BIEV alone (PROTOCOL.md §4.3 Option C: "settling burst, then irregular, median ~20s, up to ~14.6 minutes"). This Group is a dedicated, 15+ minute, genuinely idle bracket to extend that characterization across all four channels at once — this is exactly the capture scenario TESTPLAN_BLUETOOTH_HCI_SNOOP.md's OBS-002 row has been waiting for.

Runs on the Pixel 7a with the official Pixel Buds companion app (backgrounded, not kept open) and Google Play Services enabled, same baseline. Dock state must be picked once and held constant for the whole session (worn or docked, either is fine, but ADR-024 shows dock state affects at least one of these frames' content, so a mid-session dock-state change would confound the cadence measurement).

Test-ID: OBS-002 (existing — this is that row's first dedicated capture scenario).

Procedure: connect normally, confirm the fixed dock state once, then background the app and leave everything untouched (ideally screen off) for at least 15 minutes. See CAP-042-EVENT-NOTES.md for the full procedure, including a note on using periodic wall-clock check-in shots rather than continuous video for a session this long.

Analysis: extract every DLCI 0x02/0x04/0x08 push and every AT+BIEV=2 occurrence across the whole session, check whether the cross-channel near-lockstep timing holds up over dozens of occurrences (not just CAP-036's handful), and compare the gap distribution against CAP-009's existing HFP-only model. Also cross-check against CAP-027's cross-sync-caveat (DESKRESEARCH_FINDINGS.md 2026-09-04 round 2) — that session found the sync breaking down 3 times during active A2DP streaming; this idle session is the control for whether it holds up perfectly with no streaming at all. See CAP-042-EVENT-NOTES.md's Decode/Analysis checklist.

Group AJ — HOLD-005 Left/Right ANC-rotation-checklist split (occasional, added 2026-09-09)

Purpose: PROTOCOL.md §4.5.3's ANC-mode rotation checklist (qhr field 12, confirmed field-number identity as qht) has 16 wire-observed boolean flags (HOLD-005, CAP-021) but no Left/Right-distinguishing field for this specific write — unlike HOLD-001HOLD-004, it's unknown which frames belong to which earbud's own rotation list.

  1. Open Device details → Controls and gestures → the ANC-mode rotation checklist for the Left earbud specifically (the UI is confirmed to expose this per-earbud, per PROTOCOL.md §4.5.3's own UI description).
  2. Toggle each of the 4 checklist items (Noise cancellation / Off / Adaptive / Transparency) for the Left earbud one at a time, with a clear pause (≥10s) and a distinct video-visible action between each toggle, so each write can be isolated to one specific checklist item.
  3. Repeat step 2 for the Right earbud's own rotation checklist, again one item at a time.

Analysis: do the Left-earbud toggles and Right-earbud toggles produce distinguishable wire patterns (e.g. a different inner field position, a different correlation-ID pattern, or simply a confirmed video-to-frame 1:1 timing correlation good enough to assign each frame to a side by elimination)? This directly closes PROTOCOL.md §6's open item on this question.

Group AK — Volume balance (field 17) scale/direction (occasional, added 2026-09-09)

Purpose: PROTOCOL.md §4.5.7 confirms qhr field 17 = Volume balance (full identity, 🟢 FACT), but its numeric scale/range beyond the 7 samples in one continuous drag (CAP-022) and which direction (Left/Right) corresponds to negative vs. positive zigzag-decoded values remain 🔴 open — a single continuous drag at 1fps video-sampling resolution wasn't enough to resolve this.

  1. Open Device details → Sound → the Balance slider.
  2. Drag the slider to its full Left extreme, release, and hold for ≥3s before any further action (an isolated, discrete sample, not a continuous drag) — video-confirm the slider's own on-screen position/label at this extreme.
  3. Return the slider to center, pause ≥5s, then drag to its full Right extreme, release, hold ≥3s, video-confirm.
  4. Repeat steps 2–3 at least once more for a second independent sample of each extreme.
  5. Optionally, sample 1–2 clearly-labeled intermediate positions (e.g. "25% Left", "25% Right" if the UI shows a numeric/percentage label) the same isolated way.

Analysis: zigzag-decode ((n>>1) ^ -(n&1)) each isolated sample's field 17 value and match it against the video-confirmed slider position/label at that exact moment — this directly closes PROTOCOL.md §6's open item on this field's scale/direction.

Group AL — DLCI 0x0a burst trigger, purpose-built hypothesis test (occasional, added 2026-09-09)

Purpose: CAP-021's 1123-frame DLCI 0x0a burst (PROTOCOL.md §6) has recurred in exactly 1 of 16+ sessions checked — passively waiting for it is not expected to work. TODO.md's own "Recommended priority order" §5 asks for a purpose-built hypothesis test bracketing candidate triggers one at a time, per PROJECT_RULES.md §4's fixed template (hypothesis, setup, expected outcome, actual outcome, conclusion).

Run as up to 3 separate bracketed sub-sessions, each testing exactly one candidate trigger, logged either as 3 rows under this one Group or as 3 short sequential capture windows in one continuous log with clear boundary timestamps — the maintainer's choice at execution time, record which was used:

  1. Trigger candidate 1 — app backgrounded/foregrounded: with the Buds connected and idle, background the official app for ≥2 minutes, then foreground it again; log throughout.
  2. Trigger candidate 2 — a scheduled sync window: leave the Buds connected and the phone otherwise idle (screen off, app backgrounded) for an extended window (≥15 minutes, matching CAP-042's own idle-bracket precedent) to see if the burst appears without any explicit action.
  3. Trigger candidate 3 — a charge-state change: dock one or both Buds into the case (charging begins) or remove them (charging stops) while logging, isolating this specific transition.

Analysis, per PROJECT_RULES.md §4's template: for each bracketed trigger, record explicitly whether the 1123-frame (or any size) DLCI 0x0a burst appeared in or shortly after that specific window — a negative result for all three is itself a valuable, reportable outcome, not a failed session.

Group AM — qhr field 13 ANC-parallel-path wire confirmation (occasional, added 2026-09-09)

Purpose: REVERSE_ENGINEERING.md's qhr entry fully traces field 13 (ANC state, DLCI 0x02) to exactly two code-side callers — an in-app QuickActionsFragment toggle-group tap, and a physical press-and-hold gesture (gvi/gvj) — but neither has ever been wire-confirmed: no capture has yet correlated a qhr-field-13 write on DLCI 0x02 with an itself otherwise-unexplained DLCI-0x04 Notify (the specific pattern CAP-038-FINDINGS.md §5 observed twice with no preceding Get/Set on DLCI 0x04).

  1. With full DLCI 0x02 traffic retained (raw-path extraction, not btsnooz.py), perform an isolated ANC-mode change via the in-app QuickActionsFragment toggle group (a single tap, pause ≥10s before the next action, matching CAP-006's own isolated-tap discipline).
  2. Separately, perform an isolated ANC-mode change via a physical press-and-hold gesture on one earbud (again, single gesture, pause ≥10s).

Analysis: for each of the two actions, check whether a field5{field4{field13=N}}} write appears on DLCI 0x02 at that moment — a positive match (especially time-correlated with the tap/gesture, mirroring CAP-006's DLCI-0x04 confirmation methodology) would be strong evidence DLCI 0x02 also carries ANC state in parallel to DLCI 0x04's already-confirmed path.

Group AN — CAP-041 Case%-change bracket (occasional, added 2026-09-09)

Purpose: CAP-041-FINDINGS.md §4 and CAP-036-FINDINGS.md §12.6 both found a recurring 2-field sub-message inside DLCI 0x02's connect-time/periodic burst holding a constant value that happens to match the on-screen Case battery percentage throughout the session — but because the value never changed in either session, this is consistent with, but does not confirm, the field tracking Case battery (it could equally be any other session-constant value that happens to match).

  1. Start a session with the Case at a known, video-confirmed battery percentage (check via the official app's Device details screen before starting).
  2. Over an extended session (≥30 minutes, allowing genuine charge/discharge to occur — e.g. leave the Case charging via USB for part of the window, or simply let it discharge naturally if a bud is docked), periodically re-check and video-confirm the on-screen Case percentage.
  3. Ensure the DLCI 0x02 periodic push (already confirmed to recur every few minutes when the app is foregrounded, per CAP-036-FINDINGS.md §12.5) is captured throughout.

Analysis: does the recurring 2-field sub-message's value actually change in step with the video-confirmed Case percentage changes? A positive correlation across a genuine change would promote this from "consistent with" to real evidence; a mismatch would be an equally useful, reportable negative result.

Group AO — EQ outer field 16-vs-18: drag-and-release without ever tapping Save (occasional, added 2026-09-13, ai-sessions/0017)

Purpose: PROTOCOL.md §4.2's "Outer field 16 vs. 18" item is a genuine, unreconciled tension: CAP-015's wire timing reads as "field 18 fires on slider-release" (no video-visible Save tap before any of 15 field-18 frames), but a full call-graph trace of fyd.d/fyd.e (REVERSE_ENGINEERING.md's qjw entry) found field 18 (fyd.d) reachable through exactly two code paths — a dedicated key_eq_save_button/title_eq_save_button click handler (self-describing log "On click save EQ button"), and, newly found by this session's own re-verification, a navigate-away-from-the-EQ-screen path (hod.java:36, self-describing log "Navigate away, save EQ", gated on an unsaved-changes-shaped flag) — but no slider-release code path to field 18 anywhere in this app version's decompiled source. This capture is designed to isolate all three candidate triggers from each other for the first time.

  1. Open the EQ screen (Device details → Sound → Equalizer → custom sliders).
  2. Drag slider 1 to a new position, release, and wait ≥10s with the Save button never tapped and without navigating away from the EQ screen (stay on this exact screen the whole time) — video must clearly show both: the Save button not being touched, and the screen not changing.
  3. Repeat step 2 for slider 2 (a second, independent release-only sample, still without leaving the screen or tapping Save).
  4. As a clearly separated second half of the same session: drag slider 3, release, wait ≥10s (no Save tap, no navigation, replicating steps 2–3's isolation once more), then deliberately tap the Save button and video-confirm the tap.
  5. As a third, separated part: drag slider 4, release, wait ≥3s, then navigate away from the EQ screen (e.g. press back to Device details) without ever tapping Save — video must show the screen change clearly.

Analysis: for each of the three isolated conditions (release-only, release+Save, release+navigate-away), does a field5{field4{field18=...}} write appear on DLCI 0x02, and if so, at which specific action? This directly distinguishes the three candidate triggers REVERSE_ENGINEERING.md's qjw entry now names, closing PROTOCOL.md §4.2's own open item with a within-session positive/negative contrast instead of a single ambiguous reading.

Group AP — Battery Notification: bracket a single-bud insertion/removal, connection-free (occasional, added 2026-09-13, ai-sessions/0017)

Purpose: CAP-043 (Group Q repeat) established, under rigorously clean connection-free isolation, that the Buds' idle/case-closed 0xFE2C BLE advertisement does not structurally match PROTOCOL.md §4.3 Option A's documented Battery Notification layout — but only tested the idle/case-closed condition. The official Fast Pair spec itself describes the Battery Notification extension as "optional when a single bud is inserted/removed" — a materially different trigger condition, not yet bracketed by any capture to date.

  1. Force-stop the official Pixel Buds app (as CAP-043 did) and confirm, before starting the log, that no classic RFCOMM connection to the Buds is active (system Bluetooth settings showing "not connected," or the Buds already disconnected).
  2. Start HCI snoop logging and a screen/phone-camera recording of the system Bluetooth settings panel (matching CAP-043's own methodology).
  3. With the case closed and the phone otherwise idle, remove exactly one earbud from the case (video-confirm the exact removal moment), then wait ≥15s without touching anything else.
  4. Re-insert that same earbud into the case (video-confirm), wait ≥15s again.
  5. Repeat steps 3–4 once more for the other earbud, as an independent second sample.
  6. Throughout, avoid opening the official app or making any classic RFCOMM connection — per AGENTS.md §7's bounded scanning exception (filtered to the bonded device, foreground-triggered, time-boxed).

Analysis: does a 0xFE2C service-data advertisement carrying the documented Battery Notification layout ([Flags=0x00][Account Key Data][0x33/0x34 marker][L][R][Case]) appear within a few seconds of either bracketed insertion/removal event, even though it does not appear during idle/case-closed conditions? A clean negative here (matching CAP-011/CAP-036/CAP-043's existing non-matches) would materially strengthen the case for reframing PROTOCOL.md §4.3 Option A's status, per CAP-043-FINDINGS.md §7's own recommendation — see ai-sessions/0017_MAINTENANCE_RESULT_2026_09_13.md Phase 2 for that scoping question.

Group AQ — Head gestures (Nod/Shake) with an active call/notification (occasional, added 2026-09-13, ai-sessions/0017)

Purpose: CAP-028 (Group O) found zero wire-visible traffic on the Buds' own connection during a claimed Nod/Shake gesture window — but TESTPLAN_BLUETOOTH_HCI_SNOOP.md's own description ties Nod/Shake's actual function to an active call ("answers a call") or notification ("dismisses a text reply"), and no call/notification was active during CAP-028's own window, so the clean-negative result cannot distinguish "functionally inert, as expected" from "gesture not actually triggered." This Group fixes that specific gap.

  1. Confirm "Use head gestures" is toggled ON (Device details → Controls and gestures) before starting — per CAP-020's own precondition, re-confirm on camera this time (do not rely on a carried-over assumption).
  2. With the Buds connected and worn, trigger an actual incoming phone call (e.g. call the test phone from a second phone). While the call is ringing, perform a Nod gesture — camera angled to capture both the phone screen (showing the call being answered) and the user's head/ears (the gesture itself), unlike CAP-028's phone-only framing.
  3. During the call, end it normally; then trigger a second incoming call and perform a Shake gesture to reject it — same dual camera framing.
  4. Separately, trigger a text-message notification (with 'Spoken notifications'/dictation-reply context if feasible) and perform a Nod (reply via dictation) or Shake (dismiss) gesture, camera angled the same dual way.

Analysis: does a Nod/Shake gesture performed while a call/notification is genuinely active produce any wire-visible signal (SCO/eSCO setup change, AVRCP command, DLCI 0x02/0x04/0x08 write) correlated with the gesture, now that both the gesture itself and its effect are camera-confirmed? Also check the REVERSE_ENGINEERING.md HeadGesture/qin register (updated by ai-sessions/0017 Phase 3 — the SubscribeToResults response enum is confirmed 3-valued, structurally consistent with exactly two real gesture types plus one unset/unknown sentinel, though which raw value is Nod vs. Shake is not recoverable from static analysis) against any DLCI 0x02 traffic this session captures.

Group AR — HOLD-005 ANC-rotation-checklist Left/Right split, genuine re-run (occasional, added 2026-09-13, ai-sessions/0017)

Purpose: CAP-045 (Group AJ) did not run Group AJ's own procedure — the maintainer performed physical press-and-hold ANC cycling instead, and the rotation-checklist screen was never opened, so HOLD-005's Left/Right question remains exactly as open as before CAP-045. This Group is a fresh attempt at the same question, not a CAP-045 v2 reusing Group AJ's own letter — Group AJ's procedure is unchanged and still valid; this Group exists specifically to add an explicit anti-repeat safeguard, named as such, referencing CAP-045 by name as the reason for the extra emphasis.

  1. Anti-repeat safeguard (mandatory, named per CAP-045's own gap): before toggling anything, video-confirm the actual on-screen destination is "Device details → Controls and gestures → [the ANC-mode rotation checklist screen]" — a checkbox list titled with the four modes (Noise cancellation / Off / Adaptive / Transparency) must be clearly visible on-camera for at least one full toggle before this session counts as having run the procedure at all. If the checklist screen is not visible on camera at this point, stop and restart — do not proceed and hope the wire data disambiguates it after the fact, as happened in CAP-045.
  2. With the checklist screen confirmed open for the Left earbud specifically (per PROTOCOL.md §4.5.3's own UI description confirming this is per-earbud), toggle each of the 4 checklist items one at a time, with a clear pause (≥10s) and a distinct video-visible action between each toggle.
  3. Navigate to the Right earbud's own rotation checklist (video-confirm the screen again, per step 1's safeguard) and repeat step 2.

Analysis: identical to Group AJ's own — do the Left-earbud and Right-earbud toggles produce distinguishable wire patterns (inner field position, correlation-ID pattern, or a confirmed video-to-frame 1:1 timing correlation good enough to assign each frame to a side by elimination)?

Group AS — Live GetSoftwareInfo/GetHardwareInfo correlation against the connect-time burst (occasional, added 2026-09-13, ai-sessions/0017)

Purpose: PROTOCOL.md §6's serial-number candidate (CAP-036 frame 1423, three length-14 strings inside DLCI 0x02's connect-time burst) was re-traced this session (ai-sessions/0017_MAINTENANCE_RESULT_2026_09_13.md Phase 5): the decoded sub-message's own top-level fields (1/2/3, each a direct STRING) structurally match qjm/qjr (GetHardwareInfo's two oneof alternatives, qiv) exactly, and do not match qie (GetSoftwareInfo's own oneof alternative, whose 3 fields are typed MESSAGE, requiring an extra nesting level not present in the observed bytes) — a correction to, not a confirmation of, the existing HYPOTHESIS. Static analysis alone cannot go further; this needs a live correlation.

  1. With full DLCI 0x02 traffic retained (raw-path extraction, not btsnooz.py), open Device details → More settings → "View firmware version per component (L/R/Case)" and "View serial numbers per component" (FW-002/FW-003) — video-confirm and transcribe the exact displayed serial numbers per component (Left/Right/Case) on camera.
  2. Separately, disconnect and reconnect the Buds (a fresh classic connection) to capture a fresh connect-time DLCI 0x02 burst.

Analysis: does the connect-time burst's own three-length-14-string sub-message match the video-transcribed serial numbers from step 1, confirming the "EC"/"DR"/"DL"-substring Case/Right/Left semantic reading (still 🟡 HYPOTHESIS per AGENTS.md §13.6 even if the RPC identity resolves)? And does decoding the burst's surrounding RPC envelope (service/method identifiers, per REVERSE_ENGINEERING.md's nqx/npy/nqo pw_rpc entries) show a GetHardwareInfo call/response rather than GetSoftwareInfo, resolving this session's own structural finding one way or the other?

4.2 Pixel 9a (GrapheneOS) — secondary/validation session

No app-driven commands are possible here, so this session focuses on connection-level and passive behavior:

  1. Pairing [PAIR-001], via system Bluetooth settings (Settings → Connected devices → Pair new device). Note the start time precisely — this is the most information-dense part of this session (bonding handshake, initial service discovery), and like Group A #1 it's one user action producing a multi-frame burst, not something to isolate frame by frame.
  2. Idle observation [BATT-003] — once connected, wait ~30–60 seconds without touching anything. This can reveal spontaneous status frames the Buds send unprompted. Note the observation start (once you stop touching anything, leaving a clean gap after the pairing burst settles), the observation end, and confirm the connection state stayed connected throughout — so this window isn't later confused with settling traffic from item 1.
  3. Open the Bluetooth device detail screen for the Buds in system settings [GATT-001] (this can trigger GATT service/characteristic discovery on some Android versions).
  4. Disconnect and reconnect once, as its own isolated pair of actions [PAIR-003], to observe both a clean teardown and a reconnection to an already-bonded device (useful for validating ARCHITECTURE.md §6/§7 resilience assumptions).

Pull the bugreport (§3) the same way.

4.3 Hardware Actions (either phone)

Source: TESTPLAN_BLUETOOTH_HCI_SNOOP.md sections 2 (User Actions via the Case & Buds) and 4 (Automatic Actions Initiated by the Hardware). These actions are grouped separately from 4.1/4.2 because they aren't tied to a specific phone — a tap on the bud, or the case button, behaves the same regardless of which device is connected. Run this group on whichever phone you already have connected and logging at the time; if you want to compare hardware behavior across both OSes for a specific action, repeat that one action on the other phone as its own short session rather than redoing the whole group.

Same rhythm as before: wait ~5s → note the exact time → perform the action → wait ~5–10s → move to the next action.

Group M — Case & wear state

  1. Open the charging case lid [CASE-003]. Wait. Note time.
  2. Remove Left earbud from the case [CASE-004]. Wait. Note time.
  3. Remove Right earbud from the case [CASE-005]. Wait. Note time.
  4. Insert Left earbud into the ear [INEAR-002]. Wait. Note time.
  5. Insert Right earbud into the ear [INEAR-003]. Wait. Note time.
  6. Place buds back in the case and close the lid [CASE-006]. Wait. Note time. (Expected to terminate the active Bluetooth Classic connection — good for validating ARCHITECTURE.md §6/§7 disconnect handling.)

Group N — Touch gestures

  1. Tap once on a bud [TOUCH-002]. Wait. Note time.
  2. Double-tap on a bud [TOUCH-003]. Wait. Note time.
  3. Triple-tap on a bud [TOUCH-004]. Wait. Note time.
  4. Swipe forward on a bud (volume up) [TOUCH-005]. Wait. Note time.
  5. Swipe backward on a bud (volume down) [TOUCH-006]. Wait. Note time.
  6. Press and hold on a bud [TOUCH-007]. Wait. Note time. (Behavior depends on the per-earbud configuration set in §4.1 Group G — note which mode was active when you test this.)

Group O — Head gestures

Requires 'Head gestures' enabled (§4.1 Group F). 13. Nod [HEAD-002] (simulating answering a call, or a text reply if 'Spoken notifications' is on). Wait. Note time. 14. Shake [HEAD-003] (simulating rejecting a call/dismissing a text reply). Wait. Note time.

Group P — Voice & case button

  1. Start speaking with Conversation Detection on [CONV-002] (§4.1 Group C), to trigger the detection event. Wait. Note time.
  2. Hold the case button for 30 seconds [CASE-007] (case open, buds inside, plugged into power) — ⚠️ this is a confirmed full factory reset, not just pairing mode (per TESTPLAN_BLUETOOTH_HCI_SNOOP.md §2). Do this deliberately, last, and only once you're ready to re-pair from scratch — it will also reset the Find My Device link on the Pro 2. If you do trigger it, capture the subsequent re-pair as its own isolated session right afterward [PAIR-002] (same rhythm as Group A #1) — this gives a second, from-true-factory-state bonding capture to compare against Group A's lightweight forget-and-re-pair baseline. It is optional and not a prerequisite for anything else in this guide — Group A's lightweight baseline is sufficient on its own for every other group.
  3. (Open question, see PROTOCOL.md §6) Try a shorter/different press [CASE-008] on the case button to see if it triggers pairing mode without a full reset. No officially confirmed duration exists for this — treat your own finding here as [VERIFIED-LOCAL] material for the relevant CAP-NNN-FINDINGS.md once confirmed.

Group Q — Automatic hardware behavior (observation, not action)

These are waiting periods to catch spontaneous hardware-initiated traffic per TESTPLAN_BLUETOOTH_HCI_SNOOP.md §4 — nothing to tap, just capture while the condition holds. As with Group L, log explicit observation start and observation end boundaries, not just a single timestamp, so this traffic isn't confused with settling traffic from whatever preceded the window.

For items 19–20 below, "nothing found" is not a single outcome — record which of these three actually happened, since they mean different things for protocol reconstruction:

  • Local behavior confirmed + Bluetooth traffic observed → record the frame(s) as [VERIFIED-LOCAL] per the usual process (§5 step 4).
  • Local behavior confirmed, but no Bluetooth traffic in the log despite a clean observation window → this is itself a positive finding, not an absence of one — it's evidence for a purely on-device implementation. Record it in the relevant CAP-NNN-FINDINGS.md as such (e.g. 🟢 FACT: "no wire-visible signal observed on trigger, N attempts"), don't just leave the row blank.
  • Inconclusive — you're not sure the local trigger actually fired (e.g. unclear whether the clap was loud enough, or the environment change was large enough), or the observation window was contaminated by other traffic. This does not support either conclusion above; note it as 🔴 unconfirmed and, if practical, retry with a clearer trigger before drawing any conclusion.
  1. Passive BLE scan while the case is closed and idle [BATT-002/BATT-003] — intended to catch the Fast Pair Battery Notification advertisement (PROTOCOL.md §4.3 Option A) without any active RFCOMM connection. This doesn't require the buds to be connected to the capturing phone at all — any nearby scan should do, per the spec. This is a one-off, manual reverse-engineering capture, not a template for the app. The production app's own BLE scanning stays governed separately, and more narrowly, by the bounded exception in AGENTS.md §7 / DECISIONS.md ADR-006 — this experiment does not authorize a broader scanning implementation than that.
  2. Trigger a loud, sudden sound near the buds while worn [LOUD-001] (e.g. clap sharply nearby) to attempt to observe Loud Noise Protection engaging (PROTOCOL.md §4.5/§6) — confirm you actually noticed the local effect (e.g. audible volume dip) before concluding anything about the Bluetooth traffic (or lack of it); see the three-way outcome guidance above.
  3. Move between distinctly different acoustic environments while worn [ADAPT-002] (e.g. quiet room → street) to attempt to observe Adaptive Audio adjusting (PROTOCOL.md §4.5/§6) — same guidance as #19: confirm the local effect first, then classify the Bluetooth-traffic outcome using the three categories above.

5. Analyzing in Wireshark

  1. Open the extracted CAP-*-btsnoop_hci.log file directly in Wireshark (File → Open, or drag-and-drop — no special import steps needed, Wireshark recognizes the BTSnoop file format natively). This gets you HCI/L2CAP/RFCOMM/ATT-level framing for free — it does not mean Wireshark understands the libmaestro payload itself. There is no dissector for a proprietary, undocumented protocol, so the actual command bytes inside an RFCOMM frame will show up as opaque raw data; decoding what they mean is manual work you do against the hypotheses in PROTOCOL.md §2 (see step 4 below) — that manual decoding is the actual point of this whole procedure.
  2. Useful filters to narrow the view:
    • bthci_acl — general ACL-level Bluetooth traffic.
    • btrfcomm — RFCOMM traffic specifically (this is where libmaestro frames live).
    • btatt — GATT/ATT traffic (relevant for the BLE battery-service investigation).
    • btle — BLE Link Layer traffic (advertising/scan reports) — this is where the Fast Pair Battery Notification capture (Group Q #18) shows up, not btrfcomm.
    • bluetooth.addr == <buds MAC> — restrict to the Buds' Bluetooth address once you've identified it (visible in the pairing frames or in Wireshark's Bluetooth device list under View → Bluetooth Devices).
  3. Use the timestamp column together with your own action log (§1.3, §4) to identify which frame(s) correspond to which action. Wireshark's relative or UTC time display (View → Time Display Format) should be set to whatever you used when noting action times, to avoid an offset mismatch. Since the capture-side wait time (§4) is a heuristic, not a guarantee: if a frame appears just after your noted "wait ~5–10s" window for one action but before the next action's timestamp, treat it as a probable late response to the earlier action rather than automatically attributing it to whatever came next — check payload plausibility against both candidates before deciding, and note the ambiguity in this session's CAP-NNN-FINDINGS.md if it can't be resolved from the log alone.
  4. For each identified command frame:
    • Note the raw bytes (right-click → Copy → ...as Hex Stream is fastest).
    • If it's an RFCOMM frame (btrfcomm — an app-triggered command, or the Find My Buds/Ring action): compare the structure against the envelope hypothesis in PROTOCOL.md §2 (magic byte, length field, channel/msg ID, payload, checksum).
    • If it's a BLE advertisement (btle — the Battery Notification, Group Q #18): compare it against the Fast Pair Battery Notification structure in PROTOCOL.md §4.3 Option A (flags, account key data, battery-level-length/type byte, then the 3 battery bytes) instead. This is a different, unrelated structure — it is not RFCOMM traffic and was never expected to match the §2 envelope hypothesis; don't force-fit it there or record a false "doesn't match" finding.
    • Record the confirmed values in this session's CAP-NNN-FINDINGS.md first, then promote them into PROTOCOL.md §4.1's opcode table (RFCOMM frames) or §4.3 (battery), mark the entry [VERIFIED-LOCAL] with today's date, and raise its confidence to 🟢. Include the Test-ID from TESTPLAN_BLUETOOTH_HCI_SNOOP.md (e.g. ANC-001) and this session's CAP-NNN ID in that note — this is what closes the evidence chain in TESTPLAN_BLUETOOTH_HCI_SNOOP.md §0.1. Also add the Test-ID to this session's row in the Capture Index (§9) Test(s) column if it isn't there yet.
  5. If a frame doesn't match the expected envelope shape at all, don't force-fit it — note it as an open question (PROTOCOL.md §6) rather than recording a guess as fact.

6. Notes & Gotchas

  • Log rotation: the in-device snoop buffer is finite; very long or idle-heavy sessions can push earlier frames out before you extract them. Keep sessions focused (§2, §4).
  • A Bluetooth restart is required for the HCI snoop logging setting to take effect — per Android's own source documentation, not just this guide's experience (this project has no capture statistics of its own yet to say how common empty-log failures are). Reboot by default (§2 step 5); a plain toggle only if you've confirmed it works reliably on your specific phone.
  • adb root will not work on either phone for this purpose — both stock Pixel firmware and GrapheneOS are, as set up for this project, normal production/non-rooted builds, on which adbd cannot run as root. This is about the current, as-configured state of these two phones, not a claim that Pixel hardware can't be rooted by other means — don't waste time trying to root a device just for this; adb bugreport is the supported path and is sufficient.
  • Path differences across Android versions: the internal bugreport path for the snoop log has moved between FS/data/log/bt/ and FS/data/misc/bluetooth/logs/ across Android releases — if your expected path is empty, search the archive by filename instead of assuming the path is wrong.
  • GrapheneOS specifics: no additional developer-mode dance is required beyond standard USB debugging — adb bugreport functions the same as on stock Android for this purpose on a non-rooted production build.
  • Firmware version drift: if automatic Buds firmware updates are still enabled (see the earlier discussion on checking/disabling them in the official app), a capture done today may not match one done next month. Always note the confirmed firmware version (PROTOCOL.md §0/§0.1) alongside any capture you take.
  • Two Buds, one identity: the case, left bud, and right bud may all appear as distinct addresses/roles in some traffic (notably GATT). Don't assume all frames come from a single logical peer — check bluetooth.addr per frame when in doubt.
  • btsnooz ACL truncation isn't limited to BLE/GATT — added 2026-08-26, CAP-012. CAP-017-FINDINGS.md §2 already documented severe (~15-byte) ACL truncation on a BLE/GATT session's log; CAP-012-FINDINGS.md §1 shows the identical phenomenon on a classic BR/EDR session (RFCOMM/SDP/HFP data, not GATT) — confirmed the same way, frame.cap_len capped at a small value regardless of frame.len for every HCI_ACL packet. This is the signature of §3 step 4's btsnooz.py-from-bugreport fallback path being used instead of step 3's raw, untruncated btsnoop_hci.log — check capinfos (Packet size limit: inferred: ...) right after extraction, before spending analysis time on a session, and prefer the step 3 raw path whenever it's present in the bugreport archive.

7. FAQ

Q: Do I need to do a full capture session every time I want to check one new command? No — you can pull a fresh bugreport (§3) any time after enabling logging (§2), covering just that one new isolated action. You don't need to redo pairing or previously-confirmed commands each time.

Q: The zip from adb bugreport doesn't contain a Bluetooth folder at all — what now? This has been reported when the HCI snoop toggle wasn't actually active during the session (see the "A Bluetooth restart is required" note in §6) — re-check §2 step 5, reboot the phone this time even if you used a plain toggle before, reproduce the action, and pull a fresh bugreport. If it still doesn't appear, search the whole archive by filename (find . -iname "*btsnoop*") rather than assuming a fixed path.

Q: Can I use Wireshark's live capture (the "Android Bluetooth Btsnoop" USB interface) instead of pulling a bugreport each time? In principle yes on some Android versions, but on modern Android the live-capture port is frequently not exposed by default, requiring workarounds (e.g. manually forwarding a local port via nc over adb shell) that add complexity without much benefit for this project's needs. The adb bugreport method in §3 is simpler and reliable across both phones — stick with it unless you have a specific reason to need live capture.

Q: Is it safe/expected that adb root fails on GrapheneOS? Yes — this is expected, deliberate behavior on any non-rooted production Android build, not a GrapheneOS-specific restriction, and not something to work around by rooting the device. Rooting significantly weakens the security model this whole project is built around (see AGENTS.md §2), so adb bugreport remains the correct approach even though it's slightly more roundabout than a direct file pull.

Q: My Pixel 7a capture shows encrypted-looking or unreadable payload bytes even inside the RFCOMM stream — is that expected? Most likely, not actual encryption — a few distinct things get conflated under "looks encrypted," worth separating:

  • HCI-boundary visibility: an HCI snoop log is captured at the boundary between the host (Android's Bluetooth stack) and the controller (the Bluetooth chip). On typical implementations, Bluetooth link-layer encryption/decryption happens in the controller, below this boundary — so ACL data crossing HCI toward the host is usually already plaintext at the L2CAP/RFCOMM level, not still link-encrypted.
  • What this means in practice: raw, unfamiliar-looking bytes inside an RFCOMM frame are, for this project, far more likely to just be serialized protobuf data you don't recognize yet (protobuf's binary encoding often looks fairly random to the eye without the schema) than genuine Bluetooth link-layer encryption. Don't assume "unreadable" means "encrypted, therefore unusable" — it's probably exactly the payload you're trying to reverse engineer.
  • When it might genuinely be encryption: if bytes are unreadable consistently, across many otherwise-isolated captures, and never resolve into anything matching the envelope hypothesis no matter how you slice the offsets, that's a better (though still not certain) signal of something below Wireshark's visibility — e.g. a higher-layer/application-level encryption scheme, rather than standard Bluetooth link encryption, which per the point above shouldn't normally still be present at this layer.
  • If this happens consistently, note it as an open question in PROTOCOL.md §6 — including which of the above you've ruled out — rather than assuming the envelope hypothesis in §2 is simply wrong.

Q: Should I capture on the GrapheneOS phone first, since that's my actual target platform? No — do the Pixel 7a (official app) capture first. It's the only source of frames you can positively attribute to a specific command, since you're the one triggering them through the known, official UI. The GrapheneOS capture is for validating connection/pairing/passive behavior on the target OS, not for discovering new commands.

Q: How do I know when I've captured "enough" and can stop? When every row in PROTOCOL.md §4.1's opcode table and every open question in §6 that's answerable via traffic analysis (as opposed to, say, firmware version lookup) has moved from 🔴/🟡 to 🟢 with a [VERIFIED-LOCAL] tag — or when you've made a deliberate, documented decision to leave a specific low-priority item (e.g. Find My Buds) unverified for now.


8. After Capturing: What to Update

Every capture session should end with at least one of these, recorded first in that session's CAP-NNN-FINDINGS.md and then promoted directly into PROTOCOL.md (PROTOCOL_NOTES.md has been retired — there is no intermediate buffer):

  • PROTOCOL.md §2 — envelope structure fields confirmed/corrected.
  • PROTOCOL.md §4.1 — opcode table rows filled in, confidence raised to 🟢.
  • PROTOCOL.md §4.3 — battery approach confirmed or ruled out (now split into Option A: BLE advertisement, and Option B: RFCOMM Message Stream — see the current version of that section).
  • PROTOCOL.md §0.1 — firmware version and any version-specific differences logged.
  • PROTOCOL.md §6 — open questions resolved where this capture resolves them.

Mandatory, not optional: every new 🔴 OPEN QUESTION recorded in a CAP-NNN-FINDINGS.md (or DESKRESEARCH_FINDINGS.md) MUST also be copied into PROTOCOL.md §6, in the matching Framing/Commands & schemas/Behavior subsection, even if it doesn't get resolved this session. An open question that stays only in the findings doc is effectively invisible to future sessions, since §6 is the consolidated list agents are expected to check (AGENTS.md §0.1) — it must not depend on someone remembering to go re-read every individual findings file. (Example of this rule being applied: the UI-baseline-vs-wire-baseline firmware distinction, first noted in §0.1's 2026-08-14 addendum but not copied into §6 until 2026-08-15 — don't repeat that gap for new open questions going forward.)

Treat an capture session that doesn't result in at least one of the above as incomplete — either the action wasn't actually isolated/identifiable, or something in the setup (§2, §6) needs revisiting before the next attempt.


9. Capture Index

Every capture session gets a row here, added at or immediately after extraction (§3) — this is the authoritative index that CAP-NNN-FINDINGS.md and PROTOCOL.md evidence entries reference back to, per PROJECT_RULES.md rule 3 (traceability) and rule 14 (capture metadata). A capture that never gets a row here is, for evidence purposes, effectively lost — don't skip this step, even for a quick one-action session.

ID format: CAP-NNN, zero-padded, strictly incrementing, never reused — if a capture turns out to be unusable, mark it discarded in Status rather than deleting the row or reassigning its number to a later capture. Register every new CAP-NNN in id_registry.csv (repo root) when adding its row here — scripts/lint_docs.py checks every reference against that registry, which is how the 2026-08-18 CAP-005/CAP-007/CAP-010 ID-reuse incident (see CHANGELOG.md) would have been caught mechanically instead of found by hand.

ID Date Phone Android Buds FW App version Group(s) Test(s) Purpose Bugreport file Extracted log Status
CAP-001 2026-08-09 Pixel 7a 17 release_5.203 1.0.955078536 Z, A, B, M PAIR-001, CASE-003, CASE-004, CASE-005, ANC-001, ANC-002, ANC-003, ANC-004, CASE-006 Pipeline validation; scope grew beyond Z into full pairing baseline + all 4 ANC modes + case/bud handling captures/CAP-001-2026-08-09_08-51-00_08-52-20-Group_Z/CAP-001-btsnoop_hci.log same file (already btsnooz-extracted) analyzed — see CAP-001-FINDINGS.md in that folder; ANC-opcode attribution inconclusive due to lack of action isolation
CAP-002 2026-08-09 Pixel 7a 17 release_5.203 1.0.955078536 A PAIR-001, CASE-001 Fresh pairing/bonding baseline (deleted stored link key first) through the Pixel Buds app's first-run setup flow (Fast Pair save-to-account, CDM permission, Device details load) captures/CAP-002-2026-08-09_17-04-53_17-06-46-Group_A/CAP-002-btsnoop_hci.log (sliced from a shared, non-restarted ~8h20m snoop log — see that folder's CAP-002-EVENT-NOTES.md process note) same file analyzed — see CAP-002-FINDINGS.md in that folder; Fast Pair Message Stream Device Information group tentatively identified (channel 2/DLCI 0x04); no RFCOMM traffic found during app setup/Device-details load; HFP channel opened but no AT-command traffic observed (contrast with CAP-001)
CAP-003 2026-08-10 Pixel 7a 17 release_5.203 nRF Connect (generic BLE tool), then official app v1.0.955078536 (took over partway) R PAIR-001 Forced GATT rediscovery attempt (pairing removed via system settings, connected via nRF Connect instead of the official app) to resolve CAP-002's open 0x0f2a/0x0c0X handle UUIDs; classic pairing captured as a bonus data point captures/CAP-003-2026-08-10_20-59-16_21-00-37-Group_R/CAP-003-btsnoop_hci.log (short, freshly-restarted log, no slicing needed) same file analyzed — see CAP-003-FINDINGS.md in that folder; primary goal not achieved — Android's GATT database cache survived the pairing removal, so zero Read By Group Type/characteristic-discovery traffic occurred; 0x0f2a/0x0c0X UUIDs still unresolved; new handle 0x0f28 found (polled every ~60s, value 0x31); reinforces that RFCOMM channel numbers are session-local while GATT handles are stable across sessions
CAP-004 2026-08-11 Pixel 7a 17 release_5.203 nRF Connect (BLE phase), then system Bluetooth settings (no Pixel Buds app at any point — uninstalled) S PAIR-001, CASE-003 GFPS-001 — Google Play Services disabled + Pixel Buds app uninstalled, to isolate whether CAP-002's Fast Pair Message Stream traffic is Buds-initiated or GMS-driven; bonus classic pairing captured (procedure deviated from Group S's system-settings-only description — nRF Connect was used first) captures/CAP-004-2026-08-11_06-22-36_06-25-12-Group_S/CAP-004-btsnoop_hci.log (contains unrelated background Fitbit Charge 6 traffic, excluded — see CAP-004-FINDINGS.md §1) same file analyzed — see CAP-004-FINDINGS.md in that folder; mixed GFPS-001 result: CAP-002 §3's channel-2/DLCI-0x04 TLV content (Model ID, "Revision 6", etc.) is absent (channel 2 never opens) — GMS-and/or-app-dependent, unresolved which (GMS was disabled and the official app was uninstalled together in this session, a confound not yet isolated — see CAP-004-FINDINGS.md §4a); but CAP-001's channel-4/DLCI-0x08 content (google-pixel-buds-pro-v1, Europe/Amsterdam) reappears unchanged — not GMS-dependent (this one isn't confounded, since it's a presence-despite-both-removed result, not an absence); classic bonding used Cross-Transport Key Derivation (LE Secure Connections → classic key), not classic SSP as in CAP-002/CAP-003; new, unidentified Message Stream Groups 0x04/0x05/0x09 found; nRF Connect's cached GATT service list gives named candidate services (Google Fast Pair Service 0xFE2C, Accessory Non-Owner Service, Device Information) for the still-open 0x0f2a/0x0c0X handle questions
CAP-005 2026-08-15 Pixel 7a 17 release_5.203 1.0.955078536 T EQP-002, EQS-004 Highest priority. EQ command isolation — single isolated EQ preset change ("Heavy bass"), then a second isolated EQ Bass-slider drag + Save, to find EQ's command channel now that it's known not to share ANC's (PROTOCOL.md §4.1/§2.3) captures/CAP-005-2026-08-15_15-02-31_15-03-45-Group_T/CAP-005-btsnoop_hci.log same file analyzed — see CAP-005-FINDINGS.md in that folder; goal achieved (🟡 HYPOTHESIS level): both actions isolate cleanly to DLCI 0x02 (libmaestro's Pigweed pw_hdlc channel, PROTOCOL.md §2.2a) only — DLCI 0x04/0x08 silent in both windows; a nested, protobuf-tag-consistent envelope decodes down to a 5×float32 band-gain quintet, with only the Bass field changing (+3.0 on preset, then −4.1 on the drag); the slider drag additionally revealed a second, later "Save"-tap wire burst (outer field 18) distinct from the live-drag burst (outer field 16), not anticipated by this capture's own event notes — first EQ-attributable content decoded on any channel, still 🟡 not 🟢 (single capture, field-to-band mapping inferred from one changed field)
CAP-006 2026-08-15 Pixel 7a 17 release_5.203 1.0.955078536 B (repeat) ANC-001, ANC-002, ANC-003, ANC-004 Clean, single-tap-per-window repeat of Group B to resolve CAP-001's first-two-taps-have-no-0x12-frame open sub-question and re-confirm the ANC bit-mapping before implementation captures/CAP-006-2026-08-15_17-23-49_17-25-06-Group_B/CAP-006-btsnoop_hci.log same file analyzed — see CAP-006-FINDINGS.md in that folder; goal achieved, ADR-009 blocker resolved: all 4 isolated single taps produced a matching 0x12 "Set ANC state" frame (exactly 4 0x12 frames in the whole 233s log, one per tap, zero extras/misses), each within ~1.3s of its video-observed tap; bit-mapping re-confirmed byte-for-byte against PROTOCOL.md §4.1 (0x08=ANC, 0x20=Off, 0x40=Adaptive, 0x80=Transparency); CAP-001's 2/6 miss rate does not reproduce under isolated single-tap conditions — FrameEncoder implementation for this command proposed as unblocked, pending maintainer sign-off/DECISIONS.md update
CAP-007 2026-08-16 Pixel 7a 17 (⚪ assumed, not visible on screen this capture) release_5.203 n/a (Android system Bluetooth "Device details" page, not the companion app) U INEAR-004(partial), CASE-004, OBS-003 DLCI 0x08 Group 0x04 Code 0x12 liveness/event bracket — bud-removed-from-case, case-closed-while-active, and multi-minute idle brackets, to test whether the alternating value is event-driven or a free-running counter (CAP-004-FINDINGS.md §5a Task 5) captures/CAP-007-2026-08-16_09-14-10_09-17-57-Group_U/CAP-007-btsnoop_hci.log same file analyzed — see CAP-007-FINDINGS.md in that folder; Group U's own question answered (🟢 FACT): Code 0x12 is Buds-initiated (Rcvd-only), fires on DLCI 0x08 channel-(re)open and also autonomously after a ~184s idle gap with no channel churn — neither purely reactive nor purely free-running; value cycles 0x02/0x03/0x04 (corrects CAP-004's "2↔3" characterization); original in-ear-mapping goal not achieved — no earbud was ever inserted into an ear in this session (only removed from the open case), and that one physical event produced no value change on any DLCI (clean negative result); case-lid-close bracket also produced zero wire traffic (clean negative result)
CAP-008 2026-08-26 Pixel 7a 17 (⚪ assumed) release_5.203 1.0.955078536 (⚪ assumed, not shown on screen this session) V CALL-001 First capture of an actual phone call — resolves whether HFP AT-command SLC setup reoccurs (CAP-002-FINDINGS.md §5) and whether channel 5/DLCI 0x0a carries any payload or SCO/eSCO HCI event (CAP-001-FINDINGS.md §6 Task 6) captures/CAP-008-2026-08-26_09-38-44_09-41-36-Group_V/CAP-008-btsnoop_hci.log same file analyzed — see CAP-008-FINDINGS.md in that folder; both open questions resolved: the full HFP SLC handshake reoccurs on a fresh classic-link connection (frames 776–1132, on RFCOMM channel 6/DLCI 0x0c this session — a third distinct channel-number assignment for the same profile, reinforcing CAP-001's "channel numbers are session-local" note); two clean Enhanced Setup Synchronous Connection/Synchronous Connection Complete (mSBC, eSCO) pairs appear, one per call — the first SCO/eSCO HCI events in this project, closing CAP-001-FINDINGS.md §6 Task 6. CALL-001 itself is now sub-second wire/video-correlated for both call start and end, across two independent calls. DLCI 0x0a stays silent through both calls (14th consecutive negative result) but is now specifically ruled out as the call's audio path. Bonus findings: AVDTP Suspend/Start (A2DP) brackets each call almost exactly; AT+BIEV HF Indicator #2 tracks the Left earbud this session (vs. Right in CAP-009) — refines the open "fixed-Right vs. HFP-primary" question; one isolated unsolicited +CIEV:6,... (battchg) push, a new 🔴 open question
CAP-009 2026-08-23 Pixel 7a (⚪ assumed) 17 (⚪ assumed) release_5.203 (⚪ assumed) n/a (system Bluetooth only, no companion app screen this session) X BATT-006 Battery-level discrepancy bracket — cross-check AT+CIND/battchg against AT+BIEV over a natural battery decline (CAP-001-FINDINGS.md §3); no phone call occurred, so not combined with CAP-008 captures/CAP-009-2026-08-23_18-33-52_20-12-55-Group_X/CAP-009-btsnoop_hci.log same file analyzed — see CAP-009-FINDINGS.md in that folder; BATT-006 resolved: AT+CIND?'s battchg is a single non-repeating snapshot at SLC setup (never a live reading); AT+BIEV=2 pushes at an irregular (not fixed ~6–7s) cadence and tracks the Right earbud's percentage specifically, not Left/Case/an aggregate, across 86 minutes; HFP/RFCOMM never reopens after the 20:02:27 case+USB reconnect (A2DP-only from then on). Bonus: 75-occurrence, 101-minute confirmation of the already-FACT PROTOCOL.md §4.3 Option E (DLCI 0x08 Group 0x0e Code 0x01, DECISIONS.md ADR-014) — first session to capture a live charge cycle (Left 93%→100%) and the case's empty→populated transition. Re-analyzed 2026-08-2x (independent video timeline, full wire re-run): all 7 maintainer-noted transitions confirmed (2 refined by minutes, periodic-manual-check lag not an error); corrected an assumed ~50s video-coverage gap to the actual ~2s; found a 🟡 HYPOTHESIS candidate for PROTOCOL.md §4.3 Option B's open battery code (DLCI 0x04 Group 0x03 Code 0x03) and a 🟡 HYPOTHESIS BLE Fast Pair scan explanation for the post-reconnect on-screen updates. All 5 proposals from this pass maintainer-approved (DECISIONS.md ADR-015)
CAP-010 2026-08-16 Pixel 7a 17 release_5.203 official Pixel Buds Companion App (version not visible on screen) W (attempted) PAIR-001 Intended as Group W's stronger GATT cache-busting attempt; actual on-screen procedure was a standard system-Settings forget-and-re-pair on the same Pixel 7a used throughout this project — neither pm clear com.android.bluetooth nor the Pixel 9a was used, so Group W's own method was not actually exercised (CAP-010-FINDINGS.md §1) captures/CAP-010-2026-08-16_11-42-31_11-45-01-Group_W/CAP-010-btsnoop_hci.log same file analyzed — see CAP-010-FINDINGS.md in that folder; GATT-001 still unresolved, 4th consecutive negative result (CAP-002, CAP-003, CAP-004, CAP-010): zero Read By Group Type/Find Information traffic despite a genuinely fresh classic bond this time — explained by the procedure gap above, not new evidence against Group W's untried methods; independently reproduces the 0x0f2a/0x0c0X handle cluster's stable numbering and FORM (now a 3rd–4th confirming session), the DLCI 0x08 private handshake incl. release_5.203 (5th confirming session), and the classic fresh-pairing state machine (4th confirming session); new byte-level detail for 0x0c0c (40B notify) and 0x0c13/0x0c14 (9/10/32B, doesn't fit the 0x0c04/0x0c05 AES-block pattern — 🟡 possibly a structurally distinct characteristic)
CAP-011 2026-08-21 Pixel 7a 17 (⚪ assumed) release_5.203 (⚪ assumed) official Pixel Buds Companion App (open for most of session) Q (#18) BATT-002, BATT-003 Passive BLE scan intended for case-closed/idle, to capture the Fast Pair Battery Notification advertisement independently of RFCOMM (PROTOCOL.md §4.3 Option A) captures/CAP-011-2026-08-21_09-45-17_09-55-16-Group_Q/CAP-011-btsnoop_hci.log same file analyzed — see CAP-011-FINDINGS.md in that folder; procedure deviation: an active classic RFCOMM+GATT connection was present throughout (app left open on "Device details"), not the intended connection-free scan; 0xFE2C Fast Pair Service BLE advertisements confirmed present (🟢 FACT, 634 frames, 5 rotating addresses) but the sampled payloads do not structurally match PROTOCOL.md §4.3 Option A's documented Battery Notification byte layout — recorded as inconclusive, not force-fit; a clean connection-free repeat is still needed. Re-analyzed 2026-08-23 (maintainer spotted a 1% battery drop in the recording): pinpointed the exact change to 09:52:25.8 and found a DLCI 0x08 message (Group 0x0e Code 0x01, cross-confirmed by Group 0x04 Code 0x03) tracking the Left/Right values across 4 occurrences in the log — proposed as PROTOCOL.md §4.3 Option E, 🟡 HYPOTHESIS pending sign-off; see CAP-011-FINDINGS.md §7
CAP-012 2026-08-26 Pixel 7a 17 (⚪ assumed, not visible on screen this capture) release_5.203 (⚪ assumed — not re-confirmed on the wire this session, log severely ACL-truncated) uninstalled (GMS disabled) S (repeat) GFPS-001, PAIR-001, CASE-003, bonus PAIR-003 (first Pixel 7a occurrence) Repeat of Group S following its original system-settings-only procedure exactly (no nRF Connect at any point, independently confirmed both on screen and on the wire — zero BLE connection to the Buds anywhere in the log), to isolate whether CAP-004's Cross-Transport-Key-Derivation bonding result was an artifact of nRF Connect's early BLE connection (CAP-004-FINDINGS.md §8 item 4) captures/CAP-012-2026-08-26_15-30-19_15-32-28-Group_S/CAP-012-btsnooz_hci.log (severely ACL-truncated, ~15-byte captured length per data packet — btsnooz-fallback extraction rather than a raw untruncated log; see CAP-012-FINDINGS.md §1) same file analyzed — see CAP-012-FINDINGS.md in that folder; hypothesis confirmed (🟢 FACT): this session uses classic Secure Simple Pairing, not CTKDCAP-004's CTKD result is isolated to nRF Connect's early BLE connection, not to the GMS-disabled/no-app condition itself (§2/§10 there); GFPS-001's channel-topology result reproduces CAP-004's (DLCI 0x04 never opens) but the payload-content sub-question is inconclusive, not resolved, due to this log's truncation (§1/§4); bonus finding: a manual disconnect/reconnect (PAIR-003) retriggers the full HFP AT-command handshake (§6), narrowing a PROTOCOL.md §6 open question
CAP-013 2026-08-26 Pixel 7a 17 (⚪ assumed, not screen-confirmed) release_5.203 official Pixel Buds Companion App (version not visible on screen) A (repeat) PAIR-001, PAIR-004, incidental BATT-004, APP-001, APP-002 Intended: HCI snoop logging started before any prior association with the device exists — resolves whether a clearing action fully clears prior bonding/BLE-association state (CAP-001-FINDINGS.md §6). PROPOSAL — pending maintainer approval: status/row text below proposed, not yet maintainer-approved. captures/CAP-013-2026-08-26_17-09-01_17-14-04-Group_A/CAP-013-btsnooz_hci.log (severely ACL-truncated, ~15-byte captured length per data packet — btsnooz-fallback extraction, same issue as CAP-012; see CAP-013-FINDINGS.md §1) same file analyzed — partial: pre-clearing-action window not captured — see CAP-013-FINDINGS.md/CAP-013-EVENT-NOTES.md in that folder. Verified (not assumed) that the intended method wasn't actually followed: the on-screen clearing action was "Reset Bluetooth & Wi-Fi" (not a single-device "Forget"), and the log's first frame (17:11:45.8) starts 2m21s after that action — also after Bluetooth re-enable, case-open, pair-button-press, and device-list-tap, none of which are logged. Primary question (CAP-001-FINDINGS.md §6) remains 🔴 OPEN, not answered. Secondary question (PAIR-004) is answered: 🟢 CONFIRMED fresh classic SSP handshake (full IO-Capability/User-Confirmation/new-Link-Key-Notification sequence, frames 117–270) for the bonding state active when this capture's window began — no key-reuse path observed. Bonus: DLCI 0x02 (libmaestro candidate) opens ~61s after the other 4 channels this session, coinciding with an app-permission "Allow" tap — 🟡 single-sample, not promoted; a second BLE link to an unrelated random address appears mid-session, unattributed (🔴 OPEN). A genuine repeat (logging before the clearing action itself) is still needed — see proposed follow-up capture ID below.
CAP-014 2026-08-27 Pixel 7a 17 (⚪ assumed, not re-confirmed on screen this session) release_5.203 (confirmed on-screen 20:59:07) nRF Connect for Mobile (Nordic Semiconductor); official Pixel Buds app also surfaced twice, 20:54:39 and 20:58:52–53, unintentionally W (repeat — PROPOSAL, pending maintainer approval: neither of Group W's own candidate methods was actually used this session either, see deviation note below) GATT-001, incidental PAIR-001 Intended: repeat the CAP-017 nRF-Connect procedure with (1) a fixed/longer HCI snoop snaplen and (2) an on-screen drill-down into "Accessory Non-Owner Service"/"Unknown Service" — (2)'s Accessory-Non-Owner half was correctly not executed, that instruction conflicted with DECISIONS.md ADR-008 (CAP-014-FINDINGS.md §2) captures/CAP-014-2026-08-27_20-53-37_20-59-17-Group_W/CAP-014-btsnoop_hci.log same file analyzed — see CAP-014-FINDINGS.md in that folder; PROPOSAL, pending maintainer approval: (1) succeeded — snaplen confirmed fixed, 0 truncated frames out of 4,663 (§0); but GATT-001's handle↔UUID mapping is still 🔴 OPEN — root cause now identified precisely (§4): this session reused the same, already-bonded Pixel 7a with a cached GATT client, so Android served the 0x0c0X/0x0f2X cluster from its cached database instead of re-declaring it on the wire (only the GATT service itself, handles 0x00010x0009, was genuinely re-discovered live). Neither Group W Option (a) pm clear com.android.bluetooth nor Option (b) (Pixel 9a) was actually tried — both remain the clearly-identified next step, now combined with a confirmed-working snaplen for the first time. Reproduces 0x0f2a="Revision 6" (3rd session, byte-identical to CAP-002) and 0x0f32=0x64/0x0f33 CCCD (2nd session, byte-identical to CAP-017, narrowing that open question); bonding used Cross-Transport Key Derivation (2nd confirmed instance after CAP-004, consistent with CAP-012's "BLE-tool-first" explanation)
CAP-015 2026-08-18 Pixel 7a 17 release_5.203 1.0.955078536 T EQP-002, EQS-004 Completes/supersedes CAP-005's Group T goal: five distinct EQ presets tapped in sequence, then all five EQ sliders each dragged to both extremes and back near-zero (3 passes), to resolve CAP-005's open field-to-band mapping question captures/CAP-015-2026-08-18_06-11-06_06-17-40-Group_T/CAP-015-btsnoop_hci.log same file analyzed — see CAP-015-FINDINGS.md in that folder; field-to-band mapping promoted to 🟢 FACT (field 1↔Low bass, 2↔Bass, 3↔Mid, 4↔Treble, 5↔Upper treble, wire order reversed from on-screen order) — matches CAP-005's single-band inference exactly; also established the ±6.0 band-gain clamp (🟢 FACT, units unconfirmed) and a confirmed preset-quintet reference table
CAP-016 2026-08-18 Pixel 7a 17 (build CP2A.260705.006, confirmed on-screen) release_5.203 n/a (Android system Bluetooth "Device details" page, not the companion app) U CASE-004, CASE-005, CASE-006, OBS-003 Re-run of Group U (distinct session from CAP-007): Bluetooth-on, both buds removed from case one at a time, empty-case lid close/reopen, both buds re-docked, disconnect, lid close — general case/bud-removal correlation rather than Group U's own narrower liveness-bracket procedure (see CAP-016-FINDINGS.md scope note) captures/CAP-016-2026-08-18_06-31-31_06-33-58-Group_U/CAP-016-btsnoop_hci.log same file analyzed — see CAP-016-FINDINGS.md in that folder; clean single-attempt, Buds-initiated reconnect on first bud removal (🟢 FACT, contrast CAP-001's 3-attempt phone-initiated connect); ACL disconnects the instant both buds are re-docked, Buds-initiated (reason 0x13), 🟢 FACT; case-lid open/close while buds are elsewhere produces zero wire signal, 🟢 FACT — 2nd independent capture confirming this (CAP-007 §3.4); a full RFCOMM channel bounce (all 4 DLCIs) recurs with no camera-visible trigger this time, ruling out "bud removal is the sole cause" for that class of event (CAP-007 §3.3's hypothesis); ANC-state Notify's "settable-toggles" byte (0x00 vs 0xe8) newly observed to change value, tracking the app's own "no ANC mode selectable" UI state — proposed refinement to PROTOCOL.md's ANC Notify field table, not yet promoted; INEAR-002/INEAR-003/INEAR-004 still not exercised — no earbud is shown inserted into or removed from an ear on camera in this session either
CAP-017 2026-08-16 Pixel 7a 17 release_5.203 (⚪ assumed — not re-confirmed on-the-wire this session, DLCI 0x08 never opened) nRF Connect for Mobile (Nordic Semiconductor), not the official app W GATT-001 Second same-day Group W attempt (distinct session from CAP-010's 11:42 attempt), this time driving a brand-new third-party GATT client (nRF Connect) instead of the official app, to force a real cache-miss discovery walk captures/CAP-017-2026-08-16_18-30-12_18-37-12-Group_W/CAP-017-btsnoop_hci.log same file analyzed — see CAP-017-FINDINGS.md in that folder; GATT-001's discovery goal achieved for the first time in this project (🟢 FACT) via a third path not previously identified (fresh GATT client UID has no cache to hit) — 137 live Read By Type/Read By Group Type/Find Information frames on the wire (vs. zero in every prior attempt); full 15-service GATT profile recovered from video (incl. Google Fast Pair Service 0xFE2C and two previously-undocumented 128-bit UUIDs); but the wire log itself is severely ACL-truncated (~15B snaplen), so UUID bytes aren't recoverable from the log, and no characteristic-level drill-down happened on screen — confirmed by a dedicated full-video verification pass (§4b) that the handle↔UUID mapping cannot be closed from this capture's artifacts; recapture needed with (1) fixed snaplen and (2) on-screen characteristic drill-down into "Unknown Service" (109b862f-…) — "Accessory Non-Owner Service" is out of scope for any drill-down/read, per DECISIONS.md ADR-008; do not include it in a future session's plan (correction added post-CAP-014, which caught this exact instruction still standing here and had to decline it — see CAP-014-FINDINGS.md §2). CAP-014's repeat (below) fixed the snaplen but did not achieve a genuine cache-miss discovery, so the mapping is still open — see that row
CAP-018 2026-09-12 Pixel 7a 17 release_5.203 (⚪ assumed, carried over) 1.0.955078536 Y GATT-002 BLE-only connection isolation for the 0x0044 notification burst (CAP-016-FINDINGS.md §11) — enable Bluetooth and let the BLE link to the already-paired Buds form without touching the buds/case at all, to isolate whether the burst is triggered by BLE link establishment alone, independent of any bud/case action captures/CAP-018-2026-09-12_06-07-20_06-09-52-Group_Y/CAP-018-btsnoop_hci.log same file analyzed — see CAP-018-FINDINGS.md in that folder; isolation confirmed (buds/case untouched for the full 152s video), but the burst itself is not attributable to the Buds this session: the only LE connection in the whole log (chandle 0x0004, address distinct from the Buds' classic address) carries a GATT profile matching an unrelated nearby device (a standard Heart Rate service present, no Fast Pair Service/no Buds-known handle cluster) — the burst rides handle 0x0044, inside that connection's own 0x00400x0045 "Unknown Service" range, structurally the same unattributed-background-device pattern already seen in CAP-032-FINDINGS.md §5. The Buds' own classic connection carries zero ATT traffic throughout. Bonus: this session's classic reconnect took an unusually long ~64s from Bluetooth-toggle to Create Connection, unexplained
CAP-019 2026-08-21 Pixel 7a 17 (⚪ assumed) release_5.203 (⚪ assumed) 1.0.955078536 (assumed same as prior sessions) C CONV-001, MULTI-001 Conversation Detection and Multipoint on/off toggles captures/CAP-019-2026-08-21_07-35-50_07-39-30-Group_C/CAP-019-btsnoop_hci.log same file analyzed — see CAP-019-FINDINGS.md in that folder; both toggles isolate to a single DLCI 0x02 Sent frame each (🟡 HYPOTHESIS, field5{field4{field22=1}}/field5{field4{field11=1}}); Multipoint additionally triggers a DLCI 0x04 Group 0x07 (SASS) burst including an ASCII "in-use" string — first content-level correlation of SASS traffic to a specific action
CAP-020 2026-08-21 Pixel 7a 17 (⚪ assumed) release_5.203 (⚪ assumed) 1.0.955078536 (assumed) F TOUCH-001, HEAD-001 Touch controls and Head gestures top-level on/off toggles; leaves Head gestures ON for CAP-028 (Group O) captures/CAP-020-2026-08-21_07-46-14_07-47-49-Group_F/CAP-020-btsnoop_hci.log same file analyzed — see CAP-020-FINDINGS.md in that folder; both toggles isolate to a single DLCI 0x02 Sent frame each (🟡 HYPOTHESIS, field5{field4{field4=1}}/field5{field4{field29=2}}) — first identification of the general-purpose field5{field4{...}} settings-write envelope, reused by every other capture in this batch
CAP-021 2026-08-21 Pixel 7a 17 (⚪ assumed) release_5.203 (⚪ assumed) 1.0.955078536 (assumed) G HOLD-001HOLD-005 Per-earbud press-and-hold configuration and ANC-mode rotation list captures/CAP-021-2026-08-21_07-59-36_08-07-04-Group_G/CAP-021-btsnoop_hci.log same file analyzed — see CAP-021-FINDINGS.md in that folder; all 4 Left/Right × ANC/Assistant combinations (HOLD-001HOLD-004) isolate cleanly (🟡 HYPOTHESIS, field5{field4{field7{field1|2{field4=5|6}}}}, field1=Left/field2=Right, 5=ANC/6=Digital assistant); HOLD-005's 16-frame checklist burst confirms field order matches on-screen order but cannot be split between Left's/Right's lists from wire content alone
CAP-022 2026-08-21 Pixel 7a 17 (⚪ assumed) release_5.203 (⚪ assumed) 1.0.955078536 (assumed) H AUDIO-001AUDIO-003 Mono audio, Volume EQ, Volume balance captures/CAP-022-2026-08-21_08-15-24_08-17-27-Group_H/CAP-022-btsnoop_hci.log same file analyzed — see CAP-022-FINDINGS.md in that folder; all 3 isolate to DLCI 0x02 writes (🟡 HYPOTHESIS: field19=Mono audio, field15=Volume EQ, field17=Volume balance, 7 samples from one continuous drag); Volume balance's persistent-write claim not tested (no disconnect/reconnect this session)
CAP-023 2026-08-21 Pixel 7a 17 (⚪ assumed) release_5.203 1.0.955078536 (assumed) I FW-001, FW-002 (FW-003/FW-004 not exercised — gap) Firmware/device-info screen; resolves PROTOCOL.md §0.1's open wire-baseline-vs-UI-baseline firmware-version question captures/CAP-023-2026-08-21_08-23-40_08-25-21-Group_I/CAP-023-btsnoop_hci.log same file analyzed — see CAP-023-FINDINGS.md in that folder; primary goal achieved: on-screen firmware version (release_5.203, all 3 components) matches DLCI 0x08's private-envelope string byte-for-byte, same session — resolves which on-the-wire string the app calls "the firmware version"; FW-001's manual check produces zero wire traffic (🟢 FACT, cached not live-queried); FW-003/FW-004 not visited this session
CAP-024 2026-08-21 Pixel 7a 17 (⚪ assumed) release_5.203 (confirmed same day, CAP-023) 1.0.955078536 (assumed) J INEAR-001, CASE-001, CASE-002 In-ear detection toggle and case-sound settings captures/CAP-024-2026-08-21_08-31-27_08-33-24-Group_J/CAP-024-btsnoop_hci.log same file analyzed — see CAP-024-FINDINGS.md in that folder; all 3 isolate to DLCI 0x02 writes (🟡 HYPOTHESIS: field2=In-ear detection, field28="Bud return"/CASE-001, field27="Other alerts"/CASE-002); no case-specific vs. bud-specific channel distinction found — same shared envelope
CAP-025 2026-08-21 Pixel 7a 17 (⚪ assumed) release_5.203 (⚪ assumed) 1.0.955078536 (assumed) K FIND-001, FIND-002 (FIND-003/FIND-004 attempted, resolve to a different mechanism) Find My Buds/Ring, the last remaining fully unattributed app command; PROTOCOL.md §4.4's hypothesis tested captures/CAP-025-2026-08-21_08-40-52_08-45-26-Group_K/CAP-025-btsnoop_hci.log same file analyzed — see CAP-025-FINDINGS.md in that folder; FIND-001/FIND-002 strongly confirmed, video-correlated: DLCI 0x04 Group 0x04 Code 0x01, Value=0x01(start Right)/0x02(start Left)/0x00(stop) — matches the spec's worked ACK example exactly; major structural finding: FIND-003(Case)/FIND-004(both) route through a separate Find Hub/Find-My-Device-Network UI flow that produces no local Ring command on the wire — a different, likely GMS/account-mediated mechanism, not a capture gap
CAP-026 2026-09-12 Pixel 7a 17 release_5.203 (⚪ assumed) 1.0.955078536 L BATT-001, OBS-001 Passive observation windows (idle-wait-for-reconnect-notification, force-close/reopen) captures/CAP-026-2026-09-12_06-49-53_06-52-49-Group_L/CAP-026-btsnoop_hci.log same file analyzed — see CAP-026-FINDINGS.md; BATT-001 wire-correlated for the first time: connect-time battery data (DLCI 0x04 Option B candidate + DLCI 0x08 Option E) lands ~12–13s before the on-screen reconnect notification appears — Left/Right match on-screen exactly, Case reads 95 vs. on-screen 93% (a further instance of ADR-014's short/no-flag-form-may-be-stale pattern). OBS-001 clean negative: app force-close+reopen (classic connection never drops) produces zero DLCI 0x02/0x04/0x08 traffic
CAP-027 2026-08-30 Pixel 7a (⚪ assumed, not screen-confirmed) 17 (⚪ assumed) release_5.203 (🟢 confirmed on-wire) official Pixel Buds Companion App (not screen-confirmed; Spotify used as the music source) N TOUCH-002TOUCH-007 Physical touch gestures on the bud hardware; TOUCH-007's behavior depends on the per-earbud mode set in CAP-021 (Group G) captures/CAP-027-2026-08-30_15-45-14_15-49-07-Group_N/CAP-027-btsnoop_hci.log same file analyzed — see CAP-027-FINDINGS.md in that folder; structural finding: TOUCH-002TOUCH-006 ride AVRCP (Pass Through/VolumeChanged), not any RFCOMM DLCI at all — all 13 instances 1:1-correlated to a wire event; TOUCH-007 (press-and-hold) is the exception, confirmed on DLCI 0x04 (official Fast Pair Message Stream, already-FACT "Notify ANC state" 0x13 shape from PROTOCOL.md §4.1), not DLCI 0x02 (libmaestro). Both TOUCH-007 timestamps (previously TBD) recovered this way. Left-bud TOUCH-007 shows two rotation-step Notify frames 5.68s apart, unresolved whether that's one long hold or two presses (🔴 open)
CAP-028 2026-09-12 Pixel 7a 17 release_5.203 (⚪ assumed) 1.0.955078536 O HEAD-002, HEAD-003 Physical head gestures; Head gestures already enabled from CAP-020 (Group F) captures/CAP-028-2026-09-12_07-16-15_07-17-18-Group_O/CAP-028-btsnoop_hci.log same file analyzed — see CAP-028-FINDINGS.md; camera points at the phone screen, not the user's head, so the gestures themselves are not video-visible. Clean negative on the Buds' own connection: zero DLCI 0x02/0x04/0x08 traffic during the entire claimed gesture window, no AVRCP traffic, no SCO/eSCO call anywhere in the log — consistent with (not distinguishable from) Nod/Shake being functionally inert with no active call/notification. Bonus, third occurrence of a pattern: a second, unrelated LE connection (standard Heart Rate service, no Fast Pair Service) coincidentally overlaps the gesture window with substantial ATT traffic on handle 0x0044/0x0042 — same signature as CAP-018, not Buds-attributable
CAP-029 2026-09-12 Pixel 7a 17 release_5.203 (⚪ assumed) 1.0.955078536 P CONV-002, CASE-007, PAIR-002 (CASE-008 not exercised) Conversation Detection voice trigger, factory reset (CASE-007, run this session) and its re-pair (PAIR-002) captures/CAP-029-2026-09-12_07-53-53_07-58-05-Group_P/CAP-029-btsnoop_hci.log same file analyzed — see CAP-029-FINDINGS.md; CONV-002 clean negative: media visibly pauses on screen but zero DLCI 0x02/0x04/0x08 traffic accompanies it, no ANC-mode change anywhere pre-reset. CASE-007/PAIR-002 confirmed: reproduces the established factory-reset pattern and a genuine fresh-SSP re-pair. CASE-008 not attempted. Major correction to the draft: the draft's claimed final "forget the device again" did not happen — video and wire both confirm the device stays connected through the end of the session
CAP-030 planned either phone TBD TBD TBD Q (items #19–20) LOUD-001, ADAPT-002 Group Q's two remaining items (item #18 already planned separately as CAP-011) — attempt to observe Loud Noise Protection and Adaptive Audio engaging; requires firmware ≥4.467 planned
CAP-031 2026-08-27 Pixel 7a 17 (⚪ assumed, not screen-confirmed) release_5.203 official Pixel Buds Companion App (version not visible on screen) A (repeat, 3rd attempt) PAIR-001, PAIR-004, incidental BATT-004 Third attempt at CAP-001-FINDINGS.md §6's original goal — HCI snoop logging started before any prior association with the device exists, this time with a live in-recording file-size-polling check added specifically to avoid CAP-013's failure mode. PROPOSAL — pending maintainer approval: status/row text below proposed, not yet maintainer-approved. captures/CAP-031-2026-08-27_06-04-48_06-08-10-Group_A/CAP-031-btsnooz_hci.log (btsnooz-format, inferred 15–126-byte captured length per packet, same truncation issue as CAP-012/CAP-013; see CAP-031-FINDINGS.md §1) same file analyzed — partial: pre-clearing-action window not captured, a second consecutive failure of this method — see CAP-031-FINDINGS.md/CAP-031-EVENT-NOTES.md in that folder. This session used a genuine narrow, per-device "Forget" (screenshot-confirmed, unlike CAP-013's broader reset) and a live snoop-log file-size-polling check during recording, but the log's first frame (06:06:37.16) still starts 66s after the on-screen Forget tap (06:05:31) — also after case-open, pair-button-press, and the entire first "Pair new device" scan attempt, none of which are logged. Primary question (CAP-001-FINDINGS.md §6) remains 🔴 OPEN, not answered, untested a third time. Secondary question (PAIR-004) is reconfirmed: 🟢 CONFIRMED fresh classic SSP handshake (frames 598–689, a sixth confirming instance) — no key-reuse path observed. Bonus, both negative results: CAP-013's DLCI 0x02 ~61s-delay does not reproduce (opens 1.64s after DLCI 0x00, within the initial burst) and CAP-013's unattributed second BLE link does not reproduce (exactly one LE link this session, to the Buds' own public address) — both now look like single-session artifacts. A fourth attempt is still needed, this time verifying snoop-log content freshness (not just file size) before the Forget tap.
CAP-032 2026-08-27 Pixel 7a 17 (⚪ assumed, not screen-confirmed) release_5.203 official Pixel Buds Companion App (version not visible on screen) A (repeat, 4th attempt) PAIR-001, PAIR-004, incidental BATT-004 Fourth attempt at CAP-001-FINDINGS.md §6's original goal — this time extracted via the raw BTSnoop file path (CAPTURE_BLUETOOTH_HCI_SNOOP.md §3 step 3) rather than the btsnooz.py fallback used for CAP-012/CAP-013/CAP-031. PROPOSAL — pending maintainer approval: status/row text below proposed, not yet maintainer-approved. captures/CAP-032-2026-08-27_18-30-15_18-32-33-Group_A/CAP-032-btsnoop_hci.log (genuine raw, untruncated BTSnoop — frame.cap_len == frame.len for all 2,455 frames, no capinfos-inferred size cap; see CAP-032-FINDINGS.md §0.1) same file analyzed — success: pre-clearing-action window captured for the first time in four attempts — see CAP-032-FINDINGS.md/CAP-032-EVENT-NOTES.md in that folder. The log's first frame (18:29:45.72) starts ~58s before the on-screen Forget tap (18:30:42) and ~30s before the video itself begins. Primary question (CAP-001-FINDINGS.md §6) is now answered for this session: 🟢 no prior BLE link or valid classic link key existed for the Buds anywhere in the covered pre-Forget window (CAP-032-FINDINGS.md §0.3) — this does not reproduce CAP-001's original finding (a clean counter-example, not a contradiction; CAP-001's own session-specific puzzle remains independently open). Secondary question (PAIR-004) reconfirmed: 🟢 CONFIRMED fresh classic SSP handshake (frames 1090–1153, a seventh confirming instance). Bonus: the untruncated log fully decodes DLCI 0x08's battery push ([100,1,1]/[100,1,2]/[57,1,3] = Left/Right/Case, matching the on-screen reading exactly) and firmware/capability-identifier fields; a previously-undocumented vendor-specific HCI command (0xFD57/0x0157, frame 91) embeds the Buds' address 105ms into the log, structurally consistent with bulk bonded-device provisioning at BT-enable time, not a connection — recorded 🔴 OPEN QUESTION, not bearing on the primary question. The btsnooz-vs-raw extraction-path hypothesis (CAP-013-FINDINGS.md §1, CAP-031-FINDINGS.md §1) is supported by this one data point (CAP-032-FINDINGS.md §0.1/§7 Test C), not yet independently isolated.
CAP-033 2026-08-30 Pixel 7a TBD release_5.203 (🟢 confirmed on-wire) n/a — app force-stopped throughout the entire recorded session AA SDP-001, SDP-002(not attempted — no update pending) SDP UUID branch isolation for gbm.a()'s "default internal rfcomm socket" path — system-settings-only pairing (app force-stopped) to check whether the pre-app-fetch SDP UUID set ever differs from every existing capture's "pigweed"-only result (REVERSE_ENGINEERING.md's gbm/fzd entries, DECISIONS.md ADR-018) captures/CAP-033-2026-08-30_15-17-03_15-19-52-Group_AA/CAP-033-btsnoop_hci.log same file analyzed, isolation not fully clean — see CAP-033-FINDINGS.md §1 ("Forget" preceded Force-stop by ~10s, reverse of procedure, though scoped away from the actual SDP-browse window; Step 3's app-open baseline was never executed at all, on- or off-camera). SDP-001 result capped at 🟡 HYPOTHESIS: "default" UUID still zero occurrences (raw byte scan, both byte orders), "pigweed" UUID confirmed present and named "MAESTRO APP" on-the-wire (SDP service-name string, frame 1279) — first wire-level (not just APK-code) corroboration of DECISIONS.md ADR-018's channel identity. Bonus structural finding: the same SDP response names DLCI 0x08 as "GSND CONTROL" and DLCI 0x0a as "GSND AUDIO" — new, previously-undocumented leads for PROTOCOL.md §2.3's open DLCI-0x08 identity question and CAP-021-FINDINGS.md §4a's unattributed DLCI-0x0a burst, proposed pending maintainer review, not promoted
CAP-034 2026-09-01 Pixel 9a (GrapheneOS) — never before connected to this Buds unit TBD ⚪ assumed release_5.203 (not re-confirmed — no official app used, DLCI 0x08 never opens) nRF Connect for Mobile (Nordic Semiconductor); official Pixel Buds Companion App not installed W (4th attempt) GATT-001, incidental PAIR-001/PAIR-003/BATT-003 4th Group W attempt at the 0x0c0X/0x0f2X GATT handle↔UUID mapping — combines CAP-014's confirmed-unlimited snaplen fix with Group W's own untried cache-busting method (pm clear com.android.bluetooth on a phone never before connected to this Buds unit), the first session to have both at once captures/CAP-034-2026-09-01_06-46-31_06-52-45-Group_W/CAP-034-btsnoop_hci.log same file analyzed, ✅ RESOLVED — maintainer sign-off obtained per AGENTS.md §6 — see CAP-034-FINDINGS.md in that folder. Confirmed 0/3,717 truncated frames (max 684B). The sole LE connection (chandle 0x0040) yields one genuine, full 0x00010xffff discovery walk (06:47:42.147–45.490) before bonding — the "reconnect" at 06:51:22 is a Database Hash cache-hit only, no second discovery pass. Resolves the full 15-primary-service GATT profile: 0x0c000x0c14 = Google Fast Pair Service (0xFE2C) with all 5 spec-defined characteristics (Model ID, Key-based Pairing, Passkey, Account Key, Additional Data) plus Message Stream PSM and one still-unnamed FE2C1238… characteristic; 0x0f200x0f2a = Device Information (0x0f28=Serial Number String, 0x0f2a=Firmware Revision String); 0x0f300x0f33 = Battery Service (0x0f32=Battery Level). Independently corroborated by nRF Connect's own on-screen UUID rendering and live-verified against the official Fast Pair spec. Corrects CAP-017-FINDINGS.md §6's hypothesis that "Unknown Service" (109b862f-…) might be the 0x0c0X cluster's container — it does not; it occupies a separate range (0x0f370x0f3e), own purpose still unidentified. ADR-008 compliance confirmed (Accessory Non-Owner Service appears only in unavoidable discovery inventory, never read/written). See PROTOCOL.md §6 and §4.3 Option D for the promoted findings
CAP-035 2026-09-02 Pixel 9a (GrapheneOS) TBD ⚪ assumed release_5.203 (not re-confirmed — no official app used, DLCI 0x08's firmware string was never independently re-checked) none — no Pixel Buds app, no nRF Connect, system Bluetooth settings only AB GSND-001, incidental PAIR-001/PAIR-003/BATT-003 GMS-independence check for DLCI 0x08 ("GSND CONTROL")/0x0a ("GSND AUDIO")/0x06 ("DEBUG APP")/0x12 ("BTIS"), per CAP-033-FINDINGS.md §3's negative APK-search result (see CAPTURE_BLUETOOTH_HCI_SNOOP.md Group AB) captures/CAP-035-2026-09-02_06-50-53_06-57-24-Group_AB/CAP-035-btsnoop_hci.log same file analyzed, maintainer sign-off obtained per AGENTS.md §6 — see CAP-035-FINDINGS.md in that folder. Confirmed 0/1,945 truncated frames. Two videos turned out to be sequential with only a ~7s recording-stop/restart gap (not the ~5min originally logged) — resolved directly from frame comparison, correcting the event timeline including a ~91–97s error in the original disconnect/reconnect time estimates. GMS precondition: com.google.android.gms present but dumpsys-verified disabled (enabled=3 = COMPONENT_ENABLED_STATE_DISABLED_USER) — not genuinely absent, so this is a rigorously verified second data point for CAP-004-FINDINGS.md §4a's existing "GMS present but disabled" finding, not a stronger novel "GMS-absent" confirmation. Result: DLCI 0x08 content reproduces byte-identical twice (connect + reconnect); DLCI 0x0a opens in lockstep both times but carries zero payload; DLCI 0x06/0x12 never open at all — clean negatives for both, first time either has been specifically checked. ADR-008 compliant. Still open: a repeat with GMS genuinely uninstalled, for full closure of the OS-stack-vs-GMS question
CAP-036 2026-09-04 Pixel 7a 14 ⚪ assumed release_5.203 (carried over, not re-checked on-screen this session) 1.0.955078535 AC OBS-004, incidental PAIR-003 Settings-state read-back isolation — official app + GMS enabled (normal baseline, the opposite of Group S/AB), idle-only observation windows (reconnect, EQ screen, Controls-and-gestures/Touch-controls screens, Multipoint screen) with no setting touched at any point, to isolate whether the official app ever issues a state query (as opposed to the write-direction-only evidence in PROTOCOL.md §4.2/§4.5) before any user-initiated change — the requirement ARCHITECTURE.md §3.1 imposes on this project's own app. A clean negative is a real result here, not a failed session captures/CAP-036-2026-09-04_06-35-58_06-41-18-Group_AC/CAP-036-btsnoop_hci.log (raw path, confirmed untruncated: 0/2,492 mismatched frames, no snaplen cap) same file analyzed — see CAP-036-FINDINGS.md in that folder. Split outcome, by trigger: on reconnection, DLCI 0x04's spec-documented but never-before-observed "Get ANC state" (0x11) query fires once, 34ms after the channel opens, answered ~10.7ms later by "Notify ANC state" (0x13, current=Off, matching the on-screen state) — 🟡 HYPOTHESIS pending replication, proposed (not self-promoted) for PROTOCOL.md §4.1. On settings-screen-open (EQ, Controls-and-gestures, Touch controls, More settings, Multipoint — five clean, video-verified idle windows), zero query or write traffic occurs on DLCI 0x02/0x04/0x08 — a citable negative result. A frame-by-frame video re-pass corrected several of this session's own placeholder timestamps by 5–30 seconds against what the video actually shows (see CAP-036-FINDINGS.md §2). Three new 🔴 open questions raised (DLCI 0x08 Get-shaped connect-time frames with unmapped Group/Code pairs; an undecoded DLCI 0x02 connection-settling RPC burst; a Settable toggles byte discrepancy between this session's Notify frame and previously-documented Set frames)
CAP-037 2026-09-06 Pixel 7a 14 (⚪ assumed, carried from CAP-036) ⚪ assumed release_5.203 1.0.955078536 (⚪ assumed) AD OBS-004, incidental PAIR-003 Purpose-built repeat of the "Get ANC state" reconnect-reliability + dock-state-transition question — planned as 5 isolated reconnects, alternating docked/undocked captures/CAP-037-2026-09-06_06-11-46_06-29-38-Group_AD/CAP-037-btsnoop_hci.log same file, raw path, 0/29,956 truncated analyzed — see CAP-037-FINDINGS.md. Procedure ran far longer than planned: 34 reconnects over ~20 minutes (not 5), 26 with real DLCI 0x04 payload — all 26 produce a 08 11/08 13 Get/Notify pair, zero misses, the largest single-session replication of DECISIONS.md ADR-022 on file. All 26 Settable-toggles samples (16 docked=0x00, 10 undocked=0xe8) match video-confirmed dock state, reconfirming ADR-024; a perfect SettableCurrent-state co-occurrence pattern proposed as a new 🟡 HYPOTHESIS. One 🔴 open anomaly: a mid-connection Settable flip with no preceding Get on one chandle
CAP-038 2026-09-06 Pixel 7a 14 (⚪ assumed) ⚪ assumed release_5.203 1.0.955078536 (⚪ assumed) AE OBS-005, incidental PAIR-003, INEAR-family Realistic physical reconnect trigger (Buds removed from case and worn) vs. CAP-036/CAP-037's OS-toggle-only trigger captures/CAP-038-2026-09-06_06-49-50_06-53-57-Group_AE/CAP-038-btsnoop_hci.log same file, raw path, 0/4,426 truncated analyzed — see CAP-038-FINDINGS.md. Video (247.07s) ends ~2m10s before the log; the documented "Window 2" (case-lid-only trigger) has zero video or wire evidence — not verifiably executed, a capture gap not a result. New finding: on the first (most "realistic") reconnect, the Fast Pair Message Stream/libmaestro channels opened under session-local DLCI numbers 0x05/0x03 instead of the usual 0x04/0x02 (consistent with CAP-001-FINDINGS.md §2's established session-local-numbering finding, not a new anomaly) — but that same reconnect's "Notify ANC state" frame read Settable-toggles=0x00 (docked) immediately after physical removal from the case, an unreconciled 🔴 tension with ADR-024. Two ANC Notify frames occur with no Get and no Set anywhere in the log, plausibly a hardware press-and-hold gesture (matches CAP-027-FINDINGS.md §4's mechanism). No A2DP/AVDTP signaling found. Settable-toggles again tracks dock state correctly (reconfirms ADR-024)
CAP-039 2026-09-06 Pixel 7a 14 (⚪ assumed) ⚪ assumed release_5.203 1.0.955078536 (⚪ assumed) AF OBS-006, incidental ANC-family, PAIR-003 Same-session comparison of the "Notify ANC state" Settable-toggles byte after a Set vs. a Get, dock state held constant captures/CAP-039-2026-09-06_07-07-17_07-11-43-Group_AF/CAP-039-btsnoop_hci.log same file, raw path, 0/6,184 truncated analyzed — see CAP-039-FINDINGS.md. The documented "one Set, one Get" plan did not occur: video/wire re-pass found 6 reconnects and 4 ANC taps (alternating Noise cancellation↔Adaptive, never "Off") instead. All 10 same-session Set/Get Settable-toggles samples read 0xe8 (undocked, constant) — a strong, zero-counter-example same-session confirmation of DECISIONS.md ADR-024's trigger-independence. Bonus: DLCI 0x04 Group 0x03 Code 0x03 reproduces the existing battery-code candidate with a live 98→97% Left transition
CAP-040 2026-09-06 Pixel 7a 14 (⚪ assumed) ⚪ assumed release_5.203 1.0.955078536 (⚪ assumed) AG PRIV-001, incidental PAIR-003, BATT-family DLCI 0x08's unmapped zero-length Get-shaped codes decoded via correlation against a known-changing value — executed as ~15 repeats of a docked/undocked cycle using the app's own Connect/Disconnect buttons captures/CAP-040-2026-09-06_07-28-00_07-57-05-Group_AG/CAP-040-btsnoop_hci.log (its .log.last is not part of this session — see below) main file only, raw path, 0/8,539 truncated analyzed — see CAP-040-FINDINGS.md. Integrity correction: CAP-040-btsnoop_hci.log.last is leftover on-device buffer content from CAP-039's own session plus the idle gap before CAP-040, not an intra-session rotation — only the main log is this session's own evidence (mirrors the same pattern independently found for CAP-042/CAP-041, below). Major finding: the app's own in-app "Connect"/"Disconnect" buttons produce zero wire-visible signal (no ACL teardown, no DLCI bounce) across ~15 taps — invalidating the session's own dock/undock bracketing for PRIV-001's intended correlation test (DLCI 0x08 opens exactly once, so all 7 flagged codes have N=1). Bonus, in the video's own previously-unreviewed tail: a genuine real reconnect cluster and a 🟡 HYPOTHESIS that the 0xff battery sentinel appears momentarily at the instant of physical docking
CAP-041 2026-09-06 Pixel 7a 14 (⚪ assumed) 🟢 confirmed release_5.203 (on-screen, 17:11:32) 1.0.955078536 (⚪ assumed) AH OBS-007, incidental PAIR-003 DLCI 0x02's connect-time RPC burst captured with deliberately non-default EQ/touch-controls settings, diffed against CAP-036's own default-settings burst captures/CAP-041-2026-09-06_17-10-39_17-17-48-Group_AH/CAP-041-btsnoop_hci.log same file, raw path, 0/4,003 truncated analyzed — see CAP-041-FINDINGS.md. Settings were changed continuously through the session instead of once beforehand, producing 3 wire-confirmed reconnects (not the planned 4) each against its own distinct, non-default settings state. The connect-time burst's length/shape signature is essentially invariant across all 3 states and CAP-036's default baseline — a scoped negative for a libmaestro-side settings read-back at the length level (full byte-for-byte content diff not completed, flagged as a follow-up). Bonus: a recurring 2-field sub-message inside the burst holds a constant value matching on-screen Case% throughout — consistent with, not confirming, CAP-036-FINDINGS.md §12.6's open item
CAP-042 2026-09-06 Pixel 7a ⚪ assumed (carried from CAP-036, not visible — screen off) ⚪ assumed release_5.203 n/a (backgrounded) AI OBS-002 Long, genuinely idle bracket to characterize the periodic DLCI 0x02/0x04/0x08/HFP cross-channel push cadence, app backgrounded captures/CAP-042-2026-09-06_17-30-02_18-07-42-Group_AI/CAP-042-btsnoop_hci.log (its .log.last is not part of this session — see below) main file only, raw path, 0/8,560 truncated analyzed — see CAP-042-FINDINGS.md. Integrity correction: CAP-042-btsnoop_hci.log.last reproduces CAP-041's own three Connection Complete events verbatim and simply continues through the idle gap before CAP-042 — it is not this session's own data, contrary to this task's own initial "confirmed rotated" framing. True session duration is ~37m39s (main log only), not ~55m45s. Central finding: the periodic push recurs only twice in the whole session (~16m and ~35m in, far sparser than CAP-036's short-session sample), and HFP's AT+BIEV drops out of the previously-documented 4-channel near-lockstep sync entirely — DLCI 0x02/0x04/0x08 keep their own mutual sync. Proposed 🔴 open question: an app-foreground-driven trigger, not a Buds-autonomous mechanism. Also revises CAP-027-FINDINGS.md's "streaming-specific" sync-breakdown framing — idle sessions desync too
CAP-043 2026-09-13 Pixel 7a 17 release_5.203 (⚪ assumed, maintainer-supplied context) 1.0.955078536 (⚪ assumed, maintainer-supplied context) Q (repeat) BATT-002, BATT-003 Repeat of CAP-011's Battery Notification BLE scan, this time with the official app force-stopped and no active classic RFCOMM+GATT connection throughout the entire capture — closes CAP-011-FINDINGS.md §4's open item either way captures/CAP-043-2026-09-13_09-50-31_09-53-41-Group_Q/CAP-043-btsnoop_hci.log same file analyzed — see CAP-043-FINDINGS.md in that folder; isolation genuinely clean this time (zero classic/RFCOMM/SDP traffic to the Buds' address across the entire 306.9s log, confirmed by both dissector filter and raw-byte scan); own-Buds 0xFE2C advertisement (RSSI -20..-23 dBm, 60 occurrences, byte-identical payload throughout) still does not structurally match PROTOCOL.md §4.3 Option A's documented Battery Notification layout (first byte 0x10, not 0x00; no 0x33/0x34 marker at any offset) — a second confirmed non-match, this time ruling out "active connection suppresses the advertisement" as an explanation; BATT-003 not exercised (no value change occurred this session by design)
CAP-044 2026-09-13 Pixel 7a 17 release_5.203 (🟢 confirmed on-wire) 1.0.955078536 (⚪ assumed, maintainer-supplied context) AA (repeat, 2nd attempt) SDP-001, SDP-002 (opportunistic, not attempted) Repeat of CAP-033's SDP UUID branch isolation, this time with Force-stop strictly preceding Forget and step 3's app-open baseline browse actually executed captures/CAP-044-2026-09-13_13-05-51_13-11-07-Group_AA/CAP-044-btsnoop_hci.log same file analyzed — see CAP-044-FINDINGS.md in that folder; isolation partially achieved, for a different reason than CAP-033: Force-stop does precede Forget this time, but no Force-stop is confirmed covering the 2m8s window between the in-app Forget and the actual "Pair" tap (13:08:07, corrected from the draft's assumed 13:07:00) that triggers the one-and-only SDP browse in this log; step 3 (app-open) is confirmed executed on-camera but produces no second SDP transaction since the classic connection/RFCOMM channels never close between the initial pairing and the later app reopen. "Default" UUID still absent (5th consecutive negative); "MAESTRO APP"/"GSND CONTROL"/"GSND AUDIO"/"GFPS RFCOMM" channel-name↔UUID↔RFCOMM-channel mappings from CAP-033 all reproduce byte-for-byte via this session's targeted per-UUID SDP queries (a different query style than CAP-033's single generic wildcard browse)
CAP-045 2026-09-12 Pixel 7a 17 release_5.203 (⚪ assumed) 1.0.955078536 AJ (new) HOLD-005 (not actually exercised), TOUCH-007 (incidental) Intended: HOLD-005 Left/Right ANC-rotation-checklist split captures/CAP-045-2026-09-12_08-22-51_08-24-32-Group_AJ/CAP-045-btsnoop_hci.log same file analyzed — see CAP-045-FINDINGS.md; the planned procedure was not run: the checklist screen was never opened — instead the maintainer physically long-pressed each earbud to cycle ANC mode (TOUCH-007's already-documented Notify-without-Set mechanism, CAP-027-FINDINGS.md §4). Zero rotation-checklist (qhr field 12) writes anywhere in the log. HOLD-005's own Left/Right question remains open; a genuine re-run is still needed. Bonus: 10-sample replication confirms the Notify frame structurally has no Left/Right field — side is only inferable from timing/video
CAP-046 2026-09-12 Pixel 7a 17 release_5.203 (⚪ assumed) 1.0.955078536 AK (new) AUDIO-003 Volume balance (field 17) scale/direction — isolated extreme-position samples with tight video correlation captures/CAP-046-2026-09-12_17-02-59_17-05-49-Group_AK/CAP-046-btsnoop_hci.log same file analyzed — see CAP-046-FINDINGS.md; polarity resolved, opposite of the working assumption: field17=+100=Left, field17=-100=Right (3/3 video-confirmed), range exactly ±100. Only 2 of the draft's claimed 4 samples actually occurred — no intermediate-position data exists. Bonus: field19 (Mono audio) fires in exact lockstep (8/8) with every Balance-extreme/center transition
CAP-047 2026-09-14 Pixel 7a 17 release_5.203 1.0.955078536 AL (new) none existing — a candidate Test-ID (area CASE) is proposed in CAP-047-FINDINGS.md §9 item 4 for "swapped-slot (mismatched L/R) docking", awaiting maintainer sign-off Purpose-built hypothesis test for the CAP-021 DLCI 0x0a burst trigger — only Trigger candidate 3 (charge-state change) run this session, across two recordings due to a repeated swapped-slot (not corrected) docking attempt; Trigger candidates 1/2 not attempted captures/CAP-047-2026-09-14_05-51-38_06-35-30-Group_AL/CAP-047-btsnoop_hci.log (Recording 1) + CAP-047-btsnoop_hci-2.log (Recording 2); its .log.last is not part of this session — see below same files, raw path, 0/3,250 and 0/4,716 truncated analyzed — see CAP-047-FINDINGS.md. Integrity correction: CAP-047-btsnoop_hci.log.last is leftover pre-session (logging-setup) buffer content, not part of this session — only the two named logs above are this session's own evidence (mirrors the CAP-040/CAP-042 pattern, not CAP-049's). Trigger 3: clean, complete negative — zero DLCI 0x0a-role payload across six bracketed charge-state transitions in two full, untruncated logs (raises the burst's "1 of N" denominator to at least 20 sessions checked). Major bonus finding: a dense video re-check (1fps + 4fps, cross-validated against the phone's own per-earbud charging-icon indicator) confirms no corrected (matching-slot) docking ever occurred in either recording, contrary to a maintainer recollection. Further bonus findings: DECISIONS.md ADR-024's dock-state byte reads "both docked" during a swapped-slot seating (confirms it doesn't check slot correctness); Recording 1's swapped dock never disconnects the ACL link while both of Recording 2's swapped docks do, despite an identical dock-sensor reading — an unreconciled tension with ADR-016, flagged for maintainer review; two new ADR-024 counter-example readings found, alongside CAP-048-FINDINGS.md's existing ones
CAP-048 2026-09-12 Pixel 7a 17 release_5.203 (⚪ assumed) 1.0.955078536 AD (repeat) OBS-004 Repeat of CAP-037's dock-state anomaly with an open ACL, with continuous physical dock-state video captures/CAP-048-2026-09-12_17-41-41_17-52-44-Group_AD/CAP-048-btsnoop_hci.log (.log.last present, determined leftover pre-session content, not used) same file analyzed — see CAP-048-FINDINGS.md. CAP-037's own flagged anomaly directly resolved: a mid-connection Settable-toggles flip with no preceding Get is confirmed, via video at the exact wire timestamp, to be a genuine real-time docking action — consistent with ADR-016 (disconnect follows once the second bud docks too). 13/13 zero-miss ADR-022 replication. A genuine, video-confirmed counter-example to a simple ADR-024 reading: two fresh reconnects report "docked" while the case is visibly empty on video — reported plainly, not reconciled away. The draft's own claimed 25-reconnect rapid-alternation table does not survive verification — actual cadence was much slower/less regular. Bonus: an unexplained 7-event connection-retry burst, plausibly linked to a closed case
CAP-049 2026-09-12 Pixel 7a 17 release_5.203 (⚪ assumed) 1.0.955078536 AF (repeat) OBS-006 Repeat of CAP-039's unexplained disconnect/reconnect cycling, with continuous phone-screen recording captures/CAP-049-2026-09-12_18-13-54_18-21-49-Group_AF/CAP-049-btsnoop_hci-combined.log (genuine same-session .log.last rotation, safely combined per this session's own Step G method — see CAP-049-FINDINGS.md §0; video renamed from -recordings.mp4) combined file (main+.log.last) analyzed — see CAP-049-FINDINGS.md. Clean negative: the cycling did not reproduce — after one deliberate reconnect, the classic connection stays open and stable for the rest of the ~9-minute session (RFCOMM traffic still flowing near the log's own end). OBS-006's Set-vs-Get Settable-toggles comparison confirmed consistent (both 0xe8), a further same-session ADR-024 confirmation
CAP-050 2026-09-14 Pixel 7a 17 (⚪ assumed, carried over) ⚪ assumed release_5.203 (carried over) 1.0.955078536 (⚪ assumed) AG (repeat) PRIV-001 Repeat of CAP-040's DLCI 0x08 unmapped Get-shaped codes correlation, this time using a trigger confirmed to actually reopen DLCI 0x08 (OS Bluetooth toggle or physical dock/undock), not the app's own Connect/Disconnect buttons captures/CAP-050-2026-09-14_21-01-01_21-13-30-Group_AG/CAP-050-btsnoop_hci.log same file, raw path, 0/20,060 truncated analyzed — see CAP-050-FINDINGS.md. Session-local DLCI reassignment confirmed a second time (one reconnect placed HFP, not the private envelope, on DLCI 0x08) — the private envelope was re-identified per reconnect by content signature, giving 15 confirmed + 1 inconclusive channel opens (not the video's own "well over 20" in-app-label estimate). All 7 PRIV-001-flagged codes fire on every private-envelope (re)open (14 full samples, vs. CAP-040's N=1); 5 of 7 resolve to "not a match"/"no candidate found" (constant neighbors), 2 (04 04/04 15) remain inconclusive — their neighbors (04.05/04.16) fluctuate near dock-state changes but don't reproduce for the same physical configuration across reconnects. Both of CAP-050-EVENT-NOTES.md's own flagged ambiguous dock-state windows resolved via wire+video correlation; one genuine Settable-toggles stale-reading counter-example found (consistent with CAP-048-FINDINGS.md §5's already-documented pattern, not a new contradiction of DECISIONS.md ADR-024). No clear video evidence of the maintainer-recalled mis-docking event at the review densities applied
CAP-051 2026-09-14 Pixel 7a 17 (⚪ assumed, carried over) ⚪ assumed release_5.203 (carried over) 1.0.955078536 (⚪ assumed) AM (new) none existing — a candidate Test-ID (ANC-005) is proposed in CAP-051-FINDINGS.md §5 item 4, awaiting maintainer sign-off; incidental ANC-family, TOUCH-007 qhr field 13 ANC-parallel-path wire confirmation — isolated in-app tap and isolated physical press-and-hold gesture, checking whether either correlates with a DLCI 0x02 field13 write alongside DLCI 0x04's confirmed path captures/CAP-051-2026-09-14_21-42-55_21-44-09-Group_AM/CAP-051-btsnoop_hci.log same file, raw path (confirmed, not just claimed), 0/2,457 truncated analyzed — see CAP-051-FINDINGS.md. All four ANC-mode actions confirmed against DLCI 0x04's already-FACT path: the in-app tap produces a genuine Set(0x12)+ACK+Notify sequence; all three physical press-and-hold gestures produce only a spontaneous Notify(0x13) with no preceding Set/Get anywhere in the log — directly confirming CAP-038-FINDINGS.md §5's previously-unconfirmed "physical gesture" hypothesis for its own Get-less/Set-less Notify frames. Central Group AM question: clean, confirmed negative — no field5{field4{field13=N}} write (or any DLCI 0x02 Sent-direction payload at all) appears on DLCI 0x02 for any of the four actions, contrasting with REVERSE_ENGINEERING.md's own qhr-entry static-analysis finding (a real, compiled write path exists for both triggers) — not contradicted, but not observed to fire in this session
CAP-052 planned Pixel 7a TBD TBD TBD AN (new) none existing — flagged as a TESTPLAN_BLUETOOTH_HCI_SNOOP.md follow-up, not assigned here (see CAP-052-EVENT-NOTES.md) CAP-041 Case%-change bracket — ≥30-minute session with a genuine Case battery charge/discharge, to test whether DLCI 0x02's recurring 2-field sub-message actually tracks Case% or merely coincided with a constant value planned
CAP-053 planned Pixel 7a TBD TBD TBD AO (new) EQS-004, EQP-008 EQ outer field 16-vs-18 — drag-and-release without ever tapping Save, isolated from the newly-found navigate-away trigger too, added ai-sessions/0017 planned
CAP-054 planned Pixel 7a TBD TBD TBD AP (new) BATT-002, BATT-003 Connection-free Battery Notification scan bracketing a single-bud insertion/removal event (per the Fast Pair spec's own "optional" trigger condition), added ai-sessions/0017 planned
CAP-055 planned Pixel 7a TBD TBD TBD AQ (new) HEAD-002, HEAD-003 Nod/Shake head gestures performed while an actual incoming call/notification is active, camera angled to also capture the gesture itself, added ai-sessions/0017 planned
CAP-056 planned Pixel 7a TBD TBD TBD AR (new) HOLD-005 Genuine re-run of the ANC-rotation-checklist Left/Right split (CAP-045 never opened the checklist screen), with a mandatory on-camera anti-repeat safeguard, added ai-sessions/0017 planned
CAP-057 planned Pixel 7a TBD TBD TBD AS (new) FW-002, FW-003 (incidental) Live GetSoftwareInfo/GetHardwareInfo correlation against the DLCI 0x02 connect-time burst's 3-string sub-message, following this session's structural finding that it matches qjm/qjr (GetHardwareInfo) better than qie (GetSoftwareInfo), added ai-sessions/0017 planned

Column notes:

  • PhonePixel 7a (primary, official app) or Pixel 9a (secondary, GrapheneOS), per the two-device setup described at the top of this document.
  • Group(s) — the letter(s) from §4 (Z, A–Q, R, S — Z, R, and S are special-purpose groups that intentionally sort outside the A–Q run-through: Z is pipeline-validation, always done first; R is the occasional forced-GATT-discovery procedure; S is the occasional GMS-disabled/no-app procedure — both R and S are done only when needed) covered in this session; one bugreport pull can cover several groups if captured as one continuous logging session (§4.1).
  • Test(s) — the TESTPLAN_BLUETOOTH_HCI_SNOOP.md Test-ID(s) actually exercised in this session (e.g. ANC-001, ANC-002) — this is what a CAP-NNN-FINDINGS.md finding should ultimately trace back through: finding → this row's capture ID + frame number → the Test-ID here → the catalog entry in TESTPLAN_BLUETOOTH_HCI_SNOOP.md. Usually a subset of everything the Group(s) column's scenario covers, since not every attempt succeeds cleanly.
  • Status — one of: planned (added 2026-08-14 — a specified but not yet run session, per its own capture-scenario paragraph in §4; every other column stays TBD/ until the session actually happens), captured (extracted, not yet reviewed), analyzed (reviewed in Wireshark per §5, findings recorded per §8), promoted (a finding from this capture has been written into PROTOCOL.md with a [VERIFIED-LOCAL] tag), discarded (unusable — note why, e.g. in an extra remarks column if needed).
  • Reference a capture from PROTOCOL.md by its ID (e.g. "confirmed in CAP-001, frame 214") rather than by date or description, so the reference survives even if this row's description is later edited.

https://github.com/tedsluis/opencontrolpixelbudspro2/blob/main/CAPTURE_BLUETOOTH_HCI_SNOOP.md - https://tedsluis.github.io/opencontrolpixelbudspro2/CAPTURE_BLUETOOTH_HCI_SNOOP