Architecture and design decisions, in ADR (Architecture Decision Record) style.
Every significant choice — especially if it deviates from an earlier assumption or an AI suggestion — is recorded here before it is implemented broadly. An earlier decision is never silently overwritten: a new, conflicting decision explicitly references the number it replaces ("supersedes ADR-00X"), and the superseded ADR's status is updated accordingly rather than deleted.
Numbering: ADRs are numbered sequentially and a number is never reused,
even if an ADR is later rejected or superseded. Before adding a new ADR, check
the highest existing number below and use the next one — do not guess or
pre-assign a number in another document (e.g. TODO.md), since a task
written today can be overtaken by another ADR being added first. Register
every new ADR in id_registry.csv (repo root) alongside adding it here —
scripts/lint_docs.py checks every ADR-NNN reference against that registry
and flags anything unregistered, catching a reused/mistyped number
mechanically instead of relying on a human noticing (see CHANGELOG.md's
fix: entry on the CAP-005/CAP-007/CAP-010 ID-reuse incident that
motivated this).
## ADR-XXX — <title>
- **Date**:
- **Status**: Proposed / Accepted / Rejected / Superseded by ADR-YYY
- **Context**: what problem or question was at play
- **Options considered**:
- **Decision**:
- **Consequences**: what becomes easier/harder as a result
- Date: (project start)
- Status: Accepted
- Context: A clear separation is needed between (fast-evolving) protocol knowledge and the rest of the app, because protocol knowledge keeps changing throughout the project as reverse engineering progresses.
- Options considered: MVC, MVVM + Clean Architecture, MVI
- Decision: Clean Architecture with MVVM in the UI layer and a Repository
pattern between the domain and data layers, split across five Gradle
modules (
:app,:ui,:domain,:data,:hardware, with:appas the composition/DI-wiring module) with enforced one-way dependency direction. SeeARCHITECTURE.md§2. - Consequences: somewhat more boilerplate (module boundaries, sealed result types), but protocol changes stay isolated in the data/hardware layers and the UI/domain layers remain independently unit-testable without real Bluetooth hardware.
- Date: 2026-08-07
- Status: Accepted
- Context: The project is open source and reconstructs a protocol through
reverse engineering; the license needs to protect that reverse-engineered
knowledge and any modified version of the app — including one deployed as a
network-accessible service — from being turned into a closed-source fork,
in line with the project's Zero-GMS / privacy-first goals (see
PROJECT.md,AGENTS.md§1). - Options considered:
- MIT — maximally permissive, allows closed-source forks; offers no protection against a proprietary derivative being redistributed without sharing improvements back.
- GPL-3.0 — strong copyleft for distributed binaries, but does not cover the case of a modified version run only as a network service without distributing the binary (the "SaaS loophole").
- AGPL-3.0 — same copyleft guarantees as GPL-3.0, and additionally requires that anyone running a modified version on a network server make the modified source available to that server's users.
- Decision: GNU Affero General Public License, version 3 (AGPL-3.0). See
LICENSE. - Consequences: any distributed or network-deployed modified version of this app must have its source made available, which keeps future improvements to the protocol reconstruction and app in the open. This may discourage some proprietary reuse or commercial integrations that would otherwise consider a permissive license — considered acceptable given the project's privacy/openness goals. Contributors should be aware of the AGPL's network-use clause when integrating third-party code.
- Update (2026-08-15): reaffirmed after
AGENTS.md§12 andREADME.mddropped the "clean-room" framing in favor of "independent implementation based on reverse-engineering" (the earlier phrase was legally imprecise, since a true clean-room process requires a second team that never examined the original implementation, whereas this project's own reverse engineering includes JADX/apktool decompilation of the official APK). That relabeling does not change this decision: AGPL-3.0 vs. GPL-3.0 is a question about redistribution terms for this project's own code, not about how that code was derived, and the SaaS-loophole rationale above is unaffected either way. GPL-3.0 was re-examined and rejected again for the same reason as originally: it does not require sharing modifications made to a version deployed only as a network service. AGPL-3.0 stands.
ADR-003 — Reverse engineering method: capture + APK analysis, no binary reverse engineering of protocol internals by the AI
- Date: (project start)
- Status: Superseded by ADR-017 (see below)
- Context:
.protoschemas and opcodes referenced by the app are extracted fromlibmaestro/libgfpsbinaries via external tooling (e.g.pbtk). There's a question of whether an AI coding assistant should attempt to reverse engineer these binaries directly during a session. - Options considered:
- Let the AI attempt to reverse engineer/guess undocumented opcodes directly from binaries or APK bytecode during implementation.
- Require
.protoschemas and opcodes to be extracted up front by the maintainer (viapbtk/JADX/apktool) and treated as given inputs; the AI only consumes and applies already-extracted, evidenced protocol knowledge.
- Decision: the second option. See
AGENTS.md§4 and §6, and the evidence rules inPROJECT_RULES.md§1. - Consequences: slower iteration when new protocol knowledge is needed (requires a maintainer-driven extraction step first), but avoids an AI silently inventing plausible-looking but unverified opcodes or APIs, which would violate the project's evidence-based reverse-engineering principle.
- Update (2026-08-30): superseded by ADR-017. The maintainer explicitly requested, in
conversation, that AI assistance be allowed to help with the mechanical parts of APK
decompiling and proto-schema extraction (including running
pbtkitself and, per a separate explicit decision, explaining native.sodisassembly output) ahead of Phase 2 (APK reverse engineering) work. ADR-017 replaces this decision's blanket restriction with a narrower boundary: the AI may run searches, list candidates, and explain already-surfaced code/disassembly, but never decides relevance or promotes a finding to a recordedREVERSE_ENGINEERING.mdHYPOTHESIS — see ADR-017 for the full boundary. This entry's original text is left standing perPROJECT_RULES.md§3 rule 9's non-destructive-update convention.
- Date: (project start)
- Status: Accepted
- Context: The project's core motivation is an app that works fully offline and independent of Google Play Services, including on GrapheneOS where GMS may be absent or sandboxed.
- Options considered:
- Support an optional GMS-based path (e.g. for update checks or Fast Pair UI integration) alongside a GMS-free path.
- Ban GMS and the
INTERNETpermission entirely, with no exceptions.
- Decision: full ban — no
com.google.android.gms.*, noINTERNETpermission, under any circumstance. SeeAGENTS.md§1. - Consequences: no in-app update checks, crash reporting, or cloud EQ presets; updates are distributed manually (e.g. via GitHub Releases). This is treated as an acceptable, intentional trade-off rather than a limitation to work around.
- Date: (project start)
- Status: Accepted
- Context: Continuous background BLE scanning is fingerprintable and conflicts with GrapheneOS's threat model, which the app targets as its primary reference OS.
- Options considered:
- Custom continuous/periodic BLE scanning for device discovery.
BluetoothAdapter.getBondedDevices()for already-paired devices plusCompanionDeviceManager(API 26+) for first-time pairing.
- Decision: the second option. See
AGENTS.md§7 andARCHITECTURE.md§9. - Consequences: pairing UX is delegated to the OS picker rather than a
custom in-app scan screen, but the app never needs
ACCESS_FINE_LOCATION/ACCESS_COARSE_LOCATIONorBLUETOOTH_PRIVILEGED, and only gains access to the specific device the user selects.
ADR-006 — Bounded exception to the no-BLE-scanning rule, for the Fast Pair Battery Notification only
- Date: 2026-08-08
- Status: Accepted
- Context:
AGENTS.md§7 (per ADR-005) bans continuous background BLE scanning for device discovery, in line with GrapheneOS's threat model. Separately,PROTOCOL.md§4.3 Option A identifies the officially documented Fast Pair "Battery Notification" BLE advertisement as the lowest-cost battery reporting mechanism (no active RFCOMM connection required). Read literally, the discovery-scanning ban risked being interpreted as also blocking this unrelated, already-bonded-device use case — since agents are instructed to strictly followAGENTS.md, a rule with no carve-out could cause an agent to refuse to implementPROTOCOL.md§4.3 Option A entirely, forcing battery status onto the connection-requiring RFCOMM path (Option B) as the only available mechanism. This tension was flagged inARCHITECTURE.md§9.1 as an open question. - Options considered:
- Leave the discovery-scanning ban as an absolute, unqualified rule and never use BLE scanning for battery reporting, relying only on the RFCOMM-connected path (Option B).
- Treat the Battery Notification as fully exempt from the scanning rule with no additional constraints, on the reasoning that it isn't "discovery."
- Define a narrow, explicitly bounded exception: permitted, but only when filtered to the bonded device, foreground-triggered, time-boxed, and stopped on backgrounding.
- Decision: the third option. The exact rule agents must follow is
recorded in
AGENTS.md§7 (authoritative wording), summarized for architectural context inARCHITECTURE.md§9.1. In short: scanning for the Battery Notification is permitted only when (a) filtered to the already-bonded device's own identifiers, (b) triggered by a user-visible event rather than a background timer, (c) time-boxed to roughly the advertisement's own visibility window (~8–20s), and (d) stopped immediately if the app leaves the foreground. - Consequences: the app can use the lowest-cost, connection-free battery
path as originally intended in
PROTOCOL.md§4.3, without an AI agent correctly-but-unhelpfully refusing to implement it as a false positive against the discovery-scanning ban. The exception is deliberately narrow — any future feature needing broader or continuous scanning (e.g. general device discovery) remains fully covered by the original ban inAGENTS.md§7 and would need its own, separate decision; it is not opened up by this ADR.
ADR-007 — CAPTURE_BLUETOOTH_HCI_SNOOP.md Groups are capture scenarios, not tests; TESTPLAN_BLUETOOTH_HCI_SNOOP.md is the test/behavior catalog
- Date: 2026-08-08
- Status: Accepted
- Context:
CAPTURE_BLUETOOTH_HCI_SNOOP.md's Groups A–Q andTESTPLAN_BLUETOOTH_HCI_SNOOP.md's four action tables had grown to describe largely the same ~66–70 Buds actions/behaviors, but with different groupings, wording, and no ID linkage between them, and no structured place to record per-action results (only a session-level Capture Index existed). This risked the same finding being independently re-described in two places, and gave "a Group" no clear relationship to "an official test." - Options considered (evaluated against three criteria: supporting live
execution, complete/clear recording, and ease of later analysis):
- Two files,
CAPTURE= procedure + testing,TESTPLAN= results only — rejected: leaves the original duplication largely intact, since Groups would still function as a de facto test catalog. - Three files/layers — a stable action catalog, a pure procedure document, and a separate results/evidence log — cleanest separation, but adds a third artifact and ID namespace before the project has completed even one real capture; assessed as premature for the project's current stage.
- Two files, redefined roles:
CAPTURE's Groups become explicit capture scenarios (how to run an efficient session),TESTPLANbecomes a stable action/behavior catalog with permanent Test-IDs, existence confidence, linked Group(s), and a thin evidence pointer intoPROTOCOL.md(never a duplicate results table).
- Two files,
- Decision: option 3. See
TESTPLAN_BLUETOOTH_HCI_SNOOP.md§0 for the full reasoning and the Test-ID convention, andCAPTURE_BLUETOOTH_HCI_SNOOP.md§4's intro for the capture-scenario framing. Every numbered action inCAPTUREis annotated with its Test-ID; the Capture Index (§9) gained a Test(s) column, closing the chain: Test-ID → Group →CAP-NNNcapture → frame →PROTOCOL.mdfinding. - Consequences: a Group can now legitimately bundle unrelated Test-IDs
for capture efficiency (e.g. Group C bundles
CONV-001andMULTI-001) without that being a modeling problem. Mapping the two documents onto each other surfaced two genuine, previously-untracked gaps (no capture scenario yet forINEAR-004andGATT-001), now recorded inTESTPLAN's open-items section rather than silently missing. Trade-off: two ID namespaces (CAP-NNNsessions,<AREA>-NNNtests) instead of one, requiring the same numbering discipline asDECISIONS.mdADRs (never reused, checked against existing entries). If the project later needs option 2's three-layer separation (e.g. once results volume grows), this ADR should be superseded rather than silently reinterpreted.
ADR-008 — Fast Pair Account Linking, Ownership Transfer, and the Accessory Non-Owner Service are out of scope
- Date: 2026-08-15
- Status: Accepted
- Context: nRF Connect's cached GATT service list (
CAP-004-FINDINGS.md§6) surfaced a named "Accessory Non-Owner Service" candidate alongside the Google Fast Pair Service, and the official Fast Pair spec separately defines Account Key-based Account Linking (associating a device with a Google account) and Ownership Transfer (re-linking a device to a new owner's account) as part of the broader Fast Pair ecosystem. None of these have been targeted by any capture or test plan so far, but nothing had explicitly ruled them out either — leaving room for a future session to drift into investigating them without a scope check. - Options considered:
- In scope, investigate opportunistically — rejected: these mechanisms
exist specifically to manage a device's relationship with a Google
account, which is exactly the GMS/cloud dependency this project exists
to route around (
PROJECT.mdnon-goals,AGENTS.md§1's Zero-GMS rule). Reverse-engineering them would not serve the app's actual feature set (ANC, EQ, touch controls, battery, case sounds —PROJECT.md's v1 scope list) and risks scope creep into account-security-adjacent territory this project has no reason to touch. - Out of scope, explicit — adds one line of friction (checking this ADR before starting related work) in exchange for closing off a plausible-looking but unproductive research direction before any time is sunk into it.
- In scope, investigate opportunistically — rejected: these mechanisms
exist specifically to manage a device's relationship with a Google
account, which is exactly the GMS/cloud dependency this project exists
to route around (
- Decision: Fast Pair Account Linking, Ownership Transfer, and the
Accessory Non-Owner Service (and any other Fast Pair mechanism whose
purpose is managing the device's relationship to a Google account rather
than device control) are explicitly out of scope for this project. This
is a scope boundary, not a protocol finding — no capture time should be
spent decoding these mechanisms' wire behavior. If a future capture
incidentally surfaces traffic belonging to one of these mechanisms (as
CAP-004already has, via the GATT service list), it should be labeled/skipped as out-of-scope rather than investigated further, and this ADR updated only if the maintainer explicitly decides to bring one of these in scope later. - Consequences:
PROJECT.md's non-goals should reference this ADR (kept in sync there); any AI agent encountering Account-Linking/Ownership-Transfer/ Non-Owner-Service traffic declines to pursue it and points to this entry instead of silently expanding scope (AGENTS.md§15's "never silently expand scope" rule).
ADR-009 — ANC command channel confirmed as Fast Pair Message Stream (DLCI 0x04); FrameEncoder implementation blocked pending CAP-006
- Date: 2026-08-15
- Status: Accepted
- Context:
ARCHITECTURE.md§5's implementation gate requires that a DLCI's framing/command identification reach 🟢 FACT inPROTOCOL.mdand be recorded as aDECISIONS.mdADR before that channel'sFrameEncoder/FrameDecodermay be implemented. The ANC Set/Get/Notify command was promoted to 🟢 FACT inPROTOCOL.md§4.1 on 2026-08-12 (CAP-001-FINDINGS.md§5's "Full resolution") but never got the corresponding ADR — this entry closes that gap, perAGENTS.md§6's requirement that the same FACT determination trigger both the code-gate and the ADR together, not one without the other. - Finding being recorded: ANC mode is controlled via Google's official
Fast Pair Hearable Controls extension (
[OFFICIAL-SPEC]), Message Group0x08, over the official Fast Pair Message Stream on DLCI 0x04 — notlibmaestro's Pigweed-HDLC channel (DLCI 0x02) and not the private DLCI-0x08 envelope, both of which were live candidates before this resolution. Codes:0x11Get,0x12Set (Seeker→Provider, MAC+ACK),0x13Notify (Provider→Seeker); one-hot mode bitmask0x80=Transparent,0x40=Adaptive,0x20=Off,0x08=ANC. Evidence: official spec byte-match plus an internal content+timing cross-check withinCAP-001(4 of 4 decodedSetframes matched their nearest observed UI tap, in sequence, within ~1.5s) — seePROTOCOL.md§4.1 for the full write-up. - What this ADR does NOT clear, and why
FrameEncoderstays blocked: the FACT status above covers what a0x12frame means when one appears — it does not establish that every user-initiated ANC change reliably produces one.CAP-001is the only capture with this evidence, and in that single capture, 2 of the 6 physical ANC taps produced no matching0x12frame at all (CAP-001-FINDINGS.md§5's "Not resolved" note and 2026-08-15 risk flag). The leading explanation — first-tap UI-state realization while the ANC row was still greyed out — is plausible but unconfirmed; the alternative (real taps can silently fail to produce a command under some condition) would be a functional defect risk in the app being built, not just a documentation gap, if implemented on this evidence alone. - Decision: the ANC channel/opcode/framing determination is accepted as 🟢
FACT for documentation purposes (
PROTOCOL.md§4.1 stands). The KotlinFrameEncoder/FrameDecoderimplementation for this specific command is explicitly BLOCKED untilCAP-006(CAPTURE_BLUETOOTH_HCI_SNOOP.md's Capture Index — a clean, single-tap-per-window repeat of Group B) confirms that isolated, individually-triggered ANC taps reliably produce a0x12frame every time, closing the 2/6 gap. This is a narrower, command-specific block layered on top ofARCHITECTURE.md§5's general per-DLCI gate — DLCI 0x04's framing being FACT does not by itself clear every command that rides on it for implementation; each command's own reliability evidence matters too. - Consequences: implementation of the ANC control feature in
:datawaits onCAP-006, which should be prioritized accordingly inTODO.md. IfCAP-006confirms 100% reliability, this ADR should be updated (not superseded — the underlying framing finding doesn't change) to record the block as lifted, with a pointer to that capture's evidence. IfCAP-006reproduces misses, that is a new, higher-priority open question forPROTOCOL.md§6, not a reason to proceed with implementation regardless. - Update (2026-08-15):
CAP-006confirms 100% reliability — the block is lifted.CAP-006(CAPTURE_BLUETOOTH_HCI_SNOOP.md's Capture Index) ran the exact repeat this ADR called for: Bluetooth enabled and the connection allowed to settle, the ANC row confirmed fully active (not greyed out) before any tap, then each of the four ANC modes tapped exactly once in isolation. Result (CAP-006-FINDINGS.md§3): filtering the entire 233s log for Group0x08Code0x12returns exactly four frames — one per tap, in tap order, zero extras, zero misses — each within ~1.3s of its video-observed tap (frames 1393/1627/1731/1862, modes0x08/0x20/0x40/0x80matching Noise Cancellation/Off/Adaptive/ Transparency respectively). This is a clean 4/4, contrasting withCAP-001's 4/6 under bundled, unpaused conditions — the leading explanation from this ADR's "What this ADR does NOT clear" section (first-tap UI-state realization while the row was still greyed out, not a genuine command) is now the explanation best supported by the evidence, not merely plausible. TheFrameEncoder/FrameDecoderimplementation block for the ANC command is lifted. The underlying framing/opcode finding (PROTOCOL.md§4.1) is unchanged by this update, per this ADR's own note above that a confirming result would not require superseding it. This update does not extend to any other channel or command — perAGENTS.md§6, the implementation gate remains per channel/feature.
- Date: 2026-08-23
- Status: Accepted
- Note on process: this ADR was drafted by an AI agent, but per
AGENTS.md§6's requirement for explicit human/maintainer sign-off before an agent commits a newDECISIONS.mdADR as settled: the maintainer directly reviewedAUDIT_REPORT_2026-08-22.md's finding below and explicitly instructed that its recommendations, including this ADR, be carried out (session of 2026-08-23). That instruction is the explicit approval this rule requires — recorded here so the provenance is auditable, not assumed. - Context:
AUDIT_REPORT_2026-08-22.mdfound a direct textual conflict between two binding project documents.PROJECT_RULES.mdrule 19 states: "Sensitive or personal data (e.g. MAC addresses of your own devices, account details) is anonymized or excluded via.gitignorebefore committing."CONTRIBUTING.md's "Protocol/capture contributions" section separately and explicitly states the opposite for the maintainer's own captures: "The maintainer's own existing and future Bluetooth captures... intentionally retain real data — MAC addresses, timestamps, device identifiers... That is a decision only the maintainer can make about their own data, and it is not revisited by this document." This is a real, intentional, long-standing practice (everycaptures/CAP-NNN-*/session committed to date retains real identifiers), but the deviation from rule 19's literal text had never been recorded as aDECISIONS.mdADR, asPROJECT_RULES.md's own preamble requires for any knowing deviation from its rules. A reader encountering rule 19 in isolation would reasonably (and incorrectly) conclude the repo's own capture data is non-compliant with its own rules. - Options considered:
- Anonymize all existing and future captures to satisfy rule 19 literally — rejected: this data
is the evidentiary backbone of the entire reverse-engineering effort; the maintainer has
already made an informed decision (
CONTRIBUTING.md) to publish their own captures unredacted, and redoing that retroactively would provide no privacy benefit to a third party (it is the maintainer's own hardware/accounts) while destroying reproducibility for anyone trying to correlate aCAP-NNNfinding back to its exact source bytes. - Leave the conflict as-is — rejected:
PROJECT_RULES.md's own conflict-resolution clause specifically anticipates and requires recording exactly this kind of deviation; leaving it unrecorded is itself the gap being fixed. - Record the existing, already-practiced exception as a formal ADR, scoped narrowly to
captures/CAP-NNN-*and to the maintainer's own data specifically — chosen.
- Anonymize all existing and future captures to satisfy rule 19 literally — rejected: this data
is the evidentiary backbone of the entire reverse-engineering effort; the maintainer has
already made an informed decision (
- Decision:
PROJECT_RULES.mdrule 19's anonymize-or-exclude requirement does not apply to the maintainer's own Bluetooth captures undercaptures/CAP-NNN-*/(raw logs, event notes, findings, recordings, and any other artifact type present there) — this is a deliberate, informed, maintainer-only exception, not a general relaxation of rule 19. Rule 19 continues to apply in full to everything else (e.g. account details, credentials, any data outsidecaptures/) and, perCONTRIBUTING.md's existing PII-exception section, continues to apply in full to any third-party contributor's capture data, which must still be redacted before submission.CONTRIBUTING.md's existing explanation of why the maintainer's own data is exempt is unchanged and remains the canonical rationale; this ADR is the formal record of the deviation thatPROJECT_RULES.mditself requires. - Consequences: closes the textual conflict between
PROJECT_RULES.mdandCONTRIBUTING.mdwithout changing actual practice (which was already consistent withCONTRIBUTING.md, not rule 19's literal text). Future agents/contributors reading rule 19 should cross-reference this ADR andCONTRIBUTING.mdrather than concluding the repo's own captures are non-compliant. Does not affect the separate, unrelated logging rules for the app's own runtime code (AGENTS.md§7/§9 — never log the paired device's MAC address atINFOlevel or above), which govern the shipped app's behavior, not this repo's committed research data.
ADR-011 — Find My Buds Left/Right confirmed as Fast Pair Message Stream Action (DLCI 0x04, Group 0x04, Code 0x01); FrameEncoder implementation unblocked
- Date: 2026-08-23
- Status: Accepted
- Context:
PROTOCOL.md§4.4 carried a 🟡 HYPOTHESIS (strong) finding fromCAP-025(2026-08-21): Ring commands for the Left/Right earbuds ride the same Fast Pair Message Stream channel (DLCI 0x04) already established as 🟢 FACT for ANC (ADR-009), using Group0x04(Action), Code0x01(Ring). The maintainer reviewed this finding directly (session of 2026-08-23) and gave explicit sign-off to promote it, perAGENTS.md§6's requirement that an agent may propose but never unilaterally commit a FACT promotion. - Finding being recorded:
Group=0x04/Code=0x01on DLCI 0x04,Valuebyte0x01= start ringing Right,0x02= start ringing Left,0x00= stop/mute (shared, not per-earbud). Evidence: 4 action/response pairs (2 starts, 2 stops) inCAP-025, each individually video-correlated to a specific tap under Group K's one-action-per-window discipline, riding the same envelope mechanism already confirmed for ANC — not merely a surface resemblance to the spec's own worked example. SeePROTOCOL.md§4.4 for the full write-up. - What this ADR does NOT clear: Case and "both simultaneously" are a separate, unresolved
mechanism (
PROTOCOL.md§4.4's "Major structural finding") — video-confirmed to route through a different, likely GMS/Find-Hub-mediated path with zero localGroup 0x04 Code 0x01traffic across a ~2.5-minute observation window. This ADR covers Left/Right only; Case/"both" stays 🔴 OPEN QUESTION, flagged separately as a possible Zero-GMS scope limit. Also unresolved: the exact content of the second ACK variant's extra byte(s) — an audit pass on 2026-08-23 found the previously-cited "spec worked example" for the ACK itself was miscited (PROTOCOL.md§2.1's correction); this affects the ACK-byte interpretation only, not the Group/Code/Value command mapping this ADR records. - Decision: the Ring command's channel/opcode/value-mapping determination is accepted as 🟢
FACT for Left/Right specifically.
FrameEncoder/FrameDecoderimplementation for this command is unblocked, perARCHITECTURE.md§5's per-command implementation gate — no further capture is required before implementation begins, unlike ANC'sADR-009(which neededCAP-006's isolated repeat to close a reliability gap;CAP-025already used the same isolated, single-tap-per-window methodology from the start). - Consequences: Left/Right Find My Buds can be implemented in
:dataimmediately. Case/"both" stays out of scope for implementation until the separate Find Hub question is resolved (seePROTOCOL.md§6, Behavior).
ADR-012 — Wire-baseline firmware version confirmed as "release_5.203" (DLCI 0x08, Group 0x03, Code 0x02)
- Date: 2026-08-23
- Status: Accepted
- Context:
PROTOCOL.md§0.1 had tracked, since 2026-08-14, an open question distinguishing the UI-baseline firmware version ("release_5.203", confirmed via official app screenshot) from whichever value(s) the same string might correspond to on the wire, given four different version-like strings were independently documented across multiple channels ("release_5.203","Revision 6","cape2_sm","500m"–"500p").CAP-023(2026-08-21) captured, for the first time, a session that recorded both the app's own firmware-display screen and the wire traffic. The maintainer reviewed this finding directly (session of 2026-08-23) and gave explicit sign-off to promote it. - Finding being recorded: in
CAP-023, the on-screen "Device firmware version" (Left/Right/Case, allrelease_5.203, video-confirmed at 08:24:17) is byte-for-byte identical to the string independently present on DLCI 0x08's private envelope (Group0x03Code0x02) in the same session's connection-time handshake (frame 849, 08:23:46.038) — critically, before the firmware screen was even opened, ruling out the screen-open action itself as the source of the wire value. This is the first same-session match between an on-screen value and a wire value this project has recorded for this question. - What this ADR does NOT clear: what
"Revision 6"(DLCI 0x04's official Fast Pair Device Information field, Code0x09) represents, if not the user-facing firmware version, stays 🔴 OPEN QUESTION — this ADR resolves which string the app calls "the firmware version," not what every other version-like string on the wire means."cape2_sm"/"500m"–"500p"likewise remain unresolved, unchanged by this ADR. - Decision:
"release_5.203", as carried on DLCI 0x08's private envelope (Group0x03Code0x02), is accepted as 🟢 FACT to be what the official app displays as the Buds' firmware version. - Consequences: any future Startup Handshake / firmware-compatibility check
(
ARCHITECTURE.md§8.1) implemented against DLCI 0x08's Group0x03Code0x02value can treat it as the authoritative firmware-version string, not merely a plausible candidate. Does not by itself unblock anyFrameEncoder/FrameDecoderwork — this is a data-field identification, not a command channel.
ADR-013 — DLCI 0x02 general-purpose settings-write envelope shape confirmed (field5{field4{...}}} outer wrapper); generic write-path implementation unblocked, individual field semantics remain HYPOTHESIS
- Date: 2026-08-23
- Status: Accepted
- Context:
PROTOCOL.md§4.5's shared preamble documented a 🟡 HYPOTHESIS (strong) finding from the 2026-08-21 capture batch (CAP-019–CAP-024): every one of 9+ distinct settings (Conversation Detection, Multipoint, Touch controls, Head gestures, press-and-hold ×4, ANC-mode rotation, Mono audio, Volume EQ, Volume balance, In-ear detection, 2 Case-sound toggles) writes through DLCI 0x02 inside an identical two-level outer wrapper,field 5 { field 4 { ... } }, across 6 independent capture sessions with zero counter-examples. The maintainer reviewed this finding directly (session of 2026-08-23) and gave explicit sign-off to promote the envelope pattern itself — explicitly declining to blanket-promote every individual field mapping at the same time, since those vary widely in evidence strength (see below). - Finding being recorded: the outer
field5{field4{...}}}wrapper (standard protobuf wire-format tags), preceded by a constant, cross-session-stable 13-byte prefix, is a genuine, general-purposelibmaestrosettings-apply envelope — not a coincidental per-setting shape. This cross-capture, no-counter-example replication (9+ settings, 6 sessions, multiple days) is comparable in kind to how DLCI 0x02's own HDLC framing mechanism was promoted to FACT inPROTOCOL.md§2.2a. - What this ADR explicitly does NOT clear — narrower than it may look: only the outer
wrapper's existence and shape is FACT. Each subsection's specific field-number-to-setting
mapping in
PROTOCOL.md§4.5.1–§4.5.8 remains individually 🟡 HYPOTHESIS, unchanged by this ADR, reflecting genuinely different evidence strength per setting:- Better-evidenced (2+ independent samples within their capture): In-ear detection (both directions), Volume EQ (both directions), press-and-hold (4/4 Left/Right × ANC/Assistant combinations).
- Single-sample, one direction only: Conversation Detection, Multipoint, the Touch-controls and Head-gestures top-level toggles, and one of the two Case-sound toggles ("Bud return," whose one sample isn't even cleanly disambiguated from a screen-open state-sync).
- Volume Balance: field identity plausible, but scale/direction is explicitly still 🔴 open — unaffected by this ADR. No individual field mapping is promoted by this ADR. A future ADR (or a batch of them) would be needed before promoting any specific field's meaning, following the same per-item sign-off process used here.
- Decision: the envelope shape/pattern is accepted as 🟢 FACT. Per
ARCHITECTURE.md§5's per-command implementation gate, this unblocks implementing the generic write path — theFrameEncoderlogic that builds the two-level wrapper and the constant prefix — but does not unblock implementing what any specific field number means; aFrameEncodercall site that writes a real setting still requires its own field's HYPOTHESIS to be independently strengthened and separately promoted first. - Consequences:
:data'sCodecRoutercan implement and unit-test the shared envelope encode/decode logic now, against fixed byte-array fixtures, ahead of any specific setting being wired up — but no UI control for an individual setting (Conversation Detection, Multipoint, etc.) should ship against this ADR alone.
ADR-014 — DLCI 0x08 Group 0x0e Code 0x01 confirmed as a per-earbud+case battery push (index=1/2/3 → Left/Right/Case)
- Date: 2026-08-23
- Status: Accepted
- Context: while re-analyzing
CAP-011for an unrelated, maintainer-requested task (locating the exact video timestamp of a 1%-battery UI change), a message on DLCI 0x08 — the private envelope whose overall identity remains 🔴 OPEN QUESTION (§2.3) — was found to decode to 3 repeated[value, flag, index]entries. WithinCAP-011alone, entries index=1/2 tracked the on-screen Left/Right percentages across 4 occurrences in one session, including a video-confirmed live change. To check whether this held beyond one session, the same decode was run againstGroup 0x0e Code 0x01frames inCAP-001andCAP-002(both 2026-08-09, 12 days beforeCAP-011), picked near each session's own independently-recorded on-screen battery notification. The maintainer reviewed this cross-capture result directly (session of 2026-08-23) and gave explicit sign-off to promote it, perAGENTS.md§6. - Finding being recorded:
Group 0x0e Code 0x01's three repeated entries correspond to Left (index=1), Right (index=2), and Case (index=3) battery percentages. Evidence: a clean 3-for-3 match against on-screen values in bothCAP-001(frame 1114:[100,100,62]vs. on-screen "Left 100% Case 62% Right 100%") andCAP-002(frame 49024:[100,100,57]vs. on-screen "Left 100% Case 57% Right 100%"), plusCAP-011's own 4-occurrence, video-correlated Left/Right tracking (including a live 93→92/88→87 transition matched ~0.86s before the UI itself updated) and an independent cross-check via a second message (Group 0x04 Code 0x03) at the same 4 moments. This is a semantic decode of an already-structurally-known message, not a newly-found packet type —CAP-002-FINDINGS.md§2a documented the same shape back on 2026-08-12 without interpreting it. SeePROTOCOL.md§4.3 Option E andCAP-011-FINDINGS.md§7 for the full write-up. - What this ADR does NOT clear:
CAP-011's own Case (index=3) reading is stale, not live — it reads 92 throughout that session against an on-screen Case value that stayed at 89%, unlikeCAP-001/CAP-002where index=3 matched live. The index→component mapping is accepted as FACT; this session-specific staleness is a separate, still-open behavioral question (plausibly tied to that session's own documented procedure deviation — the case sat open and empty throughout — not confirmed).- The
flagfield (field2)'s meaning — observed as1on every fresh reading and absent onCAP-011's one stale reading, plausibly a "fresh/valid" bit, not confirmed as such. - The burst's trigger — recurs at irregular intervals in
CAP-011(4:02, 2:56, 8:21 apart); checked against that session's own near-continuous BLE reconnect churn and found no correlation. Genuinely unresolved. - DLCI 0x08's own identity/ownership as a channel — unaffected by this ADR, still 🔴 OPEN QUESTION (§2.3); this ADR resolves one message's meaning on that channel, not what the channel itself is or belongs to.
- Decision: the index=1/2/3 → Left/Right/Case mapping for DLCI 0x08's
Group 0x0e Code 0x01message is accepted as 🟢 FACT. - Consequences: this becomes a fifth candidate battery-reporting mechanism (
PROTOCOL.md§4.3 Option E), usable as a secondary/cross-validation signal alongside the already-FACT HFP option (C) if implemented — but not yet placed in the implementation-priority ordering, since its trigger/cadence is still unconfirmed and one observed session showed a stale field. Does not itself unblockFrameEncoder/FrameDecoderwork on DLCI 0x08 more broadly — that channel's other Groups (0x01/0x02/0x05/0x09) remain unidentified, unaffected by this ADR.
ADR-015 — BATT-006 resolved: AT+CIND battchg confirmed a stale single snapshot; AT+BIEV confirmed per-earbud (Right), not a fixed-aggregate/fixed-cadence indicator
- Date: 2026-08-2x
- Status: Accepted
- Context:
BATT-006(TESTPLAN_BLUETOOTH_HCI_SNOOP.md, added 2026-08-14) asked whetherAT+CIND?'sbattchgorAT+BIEV=2's HF Indicator #2 (or neither) tracks a real battery-level change over time, followingCAP-001-FINDINGS.md§3's single-snapshot disagreement between the two.CAP-009(2026-08-23) ran a dedicated, purpose-built 101-minute natural-discharge bracket for this question, then an independent repeat pass re-derived the same conclusions from a fresh video timeline and a full (not spot-checked) re-scan of the wire log. The maintainer reviewedCAP-009-FINDINGS.md§1–§5 directly and gave explicit sign-off to promote/record the findings below, perAGENTS.md§6. - Finding being recorded:
AT+CIND?'sbattchgis a single, non-repeating snapshot — queried exactly once, at HFP Service Level Connection setup, and never refreshed again for the rest of the session, regardless of real battery-level changes on the peer. Evidence: 101 minutes, one query (frame 884), zero repeats, including after a full reconnect later in the same log; the peer's Right earbud genuinely changed by ~13 percentage points in that window with nobattchgupdate at all.AT+BIEV=2tracks a real, individual earbud's percentage — specifically Right in this session — not a fixed aggregate of Left/Right/Case. All 5 of its distinct values across the session matched the Right earbud's on-screen percentage at every transition; none of Left's or Case's on-screen values ever appeared in theAT+BIEVsequence. This revises the project's earlier working assumption (PROTOCOL.md§4.3 Option C, pre-CAP-009) that both HFP indicators report one aggregate value.AT+BIEV's push cadence is not a fixed ~6–7s rate for the life of the connection. The ~6–7s spacingCAP-001observed is a connection-settling burst —CAP-009shows gaps widening to a median of ~20s and as much as ~14.6 minutes once the session goes idle. SeePROTOCOL.md§4.3 Option C andCAP-009-FINDINGS.md§1–§5 for the full write-up.
- What this ADR does NOT clear:
- Whether
AT+BIEValways reports physical-Right, or whichever earbud is currently HFP-primary — R happened to be primary in this one session; a session with confirmed-L primary is needed to distinguish these. Recorded as 🟡 HYPOTHESIS inPROTOCOL.md, not FACT. - Whether
AT+CIND?'sbattchgis itself aggregate or per-earbud — it was only ever observed once per session (here and inCAP-001), so this remains untested either way. - What exactly triggers an
AT+BIEVpush once the connection has settled —CAP-009cannot distinguish "push-on-change, with the change itself this infrequent" from "a poll that simply slows down while idle." Recorded as 🟡 HYPOTHESIS. - Two further
CAP-009findings are explicitly not covered by this ADR — proposed separately, at HYPOTHESIS level, and not requiring FACT-level sign-off: DLCI0x04'sGroup 0x03 Code 0x03as a candidate forPROTOCOL.md§4.3 Option B's still-open battery code, and a BLE Fast Pair scan as a candidate explanation for post-reconnect on-screen updates (PROTOCOL.md§4.3 Option A). Both remain 🟡 HYPOTHESIS pending further verification.
- Whether
- Decision:
battchg's single-snapshot behavior, andAT+BIEV's per-earbud (not aggregate) tracking of Right inCAP-009, are accepted as 🟢 FACT.AT+BIEV's non-fixed push cadence is accepted as 🟢 FACT for the specific claim "not a sustained ~6–7s rate"; the precise trigger mechanism remains 🟡 HYPOTHESIS. - Consequences:
BATT-006is closed as a Test-ID (TESTPLAN_BLUETOOTH_HCI_SNOOP.md). Any future battery-UI implementation relying on HFP (AGENTS.md§5) must not treatAT+CINDas a live source, must not assumeAT+BIEVrepresents a combined/aggregate value, and must not use a missed ~6–7s beat as a liveness signal —AGENTS.md§5 updated accordingly. Does not resolve DLCI0x04/BLE-scan HYPOTHESES noted above; those need their own follow-up before any further promotion.
ADR-016 — Retroactive sign-off: EQ field-to-band mapping/gain-clamp/preset quintets, and four CAP-016 hardware-behavior FACTs
- Date: 2026-08-28
- Status: Accepted
- Note on process: this ADR was drafted by an AI agent, but per
AGENTS.md§6's requirement for explicit human/maintainer sign-off before an agent commits a newDECISIONS.mdADR as settled: the maintainer directly reviewed the 2026-08-28 project-wide audit'sGOV-01finding and explicitly approved consolidating sign-off for all findings below into one ADR (session of 2026-08-28). That instruction is the explicit approval this rule requires — recorded here so the provenance is auditable, not assumed. - Context: on 2026-08-18,
PROTOCOL.mdwas updated directly from two independent capture sessions —CAP-015(EQ, completing/supersedingCAP-005's partial attempt) andCAP-016(Group U re-run) — promoting seven distinct claims to 🟢 FACT. Unlike every FACT promotion from 2026-08-21 onward (ADR-011–ADR-015), these seven were never given a correspondingDECISIONS.mdADR or an explicit "maintainer sign-off obtained" citation;PROTOCOL.md's changelog table still marked both 2026-08-18 entries "not yet reviewed by maintainer" as of the 2026-08-28 project-wide audit'sGOV-01finding. This ADR closes that gap. - Findings being recorded:
- EQ field-to-band mapping (
PROTOCOL.md§4.2): quintet field 1↔Low bass, 2↔Bass, 3↔Mid, 4↔Treble, 5↔Upper treble (wire order is the reverse of the on-screen top-to-bottom order). Evidence:CAP-015-FINDINGS.md§5 — all 5 sliders dragged individually, 3 passes each; 4 of 5 fields video-confirmed by finger-on-slider position, the 5th by elimination against a perfectly repeating field-change order across all 3 passes; matchesCAP-005's earlier single-band inference exactly, 5 days apart, independently. - Band-gain range, ±6.0 clamp (
PROTOCOL.md§4.2). Evidence:CAP-015-FINDINGS.md§4 — 8 of 10 extreme-drag samples land at exactly ±6.0, the remaining 2 at 5.8/5.9 (consistent with the drag gesture not quite reaching the slider's physical edge before release, not a different clamp value). Units not independently confirmed (plausibly dB), unaffected by this promotion. - Confirmed preset quintets (
PROTOCOL.md§4.2):Last saved/Heavy bass/Light bass/ Balanced/Vocal boost/Clarity, each a[Low bass, Bass, Mid, Treble, Upper treble]5-tuple. Evidence:CAP-015-FINDINGS.md§5 — Heavy bass's quintet independently matches the 2026-08-15 capture's own decode byte-for-byte. - Reconnect, Buds-initiated variant (
PROTOCOL.md§5.1): a singleRcvd Connect Request→Sent Accept Connection Request→Rcvd Connect Completesequence, landing within 0.5s of on-camera earbud removal from the case. Evidence:CAP-016-FINDINGS.md§1, frames 1213–1217. - Disconnect-on-redock (
PROTOCOL.md§7): ACLDisconnection Complete(reason0x13, Buds-initiated) fires the instant the second bud is placed in the case, not on lid-close alone. Evidence:CAP-016-FINDINGS.md§1. - Case-lid zero-signal (
PROTOCOL.md§7): opening/closing the case lid while both buds remain outside the case produces no wire-visible signal on any RFCOMM channel. Evidence:CAP-016-FINDINGS.md§5, independently reproducingCAP-007-FINDINGS.md(old) §3.4 — 2-capture-confirmed. - DLCI 0x08
Group 0x04 Code 0x12's alternating value is event-driven and autonomous (PROTOCOL.md§6 Resolved): fires in step with DLCI-0x08 channel-(re)open events, and also continues firing during otherwise-idle stretches with no channel churn — neither purely reactive nor purely free-running. Evidence: first characterized this way inCAP-004-FINDINGS.md§5a Task 5 andCAP-007-FINDINGS.md(old) §3.2/§5, independently reconfirmed byCAP-016-FINDINGS.md§7 (8 pushes, cycling0x02/0x03, 2 in step with channel-(re)opens, 4 during idle stretches with no churn).
- EQ field-to-band mapping (
- What this ADR does NOT clear:
- EQ's outer field 16 vs. 18 ("preview" vs. "fires on slider-release") reading remains 🟡
HYPOTHESIS, unaffected —
PROTOCOL.md§4.2 already states this explicitly; not promoted here. - DLCI 0x08 Code
0x12's value's actual meaning (what0x02/0x03/0x04represents) remains 🔴 OPEN — this ADR covers only the event-driven-and-autonomous behavior characterization, not the value's semantics. CAP-016's other findings, already explicitly marked "not promoted"/"awaiting maintainer sign-off" in its own §8 (the ANC settable-toggles-byte refinement, theAndroidHeadTrackerHID decode), are not covered by this ADR — they remain open, as already correctly tracked.- Band-gain units (dB or otherwise) remain unconfirmed.
- EQ's outer field 16 vs. 18 ("preview" vs. "fires on slider-release") reading remains 🟡
HYPOTHESIS, unaffected —
- Decision: all seven findings above are accepted as 🟢 FACT.
- Consequences:
PROTOCOL.md's changelog rows for 2026-08-18 updated to cite this ADR instead of "not yet reviewed by maintainer"; the corresponding body sections (§4.2, §5.1, §7 ×2, §6 Resolved) gain an explicitADR-016citation, matching the citation style already used forADR-011–ADR-015.
ADR-017 — Supersedes ADR-003: AI-assisted mechanical decompilation and proto-schema extraction, within a maintainer-decides-relevance boundary; native .so disassembly assistance now in scope
- Date: 2026-08-30
- Status: Accepted
- Context: ADR-003 banned an AI coding assistant from attempting to reverse engineer
libmaestro/libgfpsbinaries directly, requiring.protoschemas and opcodes to be extracted up front by the maintainer and treated as given inputs. The maintainer has now explicitly requested, in conversation, that AI assistance be allowed to help with the mechanical parts of APK decompiling and proto-schema extraction ahead of the newly-planned Phase 2 (APK reverse engineering) work (TODO.md, currently 0% done) — this is a maintainer-directed policy change, not the AI expanding its own scope. PerPROJECT_RULES.md§3 rule 9, this is recorded as a new, superseding ADR rather than an edit to ADR-003's existing text. - Options considered:
- Leave ADR-003 as-is (fully manual extraction only) — rejected per explicit maintainer instruction to enable AI assistance for Phase 2.
- Let the AI independently decide which classes/strings/findings are relevant and record them as
HYPOTHESIS entries in
REVERSE_ENGINEERING.md— rejected: this would erode the evidence discipline inPROJECT_RULES.md§1 and conflicts withAGENTS.md§6's principle that relevance/promotion judgments are the maintainer's call, not an AI's. - Allow AI mechanical assistance only (running searches, listing candidate matches, explaining
syntax/structure of already-surfaced code, running
pbtkextraction, and — per the maintainer's explicit answer to this ADR's native-library question — disassembly-output analysis for native.solibraries), while the maintainer retains every relevance and hypothesis-recording decision — chosen.
- Decision:
- This ADR supersedes ADR-003.
- New boundary. An AI session may: run keyword/string searches across
jadx-output/,apktool-output/, andpbtk-output/; runpbtkto extract.protoschemas from an already-obtained APK; list candidate matching classes/methods/strings; and explain the syntax/structure of already-surfaced decompiled or disassembled code — including native.sodisassembly output (Ghidra/radare2 or similar), which the maintainer has explicitly placed in scope for this same mechanical-assistance boundary (resolving the question this ADR was asked to record, see below). An AI session does not decide which class, string, or finding is relevant to the protocol, and does not decide whether something becomes a recorded HYPOTHESIS (or FACT/ASSUMPTION) entry inREVERSE_ENGINEERING.md— both remain the maintainer's calls, unchanged from ADR-003's original intent. - Unaffected rule. This ADR does not change
AGENTS.md§6/§15's sign-off requirement: promoting anything to 🟢 FACT inPROTOCOL.md, or writing any otherDECISIONS.mdADR (including one superseding this one), still requires explicit maintainer approval — an AI session may propose, never commit, exactly as before. - Native
.soboundary, explicitly decided (not silently inherited): disassembling native.solibraries is a materially deeper form of reverse engineering than DEX/Java decompilation, and was called out separately rather than left to ride along with this change. The maintainer's explicit answer (session of 2026-08-30): in scope for AI mechanical assistance, on the same terms as §2 above — search, list, and explain only; relevance and hypothesis decisions stay with the maintainer.REVERSE_ENGINEERING.md's Native Libraries section note (written under ADR-003's old blanket restriction) is updated accordingly so it no longer contradicts this ADR.
- Consequences: Phase 2 (APK static analysis,
TODO.md) can proceed with AI assistance on its mechanical steps — keyword/string search,pbtkextraction, native-binary disassembly-output explanation — without waiting for the maintainer to perform every step manually. The evidence discipline inPROJECT_RULES.md§1 is preserved because relevance and hypothesis-recording decisions stay exclusively with the maintainer. This does not change §4/§8 rule 20's rules on what gets committed to this project's own codebase (no copied code, no committed decompiled output, no committed APK — see the versioned storage structure introduced alongside this ADR). Native.sodisassembly assistance being newly in scope is a deliberate, separately-recorded decision (this ADR's §4), not an incidental scope expansion.
ADR-018 — DLCI 0x02 confirmed as the companion app's own internal RFCOMM channel (SDP UUID + APK-code correlation, 3 independent captures); channel ownership promoted to FACT, Sent-payload content remains HYPOTHESIS
Maintainer sign-off obtained 2026-08-30 (session record: maintainer selected "Option 2" from the options below). This entry was originally drafted by an AI session as a labeled proposal (
Status: Proposed) perAGENTS.md§6, and is updated in place — not stacked as a new entry — now that the maintainer has reviewed and decided, per this file's non-destructive-update convention.
- Date: 2026-08-30
- Status: Accepted — Option 2 (narrow promotion)
- Context:
PROTOCOL.md§2.2a already promoted DLCI 0x02's framing mechanism (HDLC flag/ escape/LEB128-address/CRC-32) to 🟢 FACT (2026-08-12,pbpctrl-notes cross-reference + 640/640-subframe CRC verification acrossCAP-001–CAP-003). What §2.2a/§2.3 explicitly left at 🟡 HYPOTHESIS (strong) is a narrower claim: that this specific channel islibmaestro's own settings channel, as opposed to some other Pigweed-RPC-based Google service sharing the same framing library. §2.2a states two paths to close that gap: (a) decode the opaque "Sent"-direction payload bytes and recognize an actuallibmaestromethod call, or (b) an isolated single-action capture correlating one "Sent" write to one specific user action. Neither had happened yet. An AI-run §4 keyword-search pass overv1.0.955078536-10253511's decompiled APK (DECISIONS.mdADR-017's mechanical-assistance boundary; full write-up inREVERSE_ENGINEERING.md'sfzd/gbm/gau/gbd/fxm/fsz/fut/fux/ghd/goqentries) found the app's own RFCOMM-socket-selection logic:gbm.java:35-43picks between two internal RFCOMM sockets by checking which of two 128-bit UUIDs (each present in both a canonical and a byte-reversed form,fzd.java:9) is in the discovered SDP UUID set, logging"Provide pigweed internal rfcomm socket"for UUID25e97ff7-24ce-4c4c-8951-f764a708f7b5and"Provide default internal rfcomm socket"for a second, distinct UUID (3a046f6d-24d2-7655-6534-0d7ecb759709). Separately,fsz.java:223— a Kotlin function-reference metadata string that survived R8 renaming — literally names the app's owncom.google.android.apps.wearables.maestro.companion.pw.hdlc.RouteProto$Routeclass and the upstreamdev.pigweed.pw_rpc.MethodClientclass, andfux.java/fxm.java/others enumerate realmaestro_pw.*pw_rpc services (Maestro,HeadGesture,EartipFitTest,Dosimeter,JitterBuffer,Multipoint,DynamicServerConfigService) called through this same selection path. This "pigweed" UUID was then checked against 3 independent captures already incaptures/(CAP-001,CAP-002,CAP-032;bluetooth.addr == 04:00:6e:cf:6e:07): in every session, the SDP Service Search Attribute Response lists25e97ff7-24ce-4c4c-8951-f764a708f7b5, its Protocol Descriptor List response resolves it to RFCOMM server channel 1, andtshark's ownbtrfcomm.dlcifield reads0x02for every frame once that channel opens (CAP-001frame 1334 @ 42.545s;CAP-032frame 1645 @ 105.173s) — a direct wire reading, not the2×channelarithmetic applied blind. Full frame/timestamp citations are inREVERSE_ENGINEERING.md'sgbmentry and §UUID register. The second, "default"-labeled UUID (3a046f6d-...) was searched for (both byte orders) across all 23 raw*btsnoop_hci.logfiles undercaptures/and found in none of them — an open question, not explained by this pass. - What this new evidence is, precisely — and what it is not: it establishes, from the app's own
compiled selection logic plus a reproducible SDP/RFCOMM wire correlation, that DLCI 0x02 is the
specific RFCOMM channel this companion app itself selects and labels "pigweed," and that the app
calls real
maestro_pw.*pw_rpc services (includingWriteSetting) through that same selection path. It does not decode the opaque "Sent"-direction payload bytes on DLCI 0x02, and does not correlate one specific "Sent" write to one specific user action — i.e. it does not satisfy either of §2.2a's two originally-stated paths (a)/(b) in the form they were written. It is a third, independent evidentiary path: static app-code correlation via the SDP layer, rather than payload decoding or capture isolation. - Options considered (maintainer's choice, not decided by this proposal):
- Promote fully: treat this SDP+code correlation as sufficient to move DLCI 0x02's
channel-identity claim ("this is
libmaestro's channel") from 🟡 HYPOTHESIS (strong) to 🟢 FACT inPROTOCOL.md§2.2a/§2.3, on the reasoning that tying the DLCI directly to the app's own compiled selection logic and self-identifying log string is at least as strong as decoding one opaque payload would be. - Promote narrowly (mirrors ADR-013's precedent of promoting only what's cleanly warranted):
record as 🟢 FACT only that this RFCOMM channel is the companion app's own internal channel,
distinct from any other/generic Pigweed-based service — leave "and its Sent-payload content is
specifically
libmaestro's ANC/EQ/settings commands" at 🟡 HYPOTHESIS (strong) pending §2.2a's original paths (a)/(b). - Do not promote: keep §2.2a/§2.3's status text exactly as-is, and append this SDP+code
correlation to
PROTOCOL.mdpurely as additional strengthening evidence for the existing 🟡 HYPOTHESIS (strong) label, explicitly reserving promotion for actual payload-content decoding or an isolated single-action capture.
- Promote fully: treat this SDP+code correlation as sufficient to move DLCI 0x02's
channel-identity claim ("this is
- Decision: Option 2, accepted. Per
ARCHITECTURE.md§2.1/PROJECT_RULES.md§1's promotion rules,PROTOCOL.mdis updated to record 🟢 FACT that DLCI 0x02 is the Pixel Buds companion app's own internal RFCOMM channel — distinct from any other/generic Pigweed-based service — based on the SDP UUID (25e97ff7-24ce-4c4c-8951-f764a708f7b5) the app's own code (gbm.java/fzd.java) selects and labels "pigweed internal rfcomm socket," confirmed on the wire as RFCOMM channel 1 = DLCI 0x02 acrossCAP-001/CAP-002/CAP-032. Not promoted: that this channel's Sent-direction payload content specifically carrieslibmaestro's ANC/EQ/settings commands — that stays 🟡 HYPOTHESIS (strong), pending §2.2a's original paths (a) decoding the opaque payloads via a pw_rpc/protobuf schema, or (b) an isolated single-action capture. SeePROTOCOL.md§2.2a ("Channel ownership" finding), §2.3's three-channel table, the 2026-08-14 addendum's Status line, and §4.2's EQ entry — all updated together for consistency, since they restate the same underlying claim. - Consequences:
ARCHITECTURE.md§2.1's per-channel implementation gate is not unblocked forFrameEncoder/FrameDecoderwork against DLCI 0x02's actual settings semantics — the opaque "Sent" payload content remains undecoded; only the channel-identity question is settled. This does give future work a firmer footing to state "this islibmaestro's own channel" without hedging, when discussing which channel to target for payload-decoding work (§2.2a's paths (a)/(b)). Neither DLCI 0x08's still-🔴 open identity question nor the "default internal rfcomm socket" UUID's unexplained absence from every capture searched so far is affected by this decision.
ADR-019 — qhr's oneof structure confirmed inside DLCI 0x02's field5{field4{...}} wrapper (2 sampled fields); qhr fields 4 and 7 promoted to FACT; qhr field 12's field-number identity (not its name) promoted to FACT
-
Date: 2026-08-30
-
Status: Accepted
-
Context: a 2026-08-30 session combined (a) a Tier 0 re-decode of existing captures against a same-day APK static-analysis pass that recovered
libmaestro's realWriteSettingrequest schema (REVERSE_ENGINEERING.md'sqjc/qja/qhr/qjo/qju/qjg/qhtentries), and (b) a Tier 2 static-analysis pass tracingqhr's remaining write/read call sites.ADR-013had promoted only DLCI 0x02's outerfield5{field4{...}}wrapper shape to FACT, explicitly leaving the "..." itself undecoded;ADR-018(Option 2) separately promoted the channel's ownership to FACT while leaving its payload content at 🟡 HYPOTHESIS (strong). This session's findings were presented to the maintainer as four discrete candidate promotions (session of 2026-08-30); the maintainer reviewed each individually and approved all four, three as proposed and the fourth in its narrower form (field-number identity only, not the semantic name), perAGENTS.md§6's requirement that an agent may propose but never unilaterally commit a FACT promotion. -
Findings being recorded:
- DLCI 0x02's
field5{field4{...}}wrapper's inner content, for the two fields sampled, isqhr's own protobuf oneof, addressed via standard wire-format tags — not merely "plausible" perADR-013's own note. Two existingCAP-020Sent frames already identified asfield5{field4{...}}(CAP-020-FINDINGS.md§3/§4, frames 1741/1935 —TOUCH-001/HEAD-001) were re-pulled directly from the raw log, HDLC-unescaped/CRC-verified, and decomposed one level further than that file's own original decode. Frame 1741's inner bytes decode toqhrfield 4, value1; frame 1935's toqhrfield 29, value2— an exact, byte-for-byte match to the independently-derived (APK code, not wire)qhrschema, on both sampled fields, no counter-example. This is two independent evidence paths (real wire bytes vs. compiled app code) converging on the same structure, not one path repeated. qhrfield 4 = the "Use touch controls" master enable toggle (PROTOCOL.md§4.5.3's top-level toggle). Evidence:CAP-020frame 1741 (wire+video correlation,TOUCH-001, already 🟡 HYPOTHESIS) and, independently, the app's own code — write sitefyo.java:124-144, read sitefxb.javacase 4 logging"Log Gestures Enable setting"(self-describing, not a naming inference).qhrfield 7 =qju= the Left/Right press-and-hold gesture-action customization (ANC / Digital assistant / None), matchingPROTOCOL.md§4.5.3's already-strong press-and-hold HYPOTHESIS. Evidence:CAP-021frames 1895/3619/4315/4976 (HOLD-001–HOLD-004, all 4 of the 2×2 Left/Right × ANC/Assistant combinations, wire+video correlated) and, independently, the app's own code — write sitefyo.java:300-374(t(gdx)), read sitefxb.javacase 7 logging"Log Gestures Customization for touch and hold setting, left: %s, right: %s"— a literally self-describing match, not an inference from shape or position. Re-decoding the 4 wire frames also surfaced a nesting level finer thanCAP-021-FINDINGS.md's original notation: the value sits inside aqju.field{1|2}→qik→qhochain, not a bare varint directly underfield1/field2— recorded as a correction to that file's own decode, not a new claim.qhrfield 12 =qht— the field-number identity only. Evidence:CAP-021frames 5237/5247/5255 (HOLD-005) decode toqhrfield 12 with exactly 4 boolean sub-fields, matchingqht's independently-confirmed shape (APK code:qhr's field-12 alternative, write sitehgj.java:216-331, read sitefxb.javacase 12 logging"Log ANC gesture loop to Clearcut"). Not promoted: whether the app's own internal name for this field, "ANC gesture loop," is the same UI feature asPROTOCOL.md§4.5.3's existing "ANC-mode rotation checklist" (HOLD-005) HYPOTHESIS — the two could describe the same setting seen from two angles, or two different settings that happen to share a 4-boolean shape; this has not been reconciled, and the maintainer explicitly declined to promote that equivalence at this time.
- DLCI 0x02's
-
What this ADR does NOT clear:
- The nesting-structure finding (1) is sampled on exactly 2 fields (4 and 29) in one capture
session — it establishes that the "..." is
qhr's oneof for those two instances, not that every one ofqhr's 38 fields has been wire-confirmed to decode this way. It does not by itself resolveADR-018's own remaining HYPOTHESIS (that DLCI 0x02's Sent-direction content specifically carrieslibmaestro's settings-write commands in general) — it substantially strengthens that HYPOTHESIS for the specific fields tested, butADR-018's broader claim is not re-litigated or promoted by this entry. - Finding (2)'s field 4 is one-direction (OFF→ON) only, one session.
- Finding (3)'s 4/4 combination coverage is strong, but still one capture session for the wire half; the code half (the self-describing log message) is a separate, independent confirmation type, not a second capture.
- Finding (4) explicitly does not promote
qht's app-internal name or its equivalence to the rotation-checklist HYPOTHESIS — only that wire field 12 = code'sqht(a field-number/shape match). qhrfield 29 (Head gestures,PROTOCOL.md§4.5.4) was also re-confirmed at the wire level this session (frame 1935) but is not part of this ADR — no self-describing code-side name was found for field 29 (its write call site was not located by the static pass), so only one evidence path exists for it; it remains 🟡 HYPOTHESIS, unchanged.
- The nesting-structure finding (1) is sampled on exactly 2 fields (4 and 29) in one capture
session — it establishes that the "..." is
-
Decision: findings 1, 2, and 3 above are accepted as 🟢 FACT in full. Finding 4 is accepted as 🟢 FACT for the field-number identity (
qhrfield 12 =qht) only; the "ANC gesture loop" / "ANC-mode rotation checklist" naming equivalence remains 🟡 HYPOTHESIS. -
Consequences:
PROTOCOL.md§4.5.3 updated — the top-level toggle and press-and-hold-action opcodes move from 🟡 HYPOTHESIS to 🟢 FACT; the rotation-checklist opcode gains a FACT-confirmed field number but keeps its HYPOTHESIS status for what the field represents.PROTOCOL.md§2.2a/§2.3 and §6's "what do DLCI 0x02's confirmed inner field numbers actually represent" open item are updated to record that, for the 3 fields tested, the answer is "realqhrprotobuf field numbers from the app's own recovered schema," narrowing (not fully closing, per the scope note above) that question. Does not unblockARCHITECTURE.md§2.1'sFrameEncoder/FrameDecoderimplementation gate for DLCI 0x02 generally — that still requires the broader payload-content HYPOTHESIS inADR-018to reach FACT, which this ADR narrows but does not itself complete. -
Update (2026-09-03): clarifying how findings 2-4 and the field-29 exclusion above were actually decided, after a later summary compressed the reasoning into a single "a self-describing code site exists" test. That compression loses a distinction this ADR relied on — two separate dimensions were in play, not one: a. Whether an independent code-side evidence path exists at all. Field 29's exclusion (above) was not "a code site existed but wasn't self-describing" — this ADR's own text is explicit that "no self-describing code-side name was found for field 29 (its write call site was not located by the static pass), so only one evidence path exists for it." The write call site itself was never located; there was no code-side path to evaluate for self-description in the first place. A field whose code path simply hasn't been found yet is not evidence-equivalent to a field whose code path was found and found generic or unnamed — anyone re-running this method on further fields should keep that distinction. b. A self-describing code path existing does not, by itself, clear the bar for full semantic promotion. Field 12 is this ADR's own counter-example: its read site's log message ("Log ANC gesture loop to Clearcut," finding 4) is self-describing, and yet only the field-number identity was promoted, not the full semantic claim — because the code's own name ("ANC gesture loop") was not reconciled with the pre-existing "ANC-mode rotation checklist" HYPOTHESIS the wire evidence had already proposed. Full semantic promotion, as granted in full to findings 2 and 3, requires the self-describing code name to match, or be explicitly reconciled with, whatever hypothesis-level name already existed — not merely to exist.
Neither point changes findings 1-4, "What this ADR does NOT clear," or the Decision/Consequences above — this note only corrects a compressed restatement of reasoning already used to reach them.
-
Update (2026-09-03): five further
qhrfield promotions, reviewed and approved by the maintainer individually per field, applying this ADR's own evidence standard (independent wire-capture evidence plus independently-traced app code, cross-validated) and the two-dimension distinction clarified in the note directly above.- Field 17 = "Volume balance" — full identity, 🟢 FACT. Evidence:
CAP-022-FINDINGS.md§5 (7 wire samples across one continuous drag gesture —CAP-022frames 1922/1944/2019/2039/2056/ 2073/2099, all CRC-32 verified, raw hex backfilled this session) and, independently, the app's own code — write sitefxf.java:82-133(case 16 of that dispatcher), read side logging"received last saved volume balance setting value"(fxb.javacase 17) — a self-describing match to the pre-existing "Volume balance" HYPOTHESIS, satisfying dimension (b) above. Correction accompanying this promotion:qhr's own schema types field 17 asSINT32(REVERSE_ENGINEERING.mdline 856), so its wire values must be zigzag-decoded, not read as raw unsigned varints. The 7 sampled values were previously recorded as199, 123, 49, 30, 150, 200, 10; correctly zigzag-decoded ((n>>1) ^ -(n&1)) they are-100, -62, -25, 15, 75, 100, 5. The scale/range beyond these 7 samples and which direction (Left/Right) corresponds to negative vs. positive values remain 🔴 open — this correction narrows, but does not resolve,PROTOCOL.md§6's existing open item on this field. - Field 19 = "Mono audio" — full identity, 🟢 FACT. Evidence:
CAP-022-FINDINGS.md§3 (CAP-022frames 1621/1823, both directions, CRC-32 verified) and, independently, the app's own code — write sitefyo.java:278-298(s), read side logging"received mono setting value"(fxb.javacase 19) — a self-describing match to the pre-existing "Mono audio" HYPOTHESIS. - Field 22 =
qhr's own "Speech Detection" — field-number/type identity only, 🟢 FACT. Evidence:CAP-019-FINDINGS.md§3 (single OFF→ON sample) and, independently, the app's own code — write sitehnz.java:29-49(a, logging"Set Speech Detection"), read sidefxb.javacase 22. Per dimension (b) above: the code's own name, "Speech Detection," is not the same string asPROTOCOL.md§4.5.1's pre-existing "Conversation Detection" UI-label HYPOTHESIS — plausibly the same feature seen from two angles (an internal/engineering name vs. the UI's own label), but not reconciled. The maintainer reviewed this specifically and declined to promote that equivalence; it remains 🟡 HYPOTHESIS. - Field 27 = a real, code-confirmed case-sound-family boolean — category level only, 🟢 FACT.
Evidence:
CAP-024-FINDINGS.md§5 (CAP-024frames 2053/2084, both directions, CRC-32 verified, raw hex backfilled this session) and, independently, the app's own code — write sitefyo.java:80-100(e), read side logging"received case earcon setting value"(fxb.javacase 27). The code's own log message confirms this is a case-sound-category setting but does not itself distinguish which one — the specific "Other alerts"/"Other notifications" label (CAP-024's ownCASE-002test) remains 🟡 HYPOTHESIS, unreconciled with the generic code-side name, per dimension (b) above. - Field 28 = "Bud return"/"Earbuds replaced" — full identity, 🟢 FACT. Evidence:
CAP-024-FINDINGS.md§4 (CAP-024frames 1988/2023, raw hex backfilled this session) and, independently, the app's own code — write sitefyo.java:58-78(d), read side logging"received bud return sound setting value"(fxb.javacase 28) — a self-describing match to the pre-existing "Bud return" HYPOTHESIS.
As with the original four findings, none of these five promotions change the outer envelope's own already-FACT status (
DECISIONS.mdADR-013) or unblockARCHITECTURE.md§5's per-command implementation gate for any field beyond the ones explicitly promoted here — fields 11 and 15 are unaffected by this update and remain 🟡 HYPOTHESIS. - Field 17 = "Volume balance" — full identity, 🟢 FACT. Evidence:
-
Update (2026-09-08, maintainer sign-off via the chat session that authored prompt
ai-sessions/0003_MAINTENANCE_PROMPT_2026_09_08.md, implementing that session's Phase 3 item 1 finding): a sixth field promotion, applying this ADR's own evidence standard.- Field 2 = "CATEGORY_OHD" (On-Head/In-ear Detection) — category-level identity, 🟢 FACT.
Evidence:
CAP-024-FINDINGS.md§3 (CAP-024frames 1850/1912, both directions, video-confirmed) and, independently, the app's own code — the already-known write site (fyo.java:169-188, methodl) is also reached fromMaestroDeviceSettingsProviderServicecase2102(the system Settings app's own Bluetooth-device-details page), logged there under the internal category name"CATEGORY_OHD"(fjm.H(14), a self-describing internal settings-taxonomy name, a source type not previously used for anyqhrfield promotion). Per dimension (b) of the 2026-09-03 clarifying note above: the code's own name ("OHD"/"On-Head Detection") is closely related to, but not verbatim identical to,PROTOCOL.md§4.5.5's pre-existing "In-ear detection" UI-label HYPOTHESIS — the same kind of gap that kept fields 12/22/27 at category/field-number-level identity rather than full semantic identity. Promoted for field-number/category-level identity only; the specific "In-ear detection" label equivalence remains 🟡 HYPOTHESIS.
- Field 2 = "CATEGORY_OHD" (On-Head/In-ear Detection) — category-level identity, 🟢 FACT.
Evidence:
- Date: 2026-09-03
- Status: Accepted
- Context:
ADR-016(2026-08-28) promoted EQ's wire envelope shape, its field-to-band mapping, the ±6.0 band-gain clamp, and the confirmed preset quintets to 🟢 FACT (PROTOCOL.md§4.2). UnlikeADR-009(ANC) andADR-011(Find My Buds Left/Right), which each explicitly state that theARCHITECTURE.md§5 per-command implementation gate is cleared,ADR-016never made the equivalent statement for EQ — a 2026-09-02 documentation audit flagged this as a gap: EQ's protocol knowledge is fully FACT-level, but its implementation-readiness status was left ambiguous rather than explicitly settled. The maintainer reviewed this gap directly (session of 2026-09-03) and explicitly instructed that it be closed via a new ADR, matchingADR-009/ADR-011's pattern rather than an in-place edit toADR-016's own text (PROJECT_RULES.md§3 rule 9's non-destructive-update convention). - Finding being recorded: none new — this ADR does not add any protocol knowledge. It records
the maintainer's explicit decision that the FACT-level findings
ADR-016already promoted (5×float32band-gain quintet on DLCI 0x02'sfield5{field4{...}}envelope, field 1↔Low bass / 2↔Bass / 3↔Mid / 4↔Treble / 5↔Upper treble, wire order reversed from on-screen order, ±6.0 clamp, and the six confirmed preset quintets) are sufficient, on their own, to unblock implementation. - What this ADR does NOT clear: EQ's outer field 16 vs. field 18 distinction (
PROTOCOL.md§4.2/§6 — "live value" vs. "persisted value," and whether that maps to "preview" vs. "slider-release"/"commit") remains 🟡 HYPOTHESIS, unaffected by this ADR. An implementation needs to pick one field for a given write; perPROTOCOL.md§4.2's own code-derived reading (REVERSE_ENGINEERING.md'sqjwentry: field 16 =fyp.f(), "update user eq," fired on every slider-drag value change and on preset selection; field 18 =fyp.d(), "update last saved user eq," fired once per gesture and also persisted locally), field 16 is the correct target for a live/preview-style write — this ADR does not promote that reading to FACT, it only notes it as the practical default for an initial implementation. The gain unit (plausibly dB, never independently confirmed) and the ~13-byte correlation-ID/call_idregion also remain unconfirmed, unaffected. - Decision: EQ's
FrameEncoder/FrameDecoderimplementation is unblocked, perARCHITECTURE.md§5's per-command implementation gate, for the elementsADR-016already promoted to FACT (the envelope wrapper, the 5-band quintet and its field-to-band mapping, the ±6.0 gain clamp, and the preset quintets). - Consequences:
:datacan implement EQ'sFrameEncoder/FrameDecodernow, against fixed byte-array fixtures perAGENTS.md§11, using field 16 for live/slider-drag writes as the practical default described above. The field-16-vs-18 semantic question and the gain-unit question remain open research items (PROTOCOL.md§6,TODO.md) and should be resolved before EQ ships a "Save as preset"-style UI affordance that specifically depends on field 18's exact semantics.
ADR-021 — "Get ANC state" (0x11) opcode identity confirmed on the wire for the first time (DLCI 0x04); trigger-reliability explicitly NOT promoted
- Date: 2026-09-04
- Status: Accepted
- Context:
PROTOCOL.md§4.1 has documented Message Group0x08Code0x11("Get ANC state", Seeker→Provider) since the group was resolved from the official Fast Pair Hearable Controls spec (2026-08-12) — but no capture had ever observed a0x11frame on the wire; only0x12(Set) and0x13(Notify) had been seen (CAP-001-FINDINGS.md§5).CAPTURE_BLUETOOTH_HCI_SNOOP.mdGroup AC /TESTPLAN_BLUETOOTH_HCI_SNOOP.mdOBS-004was designed specifically to isolate whether the official app ever issues this (or any) settings-state query, on two candidate triggers: reconnection and settings-screen-open.CAP-036(2026-09-04) ran that isolation and, in a clean reconnect window, found08 11 00 00(Sent, DLCI 0x04, frame 1169) fired 34ms after the channel opens, answered ~10.7ms later by08 13 00 04 01 e8 00 20(Rcvd, frame 1182 — decodes to current ANC state = Off), which matched the on-screen ANC state confirmed later in the same session. The maintainer reviewed this finding directly (session of 2026-09-04) and explicitly approved promoting the opcode's identity to FACT, while declining to promote the broader trigger-reliability claim from a single sample. - Finding being promoted:
PROTOCOL.md§4.1'sGet ANC state(0x11) opcode — its exact Group/Code values, its zero-length/no-payload structure, its Seeker→Provider direction, and that it is real, observed wire traffic (not merely a documented-but-theoretical spec entry) — is now 🟢 FACT, on the strength of an exact structural match to the official spec plus an internal content cross-check within the same capture (the Notify response's decoded value matching on-screen ground truth) — the same evidentiary pattern already used for0x12's promotion (PROTOCOL.md§4.1, this document's earlier ADRs). - What this ADR explicitly does NOT promote: whether this query reliably fires on every
reconnection (this is a single sample from one session —
CAP-036ran exactly one reconnect). This trigger-reliability claim remains 🟡 HYPOTHESIS pending replication in a second, independent capture. Also not promoted:CAP-036's clean-negative finding that no query of any kind occurs on settings-screen-open (five clean windows, one session) — that stays 🟡 HYPOTHESIS for the same reason. Also not resolved: theSettable togglesbyte inCAP-036's Notify frame reads0x00, differing from every previously-documented Set frame's0xe8in the same position — left as an open question (PROTOCOL.md§6), not reconciled or promoted by this ADR. - Decision:
PROTOCOL.md§4.1's "Get ANC state" (0x11) opcode entry is promoted to 🟢 FACT for its identity/structure as described above. The trigger-reliability and settings-screen-open negative-result claims from the same capture remain 🟡 HYPOTHESIS, unaffected by this ADR. - Consequences: a future
FrameEncoder/FrameDecoderimplementation of the ANC read path (if and when this project's own app wants to issue an equivalent read-on-reconnect query perARCHITECTURE.md§3.1) can now target a confirmed opcode rather than a spec-only placeholder. Implementation should not yet assume the query is guaranteed to appear on every reconnect in the wild (the reliability question is still open) — a second capture reproducing this pair is a recommended, low-cost next step (CAP-036-FINDINGS.md§11's replication proposal) before treating the trigger itself as dependable.
ADR-022 — "Get ANC state" (0x11) trigger-reliability promoted to FACT: 17 occurrences across 10 independent captures, zero misses
- Date: 2026-09-04
- Status: Accepted
- Context:
ADR-021promoted this opcode's identity to FACT from a singleCAP-036sample, while explicitly declining to promote whether it reliably fires on every reconnect — that required more evidence than one session could provide. The maintainer subsequently asked for a bonus battery/firmware analysis pass across other existing captures (two rounds,DESKRESEARCH_FINDINGS.md's 2026-09-04 entries); the second round specifically targeted seven settings-toggle sessions (CAP-019–CAP-025) that had never been checked for this opcode, plusCAP-006andCAP-010. - Finding being promoted: across
CAP-006(×3),CAP-010(×2),CAP-016(×1, from the first bonus round),CAP-019–CAP-024(×1 each),CAP-025(×5), andCAP-036(×1) — 17 total occurrences across 10 independent capture files —08 11 00 00(Sent, DLCI 0x04) fires and is answered by08 13(Rcvd) within tens of milliseconds, every single time, under a precisely identified trigger condition: DLCI 0x04 (re)establishes (SABM→UA) and subsequently carries real Message Stream payload. This holds even when the underlying classic ACL link does not itself disconnect/reconnect —CAP-006andCAP-025each show the query re-firing on a DLCI-0x04-only channel bounce within one continuous ACL connection (CAP-025shows this 5 times in one log, confirmed via its own single, unbroken HCIConnection Complete). The negative control also holds:CAP-025additionally contains 3 bareSABM→UA→DISCchannel bounces carrying zero payload, and none of those trigger a newGet— the trigger condition is precise, not "any DLCI 0x04 activity." - Evidentiary bar met: 17 occurrences / 10 independent sessions / zero misses against a
precisely-scoped condition exceeds the sample size this project has previously required for FACT
(
ADR-009: 4 samples in one capture;ADR-014: 4 independent sessions). - What this ADR does NOT promote: the Settable-toggles byte's own meaning (still 🟡 HYPOTHESIS,
PROTOCOL.md§4.1 — now read as tracking whether the Buds are in/near the case rather than connect-timing, per the same bonus analysis, but not maintainer-reviewed for promotion);CAP-036's settings-screen-open clean-negative result (a different sub-question, unaffected); any claim about why the query fires on this trigger (mechanism/purpose not investigated). - Decision:
PROTOCOL.md§4.1's "Get ANC state" (0x11) trigger-reliability claim — "fires whenever DLCI 0x04 (re)establishes and carries real Message Stream payload, independent of whether the underlying classic link itself reconnects" — is promoted to 🟢 FACT. - Consequences: this project's own
ARCHITECTURE.md§3.1 (State Reconciliation) design — query hardware state on every (re)connection before trusting a cached value — is now confirmed to match a real, reliably-observed behavior of the official app for ANC specifically, not merely a single-session anecdote. AFrameEncoder/FrameDecoderimplementing this specific read (if pursued) can rely on the trigger condition described above.
ADR-023 — Retroactive sign-off: Option C (HFP battery) confirmed independent of GMS/app, and confirmed on GrapheneOS
- Date: 2026-09-05
- Status: Accepted
- Note on process: this ADR closes a self-caught process gap, the same pattern as
ADR-016. While writing upDESKRESEARCH_FINDINGS.md's 2026-09-04 bonus battery/firmware cross-check, the two findings below were marked 🟢 FACT directly inPROTOCOL.md§4.3 Option C without first obtaining the explicit maintainer sign-offAGENTS.md§6 requires for every FACT promotion, no exceptions. This was caught by the agent itself on a later pass (not flagged by the maintainer) and surfaced explicitly in the next session (2026-09-05) rather than left standing uncorrected. The maintainer reviewed both findings directly in that session and explicitly approved recording them as FACT retroactively, rather than reverting them to HYPOTHESIS pending a separate review. - Findings being recorded:
- Option C (HFP
AT+BIEV=2,<value>battery reporting) is independent of both Google Play Services and the official companion app (PROTOCOL.md§4.3 Option C). Evidence:CAP-004(DESKRESEARCH_FINDINGS.md2026-09-04 entry) — GMS disabled and the official app uninstalled together (the strongest independence condition of any capture on disk) —AT+BIEV=2,100still fires normally, multiple times.CAP-033— the official app force-stopped for the entire session, GMS untouched —AT+BIEV=2,100also fires normally, multiple times. Both are simple, unambiguous binary observations (the AT command is present in the log or it is not) rather than an interpretive reading, extendingCAP-035-FINDINGS.md's existing GMS-independence result (which only checked DLCI 0x08/0x0a/0x06/0x12) to Option C specifically. - Option C also works on GrapheneOS itself, not only stock Android (
PROTOCOL.md§4.3 Option C). Evidence:CAP-035(Pixel 9a/GrapheneOS, GMS present butdumpsys-verified disabled, no official app, no nRF Connect) —AT+BIEV=2,100fires on both the fresh connect and the later reconnect in that session. Same evidentiary character as finding 1: a direct, unambiguous presence/absence observation.
- Option C (HFP
- What this ADR does NOT clear: no other finding from either bonus-analysis round is affected
— the Settable-toggles "in/near-case vs. actively-worn" reading (
PROTOCOL.md§4.1) remains 🟡 HYPOTHESIS (correlational, not reconciled against any documented field meaning, and the "actively worn" status for several sessions is inferred from the session's own procedure rather than directly video-verified per sample) — not promoted by this ADR, and not proposed for promotion this round; theCAP-027cross-channel-sync-caveat and theCAP-036BLE device-attribution advance likewise remain untouched, single-session HYPOTHESES. - Decision: both findings above are accepted as 🟢 FACT, as already written in
PROTOCOL.md§4.3 Option C. - Consequences:
ARCHITECTURE.md§4's battery-fallback priority order can rely on Option C (HFP) as a mechanism that does not depend on Google Play Services or the companion app being installed/running, and functions on GrapheneOS specifically — directly relevant to this project's Zero-GMS goal (AGENTS.md§1) and its GrapheneOS target platform (AGENTS.md§2).
ADR-024 — "Notify ANC state" Settable-toggles byte confirmed as a dock-state indicator: 0x00 when both earbuds are seated in the case, 0xe8 otherwise
- Date: 2026-09-05
- Status: Accepted
- Context:
CAP-036-FINDINGS.md§3 flagged the "Notify ANC state" frame'sSettable-togglesbyte reading0x00as an unreconciled discrepancy against every prior sample's0xe8.DESKRESEARCH_FINDINGS.md's first bonus round foundCAP-016-FINDINGS.md§4 had already observed the same0x00value, with its own 🟡 HYPOTHESIS that it tracks whether the Buds have "reported which ANC modes are currently selectable" — plausibly tied to dock state. The second bonus round found 12 more samples (7 sessions) all showing0xe8, each in a session where the Buds were presumed (not directly checked) to be actively in use — sharpening the hypothesis to "in/near-case vs. actively worn," still uncorroborated by direct video evidence for most samples. The maintainer asked for that video verification before considering promotion. - Finding being promoted: a dedicated video check (
DESKRESEARCH_FINDINGS.md2026-09-05 entry), usingffmpegframe extraction against each video's own wall-clock overlay, checked 3 new samples against their exact wire timestamps and found the case's dock state, not "worn" per se, is the determining factor:CAP-010,Settable=0x00— both earbuds visibly seated in the case's charging slots, LED lit (mid Fast-Pair "Save device to account" dialog).CAP-021,Settable=0xe8— case open, both slots empty (confirmed via a cropped/zoomed frame), Buds off-frame.CAP-025,Settable=0xe8— case open, both slots empty, both Buds visible resting loose beside the case (not docked, not necessarily worn either — refining "worn" to "not docked"). Combined withCAP-016's original frame (both Buds docked,0x00) andCAP-036's entire session (Buds sitting in the open case throughout, never removed,0x00— confirmed via that session's own full video re-pass), this is 5 of 5 video-checked samples confirming the same pattern, zero counter-examples, across 5 independently-run sessions with different procedures (a case/bud-removal test, a fresh-pairing repeat, two settings-toggle sessions, and a reconnect-isolation test).
- What this ADR does NOT clear:
CAP-006's own two samples (0xe8then0x00within one session) remain unverified —CAP-006-recording.mp4fails to open inffmpeg(stream 1, contradictionary STSC and STCO/error reading header) and no repair tool was available; this would have been the first within-session transition check and is a genuine gap, not a negative result.CAP-036's settings-screen-open clean-negative finding (a separate sub-question) is unaffected. The remaining 120xe8samples fromCAP-019/020/022–024were not individually video-checked this pass (their session type — active settings-toggle tests — is consistent with the pattern but not each individually confirmed frame-by-frame). - Update (2026-09-05, same day): the maintainer re-pulled
CAP-006-recording.mp4from the phone; the replacement file (79.49s, opens cleanly inffmpeg) coversCAP-006's first twoSettablesamples. Both video-confirmed:17:23:54.37(Settable=0xe8) — case open, both slots empty at video start and throughout;17:25:02.03(Settable=0xe8) — case still empty. Now 7 of 7 video-checked samples confirm the pattern, zero counter-examples (adds 2 to the 5 above).CAP-006's own third sample (17:26:55.06,Settable=0x00) remains unverified — the replacement file, like the original, ends at ~17:25:08, and the session's own log runs to 17:27:30, well past either video's coverage. The within-session0xe8→0x00transition this ADR's "what this ADR does NOT clear" section flagged is therefore still open, independent of the file corruption issue being resolved. - Decision:
PROTOCOL.md§4.1'sSettable-togglesbyte is promoted to 🟢 FACT as a dock-state indicator:0x00when both earbuds are seated in the case, a non-zero value (0xe8in every sample seen to date) otherwise. - Consequences: a future implementation reading this field can treat it as a live dock-state
signal from the accessory itself, independent of (and potentially more immediate than) the
case/bud-removal Bluetooth events
PROTOCOL.md§5/§7 already document from other channels — a candidate cross-check forARCHITECTURE.md's connection/dock-state model. The exact bit-level meaning of0xe8beyond "not both docked" (e.g. whether it varies further for one-bud-docked states) remains unexplored and is not claimed by this ADR. - Update (2026-09-13, maintainer sign-off,
ai-sessions/0015_MAINTENANCE_RESULT_2026_09_13.md): two counter-examples found in a single session, not yet reconciled.CAP-048(CAP-048-FINDINGS.md§5, a purpose-built repeat with continuous dock-state video) found two fresh classic reconnects (17:44:45,17:47:42) reportingSettable-toggles=0x00(docked) while the video, checked at essentially the same wire timestamp, shows the case visibly empty. Four other readings in the same session (including two same-chandle DLCI reopens, not fresh reconnects) are correct — this is not a reversal of this ADR's own dock-state-indicator finding, which the same session's other readings continue to confirm. 🟡 HYPOTHESIS, not confirmed: a fresh reconnect's own Get/Notify may occasionally return a value queried before the Buds' own firmware has settled on an already-changed physical state — offered as a testable direction only; does not by itself explain why the other four fresh reconnects in the same session read correctly. Implementations reading this field on a fresh reconnect specifically (as opposed to a same-chandle DLCI reopen) should treat it as usually, not unconditionally, reliable immediately after connection.
ADR-025 — Google Play Services (GMS) reverse-engineering is out of scope; DLCI 0x04/0x08 implementation proceeds clean-room, from wire evidence only
- Date: 2026-09-07
- Status: Accepted
- Context:
AUDIT_REPORT_2026-09-07.md§1.0 found, via exhaustive full-tree string/identifier searches across the companion app's entire decompiled source (12,545 files), no trace of the Fast Pair Message Stream (DLCI 0x04) or DLCI 0x08's private-envelope transport logic anywhere in this app's own code — noMessageStream/HearableControls/ANC-opcode literals, no"GSND"/capability- string matches, noSettable-togglesparse site. The only ANC-adjacent code found is a downstream domain-model sink (gck.java/gcl.java/eht.java) that receives an already-decoded value from elsewhere and caches it locally in a Room/SQLitedevice_infotable — strongly suggesting the actual transport (RFCOMM socket ownership, frame construction/parsing) for these two channels lives inside Google Play Services' own system-level Fast Pair/Nearby component, not in the companion app's own APK. That report explicitly deferred the resulting scope question, perADR-017's boundary (an AI session proposes, the maintainer decides): should this project bring GMS's own module into its reverse-engineering effort to close Q1–Q3 from the code side? A second, independent external review (ANTIGRAVITY_AUDIT_REPORT_2026-09-07.md, cross-validated inEXTERNAL_REVIEW_VALIDATION_2026-09-07.md) reached a similar-sounding conclusion but additionally mischaracterized it as an implementation-blocking "Impossibility" under the Zero-GMS rule — the validation pass found that framing contradicted by this project's own already-practiced architecture (see Consequences below). The maintainer has now made the underlying scope decision directly. - Options considered:
- Pull, decompile, and analyze the relevant Google Play Services module(s) the same way this project already treats the companion app, to locate DLCI 0x04/0x08's actual transport code.
- Leave GMS out of scope; continue implementing DLCI 0x04/0x08 exclusively from wire-capture evidence (and, for DLCI 0x04, the official public Fast Pair specification) — exactly as this project already does today for every command confirmed so far (ANC, Find My Buds, EQ), none of which has ever required a companion-app code cross-reference to reach 🟢 FACT status.
- Decision: the second option. Google Play Services reverse-engineering is explicitly out of
scope for this project, for reasons distinct from (and in addition to)
ADR-008's existing GMS-adjacent exclusions (Account Linking/Ownership Transfer/Accessory Non-Owner Service):- Legal/scale. GMS is a much larger, actively-updated, closed-source system component, not
"software the maintainer has personally installed for interoperability with hardware they own"
in the same narrow sense
PROJECT_RULES.md§8 rule 20 frames this project's existing APK analysis — decompiling it would be a materially different, larger undertaking than analyzing one companion app, with its own legal/scope questions this decision does not attempt to resolve. - Unnecessary. This project's own evidentiary chain for DLCI 0x04 has never depended on
companion-app code — every 🟢 FACT promotion for DLCI 0x04 traces to wire captures matched
against the official Fast Pair spec, never an APK file+line (
PROTOCOL.md§4.1). DLCI 0x08 is implemented the same way in principle (wire evidence + correlation, perAGENTS.md§13.6's zero-creativity rule) once its own Group/Code semantics are decoded. A decompiled reference was never the blocking dependency for either channel's ownFrameEncoder/FrameDecoderwork.
- Legal/scale. GMS is a much larger, actively-updated, closed-source system component, not
"software the maintainer has personally installed for interoperability with hardware they own"
in the same narrow sense
- Consequences:
- DLCI 0x04/0x08
FrameEncoder/FrameDecoderimplementation proceeds clean-room, from wire capture evidence alone (plus, for DLCI 0x04, the public Fast Pair spec) — exactly the same method already used for ANC (ADR-009), Find My Buds Left/Right (ADR-011), and EQ (ADR-020), none of which needed a companion-app code cross-reference to reach FACT/implementation-ready status. This is not a workaround forced by this decision — it is this project's proven, already-practiced method for exactly these kinds of channels;ARCHITECTURE.mdis updated with a short note recording this explicitly. AUDIT_REPORT_2026-09-07.md§1.0's Q1–Q3 "not found in this APK" results are treated as closed from the code side for this APK version — future work on DLCI 0x04/0x08 opcodes should not expect, or spend further effort searching for, a companion-app code citation for these two channels' own transport/framing.REVERSE_ENGINEERING.md's "Message Group / Code register" table is expected to remain empty for DLCI 0x04/0x08 specifically (its own header already scopes it to APK-derived values only) — a clarifying note is added there rather than leaving this looking like an oversight.PROJECT.md's non-goals gain a corresponding bullet citing this ADR.TODO.md's Phase 2 section records this decision explicitly rather than leaving the question implicitly open.
- DLCI 0x04/0x08
- Update (2026-09-08, maintainer sign-off via the chat session that authored prompt
ai-sessions/0002_MAINTENANCE_PROMPT_2026_09_08.md, implementingai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.mdPhase 1/Phase 4's approved proposals) — GMS-boundary finding strengthened, not weakened. A deeper Phase 1 search (structural AIDL/ServiceConnectionpattern search plus a full manifest read, going beyond the original audit's literal-keyword grep) found that Google Play Services' Fast Pair module is reachable from this companion app's own decompiled code, via two genuinely named, unobfuscated AIDL interfaces (com.google.android.libraries.bluetooth.fastpair.IFastPairDeviceDetailService,...fmd.IFastPairFmdProxyService) bound through Google's Chimera dynamic-module broker (com.google.android.gms.chimera.GmsBoundBrokerService). This does not weaken this ADR's decision or its "DLCI 0x04/0x08 transport code is absent from this APK" evidentiary basis — it strengthens it. The newly-found boundary carries only already-decoded objects (aTrueWirelessHeadsetbattery summary; anFmdRequest/FmdResponseconsent-flow pair) — not raw Message-Stream/private-envelope frame bytes — and an exhaustive sweep of everyqueryLocalInterface(...)call in this APK version (31 total) found no third, ANC/settings-shaped GMS interface. The original conclusion — that DLCI 0x04/0x08's actual frame construction/parsing lives inside GMS itself, not this companion app — is now supported by a positive architectural finding (a concrete, named, working example of exactly this kind of higher-level GMS boundary existing and being reachable) in addition to the original negative one (no transport code found). No change to this ADR's Decision or Consequences sections — this Update records the strengthening finding perPROJECT_RULES.md§3's non-destructive-update convention. SeeREVERSE_ENGINEERING.md'sijk/ijp/TrueWirelessHeadset/FmdWorkerentry andPROTOCOL.md§6 for the full trace. - Update (2026-09-08, same maintainer sign-off) —
qhrfields 11 (Multipoint) and 15 (Volume EQ) promoted to 🟢 FACT for full field-number/semantic identity. ApplyingDECISIONS.mdADR-019's same static-analysis method (a forward trace from a named UI fragment/preference key to the write call site, rather than the log-message-backward technique used for ADR-019's own fields) to the two fields explicitly flagged as still-unchecked inTODO.md's "Targeted research follow-ups": field 11 = "Multipoint" (MultipointFragment'skey_multipoint_main_toggletoggle →hiy.java:32's self-describing"Set device Multipoint as: %s"log →fyo.java:146-166) and field 15 = "Volume EQ" (hlv.java:2127's self-describing"Set volume eq: %s"log, gated on the literal Android preference-key string"volume_eq_switch"→fyo.java:376-396) — both readings match the pre-existing wire-derived HYPOTHESIS labels exactly, with no naming-equivalence gap of the kind that kept fields 12/22/27 at field-number-only status. SeePROTOCOL.md§4.5.2/§4.5.6 andREVERSE_ENGINEERING.md'sqhrentry (2026-09-08 update) for the full evidence.
ADR-026 — Volume Balance (qhr field 17) range and Left/Right polarity confirmed: ±100, +100=Left, -100=Right
- Date: 2026-09-13
- Status: Accepted
- Context:
DECISIONS.mdADR-019 already promotedfield 17's field-number/semantic identity ("Volume balance") to 🟢 FACT, explicitly leaving the numeric scale/range and which direction (Left/Right) corresponds to negative vs. positive values open (PROTOCOL.md§4.5.7/§6).CAP-046(Group AK, 2026-09-12) ran a dedicated isolated-extreme-position capture specifically to close this gap, andai-sessions/0012_CROSSCHECK_RESULT_2026_09_12.mdFinding 125 independently re-derived its wire-side values with an exact byte-level match on all 8 samples. This proposal was drafted inai-sessions/0013_FEATURE_RESULT_2026_09_13.mdPhase 4 and explicitly approved by the maintainer in the same chat session that authored that prompt. - Finding being recorded:
field 17's range clamps at exactly ±100.field17 = +100corresponds to the Volume Balance slider's Left extreme;field17 = -100corresponds to the Right extreme — the opposite of this project's own earlier, unstated assumption (CAP-022-FINDINGS.md§5 implicitly labeled its first negative sample "Left"). Evidence: 3 of 3 extreme-position samples video-confirmed (CAP-046-FINDINGS.md§2), zero counter-examples, independently re-derived at the wire level a second time (0012Finding 125, exact byte-level match on all 8 samples). - What this ADR does NOT clear: whether
field17scales linearly (or at all) between center and the ±100 extremes — untested, no intermediate-position sample exists in any capture to date (CAP-046-FINDINGS.md§4/§7). Thefield17/field19(Mono audio) timing correlationCAP-046-FINDINGS.md§3 also found is not covered by this ADR and stays 🟡 HYPOTHESIS. - Decision: the range (±100) and Left/Right polarity (
+100=Left,-100=Right) above are accepted as 🟢 FACT. - Consequences:
PROTOCOL.md§4.5.7 and §6's matching open item are updated to record the range/polarity as 🟢 FACT. A future EQ/Volume-Balance UI implementation can render the slider's Left/Right mapping without hedging — but should still clamp/interpolate defensively for intermediate positions, since linearity there remains unconfirmed and is not settled by this ADR.
ADR-027 — Find My Buds Case/"both simultaneously": ship v1 with Left/Right ring only, no local fallback
- Date: 2026-09-13
- Status: Accepted
- Context:
PROTOCOL.md§4.4's "Major structural finding" and §6 Behavior's matching open item established that Case ring and "ring both simultaneously" are reachable, in the official app, only via a separate Find Hub/Find My Device map-view flow that is account/cloud-mediated (video-confirmed "Connecting…" state, on-screen copy referencing "another device linked with your Google Account").PROTOCOL.md§4.4 confirms a checked negative — zero localGroup 0x04 Code 0x01(Ring) traffic occurs while this flow is active, across a ~2.5-minute observation window — and a later code-level trace (ai-sessions/0001_CROSSCHECK_RESULT_2026_09_07.mdPhase 3) found the companion app's own code constructs Find My Device Terms-of-Service accept/skip requests only, with no ring/play-sound trigger anywhere in its own decompiled source.TODO.mdandai-sessions/0013_FEATURE_RESULT_2026_09_13.mdPhase 5 both already framed this as "a genuine Zero-GMS scope trade-off... no capture or static analysis can resolve this, only a maintainer product decision can." The maintainer made that decision directly in the chat session that authoredai-sessions/0013_FEATURE_PROMPT_2026_09_13.md. - Options considered:
- Accept a Google Find Hub/account-mediated fallback for Case/"both" ring specifically — rejected:
this would require a GMS/Google-account dependency, exactly what this project's Zero-GMS goal
(
AGENTS.md§1,PROJECT.mdnon-goals) exists to avoid, for one sub-feature whose local wire mechanism this project has already checked and found does not exist. - Ship v1 without local Case/"both" ring support, documenting it as a permanent, explicit limitation — chosen.
- Accept a Google Find Hub/account-mediated fallback for Case/"both" ring specifically — rejected:
this would require a GMS/Google-account dependency, exactly what this project's Zero-GMS goal
(
- Decision: v1 ships with Left/Right Find My Buds ring only (already 🟢 FACT and
implementation-unblocked,
DECISIONS.mdADR-011). Case ring and "ring both simultaneously" are explicitly out of scope for this project, unless a future capture or protocol change finds a genuine local (non-GMS-mediated) mechanism — no such evidence exists today. - Consequences:
PROJECT.md's non-goals gain a corresponding bullet citing this ADR.TODO.md's Phase 1 open item andPROTOCOL.md§4.4/§6 Behavior are updated to record this as a closed scope decision rather than an open research question.:app's eventual Find My Buds UI screen should offer Left/Right controls only, with no "Case"/"both" affordance implying a capability this project does not provide.
- Date: 2026-09-13
- Status: Accepted
- Context:
ARCHITECTURE.md§10/§15 left dependency injection as an open architecture question between Hilt/Dagger and a manual, light service locator —AGENTS.md§1 already clarifies Hilt itself does not touchcom.google.android.gms.*and does not itself require Google Play Services, so it is not disqualified by the Zero-GMS rule on that basis alone.ai-sessions/0013_FEATURE_RESULT_2026_09_13.mdPhase 6 surfaced this to the maintainer, recommending Hilt (this project's own module graph —:appas composition root wiring four other modules together,ARCHITECTURE.md§2 — is exactly the shape Hilt/Dagger's compile-time DI is built to reduce boilerplate for). The maintainer approved this recommendation directly in the chat session that authoredai-sessions/0013_FEATURE_PROMPT_2026_09_13.md. - Options considered:
- Hilt/Dagger — chosen. Mature, widely-used, AndroidX-adjacent, compile-time DI; reduces
:app's own composition-root boilerplate across:domain/:data/:hardware/:ui. - Manual service locator — full independence from Google-authored build tooling, at the cost of hand-written wiring code across all five modules; rejected as a reasonable but non-preferred alternative given the maintainer's own priorities favor less boilerplate.
- Hilt/Dagger — chosen. Mature, widely-used, AndroidX-adjacent, compile-time DI; reduces
- Decision: Hilt is this project's dependency-injection framework, per
AGENTS.md§10's dependency policy (pinned version, justified, no network/analytics SDK bundled transitively — confirmed via./gradlew :app:dependencies, seeai-sessions/0013_FEATURE_RESULT_2026_09_13.md's Hilt-wiring update for the check). - Consequences:
:appbecomes a real Hilt composition root (@HiltAndroidAppApplication/@AndroidEntryPointMainActivity,@Module/@InstallInbindings forBudsTransport/BudsRepository).ARCHITECTURE.md§10/§15 updated to record this as decided, not open. Every future module needing a dependency graph entry uses Hilt's@Inject/@Providesconventions rather than a hand-rolled locator.
- Date: 2026-09-13
- Status: Accepted
- Context:
ARCHITECTURE.md§15 left the minimum supported Android API level open — compile/ target SDK was already fixed at API 34 (Android 14), but how far down the minimum should go for broader AOSP-ROM compatibility was undecided. The open item itself noted this wasn't blocked on a single API: the generic battery broadcast (ARCHITECTURE.md§4 option 0) needs API 31+, but the confirmed primary battery/ANC/EQ paths (Fast Pair advertisement, Message Stream, HFP, GATT) don't depend on it, andCompanionDeviceManager(DECISIONS.mdADR-005) only needs API 26. The maintainer decided directly, in conversation, that the minimum should simply match the already-fixed compile/target SDK rather than support a wider, lower floor. - Options considered:
- A lower minimum (e.g. API 26, the floor
CompanionDeviceManageritself needs) for broader AOSP-ROM/older-device compatibility, accepting that API 31's generic battery broadcast (a cheap supplementary check, not a required mechanism) degrades gracefully below that version. - API 34 (Android 14), matching compile/target SDK exactly — chosen. Simplest option: no
version-gated code paths anywhere in the app, and this project's primary reference platform
(GrapheneOS,
ARCHITECTURE.md§1) tracks current Android releases closely, so a lower floor buys little real compatibility benefit for this project's actual user base.
- A lower minimum (e.g. API 26, the floor
- Decision: minimum supported Android API level is 34 (Android 14), identical to compile/target SDK. No lower-API compatibility path is pursued.
- Consequences:
ARCHITECTURE.md§1/§15 updated to record this as decided, not open. The Android Gradle project'sminSdkis set to 34 across every module that declares one (:app,:hardware,:ui) — simpler than the API-26 floor used provisionally before this decision, since every Bluetooth/battery mechanism this project relies on is available well below API 34 anyway. This forecloses running on older Android versions/ROMs that can't be updated past API 33, a deliberate trade-off given this project's GrapheneOS-first target.
https://github.com/tedsluis/opencontrolpixelbudspro2/blob/main/DECISIONS.md - https://tedsluis.github.io/opencontrolpixelbudspro2/DECISIONS