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>
This commit is contained in:
blue — ESP32/PlatformIO firmware 2026-06-11 10:44:45 -04:00
parent 9ed1899285
commit 4b432c6123
4 changed files with 404 additions and 24 deletions

View File

@ -31,12 +31,12 @@ build_src_filter = +<board_sink.cpp>
; --- Board B: I2S slave -> A2DP source -> JBL Charge 5 ------------------------ ; --- Board B: I2S slave -> A2DP source -> JBL Charge 5 ------------------------
[env:source_jbl] [env:source_jbl]
build_src_filter = +<board_source.cpp> build_src_filter = +<board_source.cpp>
build_flags = '-DTARGET_SPEAKER="JBL Charge 5"' build_flags = '-DTARGET_SPEAKER="JBL Charge 5"' -DHUB_I2C_ADDR=0x10
; --- Board C: I2S slave -> A2DP source -> Cardo (Tangerine EDGE) -------------- ; --- Board C: I2S slave -> A2DP source -> Cardo (Tangerine EDGE) --------------
[env:source_cardo] [env:source_cardo]
build_src_filter = +<board_source.cpp> build_src_filter = +<board_source.cpp>
build_flags = '-DTARGET_SPEAKER="Tangerine EDGE"' build_flags = '-DTARGET_SPEAKER="Tangerine EDGE"' -DHUB_I2C_ADDR=0x11
; --- Hub: ESP32-S3-Touch-LCD-1.28 (round GC9A01 LCD + CST816S touch) ---------- ; --- Hub: ESP32-S3-Touch-LCD-1.28 (round GC9A01 LCD + CST816S touch) ----------
; Different chip (esp32s3) from the relay boards. Drives the UI; later the ; Different chip (esp32s3) from the relay boards. Drives the UI; later the

View File

