13 Commits

Author SHA1 Message Date
2c6e7e8762 hardware: KiCad bootstrap for the Resound carrier PCB (Small Build)
Netlist-first KiCad-import package for the single carrier PCB (ESP32-WROOM-32E,
SINK/BROADCASTER assembly variants):
- hardware/carrier/resound-carrier.net : KiCad s-expr netlist, 44 components /
  40 nets, real library footprints (RF_Module:ESP32-WROOM-32, 2x13 stacking
  header, 0402 passives, SOT-23 auto-reset, JST-SH HUD conn). Parens balanced.
- hardware/carrier/BOM.csv : 44 parts (ref/value/footprint/MPN/DNP/notes).
- hardware/carrier/LAYOUT.md : 45x45 4-layer stackup, 15mm antenna keep-out,
  placement, JLCPCB DRC, SINK-vs-BROADCASTER variant + address-strap table.
- hardware/carrier/README.md : KiCad import steps + caveats.

Agent decisions flagged for EE: LED moved off the GPIO13 strap to LED_DAT=GPIO21
/ LED_CLK=GPIO4; verify WROOM footprint pad numbering; UART2 is connector-only
this rev. Deliverable is import-ready netlist+BOM+spec, not a finished .kicad_pcb.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:10:15 -04:00
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
dca7d3ba46 Phase 4 LVGL UI + guest channel + brand-agnostic naming
- LVGL 8.3 round-screen UI (replaces the LovyanGFX direct-draw functional UI):
  master volume arc, per-channel connection dots, SPEAKERS list, per-channel
  DETAIL (volume arc + delay -/+ + scan), SCAN list. Rendered through LovyanGFX;
  configured via build flags (LV_CONF_SKIP); tick via lv_tick_inc in loop.
- Third channel "Guest" @ I2C 0x12 (the onboard-antenna ESP32 — the long-range
  link to an occasional passenger speaker). New [env:source_guest].
- Channels are now brand-agnostic, in order: index0 "Headset" (0x11),
  index1 "Speaker 1" (0x10), index2 "Guest" (0x12). Discovery picks the real
  device per channel, so labels stay generic.
- hub_proto.h: HUB_I2C_ADDR_GUEST 0x12.

Built clean: hub_s3 (LVGL, 3 channels) + source_guest. LVGL render verified on
hardware (basic); guest board needs flashing + wiring into the I2S/I2C buses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:06:10 -04:00
de61905e9c Phase 3: BT speaker discovery + selection (no more hardcoded names)
The source boards now scan for nearby Bluetooth speakers and connect to a
user-chosen one; the hub screen drives it. Hardcoded TARGET_SPEAKER remains
only as a first-boot fallback when nothing is saved.

- hub_proto.h: status payload -> 7 bytes (adds scanning, scan_count,
  has_device). New cmds: SCAN_START/STOP, SELECT(index), FORGET,
  GET_SCANITEM(index)->33-byte item (valid,rssi,mac,namelen,name),
  GET_CURNAME->24-byte name.
- board_source.cpp: set_ssid_callback collects nearby sinks (dedupe by MAC,
  COD-filtered to audio), set_discovery_mode_callback stops the scan loop,
  connect_to(mac) + set_auto_reconnect(mac) on SELECT, MAC+name persisted to
  NVS (spk_mac/spk_name). Boot connects to saved MAC or falls back to the
  name. onRequest multiplexes status/scanitem/curname by last read command.
- hub_s3.cpp: MAIN screen now shows each speaker's selected device name + a
  "pick" button; SCREEN_SCAN shows the live discovery list (RSSI-sorted,
  scrollable) with tap-to-select, Rescan, Back. All I2C on Wire1.

Built by two parallel sub-agents against hub_proto.h; all three envs build
clean first try. Needs the I2C bus wired to test on hardware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:57:48 -04:00
4b432c6123 Phase 2: I2C control bus (S3 hub master <-> B/C slaves)
Wired control path so the S3 hub can set delay/volume on the source boards
and read their status, over an I2C bus (separate from the audio I2S bus).

- src/hub_proto.h: shared protocol — addresses (JBL 0x10, Cardo 0x11),
  commands SET_DELAY (u16), SET_VOLUME (u8), GET_STATUS (4-byte payload:
  connected, delay LE, volume).
- board_source.cpp (B/C): I2C slave on GPIO32/33 (addr via -DHUB_I2C_ADDR).
  Light onReceive/onRequest callbacks stash requests; loop() applies them
  (reuses set_delay; adds current_volume -> source.set_volume, NVS-persisted).
  Relay/FIFO/touch untouched.
- hub_s3.cpp: I2C master on Wire1 (GPIO15/16) — peripheral 1, since the
  CST816S touch owns peripheral 0. Polls both boards ~1Hz; LovyanGFX UI shows
  each speaker's connect/delay/volume with touch +/- zones that send commands.
- platformio.ini: -DHUB_I2C_ADDR per source env.

Written by two parallel sub-agents (one per side) against hub_proto.h; fixed
the Wire->Wire1 peripheral conflict. All three envs build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 10:44:45 -04:00
9ed1899285 S3 hub Phase 1: round LCD + touch bring-up (LovyanGFX)
ESP32-S3-Touch-LCD-1.28 first light: GC9A01 round display + backlight + CST816S
touch working via LovyanGFX. Draws a label and red dots on touch.

- src/hub_s3.cpp: LovyanGFX LGFX config for this board (GC9A01 SPI
  SCLK10/MOSI11/MISO12/CS9/DC8/RST14, backlight GPIO2; CST816S touch I2C
  SDA6/SCL7 polled, addr 0x15).
