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>
This commit is contained in:
parent
4b432c6123
commit
de61905e9c
@ -84,12 +84,78 @@ 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};
|
||||
// [4]=scanning [5]=scan_count [6]=has_device (Phase 3)
|
||||
static volatile uint8_t status_buf[HUB_STATUS_LEN] = {0, 0, 0, 100, 0, 0, 0};
|
||||
|
||||
// --- Phase 3: BT speaker discovery / selection --------------------------
|
||||
// A discovered device collected during an inquiry scan.
|
||||
struct ScanDev {
|
||||
char name[HUB_NAME_MAX + 1];
|
||||
uint8_t mac[6];
|
||||
int8_t rssi;
|
||||
};
|
||||
static ScanDev scan_list[HUB_MAX_SCAN];
|
||||
static volatile uint8_t scan_count = 0;
|
||||
static volatile bool scanning = false;
|
||||
|
||||
// Currently selected / saved speaker.
|
||||
static char cur_name[HUB_NAME_MAX + 1] = {0};
|
||||
static uint8_t cur_mac[6] = {0};
|
||||
static bool has_device = false;
|
||||
|
||||
// I2C request flags set by the Wire callbacks, applied in loop().
|
||||
static volatile bool req_scan_start = false;
|
||||
static volatile bool req_scan_stop = false;
|
||||
static volatile bool req_select = false;
|
||||
static volatile uint8_t pending_select = 0;
|
||||
static volatile bool req_forget = false;
|
||||
|
||||
// Which read buffer the next onRequest should serve. The master sets it by
|
||||
// writing the read command byte just before issuing the read. Multiplexed in
|
||||
// on_i2c_request() so GET_STATUS / GET_SCANITEM / GET_CURNAME can share the bus.
|
||||
static volatile uint8_t last_read_cmd = HUB_CMD_GET_STATUS;
|
||||
static volatile uint8_t pending_scanitem = 0; // index requested via GET_SCANITEM
|
||||
// Pre-filled in the callback (light memcpy) and handed back on the read.
|
||||
static volatile uint8_t scanitem_buf[HUB_SCANITEM_LEN] = {0};
|
||||
|
||||
// Per-device callback fired during a scan (plain C fn ptr — no captures).
|
||||
// Stash discovered audio sinks into scan_list; never connect (return false).
|
||||
static bool on_ssid(const char *ssid, esp_bd_addr_t addr, int rssi) {
|
||||
if (!scanning) return false;
|
||||
if (scan_count >= HUB_MAX_SCAN) return false;
|
||||
// Dedupe by MAC.
|
||||
for (uint8_t i = 0; i < scan_count; i++) {
|
||||
if (memcmp(scan_list[i].mac, addr, 6) == 0) return false;
|
||||
}
|
||||
ScanDev &d = scan_list[scan_count];
|
||||
if (ssid) {
|
||||
strncpy(d.name, ssid, HUB_NAME_MAX);
|
||||
d.name[HUB_NAME_MAX] = 0;
|
||||
} else {
|
||||
d.name[0] = 0;
|
||||
}
|
||||
memcpy(d.mac, addr, 6);
|
||||
if (rssi > 127) rssi = 127;
|
||||
if (rssi < -128) rssi = -128;
|
||||
d.rssi = (int8_t)rssi;
|
||||
scan_count++;
|
||||
return false; // keep scanning; collect everything
|
||||
}
|
||||
|
||||
// Discovery state changes. The library auto-restarts discovery after STOPPED if
|
||||
// nothing matched, so explicitly cancel to halt the loop.
|
||||
static void on_disc_state(esp_bt_gap_discovery_state_t state) {
|
||||
if (state == ESP_BT_GAP_DISCOVERY_STOPPED) {
|
||||
scanning = false;
|
||||
source.cancel_discovery();
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
last_read_cmd = cmd; // remember for the following onRequest (if any)
|
||||
switch (cmd) {
|
||||
case HUB_CMD_SET_DELAY:
|
||||
if (Wire.available() >= 2) {
|
||||
@ -105,6 +171,41 @@ static void on_i2c_receive(int n) {
|
||||
have_volume = true;
|
||||
}
|
||||
break;
|
||||
case HUB_CMD_SCAN_START:
|
||||
req_scan_start = true;
|
||||
break;
|
||||
case HUB_CMD_SCAN_STOP:
|
||||
req_scan_stop = true;
|
||||
break;
|
||||
case HUB_CMD_SELECT:
|
||||
if (Wire.available() >= 1) {
|
||||
pending_select = Wire.read();
|
||||
req_select = true;
|
||||
}
|
||||
break;
|
||||
case HUB_CMD_FORGET:
|
||||
req_forget = true;
|
||||
break;
|
||||
case HUB_CMD_GET_SCANITEM: {
|
||||
// Prep scanitem_buf NOW from the list (light memcpy is OK here).
|
||||
uint8_t idx = Wire.available() >= 1 ? Wire.read() : 0xFF;
|
||||
pending_scanitem = idx;
|
||||
uint8_t *b = (uint8_t *)scanitem_buf;
|
||||
memset(b, 0, HUB_SCANITEM_LEN);
|
||||
if (idx < scan_count) {
|
||||
ScanDev &d = scan_list[idx];
|
||||
b[0] = 1; // valid
|
||||
b[1] = (uint8_t)d.rssi; // rssi (int8)
|
||||
memcpy(&b[2], d.mac, 6); // MAC
|
||||
uint8_t nl = (uint8_t)strnlen(d.name, HUB_NAME_MAX);
|
||||
b[8] = nl; // name length
|
||||
memcpy(&b[9], d.name, nl); // name bytes
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HUB_CMD_GET_CURNAME:
|
||||
// Nothing to do: onRequest serves cur_name (NUL-padded).
|
||||
break;
|
||||
case HUB_CMD_GET_STATUS:
|
||||
// Nothing to do: onRequest serves the pre-filled status_buf.
|
||||
break;
|
||||
@ -114,9 +215,19 @@ static void on_i2c_receive(int n) {
|
||||
while (Wire.available()) Wire.read(); // drain any extra bytes
|
||||
}
|
||||
|
||||
// Master issued a read after GET_STATUS: hand back the current snapshot.
|
||||
// Master issued a read; pick the buffer keyed by the last command byte.
|
||||
static void on_i2c_request() {
|
||||
if (last_read_cmd == HUB_CMD_GET_SCANITEM) {
|
||||
Wire.write((uint8_t *)scanitem_buf, HUB_SCANITEM_LEN);
|
||||
} else if (last_read_cmd == HUB_CMD_GET_CURNAME) {
|
||||
uint8_t nbuf[HUB_NAME_MAX];
|
||||
uint8_t nl = (uint8_t)strnlen(cur_name, HUB_NAME_MAX);
|
||||
memcpy(nbuf, cur_name, nl);
|
||||
if (nl < HUB_NAME_MAX) memset(nbuf + nl, 0, HUB_NAME_MAX - nl);
|
||||
Wire.write(nbuf, HUB_NAME_MAX);
|
||||
} else {
|
||||
Wire.write((uint8_t *)status_buf, HUB_STATUS_LEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Continuously pull I2S into the FIFO (paced by Board A's master clock).
|
||||
@ -221,11 +332,31 @@ 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(current_volume);
|
||||
|
||||
// Load any saved/selected speaker (Phase 3). "spk_mac" is a 6-byte blob.
|
||||
uint8_t savedMac[6];
|
||||
size_t got = prefs.getBytes("spk_mac", savedMac, 6);
|
||||
if (got == 6) {
|
||||
memcpy(cur_mac, savedMac, 6);
|
||||
String sn = prefs.getString("spk_name", "");
|
||||
strncpy(cur_name, sn.c_str(), HUB_NAME_MAX);
|
||||
cur_name[HUB_NAME_MAX] = 0;
|
||||
has_device = true;
|
||||
source.set_auto_reconnect(cur_mac); // remember + auto-reconnect to it
|
||||
source.start();
|
||||
Serial.printf("[SRC] Reconnecting to saved '%s' (%02X:%02X:%02X:%02X:%02X:%02X) — trim %u ms\n",
|
||||
cur_name, cur_mac[0], cur_mac[1], cur_mac[2],
|
||||
cur_mac[3], cur_mac[4], cur_mac[5], delay_ms_current);
|
||||
} else {
|
||||
// No saved device — fall back to the hardcoded target by name.
|
||||
has_device = false;
|
||||
cur_name[0] = 0;
|
||||
source.set_auto_reconnect(true, 5);
|
||||
source.start(TARGET_SPEAKER);
|
||||
Serial.printf("[SRC] Connecting to '%s' — trim %u ms; touch + GPIO4, - GPIO27\n",
|
||||
Serial.printf("[SRC] Connecting to fallback '%s' — trim %u ms; touch + GPIO4, - GPIO27\n",
|
||||
TARGET_SPEAKER, delay_ms_current);
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
@ -249,11 +380,63 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Phase 3: apply discovery / selection requests (heavy work here) ---
|
||||
if (req_scan_start) {
|
||||
req_scan_start = false;
|
||||
scan_count = 0;
|
||||
memset(scan_list, 0, sizeof(scan_list));
|
||||
scanning = true;
|
||||
source.disconnect();
|
||||
source.set_auto_reconnect(false); // force a fresh discovery
|
||||
source.set_ssid_callback(on_ssid);
|
||||
source.set_discovery_mode_callback(on_disc_state);
|
||||
source.start(); // begin inquiry scan
|
||||
Serial.printf("[SRC %s] scan started\n", TARGET_SPEAKER);
|
||||
}
|
||||
if (req_scan_stop) {
|
||||
req_scan_stop = false;
|
||||
source.cancel_discovery();
|
||||
scanning = false;
|
||||
Serial.printf("[SRC %s] scan stopped (%u found)\n", TARGET_SPEAKER, scan_count);
|
||||
}
|
||||
if (req_select) {
|
||||
req_select = false;
|
||||
uint8_t idx = pending_select;
|
||||
if (idx < scan_count) {
|
||||
source.cancel_discovery();
|
||||
scanning = false;
|
||||
memcpy(cur_mac, scan_list[idx].mac, 6);
|
||||
strncpy(cur_name, scan_list[idx].name, HUB_NAME_MAX);
|
||||
cur_name[HUB_NAME_MAX] = 0;
|
||||
source.connect_to(cur_mac);
|
||||
source.set_auto_reconnect(cur_mac);
|
||||
prefs.putBytes("spk_mac", cur_mac, 6);
|
||||
prefs.putString("spk_name", cur_name);
|
||||
has_device = true;
|
||||
Serial.printf("[SRC %s] selected '%s' (%02X:%02X:%02X:%02X:%02X:%02X)\n",
|
||||
TARGET_SPEAKER, cur_name, cur_mac[0], cur_mac[1], cur_mac[2],
|
||||
cur_mac[3], cur_mac[4], cur_mac[5]);
|
||||
}
|
||||
}
|
||||
if (req_forget) {
|
||||
req_forget = false;
|
||||
source.disconnect();
|
||||
prefs.remove("spk_mac");
|
||||
prefs.remove("spk_name");
|
||||
has_device = false;
|
||||
cur_name[0] = 0;
|
||||
memset(cur_mac, 0, 6);
|
||||
Serial.printf("[SRC %s] forgot saved device\n", TARGET_SPEAKER);
|
||||
}
|
||||
|
||||
// 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;
|
||||
status_buf[4] = scanning ? 1 : 0;
|
||||
status_buf[5] = scan_count;
|
||||
status_buf[6] = has_device ? 1 : 0;
|
||||
|
||||
static unsigned long last_touch = 0;
|
||||
if (now - last_touch >= TOUCH_REPEAT_MS) {
|
||||
|
||||
@ -25,4 +25,28 @@
|
||||
// [1] delay_ms low byte
|
||||
// [2] delay_ms high byte
|
||||
// [3] volume (0..100)
|
||||
#define HUB_STATUS_LEN 4
|
||||
// [4] scanning (0/1) (Phase 3)
|
||||
// [5] scan device count (Phase 3)
|
||||
// [6] has a saved/selected device (0/1) (Phase 3)
|
||||
#define HUB_STATUS_LEN 7
|
||||
|
||||
// --- Phase 3: speaker discovery / selection -------------------------------
|
||||
// The source board runs a BT inquiry scan (audio pauses during a scan),
|
||||
// collects nearby audio sinks (name+MAC), and connects to a chosen one.
|
||||
#define HUB_CMD_SCAN_START 0x03 // write: clear list + begin a fresh discovery scan
|
||||
#define HUB_CMD_SCAN_STOP 0x04 // write: cancel scanning
|
||||
#define HUB_CMD_SELECT 0x05 // write [uint8 index]: connect to scan item + persist (NVS)
|
||||
#define HUB_CMD_FORGET 0x06 // write: forget saved device (disconnect + erase)
|
||||
#define HUB_CMD_GET_SCANITEM 0x14 // write [uint8 index]; THEN read HUB_SCANITEM_LEN bytes
|
||||
#define HUB_CMD_GET_CURNAME 0x15 // read HUB_NAME_MAX bytes: current/selected device name (NUL-padded)
|
||||
|
||||
#define HUB_MAX_SCAN 12 // max devices reported
|
||||
#define HUB_NAME_MAX 24 // max device-name length carried over the bus
|
||||
|
||||
// Scan-item payload (fixed length), returned on the read after GET_SCANITEM[index]:
|
||||
// [0] valid (0/1 — 0 if index >= count)
|
||||
// [1] rssi (int8)
|
||||
// [2..7] MAC (6 bytes)
|
||||
// [8] name length (<= HUB_NAME_MAX)
|
||||
// [9 ..] name bytes
|
||||
#define HUB_SCANITEM_LEN (9 + HUB_NAME_MAX) // 33
|
||||
|
||||
543
src/hub_s3.cpp
543
src/hub_s3.cpp
@ -1,11 +1,16 @@
|
||||
/**
|
||||
* BikeAudio — Hub (ESP32-S3-Touch-LCD-1.28) : PHASE 2 control bring-up
|
||||
* BikeAudio — Hub (ESP32-S3-Touch-LCD-1.28) : PHASE 3 discovery + control
|
||||
*
|
||||
* 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.
|
||||
* simple functional touch UI: each speaker's connect state, current device
|
||||
* name, delay (ms) and volume (%), with touch zones to nudge delay / volume
|
||||
* +/- and a "pick" button to scan for and select a nearby BT speaker.
|
||||
*
|
||||
* Two screens, dispatched in loop() by `screen`:
|
||||
* SCREEN_MAIN : per-speaker name + connect dot + [d-][d+][v-][v+] + [pick]
|
||||
* SCREEN_SCAN : live discovery list for one speaker (tap a row to select),
|
||||
* with [Rescan] and [Back] controls.
|
||||
*
|
||||
* Two I2C buses are in use, on separate ports:
|
||||
* - port 0 (Wire1, owned by LovyanGFX): CST816S touch SDA=6 SCL=7
|
||||
@ -22,6 +27,7 @@
|
||||
#define LGFX_USE_V1
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <string.h>
|
||||
#include <LovyanGFX.hpp>
|
||||
#include "hub_proto.h"
|
||||
|
||||
@ -94,18 +100,43 @@ LGFX lcd;
|
||||
// transaction itself succeeded (board present on the bus at all).
|
||||
struct Speaker {
|
||||
uint8_t addr;
|
||||
const char *name;
|
||||
const char *name; // role label (JBL / Cardo), not the BT device name
|
||||
bool online; // I2C transaction succeeded this poll
|
||||
bool connected; // board says its BT sink is connected
|
||||
uint16_t delay_ms;
|
||||
uint8_t volume;
|
||||
// Phase 3 discovery state (from the 7-byte status payload).
|
||||
bool scanning; // board is currently running a BT inquiry scan
|
||||
uint8_t scan_count; // devices found so far in the current scan
|
||||
bool has_device; // a device is saved/selected on the board
|
||||
char cur_name[HUB_NAME_MAX + 1]; // selected BT device name (NUL-term)
|
||||
};
|
||||
|
||||
static Speaker speakers[2] = {
|
||||
{ HUB_I2C_ADDR_JBL, "JBL", false, false, 0, 0 },
|
||||
{ HUB_I2C_ADDR_CARDO, "Cardo", false, false, 0, 0 },
|
||||
{ HUB_I2C_ADDR_JBL, "JBL", false, false, 0, 0, false, 0, false, {0} },
|
||||
{ HUB_I2C_ADDR_CARDO, "Cardo", false, false, 0, 0, false, 0, false, {0} },
|
||||
};
|
||||
|
||||
// A discovered scan item, mirrored from the board over GET_SCANITEM.
|
||||
struct ScanItem {
|
||||
uint8_t index; // index on the board (passed back in SELECT)
|
||||
int8_t rssi;
|
||||
uint8_t mac[6];
|
||||
char name[HUB_NAME_MAX + 1]; // NUL-terminated
|
||||
};
|
||||
|
||||
// Local mirror of the current scan list (for the speaker being configured).
|
||||
static ScanItem scanItems[HUB_MAX_SCAN];
|
||||
static int scanItemCount = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Screen state machine
|
||||
// ---------------------------------------------------------------------------
|
||||
enum Screen { SCREEN_MAIN, SCREEN_SCAN };
|
||||
static Screen screen = SCREEN_MAIN;
|
||||
static int cfgSpeaker = 0; // which speaker SCREEN_SCAN is configuring
|
||||
static int scanScroll = 0; // first visible row in the scan list
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// I2C helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -126,14 +157,118 @@ static void pollStatus(Speaker &s) {
|
||||
s.online = false;
|
||||
return;
|
||||
}
|
||||
uint8_t b0 = Wire1.read();
|
||||
uint8_t b1 = Wire1.read();
|
||||
uint8_t b2 = Wire1.read();
|
||||
uint8_t b3 = Wire1.read();
|
||||
uint8_t b[HUB_STATUS_LEN];
|
||||
for (int i = 0; i < HUB_STATUS_LEN; i++) b[i] = Wire1.read();
|
||||
s.online = true;
|
||||
s.connected = (b0 != 0);
|
||||
s.delay_ms = (uint16_t)(b1 | (b2 << 8));
|
||||
s.volume = b3;
|
||||
s.connected = (b[0] != 0);
|
||||
s.delay_ms = (uint16_t)(b[1] | (b[2] << 8));
|
||||
s.volume = b[3];
|
||||
s.scanning = (b[4] != 0);
|
||||
s.scan_count = b[5];
|
||||
s.has_device = (b[6] != 0);
|
||||
}
|
||||
|
||||
// Fetch the board's current/selected device name into out (HUB_NAME_MAX+1).
|
||||
// On failure out is left empty. The name comes back NUL-padded.
|
||||
static void getCurName(Speaker &s, char *out) {
|
||||
out[0] = '\0';
|
||||
Wire1.beginTransmission(s.addr);
|
||||
Wire1.write(HUB_CMD_GET_CURNAME);
|
||||
if (Wire1.endTransmission(true) != 0) return;
|
||||
int n = Wire1.requestFrom((int)s.addr, (int)HUB_NAME_MAX);
|
||||
if (n < HUB_NAME_MAX) {
|
||||
// drain whatever arrived to keep the bus sane
|
||||
while (Wire1.available()) Wire1.read();
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < HUB_NAME_MAX; i++) {
|
||||
out[i] = (char)Wire1.read();
|
||||
}
|
||||
// Payload is NUL-padded; force a terminator at the end of the buffer in
|
||||
// case the board sent a full HUB_NAME_MAX-length name with no NUL.
|
||||
out[HUB_NAME_MAX] = '\0';
|
||||
}
|
||||
|
||||
// Begin a fresh discovery scan on speaker s.
|
||||
static void startScan(Speaker &s) {
|
||||
Wire1.beginTransmission(s.addr);
|
||||
Wire1.write(HUB_CMD_SCAN_START);
|
||||
Wire1.endTransmission(true);
|
||||
}
|
||||
|
||||
// Cancel an in-progress scan on speaker s.
|
||||
static void sendScanStop(Speaker &s) {
|
||||
Wire1.beginTransmission(s.addr);
|
||||
Wire1.write(HUB_CMD_SCAN_STOP);
|
||||
Wire1.endTransmission(true);
|
||||
}
|
||||
|
||||
// Select scan item `index` on speaker s (connect + persist on the board).
|
||||
static void sendSelect(Speaker &s, uint8_t index) {
|
||||
Wire1.beginTransmission(s.addr);
|
||||
Wire1.write(HUB_CMD_SELECT);
|
||||
Wire1.write(index);
|
||||
Wire1.endTransmission(true);
|
||||
}
|
||||
|
||||
// Read scan item `index` from speaker s into `out`. Returns true if valid.
|
||||
static bool getScanItem(Speaker &s, uint8_t index, ScanItem &out) {
|
||||
Wire1.beginTransmission(s.addr);
|
||||
Wire1.write(HUB_CMD_GET_SCANITEM);
|
||||
Wire1.write(index);
|
||||
if (Wire1.endTransmission(true) != 0) return false;
|
||||
int n = Wire1.requestFrom((int)s.addr, (int)HUB_SCANITEM_LEN);
|
||||
if (n < HUB_SCANITEM_LEN) {
|
||||
while (Wire1.available()) Wire1.read();
|
||||
return false;
|
||||
}
|
||||
uint8_t valid = Wire1.read();
|
||||
int8_t rssi = (int8_t)Wire1.read();
|
||||
uint8_t mac[6];
|
||||
for (int i = 0; i < 6; i++) mac[i] = Wire1.read();
|
||||
uint8_t namelen = Wire1.read();
|
||||
if (namelen > HUB_NAME_MAX) namelen = HUB_NAME_MAX;
|
||||
char name[HUB_NAME_MAX + 1];
|
||||
for (int i = 0; i < HUB_NAME_MAX; i++) {
|
||||
char c = (char)Wire1.read();
|
||||
if (i < namelen) name[i] = c;
|
||||
}
|
||||
name[namelen] = '\0';
|
||||
if (!valid) return false;
|
||||
out.index = index;
|
||||
out.rssi = rssi;
|
||||
memcpy(out.mac, mac, 6);
|
||||
strncpy(out.name, name, sizeof(out.name));
|
||||
out.name[HUB_NAME_MAX] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refresh the local scan-list mirror for speaker `i` from the board. Reads up
|
||||
// to scan_count items (capped to HUB_MAX_SCAN) and stores the valid ones,
|
||||
// sorted strongest-RSSI first so the cap keeps the best candidates.
|
||||
static void pollScanList(int i) {
|
||||
Speaker &s = speakers[i];
|
||||
int want = s.scan_count;
|
||||
if (want > HUB_MAX_SCAN) want = HUB_MAX_SCAN;
|
||||
int got = 0;
|
||||
for (int idx = 0; idx < want && got < HUB_MAX_SCAN; idx++) {
|
||||
ScanItem it;
|
||||
if (getScanItem(s, (uint8_t)idx, it)) {
|
||||
scanItems[got++] = it;
|
||||
}
|
||||
}
|
||||
// Strongest RSSI first (simple insertion sort; list is tiny).
|
||||
for (int a = 1; a < got; a++) {
|
||||
ScanItem key = scanItems[a];
|
||||
int b = a - 1;
|
||||
while (b >= 0 && scanItems[b].rssi < key.rssi) {
|
||||
scanItems[b + 1] = scanItems[b];
|
||||
b--;
|
||||
}
|
||||
scanItems[b + 1] = key;
|
||||
}
|
||||
scanItemCount = got;
|
||||
if (scanScroll > scanItemCount) scanScroll = 0;
|
||||
}
|
||||
|
||||
// Command a new delay (ms) to a speaker.
|
||||
@ -153,32 +288,40 @@ static void sendVolume(const Speaker &s, uint8_t vol) {
|
||||
Wire1.endTransmission();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI layout
|
||||
// ---------------------------------------------------------------------------
|
||||
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;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// SCREEN_MAIN 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.
|
||||
// safe center column so they stay clear of the round bezel. Each half stacks:
|
||||
// title (role + connect dot), current device name, "Dnnn Vnn%" status,
|
||||
// a row of five buttons: [d-] [d+] [v-] [v+] [pick].
|
||||
//
|
||||
// y rows:
|
||||
// JBL title ~22, status ~46, buttons ~70..102
|
||||
// Cardo title ~138, status ~162, buttons ~186..218
|
||||
// y rows (per speaker half):
|
||||
// JBL title 20, name 38, status 56, buttons 74..106
|
||||
// Cardo title 138, name 156, status 174, buttons 134... (mirrored below)
|
||||
|
||||
#define BTN_W 34
|
||||
#define BTN_H 32
|
||||
#define BTN_GAP 6
|
||||
#define BTN_W 30
|
||||
#define BTN_H 30
|
||||
#define BTN_GAP 5
|
||||
|
||||
// 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
|
||||
// Main-screen button kinds, in draw/hit order.
|
||||
enum BtnKind { B_DELAY_DOWN = 0, B_DELAY_UP, B_VOL_DOWN, B_VOL_UP, B_PICK, B_COUNT };
|
||||
static const char *kBtnLabel[B_COUNT] = { "d-", "d+", "v-", "v+", "pk" };
|
||||
|
||||
static const int kBtnYTop[2] = { 70, 186 }; // button-row top Y for each speaker
|
||||
// 5 buttons centered horizontally.
|
||||
static const int kBtnTotalW = B_COUNT * BTN_W + (B_COUNT - 1) * BTN_GAP;
|
||||
static const int kBtnX0 = (240 - kBtnTotalW) / 2;
|
||||
static const int kBtnYTop[2] = { 78, 188 }; // button-row top Y per 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+" };
|
||||
static const int kTitleY[2] = { 18, 136 };
|
||||
static const int kNameY[2] = { 38, 156 };
|
||||
static const int kStatusY[2] = { 58, 176 };
|
||||
|
||||
// Bounding box of button `k` for speaker index `i`.
|
||||
// Bounding box of main-screen 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];
|
||||
@ -186,16 +329,14 @@ static void btnRect(int i, int k, int &x, int &y, int &w, int &h) {
|
||||
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.
|
||||
// Draw the control buttons for speaker `i`. `pressed` highlights one. The
|
||||
// pick button is tinted differently so it reads as a mode switch, not a nudge.
|
||||
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;
|
||||
uint16_t base = (k == B_PICK) ? TFT_PURPLE : TFT_NAVY;
|
||||
uint16_t fill = (k == pressed) ? TFT_DARKCYAN : base;
|
||||
lcd.fillRoundRect(x, y, w, h, 5, fill);
|
||||
lcd.drawRoundRect(x, y, w, h, 5, TFT_DARKGREY);
|
||||
lcd.setTextColor(TFT_WHITE, fill);
|
||||
@ -204,21 +345,29 @@ static void drawButtons(int i, int pressed = -1) {
|
||||
}
|
||||
}
|
||||
|
||||
// Title + status line for speaker `i` (online/connected, delay, volume).
|
||||
// Title + device name + delay/volume status for speaker `i`.
|
||||
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.
|
||||
// Title (role label) 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);
|
||||
|
||||
lcd.fillRect(0, kTitleY[i] - 9, 240, 18, TFT_BLACK);
|
||||
lcd.drawString(s.name, 120, kTitleY[i], &fonts::Font2);
|
||||
uint16_t dot = !s.online ? TFT_DARKGREY : (s.connected ? TFT_GREEN : TFT_RED);
|
||||
lcd.fillCircle(120 - 36, titleY, 5, dot);
|
||||
lcd.fillCircle(120 - 40, kTitleY[i], 5, dot);
|
||||
|
||||
// Status line: either a dash (offline) or "Dnnn Vnn%".
|
||||
// Current device name (or a hint to pick one).
|
||||
lcd.fillRect(0, kNameY[i] - 8, 240, 16, TFT_BLACK);
|
||||
if (s.cur_name[0]) {
|
||||
lcd.setTextColor(TFT_YELLOW, TFT_BLACK);
|
||||
lcd.drawString(s.cur_name, 120, kNameY[i], &fonts::Font2);
|
||||
} else {
|
||||
lcd.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||
lcd.drawString("(no device)", 120, kNameY[i], &fonts::Font2);
|
||||
}
|
||||
|
||||
// Delay/volume (or offline dash).
|
||||
char buf[40];
|
||||
if (!s.online) {
|
||||
snprintf(buf, sizeof(buf), "-- offline --");
|
||||
@ -227,13 +376,12 @@ static void drawStatus(int i) {
|
||||
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);
|
||||
lcd.fillRect(0, kStatusY[i] - 8, 240, 16, TFT_BLACK);
|
||||
lcd.drawString(buf, 120, kStatusY[i], &fonts::Font2);
|
||||
}
|
||||
|
||||
// Redraw the whole static frame once (background, divider, both halves).
|
||||
static void drawFrame() {
|
||||
// Redraw the whole MAIN frame (background, divider, both halves).
|
||||
static void drawMainScreen() {
|
||||
lcd.fillScreen(TFT_BLACK);
|
||||
lcd.drawCircle(120, 120, 118, TFT_DARKGREY);
|
||||
lcd.drawFastHLine(20, 120, 200, TFT_DARKGREY); // top/bottom divider
|
||||
@ -243,16 +391,10 @@ static void drawFrame() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Apply a delay/volume nudge for button `k` on speaker `i` and send it. The
|
||||
// pick button is handled by the caller (it switches screens). Returns true if
|
||||
// a value button was handled.
|
||||
static bool applyMainButton(int i, int k) {
|
||||
Speaker &s = speakers[i];
|
||||
switch (k) {
|
||||
case B_DELAY_DOWN: {
|
||||
@ -260,41 +402,265 @@ static bool handleTouch(int x, int y) {
|
||||
if (d < 0) d = 0;
|
||||
s.delay_ms = (uint16_t)d;
|
||||
sendDelay(s, s.delay_ms);
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
// Visual feedback + optimistic value update.
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// SCREEN_SCAN layout
|
||||
// ===========================================================================
|
||||
// One speaker's discovery list. Header at top, up to SCAN_ROWS device rows in
|
||||
// the safe center column, and a bottom control row: [<] [Rescan] [>] [Back].
|
||||
// - device rows are tappable -> SELECT that item
|
||||
// - [<]/[>] scroll when there are more devices than fit
|
||||
// - [Rescan] re-issues SCAN_START; [Back] cancels + returns to MAIN
|
||||
|
||||
#define SCAN_ROWS 4 // device rows visible at once
|
||||
#define SCAN_ROW_H 26
|
||||
#define SCAN_LIST_X 30
|
||||
#define SCAN_LIST_W 180
|
||||
static const int kScanListY0 = 56; // top of first device row
|
||||
|
||||
// Bottom control row.
|
||||
#define SCAN_CTL_Y 200
|
||||
#define SCAN_CTL_H 32
|
||||
enum ScanCtl { SC_SCROLL_UP = 0, SC_RESCAN, SC_SCROLL_DOWN, SC_BACK, SC_COUNT };
|
||||
static const char *kScanCtlLabel[SC_COUNT] = { "^", "Rscn", "v", "Back" };
|
||||
#define SCAN_CTL_W 40
|
||||
#define SCAN_CTL_GAP 6
|
||||
static const int kScanCtlTotalW = SC_COUNT * SCAN_CTL_W + (SC_COUNT - 1) * SCAN_CTL_GAP;
|
||||
static const int kScanCtlX0 = (240 - kScanCtlTotalW) / 2;
|
||||
|
||||
static void scanRowRect(int row, int &x, int &y, int &w, int &h) {
|
||||
x = SCAN_LIST_X;
|
||||
y = kScanListY0 + row * SCAN_ROW_H;
|
||||
w = SCAN_LIST_W;
|
||||
h = SCAN_ROW_H - 3;
|
||||
}
|
||||
|
||||
static void scanCtlRect(int c, int &x, int &y, int &w, int &h) {
|
||||
x = kScanCtlX0 + c * (SCAN_CTL_W + SCAN_CTL_GAP);
|
||||
y = SCAN_CTL_Y;
|
||||
w = SCAN_CTL_W;
|
||||
h = SCAN_CTL_H;
|
||||
}
|
||||
|
||||
// Redraw just the device-list area (header + rows). Used live while scanning.
|
||||
static void drawScanList() {
|
||||
const Speaker &s = speakers[cfgSpeaker];
|
||||
|
||||
// Header: "<role>: scanning" with a spinner, or "<n> found".
|
||||
lcd.fillRect(0, 24, 240, 22, TFT_BLACK);
|
||||
lcd.setTextDatum(middle_center);
|
||||
char hdr[40];
|
||||
if (s.scanning) {
|
||||
static const char spin[] = { '|', '/', '-', '\\' };
|
||||
char c = spin[(millis() / 250) % 4];
|
||||
snprintf(hdr, sizeof(hdr), "%s: scan %c", s.name, c);
|
||||
lcd.setTextColor(TFT_ORANGE, TFT_BLACK);
|
||||
} else {
|
||||
snprintf(hdr, sizeof(hdr), "%s: %d found", s.name, scanItemCount);
|
||||
lcd.setTextColor(TFT_CYAN, TFT_BLACK);
|
||||
}
|
||||
lcd.drawString(hdr, 120, 34, &fonts::Font2);
|
||||
|
||||
// Device rows.
|
||||
lcd.fillRect(0, kScanListY0 - 2, 240, SCAN_ROWS * SCAN_ROW_H + 4, TFT_BLACK);
|
||||
lcd.setTextDatum(middle_left);
|
||||
for (int row = 0; row < SCAN_ROWS; row++) {
|
||||
int idx = scanScroll + row;
|
||||
int x, y, w, h;
|
||||
scanRowRect(row, x, y, w, h);
|
||||
if (idx >= scanItemCount) {
|
||||
if (idx == 0 && !s.scanning) {
|
||||
// empty result
|
||||
lcd.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||
lcd.setTextDatum(middle_center);
|
||||
if (row == 0)
|
||||
lcd.drawString("(none)", 120, y + h / 2, &fonts::Font2);
|
||||
lcd.setTextDatum(middle_left);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const ScanItem &it = scanItems[idx];
|
||||
lcd.fillRoundRect(x, y, w, h, 4, TFT_NAVY);
|
||||
lcd.drawRoundRect(x, y, w, h, 4, TFT_DARKGREY);
|
||||
// RSSI on the right, name on the left (clipped by the box width).
|
||||
char rb[8];
|
||||
snprintf(rb, sizeof(rb), "%d", it.rssi);
|
||||
lcd.setTextColor(TFT_GREENYELLOW, TFT_NAVY);
|
||||
lcd.setTextDatum(middle_right);
|
||||
lcd.drawString(rb, x + w - 6, y + h / 2, &fonts::Font2);
|
||||
lcd.setTextColor(TFT_WHITE, TFT_NAVY);
|
||||
lcd.setTextDatum(middle_left);
|
||||
// Truncate name to keep it from colliding with the RSSI.
|
||||
char nm[HUB_NAME_MAX + 1];
|
||||
strncpy(nm, it.name, sizeof(nm));
|
||||
nm[HUB_NAME_MAX] = '\0';
|
||||
if (strlen(nm) > 14) nm[14] = '\0';
|
||||
lcd.drawString(nm[0] ? nm : "(unnamed)", x + 6, y + h / 2, &fonts::Font2);
|
||||
}
|
||||
|
||||
// "more below/above" hint when the list overflows.
|
||||
if (scanItemCount > SCAN_ROWS) {
|
||||
lcd.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||
lcd.setTextDatum(middle_center);
|
||||
char mb[24];
|
||||
snprintf(mb, sizeof(mb), "%d-%d / %d",
|
||||
scanScroll + 1,
|
||||
min(scanScroll + SCAN_ROWS, scanItemCount),
|
||||
scanItemCount);
|
||||
lcd.drawString(mb, 120, kScanListY0 + SCAN_ROWS * SCAN_ROW_H + 6,
|
||||
&fonts::Font2);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the bottom control row.
|
||||
static void drawScanCtls(int pressed = -1) {
|
||||
for (int c = 0; c < SC_COUNT; c++) {
|
||||
int x, y, w, h;
|
||||
scanCtlRect(c, x, y, w, h);
|
||||
uint16_t base = (c == SC_BACK) ? TFT_MAROON : TFT_NAVY;
|
||||
uint16_t fill = (c == pressed) ? TFT_DARKCYAN : base;
|
||||
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(kScanCtlLabel[c], x + w / 2, y + h / 2, &fonts::Font2);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawScanScreen() {
|
||||
lcd.fillScreen(TFT_BLACK);
|
||||
lcd.drawCircle(120, 120, 118, TFT_DARKGREY);
|
||||
drawScanList();
|
||||
drawScanCtls();
|
||||
}
|
||||
|
||||
// Enter SCREEN_SCAN for speaker `i`: kick off a scan and draw the screen.
|
||||
static void enterScan(int i) {
|
||||
cfgSpeaker = i;
|
||||
scanScroll = 0;
|
||||
scanItemCount = 0;
|
||||
screen = SCREEN_SCAN;
|
||||
startScan(speakers[i]);
|
||||
Serial.printf("[scan] start for %s\n", speakers[i].name);
|
||||
drawScanScreen();
|
||||
}
|
||||
|
||||
// Leave SCREEN_SCAN back to MAIN (optionally cancelling an active scan).
|
||||
static void leaveScan(bool cancel) {
|
||||
if (cancel) sendScanStop(speakers[cfgSpeaker]);
|
||||
screen = SCREEN_MAIN;
|
||||
drawMainScreen();
|
||||
}
|
||||
|
||||
// ---- touch handlers, per screen ----
|
||||
|
||||
// Returns true if a control was hit (consumes the touch for debounce).
|
||||
static bool handleTouchMain(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;
|
||||
|
||||
if (k == B_PICK) {
|
||||
drawButtons(i, k); // brief feedback before redraw
|
||||
enterScan(i);
|
||||
return true;
|
||||
}
|
||||
applyMainButton(i, k);
|
||||
drawButtons(i, k);
|
||||
drawStatus(i);
|
||||
Serial.printf("[touch] %s %s -> D%ums V%u%%\n",
|
||||
s.name, kBtnLabel[k], s.delay_ms, s.volume);
|
||||
speakers[i].name, kBtnLabel[k],
|
||||
speakers[i].delay_ms, speakers[i].volume);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool handleTouchScan(int x, int y) {
|
||||
// Device rows first.
|
||||
for (int row = 0; row < SCAN_ROWS; row++) {
|
||||
int idx = scanScroll + row;
|
||||
if (idx >= scanItemCount) continue;
|
||||
int rx, ry, rw, rh;
|
||||
scanRowRect(row, rx, ry, rw, rh);
|
||||
if (inRect(x, y, rx, ry, rw, rh)) {
|
||||
const ScanItem &it = scanItems[idx];
|
||||
Serial.printf("[scan] select %s idx=%u for %s\n",
|
||||
it.name, it.index, speakers[cfgSpeaker].name);
|
||||
sendSelect(speakers[cfgSpeaker], it.index);
|
||||
// Optimistically show it as the current device on MAIN.
|
||||
strncpy(speakers[cfgSpeaker].cur_name, it.name,
|
||||
sizeof(speakers[cfgSpeaker].cur_name));
|
||||
speakers[cfgSpeaker].cur_name[HUB_NAME_MAX] = '\0';
|
||||
leaveScan(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Bottom controls.
|
||||
for (int c = 0; c < SC_COUNT; c++) {
|
||||
int cx, cy, cw, ch;
|
||||
scanCtlRect(c, cx, cy, cw, ch);
|
||||
if (!inRect(x, y, cx, cy, cw, ch)) continue;
|
||||
drawScanCtls(c);
|
||||
switch (c) {
|
||||
case SC_SCROLL_UP:
|
||||
if (scanScroll > 0) { scanScroll--; drawScanList(); }
|
||||
drawScanCtls();
|
||||
break;
|
||||
case SC_SCROLL_DOWN:
|
||||
if (scanScroll + SCAN_ROWS < scanItemCount) {
|
||||
scanScroll++;
|
||||
drawScanList();
|
||||
}
|
||||
drawScanCtls();
|
||||
break;
|
||||
case SC_RESCAN:
|
||||
scanScroll = 0;
|
||||
scanItemCount = 0;
|
||||
startScan(speakers[cfgSpeaker]);
|
||||
Serial.printf("[scan] rescan for %s\n",
|
||||
speakers[cfgSpeaker].name);
|
||||
drawScanScreen();
|
||||
break;
|
||||
case SC_BACK:
|
||||
leaveScan(true);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
@ -309,32 +675,61 @@ void setup() {
|
||||
lcd.init();
|
||||
lcd.setRotation(0);
|
||||
lcd.setBrightness(200);
|
||||
drawFrame();
|
||||
|
||||
// Seed device names + status once so MAIN draws populated.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
pollStatus(speakers[i]);
|
||||
getCurName(speakers[i], speakers[i].cur_name);
|
||||
}
|
||||
drawMainScreen();
|
||||
Serial.println("[LCD] UI drawn");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
unsigned long now = millis();
|
||||
|
||||
// ---- touch (debounced) ----
|
||||
// ---- touch (debounced), dispatched by current screen ----
|
||||
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;
|
||||
}
|
||||
bool hit = (screen == SCREEN_MAIN) ? handleTouchMain(tx, ty)
|
||||
: handleTouchScan(tx, ty);
|
||||
if (hit) lastTouch = now;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- status poll (~1 Hz) ----
|
||||
// ---- per-screen periodic work ----
|
||||
static unsigned long lastPoll = 0;
|
||||
if (now - lastPoll >= POLL_PERIOD_MS) {
|
||||
lastPoll = now;
|
||||
if (screen == SCREEN_MAIN) {
|
||||
// Status + occasional device-name refresh for both speakers.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
pollStatus(speakers[i]);
|
||||
getCurName(speakers[i], speakers[i].cur_name);
|
||||
drawStatus(i);
|
||||
}
|
||||
} else { // SCREEN_SCAN
|
||||
Speaker &s = speakers[cfgSpeaker];
|
||||
bool wasScanning = s.scanning;
|
||||
uint8_t prevCount = s.scan_count;
|
||||
pollStatus(s);
|
||||
// Fetch the list when devices appear or when the scan finishes.
|
||||
if ((s.scan_count > 0 && s.scan_count != prevCount) ||
|
||||
(wasScanning && !s.scanning)) {
|
||||
pollScanList(cfgSpeaker);
|
||||
}
|
||||
drawScanList(); // also animates the scanning spinner
|
||||
}
|
||||
}
|
||||
|
||||
// While scanning, animate the spinner a bit faster than the 1 Hz poll.
|
||||
static unsigned long lastSpin = 0;
|
||||
if (screen == SCREEN_SCAN && speakers[cfgSpeaker].scanning &&
|
||||
now - lastSpin >= 250) {
|
||||
lastSpin = now;
|
||||
drawScanList();
|
||||
}
|
||||
|
||||
// ---- heartbeat ----
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user