blue-repeat/README_RELAY.md
blue b5d1169392 Rename: product -> "Resound"; firmware roles -> hud/sink/broadcaster
Product name BikeAudio -> "Resound" (the device isn't bike-specific — works at
home, backyard, camping, parties, group rides). This is the A2DP advertise name
the phone connects to: sink.start("Resound"). All banners/comments + README
updated. (After reflashing the sink, the phone must forget "BikeAudio" and
connect to "Resound".)

Firmware vocabulary clarified (the old hub/sink/source was confusing —
"source" read backwards since those boards SEND audio):
  hub_s3.cpp / env hub_s3        -> hud.cpp / env hud
  board_sink.cpp                 -> sink.cpp        (env sink)
  board_source.cpp               -> broadcaster.cpp (envs broadcaster_headset
                                    0x11, broadcaster_speaker1 0x10,
                                    broadcaster_guest 0x12)
  hub_proto.h                    -> bus_proto.h
default_envs = sink + the three broadcasters. All envs build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:14:16 -04:00

5.1 KiB
Raw Blame History

Resound — 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.

Sync (touch pads on each source board)

JBL and Cardo have independent Bluetooth buffering, so one lags the other. Each source board has an adjustable delay (0200 ms) you trim live, by ear, with two capacitive-touch pads — raise the delay on whichever speaker is early until they line up. The value is saved to flash (survives power-cycles).

Touch pad Pin Action
+ GPIO4 tap = +5 ms, hold = ramp up
GPIO27 tap = 5 ms, hold = ramp down

Attach a short wire or a bit of foil to GPIO4 and GPIO27 on each source board and touch the end. (There is intentionally no Wi-Fi/phone UI: Wi-Fi + Bluetooth + the audio buffer don't fit in RAM on the classic ESP32 — it starves the Bluetooth stack — so the control is local touch.)

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 "Resound".
  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 lags the other. The per-board touch delay (above) lets you trim a fixed offset to line them up by ear; it is not continuous sample-lock, so slow drift over long sessions is possible. 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.