Enhance Bluetooth functionality and GUI integration

- Add Bluetooth device list management with discovery and pairing support
- Implement volume control with AVRC integration
- Add system event handling for GUI-to-Bluetooth communication
- Enhance menu system with device selection and volume control pages
- Improve memory management by making menu creation lazy
- Add proper event subscription between GUI and Bluetooth modules
- Update filter coefficient for improved audio clicking behavior

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Brent Perteet
2025-08-25 16:46:16 +00:00
parent 25f875b3b2
commit 439c6ef22d
7 changed files with 826 additions and 5831 deletions

View File

@@ -1,5 +1,6 @@
#include <stdio.h>
#include <math.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -19,6 +20,7 @@
#include "keypad.h"
#include "bubble.h"
#include "system.h"
#include "bt_app.h"
#define DEVKIT
#undef DEVKIT
@@ -62,6 +64,13 @@ static void gui_task(void *pvParameters);
static void createBubble(lv_obj_t * scr);
static void build_scrollable_menu(void);
static void currentFocusIndex(menu_context_t *ctx);
static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text);
static lv_obj_t* create_menu_container(void);
static void show_bt_device_list(void);
static void show_volume_control(void);
static lv_obj_t* create_volume_page(void);
static void update_volume_display(int volume);
static void ensure_menu_styles(void);
#define MAX_ITEMS 10
#define VISIBLE_ITEMS 3
@@ -98,6 +107,54 @@ static bool notify_lvgl_flush_ready(void *user_ctx) {
return true;
}
static lv_obj_t* create_main_page(void) {
lv_obj_t* main_page;
ensure_menu_styles();
lv_obj_t* menu = create_menu_container();
main_page = lv_menu_page_create(menu, NULL);
lv_obj_set_style_radius(main_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(main_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_scrollbar_mode(main_page, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_size(main_page, lv_pct(100), lv_pct(100));
// Add menu items
lv_obj_t* tmpObj;
tmpObj = addMenuItem(main_page, "Bluetooth");
lv_obj_add_state(tmpObj, LV_STATE_FOCUSED); // First item focused
addMenuItem(main_page, "Calibration");
addMenuItem(main_page, "Volume");
addMenuItem(main_page, "About");
addMenuItem(main_page, "Exit");
return main_page;
}
static void show_menu(void) {
lv_obj_t* menu = create_menu_container();
lv_obj_t* main_page = create_main_page();
lv_menu_set_page(menu, main_page);
_currentPage = main_page;
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
}
static void cleanup_menu(void) {
if (_menu) {
lv_obj_del(_menu);
_menu = NULL;
_currentPage = NULL;
ESP_LOGI(TAG, "Menu cleaned up to free memory");
}
}
static void show_bubble(void) {
cleanup_menu(); // Free menu memory when returning to bubble
lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
}
static void create_lvgl_demo(void)
{
@@ -107,7 +164,7 @@ static void create_lvgl_demo(void)
lv_obj_t *scr = lv_scr_act();
createBubble(scr);
//build_scrollable_menu();
// Menu will be created lazily when needed
lvgl_port_unlock();
@@ -167,7 +224,7 @@ static void lvgl_init(void)
#if 1
const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = 4, // LVGL task priority
.task_stack = 16384, // LVGL task stack size
.task_stack = 8192, // LVGL task stack size (reduced for memory savings)
.task_affinity = 0, // LVGL task can run on any core
.task_max_sleep_ms = 500, // Maximum sleep in LVGL task
.timer_period_ms = 5 // LVGL timer period
@@ -236,7 +293,7 @@ void gui_start(void)
gpio_set_level(PIN_NUM_BK_LIGHT, 1);
xTaskCreate(gui_task, "gui_task", 4096, NULL, 5, NULL);
xTaskCreate(gui_task, "gui_task", 8192, NULL, 5, NULL);
}
@@ -281,11 +338,30 @@ static const char * items[] = { // your menu entries
static lv_obj_t * btn_array[ITEM_COUNT];
static int selected_idx = 0;
// Called whenever a button is activated (Enter/Click)
// Called whenever a button is "activated" (Enter/Click)
static void btn_click_cb(lv_event_t * e) {
lv_obj_t * btn = lv_event_get_target(e);
const char * txt = lv_label_get_text(lv_obj_get_child(btn, 0));
ESP_LOGI(TAG, "Activated: %s\n", txt);
// Handle specific menu items
if (strcmp(txt, "Bluetooth") == 0) {
LOCK();
show_bt_device_list();
UNLOCK();
} else if (strcmp(txt, "Volume") == 0) {
LOCK();
show_volume_control();
UNLOCK();
} else if (strcmp(txt, "Exit") == 0) {
LOCK();
_mode = GUI_BUBBLE;
show_bubble();
UNLOCK();
}
// Add more menu handlers here as needed
ESP_LOGI(TAG, "End btn_click_cb");
}
// Repaint all rows so only btn_array[selected_idx] is highlighted
@@ -384,10 +460,22 @@ static void activate_selected(void)
static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text)
{
if (!page || !text) {
ESP_LOGE(TAG, "Null parameters in addMenuItem");
return NULL;
}
// Ensure styles are initialized
ensure_menu_styles();
lv_obj_t * btn = lv_btn_create(page);
if (!btn) {
ESP_LOGE(TAG, "Failed to create button in addMenuItem");
return NULL;
}
lv_obj_set_size(btn, LV_PCT(100), ROW_H);
lv_obj_add_style(btn, &_styleUnfocusedBtn, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_add_style(btn, &_styleFocusedBtn, LV_PART_MAIN | LV_STATE_FOCUSED);
@@ -402,6 +490,12 @@ static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text)
// label & center
lv_obj_t * lbl = lv_label_create(btn);
if (!lbl) {
ESP_LOGE(TAG, "Failed to create label in addMenuItem");
lv_obj_del(btn);
return NULL;
}
lv_label_set_text(lbl, text);
lv_obj_set_style_radius(lbl, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(lbl, 0, LV_PART_MAIN | LV_STATE_ANY);
@@ -441,6 +535,309 @@ static void currentFocusIndex(menu_context_t *ctx)
}
// ───── LAZY MENU CREATION ─────
static void ensure_menu_styles(void) {
static bool styles_initialized = false;
if (!styles_initialized) {
lv_style_init(&_styleFocusedBtn);
lv_style_init(&_styleUnfocusedBtn);
lv_style_set_bg_color(&_styleUnfocusedBtn, lv_color_make(0xff,0xff,0xff)); // white bg
lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_black()); // orange text
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0x33,0x99,0xFF)); // blue bg
lv_style_set_text_color(&_styleFocusedBtn, lv_color_white()); // black text
styles_initialized = true;
}
}
static lv_obj_t* create_menu_container(void) {
if (_menu == NULL) {
_menu = lv_menu_create(lv_scr_act());
lv_obj_set_style_radius(_menu, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(_menu, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_size(_menu, lv_pct(100), lv_pct(100));
lv_obj_center(_menu);
lv_obj_set_scrollbar_mode(_menu, LV_SCROLLBAR_MODE_AUTO);
lv_obj_add_flag(_menu, LV_OBJ_FLAG_HIDDEN); // Hidden by default
}
return _menu;
}
// ───── BLUETOOTH DEVICE LIST PAGE ─────
static lv_obj_t* _bt_page = NULL;
static int _bt_selected_device = 0;
// ───── VOLUME CONTROL PAGE ─────
static lv_obj_t* _volume_page = NULL;
static lv_obj_t* _volume_bar = NULL;
static lv_obj_t* _volume_label = NULL;
static int _current_volume = 50; // Default volume (0-100)
static void bt_device_click_cb(lv_event_t * e) {
if (!e) {
ESP_LOGE(TAG, "Null event in bt_device_click_cb");
return;
}
lv_obj_t * btn = lv_event_get_target(e);
if (!btn) {
ESP_LOGE(TAG, "Null button in bt_device_click_cb");
return;
}
lv_obj_t * child = lv_obj_get_child(btn, 0);
if (!child) {
ESP_LOGE(TAG, "Null child in bt_device_click_cb");
return;
}
const char * txt = lv_label_get_text(child);
if (!txt) {
ESP_LOGE(TAG, "Null text in bt_device_click_cb");
return;
}
// Handle special buttons
if (strcmp(txt, "Back") == 0) {
LOCK();
bt_stop_discovery();
_mode = GUI_MENU;
show_menu();
UNLOCK();
return;
} else if (strcmp(txt, "Refresh") == 0) {
LOCK();
// Use system event instead of direct BT call
system_requestBtRefresh();
UNLOCK();
return;
}
// Find which device was clicked
bt_device_list_t* device_list = bt_get_device_list();
if (!device_list) {
ESP_LOGE(TAG, "Null device list in bt_device_click_cb");
return;
}
if (!_bt_page) {
ESP_LOGE(TAG, "Null _bt_page in bt_device_click_cb");
return;
}
for (int i = 0; i < device_list->count; i++) {
lv_obj_t * child = lv_obj_get_child(_bt_page, i);
if (child == btn) {
ESP_LOGI(TAG, "Requesting connection to device %d: %s", i, device_list->devices[i].name);
// Use system event instead of direct BT call
system_requestBtConnect(i);
// Return to bubble mode after selection
_mode = GUI_BUBBLE;
show_bubble();
return;
}
}
}
static lv_obj_t* create_bt_device_page(void) {
ESP_LOGI(TAG, "Creating Bluetooth device page");
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container");
return NULL;
}
_bt_page = lv_menu_page_create(menu, NULL);
if (!_bt_page) {
ESP_LOGE(TAG, "Failed to create Bluetooth page");
return NULL;
}
lv_obj_set_style_radius(_bt_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(_bt_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_scrollbar_mode(_bt_page, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_size(_bt_page, lv_pct(100), lv_pct(100));
ESP_LOGI(TAG, "Starting Bluetooth discovery");
bool discovery_started = true;
#if 0
// Try to start discovery (may fail if BT stack is busy)
bool discovery_started = bt_start_discovery();
if (!discovery_started) {
ESP_LOGW(TAG, "Discovery not started - will show paired devices only");
}
#endif
// Get device list
ESP_LOGI(TAG, "Getting device list");
bt_device_list_t* device_list = bt_get_device_list();
if (!device_list) {
ESP_LOGE(TAG, "Failed to get device list");
return _bt_page;
}
if (device_list->count == 0) {
// Show appropriate message based on discovery status
const char* msg = discovery_started ? "Scanning for devices..." : "No devices found";
lv_obj_t* tmpObj = addMenuItem(_bt_page, msg);
lv_obj_add_state(tmpObj, LV_STATE_DISABLED);
} else {
// Add devices to the page
bool first = true;
for (int i = 0; i < device_list->count; i++) {
char device_text[64];
snprintf(device_text, sizeof(device_text), "%s%s",
device_list->devices[i].name,
device_list->devices[i].is_paired ? " (paired)" : "");
lv_obj_t* btn = addMenuItem(_bt_page, device_text);
lv_obj_add_event_cb(btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
if (first) {
lv_obj_add_state(btn, LV_STATE_FOCUSED);
first = false;
}
}
}
// Add back/refresh options
lv_obj_t* refresh_btn = addMenuItem(_bt_page, "Refresh");
lv_obj_add_event_cb(refresh_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t* back_btn = addMenuItem(_bt_page, "Back");
lv_obj_add_event_cb(back_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
return _bt_page;
}
static void show_bt_device_list(void) {
ESP_LOGI(TAG, "Showing Bluetooth device list");
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container for Bluetooth list");
return;
}
lv_obj_t* bt_page = create_bt_device_page();
if (!bt_page) {
ESP_LOGE(TAG, "Failed to create Bluetooth device page");
return;
}
lv_menu_set_page(menu, bt_page);
_currentPage = bt_page;
_mode = GUI_MENU; // Keep in menu mode
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
ESP_LOGI(TAG, "Bluetooth device list displayed");
}
// ───── VOLUME CONTROL PAGE ─────
static lv_obj_t* create_volume_page(void) {
ESP_LOGI(TAG, "Creating volume control page");
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container for volume control");
return NULL;
}
_volume_page = lv_menu_page_create(menu, NULL);
if (!_volume_page) {
ESP_LOGE(TAG, "Failed to create volume page");
return NULL;
}
lv_obj_set_style_radius(_volume_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(_volume_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_scrollbar_mode(_volume_page, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_size(_volume_page, lv_pct(100), lv_pct(100));
// Create title label
lv_obj_t* title = lv_label_create(_volume_page);
lv_label_set_text(title, "Volume Control");
lv_obj_set_style_text_align(title, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 5);
// Create volume bar (progress bar)
_volume_bar = lv_bar_create(_volume_page);
lv_obj_set_size(_volume_bar, 120, 20);
lv_obj_align(_volume_bar, LV_ALIGN_CENTER, 0, -10);
lv_bar_set_range(_volume_bar, 0, 100);
lv_bar_set_value(_volume_bar, _current_volume, LV_ANIM_OFF);
// Create volume percentage label
_volume_label = lv_label_create(_volume_page);
char volume_text[16];
snprintf(volume_text, sizeof(volume_text), "%d%%", _current_volume);
lv_label_set_text(_volume_label, volume_text);
lv_obj_set_style_text_align(_volume_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(_volume_label, LV_ALIGN_CENTER, 0, 15);
// Create instruction labels
lv_obj_t* instr1 = lv_label_create(_volume_page);
lv_label_set_text(instr1, "KEY0: Volume Up");
lv_obj_set_style_text_align(instr1, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(instr1, LV_ALIGN_BOTTOM_MID, 0, -25);
lv_obj_t* instr2 = lv_label_create(_volume_page);
lv_label_set_text(instr2, "KEY1: Volume Down");
lv_obj_set_style_text_align(instr2, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(instr2, LV_ALIGN_BOTTOM_MID, 0, -10);
return _volume_page;
}
static void show_volume_control(void) {
ESP_LOGI(TAG, "Showing volume control");
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container for volume control");
return;
}
lv_obj_t* volume_page = create_volume_page();
if (!volume_page) {
ESP_LOGE(TAG, "Failed to create volume page");
return;
}
lv_menu_set_page(menu, volume_page);
_currentPage = volume_page;
_mode = GUI_MENU; // Keep in menu mode
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
ESP_LOGI(TAG, "Volume control displayed");
}
static void update_volume_display(int volume) {
if (_volume_bar && _volume_label) {
LOCK();
lv_bar_set_value(_volume_bar, volume, LV_ANIM_ON);
char volume_text[16];
snprintf(volume_text, sizeof(volume_text), "%d%%", volume);
lv_label_set_text(_volume_label, volume_text);
UNLOCK();
_current_volume = volume;
ESP_LOGI(TAG, "Volume display updated to %d%%", volume);
}
}
// ───── BUILD THE MENU ─────
static void build_scrollable_menu(void) {
@@ -523,10 +920,8 @@ static void gui_task(void *pvParameters)
uint32_t ev = 0;
LOCK();
// _mode = GUI_MENU;
// lv_obj_remove_flag(_menu, LV_OBJ_FLAG_HIDDEN);
_mode = GUI_BUBBLE;
lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
//lv_obj_add_flag(_menu, LV_OBJ_FLAG_HIDDEN);
UNLOCK();
ESP_LOGI(TAG, "Start GUI Task...");
@@ -537,7 +932,7 @@ static void gui_task(void *pvParameters)
if (xQueueReceive(q, &ev, pdMS_TO_TICKS(10)) == pdTRUE)
{
switch (ev) {
#if 0
case (KEY_UP << KEY_LONG_PRESS):
{
system_setZeroAngle();
@@ -549,48 +944,95 @@ static void gui_task(void *pvParameters)
system_clearZeroAngle();
break;
}
#if 0
case (KEY0 << KEY_SHORT_PRESS):
#endif
case (KEY_UP << KEY_SHORT_PRESS):
if (_mode == GUI_MENU)
{
menuNext();
// Check if we're on the volume control page
if (_currentPage == _volume_page) {
// Volume up
if (_current_volume < 100) {
_current_volume += 5;
if (_current_volume > 100) _current_volume = 100;
update_volume_display(_current_volume);
system_requestVolumeUp();
}
} else {
menuNext();
}
}
ESP_LOGI(TAG, "MAIN: Button 1 SHORT");
ESP_LOGI(TAG, "MAIN: Button UP SHORT");
break;
case (KEY_DOWN << KEY_SHORT_PRESS):
{
if (_mode == GUI_MENU)
{
// Check if we're on the volume control page
if (_currentPage == _volume_page) {
// Volume down
if (_current_volume > 0) {
_current_volume -= 5;
if (_current_volume < 0) _current_volume = 0;
update_volume_display(_current_volume);
system_requestVolumeDown();
}
} else {
menuPrevious();
}
}
ESP_LOGI(TAG, "MAIN: Button DOWN SHORT");
break;
}
case (KEY0 << KEY_LONG_PRESS):
{
ESP_LOGI(TAG, "MAIN: Button 0 LONG - Enter");
LOCK();
if (_mode != GUI_MENU)
{
_mode = GUI_MENU;
lv_obj_remove_flag(_menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
show_menu(); // Use lazy loading
}
else
if (_mode == GUI_MENU)
else if (_mode == GUI_MENU)
{
activate_selected();
_mode = GUI_BUBBLE;
// lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
// lv_obj_add_flag(_menu, LV_OBJ_FLAG_HIDDEN);
}
break;
}
case (KEY1 << KEY_SHORT_PRESS):
{
if (_mode == GUI_MENU)
{
menuPrevious();
// Check if we're in special pages - don't auto-exit
if (_currentPage == _bt_page) {
// Don't automatically exit to bubble mode from Bluetooth page
} else if (_currentPage == _volume_page) {
// Don't automatically exit to bubble mode from Volume page
} else {
ESP_LOGI(TAG, "return to main");
// In main menu - activate selection and exit to bubble
activate_selected();
_mode = GUI_BUBBLE;
show_bubble(); // Cleanup menu and show bubble
}
}
UNLOCK();
ESP_LOGI(TAG, "MAIN: Button 0 LONG - Exit");
break;
}
case (KEY1 << KEY_LONG_PRESS):
ESP_LOGI(TAG, "MAIN: Button 2 LONG");
gpio_set_level(PIN_NUM_nON, 0);
{
LOCK();
if (_mode == GUI_MENU)
{
_mode = GUI_BUBBLE;
show_bubble(); // Cleanup menu and show bubble
}
else
{
// Power off on long press from bubble mode
gpio_set_level(PIN_NUM_nON, 0);
}
UNLOCK();
ESP_LOGI(TAG, "MAIN: Button 1 LONG - Back/Power");
break;
#endif
}
default:
break;
}