- platformio.ini: new [env:hub_s3] (esp32-s3-devkitc-1, 4MB, LovyanGFX).
  Not in default_envs. Flash: esp32s3, bootloader@0x0 +boot_app0@0xe000.

Foundation for the on-device UI hub. PSRAM not yet enabled (not needed for
bring-up; will enable for LVGL). Next: I2C control bus to Boards B/C.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 10:12:17 -04:00
0b1c34074f Fix robotic audio + crash: proper FIFO delay line
The previous delay buffer re-derived the read position from the write head on
every A2DP pull (read = write - delay), so it sampled the buffer at the
Bluetooth pull-rate while positioning by the I2S write-rate — skipping/
repeating samples (robotic) and, with no cushion at delay=0, constantly
under/overrunning until it destabilized and crashed.

Replace with a real FIFO: a sequential read pointer that advances by the
frames consumed, held BASE_DELAY_MS (40 ms jitter cushion) + the touch trim
behind the I2S write head, with underrun (pad silence) and overrun (resync)
handling. Also carry partial frames across I2S reads so L/R never slips.

Verified on hardware: clean audio to both speakers, stable for hours,
touch-pad sync aligns JBL and Cardo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:07:49 -04:00
66f56f1e09 docs: document touch-pad sync (GPIO4/27), no WiFi UI
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:18:05 -04:00
2a34ed5abe Sync via capacitive touch (drop Wi-Fi) — fixes BT starvation
The Wi-Fi/web/ESP-NOW sync approach was unviable: Wi-Fi + Bluetooth-A2DP +
the delay buffer exhausted RAM on the classic ESP32 (~20 KB free), and the
Bluetooth stack was so starved the source boards couldn't even connect to a
speaker. Confirmed on hardware: with Wi-Fi up the JBL would not connect; with
Wi-Fi removed it connects instantly (~65 KB free).

- board_source.cpp: remove Wi-Fi/ESP-NOW. Keep the I2S-reader-task + ring
  delay line (now 200 ms; plenty of RAM without Wi-Fi). Adjust delay live by
  ear via two capacitive-touch pads — "+" on GPIO4 (T0), "-" on GPIO27 (T7);
  tap = 5 ms step, hold = ramp. Persisted to flash (debounced).
- board_sink.cpp: reverted to the simple A2DP sink + I2S master (no Wi-Fi).
- platformio.ini: drop SPEAKER_ID. Remove relay_config.h.

All three boards connect reliably with healthy heap. Touch pins read ~120
untouched; threshold 40.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:07:43 -04:00
0b474b172b Add per-speaker delay sync with Wi-Fi/web UI + ESP-NOW
Adds runtime speaker-sync control to the relay:

- src/relay_config.h: shared Wi-Fi AP creds + ESP-NOW RelayDelayMsg
  (delay_ms[JBL], delay_ms[Cardo]) on a fixed channel.
- board_sink.cpp (Board A): hosts a Wi-Fi AP "BikeAudio-Setup" + a small
  web UI (two sliders, no app) at 192.168.4.1, and broadcasts the chosen
  per-speaker delays to the source boards over ESP-NOW. Still A2DP sink +
  I2S master. (Tune while parked — Wi-Fi/BT coexist on one radio.)
- board_source.cpp (Boards B/C): inserts an adjustable delay line between
  I2S in and A2DP out — a dedicated reader task fills a ring buffer (up to
  ~250 ms) and the A2DP callback reads delay_frames behind the write head.
  Delay arrives via ESP-NOW (per SPEAKER_ID) and is persisted to flash
  (Preferences), so it survives power cycles.
- platformio.ini: source envs get -DSPEAKER_ID (0=JBL, 1=Cardo).

Lets the rider trim JBL vs Cardo timing to sync the two speakers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:11:41 -04:00
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
04e7f20430 Convert Arduino sketch to PlatformIO project
Restructure BikeAudio for PlatformIO while preserving the original
build specs from the .ino header:
  - platform espressif32 @ ~6.6.0  -> ESP32 Arduino core 2.0.17
  - board esp32dev (ESP32 DevKitC v4)
  - board_build.partitions = huge_app.csv (Huge APP, required for BT stack)
  - monitor_speed = 115200

Changes:
  - BikeAudio.ino -> src/main.cpp (+ #include <Arduino.h>, print_status()
    forward declaration; PlatformIO compiles .cpp directly and does not
    auto-generate prototypes like the Arduino IDE).
  - Add platformio.ini with pschatzmann ESP32-A2DP + arduino-audio-tools
    pinned to exact commits (ESP32-A2DP 1.8.11, audio-tools 1.2.4) for
    reproducible builds.
  - Adapt three spots to the current library API (behavior preserved):
      * RingBuffer<uint8_t>::read() -> bool read(T&)
      * connection-state callbacks: drop esp_bd_addr_t arg
        -> (esp_a2d_connection_state_t, void*)
      * BluetoothA2DPSource::start(name, bool) -> start(name)
        (auto-reconnect already configured via set_auto_reconnect(true))

Verified: pio run -e esp32dev succeeds.
RAM 14.5% (47.6 KB), Flash 42.0% (1.32 MB / 3 MB).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:44:09 -04:00
Sebastien Vayrette
276489cb17 feat: initial BikeAudio firmware
ESP32 DevKitC v4 Bluetooth audio relay.
iPhone -> ESP32 (A2DP sink) -> JBL Charge 5 + Tangerine EDGE (dual A2DP source).
Dual mirrored ring buffers for in-sync output, auto-reconnect on boot.
2026-06-09 12:51:08 -04:00