blue-repeat/README_RELAY.md
blue baa3ef7690 3-board relay firmware: sink + I2S + dual source
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>
2026-06-10 11:35:12 -04:00

4.1 KiB

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.