# yaris-mediacenter — Bluetooth Audio Bridge for the Car **Status:** `PLANNING` — do not start building without owner approval. A new Pi-side service that acts as a transparent Bluetooth middleman between a phone (Spotify, any audio app) and the car radio. The Pi broadcasts as a Bluetooth A2DP sink that the phone connects to (phone thinks it's the car stereo), receives the audio stream, then re-broadcasts it as an A2DP source that the car radio connects to. Media controls (play, pause, skip, volume) relay bidirectionally through the Pi via AVRCP. The car radio sees a normal Bluetooth audio device. The phone sees a normal Bluetooth speaker. The Pi sits invisibly in the middle, bridging them. --- ## The Problem (why this exists) - The 2013 Yaris radio likely has either no Bluetooth at all, or an older Bluetooth module that only supports hands-free calling (HFP), not music streaming (A2DP). - Even if the radio does support Bluetooth A2DP, there's no way to have the phone simultaneously connected to the radio for audio AND the Pi for the local web UI over WiFi without manual reconnection each time. - This service makes the Pi the one permanent Bluetooth connection for the radio, and the phone connects to the Pi instead — the Pi becomes the car's infotainment hub. --- ## Architecture Overview ``` ┌──────────┐ Bluetooth A2DP/AVRCP ┌──────────┐ Bluetooth A2DP/AVRCP ┌───────────┐ │ Phone │ ──────────────────────────▶ │ Pi │ ──────────────────────────▶ │ Car Radio │ │ (Spotify)│ ◀────────────────────────── │(mediacntr)│ ◀────────────────────────── │ │ │ │ audio stream (SBC/AAC) │ │ re-encoded audio (SBC) │ │ │ │ media controls (AVRCP) │ │ media controls (AVRCP) │ │ └──────────┘ └──────────┘ └───────────┘ │ WiFi hotspot (existing) "yaris 911" @ 10.42.0.1 │ ┌──────────────────────┐ │ Existing yaris-obd UI │ │ (dashboard, dashcam, │ │ status — unchanged) │ └──────────────────────┘ ``` ### Bluetooth profiles involved | Profile | Role (Phone side) | Role (Radio side) | Purpose | |---------|-------------------|-------------------|---------| | **A2DP** | Pi is **Sink** (receives audio from phone) | Pi is **Source** (sends audio to radio) | Stereo audio streaming | | **AVRCP** | Pi is **Target** (receives commands from phone) | Pi is **Controller** (sends commands to radio) | Play/pause/skip/track metadata relay | ### Audio pipeline ``` Phone → [Bluetooth A2DP Sink] → PulseAudio source → PulseAudio loopback → PulseAudio sink → [Bluetooth A2DP Source] → Radio ``` - BlueZ registers the Pi as a connectable A2DP sink (for the phone). - BlueZ also connects the Pi to the car radio as an A2DP source. - PulseAudio bridges the two: `module-loopback` routes audio from the phone's A2DP source (in PulseAudio) to the radio's A2DP sink (in PulseAudio). - No audio file is ever written to disk — it's a live stream pipeline, minimal latency. --- ## Hardware Considerations ### Pi 3's built-in Bluetooth The Pi 3's BCM43438 combo chip provides both WiFi and Bluetooth on the same physical radio. This has two implications: 1. **Coexistence:** 2.4 GHz WiFi (especially the AP/hotspot on wlan0) shares spectrum with Bluetooth. This can cause audio dropouts. Mitigations: - Bluetooth uses adaptive frequency hopping (AFH), which avoids WiFi channels — this is built into the BCM43438 firmware and normally works well. - If dropouts are a problem in practice, a **USB Bluetooth 5.0 dongle** (~$10) can offload the phone or radio connection to a separate radio entirely, eliminating WiFi/BT contention. - The Phase 23.5 wlan1 (TP-Link Archer T3U, 5 GHz-capable) is already installed — moving the hotspot to 5 GHz would completely eliminate 2.4 GHz WiFi/Bluetooth interference. This is a configuration change, not new hardware. 2. **Disable-bt overlay:** The Pi currently uses `dtoverlay=disable-bt` in `/boot/firmware/config.txt` to free the PL011 UART for the SIM868 GPS module (see Phase 1 bring-up notes). **This completely disables the onboard Bluetooth controller.** To use Bluetooth for mediacenter: - ~~**Option A:** Use a USB Bluetooth dongle~~ — **CONFIRMED: owner has a USB Bluetooth dongle on hand.** This preserves GPS UART routing, gives cleaner separation (no WiFi/BT combo-chip contention), and likely provides better range/audio quality. - ~~**Option B:** Use the mini-UART~~ — ruled out. No need to touch the already-working GPS UART config. --- ## Phases ### Phase M1: Bluetooth stack validation (research spike) **Goal:** Prove the Pi can simultaneously be an A2DP sink + A2DP source with audio passing through, before writing any production code. - [ ] Install BlueZ + PulseAudio + `pulseaudio-module-bluetooth` on the Pi. - [ ] Pair and connect the Pi as an A2DP sink from a real phone. - Verify `pactl list sources` shows the phone's A2DP stream as an audio source. - [ ] Pair and connect the Pi as an A2DP source to a real Bluetooth speaker/radio. - Verify `pactl list sinks` shows the radio/speaker as an audio sink. - [ ] Set up a PulseAudio loopback: `pactl load-module module-loopback source= sink=`. - [ ] Play audio on the phone, confirm it comes out of the radio/speaker. - Measure rough latency (video-sync test: watch a video on the phone, observe lip sync). - [ ] Test AVRCP relay: play/pause/skip from the radio's controls should affect the phone. - This may require a custom DBus script (BlueZ exposes `org.bluez.MediaPlayer1` on both the sink and source sides — commands received on the source side need to be forwarded to the target/sink side). - [ ] Document: what exact BlueZ/PulseAudio commands, what worked, what didn't, latency numbers, codec info, audio quality observations. **Estimated time:** 2-3 hours of hands-on Pi tinkering. **Deliverable:** A markdown file in `poc/` with raw terminal logs, commands, and findings. **Go/No-go decision after M1:** If audio passes through with acceptable latency (< ~300ms additional) and AVRCP relay is feasible via DBus scripts, proceed to M2. If the onboard Bluetooth is too flaky or latency is unusable, re-evaluate USB dongle or shelve. --- ### Phase M2: `yaris-mediacenter` service scaffold **Goal:** Package the M1 proof-of-concept into a proper Python service that auto-starts alongside `yaris-obd`, with persistent configuration. - [ ] Create `modules/app_mediacenter.py`: - `BluetoothBridge` class managing the dual-role Bluetooth session. - Config loaded from `config/mediacenter.json` (device names, PIN codes, auto-connect MAC addresses, codec preferences). - Subprocess management of `bluetoothctl`, `pactl`, `parec`/`pacat` or direct DBus calls via `pydbus`/`dasbus` to avoid shelling out. - Health check: are both connections alive? Reconnect logic on disconnect. - [ ] PulseAudio loopback management: - Create/destroy the loopback module programmatically. - Handle dynamically appearing/disappearing sources and sinks (phone connects, radio connects — the loopback needs to adapt). - [ ] AVRCP relay via DBus: - Listen for `org.bluez.MediaPlayer1.PropertiesChanged` on the source side (radio). - Forward `Play`, `Pause`, `Next`, `Previous`, `FastForward`, `Rewind` to the target side (phone). - Relay track metadata (Title, Artist, Album, Duration) from the phone to the radio side — the radio's display can show "now playing" info. - [ ] systemd unit: `deploy/yaris-mediacenter.service` - Requires `bluetooth.service` and `pulseaudio.service` (user-mode pulseaudio for the `yaris` user). - `Wants=yaris-obd.service` (co-located, not strictly dependent). - [ ] Install script: `deploy/install_mediacenter.sh` - Installs required apt packages (BlueZ, PulseAudio, pulseaudio-module-bluetooth, Python DBus bindings). - Configures PulseAudio for the `yaris` user (systemd user service). - Enables Bluetooth discoverable/connectable mode permanently. - [ ] UI: minimal status indicator on the existing `/status` page: - Phone connected? (MAC, device name, codec) - Radio connected? (MAC, device name, codec) - Audio flowing? (PulseAudio loopback state) - Latency estimate. **Deliverable:** Running service on the Pi, phone → Pi → radio audio chain verified. **Dependencies:** M1 must be complete (Bluetooth stack proven). --- ### Phase M3: Pairing & auto-connect UX **Goal:** Make pairing practical — not a manual `bluetoothctl` sequence every time the car starts. - [ ] **Phone pairing (persistent):** - Pi always discoverable as "yaris-mediacenter" (or configurable name). - First-time phone pairing is manual (phone scans, finds Pi, pairs — standard). - Pi remembers paired phones via BlueZ's trusted-device store. - Auto-connect: when a trusted phone comes in range, Pi automatically accepts the A2DP connection. No user action needed after the first pairing. - [ ] **Radio pairing (persistent):** - If the car radio supports Bluetooth scanning, the radio finds "yaris-media" (separate name from phone side — the two A2DP roles need distinct names). - Pair once, Pi remembers the radio's MAC. - Auto-connect: on service start, Pi actively connects to the radio's MAC as an A2DP source (the radio is the "speaker" from the Pi's perspective). - [ ] **Reconnection ordering:** - On ignition-on (Pi boots): mediacenter starts → connects to radio first (A2DP source) → then enables discoverability for the phone (A2DP sink). - If radio isn't in range yet (radio boots slower than the Pi), retry with exponential backoff. - If phone disconnects (user walks away), audio pipeline gracefully pauses, reconnects when phone returns. - [ ] **Settings page** (new section on existing `/settings`): - Radio MAC address (manual entry or scan-and-select). - Phone-side Bluetooth device name. - Radio-side Bluetooth device name. - Auto-connect toggle (on/off). - Preferred codec (auto / SBC / AAC if supported). - [ ] **Quick-access control widget:** - A small "Now Playing" card on the dashboard showing track metadata (Title, Artist) when audio is flowing. - Play/pause/skip buttons (passenger-safe, not for driver use while moving). **Deliverable:** Phone and radio pair once, reconnect automatically every drive. **Dependencies:** M2 service scaffold complete. --- ### Phase M4: Audio quality & latency tuning **Goal:** Make it sound good and feel responsive. - [ ] **Codec selection:** - Phone → Pi: negotiate the best available codec (AAC preferred on iPhone, aptX if the USB dongle supports it, SBC as fallback). - Pi → Radio: most car radios only support SBC. SBC bitpool tuning (higher bitpool = better quality, more bandwidth). - Document negotiated codecs per connection in the status endpoint. - [ ] **Latency measurement & reduction:** - Measure end-to-end latency (phone speaker → Pi → radio speaker) with a clap test or audio sync video. - Tune PulseAudio buffer sizes (`latency_msec` in `module-loopback`). - If latency > 500ms, consider: - Smaller Bluetooth packet sizes. - Lower PulseAudio fragment sizes. - Direct ALSA loopback (bypass PulseAudio entirely) as a fallback path. - [ ] **Audio glitch handling:** - Detect buffer underruns (audio dropouts), log them. - Automatic recovery: if the loopback module dies, recreate it. - Graceful handling of temporary Bluetooth interference. - [ ] **Volume passthrough:** - Phone volume → Pi's A2DP sink volume → Pi's A2DP source volume → Radio volume. - Keep the chain linear: phone volume controls the full path, radio volume is a secondary hardware trim. - AVRCP absolute volume if the radio supports it. **Deliverable:** Measured latency numbers, acceptable audio quality. **Dependencies:** M3 connection stability. --- ### Phase M5: Hands-free calling (HFP) relay (stretch goal) **Goal:** If the car radio has Bluetooth but only supports HFP (hands-free calling), not A2DP (music), the Pi can bridge phone calls too. If the radio has NO Bluetooth at all, this does nothing — the Pi would need a mic/speaker connected directly, which is a different scope. - [ ] Pi acts as HFP Audio Gateway (AG) for the radio. - [ ] Pi acts as HFP Hands-Free (HF) for the phone. - [ ] Call audio relay: same PulseAudio loopback pattern. - [ ] Call control relay: answer, hang up, dial. - [ ] Phonebook access relay (PBAP) — the radio can show incoming caller ID. **This phase is explicitly optional** — most users will use A2DP music streaming, and HFP relay adds significant complexity (SCO audio routing, AT command handling, call state machine). Only build if M1-M4 are all solid and the user explicitly requests it. --- ### Phase M6: Spotify Connect / media source integration (stretch goal) **Goal:** Instead of the phone streaming Bluetooth audio, run a lightweight Spotify client directly on the Pi so the phone is only a remote control, not the audio source. This eliminates the phone→Pi Bluetooth hop entirely. - [ ] Install `spotifyd` or `librespot` on the Pi (headless Spotify Connect receiver). - [ ] Route Spotify audio directly to the A2DP source (Pi → Radio), no loopback needed. - [ ] Phone uses the Spotify app as a remote (Spotify Connect protocol — built into the Spotify app, no custom code needed). - [ ] Fallback: if the user isn't playing Spotify (e.g., YouTube, podcasts, navigation voice), fall back to the Bluetooth A2DP relay from M1-M3. **Trade-off vs. M1-M3 approach:** - **Pro:** One fewer Bluetooth hop = less latency, better audio quality, lower battery drain on the phone (phone just sends control commands, not an audio stream). - **Con:** Only works for Spotify. Any other audio app (Apple Music, YouTube, podcast apps, navigation) still needs the Bluetooth relay. - **Con:** `spotifyd`/`librespot` requires a Spotify Premium account and the Pi needs internet access (phone hotspot uplink via wlan1 — already built in Phase 22). **Recommendation:** Build M1-M3 (Bluetooth relay) first, since it works for ALL audio. Add M6 later as an optimization for Spotify specifically. --- ### Phase M7: Idle "now playing" telemetry broadcast (stretch goal) **Goal:** Show live OBD values (speed, RPM, coolant) on the car radio's display using the Bluetooth link that M1-M6 already establishes — **no Android Auto, no extra app.** When nothing is playing, the Pi's A2DP source broadcasts live telemetry as the AVRCP track metadata, so the radio's "now playing" field shows the gauges instead of a song. - [ ] When the audio pipeline is idle (no phone A2DP stream, loopback inactive), have the Pi's BlueZ `org.bluez.MediaPlayer1` (source side, facing the radio) emit a synthetic track: - Title: `87 km/h` - Artist: `RPM 2400 · Coolant 90°C` - [ ] Refresh the metadata every 1–2 s from `modules/app_obd.py` (speed, RPM, coolant) via a lightweight DBus call — no new hardware, no new app. - [ ] The moment real audio starts (phone stream appears / loopback goes active), swap the metadata back to the real track (Title/Artist relay from M2). **Constraints:** - **Text only** — the radio shows whatever fields it supports (usually Title/Artist/Album); this is a text readout, not a gauge cluster. - **Radio-dependent** — the car radio must display AVRCP metadata (Q5); otherwise the readout is invisible. The portable 9.26" CarPlay/AA screen already satisfies this. - **No conflict** — unlike an Android Auto app, this rides the existing A2DP link, so there is no WiFi conflict, no Play review, no distribution problem. **Note:** this is Pi-side (lives in `modules/app_mediacenter.py`), consistent with M1-M6. It also makes the telemetry readout work on the portable CarPlay/AA screen **without** any Android Auto app — the Pi is the "player" from the radio's perspective. --- ## Hardware Status | Component | Status | |-----------|--------| | USB Bluetooth dongle | ✅ Confirmed — owner has one on hand | | Car radio Bluetooth capability | ❓ Unknown — Q1 (does the 2013 Yaris radio have Bluetooth/A2DP?) | | Test Bluetooth speaker (for M1 validation) | ❓ Unknown — needed to simulate radio side if car radio has no BT | | Pi 3 USB port availability | ⚠️ Check — already has PL2303 OBD adapter + TP-Link T3U WiFi + possibly E3372 LTE modem; may need a powered USB hub | --- ## Open Questions (to resolve during M1) | # | Question | Impact | |---|----------|--------| | Q1 | Does the 2013 Yaris radio have Bluetooth at all? If so, which profiles? | Defines what the Pi needs to broadcast (A2DP vs HFP only) | | Q2 | Can the Pi's BCM43438 (or a USB dongle) maintain two simultaneous Bluetooth connections (phone + radio) without audio dropouts? | Determines if USB dongle is required | | Q3 | What is the real end-to-end latency (phone → Pi → radio)? | Determines if this is usable for video/navigation or music-only | | Q4 | ~~Does `dtoverlay=disable-bt` permanently block the onboard Bluetooth?~~ | ✅ Resolved — USB dongle confirmed available, onboard BT stays disabled for GPS | | Q5 | Does the car radio support AVRCP metadata display (track title/artist on the radio screen)? | Determines if Phase M2 metadata relay is worth building | --- ## Deployment Impact - **New apt packages:** `pulseaudio`, `pulseaudio-module-bluetooth`, `bluez-tools`, Python `pydbus` or `dasbus`. - **New systemd units:** `yaris-mediacenter.service` (possibly a user unit for PulseAudio). - **New config files:** `config/mediacenter.json`. - **Existing services:** no changes to `yaris-obd.service`, GPS, OBD, WiFi, or uplink paths. Mediacenter is an independent service that happens to run on the same Pi. - **WiFi/Bluetooth coexistence:** if using a USB dongle on a USB 3.0 port, be aware of USB 3.0 2.4 GHz interference (this is a known hardware issue with Pi 4; Pi 3 only has USB 2.0, so not a concern here). --- ## Risk Assessment | Risk | Likelihood | Impact | Mitigation | |------|-----------|--------|------------| | Audio latency too high for video/nav | Medium | High | Phase M1 validates early; M6 (Spotify Connect) bypasses it for music | | Bluetooth coexistence with WiFi hotspot causes dropouts | Medium | Medium | USB dongle + 5 GHz WiFi on wlan1 (already available) | | `disable-bt` overlay blocks onboard Bluetooth | High (confirmed) | Low | ✅ USB dongle confirmed available — onboard BT stays disabled, dongle handles mediacenter | | AVRCP relay via DBus is unreliable or incomplete | Medium | Medium | Fall back to phone-side controls only; M1 validates | | Car radio has no Bluetooth at all | ? | High | M1 clarifies; if no BT radio, this project needs a different approach (FM transmitter? aux cable? replace the head unit?) | --- ## Go/No-Go Checklist Before starting Phase M2 (committed code): - [ ] M1 research spike complete with real hardware. - [ ] Latency measured and deemed acceptable by owner. - [ ] Audio quality confirmed acceptable. - [ ] Both A2DP sink + source roles confirmed working simultaneously. - [ ] Owner explicit approval to proceed past M1.