# Android Auto Protocol (AAP / GAL) — Wire Specification **Status:** consolidated reverse-engineering reference · **Revision date:** 2026-09-25 This document specifies the wire protocol spoken between an Android phone and a car head unit — called **AAP** (Android Auto Projection) in Google's own documentation and **GAL** (Google Automotive Link) in the binaries and certificates. It is assembled from six independent reverse-engineering lineages plus one leaked official document, and it flags every place those sources disagree rather than silently picking a winner. It is written for someone implementing the **phone side** (the FOSS replacement for `com.google.android.projection.gearhead`), but the protocol body is descriptive and covers both directions. Sections carry a `> Phone-side note` callout where this project has made a concrete choice. ## Vocabulary Google's terms are used throughout, because the sources use them: | Term | Meaning | |---|---| | **MD** | Mobile Device — the phone. **This is us.** | | **HU** | Head Unit — the car's infotainment system. | | **AAP** | Android Auto Projection — the protocol. | | **GAL** | Google Automotive Link — the same thing, as named in binaries and the PKI. | | **DHU** | Desktop Head Unit — Google's development head-unit emulator. | | **AOAP / AOA2** | Android Open Accessory Protocol v2 — the USB transport underneath AAP. | Direction is always written `MD→HU` or `HU→MD`. **Note carefully:** the HU *initiates* the protocol (version request) and is the *TLS client*, while the phone *listens* and is the *TLS server*. Almost every intuition about which side is "the client" is backwards here. ## How to read the confidence tags | Tag | Meaning | |---|---| | `[NORMATIVE]` | Stated by the leaked Head Unit Integration Guide v1.3 — Google's own words. | | `[CONFIRMED]` | Agreed by ≥2 *independent* lineages, or by code verified against real hardware. | | `[LIKELY]` | Single good source, internally consistent, nothing contradicting it. | | `[CONTESTED]` | Sources disagree. Full evidence table inline; also listed in Appendix B. | ## Source fidelity — six lineages, and they are not independent Judging a claim requires knowing which lineage it came from. Treating the corpus as one pool of evidence produces false confidence, because several projects vendor each other's definitions. **1. GAL-extracted protobuf** — `previous-work/source/milek7-galdocs/protos.proto` (2104 lines, 186 messages, 54 enums), extracted from a real Google binary. It is **byte-identical** to `previous-work/source/github/opencardev-aasdk/docs/protos.proto`. This is the **backbone of this document**: the only single artifact carrying every per-channel message-ID enum plus a `GalConstants` block. Highest-fidelity machine-readable source available. **2. Head Unit Integration Guide v1.3.0** (2016-08-10, 104 pp) — Google's own integration guide, recovered from a search-engine cache. Converted to `previous-work/markdown/milek7-galdocs/head-unit-integration-guide-v1.3.md`; cited here as `[HUIG p.N]`. **Normative for semantics, policy and requirements — not for wire bytes.** Its own introduction says so (p.5): *"It does not describe the low-level AAP protocol implementation abstracted by a sender and receiver library on both the head unit (HU) and the mobile device (MD)."* It also contradicts itself on its own version: the title page says 1.3.0 while the body on p.11 still says *"This document covers version 1.2."* **3. aasdk** (`f1xpl` → `opencardev` → `emirhalici`) — 2018-era proto2 plus working C++, tested against real cars for years. Authoritative on framing and transport; **known-wrong in several enums** (Appendix C). **4. mrmees `open-android-auto`** — decompilation of gearhead APK v16.1–17.3, with its own Gold/Silver/Bronze confidence tiers and dated retractions. The most current source. ⚠ **`mfont-bz17/aa-linux` is not independent of it**: its `.proto` files carry `// https://github.com/mrmees/open-android-auto` headers, making it a *stale snapshot*. Its Python **code**, however, is independent and live-tested against the DHU — this document cites aa-linux's code, never its protos. **5. mikereidis/headunit → headunit-revived** — the original 2016 reverse engineering. `vincenzobpt/MOTO-HUB` is a **direct fork** (`NOTICE:52-61`); its generated `proto/*.java` are compiler output from headunit-revived's `.proto`. `andyching168/HeadunitPad` (Swift) carries no vendoring markers but matches this lineage too precisely to be coincidence. ⚠ Treat their agreement as **one lineage confirmed twice**; their *disagreements* are the informative signal. **6. Gearhead APK static-analysis notes** — `previous-work/misc/fixtures/gearhead-reference-notes.md`: clean-room **behavioral observations only** (wire constants, message sequences, state-machine structure, flag names, timing values, per-build obfuscated-class anchors) from static analysis of the phone app itself — builds **7.7.622144 / 8.2.623924 / 17.7.663624** (provenance and SHA-256 hashes: `previous-work/markdown/apk/README.md`; decompiles in `previous-work/source/apk/jadx-v{77,82,17}/`, cross-checked against persisted baksmali trees to dodge jadx decompile artifacts). No code, certificates or assets are copied — certificate material appears as metadata (subject/serial/validity) only. It overlaps lineage 4 in *subject* (both decompile gearhead) but is independent work on different builds: treat agreement with mrmees as confirmation, disagreement as signal. The 17.7 dump this lineage analyses is the same one [§3.3](#33-negotiation-is-not-validated--but-the-version-still-matters) warns not to follow mrmees's 17.3 class names into. Headline of its 7.7→8.2 diff: **almost nothing wire-visible changed** — max protocol version 4.0→4.1 and a fallback-credential rotation only. **Explicitly not a source:** `mossyhub/openautolink`'s `docs/protocol.md` describes that project's own invented app↔bridge protocol, not AAP. Its ports (5288 control / 5289 audio / 5290 video) have nothing to do with this specification, despite 5288 coinciding numerically with a real AAP port. --- # 1. Transport layer AAP is transport-agnostic. Everything from [§2 Frame format](#2-frame-format) upward is byte-identical whether the bytes arrive over USB, Wi-Fi, or a loopback TCP socket. Three transports are deployed. ## 1.1 The USB role inversion — read this first This trips up nearly every implementer, and Google's own prose does not help. `[HUIG p.19-20]`: > *"The HU operates as a USB Host and AOA accessory. The HU powers the MD, which operates as a USB > accessory."* … *"the host (HU) and client (MD)"* … *"HU attempts to connect to MD using AOAP and > advertise itself as an AOAP accessory."* Note the guide calls **both** sides an "accessory" within two sentences. Untangled: - **The car is the USB host.** It supplies power, enumerates, and issues control transfers. In AOA terms it plays *the accessory role* — it "advertises itself as an AOAP accessory". - **The phone is the USB peripheral (device).** It is switched into *accessory mode* — which is why Google's second sentence loosely calls it "a USB accessory" too. So "accessory mode" is a mode the *phone* is switched into, *by* the car. The car drives the switch using the control transfers below; the phone's kernel `f_accessory` driver responds by re-enumerating with a new VID:PID and a vendor-specific bulk interface. Two normative requirements follow `[HUIG p.20]`: - The HU **MUST** attempt AOAP initiation **regardless of the MD's initial VID/PID** — sniffing the connected device type to decide whether to try AAP is explicitly called out as unreliable. - A phone that does not support AOAP simply ignores the handshake, letting the HU fall back to MTP or charge-only. So a failed AOA switch must be non-fatal. ## 1.2 Wired: AOA2 (Android Open Accessory Protocol v2) Three vendor control requests, issued by the car (host) to the phone. `[CONFIRMED ×2]` — `f1xpl-aasdk/include/f1x/aasdk/USB/AccessoryMode*Query.hpp` and `HeadunitPad/Core/AAP/AndroidOpenAccessory.swift:79-94`: | # | Name | `bmRequestType` | `wValue` | `wIndex` | Data | |---|---|---|---|---|---| | 51 | `GET_PROTOCOL` | `0xC0` (IN, vendor) | 0 | 0 | ← u16 AOA version | | 52 | `SEND_STRING` | `0x40` (OUT, vendor) | 0 | string index | → string + NUL | | 53 | `START` | `0x40` (OUT, vendor) | 0 | 0 | none | `GET_PROTOCOL` returns 1 or 2; aasdk accepts either, and AOAv2 is backward-compatible with v1 (`AccessoryModeProtocolVersionQuery.cpp:66`). String indices (`AccessoryModeSendStringType.hpp:31-38`): ``` 0 MANUFACTURER 1 MODEL 2 DESCRIPTION 3 VERSION 4 URI 5 SERIAL ``` ### 1.2.1 Identity strings `[CONTESTED]` The phone decides whether to enter AA mode by matching these strings. `[HUIG p.19]` gives the official table — and `apserver.c` matches it exactly, field for field: | Index | Field | HUIG p.19 value | |---|---|---| | 0 | Manufacturer Name | `Android` | | 1 | Model Name | `Android Auto` | | 2 | Description | `Android Auto` | | 3 | AAP Protocol Version | `1.0` | | 4 | URI | *(empty)* | | 5 | Serial Number | *(empty)* | But field testing disagrees about the model string across the whole decade this protocol has been reverse-engineered: | Source | Model string | Year | Basis | |---|---|---|---| | **Mike Reid**, original XDA RE thread, post #16 | **`Android Open Automotive Protocol`** | **2015** | primary source — see below | | `[HUIG p.19]` `[NORMATIVE]` | `Android Auto` | 2016 | Google's own table | | `milek7` `apserver.c:14-23` | `Android Auto` | ~2018 | working libusb bridge; matches HUIG exactly | | `HeadunitPad` `AndroidOpenAccessory.swift:12-19` | `Android Auto` | 2025 | version `2.0.1`, serial `HU-AAAAAA001` | | gearhead APK `rtt.java` | accepts **both** | 2025 | decompilation | | `solarkennedy` `PLAN-androidauto.md:33-35` | `Android Open Automotive Protocol` | 2026 | field-tested; warns `Android Auto` gave "false 'no handler' results" | **The 2015 primary source, read directly** (`source/web/[CLOSED] Headunit app for Android Auto... XDA Forums.html`, post #16, 2015-03-31 — this is the thread that started the entire community lineage this document draws from): Mike Reid reports *"I have a C program that successfully connects with the AA app, as seen in the logcat"* — i.e. the accessory-mode handshake completed — then, further down the same post, quotes the string his program used: *"Ironic string: 'Android Open Automotive Protocol'. If it's 'Open', where are the documents Google?"* Also in that thread: *"The standard 0x2D01... What's important is the special manuf and device/product strings for AA. Only way I figured those out was by modifying some framework files and building a custom ROM."* **Resolution.** `Android Open Automotive Protocol` is not a 2026 rediscovery — it is the **original 2015 finding**, extracted from AOSP framework code before HUIG or any public documentation existed. `Android Auto` (HUIG, milek7, HeadunitPad) is real and independently confirmed too. The most coherent explanation, given the APK's own `rtt.java` accepts **both**: this has been a two-member accepted set since the beginning, not a value that drifted over time — and `Android Open Automotive Protocol` is the *historically prior* one, originally reverse-engineered by decompiling the framework directly rather than observed from a working reference implementation. **Recommendation:** try `Android Open Automotive Protocol` first, fall back to `Android Auto`. The manufacturer is `Android` in every source. → Appendix B (downgraded from open question to low-priority confirmation only). **Further downgrade (APK static analysis, 7.7/8.2/17.7).** The phone's own USB accessory filter (`res/xml/car_usb_accessory_filter.xml`, byte-identical in 7.7 and 8.2) accepts **three** manufacturer/model pairs: `Android`/`Android Open Automotive Protocol`, `Android`/`Android Auto`, and `Android`/`Android` — and does **not** filter the version field at all. The accepted set has three members, not two, and the third is the most generic of all. → Appendix B (fully downgraded; any of the three pairs is safe). ⚠ **Index 3 (`VERSION`) is the AOAP accessory-descriptor version, not the AAP protocol version.** `[HUIG p.19]` lists it in the "Configuring AOAP" accessory-identifier table. It has nothing to do with the in-band major/minor handshake of [§3](#3-version-handshake). Conflating the two is a common error — note `apserver.c` says `1.0` while HeadunitPad says `2.0.1`, and both work. ### 1.2.2 After the switch The phone re-enumerates as VID `0x18D1` (Google) with one of `[CONFIRMED ×3]`: | PID | Composition | |---|---| | `0x2D00` | accessory | | `0x2D01` | accessory + ADB | | `0x2D02` | audio | | `0x2D03` | audio + ADB | | `0x2D04` | accessory + audio | | `0x2D05` | accessory + audio + ADB | Interface is vendor-specific `0xFF / 0xFF / 0x00` with two bulk endpoints — **OUT `0x02`, IN `0x81`**; max packet 64 (full-speed) or 512 (high-speed) (`aa-linux/usb.py:41-51`). All AAP frames flow over this single bulk pair. ⚠ **AOA2's other features are not used by AAP.** `SET_AUDIO_MODE` (request 58, USB Audio Class) and the HID requests (54–57) are part of AOA2 but AAP uses none of them — audio and input ride AAP's own channels over the bulk endpoints. ## 1.3 Wireless path A: Bluetooth handoff Wi-Fi carries the AAP session, but Bluetooth bootstraps it. **SDP UUID** `4de17a00-52cb-11e6-bdf4-0800200c9a66` `[CONFIRMED ×4]` — `aa-linux/wireless.py:42`, `nisargjhaveri .../bluetoothHandler.cpp:24`, `aa-proxy-rs/src/bluetooth.rs:42`, and the 17.7 APK itself. The APK additionally lists a **companion UUID** `669a0c20-0008-f4bd-e611-cb52007ae14d`, an HFP-HF UUID `0000111e-…` used for car-side checks, and — decisively for the transport question — **no mDNS/NSD machinery anywhere** (confirmed across 7.7–17.7): wireless discovery is Bluetooth-based, full stop. ⚠ *Editor's analysis:* that companion UUID is the **same 16 bytes in reverse order** as the main AA UUID (`4d e1 7a 00 52 cb 11 e6 bd f4 08 00 20 0c 9a 66`, reversed byte-for-byte). It is almost certainly the same service UUID in the opposite byte order — an SDP endianness artifact, not a second service to register. **RFCOMM channel 8** `[CONFIRMED — see below]`: | Source | Channel | Basis | |---|---|---| | mrmees `wireless-bluetooth-setup.md:54,131,221` | **8** | SDP record construction, three places | | community troubleshooting knowledge (`aa-trigger-esp32` project context) | **8** | standard advice: dump SDP on a BT scanner and confirm the AA UUID sits on RFCOMM channel 8 if a connection fails | | `mretallack` `design.md:444,476` | 22 | asserted, no corroboration | Resolve to **8** — two independent sources agree, and no source beyond one unsupported assertion in `mretallack` gives 22. ⚠ **8 is not architecturally fixed — it is the channel every observed server happened to get.** The production Qt/C++ implementation in `mrmees-openauto-prodigy/src/core/aa/BluetoothDiscoveryService.cpp` (see below) does not hardcode a channel at all: it calls `QBluetoothServer::listen()`, takes whatever RFCOMM channel BlueZ hands back (`rfcommPort_`, `listenForRfcomm()`), and writes *that* into its own SDP record. **8 is simply the first channel BlueZ tends to assign when only one RFCOMM profile is registered** — the correct client behaviour is to read the channel from the SDP record (exactly as `sdptool browse` does), not to hardcode 8. Same principle as the WiFi TCP port ([§1.6](#16-ports--negotiated-not-constant-contested-resolved)): discover it, don't assume it. **On the "missing scripts."** `wireless-bluetooth-setup.md` names three companion files (`sdp_clean.c`, `aa-combined.py`, `bt-agent.py`) that mrmees's own issue tracker (`open-android-auto` #19) confirms are absent from the repository. **They were never meant to be committed.** A second, near-identical copy of this same guide survives in a *different* mrmees repo — `mrmees-openauto-prodigy/docs/archive/openauto-pro/bluetooth-wireless-aa-setup.md` — and its own "Files Reference" section is explicit: *"Working test scripts (on Pi at `/tmp/`)"*. They were throwaway files on the author's Raspberry Pi test rig, not source-controlled deliverables; nothing to recover. That archived copy is more valuable than the missing scripts would have been, for two reasons. First, it explains the **root cause** the scripts existed to work around, with an AOSP citation: Android's `sdpu_compare_uuid_with_attr()` (`system/bt/stack/sdp/sdp_utils.cc:760`) does strict SDP attribute size comparison — a BlueZ record mixing 16-bit and 128-bit UUIDs (which is how BlueZ 5.82 encodes its own standard PnP/GAP/GATT/DevInfo records) triggers `"invalid length for discovery attribute"` and Android drops the connection. This is why the AA-only, no-SPP, single-128-bit-UUID SDP record matters — it's not superstition, it's dodging a specific strict-comparison code path (the doc attributes it to a security-motivated commit, `6afad4b`). Second, the archive names a **production successor**, and that file exists in our corpus: `mrmees-openauto-prodigy/src/core/aa/BluetoothDiscoveryService.{cpp,hpp}` (602+109 lines) — a complete, working Qt implementation of the SDP-cleanup-and-RFCOMM-handshake recipe the scripts prototyped, confirmed hardware-tested: *"Tested with: BlueZ 5.82, Raspberry Pi 4, Moto G Play (Android 14), Samsung S25 Ultra. Date: 2024-02-24."* ⚠ **That production file also contains a genuine bug, found by cross-checking it against this document's own sources** — see [Appendix C](#appendix-c--known-bad-sources). **HFP presence `[LIKELY REQUIRED for wireless]`.** A 16-bit HFP AG UUID `0x111F` is probed alongside the AA UUID. `mrmees`'s two documents (the original and the archived copy, independently dated 2024-02-24) are emphatic and explicit about why: *"The Android Auto app **requires** an active HFP (Hands-Free Profile) connection to the head unit before it will initiate wireless AA setup. This is how the phone distinguishes a car head unit from a random Bluetooth device."* Failure mode: `WIRELESS_SETUP_FAILED_TO_START_NO_HFP_FROM_HU_PRESENCE`. But `[HUIG p.23]` says the opposite for the (older, wired-only) protocol it describes: > *"While AAP needs a Bluetooth HFP for telephony to function during an active AAP session, the > absence of an active HFP connection will not prevent the AAP pairing process from completing."* These are reconcilable by era and transport: HUIG 1.3 predates wireless AAP entirely and is describing USB pairing, where HFP is genuinely optional. The wireless bootstrap is a later addition with its own gate. Treat HFP as **required for the wireless path, optional for wired**. ⚠ *Complication found in web research:* `headunit-revived` (a mature, widely-deployed wireless HU implementation) describes its own HFP server as optional — it "optionally runs an HFP server to help phones detect the HU," i.e. an aid to discovery, not a hard gate it depends on. This is a data point *against* a universal hard requirement, from an implementation that demonstrably works wirelessly, set against two dated, causally-explained mrmees sources saying the opposite. It may mean the requirement is phone-app-version-dependent, or that `headunit-revived` tolerates what mrmees's exact gearhead build does not — or that `headunit-revived`'s "optional" server still reliably gets connected in practice even though the code doesn't hard-block on it. Kept `[LIKELY REQUIRED]` rather than fully resolved. **Regardless of the answer, registering an HFP AG profile costs nothing and is the correct move either way** — hold the fd open, never answer calls. → Appendix B. **RFCOMM framing** differs from AAP's own framing — it is simply: ``` [u16be total length][u16be message id][protobuf body] ``` **`WifiSetupMessage` IDs and the five-stage sequence, IDs 1–7 `[CONFIRMED ×4]` (of which 6/7 vs one outlier), IDs 8–11 `[LIKELY ×1]`** (`aa-linux/wireless.py:47-53,535-579`, mrmees `wireless-bluetooth-setup.md:160-168`, `aa-proxy-rs/src/bluetooth.rs:57-58`, gearhead 17.7 APK link-level table): | ID | Message | Direction | |---|---|---| | 1 | `WifiStartRequest` | HU→MD | | 2 | `WifiInfoRequest` | MD→HU | | 3 | `WifiInfoResponse` | HU→MD | | 4 | `WifiVersionRequest` | either | | 5 | `WifiVersionResponse` | either | | 6 | `WifiConnectStatus` | MD→HU | | 7 | `WifiStartResponse` | MD→HU | | 8 | `PING_REQUEST` | link-level | | 9 | `PING_RESPONSE` | link-level | | 10 | `CONNECTION_REJECTION` | link-level | | 11 | `SETUP_INFO` | link-level | ``` 1. HU→MD WifiStartRequest { ip_address=1 (string), port=2 (uint32) } 2. MD→HU WifiStartResponse { status = 0 } 3. MD→HU WifiInfoRequest { } -- empty 4. HU→MD WifiInfoResponse { ssid=1, key=2, bssid=3, security_mode=4, access_point_type=5 } 5. MD→HU WifiConnectStatus { status = 0 } -- after joining the AP → MD opens TCP to the ip_address:port from step 1; AAP proper begins. ``` `WifiVersionRequest`/`Response` (4/5) may interleave at any point. ⚠ `WifiInfoResponse` field 5 `[CONTESTED, minor]` — the 17.7 APK's `RESPONSE_INFO` (ID 3) field map reads `{1 ssid, 2 password, 3 bssid, 4 security, 5 status}`, a status field where the other three sources place `access_point_type`. Fields 1–4 are agreed by everyone. → Appendix B (item 23). ⚠ **IDs 6 and 7 are swapped in one source** — `mrmees-openauto-prodigy`'s own production C++ (`src/core/aa/BluetoothDiscoveryService.cpp:36-40`) defines `kMsgWifiStartResponse = 6` and `kMsgWifiConnectionStatus = 7`, the reverse of the table above. Four independent sources agree with the table (`aa-linux`, this same mrmees organization's *own* markdown doc, `aa-proxy-rs` — deployed production firmware talking to real cars — and the 17.7 APK's link-level table, which names 6 `CONNECT_STATUS` and 7 `RESPONSE_START`), against one outlier that is mrmees's *other* repo contradicting mrmees's *own documentation*. Resolve 4-to-1 in the table's favor; treat the `openauto-prodigy` file as buggy on this point — see Appendix C. **The HU runs the access point; the phone joins as a station.** Reference dongles run `hostapd`. `AccessPointType`: `STATIC=0, DYNAMIC=1`. **`WifiSecurityMode` `[RESOLVED: bitmask]`:** | Source | OPEN | WEP_64 | WEP_128 | WPA_PERSONAL | WPA2_PERSONAL | WPA_WPA2 | Enterprise variants | |---|---|---|---|---|---|---|---| | mrmees, aa-proxy-rs (bitmask) | 1 | 2 | 3 | 4 | **8** | **12** | 20/24/28 | | GAL `protos.proto:1671` (sequential) | 1 | 2 | 3 | 4 | **5** | **6** | 7/8/9 | `aa-proxy-rs` is the deployed firmware behind commercially-sold "AA Wireless Dongle" hardware — its `src/protos/WifiInfoResponse.proto` defines the bitmask reading outright (`proto2`, `WPA2_PERSONAL=8`), and it additionally has enterprise variants the GAL numbering lacks (`WPA_ENTERPRISE=20, WPA2_ENTERPRISE=24, WPA_WPA2_ENTERPRISE=28` — each is the personal value plus 16, consistent with a genuine bitmask rather than coincidence). Since this is live production firmware talking to real cars, not just an extracted proto, resolve to **bitmask** — emit 8 for WPA2-Personal, and still accept 5 defensively since the sequential reading remains in the GAL proto. → Appendix B. **Upgraded to `[CONFIRMED ×2]` by the APK itself** (17.7, first-hand): the WiFi-projection channel's credentials response carries the full bitmask enum — `0 UNKNOWN, 1 OPEN, 2 WEP_64, 3 WEP_128, 4 WPA_PERSONAL, 8 WPA2_PERSONAL, 12 WPA_WPA2, 20/24/28 enterprise variants, 32 WPA3_PERSONAL, 40 WPA2_WPA3` — including two values no other source has (WPA3 and the WPA2/WPA3 union). The sequential GAL numbering is now conclusively a proto-extraction artifact. On that WiFi-projection channel itself (§6.4): 7.7/8.2 are car-initiated only — a single `0x8002 WifiCredentialsResponse` with no phone-side request — while 17.7 adds the phone→car `0x8001 WifiCredentialsRequest` (empty) that the §6.4 enum lists. The phone then joins the car's AP via `WifiNetworkSpecifier` (hidden SSID allowed, WPA2/WPA3). Wireless timers observed in the APK: WiFi-info response wait 3000 ms; wireless-RSSI start threshold −50 dBm (both Phenotype-tunable — §11). → Appendix B (item 4 resolved). ## 1.4 Wireless path B: QR / deep-link pairing A Bluetooth-free path, reverse-engineered by MOTO-HUB (`AaWirelessPairing.kt:165-173`) — original work, not vendored. Directly relevant to this project because we control the phone. Gearhead exposes broadcast receivers accepting: ``` com.google.android.projection.gearhead.START_WIRELESS_PROJECTION_WPP com.google.android.projection.gearhead.START_WIRELESS_PROJECTION ``` and a deep link `https://androidauto.com/projection/?data=` whose payload is protobuf: ```protobuf // field 7 (security mode) is omitted — Android Auto defaults it to WPA2_PERSONAL message WirelessPairingPayload { optional string ssid = 1; optional string bssid = 2; optional string passkey = 3; optional string host_address = 4; // string, not a packed address optional uint32 port = 5; optional string bluetooth_address = 6; } ``` ## 1.5 TCP (DHU and development) Plain TCP; the **phone listens** and the DHU connects in. The phone is therefore TCP server *and* TLS server simultaneously. `aa-linux/transport.py:16` binds `0.0.0.0:5277`; `dhu.sh:67-71` launches `desktop-head-unit --adb=5277`. The APK's wireless-projection-over-TCP path carries an overall **45000 ms timeout** — do not expect an idle TCP transport to stay up indefinitely. ## 1.6 Ports — negotiated, not constant `[CONTESTED, resolved]` Four different numbers circulate. They are not competing claims about one value; they describe different things: | Port | What it actually is | Source | |---|---|---| | 5277 | DHU listener; also used for wireless AAP by MOTO-HUB and HeadunitPad | `transport.py:16`, `Discovery.swift:6` | | 5288 | What the dongle projects advertise in `WifiStartRequest` | aa-proxy-rs, nisargjhaveri, aa-linux | | 5289 | "Wireless helper / launcher" auxiliary port | `Discovery.swift` | | 30515 | `GalConstants.WIFI_PORT` in the extracted GAL binary | `protos.proto:2101` | **Resolution: the port is not fixed.** It is carried in `WifiStartRequest.port` (§1.3 step 1) or the QR payload's field 5 (§1.4). Implementations must read it, never hardcode it. `30515` is the GAL-declared default; everything else is a deployment choice. > **Phone-side note (this project).** Bring up TCP/5277 against the DHU first — it exercises the > entire stack from [§2](#2-frame-format) up with no USB or Bluetooth complications. Add AOA2 next > for the wired car case. Wireless last, and prefer path B (§1.4) over the Bluetooth handoff, since > we control the phone and can skip the HFP-presence requirement entirely. --- # 2. Frame format Every byte above the transport is framed identically. A *message* (a 2-byte type plus a protobuf body, or a media payload) is split into one or more *frames*. ## 2.1 Frame header ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | channel id | flags | frame payload length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | total message length (ONLY if flags&0x03 == 0x01) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | frame payload ... | ``` | Offset | Size | Field | Notes | |---|---|---|---| | 0 | u8 | channel id | 0 = control; all others negotiated ([§6](#6-channel-architecture)) | | 1 | u8 | flags | bitfield, below | | 2 | u16be | frame payload length | bytes of payload **in this frame** | | 4 | u32be | total message length | **present only when `(flags & 0x03) == 0x01`** | So the header is **4 bytes normally, 8 bytes on the first frame of a fragmented message**. Source: `opencardev-aasdk/src/Messenger/FrameHeader.cpp` + `FrameSize.cpp`, corroborated by `aa-linux/frame.py:52` (`struct.pack(">BBHI", ...)`). ## 2.2 Flag bits | Bit | Mask | Name | Meaning | |---|---|---|---| | 0 | `0x01` | FIRST | first frame of a fragmented message | | 1 | `0x02` | LAST | last frame of a fragmented message | | 2 | `0x04` | CONTROL | payload is a control message for this channel | | 3 | `0x08` | ENCRYPTED | payload is TLS ciphertext | Bits 0–1 together form the fragmentation state, mirrored in the GAL proto as `enum FragInfo` (`protos.proto:1334-1339`): | `flags & 0x03` | `FragInfo` | aasdk `FrameType` | Meaning | |---|---|---|---| | `0b00` | `FRAG_CONTINUATION` | `MIDDLE` | middle frame | | `0b01` | `FRAG_FIRST` | `FIRST` | first of many — **carries total length** | | `0b10` | `FRAG_LAST` | `LAST` | last of many | | `0b11` | `FRAG_UNFRAGMENTED` | `BULK` | complete message in one frame | ⚠ **Precision point most of the corpus gets wrong.** The 4-byte total-length field appears when the frame type is **FIRST exactly (`0b01`)** — *not* whenever bit 0 is set. A single unfragmented frame has `0b11` (FIRST|LAST) and carries **no** total length, because its frame length already is the total. MOTO-HUB encodes this as a literal `flags == 0x09` test (`AapReadSingleMessage.kt:43-44`) — `0x09` = FIRST|ENCRYPTED with LAST clear. ⚠ That same literal is a latent bug worth not copying: a FIRST+CONTROL-not-LAST frame (`0x0D`) would be mis-parsed. It assumes control messages never fragment — which the phone app itself enforces (channel-control messages are never fragmented and must fit in one frame, payload ≤ `maxFragment` − 4; `itx.java:213-214`, v7.7–17.7) — but the wire format does not guarantee it. Observed flag values on the wire, agreed by MOTO-HUB and HeadunitPad: `0x0B` single, `0x09` first, `0x08` middle, `0x0A` last (all with ENCRYPTED set) — an exact corroboration of the bit assignment. ## 2.3 Reconciling the "4 vs 6 vs 8 byte header" confusion Different codebases quote different header sizes for the *same bytes*. This is an accounting convention, not a wire disagreement, and knowing the mapping makes every other codebase readable: | Convention | Plaintext frame | Encrypted frame | Used by | |---|---|---|---| | Header excludes message type | 4 (or 8 if FIRST) + separate 2-byte type | same | aasdk, aa-linux, this document | | Header includes message type | **6** — `chan, flags, len, type`, where `len` covers the type | **4** — type is inside the ciphertext | MOTO-HUB, HeadunitPad | Both describe identical bytes. This document uses the first convention throughout. `[CONTESTED]` — one source genuinely disagrees. milek7's Wireshark dissector (`androidauto.lua:157-170`) reads a *single* 4-byte length at offset 2 on FIRST frames and starts the payload at offset 6: | Source | FIRST-frame header | Basis | |---|---|---| | aasdk `FrameSize.cpp` | 8 (u16 frame len + u32 total len) | C++ tested against real cars | | `aa-linux/frame.py:52` | 8 | Python, live-tested against Google's DHU | | MOTO-HUB `AapReadSingleMessage.kt:43-44` | 8 | independent lineage | | `mretallack` `design.md:475` | 8 (describes it as "2 length + 4 reserved") | validated against uglyoldbob | | milek7 `androidauto.lua:157-170` | **6** (one u32 length) | hand-written dissector | Resolve to **8**. Three independent lineages agree, two of them verified against real hardware, and the dissector's own header comment admits to hardcoded simplifying assumptions. ## 2.4 Message payload Inside the reassembled frame payload: ``` [u16be message type][protobuf body] ``` Message type numbering follows a consistent convention: - **`< 0x8000`** — bulk data. On A/V channels, `0x0000` = media data, `0x0001` = codec config. - **`>= 0x8000`** — control messages *for that channel*. On the control channel (0) the type is a `ControlMessageType` ([§6.3](#63-control-channel-messages)) and the range rule does not apply. ## 2.5 Fragmentation rules - Maximum frame payload: the **negotiated default is 16128 bytes** in the phone app itself (`max_fragment_size`, v7.7 `its.java:26`; WiFi deployments can override it via Phenotype — [§11](#11-phone-app-tunables-phenotype-flags)). The commonly quoted **`0x4000` (16384)** (`aa-linux/constants.py:106`) is the reader's SSL-ciphertext buffer and the mic-queue limit, not the negotiated send size. Receivers should treat 16384 as the safe upper bound (the u16 frame length allows up to 65535, and no peer is known to exceed 16384); senders should default to 16128. Fragmentation triggers only when the message is fragmentable **and** its payload exceeds `max_fragment_size − 4`. - `frame payload length` is u16, so a frame can never exceed 65535 regardless. - **Fragmentation happens after encryption.** The chunks are ciphertext; `total message length` refers to the plaintext total (`aa-linux/protocol.py:371-418`). - **A LAST frame is always sent, even if empty** — receivers reassemble on frame type, not by counting down `total message length` (`protocol.py:378-380`). ## 2.6 "Control message" does not mean "channel 0" A non-obvious routing rule that breaks implementations that assume otherwise. `CHANNEL_OPEN_REQUEST` is framed **on the channel being opened**, with the CONTROL bit set — not on channel 0. The receiver validates the channel in the frame header, so sending it on channel 0 is rejected (`aa-linux/protocol.py:434-446`, which notes the DHU's `MessageRouter::routeChannelControlMsg` does exactly this check). Conversely, milek7's dissector treats a frame as control if *either* the channel byte is 0 **or** the CONTROL bit is set (`androidauto.lua:185-189`). ## 2.7 QoS, writer priorities, and framing errors The phone's writer is a QoS queue (on by default for both USB and WiFi; v7.7 `ixs`): per-channel priority `FIRST(−128) < DEFAULT(0) < AUDIO(1) < VIDEO(2) < LAST(127)`, with video channels mapped to VIDEO and audio channels to AUDIO. Dispatch is per-endpoint threads — video on `RxVid` (priority −8), audio `RxAud` (−19), mic `RxMic` (−16), sensors `RxSen`, everything else `RxDef` — confirming that each channel is an independent state machine with its own reader. On a framing error the phone sends message **0xFFFF (`FRAMING_ERROR`) on channel 0 and tears the session down** — the error sentinel of [§6.3](#63-control-channel-messages) has a concrete, fatal consumer. > **Phone-side note (this project).** Implement framing as a standalone, unit-tested layer with no > knowledge of message semantics. It is the one part of the stack where a subtle error produces > symptoms — stalls, "severe static", undecodable video — that look like bugs three layers up. --- # 3. Version handshake The first exchange after the transport connects. It is **not protobuf** — raw big-endian 16-bit integers — and it is sent **in plaintext**, before TLS. `[HUIG p.15]` makes the HU the initiator: > *"the HU initiates negotiation of the AAP connection by sending a Version Request to the MD with > ReceiverLib function `GalReceiver::start()`. The HU MUST NOT send additional Version Requests or > ping requests while awaiting a response from the MD."* ## 3.1 Wire format ``` VERSION_REQUEST HU→MD flags 0x03 (BULK, plaintext), channel 0 00 01 message type = 1 -- 6 bytes total VERSION_RESPONSE MD→HU flags 0x03 (BULK, plaintext), channel 0 00 02 message type = 2 -- 8 bytes base [ VersionResponseOptions protobuf ] -- appended iff the HU's request was >= 1.6 ``` Observed response, verbatim (mrmees `02-version-ssl-auth.md:124`): ``` 00 02 00 01 00 07 00 00 -- VERSION_RESPONSE, v1.7, status MATCH ``` | `status` | Meaning | |---|---| | `0x0000` | MATCH — compatible version | | `0xFFFF` | MISMATCH — no compatible version; the connection will close | `0xFFFF` is `MessageStatus.NO_COMPATIBLE_VERSION` (`-1`) rendered as an unsigned 16-bit field. The whole `MessageStatus` enum is signed and mostly negative ([§6.5](#65-status-codes)), so any status field that is u16 on the wire shows its errors as large positive values. ⚠ **The response is not always 8 bytes.** When the HU's request is ≥ 1.6, gearhead appends a `VersionResponseOptions` protobuf (`ConnectionConfiguration` at field 1) — the "WireConfig" gate (mrmees, APK 17.3 `iyk.java:149-193`; see the caveat under §3.3). This is exactly why milek7's dissector skips 6 bytes past the message type before handing the remainder to a protobuf parser. **A parser that assumes a fixed 8 bytes will mis-handle modern head units.** ⚠ **The request can carry options too.** The phone parses an *optional appended* `VersionRequestOptions` protobuf on `VERSION_REQUEST` — `{1 flags, 2 fixed64 "snapshot"}` — with the parsing itself gated behind a flag (v7.7 `itj.java:347-370`, present through 17.7). The semantics of the fixed64 "snapshot" beyond logging are undetermined → Appendix B (item 21). ## 3.2 Which version number? `[CONTESTED]` Six values circulate. Most of the apparent conflict is a category error — three different things are being measured: | Role | Source | Value | |---|---|---| | **HU advertises** (in request) | aasdk build constant | 1.1 | | | MOTO-HUB `Messages.kt:20` (`{0,1,0,2}`) | 1.2 | | | HeadunitPad `AapTransport.swift:300` (`{0,2,0,0}`) | **2.0** | | **MD responds** | real gearhead AA 17.3, observed | 1.7 (6.1 if request > 1.7) | | | `aa-linux/constants.py` (mimicking gearhead) | 1.7 | | | gearhead APK 7.7 / 8.2 / 17.7 (static) | max supported **4.0 / 4.1 / 6.1**; answers 1.7 to any request ≤ 1.7 | | **Document / binary constant** | HUIG title page — *document* revision | 1.3.0 | | | HUIG body p.11 — protocol it describes | 1.2 | | | `GalConstants` in the extracted binary | 1.6 | Read this way a coherent timeline appears — HUIG-era ≈1.2 (2016) → GAL binary 1.6 → current gearhead 1.7 — and different projects simply froze at different points. Real gearhead logs `Car requests protocol version v1.1` / `Negotiated protocol version v1.7` (`CAR.GAL.GAL.LITE`), so aasdk's 1.1 is deliberate conservatism rather than a claim that aasdk implements AA 1.1. The APK static analysis fills in the rungs between those observations: the max-supported value climbed **4.0 (v7.7) → 4.1 (v8.2) → 6.1 (v17.7)**, and the answering algorithm is uniform across all three builds — *requested ≤ 1.7 → answer 1.7; requested > 1.7 → answer max* — with the `STATUS_NO_COMPATIBLE_VERSION` branch **dead code** (the options-proto singleton is never null), so a mismatch status is never actually emitted. The 4.1 bump in v8.2 has exactly one identified consumer: wireless version-response BT-adapter-name / BT-MAC fields (§3.3). ⚠ **There are two version series, not one continuous run:** a legacy **1.x** series and a modern **4.x – 6.1** series, with nothing observed in between on the wire — though the APK's own max-supported ladder (4.0 → 4.1 → 6.1) shows the phone side climbed through 4.x continuously while head units stayed on 1.x for years. HeadunitPad's **2.0 is the one genuine outlier** — nothing else sits in 2.x, and since nothing validates the exchange (§3.3), a mistake there would never surface. → Appendix B. ## 3.3 Negotiation is not validated — but the version still matters No reference implementation checks the peer's version. MOTO-HUB verifies only that the response *type* is 2 and ignores the payload (`AapTransport.kt:222-227`); HeadunitPad proceeds to TLS on any `VERSION_RESPONSE` (`:1197-1204`). It would be wrong to conclude the version is cosmetic. **The phone retains the HU's raw *requested* version — not the negotiated response — and gates features on it** (mrmees `02-version-ssl-auth.md:131`). The HU's request must therefore describe what it actually implements; it is not a bid for the highest mutually supported version. | Min version | Feature unlocked | Evidence (APK 17.3, relayed from mrmees) | |---|---|---| | 1.4 | Battery-status notification send (the previously unidentified gate) | `hna.java:1347`; first-hand 7.7–17.7 | | 1.6 | `WireConfig` appended to the version response | `iyk.java:149-193` | | 1.7 | Default preferred legacy response | `iyk.java:24-26,134-148` | | 4.1 | Wireless version-response BT-adapter-name / BT-MAC fields (kill-switch gated) | APK 8.2 first-hand, `gny.java:467-494` | | 4.3 | `AdditionalVideoConfig` UI / resize policy | `itt.java:277-299` | | 5.0 | Ackless audio; extended AV start data | `ipq.java:154-164`, `ipe.java:371-400` | | 5.1 | Audio `MediaOptions` updates; `VehicleEnergyForecast` | `ipe.java:541-551`, `ija.java:1101-1117` | | 6.0 | Modern media-options / video paths, incl. conditional H.265 | `iky.java`, `itq.java`, `its.java` | | 6.1 | Server-pushed ping config `{1:{1:{1..4}}}` appended to the version response (ties to §5.3); SDP request field 6 | APK 17.7 first-hand; `iyk.java:24-26,134-148` | ⚠ **These are second-hand citations into a decompilation this corpus does not contain.** They refer to gearhead **17.3**; the APK dump under `previous-work/source/apk/jadx-v17/` is **17.7**, and ProGuard/R8 reassigns obfuscated names between builds — `iyk` in 17.7 is a three-line synthetic lambda, unrelated to the class mrmees cites. **Do not follow these line numbers into our dump.** To re-verify a gate, decompile the matching APK version and search for the version comparison, not the class name. For 17.7 this is now less of a problem: the APK-notes lineage (lineage 6) re-verifies the gates first-hand in 7.7/8.2/17.7, and Appendix D's anchor map indexes the per-build class names — the 17.3 class *names* still do not carry over, but the gates themselves no longer rest on the relay alone. > **Phone-side note (this project).** We are the **responder**, not the requester: parse and retain > the HU's raw requested version, gate our own behaviour off it, and answer. Advertise **1.7** to > mirror real gearhead. Do not claim ≥5.0 or ≥6.0 until ackless audio and the modern video paths > actually work — the HU will take us at our word and stop sending acks we still depend on. --- # 4. TLS authentication The single most consequential section for this project. The protocol layer is straightforward; the **PKI is where a phone-side implementation actually gets blocked** ([§4.5](#45-pki-and-provisioning)). ## 4.1 Roles — the phone is the TLS server `[NORMATIVE]` `[HUIG p.21]`: > *"The AAP protocol uses industry standard **TLS 1.2** with Client Authentication to secure > communications between the head unit (**TLS Client**) and mobile device (**TLS Server**). The > first step in establishing a AAP connection is mutual authentication between the MD and HU > following the TLS Client-authenticated TLS handshake protocol."* `[CONFIRMED ×4]` across four independent implementations: | Implementation | Side | Evidence | |---|---|---| | `aa-linux` (phone) | **server** | `AACrypto.create_server()`, `ssl.PROTOCOL_TLS_SERVER` | | aasdk (head unit) | client | `TLS_client_method()`, `SSL_set_connect_state()` | | MOTO-HUB (head unit) | client | `AapSslContext.kt:15-16` `useClientMode = true` | | HeadunitPad (head unit) | client | `OpenSslTlsHandler.swift:45,144` | The HU also dictates our obligations `[HUIG p.21]` — the MD is responsible for *"switching to AOAP mode when requested by the HU"*, *"responding to HU initiation of Android Auto connection with an appropriate handshake"*, and *"registering and responding to HU Service Discovery declarations"*. ## 4.2 Handshake transport — TLS tunnelled inside AAP TLS does **not** run at the socket layer. Handshake records are carried as ordinary AAP control messages, which is why implementations drive TLS through memory BIOs rather than a socket: ``` message type 0x0003 ENCAPSULATED_SSL -- carries raw TLS handshake bytes message type 0x0004 AUTH_COMPLETE -- AuthResponse { int32 status = 1 } ``` The pump: feed received ciphertext into the TLS engine's inbound BIO, step the handshake, drain the outbound BIO, wrap whatever came out as a new `ENCAPSULATED_SSL` message, send. Repeat until the handshake completes. `[CONFIRMED ×3]` — `aa-linux/crypto.py` (`ssl.MemoryBIO`), HeadunitPad `OpenSslTlsHandler.swift:133-143` (`BIO_new(BIO_s_mem())` ×2), MOTO-HUB `AapSslContext.kt:328-397` (Java `SSLEngine` wrap/unwrap). milek7's `ssl_preload.cpp:103-105` independently corroborates by hardcoding the 2-byte `{0x00, 0x03}` prefix onto outbound handshake bytes. ⚠ **These frames are plaintext.** The ENCRYPTED flag bit is clear during the handshake — the payload is TLS handshake data, not application data. `[HUIG p.15]`: *"Encrypted packets MUST NOT be sent from HU to MD until authentication is complete."* `AUTH_COMPLETE` concrete bytes, agreed by both headunit-lineage implementations: ``` 00 03 00 04 00 04 08 00 │ │ └──┘ └──┘ └──┘ │ │ │ │ └─ payload: protobuf field 1 (varint) = 0 → status OK │ │ │ └────── message type 0x0004 = AUTH_COMPLETE │ │ └──────────── frame payload length = 4 │ └──────────────── flags 0x03 = BULK, plaintext └─────────────────── channel 0 ``` After `AUTH_COMPLETE`, every subsequent frame sets the ENCRYPTED bit and its payload is ciphertext. v17.7 additionally enforces a **pre-auth message-type whitelist** behind `FrameworkGalFeature__tls_auth_bypass_fix`: until the handshake completes, only control messages {1, 3, 4, 11, 12, 15, 16, 255, 65535} are accepted — version, SSL, auth, ping, byebye, and the two error sentinels. Note ping (11/12) on that list: the plaintext-ping rule of [§6.3](#63-control-channel-messages) corroborated from a second direction. (v17.7 also rejects an auth-success that arrives before the handshake completes, behind the same flag, and maps `UNSUPPORTED_PROTOCOL` handshake exceptions to an "obsolete SSL" state.) ## 4.3 TLS parameters **Version: TLS 1.2** `[NORMATIVE]`. Notably, **no implementation in the corpus pins a cipher suite** — not aa-linux, aasdk, MOTO-HUB, or HeadunitPad. Only `aa-linux` pins the protocol version (min = max = TLSv1.2); the others accept whatever their library negotiates. Cipher selection is therefore left to TLS defaults in practice. On a real phone the provider is Conscrypt. The `CAR.GAL.SECURITY` package initialises an SSL context **twice** — first TLS 1.2 with `AndroidOpenSSL`, then TLS 1.2 with `GmsCore_OpenSSL` (ethical-hacking thesis, 2021; corroborated by `mretallack` APK analysis). Both are Conscrypt builds; `GmsCore_OpenSSL` is the one shipped inside Google Play Services. The APK static analysis corroborates this first-hand and sharpens it: no build sets SNI, ALPN, a cipher-suite restriction, or `setEnabledProtocols`; v17.7 additionally pins `SSLContext.getInstance("TLSv1.2")` and prefers the `GmsCore_OpenSSL` provider. The phone presents its chain — CarService leaf plus GAL root — under server alias `com.google.android.gms.car`, RSA key type only. ## 4.4 Interop hazard: the certificates violate X.509 Verified first-hand with `openssl` against `aa-proxy-rs/hu_cert.pem`. The head-unit certificate has **two independent standards violations**: **1. It is X.509 version 1.** No extensions at all — no Basic Constraints, no Key Usage, no EKU, no SKI/AKI. **2. Its validity times are malformed.** Both are ASN.1 `UTCTime` with a **numeric timezone offset instead of the mandatory `Z`** — `notBefore = 140704000000-0700`, `notAfter = 480801102123-0700` — violating RFC 5280 §4.1.2.5.1, which requires UTCTime values to end in `Z`. OpenSSL refuses at the first one it meets: ``` $ openssl verify -CAfile galroot_cert.pem hu_cert.pem error 13 at 0 depth lookup: format error in certificate's notBefore field ``` By contrast `md_cert.pem` encodes proper `Z` times (`140704000000Z` / `260722191755Z`), so this is specific to the HU certificate, not the whole PKI. **Consequence:** strict TLS stacks reject these certificates. rustls/webpki refuses v1 certificates by default, producing a handshake that looks healthy and then dies — the reporter in [rustls#2455](https://github.com/rustls/rustls/issues/2455) saw `ClientHello → ServerHello → 1464 B → 2348 B → 1286 B →` *server cuts the link*, with the library waiting for data that never arrives. The workaround was patching webpki to accept v1; see `uglyoldbob/android-auto` branch `plain-rustls`. **Choose a lenient TLS stack.** OpenSSL, BoringSSL and Conscrypt tolerate these certificates in practice. ⚠ **As the TLS server we can sidestep the v1 problem entirely** — it only bites a peer that *validates* the HU certificate. All four reference implementations disable peer verification outright: `aa-linux` `verify_mode = CERT_NONE`, MOTO-HUB `NoCheckTrustManager.kt:10-14`, HeadunitPad `SSL_CTX_set_verify(..., SSL_VERIFY_NONE, nil)`. ⚠ **A peer presenting the phone-side identity is rejected.** The real phone additionally blacklists the car's client certificate if its subject contains **"CarService"** or **"Google Automotive Link"** (v7.7/v8.2, verified in smali to avoid a jadx negation artifact) — an anti-phone↔phone / anti-loopback guard stacked on top of the PKIX chain check. A test head unit reusing a copied MD certificate fails here, not in the chain validation. ## 4.5 PKI and provisioning `[HUIG p.22]`: mutual trust rests on **two certificates signed by the Google CA** — the HU cert which the MD verifies, and the MD cert which the HU verifies. **RSA 2048, X.509.** Google issues HU certificates to integrators and recommends a unique one per make/model/year. And `[HUIG p.21]`, which is the sentence that matters most for us: > *"If the receiver library cannot verify the sender certificate, the HU terminates the connection > and disconnects AOAP."* ### 4.5.1 The Google Automotive Link hierarchy ``` Google Automotive Link root CA X.509 v3, self-signed, RSA 2048, sha1WithRSA C=US, ST=California, L=Mountain View, O=Google Automotive Link valid 2014-06-06 .. 2044-06-05 │ ├── head-unit certs (O=Android-Auto-Internal / JVC Kenwood / Google-Android-Reference) │ long-lived — 2044-2048 │ └── mobile-device certs (O=CarService) short-lived — rotated roughly yearly ``` Three byte-identical copies of the root CA exist in the corpus (`aa-proxy-rs`, `aa-linux/certs/dev`, `mretallack/app/src/main/assets`), and it matches the CA embedded in the gearhead APK — independent corroboration that this is the real trust anchor. The APK static analysis corroborates the metadata first-hand: the root is embedded as a **plaintext PEM literal** in every build (v7.7 `iwb`, v8.2 `ixl`, v17.7 `sbb`/`rze`), byte-identical across versions — serial `C14EE7A5A4544D42`, SHA-1 signature, validity 2014-06-06 → 2044-06-05, fingerprint starting `49:E5:2E:FC:13:AD:2E:D0…`. ### 4.5.2 ⚠ Every phone-side certificate in the corpus has expired Verified with `openssl x509 -checkend 0` across the whole tree on **2026-09-25**: | Certificate | Subject `O=` | Expires | Status | |---|---|---|---| | `aa-proxy-rs/galroot_cert.pem` (×3 copies) | Google Automotive Link | 2044-06-05 | valid | | `aa-proxy-rs/hu_cert.pem`, `AACS/AAClient/ssl/headunit.crt` | Android-Auto-Internal OU=01 | 2048-08-01 | valid | | `HeadunitPad/Resources/Raw/cert` | Google-Android-Reference | 2044-07-07 | valid | | `opencardev-aasdk/cert/headunit.crt` | JVC Kenwood OU=01 | 2045-04-29 | valid | | `aa-proxy-rs/md_cert.pem` | **CarService** | 2026-07-22 | **EXPIRED** | | `mretallack/carservice_cert.pem` | **CarService** | 2026-08-19 | **EXPIRED** | | `AACS/android_auto.crt`, `aa-linux/certs/dev/server_cert.pem` | **CarService OU=53** | 2022-08-24 | **EXPIRED** | The split is systematic, and AACS issue #3 explains why: head units get long validity because their software is rarely updated, phones get short validity because the AA app updates constantly. The two most recent MD certificates lapsed in **July and August 2026** — within two months of this revision date. **We are the phone.** We must present a `CarService`-class identity, and no working public one exists. This is the project's largest single risk, not a footnote. Both `aa-proxy-rs` pairs have matching private keys (verified by comparing public-key digests), so the expired GAL identity is immediately usable if the verifier's clock cooperates. The APK's own embedded fallback leaves rotate on the same schedule (metadata only): v7.7 `O=CarService, OU=56`, serial `0344`, expires 2022-11-16 → v8.2 `OU=62`, serial `0375`, expires 2023-04-05 (both notBefore 2014-07-04) → v17.7 expires **2026-12-23** — an embedded CarService leaf that is still valid at this revision date, though §4.5.5 explains why it is not directly usable. ⚠ **This is not a new problem — it dates to the very first reverse-engineering effort.** Mike Reid, in the same March 2015 XDA post that found the AOA identity strings (§1.2.1), reports extracting a Pioneer head unit's own certificate and private key from a leaked firmware image: *"I'm laughing a bit because the Pioneer SSL/RSA certificates and private keys I extracted seem to match, and should make the crypto handshake possible, though I note one of the certs expires this summer."* Real head-unit-vendor certificates going stale, and needing to be re-extracted or worked around, has been a fact of life for this protocol since day one — a decade before this project. ### 4.5.3 Is expiry actually enforced? `[CONTESTED]` | Source | Claim | Basis | |---|---|---| | `mretallack` `decrypt_key_from_apk.md` | *"head units don't check expiry"* | their test hardware | | AACS issue #3 | *"Real headunits (Seat's LG-created headunit) DO enforce certificate validation"* | Seat Ateca 2019; they rolled the HU clock back to cope | | AACS issue #3 | OpenAuto checks neither root CA nor validity dates | source inspection | | `[HUIG p.21]` | HU **MUST** terminate if it cannot verify the sender certificate | normative | **Resolution:** CA-chain validation appears universally required — the certificate must chain to the GAL root. *Expiry* enforcement is **head-unit-dependent**. The DHU and OpenAuto check neither, which is exactly why a self-signed certificate appears to work right up until you try a real car. The mirror direction is now confirmed first-hand: the **phone** enforces full PKIX — chain **and** expiry — against the *car's* client certificate (its trust store holds only the GAL root; standard PKIX validation, 7.7/8.2/17.7). The open question above is therefore strictly about the HU's treatment of the *phone's* certificate; do not read either answer as bidirectional. It also means a phone-side implementation that *does* validate the HU cert (as the real app does) needs a clock and a tolerance policy for HU certificates whose malformed UTCTimes (§4.4) leak past lenient parsers. Note also `[HUIG p.22]`: clock skew surfaces as *"Certificate not yet valid"* or *"Certificate expired"*, and the HU **MUST** obtain correct time via GPS/NITZ/NTP. Clock rollback therefore works against a conformant head unit's own obligations, and may be undone by the HU resynchronising. ### 4.5.4 Three provisioning routes | Route | vs DHU / OpenAuto | vs real car | Durability | |---|---|---|---| | **A.** Self-signed MD certificate | works — neither validates | HU-dependent; fails wherever the chain is checked | indefinite | | **B.** Expired GAL MD cert + roll back the HU clock | works (`aa-linux` ships `certs/dev/faketime_shim.c`, `LD_PRELOAD`ed by `dhu.sh:74-80`) | AACS confirmed changing the date on a Seat Ateca 2019 | fragile — HUIG requires the HU to fix its clock | | **C.** Extract a current cert + key from the gearhead APK | works | works | ~8 months, then repeat | ### 4.5.5 Route C — extracting the MD identity from the APK Procedure from `mretallack/tools/decrypt_key_from_apk.md`. The APK embeds the CarService certificate alongside an **AES-256-CBC encrypted RSA private key**. ```bash # 1. Pull the APK from a REAL phone (see caveat below) adb shell pm path com.google.android.projection.gearhead adb pull /data/app/.../base.apk /tmp/aa.apk # 2. Extract the DEX unzip /tmp/aa.apk classes.dex -d /tmp/aa_dex ``` **3. Locate four blobs.** Search JADX or the raw DEX for `-----BEGIN CERTIFICATE-----`; the class holding it also holds the key material. Obfuscated names change every release: | Blob | v16.8 | v6.4 | Size | |---|---|---|---| | Salt (KDF parameter) | `ivq` field `b` | `SslWrapper` field `o` | 256 bytes | | Encrypted private key | `ivq` field `c` | `SslWrapper` field `p` | ~1712 bytes | | CarService certificate | PEM string in the same class | — | — | | GAL root CA | PEM string | — | byte-identical across all versions | **4. Derive the key and decrypt.** The seed is device-specific data from the app's shared preferences mixed with the certificate PEM bytes, run through **7 rounds** of a custom hash, then used for `AES/CBC/PKCS5Padding`. ⚠ **Three gotchas that will cost you a day each:** - **The decryption must run on Android** (`dalvikvm`), not a desktop JVM. Android's `Base64.decode(data, 2)` is lenient — it accepts `+` and `/` and ignores whitespace — where desktop Java decoders are strict and fail. - **The APK must come from a real phone.** Mirrors like APKPure cache older builds whose byte arrays do not match the class names above. - **JADX mis-decompiles the key loop**, emitting `byte b = bArr2[i2] & 255;` where the original is `int b = bArr2[i2] & 255;`. The narrowing conversion silently corrupts the output. ⚠ **Measured caveat (APK static analysis, 7.7 / 8.2 / 17.7): the embedded fallback does not decrypt.** In all three builds the embedded AES-256-CBC blob **does not decrypt with its own embedded tables** — treat the fallback material as a decoy or rotation artifact (analysis in `previous-work/markdown/web/aa-phone-key-extraction-research.md`). Credential delivery on production devices is **Phenotype-first**: six `SenderlibCertFeature__*` flags (`__backup_key_raw`, `__backup_key_verify`, `__backup_table`, `__backup_table_verify`, `__p_table`, `__p_table_verify` — present identically since 7.7, SHA-1 verify-before-use) carry the real key material; the embedded blob is the last resort, and in these builds a broken one. The fallback KDF itself, for the record: a 48-byte rolling state seeded from a 256-byte salt, per-byte `rol1 + 33 XOR salt[i%len] XOR input`, inputs = CarService PEM bytes → GAL-root PEM bytes → 7 self-rounds; key = state[0:32], IV = state[32:48]; plaintext recovered by `skip 28, take len−54`, base64 URL_SAFE — closely matching, but not identical to, mretallack's device-seeded description above. Route C is therefore **era-dependent**: verified working on the 6.4 / 16.8-era builds mretallack analysed, measured broken as a *fallback* on 7.7+. → Appendix B (item 9). The embedded certificate expires roughly **8 months after APK release**, so this is a recurring maintenance task, not a one-off. *Distribution note.* The two implementations that ship an AA identity took opposite positions: MOTO-HUB `.gitignore`s `aa_cert` and `aa_identity_data` as "maintained privately" (`.gitignore:15-16`), while HeadunitPad commits its certificate and private key to a public repo. Worth a deliberate decision rather than a default. > **Phone-side note (this project).** Start with **route A** against the DHU — it exercises the whole > stack with zero PKI friction, and the DHU validates nothing. Move to **route C** before any > real-car testing, and budget for periodic re-extraction — but treat route C as **era-dependent** > (the ⚠ above): on 7.7+ builds the embedded fallback is a measured decoy, so budget for the > Phenotype-delivery investigation too, not just re-extraction. Route B is useful for bench work but > fights a conformant head unit's own clock-correction obligation. Whichever route, keep the > identity behind a single swappable interface: it *will* change. --- # 5. Service discovery Once TLS is up, the phone asks the head unit what it can do. The HU's answer defines every channel for the rest of the session. ``` MD→HU 0x0005 SERVICE_DISCOVERY_REQUEST HU→MD 0x0006 SERVICE_DISCOVERY_RESPONSE HU→MD 0x001A SERVICE_DISCOVERY_UPDATE -- mid-session addition of a single service ``` ## 5.1 ServiceDiscoveryRequest (MD→HU) The phone introduces itself. GAL `protos.proto:20-27`: ```protobuf message ServiceDiscoveryRequest { optional bytes small_icon = 1; // PNG optional bytes medium_icon = 2; optional bytes large_icon = 3; optional string label_text = 4; optional string device_name = 5; optional common.PhoneInfo phone_info = 6; } ``` `[CONTESTED]` — fields 4 and 5 are named differently across lineages: | Source | Field 4 | Field 5 | Field 6 | |---|---|---|---| | GAL `protos.proto:20-27` | `label_text` | `device_name` | `PhoneInfo` | | aasdk | `device_name` (required) | `device_brand` (required) | — | | mrmees (APK 16.2) | `device_name` | `device_brand` | `SessionInfo{session_uuid,…}` | Both readings put two short display strings in 4 and 5, so the wire bytes are compatible and the disagreement is about naming, not layout. aasdk predates the icon fields entirely. → Appendix B. The APK static analysis (7.7–17.7, first-hand) lands on GAL's side of the naming and adds the phone's actual field-5 content: **4 = an HU-facing label string; 5 = `Build.MANUFACTURER + " " + Build.MODEL`** — one combined string, not separate name/brand fields. Field 6 is `{1 = persistent random UUID}` and is sent **only when the car requested ≥ 1.6** (v7.7 `itj.java:222`) — the request-side twin of the §3.3 response gate. (17.7's own summary describes field 6 as "4 strings"; the v7.7 reading is the concrete one — semantics open, Appendix B item 10.) The icon fields (1–3) are car-launcher PNGs, **not** WiFi credentials. → Appendix B (item 10 tilts to GAL). ## 5.2 ServiceDiscoveryResponse (HU→MD) GAL `protos.proto:29-46`. Note how much of it is deprecated — fields 2–11 are the original flat vehicle description, superseded by `HeadUnitInfo` at field 17: ```protobuf message ServiceDiscoveryResponse { repeated Service services = 1; optional string make = 2 [deprecated = true]; optional string model = 3 [deprecated = true]; optional string year = 4 [deprecated = true]; optional string vehicle_id = 5 [deprecated = true]; optional DriverPosition driver_position = 6; optional string head_unit_make = 7 [deprecated = true]; optional string head_unit_model = 8 [deprecated = true]; optional string head_unit_software_build = 9 [deprecated = true]; optional string head_unit_software_version = 10 [deprecated = true]; optional bool can_play_native_media_during_vr = 11 [deprecated = true]; optional int32 session_configuration = 13; optional string display_name = 14; optional bool probe_for_support = 15; optional ConnectionConfiguration connection_configuration = 16; optional common.HeadUnitInfo headunit_info = 17; } ``` **Field 6 `[CONTESTED, resolved]`** — a type-level conflict, not a rename: | Source | Field 6 | |---|---| | aasdk | `bool left_hand_drive_vehicle` | | GAL `protos.proto:25` | `DriverPosition driver_position` (enum) | | mrmees (APK) | `DriverPosition driver_position` (enum) | Resolve to the **enum** — two independent lineages against aasdk's one, and a 4-value enum (`LEFT=0, RIGHT=1, CENTER=2, UNKNOWN=3`) degrades cleanly to a bool while the reverse does not. **Field 16 `[CONTESTED]`** — GAL defines `ConnectionConfiguration` here; mrmees **retracted** this field in 2026-07, attributing it to unrelated GoogleAuth data. GAL's definition is internally coherent (it nests `PingConfiguration` and `WirelessTcpConfiguration`, both of which are referenced elsewhere), so this document keeps it and flags the disagreement. → Appendix B. `session_configuration` (13) is a bitmask — `UI_CONFIG_HIDE_CLOCK=1`, `UI_CONFIG_HIDE_PHONE_SIGNAL=2`, `UI_CONFIG_HIDE_BATTERY_LEVEL=4`, `CAN_PLAY_NATIVE_MEDIA_DURING_VR=8` (`protos.proto:1348-1353`). ## 5.3 ConnectionConfiguration ```protobuf message ConnectionConfiguration { optional PingConfiguration ping_configuration = 1; optional WirelessTcpConfiguration wireless_tcp_configuration = 2; } message PingConfiguration { optional uint32 timeout_ms = 1; optional uint32 interval_ms = 2; optional uint32 high_latency_threshold_ms = 3; optional uint32 tracked_ping_count = 4; } message WirelessTcpConfiguration { optional uint32 socket_receive_buffer_size_kb = 1; optional uint32 socket_send_buffer_size_kb = 2; optional uint32 socket_read_timeout_ms = 3; } ``` This resolves an internal contradiction in the mrmees corpus, which disagreed with itself over whether field 1 was a timeout or an interval and whether the units were ms or ns. GAL is unambiguous: **field 1 is the timeout, field 2 the interval, both in milliseconds.** ## 5.4 Service — the unit of discovery ```protobuf message Service { required int32 id = 1; // becomes the channel id optional SensorSourceService sensor_source_service = 2; optional MediaSinkService media_sink_service = 3; optional InputSourceService input_source_service = 4; optional MediaSourceService media_source_service = 5; optional BluetoothService bluetooth_service = 6; optional RadioService radio_service = 7; optional NavigationStatusService navigation_status_service = 8; optional MediaPlaybackStatusService media_playback_service = 9; optional PhoneStatusService phone_status_service = 10; optional MediaBrowserService media_browser_service = 11; optional VendorExtensionService vendor_extension_service = 12; optional GenericNotificationService generic_notification_service = 13; optional WifiProjectionService wifi_projection_service = 14; } ``` **`Service.id` is the channel number** used in the frame header for that service's traffic. Exactly one of fields 2–14 is set, and *which one* identifies the service's kind. This is the mechanism that makes channel IDs dynamic — see [§6.1](#61-channel-ids-are-negotiated-not-constant). The **service-type enum** (v7.7 `irn`), carried per `Service` — the semantic identity a channel id is validated against, distinct from the per-channel sub-messages: | 0 | `UNKNOWN` | 5 | `AUDIO_SINK_MEDIA` | 10 | `NAVIGATION_STATUS` | 15 | `RADIO` | |---|---|---|---|---|---|---|---| | 1 | `CONTROL` | 6 | `AUDIO_SOURCE` (mic) | 11 | `MEDIA_PLAYBACK_STATUS` | 16 | `VENDOR_EXTENSION` | | 2 | `VIDEO_SINK` | 7 | `SENSOR_SOURCE` | 12 | `MEDIA_BROWSER` | 17 | `WIFI_PROJECTION` | | 3 | `AUDIO_SINK_GUIDANCE` | 8 | `INPUT_SOURCE` | 13 | `PHONE_STATUS` | 18 | `WIFI_DISCOVERY` | | 4 | `AUDIO_SINK_SYSTEM` | 9 | `BLUETOOTH` | 14 | `NOTIFICATION` | | | The wire channel id for control is 0, and the control endpoint carries service type 1; every other channel id is assigned by the car in `ServiceDiscoveryResponse` and validated against this type — corroborating [§6.1](#61-channel-ids-are-negotiated-not-constant) from inside the phone app. v17.7 adds service types **19** (car control / CarProperty), **20** (car local media), **21** (buffered media playback) and **22** (car intent — NAVIGATE from the car), plus dynamically allocated proxy endpoints for SDP entries it does not recognise. ⚠ `[CONTESTED]` — aasdk and mrmees call this message `ChannelDescriptor` with a `channel_id` field and a different field ordering (their fields 12–18 disagree with each other *and* with GAL). Fields 1–6 and 8 are stable across every source; **7 and 12–18 are the least reliable region in the entire corpus.** Parse tolerantly: dispatch on whichever sub-message is present, never on field number alone. → Appendix B. ## 5.5 Capability sub-messages ```protobuf message SensorSourceService { repeated Sensor sensors = 1; // message Sensor { required SensorType sensor_type = 1; } optional uint32 location_characterization = 2; // bitmask, protos.proto:1355-1365 repeated FuelType supported_fuel_types = 3; repeated EvConnectorType supported_ev_connector_types = 4; } message MediaSinkService { // HU can RECEIVE this stream optional MediaCodecType available_type = 1 [default = MEDIA_CODEC_AUDIO_PCM]; optional AudioStreamType audio_type = 2; repeated AudioConfiguration audio_configs = 3; repeated VideoConfiguration video_configs = 4; optional bool available_while_in_call = 5; optional uint32 display_id = 6; optional DisplayType display_type = 7; optional KeyCode initial_content_keycode = 8; } message MediaSourceService { // HU can SEND this stream (microphone) optional MediaCodecType available_type = 1 [default = MEDIA_CODEC_AUDIO_PCM]; optional AudioConfiguration audio_config = 2; optional bool available_while_in_call = 3; } message AudioConfiguration { required uint32 sampling_rate = 1; required uint32 number_of_bits = 2; required uint32 number_of_channels = 3; } message InputSourceService { repeated int32 keycodes_supported = 1 [packed = true]; // raw Android keycodes repeated TouchScreen touchscreen = 2; // { width=1, height=2, TouchScreenType type=3, bool is_secondary=4 } repeated TouchPad touchpad = 3; // { width=1, height=2, ui_navigation=3, physical_width=4, // physical_height=5, ui_absolute=6, tap_as_select=7, sensitivity=8 } repeated FeedbackEvent feedback_events_supported = 4; optional uint32 display_id = 5; } message BluetoothService { required string car_address = 1; repeated BluetoothPairingMethod supported_pairing_methods = 2 [packed = true]; } message NavigationStatusService { required int32 minimum_interval_ms = 1; required InstrumentClusterType type = 2; // enum { IMAGE = 1; ENUM = 2; } optional ImageOptions image_options = 3; // { height=1, width=2, colour_depth_bits=3 } } message VendorExtensionService { required string service_name = 1; repeated string package_white_list = 2; optional bytes data = 3; } // MediaPlaybackStatusService, PhoneStatusService, MediaBrowserService, // GenericNotificationService are all empty — presence alone declares the capability. ``` ⚠ `MediaSinkService.available_type` **defaults to `MEDIA_CODEC_AUDIO_PCM` (1)** — an *audio* value, even on a video channel. Do not infer stream kind from this field; infer it from whether `audio_configs` or `video_configs` is populated. `BluetoothPairingMethod`, first-hand (7.7–17.7): `−1 UNAVAILABLE, 1 OOB, 2 NUMERIC_COMPARISON, 3 PASSKEY_ENTRY, 4 PIN` — matching the Appendix C correction of aasdk, plus an `UNAVAILABLE` sentinel below the range. The phone's pairing request sends `{1 phone BT MAC, 2 method}`; a car address of the literal string `SKIP_THIS_BLUETOOTH` skips pairing — handy for bench testing. > **Phone-side note (this project).** For OSMAnd cast, advertise a minimal request (device name plus > a label) and parse the response defensively — real head units populate deprecated fields, omit > mandatory-looking ones, and use field numbers this document marks contested. Build the channel > table from what actually arrives. --- # 6. Channel architecture AAP multiplexes every logical stream over one transport connection using the channel-id byte in the frame header. ## 6.1 Channel IDs are negotiated, not constant **This is the most important correction in this document.** Nearly every source publishes a table of "the channel IDs", and every one of those tables is different — because each is that project's own convention, not a protocol constant. The mechanism: the HU's `ServiceDiscoveryResponse` contains a list of `Service` entries, each with its own `id` (§5.4). **That `id` is the channel number.** `ChannelOpenRequest` then references it: ```protobuf message ChannelOpenRequest { required sint32 priority = 1; // note: sint32, zigzag-encoded required int32 service_id = 2; } ``` Only **channel 0 = control** is fixed. It is hardcoded identically in every source and never appears as a `Service.id`. `[CONTESTED]` — six published tables, all different (the APK's service-type-keyed table below makes six): | Channel | aasdk `ChannelId.hpp` | `aa-linux` `constants.py:13-22` | `mretallack` `design.md:129-142` | MOTO-HUB / HeadunitPad | mrmees `channel-map.md` | |---|---|---|---|---|---| | Control | 0 | 0 | 0 | 0 | 0 | | Sensor | 1 | 2 | 2 | 1 | 7 | | Video | 3 | 3 | 3 | 2 | 3 | | Input | 8 | 1 | 1 | 3 | 1 | | Media audio | 4 | 4 | 4 | 6 | 4 | | Speech audio | 5 | 5 | 5 | 4 | 5 | | System audio | 6 | 6 | 6 | 5 | 6 | | Microphone | 9 | 7 | 7 | 7 | 8 | | Bluetooth | 10 | 8 | 8 | 8 | 9 | | Navigation | 12 | — | 9 | 10 | 10 | | Wi-Fi | 18 | 14 | — | 13 | 17 | **The clinching argument.** MOTO-HUB and HeadunitPad are both *head units* — the side that **originates** `ServiceDiscoveryResponse` and therefore **chooses** these numbers. Neither contains any code path that reads a peer-assigned channel id, because neither ever receives one. Their tables are HU conventions by construction. mrmees states the rule outright: *"Channel IDs in `ChannelDescriptor` are phone-assigned after the HU sends them, not fixed protocol constants… Do not hardcode channel ID assumptions."* One further corroboration from inside the phone app (7.7–17.7): its endpoint table is keyed on **service type** (§5.4's enum) — video sink 2, guidance 3, system 4, media 5, mic 6, sensors 7, input 8, Bluetooth 9, nav status 10, … WiFi projection 17 — with channel ids assigned by the car and validated against that type. That is yet another table, and its numbers match *yet another* convention: more evidence that only channel 0 is a protocol constant and everything else is per-session state. > **Phone-side note (this project).** Build the channel map at runtime from > `ServiceDiscoveryResponse`, keyed on which capability sub-message is present. Any constant named > `CHANNEL_VIDEO = 3` in our code is a bug waiting for a head unit that numbers things differently. ## 6.2 Channel lifecycle ``` HU→MD SERVICE_DISCOVERY_RESPONSE services, each with an id and a kind MD→HU CHANNEL_OPEN_REQUEST (0x0007) on the target channel, CONTROL bit set HU→MD CHANNEL_OPEN_RESPONSE (0x0008) status 0 = OK ... channel-specific setup (§7, §8, §9, §10) ... either CHANNEL_CLOSE_NOTIFICATION (0x0009) ``` ⚠ `CHANNEL_OPEN_REQUEST` is framed **on the channel being opened**, not on channel 0 — see [§2.6](#26-control-message-does-not-mean-channel-0). The phone sends **all** `CHANNEL_OPEN_REQUEST`s back-to-back in the car's service-list order — no inter-channel wait (7.7–17.7) — and media endpoints send their SETUP message immediately on a successful open. `CHANNEL_CLOSE_NOTIFICATION` from either side is acknowledged by the peer with an empty msg 9; a channel-control message that arrives out of state is answered with **msg 255** (`MESSAGE_UNEXPECTED_MESSAGE`). Whether real HUs *require* strictly sequential opens is untested → Appendix B (item 22). ## 6.3 Control channel messages `ControlMessageType`, GAL `protos.proto:1304-1332` `[CONFIRMED ×3]` — identical in the GAL proto, milek7's dissector table, and MOTO-HUB's generated `Control.java`: | ID | Name | Direction | Body | |---|---|---|---| | 1 | `MESSAGE_VERSION_REQUEST` | HU→MD | raw u16 pair (§3) | | 2 | `MESSAGE_VERSION_RESPONSE` | MD→HU | raw u16 triple (§3) | | 3 | `MESSAGE_ENCAPSULATED_SSL` | both | raw TLS bytes | | 4 | `MESSAGE_AUTH_COMPLETE` ⚠ | MD→HU (⚠) | `AuthResponse{status=1}` | | 5 | `MESSAGE_SERVICE_DISCOVERY_REQUEST` | MD→HU | §5.1 | | 6 | `MESSAGE_SERVICE_DISCOVERY_RESPONSE` | HU→MD | §5.2 | | 7 | `MESSAGE_CHANNEL_OPEN_REQUEST` | MD→HU | `ChannelOpenRequest` | | 8 | `MESSAGE_CHANNEL_OPEN_RESPONSE` | HU→MD | status | | 9 | `MESSAGE_CHANNEL_CLOSE_NOTIFICATION` | both | — | | *10* | *(absent — reserved/unused)* | | | | 11 | `MESSAGE_PING_REQUEST` | both | `PingRequest{timestamp=1,…}` | | 12 | `MESSAGE_PING_RESPONSE` | both | `PingResponse{timestamp=1,…}` | | 13 | `MESSAGE_NAV_FOCUS_REQUEST` | MD→HU | `NavFocusType` | | 14 | `MESSAGE_NAV_FOCUS_NOTIFICATION` | HU→MD | `NavFocusType` | | 15 | `MESSAGE_BYEBYE_REQUEST` | both | `ByeByeReason` | | 16 | `MESSAGE_BYEBYE_RESPONSE` | both | — | | 17 | `MESSAGE_VOICE_SESSION_NOTIFICATION` | MD→HU | `VoiceSessionStatus` | | 18 | `MESSAGE_AUDIO_FOCUS_REQUEST` | MD→HU | `AudioFocusRequestType` | | 19 | `MESSAGE_AUDIO_FOCUS_NOTIFICATION` | HU→MD | `AudioFocusStateType` | | 20 | `MESSAGE_CAR_CONNECTED_DEVICES_REQUEST` | MD→HU | — | | 21 | `MESSAGE_CAR_CONNECTED_DEVICES_RESPONSE` | HU→MD | — | | 22 | `MESSAGE_USER_SWITCH_REQUEST` | both | — | | 23 | `MESSAGE_BATTERY_STATUS_NOTIFICATION` | MD→HU | — | | 24 | `MESSAGE_CALL_AVAILABILITY_STATUS` | HU→MD | `{bool call_available=1}` | | 25 | `MESSAGE_USER_SWITCH_RESPONSE` | both | `UserSwitchStatus` | | 26 | `MESSAGE_SERVICE_DISCOVERY_UPDATE` | HU→MD | `ServiceDiscoveryUpdate{Service=1}` | | 255 | `MESSAGE_UNEXPECTED_MESSAGE` | both | — | | 65535 | `MESSAGE_FRAMING_ERROR` | both | — | ⚠ **`AUTH_COMPLETE` (4) direction `[CONTESTED]`** — the corpus tables here and in Appendix A label it MD→HU, but three independent observations all say HU→MD: aasdk's *head-unit* code is the sender (`ControlServiceChannel::sendAuthComplete`, `ControlServiceChannel.cpp:65`); aa-linux's *phone* code is the receiver (`AUTH_RECV = 4`, `protocol.py:490`); and the 7.7–17.7 APK's phone enables encryption **on receipt** of msg 4, distinguishing `CERT_EXPIRED` (−24) from `CERT_NOT_YET_VALID` (−23) in its payload — i.e. the car reports its own phone-certificate verdict in it. No source in hand positively shows a phone *sending* it. Kept unpicked per this document's policy. → Appendix B (item 20). ⚠ Ping messages (11/12) **stay plaintext**, bypassing TLS even after encryption is established (mrmees `02-version-ssl-auth.md`). Ping timestamps are microseconds since the UNIX epoch. The APK corroborates the plaintext rule from the other side — v17.7's pre-auth whitelist (§4.2) admits 11/12 alongside the handshake types — and adds the payloads: `PING_REQUEST` carries `{1 fixed64 timestamp, 2 bool (added v8.2+), 3 bytes}`; `PING_RESPONSE` echoes `{1, 2}`. Car→phone pings are **always answered**. Phone→car *active* probing exists only for wireless sessions: a self-rescheduling handler loop, interval `WirelessLatencyMonitor__probe_interval_ms` default **200 ms**, max 10 in flight, warning threshold 300 ms, stats every 5000 ms — and **no keepalive kill on missed pongs** (logging only). v17.7 adds HU-side ping-timeout detection flags (`FrameworkGalFeature__detect_hu_gal_ping_timeout`, `__hu_gal_ping_timeout_delta_ms`) and the server-pushed ping configuration of §3.3/§5.3. Enum payloads, first-hand (7.7–17.7; name table `ilo.java:113-196` in v7.7): `ByeByeReason` 1 `USER_SELECTION`, 2 `DEVICE_SWITCH`, 3 `NOT_SUPPORTED`, 4 `NOT_CURRENTLY_SUPPORTED`, 5 `PROBE_SUPPORTED` — a car-initiated ByeBye makes the phone reply 16 and tear down; `DEVICE_SWITCH` triggers handoff instead (the peer cert's DN/serial/validity are serialized into the handoff Bundle, `itj.java:276-311`); the phone delays its own ByeBye **200 ms**. `NavFocusType` 1 `NATIVE`, 2 `PROJECTED`. `AUDIO_FOCUS_NOTIFICATION` carries `{1 focus state 0..7, 2 unsolicited bool}`. Msg 17 (`VOICE_SESSION_NOTIFICATION`) is **send-only** — absent from the v7.7 receive table, actively sent by v17.7. Messages 20/21 and 22/25 are named but unhandled in 7.7/8.2. Battery status (23) carries `{1, 2 int, 3 bool charging}`. ⚠ aasdk's enum stops at 0x13 (19) and **omits `CHANNEL_CLOSE_NOTIFICATION` (9)** entirely. It also names 15/16 `SHUTDOWN_*` where every other source says `BYEBYE_*` — same wire IDs. ## 6.4 Per-channel message IDs Each channel has its own type space. **Control messages are `>= 0x8000`; bulk data is `< 0x8000`.** All from GAL `protos.proto:1586-1783`. ``` MediaMessageId (A/V channels) SensorMessageId 0x0000 DATA 0x8001 REQUEST 0x0001 CODEC_CONFIG 0x8002 RESPONSE 0x8000 SETUP 0x8003 BATCH 0x8001 START 0x8004 ERROR 0x8002 STOP 0x8003 CONFIG InputMessageId 0x8004 ACK 0x8001 INPUT_REPORT 0x8005 MICROPHONE_REQUEST 0x8002 KEY_BINDING_REQUEST 0x8006 MICROPHONE_RESPONSE 0x8003 KEY_BINDING_RESPONSE 0x8007 VIDEO_FOCUS_REQUEST 0x8004 INPUT_FEEDBACK 0x8008 VIDEO_FOCUS_NOTIFICATION 0x8009 UPDATE_UI_CONFIG_REQUEST BluetoothMessageId 0x800A UPDATE_UI_CONFIG_REPLY 0x8001 PAIRING_REQUEST 0x800B AUDIO_UNDERFLOW_NOTIFICATION 0x8002 PAIRING_RESPONSE 0x8003 AUTHENTICATION_DATA NavigationStatusMessageId 0x8004 AUTHENTICATION_RESULT 0x8001 INSTRUMENT_CLUSTER_START 0x8002 INSTRUMENT_CLUSTER_STOP MediaPlaybackStatusMessageId 0x8003 NAVIGATION_STATUS 0x8001 STATUS 0x8004 TURN_EVENT [deprecated] 0x8002 INPUT 0x8005 DISTANCE_EVENT [deprecated] 0x8003 METADATA 0x8006 NAVIGATION_STATE 0x8007 CURRENT_POSITION WifiProjectionMessageId 0x8001 CREDENTIALS_REQUEST PhoneStatusMessageId 0x8002 CREDENTIALS_RESPONSE 0x8001 STATUS 0x8002 INPUT GenericNotificationMessageId 0x8001 SUBSCRIBE 0x8003 MESSAGE MediaBrowserMessageId 0x8002 UNSUBSCRIBE 0x8004 ACK 0x8001 ROOT_NODE … 0x8006 BROWSE_INPUT ``` ### A/V message IDs `[CONTESTED, mostly reconcilable]` The two major lineages agree on the *numbers* and differ on *names* — these are aliases, not conflicts: | ID | GAL / milek7 | aasdk | Same message? | |---|---|---|---| | 0x8000 | `SETUP` | `SETUP_REQUEST` | yes | | 0x8001 | `START` | `START_INDICATION` | yes | | 0x8002 | `STOP` | `STOP_INDICATION` | yes | | 0x8003 | `CONFIG` | `SETUP_RESPONSE` | **yes** — the HU's reply to SETUP | | 0x8004 | `ACK` | `AV_MEDIA_ACK_INDICATION` | yes | | 0x8005 | `MICROPHONE_REQUEST` | `AV_INPUT_OPEN_REQUEST` | **yes** — open the mic | | 0x8006 | `MICROPHONE_RESPONSE` | `AV_INPUT_OPEN_RESPONSE` | yes | | 0x8007 | `VIDEO_FOCUS_REQUEST` | `VIDEO_FOCUS_REQUEST` | yes | | 0x8008 | `VIDEO_FOCUS_NOTIFICATION` | `VIDEO_FOCUS_INDICATION` | yes | Genuine disagreements remained above 0x8008, where mrmees retracted an earlier `VideoFocusNotification@0x8009` reading, cascading a one-slot shift through `MediaStats` and `MediaOptions`. **Resolved by the APK static analysis**: 7.7, 8.2 and 17.7 all dispatch `0x8009 UPDATE_UI_CONFIG_REQUEST` (car→phone; the phone must not apply a theme pushed from this direction), `0x800A UPDATE_UI_CONFIG_REPLY` (phone→car) and `0x800B AUDIO_UNDERFLOW_NOTIFICATION` (car→phone, stats only) — GAL's reading, exactly as listed above. → Appendix B (item 15 resolved). ⚠ `aa-linux/constants.py` defines `AV_BINDING_REQUEST = 0x8002` and `AV_STOP_INDICATION = 0x8005`, contradicting both lineages. It interoperates only because it never sends either. Do not copy. ⚠ milek7's dissector table omits 0x8007 and mislabels 0x8008 as the *request*. Trust the enum. ⚠ **Vendor extension (16) is a raw pipe.** Its endpoint overrides message dispatch entirely: a raw byte stream with **no 2-byte message-type header**, both directions — the only channel whose frames are not `[type][protobuf]`. (v7.7 `iwh`; corroborated across 8.2 and 17.7 — see the endpoint table in Appendix D.) The mic channel's car→phone DATA arrives as media message **1** — not the `0x0000` DATA id used on sink channels — see §8.4. ## 6.5 Status codes `MessageStatus` (GAL `protos.proto:1784+`) is shared across nearly every response message in the protocol. `SUCCESS = 0`, `UNSOLICITED_MESSAGE = 1`, and **everything else is negative**: | Value | Name | Value | Name | |---|---|---|---| | -1 | `NO_COMPATIBLE_VERSION` | -9 | `INVALID_SENSOR` | | -2 | `CERTIFICATE_ERROR` | -10…-17 | Bluetooth pairing / HFP errors | | -3 | `AUTHENTICATION_FAILURE` | -18 | `KEYCODE_NOT_BOUND` | | -4 | `INVALID_SERVICE` | -19 | `RADIO_INVALID_STATION` | | -5 | `INVALID_CHANNEL` | -20 | `INVALID_INPUT` | | -6 | `INVALID_PRIORITY` | -21, -22 | radio preset / comm errors | | -7 | `INTERNAL_ERROR` | -23 | `AUTHENTICATION_FAILURE_CERT_NOT_YET_VALID` | | -8 | `MEDIA_CONFIG_MISMATCH` | -24 | `AUTHENTICATION_FAILURE_CERT_EXPIRED` | ⚠ Two consequences. In protobuf, negative `int32` enum values encode as **10-byte varints** — they are not compact. And where a status is carried as a raw u16 instead (the version response, §3.1), `-1` appears on the wire as `0xFFFF`. ⚠ aasdk collapses this entire taxonomy to `enum Status { OK=0; FAIL=1; }`. Any implementation built on aasdk cannot distinguish "certificate expired" from "invalid channel" — relevant when debugging against a real car, where -23/-24 are exactly the errors you expect (§4.5). --- # 7. Video stream One H.264 elementary stream, MD→HU, on a channel whose `Service` carried a `MediaSinkService` with `video_configs` populated. ## 7.1 Normative requirements `[HUIG p.8]` — head-unit decoder requirements: > *"H.264/AVC Baseline Profile hardware decoder · H.264 BP level 3.1 REQUIRED for both sides to > guarantee 800x480 · H.264 BP level 3.2 REQUIRED for 720p · H.264 BP level 4.2 REQUIRED for 1080p · > Max frame rate of 30 FPS or 60 FPS · Any AAP HU integration MUST support 480p at 30 FPS as the > minimum default."* Frame rate may be continuous or non-continuous between the maximum and 5 FPS. `[HUIG p.29-30]`: - Format: **H.264 elementary byte stream**, Baseline Profile. - **Only I-frames and P-frames.** Buffering set to minimum; the HU SHOULD decode each frame without waiting for additional frames. - Maximum bitrates: **800×480 → 4000 kbit/s · 1280×720 → 6000 kbit/s · 1920×1080 → 8000 kbit/s.** ## 7.2 Resolution negotiation `[HUIG p.30]` — four steps, and note the HU proposes while the MD decides: 1. During service discovery the MD reads the `VideoConfiguration` list the HU supports. 2. After the channel opens, the HU sends `CONFIG` with a **prioritised list of indices** into that list, most preferred first. 3. The MD picks one, weighing HU preference against its own encoder capability. 4. The MD sends `START` carrying the chosen `configuration_index`. `[HUIG p.30]`: *"The HU SHOULD request the highest resolution video that the protocol supports."* ```protobuf message VideoConfiguration { optional VideoCodecResolutionType codec_resolution = 1; optional VideoFrameRateType frame_rate = 2; optional uint32 width_margin = 3; optional uint32 height_margin = 4; optional uint32 density = 5; // Android density bucket optional uint32 decoder_additional_depth = 6; // extra decoder buffer frames optional uint32 viewing_distance = 7; // mm; VW MIB3 sends 900, DHU 500 optional uint32 pixel_aspect_ratio_e4 = 8; // ×10000; 10000 = square pixels optional uint32 real_density = 9; // true DPI before bucket quantisation optional MediaCodecType video_codec_type = 10; optional UiConfig ui_config = 11; // margins, content insets, UiTheme } ``` `VideoCodecResolutionType` `[CONFIRMED ×3]` (`protos.proto:1422-1433`): | Value | Resolution | | Value | Resolution | |---|---|---|---|---| | 1 | 800×480 | | 6 | 720×1280 (portrait) | | 2 | 1280×720 | | 7 | 1080×1920 (portrait) | | 3 | 1920×1080 | | 8 | 1440×2560 (portrait) | | 4 | 2560×1440 | | 9 | 2160×3840 (portrait) | | 5 | 3840×2160 | | | | `VideoFrameRateType`: `VIDEO_FPS_60 = 1`, `VIDEO_FPS_30 = 2` `[CONFIRMED ×3]`. `MediaCodecType` (`protos.proto:1439-1447`): | 1 | `AUDIO_PCM` | 2 | `AUDIO_AAC_LC` | 3 | `VIDEO_H264_BP` | 4 | `AUDIO_AAC_LC_ADTS` | |---|---|---|---|---|---|---|---| | 5 | `VIDEO_VP9` | 6 | `VIDEO_AV1` | 7 | `VIDEO_H265` | | | ⚠ **aasdk's `VideoResolution` enum is wrong for values ≥ 5** — it defines only three values and its names for 5–9 do not correspond to the table above. Anything built on aasdk's names will advertise the wrong resolution. ⚠ `VideoConfiguration.video_codec_type` inherits `MediaSinkService.available_type`'s default of `AUDIO_PCM (1)`. An unset or unrecognised codec falls back to H.264 phone-side. ⚠ **Field 6 semantics `[CONTESTED, minor]`** — GAL names it `decoder_additional_depth`; the 7.7 APK's parameter set reads it as a **layout param, default 4**. Same field number and wire position, differing semantics. Treat as opaque unless the HU documents it. → Appendix B (item 24). ## 7.3 Channel setup sequence ``` MD→HU 0x8000 SETUP Setup { MediaCodecType type = 1 } -- e.g. VIDEO_H264_BP (3) HU→MD 0x8003 CONFIG Config { Status status = 1, -- STATUS_WAIT=1, STATUS_READY=2 uint32 max_unacked = 2, repeated uint32 configuration_indices = 3 } MD→HU 0x8007 VIDEO_FOCUS_REQUEST -- resolved MD→HU, see below HU→MD 0x8008 VIDEO_FOCUS_NOTIFICATION MD→HU 0x8001 START Start { int32 session_id = 1, uint32 configuration_index = 2 } MD→HU 0x0001 CODEC_CONFIG -- SPS/PPS MD→HU 0x0000 DATA … -- frames HU→MD 0x8004 ACK Ack { int32 session_id = 1, uint32 ack = 2, repeated uint64 receive_timestamp_ns = 3 } MD→HU 0x8002 STOP ``` ⚠ **Video focus must be granted before frames are sent.** ```protobuf message VideoFocusRequestNotification { optional int32 disp_channel_id = 1 [deprecated = true]; optional VideoFocusMode mode = 2; optional VideoFocusReason reason = 3; } message VideoFocusNotification { optional VideoFocusMode focus = 1; optional bool unsolicited = 2; } ``` `VideoFocusMode`: `PROJECTED=1, NATIVE=2, NATIVE_TRANSIENT=3, PROJECTED_NO_INPUT_FOCUS=4`. `VideoFocusReason`: `UNKNOWN=0, PHONE_SCREEN_OFF=1, LAUNCH_NATIVE=2`. A focus loss is not an instantaneous teardown: the phone arms a **15000 ms video-focus-loss timer** before reacting (observed in all three builds). ⚠ aasdk models focus as binary `FOCUSED`/`UNFOCUSED` — wrong; the real protocol has four states, and `PROJECTED_NO_INPUT_FOCUS` (projection visible but input routed to the native UI) has no representation in the binary model. **Direction, resolved.** `channel-map.md`'s summary table lists `VideoFocusRequest` as HU→MD, but mrmees's own per-channel documentation is explicit and unambiguous the other way — `video.md` states outright: *"`VideoFocusRequest` (0x8007, Phone → HU)... The phone is the **initiator** of focus transitions... and the HU **responds** with the resulting state (via `VideoFocusIndication`)."* `04-channel-lifecycle.md` agrees. Treat `channel-map.md`'s summary row as the error — it is a table transposition, not a second data point. **MD→HU is correct;** the message name itself (`…RequestNotification`, sent by the side wanting focus) is consistent with this. ## 7.4 Media frame payload ``` DATA (0x0000) [u64be timestamp, milliseconds][H.264 Annex-B NAL units] CODEC_CONFIG (0x0001) [codec configuration bytes] -- NO timestamp ``` The **8-byte big-endian timestamp on DATA frames only** is the corpus's strongest independent cross-corroboration. MOTO-HUB derives it as a "2- or 10-byte header" counting the message type (`AaAudioTap.kt:55-57`, `AapVideo.kt:5-6`); HeadunitPad, written separately in Swift against live traffic, checks for an Annex-B start code at offset 8 before falling back (`AapTransport.swift:1941-1942`); `aa-linux/video.py:593-598` builds it as `struct.pack(">Q", timestamp_ms) + h264`. ⚠ **`CODEC_CONFIG` format is ambiguous in the wild `[CONTESTED]`.** `[HUIG p.29]` says the stream is an *elementary byte stream* (Annex-B). But `aa-linux` **sends an AVCC/`avcC` record** as its codec config, and HeadunitPad carries an explicit normalisation fallback for length-prefixed config it has observed, while MOTO-HUB assumes pure Annex-B unconditionally. **Accept both:** sniff for the Annex-B start code (`00 00 01` / `00 00 00 01`) and treat anything else as an `avcC` record. → Appendix B. ⚠ **Never drop a fragment carrying NAL type 7 (SPS) or 8 (PPS)** — HeadunitPad records this as a hard-won bug: losing them makes every subsequent frame undecodable. ## 7.5 Flow control `Config.max_unacked` sets the window; the sender decrements on each frame and replenishes on `ACK (0x8004)`. Values are pure implementation choice and span an order of magnitude: | Implementation | `max_unacked` | |---|---| | `aa-linux` | 3 | | mrmees (observed) | ~10 video, ~1 audio | | MOTO-HUB | 12 video / 16 other | | HeadunitPad | 30, all channels | From protocol version **5.0** onward an "ackless audio" mode exists (§3.3) in which per-buffer acks are not sent at all. The APK adds the hard limits: more than **400 outstanding unacked frames ⇒ fatal `CAR_NOT_RESPONDING`** (all three builds); session ids start at 1, increment per START, and are compared **mod 256**. Pair those with a conservative `max_unacked` — the table above spans an order of magnitude for a reason. > **Phone-side note (this project).** v1 advertises exactly one configuration — > `VIDEO_1280x720` + `VIDEO_FPS_30`, `VIDEO_H264_BP` — encoded with MediaCodec, and defers H.265. > Prepend SPS/PPS to every keyframe so each message is self-contained; three codebases arrived at > this independently. Cap the bitrate at HUIG's 6000 kbit/s for 720p — `aa-linux`'s 25 Mbps default > is four times over the normative limit and will not behave on real hardware. (Note §7.6: the real > app itself exceeds HUIG here — 12 Mbps for 720p over USB — so HUIG's ceiling is the safe choice, > not the only working one.) ## 7.6 Observed phone-side encoder parameters (APK 7.7 / 8.2 / 17.7) The real app's H.264 encoder: **Baseline profile**, surface color-format (2130708361), level selected from the larger dimension — 1280 → L3.1, 1920 → L3.2/4.0, 2560 → L5.1, 3840 → L5.1/5.2 — with 60 fps **doubling** the bitrate. Note the tension with [§7.1](#71-normative-requirements): the observed levels for 1080p (L3.2/4.0) sit *below* HUIG's normative BP 4.2 requirement, and the observed **bitrate defaults exceed HUIG's 2016 maxima outright** — the app ships 1080p at **16 Mbit/s (USB) / 3 M HEVC / 5 M WiFi / 1.5 M WiFi-HEVC; 720p at 12/2/4/0.7 M; 480p at 8/1/3/0.3 M**, with 2560 → 1.5× the 1080p rate and 3840 → 2×, I-frame interval 60, and wireless QP clamped 15–30 (USB unclamped) — all Phenotype-tunable ([§11](#11-phone-app-tunables-phenotype-flags)). HUIG's numbers describe 2016 head units, not current behaviour: a conservative implementation stays within HUIG, a byte-compatible one treats those maxima as long-since abandoned by the phone side. Other gates observed: **2.4 GHz WiFi links block >720p** unless developer-whitelisted; and the 7.7/8.2 encoders actually support only **H264_BP and H265** — the codec enum's other video values (VP9, AV1) exist in the enum, not in the encoders. --- # 8. Audio streams ## 8.1 Codecs — PCM is the requirement, not AAC `[NORMATIVE]` `[HUIG p.38]`. This table contradicts the common belief that AAP audio is AAC-LC: | Stream | Direction | Transport | Codec | Format | |---|---|---|---|---| | UI | MD→HU | AAP | **PCM (REQUIRED)** + AAC-LC (OPTIONAL) | 16-bit, 16 kHz, mono | | Guidance | MD→HU | AAP | **PCM** | 16-bit, 16 kHz, mono | | Voice | MD→HU | AAP | **PCM (REQUIRED)** + AAC-LC (OPTIONAL) | 16-bit, 16 kHz, mono | | Media | MD→HU | AAP | **PCM (REQUIRED)** + AAC-LC (OPTIONAL) | 16-bit, 48 kHz, stereo | | Microphone | HU→MD | AAP | **PCM** | 16-bit, ≥16 kHz, mono | | Legacy in-call | both | **Bluetooth SCO** | HFP 1.5 (CVSD) | — | Three independent implementations are **PCM-only** and never advertise AAC-LC: MOTO-HUB, HeadunitPad, and aasdk-based head units. Their configurations match exactly — 48000/16/2 for media, 16000/16/1 for speech, system, and microphone. `[HUIG p.38 fn.4]` on the optional AAC path: *"AAC-lc format is: RAW AAC frame without header, decoded buffer size in one frame: 1024 samples for 16 kHz, 2048 samples for 48 kHz."* So AAC frames are **raw access units, not ADTS** — `aa-linux` transcodes to ADTS then strips the headers before sending. ⚠ **There is no Opus support.** Audio is strictly PCM or AAC-LC. ⚠ Phone-call audio does **not** ride AAP. It uses Bluetooth HFP/SCO — the `TELEPHONY` stream type exists for routing and mixing decisions, not for carrying call audio. **Observed phone-side acceptance (7.7–17.7):** the phone accepts **only 48000 or 16000 Hz, 16-bit, 1 or 2 channels** from the car — no 8000, no 44100. The media channel requires 48 kHz stereo; guidance and system **prefer 48 kHz mono** and fall back to 16 kHz mono (a dev preference can force 16 kHz; `TELEPHONY` is unsupported on GAL in these builds). Note the drift from HUIG's table above, which specifies 16 kHz for UI/guidance/voice: the phone side moved to 48 kHz in newer builds while the wire format stayed identical. PCM buffers: **2048 samples per message at 48/44.1 kHz, 1024 at 16 kHz**, 16-bit, doubled for stereo. The AAC encoder, when used: `audio/mp4a-latm`, LC profile, bitrate `min(rate × channels × 8, 512000)` — raw access units, per the footnote above. ## 8.2 Stream roles Each audio channel is one `MediaSinkService` distinguished by `audio_type`. `AudioStreamType` `[CONTESTED]` — four numberings, only `MEDIA = 3` agreed by all: | Source | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |---|---|---|---|---|---|---|---|---| | GAL `protos.proto:1449` | — | `GUIDANCE` | `SYSTEM_AUDIO` | `MEDIA` | `TELEPHONY` | | | | | aasdk `AudioType` | `NONE` | `SPEECH` | `SYSTEM` | `MEDIA` | `ALARM` | | | | | MOTO-HUB `Media.java:408` | `NONE` | `SPEECH` | `SYSTEM` | `MEDIA` | `ALARM` | `GUIDANCE` | `ANNOUNCEMENT` | `RING` | | mrmees (APK) | `TELEPHONY` | `SYSTEM_AUDIO` | — | `MEDIA` | — | `GUIDANCE` | | | MOTO-HUB and mrmees both place `GUIDANCE` at 5, partially bridging the headunit lineage and the APK reading — but GAL puts it at 1. The 7.7–17.7 APK static analysis now corroborates **GAL's** numbering from inside the phone: its audio-channel mapping is built on `audioStreamType` 1=guidance, 2=system, 3=media. Tilt: GAL + APK against MOTO-HUB + mrmees — still not closed, since the headunit lineage is field-tested on its own convention and both cannot be right for the same bytes. → Appendix B (item 18). ## 8.3 Setup and payload Identical to video (§7.3) — `SETUP`/`CONFIG`/`START`/`DATA`/`ACK` — with `Setup.type = MEDIA_CODEC_AUDIO_PCM (1)` or `AUDIO_AAC_LC (2)`, and `AudioConfiguration` in place of `VideoConfiguration`. ⚠ **Audio focus is requested on the control channel, not the audio channel** — `0x0012` `AUDIO_FOCUS_REQUEST` / `0x0013` `AUDIO_FOCUS_NOTIFICATION` (§6.3). ```protobuf message AudioFocusRequestNotification { required AudioFocusRequestType request = 1; } message AudioFocusNotification { required AudioFocusStateType focus_state = 1; optional bool unsolicited = 2; } ``` `AudioFocusRequestType`: `GAIN=1, GAIN_TRANSIENT=2, GAIN_TRANSIENT_MAY_DUCK=3, RELEASE=4`. ⚠ aasdk names value 3 `GAIN_NAVI`; it is the same wire value — the Android-mirrored name `GAIN_TRANSIENT_MAY_DUCK` is the accurate one. `AudioFocusStateType`: `INVALID=0, GAIN=1, GAIN_TRANSIENT=2, LOSS=3, LOSS_TRANSIENT_CAN_DUCK=4, LOSS_TRANSIENT=5, GAIN_MEDIA_ONLY=6, GAIN_TRANSIENT_GUIDANCE_ONLY=7`. ### ⚠ Payload offset ambiguity `[CONTESTED]` — field-observed HeadunitPad found two variants in the wild and had to sniff between them (`AapTransport.swift:1990-2002`): ``` variant 1: [8-byte timestamp][PCM] variant 2: [2-byte media msg type][8-byte timestamp][PCM] ``` Its comment: *"If we always assume one variant, the other produces severe static."* MOTO-HUB uses a fixed offset keyed on the message type and may be over-simplified. A robust receiver should heuristically check whether the leading two bytes parse as a known media message type. → Appendix B. ## 8.4 Microphone (HU→MD) ```protobuf message MicrophoneRequest { required bool open = 1; optional bool anc_enabled = 2; optional bool ec_enabled = 3; optional int32 max_unacked = 4; } message MicrophoneResponse { required int32 status = 1; optional int32 session_id = 2; } ``` `[HUIG p.73]` is unusually prescriptive: PCM, 16 kHz, 16-bit, mono, buffer size 2048 (frames MUST be multiples of it); **noise reduction MUST be disabled for single-mic systems** and **automatic gain control MUST be disabled** — Google's speech recognition does its own processing and double-processing degrades accuracy. The APK obeys: its `MICROPHONE_REQUEST` sends fields 2/3 (ANC/EC) as **false** (7.7–17.7). The phone opens the mic with 0x8005, acks the car's frames with 0x8004, and waits on a **5000 ms semaphore** for the mic config. Observed quirk worth a capture check: the car's mic DATA arrives as media message **1** — not the `0x0000` DATA id used on sink channels — with the same 8-byte timestamp prefix (§6.4). > **Phone-side note (this project).** OSMAnd cast needs the guidance stream (16 kHz mono PCM) and > media (48 kHz stereo). Implement PCM first — it is the requirement, the simplest path, and what > every other implementation actually ships. Microphone is not needed for v1. --- # 9. Input channel HU→MD. All events arrive in one `InputReport` on message `0x8001`. ```protobuf message InputReport { required uint64 timestamp = 1; // microseconds (elapsedRealtime) optional int32 disp_channel_id = 2 [deprecated = true]; optional TouchEvent touch_event = 3; optional KeyEvent key_event = 4; optional AbsoluteEvent absolute_event = 5; optional RelativeEvent relative_event = 6; optional TouchEvent touchpad_event = 7; // same type as field 3, different surface } ``` ⚠ **Field 2 resolves a corpus conflict.** aasdk has `disp_channel` at field 2; mrmees says field 2 "does not exist". GAL shows it **deprecated** — so it existed, was retired, and newer APKs no longer emit it. Both sources were right about different eras. Skip it on read; never write it. ## 9.1 Touch ```protobuf message TouchEvent { repeated Pointer pointer_data = 1; // { uint32 x = 1; uint32 y = 2; uint32 pointer_id = 3; } optional uint32 action_index = 2; // which pointer this action refers to optional PointerAction action = 3; } ``` `PointerAction` — these are Android `MotionEvent` constants: | 0 | `ACTION_DOWN` | 1 | `ACTION_UP` | 2 | `ACTION_MOVED` | |---|---|---|---|---|---| | 3 | `CANCEL` | 4 | `OUTSIDE` | 5 | `ACTION_POINTER_DOWN` | | 6 | `ACTION_POINTER_UP` | | | | | Values 3 and 4 are absent from the GAL enum but present in the headunit lineage (`Input.java:1828-1895`), which completes the set. ⚠ **Coordinates are absolute pixels in the negotiated video resolution** — not normalised, not scaled. There is no pressure field; `aa-linux` notes the phone synthesises a constant 0.8. Touchscreen and touchpad share the message type, distinguished only by whether it arrives in field 3 or field 7. ## 9.2 Keys ```protobuf message KeyEvent { repeated Key keys = 1; // message Key { uint32 keycode = 1; bool down = 2; uint32 metastate = 3; bool longpress = 4; } } ``` ⚠ **`keycode` carries raw Android `KeyEvent` keycodes**, not a closed protocol enum. `ButtonCodeEnum` in the various protos is a convenience listing, not an exhaustive type. Standard Android values apply (`KEYCODE_HOME=3`, `KEYCODE_BACK=4`, `KEYCODE_DPAD_UP=19`, `KEYCODE_MEDIA_PLAY_PAUSE=85`, `KEYCODE_VOLUME_UP=24`, …), plus an AA-specific block from **65536** upward: | 65536 | `SCROLL_WHEEL` / rotary controller | 65537 | `MEDIA` | |---|---|---|---| | 65538 | `NAVIGATION` | 65539 | `RADIO` | | 65540 | `TEL` | 65541 | `PRIMARY_BUTTON` | ⚠ **Media transport control exists only here.** There is no play/pause message on the media-status channel; a head unit's physical media buttons arrive as `KeyEvent` keycodes. ## 9.3 Rotary and absolute ```protobuf message RelativeEvent { repeated Rel data = 1; } // { uint32 keycode = 1; int32 delta = 2; } message AbsoluteEvent { repeated Abs data = 1; } // { uint32 keycode = 1; int32 value = 2; } ``` Rotary encoders send `RelativeEvent` with `keycode = 65536` and a signed `delta`; zero deltas are dropped. `AbsoluteEvent` with `keycode = 65541` (`PRIMARY_BUTTON`) maps to a D-pad centre press. The 7.7–17.7 APK names 65536 `ROTARY_CONTROLLER` — same wire value as `SCROLL_WHEEL`, a different lineage's name — and routes its deltas to `AXIS_SCROLL` on source 8194: trust the value, not the name. ## 9.4 Haptic feedback (MD→HU) `INPUT_FEEDBACK (0x8004)` carries `InputFeedback { optional FeedbackEvent event = 1; }`, where `FeedbackEvent` is `FEEDBACK_SELECT=1, FOCUS_CHANGE=2, DRAG_SELECT=3, DRAG_START=4, DRAG_END=5`. The HU advertises which it supports in `InputSourceService.feedback_events_supported`. > **Phone-side note (this project).** Touch is the only input OSMAnd needs. Map `pointer_id` through > to Android `MotionEvent` pointer indices so multitouch gestures survive; the coordinate space is > already ours, since we chose the resolution in §7.2. --- # 10. Sensor channel MD→HU is the *request* direction; the HU pushes data back. The phone **subscribes per sensor type**, then receives batches. ``` MD→HU 0x8001 SENSOR_REQUEST SensorRequest { SensorType type = 1; int64 min_update_period = 2; } HU→MD 0x8002 SENSOR_RESPONSE SensorResponse { MessageStatus status = 1; } HU→MD 0x8003 SENSOR_BATCH SensorBatch { ... } HU→MD 0x8004 SENSOR_ERROR SensorError { SensorType = 1; SensorErrorType = 2; } ``` One request per sensor type. `SensorErrorType`: `SENSOR_OK=1, TRANSIENT=2, PERMANENT=3`. A `min_update_period` of **−1 unsubscribes** (7.7–17.7), and the phone waits synchronously — **2000 ms** — on the `SENSOR_RESPONSE` before giving up. ## 10.1 The structural key **`SensorBatch` field numbers are identical to `SensorType` values.** Every field is `repeated`: ```protobuf message SensorBatch { repeated LocationData location_data = 1; repeated CompassData compass_data = 2; repeated SpeedData speed_data = 3; repeated RpmData rpm_data = 4; repeated OdometerData odometer_data = 5; repeated FuelData fuel_data = 6; repeated ParkingBrakeData parking_brake_data = 7; repeated GearData gear_data = 8; repeated DiagnosticsData diagnostics_data = 9; repeated NightModeData night_mode_data = 10; repeated EnvironmentData environment_data = 11; repeated HvacData hvac_data = 12; repeated DrivingStatusData driving_status_data = 13; repeated DeadReckoningData dead_reckoning_data = 14; repeated PassengerData passenger_data = 15; repeated DoorData door_data = 16; repeated LightData light_data = 17; repeated TirePressureData tire_pressure_data = 18; repeated AccelerometerData accelerometer_data = 19; repeated GyroscopeData gyroscope_data = 20; repeated GpsSatelliteData gps_satellite_data = 21; repeated TollCardData toll_card_data = 22; } ``` `[CONFIRMED ×2]` — HeadunitPad independently hand-writes `fieldBytes(13, drivingStatus)` for driving status, sensor type 13. ## 10.2 Sensor types 1–21 are agreed by **all six lineages**. 22 is in GAL and mrmees but absent from the headunit lineage, which stops at 21 — and now corroborated by the 7.7–17.7 APK static analysis, a second independent APK-era source (types 1–22, including `TOLL_CARD` at 22). 23–26 are modern APK additions. | 1 | `LOCATION` | 8 | `GEAR` | 15 | `PASSENGER` | 22 | `TOLL_CARD` | |---|---|---|---|---|---|---|---| | 2 | `COMPASS` | 9 | `DIAGNOSTICS` | 16 | `DOOR` | 23 | `VEHICLE_ENERGY_MODEL` | | 3 | `CAR_SPEED` | 10 | `NIGHT_MODE` | 17 | `LIGHT` | 24 | `TRAILER` | | 4 | `RPM` | 11 | `ENVIRONMENT` | 18 | `TIRE_PRESSURE` | 25 | `RAW_VEHICLE_ENERGY_MODEL` | | 5 | `ODOMETER` | 12 | `HVAC` | 19 | `ACCELEROMETER` | 26 | `RAW_EV_TRIP_SETTINGS` | | 6 | `FUEL` | 13 | `DRIVING_STATUS` | 20 | `GYROSCOPE` | | | | 7 | `PARKING_BRAKE` | 14 | `DEAD_RECKONING` | 21 | `GPS_SATELLITE` | | | ## 10.3 Payloads — note the scaled integers There are no floats anywhere. Values are fixed-point, with the scale encoded in the field name (`_e3` = ×1000, `_e7` = ×10⁷): ```protobuf message LocationData { optional uint64 timestamp = 1 [deprecated = true]; required int32 latitude_e7 = 2; // degrees × 10^7 required int32 longitude_e7 = 3; optional uint32 accuracy_e3 = 4; // metres × 1000 optional int32 altitude_e2 = 5; // metres × 100 optional int32 speed_e3 = 6; // m/s × 1000 optional int32 bearing_e6 = 7; // degrees × 10^6 } message CompassData { int32 bearing_e6 = 1; int32 pitch_e6 = 2; int32 roll_e6 = 3; } message SpeedData { int32 speed_e3 = 1; bool cruise_engaged = 2; int32 cruise_set_speed = 4; } message RpmData { int32 rpm_e3 = 1; } message OdometerData { int32 kms_e1 = 1; int32 trip_kms_e1 = 2; } message FuelData { int32 fuel_level = 1; int32 range = 2; bool low_fuel_warning = 3; } message ParkingBrakeData { bool parking_brake = 1; } message GearData { Gear gear = 1; } message NightModeData { bool night_mode = 1; } message EnvironmentData { int32 temperature_e3 = 1; int32 pressure_e3 = 2; int32 rain = 3; } message DrivingStatusData{ int32 status = 1; } message DeadReckoningData{ int32 steering_angle_e1 = 1; repeated int32 wheel_speed_e3 = 2; } ``` ⚠ **`LocationData.timestamp` (field 1) resolves another corpus conflict** the same way field 2 of `InputReport` did: aasdk has it required, mrmees says numbering starts at 2, GAL shows it **deprecated**. It existed and was retired. ⚠ `SpeedData` has **no field 3** — it jumps from `cruise_engaged` (2) to `cruise_set_speed` (4). aasdk guesses field 3 is a bool `cruise_set_speed`; GAL shows the gap is real. ## 10.4 The two that change head-unit behaviour **Night mode** (type 10) — a single bool driving the HU's dark theme. `[HUIG]` strongly recommends it. ⚠ HeadunitPad does not advertise it at all, while MOTO-HUB only sends it after an explicit subscription — so a phone must not assume the HU will ask. **Driving status** (type 13) — an `int32` **bitmask**. The individual bits are `[CONFIRMED ×3]`, identical across GAL, aasdk and the headunit lineage: | Bit value | GAL name (`protos.proto:1565-1572`) | |---|---| | 0 | `DRIVE_STATUS_UNRESTRICTED` | | 1 | `DRIVE_STATUS_NO_VIDEO` | | 2 | `DRIVE_STATUS_NO_KEYBOARD_INPUT` | | 4 | `DRIVE_STATUS_NO_VOICE_INPUT` | | 8 | `DRIVE_STATUS_NO_CONFIG` | | 16 | `DRIVE_STATUS_LIMIT_MESSAGE_LEN` | ⚠ aasdk and the headunit lineage additionally define `FULLY_RESTRICTED = 31`. **The GAL enum does not** — it stops at 16. Since 31 is just `1|2|4|8|16`, this is a convenience constant rather than a protocol difference, but do not expect to find it in the GAL definitions. This is the mechanism behind driver-distraction lockout. `[HUIG]` requires `DRIVING_STATUS` to be present in every session. Both headunit-lineage implementations always report `UNRESTRICTED`. `Gear` is a **closed, non-contiguous** enum `[CONFIRMED ×2]`: `GEAR_NEUTRAL=0`, `GEAR_1=1` … `GEAR_10=10`, then a jump to `GEAR_DRIVE=100`, `GEAR_PARK=101`, `GEAR_REVERSE=102`. (The headunit lineage spells the ordinals `FIRST`…`TENTH`; the values are identical.) Being a closed enum, unknown values must be rejected rather than passed through. ## 10.5 EV energy model (types 23–26) `[CONFIRMED ×2, independent]` — mrmees APK decompilation and openautolink's reverse engineering, the latter verified on an AAOS emulator and a real **Chevrolet Blazer EV (C234, 2024)**: | Type | Name | Wrapper | |---|---|---| | 23 | `SENSOR_VEHICLE_ENERGY_MODEL_DATA` | empty message | | 24 | `SENSOR_TRAILER_DATA` | — | | 25 | `SENSOR_RAW_VEHICLE_ENERGY_MODEL` | `bytes` | | 26 | `SENSOR_RAW_EV_TRIP_SETTINGS` | `bytes` | Flow: the HU advertises 23/25/26 plus `FUEL_TYPE_ELECTRIC` and its EV connector types → the phone requests type 23 at session setup → the HU reads its VHAL (`EV_BATTERY_LEVEL`, `INFO_EV_BATTERY_CAPACITY`, `RANGE_REMAINING`) → sends a `VehicleEnergyModel` protobuf in a `SensorBatch` → Maps computes battery-on-arrival estimates. ⚠ **The energy-model payload is opaque to the Android Auto app** — it is passed straight through to GMS. A phone-side implementation never needs to parse it. Note also that Maps interprets `min_usable_capacity` as the *current* state of charge, not a floor. > **Phone-side note (this project).** We are the **consumer** here: the car supplies sensors, we > subscribe. GPS for OSMAnd comes from the phone's own fix, not the car — subscribe to `LOCATION` > only if we intend to prefer the vehicle's dead-reckoned position. Subscribe to `NIGHT_MODE` and > `DRIVING_STATUS` from day one; they are the two that must visibly change our behaviour. --- # 11. Phone-app tunables (Phenotype flags) The real gearhead app is remote-configured through **Phenotype** (Gservices) string flags named `__` with in-code defaults; the registration XMLs in `res/xml/` only register the groups — defaults live in code, not in the XMLs or the `assets/phenotype/*.binarypb` blobs (those are registration metadata, not flag values). Counts: **7.7 = 1011**, **8.2 = 973** (114 new, 152 removed), **17.7 = 1174** unique flag names. Full lists are kept as fixtures in `previous-work/misc/fixtures/`: `gearhead-flags-{7.7,8.2,17.7}.txt`, plus `gearhead-flags-new-in-8.2.txt` / `gearhead-flags-removed-in-8.2.txt`. Protocol-relevant groups and where their effects land in this document: | Group | Acts on | Where | |---|---|---| | `SenderlibCertFeature__*` (6 flags) | CarService credential delivery, SHA-1 verify | §4.5.5 | | `FrameworkGalFeature__fragment_size`, `__fragment_size_for_wifi`, `__framer_send_buffer_size(_for_wifi)` | frame/fragment sizing (16128 defaults) | §2.5 | | `FrameworkGalFeature__use_sequence_numbers`, `__tls_auth_bypass_fix`, `__gal_ping_configuration`, `__use_ping_configuration`, `__detect_hu_gal_ping_timeout` (v17.7) | pre-auth whitelist, ping config | §3.3, §4.2 | | `WirelessLatencyMonitor__probe_interval_ms` (200), `__report_interval_ms` (5000) | wireless ping cadence | §6.3 | | `WirelessProjectionInGearhead__*` | BT-MAC/name kill-switches, nearby, RSSI −50 threshold, HU denylist | §1.3 | | `VideoEncoderParams__*` | bitrates, I-frame interval 60, QP clamps | §7.6 | | `AudioFocus*__*`, `AudioPolicy__*` | audio-focus timeouts, single-channel capture, routing kill-switches | §8 | | `GalProtocolConditionalRequirementsForMinimumAppVersionsFeature__*` (v17.7) | minimum app versions | — | New-in-8.2 of note: the nearby-connections wireless set, audio-focus timeouts, handoff `bypass_first_activity_q_and_below`, `setup_timeout`. Removed-in-8.2: all `FrameRateLimiter__*`, `AudioBufferApproximationsWith*__*`, `wireless_sdp_manager_use_gh_version_kill_switch`. --- # Appendix A — Message ID quick reference Channel 0 is the control channel. All other channels are negotiated (§6.1); "channel kind" below means the `Service` sub-message that declared them. ## Control channel (0) | ID | Name | Dir | Body | |---|---|---|---| | 0x0001 | `VERSION_REQUEST` | HU→MD | raw u16 ×2 | | 0x0002 | `VERSION_RESPONSE` | MD→HU | raw u16 ×3 (+ optional protobuf ≥1.6) | | 0x0003 | `ENCAPSULATED_SSL` | both | raw TLS bytes | | 0x0004 | `AUTH_COMPLETE` ⚠ | MD→HU (⚠) | `AuthResponse` | | 0x0005 | `SERVICE_DISCOVERY_REQUEST` | MD→HU | `ServiceDiscoveryRequest` | | 0x0006 | `SERVICE_DISCOVERY_RESPONSE` | HU→MD | `ServiceDiscoveryResponse` | | 0x0007 | `CHANNEL_OPEN_REQUEST` | MD→HU | `ChannelOpenRequest` | | 0x0008 | `CHANNEL_OPEN_RESPONSE` | HU→MD | status | | 0x0009 | `CHANNEL_CLOSE_NOTIFICATION` | both | — | | 0x000B | `PING_REQUEST` | both | `PingRequest` (plaintext) | | 0x000C | `PING_RESPONSE` | both | `PingResponse` (plaintext) | | 0x000D | `NAV_FOCUS_REQUEST` | MD→HU | `NavFocusType` | | 0x000E | `NAV_FOCUS_NOTIFICATION` | HU→MD | `NavFocusType` | | 0x000F | `BYEBYE_REQUEST` | both | `ByeByeReason` | | 0x0010 | `BYEBYE_RESPONSE` | both | — | | 0x0011 | `VOICE_SESSION_NOTIFICATION` | MD→HU | `VoiceSessionStatus` | | 0x0012 | `AUDIO_FOCUS_REQUEST` | MD→HU | `AudioFocusRequestNotification` | | 0x0013 | `AUDIO_FOCUS_NOTIFICATION` | HU→MD | `AudioFocusNotification` | | 0x0014 | `CAR_CONNECTED_DEVICES_REQUEST` | MD→HU | — | | 0x0015 | `CAR_CONNECTED_DEVICES_RESPONSE` | HU→MD | — | | 0x0016 | `USER_SWITCH_REQUEST` | both | — | | 0x0017 | `BATTERY_STATUS_NOTIFICATION` | MD→HU | `{1, 2 int, 3 bool charging}` (gate: car ≥ 1.4) | | 0x0018 | `CALL_AVAILABILITY_STATUS` | HU→MD | `{bool call_available=1}` | | 0x0019 | `USER_SWITCH_RESPONSE` | both | `UserSwitchStatus` | | 0x001A | `SERVICE_DISCOVERY_UPDATE` | HU→MD | `ServiceDiscoveryUpdate` | | 0x00FF | `UNEXPECTED_MESSAGE` | both | — | | 0xFFFF | `FRAMING_ERROR` | both | — | ⚠ msg 4's direction is contested (§6.3, Appendix B item 20). ## A/V channels (`MediaSinkService` / `MediaSourceService`) | ID | Name | Dir | Body | |---|---|---|---| | 0x0000 | `DATA` | either | `[u64be ts ms][codec bytes]` | | 0x0001 | `CODEC_CONFIG` | MD→HU | codec config, no timestamp | | 0x8000 | `SETUP` | MD→HU | `Setup` | | 0x8001 | `START` | MD→HU | `Start` | | 0x8002 | `STOP` | MD→HU | `Stop` | | 0x8003 | `CONFIG` (aasdk: `SETUP_RESPONSE`) | HU→MD | `Config` | | 0x8004 | `ACK` | HU→MD | `Ack` | | 0x8005 | `MICROPHONE_REQUEST` (aasdk: `AV_INPUT_OPEN_REQUEST`) | MD→HU | `MicrophoneRequest` | | 0x8006 | `MICROPHONE_RESPONSE` (aasdk: `AV_INPUT_OPEN_RESPONSE`) | HU→MD | `MicrophoneResponse` | | 0x8007 | `VIDEO_FOCUS_REQUEST` | MD→HU | `VideoFocusRequestNotification` | | 0x8008 | `VIDEO_FOCUS_NOTIFICATION` | HU→MD | `VideoFocusNotification` | | 0x8009 | `UPDATE_UI_CONFIG_REQUEST` | HU→MD | `UpdateUiConfigRequest` (phone must not apply the pushed theme) | | 0x800A | `UPDATE_UI_CONFIG_REPLY` | MD→HU | `UpdateUiConfigReply` | | 0x800B | `AUDIO_UNDERFLOW_NOTIFICATION` | HU→MD | `AudioUnderflowNotification` (stats only) | IDs 0x8009–0x800B resolved in GAL's favour by the APK (§6.4, Appendix B item 15); directions are the APK's. Item 16 (`VideoFocusRequest` MD→HU) resolved — see §7.3. ## Other channels | Channel kind | ID | Name | Dir | |---|---|---|---| | `SensorSourceService` | 0x8001 / 0x8002 / 0x8003 / 0x8004 | `REQUEST` / `RESPONSE` / `BATCH` / `ERROR` | MD→HU / HU→MD ×3 | | `InputSourceService` | 0x8001 / 0x8002 / 0x8003 / 0x8004 | `INPUT_REPORT` / `KEY_BINDING_REQUEST` / `KEY_BINDING_RESPONSE` / `INPUT_FEEDBACK` | HU→MD, then MD→HU | | `BluetoothService` | 0x8001…0x8004 | `PAIRING_REQUEST` / `PAIRING_RESPONSE` / `AUTHENTICATION_DATA` / `AUTHENTICATION_RESULT` | both | | `NavigationStatusService` | 0x8001…0x8007 | `INSTRUMENT_CLUSTER_START` / `STOP` / `NAVIGATION_STATUS` / `TURN_EVENT`† / `DISTANCE_EVENT`† / `NAVIGATION_STATE` / `CURRENT_POSITION` | 0x8001/0x8002 HU→MD (cluster start/stop); 0x8003–0x8007 MD→HU | | `MediaPlaybackStatusService` | 0x8001 / 0x8002 / 0x8003 | `STATUS` / `INPUT` / `METADATA` | MD→HU; 0x8002 HU→MD (command enum 0..7) | | `PhoneStatusService` | 0x8001 / 0x8002 | `STATUS` / `INPUT` | 0x8001 MD→HU (call list, per-call state enum 0..6); 0x8002 HU→MD (call action) | | `MediaBrowserService` | 0x8001…0x8006 | `ROOT_NODE` … `BROWSE_INPUT` | MD→HU | | `GenericNotificationService` | 0x8001…0x8004 | `SUBSCRIBE` / `UNSUBSCRIBE` / `MESSAGE` / `ACK` | both | | `WifiProjectionService` | 0x8001 / 0x8002 | `CREDENTIALS_REQUEST` (v17.7+, empty) / `CREDENTIALS_RESPONSE` | 0x8001 MD→HU / 0x8002 HU→MD | | `RadioService` | 0x8001…0x8019 | active notification, step, seek, scan, tune, program list, presets, spacing, station info, mute, traffic, source, state | both (full car-radio control set) | † deprecated. --- # Appendix B — Contested values and how to settle them This is the project's open-questions backlog. Each entry names the experiment that would resolve it. | # | § | Question | Positions | Experiment | |---|---|---|---|---| | 1 | 1.2.1 | ~~AOA model string~~ **DOWNGRADED (web + APK)** | Both `Android Auto` and `Android Open Automotive Protocol` independently confirmed working, a decade apart (2015 Mike Reid, 2016 HUIG/2018 apserver/2025 HeadunitPad) — APK accepts both, and its own USB filter accepts a third pair, `Android`/`Android` (§1.2.1) | Low priority — try `Android Open Automotive Protocol` first per the recommendation; all three pairs are known-good. | | 2 | 1.3 | ~~RFCOMM channel~~ **LIKELY RESOLVED (web research)** | 8 (mrmees ×3 + independent community troubleshooting knowledge) vs 22 (mretallack, uncorroborated) | Low priority — `sdptool browse` a real head unit to convert LIKELY to CONFIRMED. | | 3 | 1.3 | Is HFP required before wireless AA? | Required (mrmees failure code); HUIG p.23 says not required but predates wireless; `headunit-revived` (web research) treats its own HFP server as a discovery aid, not a dependency | Attempt the wireless handoff with HFP disconnected against both a real gearhead phone and `headunit-revived`; the two may legitimately differ. | | 4 | 1.3 | ~~`WifiSecurityMode` numbering~~ **RESOLVED (web + APK)** | bitmask (WPA2=8) — live production firmware in `aa-proxy-rs` **and** first-hand in the 17.7 APK (adds WPA3=32, WPA2_WPA3=40); sequential (WPA2=5) only in the extracted GAL proto | **Resolved:** bitmask; emit 8, accept 5 defensively. A capture remains nice-to-have. | | 5 | 1.6 | Which TCP port | 5277 / 5288 / 5289 / 30515 | **Resolved:** negotiated via `WifiStartRequest.port`. No experiment needed. | | 6 | 2.3 | FIRST-frame header size | 8 bytes (×4 sources) vs 6 (milek7 dissector) | **Resolved** by weight of evidence; confirm by hand-decoding a captured fragmented message. | | 7 | 3.2 | Protocol version to advertise | 1.1 / 1.2 / 1.6 / 1.7 / 2.0; APK max-supported ladder 4.0 → 4.1 → 6.1 with "≤1.7 → answer 1.7, else answer max" (§3.2) | Log `VERSION_REQUEST` major/minor from a real head unit; log gearhead's response to each. | | 8 | 3.2 | Is HeadunitPad's 2.0 real? | Nothing else sits in 2.x | Send 2.0 to real gearhead and inspect the response and any feature gating. | | 9 | 4.5.3 | Do head units enforce cert expiry? | "don't check" (mretallack) vs "DO enforce" (AACS, Seat LG); phone-side enforcement of the *car's* cert (chain + expiry) is confirmed — the open half is HU-side only (§4.5.3); route C's embedded fallback measured as a decoy on 7.7+ (§4.5.5) | Present an expired GAL cert to several head units; record which reject with status −24. | | 10 | 5.1 | `ServiceDiscoveryRequest` fields 4/5 (+6) | `label_text`+`device_name` (GAL) vs `device_name`+`device_brand` (aasdk, mrmees); APK 7.7–17.7 lands with GAL — 4 = label, 5 = `Build.MANUFACTURER + " " + Build.MODEL` combined; field 6 = persistent UUID at car ≥ 1.6 ("4 strings" in 17.7's summary — semantics open) | Capture a real request from gearhead; compare the two strings against the phone's name and brand. | | 11 | 5.2 | `ServiceDiscoveryResponse` field 6 | bool (aasdk) vs `DriverPosition` enum (GAL, mrmees) | **Resolved** to enum; confirm by reading field 6 from a right-hand-drive head unit. | | 12 | 5.2 | Field 16 `ConnectionConfiguration` | present (GAL) vs retracted (mrmees 2026-07); strengthened — v17.7 appends `{1:{1:{1..4 ping-config}}}` to the version response at car ≥ 1.6 (first-hand), matching GAL's nesting (§3.3, §5.3) | Request version ≥1.6 and inspect whether the response carries a field-16 submessage. | | 13 | 5.4 | `Service` fields 7, 12–18 | every source differs | Enumerate a real `ServiceDiscoveryResponse` and map present field numbers to observed behaviour. | | 14 | 6.1 | Channel ID assignment | Six published tables, all different | **Resolved:** IDs come from `Service.id` in `ServiceDiscoveryResponse`; only channel 0 is fixed. Confirm by opening the same head unit twice and checking whether IDs stay stable across sessions. | | 15 | 6.4 | ~~A/V message IDs above 0x8008~~ **RESOLVED (APK)** | GAL's `0x8009/0x800A/0x800B` reading confirmed first-hand in APK 7.7/8.2/17.7; mrmees's retraction cascade was the wrong reading (§6.4) | **Resolved** in GAL's favour. | | 16 | 7.3 | ~~`VideoFocusRequest` direction~~ **RESOLVED (web research)** | `channel-map.md`'s HU→MD was a table transposition; `video.md`'s own prose is explicit: MD→HU | Settled — `video.md` states it directly, no experiment needed. | | 17 | 7.4 | `CODEC_CONFIG` framing | Annex-B (HUIG, MOTO-HUB) vs avcC (aa-linux sends it; HeadunitPad observed it) | Log the first bytes of 0x0001 from gearhead — `00 00 01` vs a length prefix. | | 18 | 8.2 | `AudioStreamType` numbering | four different tables; only `MEDIA=3` agreed; APK 7.7–17.7 tilts to GAL (1=guidance, 2=system, 3=media) against MOTO-HUB/mrmees's `GUIDANCE=5` | Open all three audio channels and correlate each `audio_type` with what actually plays. | | 19 | 8.3 | Audio payload offset | `[ts][PCM]` vs `[type][ts][PCM]` | Log leading 10 bytes of audio DATA per head unit; check whether bytes 0–1 parse as a media type. | | 20 | 6.3 | `AUTH_COMPLETE` direction | MD→HU (corpus tables) vs HU→MD (aasdk sends it; aa-linux receives it; APK phone enables encryption on receipt, §6.3) | Sniff one real session — the sender of msg 4 settles it. | | 21 | 3.1 | `VersionRequestOptions` "snapshot" (field 2, fixed64) | appended to `VERSION_REQUEST` behind a parse flag; purpose beyond logging unknown | Send one carrying flags/snapshot to gearhead and observe behavioural differences. | | 22 | 6.2 | Channel-open ordering on the car side | phone sends all msg 7s back-to-back; whether real HUs *require* strict sequential opens is untested | Open channels out of order against a real HU and record which reject. | | 23 | 1.3 | `WifiInfoResponse` field 5 | `access_point_type` (aa-linux et al.) vs `status` (17.7 APK `RESPONSE_INFO` map) | Capture one real `WifiInfoResponse` and read field 5. | | 24 | 7.2 | `VideoConfiguration` field 6 semantics | `decoder_additional_depth` (GAL) vs layout param default 4 (APK 7.7) | Send both readings to a real HU and diff behaviour. | | 25 | App D | `res/*.proto` binary configs | unknown — varint records plus int sets {15,16,18,20,21}/{1,3,4,8,9,12}, likely message-ID/channel allowlists (Clearcut/instrumentation config) | Pin the loader class in the 7.7/8.2 decompiles. | # Appendix C — Known-bad sources Places where a source is confidently wrong. Listed so these errors are not re-imported. ## aasdk (all forks) | What | Wrong | Actual | |---|---|---| | `VideoResolution` | 3 values; names wrong for ≥5 | 9 values (§7.2) | | `VideoFocusMode` | binary `FOCUSED`/`UNFOCUSED` | 4 states (§7.3) | | `VideoFocusReason` | `UNK_1`, `UNK_2` placeholders | `PHONE_SCREEN_OFF=1`, `LAUNCH_NATIVE=2` | | `ShutdownReason` | `NONE=0, QUIT=1` | 8 named reasons (`ByeByeReason`) | | `Status` | `OK=0, FAIL=1` | 34-value signed taxonomy (§6.5) | | `BluetoothPairingMethod` | `UNK_1`, `A2DP`, `UNK_3` | `OOB=1, NUMERIC_COMPARISON=2, PASSKEY_ENTRY=3, PIN=4` | | `ControlMessage` enum | stops at 0x13; omits 0x0009 | through 0x001A (§6.3) | aasdk remains authoritative on **framing and transport** — it is the best source for §2. ## aa-linux Its Python *code* is live-tested and reliable; these are defects to avoid copying. - `AV_BINDING_REQUEST = 0x8002` / `AV_STOP_INDICATION = 0x8005` — contradicts both lineages (§6.4). Harmless only because it never sends them. - `WifiInfoResponse` field names are swapped in its generated stub (fields 2/3); the code compensates by reading the wrong attribute names. The canonical order is `ssid=1, key=2, bssid=3`. - The audio-channel queueing branch in service-discovery handling is dead code — audio channels are never opened in that build. - `SERVICE_DISCOVERY_UPDATE (0x001A)` is defined but never handled. - Default video bitrate is 25 Mbps — four times HUIG's 720p limit (§7.1). - Its `.proto` files are a **stale vendored snapshot of mrmees**, not independent evidence. ## HeadunitPad - **Dead, wrong message-type constants** superseded by live testing but still present in the source and citable-looking: `KEY_CODE_EVENT=101`, `VIDEO_FOCUS_REQUEST=102`, `VIDEO_FOCUS_ACK=103`, `SENSOR_EVENT=200`, `VIDEO_CONFIG=300`, `VIDEO_FRAME=301`, `AUDIO_CONFIG=400`, `AUDIO_FOCUS=401`, `AUDIO_FRAME=402`. **None are real AAP message types.** Its live dispatch correctly uses 0x0000/0x0001/0x8000–0x8003. - Message 17 is mislabelled `AUDIO_FOCUS_RESPONSE`; it is `VOICE_SESSION_NOTIFICATION`. - Its USB path is explicitly unstable (self-disconnects, black screen despite frames arriving); wireless is the tested path. ## MOTO-HUB - Reads the extended length field on a literal `flags == 0x09`, so a fragmented **control** frame (`0x0D`) would be mis-parsed (§2.2). - **Zero unit tests** for frame parsing, TLS, or message handling; the repo is a single squashed commit. Its only protocol test covers the QR-pairing payload writer. ## milek7 dissector (`androidauto.lua`) - Reads a single 4-byte length on FIRST frames; contradicts four other sources (§2.3). - `messagemapping[2]` omits 0x8007 and mislabels 0x8008. - Channel map (0=control, 1=sensor, 2=media, 3=input) is explicitly described by its own author as a hardcoded convenience assumption. ## mrmees Self-correcting, with dated retractions — check for a retraction note before relying on any entry. Known: `VideoFocusNotification@0x8009` (retracted 2026-03-06, cascading one slot through `MediaStats`/`MediaOptions`); `ServiceDiscoveryResponse` field 16 (retracted 2026-07, but GAL defines it — Appendix B item 12); a duplicate `SensorStartRequest`; a 3-value `BluetoothPairingStatus` superseded by the shared status enum. ⚠ **`wireless-bluetooth-setup.md` cites three companion scripts that were never committed** (`sdp_clean.c`, `aa-combined.py`, `bt-agent.py` — confirmed by the org's own issue tracker, `open-android-auto` #19, and by a sister document's admission that they were "test scripts on Pi at `/tmp/`"). Nothing to recover; not a defect in the *protocol* claims, just don't expect the files. ## mrmees-openauto-prodigy — a sibling repo, and its own internal contradiction A separate, more production-oriented mrmees project (Qt/C++ head-unit implementation). Its `docs/archive/openauto-pro/bluetooth-wireless-aa-setup.md` is a near-duplicate of `open-android-auto`'s wireless doc, dated **2024-02-24**, hardware-tested (BlueZ 5.82, Raspberry Pi 4, Moto G Play, Samsung S25 Ultra) — treat it as a second, independent, *dated* confirmation of everything the two documents share (RFCOMM channel discovery, the AA-only SDP record, HFP AG being required), not merely a copy. ⚠ **But its production code disagrees with its own documentation.** `src/core/aa/BluetoothDiscoveryService.cpp:36-40` defines `kMsgWifiStartResponse = 6` and `kMsgWifiConnectionStatus = 7` — swapped relative to the doc's own table (§1.3), relative to `aa-linux`, relative to `aa-proxy-rs`, and relative to the 17.7 APK's link-level table. Four sources agree with the doc; only this one C++ file disagrees, with itself. If you ever consult this file directly, correct IDs 6 and 7 before trusting anything else in it. ## mretallack - `decrypt_key_from_apk.md`'s extraction procedure is **era-dependent**: verified working on the 6.4 / 16.8-era builds it analysed, but the APK static analysis measured the embedded fallback blob as non-decrypting in 7.7/8.2/17.7 — a decoy/rotation artifact, with Phenotype delivering the real credentials (§4.5.5). Not wrong for its era; not to be assumed for current builds. ## Not AAP at all `mossyhub/openautolink` `docs/protocol.md` describes that project's own app↔bridge protocol. Its ports (5288/5289/5290), its 16-byte video header and its 8-byte audio header have nothing to do with this specification. --- # Appendix D — Source map Where to go to re-verify a claim, and what that source is worth. | Claim class | Primary source | Corroborate with | |---|---|---| | Frame format, framing rules | `opencardev-aasdk/src/Messenger/{FrameHeader,FrameSize}.cpp` | `aa-linux/frame.py`, MOTO-HUB `AapReadSingleMessage.kt` | | Message IDs, enums, protobuf schemas | `milek7-galdocs/protos.proto` (= `opencardev-aasdk/docs/protos.proto`) | mrmees `oaa/`, MOTO-HUB generated `*.java` | | Requirements, policy, focus semantics | `markdown/milek7-galdocs/head-unit-integration-guide-v1.3.md` | — (normative) | | Handshake ordering, state machine | `aa-linux/protocol.py`, `[HUIG p.15]` | milek7 `apserver.c` (AOA only) | | TLS mechanics | `aa-linux/crypto.py`, HeadunitPad `OpenSslTlsHandler.swift` | aasdk `SSLWrapper.cpp`, MOTO-HUB `AapSslContext.kt` | | Certificates / PKI | `aa-proxy-rs/*.pem` (inspect with `openssl`) | `mretallack` APK analysis, AACS issue #3 | | AOA2 / USB | `f1xpl-aasdk/.../USB/AccessoryMode*Query`, `[HUIG p.19-20]` | HeadunitPad `AndroidOpenAccessory.swift` | | Wireless (Bluetooth) | mrmees `wireless-bluetooth-setup.md` | `aa-linux/wireless.py`, aa-proxy-rs, nisargjhaveri | | Wireless (QR / deep link) | MOTO-HUB `AaWirelessPairing.kt` | — (single source, original RE) | | Modern APK behaviour, feature gates | mrmees `docs/interactions/02-version-ssl-auth.md` | `mretallack/docs/research.md`, `misc/fixtures/gearhead-reference-notes.md` (first-hand for 7.7/8.2/17.7) | | Phone-app behaviour, gearhead 7.7 / 8.2 / 17.7 | `previous-work/misc/fixtures/gearhead-reference-notes.md` | `previous-work/source/apk/jadx-v{77,82,17}/` trees, baksmali for jadx artifacts | | EV sensors | openautolink EV energy-model RE | mrmees `sensor.md` | ## APK dump anchor map (gearhead 7.7 / 8.2 / 17.7) Obfuscated names are reassigned between builds; this map is the cross-build index for the three decompiled gearhead dumps in `previous-work/source/apk/jadx-v{77,82,17}/` (persisted baksmali trees alongside, for checking jadx decompile artifacts). Roles, per build: | Role | v7.7 | v8.2 | v17.7 | |---|---|---|---| | Control-channel state machine | `itj` | `ivj` | `rze` (legacy `s` stack) + `jcj` (refactored `j` stack; both live, identical wire behaviour) | | Endpoint base class | `ivk` | `iwx` | `jdg`/`say` family (endpoint per channel) | | Frame writer | `itw` | `ivw` | — | | Framer/session (reader+writer) | `itx` | `ivx` | — | | SSL wrapper + KDF fallback | `iwd` | `ixn` | `jep` / `uga.bl` (KDF), `uga.I` (key extract) | | Key manager (GAL root + chain) | `iwb` | `ixl` | `sbb` | | Fallback credential provider | `iwa` | `ixk` | `jen` | | Phenotype credential provider | `iwc`/`rfd`/`rff` | `ixm`/`rke`/`rkg` | `jeo`/`adhz`/`adib` | | Version value class | `ivr` | `ixe` | `sau` / `jef` | | Status enum | `nhf` | `nlw` | `xqp` | | Wireless (WPP) TLS socket setup | `gdc` | `gnc` | — | Per-channel endpoints (the channel column is the **service type** of §5.4, not a wire channel id): | Type | Role | v7.7 | v8.2 | v17.7 | |---|---|---|---|---| | 1 | control | `itj` | `ivj` | `rze`/`jcj` | | 2 | video out | `iwj extends ius` | `cmy` | `jhc` | | 3/4/5 | guidance / system / media audio out | `ise` (stream param 1/2/3) | `clu` | `rya` | | 6 | mic in | `ivc extends iur` | `cmc` | `jfy` | | 7 | sensors | `ivy` | `cmu`/`ixi` | `say`/`jel` | | 8 | input | `ium` | `clz` | `jdg` | | 9 | Bluetooth pairing | `isi` | `iui` | `rye` | | 10 | nav/cluster status | `ive` | `cmm` | `jdq` | | 11 | media playback | `iuq` | `cmb` | `jdk` | | 12 / 14 | media browser / notification | presence-only | same | same | | 13 | phone status | `ivg` | `cmo` | `jdw` | | 15 | radio | `ivu` | `cms` | `jei` | | 16 | vendor extension | `iwh` (raw pipe) + `cap` (no-op) | `cmw` + `cef` | `jes` | | 17 | WiFi projection | `iwm` | `cnb` | `jeu` | | — | unhandled service ids | `ixj` no-op sink | same pattern | runtime proxy `jgn` wrapping `jgi` | | 19–22 | car control / local media / buffered media / car intent | — | — | `jbb` / `jbi` / `jfa` / `jbg` | v17.7 uniquely contains **two complete GAL stacks** (`s*` legacy and `j*` refactored), both live, selected by a service factory; wire constants agree between them. Log tags are stable across builds: `CAR.GAL.GAL`, `CAR.GAL.AUDIO/VIDEO/MIC/INPUT/SENSOR/BT/WIFI_PROJ/RADIO-EP`, `CAR.VENDOR`, plus v17.7's `CAR.GAL.CAR_CONTROL`, `CAR.GAL.CAR_LOCAL_MEDIA`, `CAR.MEDIA.BUFFERED`, `CAR.GAL.CAR_INTENT`. **What the dumps contain (and don't):** no `.proto` **sources** and no certificate stores beyond the §4 material (root PEM literal + encrypted fallback blob); four small **binary** protobuf configs (`res/{2MO,EFE,8-l,bBj}.proto` — varint records and int sets, likely instrumentation allowlists — Appendix B item 25); `assets/phenotype/*.binarypb` (flag-registration metadata, not values — §11); `assets/sdk_impl.jar` (~724 KB, the CarService dynamite client loader, *not* the GAL protocol core — decompiled at `jadx-v{77,82}-sdkimpl/`); `assets/first_party_common_packages.txt` (Google first-party app-launch allowlist, not protocol). ## Independence warning Six lineages, and two pairs are **not** independent: ``` GAL binary ──► milek7 protos.proto ──► vendored into opencardev-aasdk/docs/ HUIG v1.3 (standalone, normative) aasdk ──► f1xpl ──► opencardev ──► emirhaluci (one lineage, three forks) mrmees oaa ──► vendored into mfont-bz17/aa-linux (aa-linux protos are NOT independent) mikereidis/headunit ──► headunit-revived ──► MOTO-HUB (HeadunitPad matches it closely) gearhead APK notes (7.7/8.2/17.7) (independent; overlaps mrmees in subject only) ``` Counting "four sources agree" is only meaningful if they come from different rows. ## A seventh lineage: the 2015 origin thread One source predates every lineage above and sits outside the fork tree entirely: **Mike Reid's original XDA reverse-engineering thread** (`source/web/[CLOSED] Headunit app for Android Auto...html`, and its companion release thread `source/web/[Android 4.1+] Headunit for Android Auto...html`), 2015-03-31 onward — the root of the *entire* community effort (every implementation in this document ultimately traces back to `mikereidis/headunit`, which began here). It predates HUIG by a year and is a primary field-research source, not a fork of anything. Read directly (not web-search-summarized) for this revision; supplied two corrections (§1.2.1, §4.5.2) that neither web search nor the rest of the corpus surfaced. Its content is narrative forum prose, not code, so it corroborates *facts* (a string worked, a cert existed) rather than byte-level schemas. ## Corrections from this revision's web research pass Three items moved from `[CONTESTED]` toward resolved after checking public sources beyond this corpus (2026-09-25): `VideoFocusRequest` direction (§7.3, fully resolved — mrmees's own `video.md` prose is unambiguous, `channel-map.md`'s table was a transposition), `WifiSecurityMode` numbering (§1.3, resolved toward bitmask — `aa-proxy-rs` is deployed production firmware, not just an extracted proto), and the AOA model string (§1.2.1, downgraded from a real conflict to two long-independently-confirmed values). One new complication surfaced: `headunit-revived` treats HFP as optional even for its wireless path, weakening mrmees's "required" claim (§1.3). One source caveat surfaced: mrmees's own issue tracker confirms `wireless-bluetooth-setup.md` cites companion scripts that don't exist in the repo — a reason to want a second source for anything resting on that document alone. Items not reachable this way — `AudioStreamType` numbering, `CODEC_CONFIG` Annex-B vs AVCC, and cert-expiry enforcement by specific real head units — remain open; they need a live capture or a real car, not more searching. ## Corrections from the gearhead APK static-analysis merge Same revision date, second pass — merging `previous-work/misc/fixtures/gearhead-reference-notes.md` (lineage 6). **Resolved:** `WifiSecurityMode` is a bitmask, first-hand in 17.7 (incl. WPA3=32 / WPA2_WPA3=40 — §1.3); A/V message IDs 0x8009–0x800B follow GAL's reading, confirmed in three builds (§6.4); the ≥1.4 version gate's purpose is the battery-status send (§3.3); the negotiated `max_fragment_size` default is 16128, not 0x4000 (§2.5); the AOA accepted set has three model strings, not two (§1.2.1). **Newly contested:** `AUTH_COMPLETE` direction — every implementation observation says HU→MD, the corpus tables say MD→HU (§6.3, Appendix B item 20). **Newly caveated:** route C certificate extraction is era-dependent — the embedded fallback is a measured decoy on 7.7+ (§4.5.5, Appendix C). The 6.1 ping-config append (§3.3) corroborates GAL's `ConnectionConfiguration` nesting (Appendix B item 12), and the APK's `audioStreamType` 1/2/3 mapping tilts the `AudioStreamType` numbering toward GAL (§8.2). ## Hardware verification status | Implementation | Verified against | |---|---| | `aa-linux` | Google DHU (live) | | aasdk / openauto | real cars, years of deployment | | AACS | Seat Ateca 2019 LG head unit; Fiat Tipo Uconnect in progress | | HeadunitPad | Xiaomi phone over USB (audio confirmed); wireless is the mature path | | MOTO-HUB | no protocol tests; forked code assumed working | | openautolink EV | AAOS emulator + Chevrolet Blazer EV C234 (2024) | | mrmees | VW MIB3 OI, Google DHU 2.1, Sony XAV-AX100 firmware, APK 16.1–17.3 | | gearhead APK notes (lineage 6) | static analysis only — no hardware; primary first-hand source for phone-side behaviour (7.7/8.2/17.7) | | mrmees-openauto-prodigy (wireless BT) | BlueZ 5.82 / Raspberry Pi 4 against a Moto G Play (Android 14) and a Samsung S25 Ultra, dated 2024-02-24 | | `aa-proxy-rs` | deployed production firmware behind commercially-sold wireless AA dongle hardware | --- *End of specification.*