345 lines
18 KiB
Markdown
345 lines
18 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 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-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.
|
|
|
|
---
|
|
|
|
## 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`,
|
|
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 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.
|