18 KiB
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-loopbackroutes 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:
-
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.
-
Disable-bt overlay: The Pi currently uses
dtoverlay=disable-btin/boot/firmware/config.txtto 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 instead (preserves GPS UART routing, cleaner separation, likely better range/audio quality with BT 5.0).
- Option B: Use the mini-UART (
/dev/ttyS0) for the SIM868 at a lower, core-frequency-dependent baud rate, re-enable the onboard Bluetooth for the PL011 UART. Riskier — mini-UART is less stable and baud rate depends on the core clock, which can drift.
Recommendation: Option A (USB dongle). The onboard Bluetooth is disabled for GPS, and a USB dongle is cheap, isolated, and doesn't require touching 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-bluetoothon the Pi. - Pair and connect the Pi as an A2DP sink from a real phone.
- Verify
pactl list sourcesshows the phone's A2DP stream as an audio source.
- Verify
- Pair and connect the Pi as an A2DP source to a real Bluetooth speaker/radio.
- Verify
pactl list sinksshows the radio/speaker as an audio sink.
- Verify
- 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.MediaPlayer1on both the sink and source sides — commands received on the source side need to be forwarded to the target/sink side).
- This may require a custom DBus script (BlueZ exposes
- 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:BluetoothBridgeclass 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/pacator direct DBus calls viapydbus/dasbusto 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.PropertiesChangedon the source side (radio). - Forward
Play,Pause,Next,Previous,FastForward,Rewindto 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.
- Listen for
- systemd unit:
deploy/yaris-mediacenter.service- Requires
bluetooth.serviceandpulseaudio.service(user-mode pulseaudio for theyarisuser). Wants=yaris-obd.service(co-located, not strictly dependent).
- Requires
- Install script:
deploy/install_mediacenter.sh- Installs required apt packages (BlueZ, PulseAudio, pulseaudio-module-bluetooth, Python DBus bindings).
- Configures PulseAudio for the
yarisuser (systemd user service). - Enables Bluetooth discoverable/connectable mode permanently.
- UI: minimal status indicator on the existing
/statuspage:- 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_msecinmodule-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
spotifydorlibrespoton 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/librespotrequires 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.
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, or can it be re-enabled while keeping the SIM868 on mini-UART? |
Determines if USB dongle is mandatory |
| 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, Pythonpydbusordasbus. - 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 all Bluetooth |
High (confirmed) | Low | USB dongle is the planned path anyway |
| 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.