382 lines
20 KiB
Markdown
382 lines
20 KiB
Markdown
# 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=<phone_source> sink=<radio_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.
|