Implements the only architecture that can relay iPhone audio to two BT
speakers at once (one ESP32 cannot be A2DP sink+source, and a source holds
only one link):
iPhone ))BT)) [Board A: A2DP sink -> I2S master]
==I2S bus==> [Board B: I2S slave -> A2DP source] ))BT)) JBL
==I2S bus==> [Board C: I2S slave -> A2DP source] ))BT)) Cardo
- src/board_sink.cpp : A2DP sink "BikeAudio", forwards decoded PCM to an
I2S master bus (BCK=5, WS=25, DATA=23); follows negotiated sample rate.
- src/board_source.cpp : I2S slave (BCK=19, WS=18, DATA=22) -> A2DP source,
target speaker via TARGET_SPEAKER build flag; pads silence on underrun.
- platformio.ini : 3 envs (sink, source_jbl, source_cardo) sharing an
[env] base; sources differ only by TARGET_SPEAKER. build_src_filter selects
the per-board source file. Libs pinned as before.
- README_RELAY.md : wiring table, I2S bus topology, flash order, pairing,
and the speaker-sync limitation.
Replaces the single-board src/main.cpp (architecturally impossible). All
three envs build clean. Hardware flash + wiring next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
4.1 KiB
Markdown
85 lines
4.1 KiB
Markdown
# BikeAudio — 3-board Bluetooth relay
|
|
|
|
Relays iPhone audio to **two** Bluetooth speakers (JBL Charge 5 + Cardo "Tangerine EDGE")
|
|
at the same time.
|
|
|
|
## Why three boards?
|
|
|
|
One ESP32 **cannot** do this alone:
|
|
|
|
1. **Sink + source can't coexist.** To receive from the iPhone the ESP32 must be an
|
|
A2DP *sink*; to play through a speaker it must be an A2DP *source*. The ESP32's
|
|
classic-Bluetooth stack registers only one A2DP role at a time (the ESP32-A2DP
|
|
library keeps a single global instance that each role overwrites). Running a sink
|
|
and a source together orphans the sink — the iPhone can't even see it.
|
|
2. **A source reaches only one speaker.** An A2DP source holds a single outgoing
|
|
link, so one board can drive one speaker, not two.
|
|
|
|
So the job is split: one board receives, one board per speaker sends, and they pass
|
|
audio between them over a short digital **I2S** wire bus.
|
|
|
|
```
|
|
iPhone ))BT)) ┌──────────────┐ I2S bus (BCK/WS/DATA + GND)
|
|
│ Board A │═══════════════╦═══════════════╗
|
|
│ A2DP SINK │ ║ ║
|
|
│ I2S MASTER │ ▼ ▼
|
|
└──────────────┘ ┌────────────┐ ┌────────────┐
|
|
│ Board B │ │ Board C │
|
|
│ A2DP SOURCE│ │ A2DP SOURCE│
|
|
│ I2S SLAVE │ │ I2S SLAVE │
|
|
└─────┬──────┘ └─────┬──────┘
|
|
))BT)) ))BT))
|
|
JBL Charge 5 Tangerine EDGE (Cardo)
|
|
```
|
|
|
|
## Wiring
|
|
|
|
Board A is the I2S **master** (it generates the clocks). Boards B and C are
|
|
**slaves** that listen to A's bus in parallel. Tie the three signals from A to the
|
|
matching input pins on **both** B and C, and tie **all grounds together**.
|
|
|
|
| Signal | Board A (master, out) | Board B (slave, in) | Board C (slave, in) |
|
|
|-------------|-----------------------|---------------------|---------------------|
|
|
| Bit clock | **GPIO5** (BCK) | GPIO19 | GPIO19 |
|
|
| Word select | **GPIO25** (WS/LRCK) | GPIO18 | GPIO18 |
|
|
| Data | **GPIO23** (DATA out) | GPIO22 (in) | GPIO22 (in) |
|
|
| Ground | **GND** | GND | GND |
|
|
|
|
- A·GPIO5 → B·GPIO19 **and** C·GPIO19
|
|
- A·GPIO25 → B·GPIO18 **and** C·GPIO18
|
|
- A·GPIO23 → B·GPIO22 **and** C·GPIO22
|
|
- A·GND → B·GND **and** C·GND (mandatory — shared clock reference)
|
|
|
|
Each board can be powered from its own USB/5V; only the grounds must be common.
|
|
|
|
## Build & flash
|
|
|
|
```
|
|
pio run # builds all three
|
|
pio run -e sink # Board A
|
|
pio run -e source_jbl # Board B (target "JBL Charge 5")
|
|
pio run -e source_cardo # Board C (target "Tangerine EDGE")
|
|
```
|
|
|
|
Flash each board with the matching environment's artifacts
|
|
(`.pio/build/<env>/{bootloader,partitions,firmware}.bin`).
|
|
|
|
**Power-on order:** bring up **Board A first** so the I2S bus is clocking before
|
|
B and C start reading it.
|
|
|
|
## First-time pairing
|
|
|
|
1. Put the **JBL** and **Cardo** in pairing mode.
|
|
2. Power Board B and Board C — each connects to its speaker by name
|
|
(auto-reconnects on later power-ups).
|
|
3. Power Board A; on the iPhone, connect to **"BikeAudio"**.
|
|
4. Play audio — both speakers should output together.
|
|
|
|
## Known limitations
|
|
|
|
- **The two speakers are not sample-synchronized.** JBL and Cardo each have their
|
|
own Bluetooth buffering, so one may lag the other by some tens of milliseconds.
|
|
Fine for music/intercom; not suitable for tight stereo L/R separation.
|
|
- Audio is fixed at the SBC standard **44.1 kHz / 16-bit / stereo**.
|
|
- If Board A reboots, the slave boards' audio pauses until A is clocking again.
|