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>
This commit is contained in:
parent
baa3ef7690
commit
0b474b172b
@ -31,9 +31,9 @@ build_src_filter = +<board_sink.cpp>
|
||||
; --- Board B: I2S slave -> A2DP source -> JBL Charge 5 ------------------------
|
||||
[env:source_jbl]
|
||||
build_src_filter = +<board_source.cpp>
|
||||
build_flags = '-DTARGET_SPEAKER="JBL Charge 5"'
|
||||
build_flags = '-DTARGET_SPEAKER="JBL Charge 5"' -DSPEAKER_ID=0
|
||||
|
||||
; --- Board C: I2S slave -> A2DP source -> Cardo (Tangerine EDGE) --------------
|
||||
[env:source_cardo]
|
||||
build_src_filter = +<board_source.cpp>
|
||||
build_flags = '-DTARGET_SPEAKER="Tangerine EDGE"'
|
||||
build_flags = '-DTARGET_SPEAKER="Tangerine EDGE"' -DSPEAKER_ID=1
|
||||
|
||||
@ -1,28 +1,30 @@
|
||||
/**
|
||||
* BikeAudio — Board A : A2DP SINK -> I2S MASTER
|
||||
* BikeAudio — Board A : A2DP SINK -> I2S MASTER + setup UI
|
||||
*
|
||||
* Part of the 3-board relay. The iPhone connects to this board over Bluetooth
|
||||
* (A2DP name "BikeAudio"). This board decodes the audio to PCM and clocks it
|
||||
* out on a shared I2S bus as the MASTER. Boards B and C (A2DP sources) listen
|
||||
* to this same bus as slaves and stream it to the JBL / Cardo speakers.
|
||||
* Receives audio from the iPhone (A2DP "BikeAudio"), clocks decoded PCM onto the
|
||||
* shared I2S bus as master, AND hosts a Wi-Fi access point with a browser-based
|
||||
* setup UI to adjust each speaker's delay (for syncing JBL vs Cardo). Delay
|
||||
* values are pushed to the source boards over ESP-NOW.
|
||||
*
|
||||
* iPhone ))BT)) [Board A: sink] ==I2S==> [Board B: src] ))BT)) JBL
|
||||
* \====> [Board C: src] ))BT)) Cardo
|
||||
* I2S OUTPUT (drives the bus): BCK=GPIO5 WS=GPIO25 DATA=GPIO23 + GND
|
||||
* Setup UI: join Wi-Fi "BikeAudio-Setup" (pass "bikeaudio"), open http://192.168.4.1
|
||||
*
|
||||
* Why a separate board: one ESP32 cannot be an A2DP sink and source at once
|
||||
* (single Bluedroid A2DP role), and an A2DP source can hold only one outgoing
|
||||
* link — so the sink and each speaker need their own chip. See README.
|
||||
* NOTE: Wi-Fi + Bluetooth share the radio — tune the delays while parked; the
|
||||
* source boards persist the values, so Wi-Fi need not be used while riding.
|
||||
*
|
||||
* I2S OUTPUT pins (this board DRIVES the bus — wire these to B and C):
|
||||
* BCK = GPIO5 WS/LRCK = GPIO25 DATA(out) = GPIO23 + common GND
|
||||
*
|
||||
* Build: pio run -e sink (compiled via build_src_filter in platformio.ini)
|
||||
* Build: pio run -e sink
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "BluetoothA2DPSink.h"
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <esp_now.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
#include "relay_config.h"
|
||||
|
||||
#define I2S_BCK_PIN 5
|
||||
#define I2S_WS_PIN 25
|
||||
@ -30,67 +32,130 @@
|
||||
|
||||
I2SStream i2s;
|
||||
BluetoothA2DPSink sink;
|
||||
WebServer server(80);
|
||||
|
||||
static uint16_t current_sample_rate = 0;
|
||||
static uint16_t current_sample_rate = 0;
|
||||
static uint8_t bcast_addr[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
|
||||
static RelayDelayMsg delays = { RELAY_MAGIC, {0, 0} };
|
||||
|
||||
// Configure / reconfigure the I2S bus as master TX at the given rate.
|
||||
static void start_i2s(uint16_t rate) {
|
||||
if (rate == 0) rate = 44100; // SBC default before negotiation
|
||||
if (rate == 0) rate = 44100;
|
||||
auto cfg = i2s.defaultConfig(TX_MODE);
|
||||
cfg.pin_bck = I2S_BCK_PIN;
|
||||
cfg.pin_ws = I2S_WS_PIN;
|
||||
cfg.pin_data = I2S_DATA_PIN;
|
||||
cfg.sample_rate = rate;
|
||||
cfg.channels = 2;
|
||||
cfg.bits_per_sample = 16;
|
||||
cfg.is_master = true; // Board A clocks the whole bus
|
||||
cfg.buffer_count = 8;
|
||||
cfg.buffer_size = 512;
|
||||
cfg.pin_bck = I2S_BCK_PIN; cfg.pin_ws = I2S_WS_PIN; cfg.pin_data = I2S_DATA_PIN;
|
||||
cfg.sample_rate = rate; cfg.channels = 2; cfg.bits_per_sample = 16;
|
||||
cfg.is_master = true; cfg.buffer_count = 8; cfg.buffer_size = 512;
|
||||
i2s.begin(cfg);
|
||||
current_sample_rate = rate;
|
||||
Serial.printf("[SINK] I2S master @ %u Hz / 16-bit / stereo\n", rate);
|
||||
}
|
||||
|
||||
// Called from the BT task with decoded PCM. Keep it cheap — just push to I2S.
|
||||
void write_pcm_to_i2s(const uint8_t *data, uint32_t len) {
|
||||
i2s.write(data, len);
|
||||
}
|
||||
void write_pcm_to_i2s(const uint8_t *data, uint32_t len) { i2s.write(data, len); }
|
||||
|
||||
void on_conn_state(esp_a2d_connection_state_t state, void *obj) {
|
||||
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) Serial.println("[SINK] iPhone CONNECTED");
|
||||
else if (state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) Serial.println("[SINK] iPhone disconnected");
|
||||
}
|
||||
|
||||
static void send_delays() {
|
||||
esp_now_send(bcast_addr, (uint8_t *)&delays, sizeof(delays));
|
||||
}
|
||||
|
||||
static const char PAGE[] PROGMEM = R"HTML(<!doctype html><html><head>
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>BikeAudio Sync</title><style>
|
||||
body{font-family:sans-serif;background:#111;color:#eee;margin:0;padding:18px}
|
||||
h2{margin:6px 0 18px}.row{margin:22px 0}label{display:block;font-size:18px;margin-bottom:6px}
|
||||
input[type=range]{width:100%}.v{font-weight:bold;color:#4ad}.hint{color:#888;font-size:13px;margin-top:24px}
|
||||
</style></head><body><h2>BikeAudio — speaker sync</h2>
|
||||
<div class=row><label>JBL delay: <span class=v id=jv>0</span> ms</label>
|
||||
<input type=range min=0 max=250 value=0 id=js oninput="set(0,this.value)"></div>
|
||||
<div class=row><label>Cardo delay: <span class=v id=cv>0</span> ms</label>
|
||||
<input type=range min=0 max=250 value=0 id=cs oninput="set(1,this.value)"></div>
|
||||
<div class=hint>Raise the delay on whichever speaker is <i>early</i> until they line up.
|
||||
Values are saved on each speaker.</div>
|
||||
<script>
|
||||
function set(i,v){document.getElementById(i?'cv':'jv').innerText=v;
|
||||
fetch('/set?i='+i+'&v='+v)}
|
||||
fetch('/get').then(r=>r.json()).then(d=>{js.value=d.jbl;jv.innerText=d.jbl;
|
||||
cs.value=d.cardo;cv.innerText=d.cardo})
|
||||
</script></body></html>)HTML";
|
||||
|
||||
static void handleRoot() { server.send_P(200, "text/html", PAGE); }
|
||||
|
||||
static void handleSet() {
|
||||
if (server.hasArg("i") && server.hasArg("v")) {
|
||||
int i = server.arg("i").toInt();
|
||||
int v = server.arg("v").toInt();
|
||||
if (v < 0) v = 0;
|
||||
if (v > RELAY_MAX_DELAY_MS) v = RELAY_MAX_DELAY_MS;
|
||||
if (i == 0 || i == 1) {
|
||||
delays.delay_ms[i] = (uint16_t)v;
|
||||
send_delays();
|
||||
Serial.printf("[SINK] set %s delay = %d ms\n", i ? "Cardo" : "JBL", v);
|
||||
}
|
||||
}
|
||||
server.send(200, "text/plain", "ok");
|
||||
}
|
||||
|
||||
static void handleGet() {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "{\"jbl\":%u,\"cardo\":%u}",
|
||||
delays.delay_ms[SPEAKER_JBL], delays.delay_ms[SPEAKER_CARDO]);
|
||||
server.send(200, "application/json", buf);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(500);
|
||||
Serial.println("=== BikeAudio Board A — A2DP SINK -> I2S master ===");
|
||||
Serial.println("=== BikeAudio Board A — SINK -> I2S master + setup UI ===");
|
||||
|
||||
start_i2s(44100);
|
||||
|
||||
// false => the sink does NOT run its own I2S; we forward PCM ourselves.
|
||||
sink.set_stream_reader(write_pcm_to_i2s, false);
|
||||
sink.set_on_connection_state_changed(on_conn_state);
|
||||
sink.set_auto_reconnect(true);
|
||||
sink.start("BikeAudio");
|
||||
|
||||
// Wi-Fi AP (setup UI) + ESP-NOW (push delays to source boards), shared channel.
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(RELAY_AP_SSID, RELAY_AP_PASS, RELAY_WIFI_CHANNEL);
|
||||
esp_wifi_set_channel(RELAY_WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
|
||||
if (esp_now_init() == ESP_OK) {
|
||||
esp_now_peer_info_t peer = {};
|
||||
memcpy(peer.peer_addr, bcast_addr, 6);
|
||||
peer.channel = RELAY_WIFI_CHANNEL;
|
||||
peer.encrypt = false;
|
||||
esp_now_add_peer(&peer);
|
||||
Serial.println("[SINK] ESP-NOW ready (broadcasting delays)");
|
||||
} else {
|
||||
Serial.println("[SINK] ESP-NOW init FAILED");
|
||||
}
|
||||
|
||||
server.on("/", handleRoot);
|
||||
server.on("/set", handleSet);
|
||||
server.on("/get", handleGet);
|
||||
server.begin();
|
||||
Serial.printf("[SINK] Setup UI: join Wi-Fi '%s' -> http://192.168.4.1\n", RELAY_AP_SSID);
|
||||
Serial.println("[SINK] Advertising 'BikeAudio' — connect from iPhone");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Follow the negotiated sample rate (iPhone usually 44100; reconfigure if not).
|
||||
server.handleClient();
|
||||
|
||||
uint16_t sr = sink.sample_rate();
|
||||
if (sr != 0 && sr != current_sample_rate) {
|
||||
Serial.printf("[SINK] sample rate changed %u -> %u, reconfiguring I2S\n",
|
||||
current_sample_rate, sr);
|
||||
Serial.printf("[SINK] sample rate %u -> %u\n", current_sample_rate, sr);
|
||||
start_i2s(sr);
|
||||
}
|
||||
|
||||
static unsigned long last = 0;
|
||||
if (millis() - last > 5000) {
|
||||
Serial.printf("[SINK] iPhone=%s heap=%u\n",
|
||||
sink.is_connected() ? "YES" : "no", ESP.getFreeHeap());
|
||||
last = millis();
|
||||
static unsigned long lastb = 0;
|
||||
if (millis() - lastb > 2000) { send_delays(); lastb = millis(); } // keep source boards in sync
|
||||
|
||||
static unsigned long lasts = 0;
|
||||
if (millis() - lasts > 5000) {
|
||||
Serial.printf("[SINK] iPhone=%s heap=%u JBL=%ums Cardo=%ums\n",
|
||||
sink.is_connected() ? "YES" : "no", ESP.getFreeHeap(),
|
||||
delays.delay_ms[SPEAKER_JBL], delays.delay_ms[SPEAKER_CARDO]);
|
||||
lasts = millis();
|
||||
}
|
||||
delay(100);
|
||||
delay(2);
|
||||
}
|
||||
|
||||
@ -1,48 +1,106 @@
|
||||
/**
|
||||
* BikeAudio — Boards B & C : I2S SLAVE -> A2DP SOURCE
|
||||
* BikeAudio — Boards B & C : I2S SLAVE -> [delay buffer] -> A2DP SOURCE
|
||||
*
|
||||
* Reads PCM from the shared I2S bus (clocked by Board A) and streams it to ONE
|
||||
* Bluetooth speaker. The target speaker name is fixed per build environment via
|
||||
* the TARGET_SPEAKER macro:
|
||||
* pio run -e source_jbl -> TARGET_SPEAKER = "JBL Charge 5"
|
||||
* pio run -e source_cardo -> TARGET_SPEAKER = "Tangerine EDGE"
|
||||
* Reads PCM from the shared I2S bus (clocked by Board A), passes it through an
|
||||
* adjustable delay line, and streams it to ONE Bluetooth speaker. The delay lets
|
||||
* you sync this speaker against the other one; Board A's web UI pushes the value
|
||||
* over ESP-NOW and it is saved to flash (survives power cycles).
|
||||
*
|
||||
* I2S INPUT pins (this board LISTENS to Board A's bus — wire to Board A):
|
||||
* BCK = GPIO19 WS/LRCK = GPIO18 DATA(in) = GPIO22 + common GND
|
||||
* Per-env build flags (platformio.ini):
|
||||
* source_jbl -> TARGET_SPEAKER="JBL Charge 5" SPEAKER_ID=0
|
||||
* source_cardo -> TARGET_SPEAKER="Tangerine EDGE" SPEAKER_ID=1
|
||||
*
|
||||
* Board A must be powered and clocking the bus for audio to flow (it clocks
|
||||
* continuously once booted, outputting silence until the iPhone plays).
|
||||
* I2S INPUT (listens to Board A): BCK=GPIO19 WS=GPIO18 DATA=GPIO22 + GND
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "AudioTools.h"
|
||||
#include "BluetoothA2DPSource.h"
|
||||
#include <WiFi.h>
|
||||
#include <esp_now.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
#include "relay_config.h"
|
||||
|
||||
#ifndef SPEAKER_ID
|
||||
#define SPEAKER_ID 0
|
||||
#endif
|
||||
#ifndef TARGET_SPEAKER
|
||||
#define TARGET_SPEAKER "BikeAudio-Speaker" // overridden by build_flags per env
|
||||
#define TARGET_SPEAKER "BikeAudio-Speaker"
|
||||
#endif
|
||||
|
||||
#define I2S_BCK_PIN 19
|
||||
#define I2S_WS_PIN 18
|
||||
#define I2S_DATA_PIN 22
|
||||
|
||||
#define SR_HZ 44100
|
||||
// Ring buffer holds max delay + headroom, in frames (1 frame = L+R int16 = 4 bytes).
|
||||
#define RING_FRAMES (((uint32_t)SR_HZ * (RELAY_MAX_DELAY_MS + 50)) / 1000)
|
||||
|
||||
I2SStream i2s;
|
||||
BluetoothA2DPSource source;
|
||||
Preferences prefs;
|
||||
|
||||
// The BT task pulls audio frames; read them off the I2S bus.
|
||||
// 1 Frame = 4 bytes (left int16 + right int16). Always return the full count,
|
||||
// padding with silence on underrun so the A2DP stream never stalls.
|
||||
int32_t read_i2s_frames(Frame *data, int32_t frame_count) {
|
||||
size_t bytes = i2s.readBytes((uint8_t *)data, frame_count * sizeof(Frame));
|
||||
int32_t frames = bytes / sizeof(Frame);
|
||||
for (int32_t i = frames; i < frame_count; i++) {
|
||||
data[i].channel1 = 0;
|
||||
data[i].channel2 = 0;
|
||||
// Delay ring buffer (interleaved L,R int16). Written by i2s_task, read by the A2DP callback.
|
||||
static int16_t ring[RING_FRAMES * 2];
|
||||
static volatile uint32_t write_frames = 0; // monotonic frame counter (producer)
|
||||
static volatile uint32_t delay_frames = 0; // current delay, in frames
|
||||
static volatile uint16_t delay_ms_current = 0; // for logging / persistence
|
||||
static volatile bool save_pending = false;
|
||||
|
||||
// Continuously pull I2S into the ring (paced by Board A's clock).
|
||||
static void i2s_task(void *arg) {
|
||||
static int16_t tmp[256 * 2];
|
||||
for (;;) {
|
||||
size_t bytes = i2s.readBytes((uint8_t *)tmp, sizeof(tmp));
|
||||
int frames = bytes / 4;
|
||||
for (int i = 0; i < frames; i++) {
|
||||
uint32_t w = write_frames % RING_FRAMES;
|
||||
ring[w * 2] = tmp[i * 2];
|
||||
ring[w * 2 + 1] = tmp[i * 2 + 1];
|
||||
write_frames++;
|
||||
}
|
||||
if (frames == 0) vTaskDelay(1); // no clock yet (Board A down) — don't spin
|
||||
}
|
||||
}
|
||||
|
||||
// A2DP pulls frames; serve them delayed by delay_frames behind the write head.
|
||||
int32_t read_delayed(Frame *data, int32_t frame_count) {
|
||||
uint32_t d = delay_frames;
|
||||
if (d < (uint32_t)frame_count) d = frame_count; // never read past the write head
|
||||
uint32_t w = write_frames;
|
||||
if (w < d) { // buffer not filled yet -> silence
|
||||
for (int32_t i = 0; i < frame_count; i++) { data[i].channel1 = 0; data[i].channel2 = 0; }
|
||||
return frame_count;
|
||||
}
|
||||
uint32_t start = w - d;
|
||||
for (int32_t i = 0; i < frame_count; i++) {
|
||||
uint32_t idx = (start + i) % RING_FRAMES;
|
||||
data[i].channel1 = ring[idx * 2];
|
||||
data[i].channel2 = ring[idx * 2 + 1];
|
||||
}
|
||||
return frame_count;
|
||||
}
|
||||
|
||||
static void apply_delay(uint16_t ms) {
|
||||
if (ms > RELAY_MAX_DELAY_MS) ms = RELAY_MAX_DELAY_MS;
|
||||
if (ms == delay_ms_current) return;
|
||||
delay_ms_current = ms;
|
||||
delay_frames = ((uint32_t)ms * SR_HZ) / 1000;
|
||||
save_pending = true;
|
||||
Serial.printf("[SRC %s] delay -> %u ms\n", TARGET_SPEAKER, ms);
|
||||
}
|
||||
|
||||
void on_recv(const uint8_t *mac, const uint8_t *data, int len) {
|
||||
if (len < (int)sizeof(RelayDelayMsg)) return;
|
||||
RelayDelayMsg m;
|
||||
memcpy(&m, data, sizeof(m));
|
||||
if (m.magic != RELAY_MAGIC) return;
|
||||
apply_delay(m.delay_ms[SPEAKER_ID]);
|
||||
}
|
||||
|
||||
void on_conn_state(esp_a2d_connection_state_t state, void *obj) {
|
||||
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED)
|
||||
Serial.printf("[SRC %s] CONNECTED\n", TARGET_SPEAKER);
|
||||
@ -53,36 +111,53 @@ void on_conn_state(esp_a2d_connection_state_t state, void *obj) {
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(500);
|
||||
Serial.printf("=== BikeAudio Source -> '%s' (I2S slave -> A2DP) ===\n", TARGET_SPEAKER);
|
||||
Serial.printf("=== BikeAudio Source -> '%s' (delay buffer, id %d) ===\n", TARGET_SPEAKER, SPEAKER_ID);
|
||||
|
||||
// Restore saved delay.
|
||||
prefs.begin("bikeaudio", false);
|
||||
apply_delay(prefs.getUShort("delay_ms", 0));
|
||||
save_pending = false; // loading isn't a change to persist
|
||||
|
||||
// Wi-Fi (STA, not connected) + ESP-NOW receive on the shared channel.
|
||||
WiFi.mode(WIFI_STA);
|
||||
esp_wifi_set_channel(RELAY_WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
|
||||
if (esp_now_init() == ESP_OK) {
|
||||
esp_now_register_recv_cb(on_recv);
|
||||
Serial.println("[SRC] ESP-NOW ready (listening for delay updates)");
|
||||
} else {
|
||||
Serial.println("[SRC] ESP-NOW init FAILED");
|
||||
}
|
||||
|
||||
// I2S slave RX — follows Board A's clock.
|
||||
auto cfg = i2s.defaultConfig(RX_MODE);
|
||||
cfg.pin_bck = I2S_BCK_PIN;
|
||||
cfg.pin_ws = I2S_WS_PIN;
|
||||
cfg.pin_data = I2S_DATA_PIN;
|
||||
cfg.sample_rate = 44100;
|
||||
cfg.channels = 2;
|
||||
cfg.bits_per_sample = 16;
|
||||
cfg.is_master = false; // slave: clocked by Board A
|
||||
cfg.buffer_count = 8;
|
||||
cfg.buffer_size = 512;
|
||||
cfg.pin_bck = I2S_BCK_PIN; cfg.pin_ws = I2S_WS_PIN; cfg.pin_data = I2S_DATA_PIN;
|
||||
cfg.sample_rate = SR_HZ; cfg.channels = 2; cfg.bits_per_sample = 16;
|
||||
cfg.is_master = false; cfg.buffer_count = 8; cfg.buffer_size = 512;
|
||||
i2s.begin(cfg);
|
||||
|
||||
source.set_data_callback_in_frames(read_i2s_frames);
|
||||
xTaskCreatePinnedToCore(i2s_task, "i2s_reader", 4096, nullptr, 5, nullptr, 0);
|
||||
|
||||
source.set_data_callback_in_frames(read_delayed);
|
||||
source.set_on_connection_state_changed(on_conn_state);
|
||||
source.set_auto_reconnect(true, 5);
|
||||
source.set_volume(100);
|
||||
source.start(TARGET_SPEAKER);
|
||||
|
||||
Serial.printf("[SRC] Connecting to '%s' — reading I2S slave @44.1k/16/stereo\n", TARGET_SPEAKER);
|
||||
Serial.printf("[SRC] Connecting to '%s' — I2S slave @44.1k, delay %u ms\n",
|
||||
TARGET_SPEAKER, delay_ms_current);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (save_pending) {
|
||||
prefs.putUShort("delay_ms", delay_ms_current);
|
||||
save_pending = false;
|
||||
Serial.printf("[SRC %s] saved delay %u ms to flash\n", TARGET_SPEAKER, delay_ms_current);
|
||||
}
|
||||
|
||||
static unsigned long last = 0;
|
||||
if (millis() - last > 5000) {
|
||||
Serial.printf("[SRC %s] connected=%s heap=%u\n", TARGET_SPEAKER,
|
||||
source.is_connected() ? "YES" : "no", ESP.getFreeHeap());
|
||||
Serial.printf("[SRC %s] connected=%s delay=%ums heap=%u\n", TARGET_SPEAKER,
|
||||
source.is_connected() ? "YES" : "no", delay_ms_current, ESP.getFreeHeap());
|
||||
last = millis();
|
||||
}
|
||||
delay(100);
|
||||
delay(50);
|
||||
}
|
||||
|
||||
27
src/relay_config.h
Normal file
27
src/relay_config.h
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Shared config for the BikeAudio relay — included by Board A (sink) and the
|
||||
* source boards. Defines the Wi-Fi AP for the setup UI and the ESP-NOW message
|
||||
* Board A broadcasts to push per-speaker delay values to the source boards.
|
||||
*/
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// Wi-Fi AP that Board A hosts for the browser-based setup UI.
|
||||
#define RELAY_AP_SSID "BikeAudio-Setup"
|
||||
#define RELAY_AP_PASS "bikeaudio" // WPA2 needs >= 8 chars
|
||||
#define RELAY_WIFI_CHANNEL 1 // all boards share this channel for ESP-NOW
|
||||
|
||||
// Per-speaker delay range (ms). 250 ms costs ~44 KB of ring buffer per source board.
|
||||
#define RELAY_MAX_DELAY_MS 250
|
||||
|
||||
// Speaker indices into RelayDelayMsg.delay_ms[].
|
||||
#define SPEAKER_JBL 0
|
||||
#define SPEAKER_CARDO 1
|
||||
|
||||
#define RELAY_MAGIC 0xB10E
|
||||
|
||||
// Broadcast Board A -> source boards. delay_ms[SPEAKER_JBL] / [SPEAKER_CARDO].
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint16_t magic; // RELAY_MAGIC sentinel
|
||||
uint16_t delay_ms[2]; // [0]=JBL, [1]=Cardo
|
||||
} RelayDelayMsg;
|
||||
Loading…
x
Reference in New Issue
Block a user