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:
parent
9ed1899285
commit
4b432c6123
@ -31,12 +31,12 @@ 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"' -DHUB_I2C_ADDR=0x10
|
||||
|
||||
; --- 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"' -DHUB_I2C_ADDR=0x11
|
||||
|
||||
; --- Hub: ESP32-S3-Touch-LCD-1.28 (round GC9A01 LCD + CST816S touch) ----------
|
||||
; Different chip (esp32s3) from the relay boards. Drives the UI; later the
|
||||
|
||||
@ -22,11 +22,23 @@
|
||||
#include "AudioTools.h"
|
||||
#include "BluetoothA2DPSource.h"
|
||||
#include <Preferences.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include "hub_proto.h"
|
||||
|
||||
#ifndef TARGET_SPEAKER
|
||||
#define TARGET_SPEAKER "BikeAudio-Speaker"
|
||||
#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_WS_PIN 18
|
||||
#define I2S_DATA_PIN 22
|
||||
@ -58,6 +70,55 @@ static volatile uint16_t delay_ms_current = 0;
|
||||
static bool save_pending = false;
|
||||
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).
|
||||
// Carry any partial frame across reads so L/R never slips out of alignment.
|
||||
static void i2s_task(void *arg) {
|
||||
@ -139,6 +200,16 @@ void setup() {
|
||||
prefs.begin("bikeaudio", false);
|
||||
set_delay(prefs.getUShort("delay_ms", 0));
|
||||
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);
|
||||
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_on_connection_state_changed(on_conn_state);
|
||||
source.set_auto_reconnect(true, 5);
|
||||
source.set_volume(100);
|
||||
source.set_volume(current_volume);
|
||||
source.start(TARGET_SPEAKER);
|
||||
Serial.printf("[SRC] Connecting to '%s' — trim %u ms; touch + GPIO4, - GPIO27\n",
|
||||
TARGET_SPEAKER, delay_ms_current);
|
||||
@ -160,6 +231,30 @@ void setup() {
|
||||
void loop() {
|
||||
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;
|
||||
if (now - last_touch >= TOUCH_REPEAT_MS) {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
if (now - last_st > 5000) {
|
||||
Serial.printf("[SRC %s] connected=%s trim=%ums fill=%u heap=%u\n",
|
||||
|
||||
28
src/hub_proto.h
Normal file
28
src/hub_proto.h
Normal 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
|
||||
293
src/hub_s3.cpp
293
src/hub_s3.cpp
@ -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
|
||||
* touch, to prove the board before building the GUI. No control bus / GUI yet.
|
||||
* Round GC9A01 LCD + CST816S touch (kept from phase 1 first-light), now wired
|
||||
* 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):
|
||||
* 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)
|
||||
* Control bus (I2C master): SDA=15 SCL=16
|
||||
*
|
||||
* Build: pio run -e hub_s3 | flash: esp32s3, bootloader@0x0
|
||||
*/
|
||||
|
||||
#define LGFX_USE_V1
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <LovyanGFX.hpp>
|
||||
#include "hub_proto.h"
|
||||
|
||||
class LGFX : public lgfx::LGFX_Device {
|
||||
lgfx::Panel_GC9A01 _panel;
|
||||
@ -67,36 +77,277 @@ public:
|
||||
|
||||
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() {
|
||||
Serial.begin(115200);
|
||||
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.setRotation(0);
|
||||
lcd.setBrightness(200);
|
||||
lcd.fillScreen(TFT_BLACK);
|
||||
lcd.drawCircle(120, 120, 118, TFT_DARKGREY);
|
||||
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");
|
||||
drawFrame();
|
||||
Serial.println("[LCD] UI drawn");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int32_t x, y;
|
||||
if (lcd.getTouch(&x, &y)) {
|
||||
lcd.fillCircle(x, y, 5, TFT_RED);
|
||||
Serial.printf("[TOUCH] %d, %d\n", x, y);
|
||||
unsigned long now = millis();
|
||||
|
||||
// ---- touch (debounced) ----
|
||||
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;
|
||||
if (millis() - last > 5000) {
|
||||
Serial.printf("[hub] alive heap=%u psram=%u\n", ESP.getFreeHeap(), ESP.getFreePsram());
|
||||
last = millis();
|
||||
// ---- status poll (~1 Hz) ----
|
||||
static unsigned long lastPoll = 0;
|
||||
if (now - lastPoll >= POLL_PERIOD_MS) {
|
||||
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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user