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 ------------------------
|
; --- 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
|
||||||
|
|||||||
@ -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
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
|
* 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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user