@ -22,11 +22,23 @@
#include "AudioTools.h" #include "AudioTools.h"
#include "BluetoothA2DPSource.h" #include "BluetoothA2DPSource.h"
#include <Preferences.h> #include <Preferences.h>
#include <Wire.h>
#include "hub_proto.h"
#ifndef TARGET_SPEAKER #ifndef TARGET_SPEAKER
#define TARGET_SPEAKER "BikeAudio-Speaker" #define TARGET_SPEAKER "BikeAudio-Speaker"
#endif #endif
// I2C slave address (which speaker this board controls). Set per-env via build
// flag; default to the JBL address if unspecified.
#ifndef HUB_I2C_ADDR
#define HUB_I2C_ADDR HUB_I2C_ADDR_JBL
#endif
#define I2C_SDA_PIN 32
#define I2C_SCL_PIN 33
#define I2S_BCK_PIN 19 #define I2S_BCK_PIN 19
#define I2S_WS_PIN 18 #define I2S_WS_PIN 18
#define I2S_DATA_PIN 22 #define I2S_DATA_PIN 22
@ -58,6 +70,55 @@ static volatile uint16_t delay_ms_current = 0;
static bool save_pending = false; static bool save_pending = false;
static unsigned long last_change_ms = 0; static unsigned long last_change_ms = 0;
// Current playback volume (0..100). Mirrored into status_buf for the hub.
static uint8_t current_volume = 100;
static bool vol_save_pending = false;
// --- I2C slave control bus (hub = master) -------------------------------
// Callbacks run in ISR/Wire context: they must be LIGHT — no Serial, NVS, or
// blocking calls. They only stash requests into volatile globals; loop() acts.
static volatile bool have_delay = false;
static volatile uint16_t pending_delay = 0;
static volatile bool have_volume = false;
static volatile uint8_t pending_volume = 0;
// Status snapshot served on HUB_CMD_GET_STATUS reads; refreshed by loop().
// [0]=connected [1]=delay lo [2]=delay hi [3]=volume
static volatile uint8_t status_buf[HUB_STATUS_LEN] = {0, 0, 0, 100};
// Master wrote [cmd][args...]; parse into request flags only.
static void on_i2c_receive(int n) {
if (n < 1) return;
uint8_t cmd = Wire.read();
switch (cmd) {
case HUB_CMD_SET_DELAY:
if (Wire.available() >= 2) {
uint8_t lo = Wire.read();
uint8_t hi = Wire.read();
pending_delay = (uint16_t)lo | ((uint16_t)hi << 8);
have_delay = true;
}
break;
case HUB_CMD_SET_VOLUME:
if (Wire.available() >= 1) {
pending_volume = Wire.read();
have_volume = true;
}
break;
case HUB_CMD_GET_STATUS:
// Nothing to do: onRequest serves the pre-filled status_buf.
break;
default:
break;
}
while (Wire.available()) Wire.read(); // drain any extra bytes
}
// Master issued a read after GET_STATUS: hand back the current snapshot.
static void on_i2c_request() {
Wire.write((uint8_t *)status_buf, HUB_STATUS_LEN);
}
// Continuously pull I2S into the FIFO (paced by Board A's master clock). // Continuously pull I2S into the FIFO (paced by Board A's master clock).
// Carry any partial frame across reads so L/R never slips out of alignment. // Carry any partial frame across reads so L/R never slips out of alignment.
static void i2s_task(void *arg) { static void i2s_task(void *arg) {
@ -139,6 +200,16 @@ void setup() {
prefs.begin("bikeaudio", false); prefs.begin("bikeaudio", false);
set_delay(prefs.getUShort("delay_ms", 0)); set_delay(prefs.getUShort("delay_ms", 0));
save_pending = false; save_pending = false;
current_volume = (uint8_t)prefs.getUShort("vol", 100);
if (current_volume > 100) current_volume = 100;
// I2C control bus: slave at HUB_I2C_ADDR on SDA=32 / SCL=33.
Wire.begin((uint8_t)HUB_I2C_ADDR, I2C_SDA_PIN, I2C_SCL_PIN, 100000);
Wire.onReceive(on_i2c_receive);
Wire.onRequest(on_i2c_request);
Serial.printf("[SRC %s] I2C slave @ 0x%02X (SDA=%d SCL=%d), vol=%u\n",
TARGET_SPEAKER, (unsigned)HUB_I2C_ADDR, I2C_SDA_PIN, I2C_SCL_PIN,
current_volume);
auto cfg = i2s.defaultConfig(RX_MODE); 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.pin_bck = I2S_BCK_PIN; cfg.pin_ws = I2S_WS_PIN; cfg.pin_data = I2S_DATA_PIN;
@ -151,7 +222,7 @@ void setup() {
source.set_data_callback_in_frames(read_delayed); source.set_data_callback_in_frames(read_delayed);
source.set_on_connection_state_changed(on_conn_state); source.set_on_connection_state_changed(on_conn_state);
source.set_auto_reconnect(true, 5); source.set_auto_reconnect(true, 5);
source.set_volume(100); source.set_volume(current_volume);
source.start(TARGET_SPEAKER); source.start(TARGET_SPEAKER);
Serial.printf("[SRC] Connecting to '%s' — trim %u ms; touch + GPIO4, - GPIO27\n", Serial.printf("[SRC] Connecting to '%s' — trim %u ms; touch + GPIO4, - GPIO27\n",
TARGET_SPEAKER, delay_ms_current); TARGET_SPEAKER, delay_ms_current);
@ -160,6 +231,30 @@ void setup() {
void loop() { void loop() {
unsigned long now = millis(); unsigned long now = millis();
// Apply I2C requests stashed by the Wire callbacks (heavy work runs here).
if (have_delay) {
have_delay = false;
set_delay((int)pending_delay);
}
if (have_volume) {
have_volume = false;
uint8_t v = pending_volume;
if (v > 100) v = 100;
if (v != current_volume) {
current_volume = v;
source.set_volume(current_volume);
vol_save_pending = true;
last_change_ms = now;
Serial.printf("[SRC %s] volume = %u\n", TARGET_SPEAKER, current_volume);
}
}
// Refresh the status snapshot served on GET_STATUS reads.
status_buf[0] = source.is_connected() ? 1 : 0;
status_buf[1] = (uint8_t)(delay_ms_current & 0xFF);
status_buf[2] = (uint8_t)(delay_ms_current >> 8);
status_buf[3] = current_volume;
static unsigned long last_touch = 0; static unsigned long last_touch = 0;
if (now - last_touch >= TOUCH_REPEAT_MS) { if (now - last_touch >= TOUCH_REPEAT_MS) {
bool plus = touchRead(TOUCH_PLUS) < TOUCH_THRESH; bool plus = touchRead(TOUCH_PLUS) < TOUCH_THRESH;
@ -174,6 +269,12 @@ void loop() {
Serial.printf("[SRC %s] saved %u ms to flash\n", TARGET_SPEAKER, delay_ms_current); Serial.printf("[SRC %s] saved %u ms to flash\n", TARGET_SPEAKER, delay_ms_current);
} }
if (vol_save_pending && now - last_change_ms > 1500) {
prefs.putUShort("vol", current_volume);
vol_save_pending = false;
Serial.printf("[SRC %s] saved vol %u to flash\n", TARGET_SPEAKER, current_volume);
}
static unsigned long last_st = 0; static unsigned long last_st = 0;
if (now - last_st > 5000) { if (now - last_st > 5000) {
Serial.printf("[SRC %s] connected=%s trim=%ums fill=%u heap=%u\n", Serial.printf("[SRC %s] connected=%s trim=%ums fill=%u heap=%u\n",

28
src/hub_proto.h Normal file
View File

@ -0,0 +1,28 @@
/**
* BikeAudio control-bus protocol shared by the S3 hub (I2C master) and the
* source Boards B/C (I2C slaves). Included by both hub_s3.cpp and board_source.cpp.
*
* Bus: I2C. Hub = master. Each source board = a slave at a fixed address.
* S3: SDA=GPIO15 SCL=GPIO16 (board expansion header)
* B/C: SDA=GPIO32 SCL=GPIO33 (free pins; I2S=19/18/22, touch=4/27)
* Shared GND + two ~4.7k pull-ups (SDA->3V3, SCL->3V3).
*/
#pragma once
#include <stdint.h>
#define HUB_I2C_ADDR_JBL 0x10
#define HUB_I2C_ADDR_CARDO 0x11
// Master -> slave WRITE: first byte = command, then args (little-endian).
#define HUB_CMD_SET_DELAY 0x01 // arg: uint16 delay_ms (0..HUB_MAX_DELAY_MS)
#define HUB_CMD_SET_VOLUME 0x02 // arg: uint8 volume (0..100)
#define HUB_CMD_GET_STATUS 0x10 // master then issues a READ of HUB_STATUS_LEN bytes
#define HUB_MAX_DELAY_MS 200
// Slave -> master status payload (returned on the read after HUB_CMD_GET_STATUS):
// [0] connected (0/1)
// [1] delay_ms low byte
// [2] delay_ms high byte
// [3] volume (0..100)
#define HUB_STATUS_LEN 4

View File

@ -1,19 +1,29 @@
/** /**
* BikeAudio Hub (ESP32-S3-Touch-LCD-1.28) : PHASE 1 bring-up * BikeAudio Hub (ESP32-S3-Touch-LCD-1.28) : PHASE 2 control bring-up
* *
* First light: drive the round GC9A01 LCD + backlight, and read the CST816S * Round GC9A01 LCD + CST816S touch (kept from phase 1 first-light), now wired
* touch, to prove the board before building the GUI. No control bus / GUI yet. * up as an I2C MASTER to the two source boards (JBL, Cardo) and showing a
* simple functional touch UI: each speaker's connect state, delay (ms) and
* volume (%), with touch zones to nudge delay / volume +/-. This proves the
* end-to-end control path before we build the polished LVGL GUI.
*
* Two I2C buses are in use, on separate ports:
* - port 0 (Wire1, owned by LovyanGFX): CST816S touch SDA=6 SCL=7
* - port "Wire" (this code, I2C master): source boards SDA=15 SCL=16
* *
* Board pins (Waveshare ESP32-S3-Touch-LCD-1.28): * Board pins (Waveshare ESP32-S3-Touch-LCD-1.28):
* GC9A01 SPI: SCLK=10 MOSI=11 MISO=12 CS=9 DC=8 RST=14 backlight=2 * GC9A01 SPI: SCLK=10 MOSI=11 MISO=12 CS=9 DC=8 RST=14 backlight=2
* CST816S touch (I2C): SDA=6 SCL=7 (polled; INT left unused) * CST816S touch (I2C): SDA=6 SCL=7 (polled; INT left unused)
* Control bus (I2C master): SDA=15 SCL=16
* *
* Build: pio run -e hub_s3 | flash: esp32s3, bootloader@0x0 * Build: pio run -e hub_s3 | flash: esp32s3, bootloader@0x0
*/ */
#define LGFX_USE_V1 #define LGFX_USE_V1
#include <Arduino.h> #include <Arduino.h>
#include <Wire.h>
#include <LovyanGFX.hpp> #include <LovyanGFX.hpp>
#include "hub_proto.h"
class LGFX : public lgfx::LGFX_Device { class LGFX : public lgfx::LGFX_Device {
lgfx::Panel_GC9A01 _panel; lgfx::Panel_GC9A01 _panel;
@ -67,36 +77,277 @@ public:
LGFX lcd; LGFX lcd;
// ---------------------------------------------------------------------------
// Control-bus (I2C master) config
// ---------------------------------------------------------------------------
#define CTRL_I2C_SDA 15
#define CTRL_I2C_SCL 16
#define CTRL_I2C_HZ 100000
#define DELAY_STEP 5 // ms per delay +/- press
#define VOL_STEP 5 // % per volume +/- press
#define POLL_PERIOD_MS 1000 // status poll cadence per speaker
#define TOUCH_DEBOUNCE 250 // ms; ignore repeat presses inside this window
// Per-speaker model. delay_ms/volume are the values we display & command;
// "connected" is reported by the board, "online" is whether the I2C
// transaction itself succeeded (board present on the bus at all).
struct Speaker {
uint8_t addr;
const char *name;
bool online; // I2C transaction succeeded this poll
bool connected; // board says its BT sink is connected
uint16_t delay_ms;
uint8_t volume;
};
static Speaker speakers[2] = {
{ HUB_I2C_ADDR_JBL, "JBL", false, false, 0, 0 },
{ HUB_I2C_ADDR_CARDO, "Cardo", false, false, 0, 0 },
};
// ---------------------------------------------------------------------------
// I2C helpers
// ---------------------------------------------------------------------------
// Poll one speaker for status. Updates s.online/connected/delay_ms/volume.
// On any bus failure the speaker is marked offline (displayed as a dash).
// NOTE: the control bus uses Wire1 (I2C peripheral 1). Peripheral 0 is already
// owned by the LovyanGFX CST816S touch (GPIO6/7), so we must not touch it here.
static void pollStatus(Speaker &s) {
Wire1.beginTransmission(s.addr);
Wire1.write(HUB_CMD_GET_STATUS);
if (Wire1.endTransmission(true) != 0) {
s.online = false;
return;
}
int n = Wire1.requestFrom((int)s.addr, (int)HUB_STATUS_LEN);
if (n < HUB_STATUS_LEN) {
s.online = false;
return;
}
uint8_t b0 = Wire1.read();
uint8_t b1 = Wire1.read();
uint8_t b2 = Wire1.read();
uint8_t b3 = Wire1.read();
s.online = true;
s.connected = (b0 != 0);
s.delay_ms = (uint16_t)(b1 | (b2 << 8));
s.volume = b3;
}
// Command a new delay (ms) to a speaker.
static void sendDelay(const Speaker &s, uint16_t ms) {
Wire1.beginTransmission(s.addr);
Wire1.write(HUB_CMD_SET_DELAY);
Wire1.write(ms & 0xFF);
Wire1.write((ms >> 8) & 0xFF);
Wire1.endTransmission();
}
// Command a new volume (0..100) to a speaker.
static void sendVolume(const Speaker &s, uint8_t vol) {
Wire1.beginTransmission(s.addr);
Wire1.write(HUB_CMD_SET_VOLUME);
Wire1.write(vol);
Wire1.endTransmission();
}
// ---------------------------------------------------------------------------
// UI layout
// ---------------------------------------------------------------------------
// 240x240 round screen split top (JBL) / bottom (Cardo). Controls live in the
// safe center column so they stay clear of the round bezel. Each half has a
// row of four square buttons: [d-] [d+] [v-] [v+]. Status text sits above.
//
// y rows:
// JBL title ~22, status ~46, buttons ~70..102
// Cardo title ~138, status ~162, buttons ~186..218
#define BTN_W 34
#define BTN_H 32
#define BTN_GAP 6
// 4 buttons centered horizontally: total width = 4*W + 3*GAP
static const int kBtnTotalW = 4 * BTN_W + 3 * BTN_GAP;
static const int kBtnX0 = (240 - kBtnTotalW) / 2; // left edge of first button
static const int kBtnYTop[2] = { 70, 186 }; // button-row top Y for each speaker
// Button kinds, in draw/hit order.
enum BtnKind { B_DELAY_DOWN = 0, B_DELAY_UP, B_VOL_DOWN, B_VOL_UP, B_COUNT };
static const char *kBtnLabel[B_COUNT] = { "d-", "d+", "v-", "v+" };
// Bounding box of button `k` for speaker index `i`.
static void btnRect(int i, int k, int &x, int &y, int &w, int &h) {
x = kBtnX0 + k * (BTN_W + BTN_GAP);
y = kBtnYTop[i];
w = BTN_W;
h = BTN_H;
}
static bool inRect(int px, int py, int x, int y, int w, int h) {
return px >= x && px < x + w && py >= y && py < y + h;
}
// Draw the four control buttons for speaker `i`. `pressed` highlights one.
static void drawButtons(int i, int pressed = -1) {
for (int k = 0; k < B_COUNT; k++) {
int x, y, w, h;
btnRect(i, k, x, y, w, h);
uint16_t fill = (k == pressed) ? TFT_DARKCYAN : TFT_NAVY;
lcd.fillRoundRect(x, y, w, h, 5, fill);
lcd.drawRoundRect(x, y, w, h, 5, TFT_DARKGREY);
lcd.setTextColor(TFT_WHITE, fill);
lcd.setTextDatum(middle_center);
lcd.drawString(kBtnLabel[k], x + w / 2, y + h / 2, &fonts::Font2);
}
}
// Title + status line for speaker `i` (online/connected, delay, volume).
static void drawStatus(int i) {
const Speaker &s = speakers[i];
int titleY = (i == 0) ? 22 : 138;
int statusY = (i == 0) ? 46 : 162;
// Title with a connect dot to its left.
lcd.setTextDatum(middle_center);
lcd.setTextColor(TFT_CYAN, TFT_BLACK);
lcd.drawString(s.name, 120, titleY, &fonts::Font2);
uint16_t dot = !s.online ? TFT_DARKGREY : (s.connected ? TFT_GREEN : TFT_RED);
lcd.fillCircle(120 - 36, titleY, 5, dot);
// Status line: either a dash (offline) or "Dnnn Vnn%".
char buf[40];
if (!s.online) {
snprintf(buf, sizeof(buf), "-- offline --");
lcd.setTextColor(TFT_DARKGREY, TFT_BLACK);
} else {
snprintf(buf, sizeof(buf), "D%ums V%u%%", s.delay_ms, s.volume);
lcd.setTextColor(TFT_WHITE, TFT_BLACK);
}
// Clear the old line first (full-width band) then draw centered.
lcd.fillRect(0, statusY - 9, 240, 18, TFT_BLACK);
lcd.drawString(buf, 120, statusY, &fonts::Font2);
}
// Redraw the whole static frame once (background, divider, both halves).
static void drawFrame() {
lcd.fillScreen(TFT_BLACK);
lcd.drawCircle(120, 120, 118, TFT_DARKGREY);
lcd.drawFastHLine(20, 120, 200, TFT_DARKGREY); // top/bottom divider
for (int i = 0; i < 2; i++) {
drawStatus(i);
drawButtons(i);
}
}
// Handle a touch at (x,y): find the button, apply step, send I2C command and
// optimistically update + redraw the affected speaker. Returns true if a
// button was hit.
static bool handleTouch(int x, int y) {
for (int i = 0; i < 2; i++) {
for (int k = 0; k < B_COUNT; k++) {
int bx, by, bw, bh;
btnRect(i, k, bx, by, bw, bh);
if (!inRect(x, y, bx, by, bw, bh)) continue;
Speaker &s = speakers[i];
switch (k) {
case B_DELAY_DOWN: {
int d = (int)s.delay_ms - DELAY_STEP;
if (d < 0) d = 0;
s.delay_ms = (uint16_t)d;
sendDelay(s, s.delay_ms);
break;
}
case B_DELAY_UP: {
int d = (int)s.delay_ms + DELAY_STEP;
if (d > HUB_MAX_DELAY_MS) d = HUB_MAX_DELAY_MS;
s.delay_ms = (uint16_t)d;
sendDelay(s, s.delay_ms);
break;
}
case B_VOL_DOWN: {
int v = (int)s.volume - VOL_STEP;
if (v < 0) v = 0;
s.volume = (uint8_t)v;
sendVolume(s, s.volume);
break;
}
case B_VOL_UP: {
int v = (int)s.volume + VOL_STEP;
if (v > 100) v = 100;
s.volume = (uint8_t)v;
sendVolume(s, s.volume);
break;
}
}
// Visual feedback + optimistic value update.
drawButtons(i, k);
drawStatus(i);
Serial.printf("[touch] %s %s -> D%ums V%u%%\n",
s.name, kBtnLabel[k], s.delay_ms, s.volume);
return true;
}
}
return false;
}
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
delay(300); delay(300);
Serial.println("=== BikeAudio Hub — S3 round LCD first light ==="); Serial.println("=== BikeAudio Hub — S3 control bring-up ===");
// Control bus: I2C master to the source boards on Wire1 (peripheral 1).
// Peripheral 0 (Wire) is used by the LovyanGFX CST816S touch (GPIO6/7).
Wire1.begin(CTRL_I2C_SDA, CTRL_I2C_SCL, CTRL_I2C_HZ);
Serial.printf("[i2c] master up SDA=%d SCL=%d @%dHz\n",
CTRL_I2C_SDA, CTRL_I2C_SCL, CTRL_I2C_HZ);
lcd.init(); lcd.init();
lcd.setRotation(0); lcd.setRotation(0);
lcd.setBrightness(200); lcd.setBrightness(200);
lcd.fillScreen(TFT_BLACK); drawFrame();
lcd.drawCircle(120, 120, 118, TFT_DARKGREY); Serial.println("[LCD] UI drawn");
lcd.setTextColor(TFT_CYAN, TFT_BLACK);
lcd.setTextDatum(middle_center);
lcd.drawString("BikeAudio", 120, 100, &fonts::FreeSansBold18pt7b);
lcd.setTextColor(TFT_WHITE, TFT_BLACK);
lcd.drawString("hub: first light", 120, 140, &fonts::Font2);
lcd.drawString("touch me", 120, 160, &fonts::Font2);
Serial.println("[LCD] drawn; touch to draw dots");
} }
void loop() { void loop() {
int32_t x, y; unsigned long now = millis();
if (lcd.getTouch(&x, &y)) {
lcd.fillCircle(x, y, 5, TFT_RED); // ---- touch (debounced) ----
Serial.printf("[TOUCH] %d, %d\n", x, y); static unsigned long lastTouch = 0;
int32_t tx, ty;
if (lcd.getTouch(&tx, &ty)) {
if (now - lastTouch >= TOUCH_DEBOUNCE) {
if (handleTouch(tx, ty)) {
lastTouch = now;
}
}
} }
static unsigned long last = 0; // ---- status poll (~1 Hz) ----
if (millis() - last > 5000) { static unsigned long lastPoll = 0;
Serial.printf("[hub] alive heap=%u psram=%u\n", ESP.getFreeHeap(), ESP.getFreePsram()); if (now - lastPoll >= POLL_PERIOD_MS) {
last = millis(); lastPoll = now;
for (int i = 0; i < 2; i++) {
pollStatus(speakers[i]);
drawStatus(i);
}
} }
// ---- heartbeat ----
static unsigned long lastBeat = 0;
if (now - lastBeat >= 5000) {
lastBeat = now;
Serial.printf("[hub] alive heap=%u JBL[%s D%u V%u] Cardo[%s D%u V%u]\n",
ESP.getFreeHeap(),
speakers[0].online ? (speakers[0].connected ? "on" : "idle") : "off",
speakers[0].delay_ms, speakers[0].volume,
speakers[1].online ? (speakers[1].connected ? "on" : "idle") : "off",
speakers[1].delay_ms, speakers[1].volume);
}
delay(15); delay(15);
} }