Files
soundshot/main/gui.c
Brent Perteet b8a3a09e9f Fix Bluetooth pairing, menu crashes, and improve UX
Major fixes and improvements to Bluetooth device management and menu navigation:

**Bluetooth Device Pairing**
- Fixed discovered devices not being saved as paired after connection
- Only save devices to NVS when successfully connected (not just discovered)
- Auto-pair discovered devices on successful A2DP connection
- Update device list to show paired status immediately after connection

**Critical Bug Fixes**
- Fixed dangling pointer crash in NVS request/response mechanism
  - Was sending pointer to stack variable, now sends result value directly
  - Prevents crash when connecting to Bluetooth devices
- Fixed use-after-free crash when clicking menu items after dynamic updates
  - Menu context now properly synchronized after adding/removing items
  - Prevents InstructionFetchError crashes in menu navigation
- Fixed memory exhaustion by reducing MAX_BT_DEVICES from 20 to 8
  - Prevents heap allocation failures when populating device list

**Menu & UX Improvements**
- "Clear Paired" button now properly disconnects active connections
- "Clear Paired" button always visible when paired devices exist
- GUI updates immediately after clearing paired devices
- Paired devices marked with asterisk prefix (* Device Name)
- Removed redundant "(paired)" suffix text
- Long device names scroll smoothly when selected (3-second animation)
- Refresh button preserved during menu updates to prevent crashes
- Menu focus state properly maintained across all dynamic updates

**Technical Details**
- bt_add_discovered_device() no longer saves to NVS
- Added currentFocusIndex() calls after all menu modifications
- Improved clear_bt_device_list() to avoid deleting active buttons
- Added bt_disconnect_current_device() for clean disconnections
- Fixed NVS notification mechanism to avoid stack variable pointers

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 11:39:58 -06:00

1577 lines
49 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
#include <math.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "lvgl.h"
#include "esp_lvgl_port.h"
#include "gui.h"
#include "gpio.h"
#include "keypad.h"
#include "bubble.h"
#include "system.h"
#include "bt_app.h"
#define DEVKIT
#undef DEVKIT
static const char *TAG = "gui";
#define UNLOCK() lvgl_port_unlock()
#define LOCK() lvgl_port_lock(0)
// LCD Pin Configuration
#define LCD_PIXEL_CLOCK_HZ (5 * 1000 * 1000)
#define LCD_SPI_HOST SPI2_HOST
#define PIN_NUM_nON 26
// ST7735S properties
#define LCD_H_RES 160
#define LCD_V_RES 80
#define LCD_CMD_BITS 8
#define LCD_PARAM_BITS 8
#define LCD_COLOR_SPACE ESP_LCD_COLOR_SPACE_RGB
#define LCD_BITS_PER_PIXEL 16
static esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_io_handle_t io_handle = NULL;
static lv_disp_t *disp = NULL;
static lv_obj_t *imu_label = NULL; // Label for IMU data
static lv_style_t style_mono8;
typedef struct
{
int selected;
int count;
lv_obj_t *obj;
} menu_context_t;
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);
static void bt_device_click_cb(lv_event_t * e);
// Menu stack management functions
static void menu_stack_push(lv_obj_t *page);
static lv_obj_t* menu_stack_pop(void);
static void menu_stack_clear(void);
static bool menu_stack_is_empty(void);
static void menu_go_back(void);
#define MAX_ITEMS 10
#define VISIBLE_ITEMS 3
#define MENU_MAX_STRING_LENGTH 30
static const char *menu_items[MAX_ITEMS] = {
"V-Moda Crossfade", "Item 2", "Item 3", "Item 4",
"Item 5", "Item 6", "Item 7", "Item 8",
"Item 9", "Item 10"
};
static lv_obj_t *list;
static lv_obj_t *buttons[MAX_ITEMS];
static int selected_index = 0;
static lv_obj_t *_bubble = NULL;
static lv_obj_t * _menu = NULL;
static lv_obj_t *_currentPage = NULL;
static GuiMode_t _mode = GUI_BUBBLE;
// Menu navigation stack
#define MAX_MENU_STACK_SIZE 8
typedef struct {
lv_obj_t *pages[MAX_MENU_STACK_SIZE];
int count;
} menu_stack_t;
static menu_stack_t _menuStack = {0};
/* 1. Prepare a default (unfocused) style */
static lv_style_t _styleUnfocusedBtn;
/* 2. Prepare a focus style */
static lv_style_t _styleFocusedBtn;
static menu_context_t _menuContext;
// Bluetooth page state
static lv_obj_t* _bt_page = NULL;
static lv_obj_t* _bt_status_item = NULL; // Top status item (unselectable)
static lv_obj_t* _bt_device_container = NULL; // Container for device list items
static TickType_t _bt_scan_start_time = 0; // When scan was started
// Volume page state
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)
// Hide *all* headers/back buttons that LVGL may show for this menu
static void menu_hide_headers(lv_obj_t *menu) {
if (!menu) return;
// Main header (normal menus)
lv_obj_t *mh = lv_menu_get_main_header(menu);
if (mh) {
lv_obj_add_flag(mh, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(mh, 0);
}
lv_obj_t *mh_back = lv_menu_get_main_header_back_button(menu);
if (mh_back) {
lv_obj_add_flag(mh_back, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_width(mh_back, 0);
lv_obj_set_height(mh_back, 0);
}
// Sidebar header (if you ever use sidebar mode)
lv_obj_t *sh = lv_menu_get_sidebar_header(menu);
if (sh) {
lv_obj_add_flag(sh, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(sh, 0);
}
lv_obj_t *sh_back = lv_menu_get_sidebar_header_back_button(menu);
if (sh_back) {
lv_obj_add_flag(sh_back, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_width(sh_back, 0);
lv_obj_set_height(sh_back, 0);
}
}
static bool notify_lvgl_flush_ready(void *user_ctx) {
if (disp) {
lv_display_flush_ready(disp);
}
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_style_pad_all(main_page, 0, LV_PART_MAIN);
lv_obj_set_style_pad_row(main_page, 0, LV_PART_MAIN);
lv_obj_set_style_pad_column(main_page, 0, LV_PART_MAIN);
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();
// Clear the menu stack when starting fresh menu navigation
menu_stack_clear();
lv_menu_set_page(menu, main_page);
_currentPage = main_page;
// Initialize menu context to track the focused item
currentFocusIndex(&_menuContext);
menu_hide_headers(menu);
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;
_bt_page = NULL;
_bt_status_item = NULL;
_bt_device_container = NULL;
_bt_scan_start_time = 0;
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)
{
lvgl_port_lock(0);
// Create a screen with black background
lv_obj_t *scr = lv_scr_act();
createBubble(scr);
// Menu will be created lazily when needed
lvgl_port_unlock();
}
static void lcd_init(void)
{
ESP_LOGI(TAG, "Initialize SPI bus");
spi_bus_config_t buscfg = {
.sclk_io_num = PIN_NUM_CLK,
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = -1,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
//.max_transfer_sz = LCD_H_RES * LCD_V_RES * 2
.max_transfer_sz = LCD_H_RES * 25 * (LCD_BITS_PER_PIXEL/8),
};
ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO));
ESP_LOGI(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = PIN_NUM_DC,
.cs_gpio_num = PIN_NUM_CS,
.pclk_hz = LCD_PIXEL_CLOCK_HZ,
.lcd_cmd_bits = LCD_CMD_BITS,
.lcd_param_bits = LCD_PARAM_BITS,
.spi_mode = 3,
.trans_queue_depth = 10,
.user_ctx = NULL,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_SPI_HOST, &io_config, &io_handle));
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = PIN_NUM_RST,
.rgb_endian = LCD_RGB_ENDIAN_BGR,
.bits_per_pixel = LCD_BITS_PER_PIXEL,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true));
ESP_ERROR_CHECK(esp_lcd_panel_set_gap(panel_handle, 1, 26)); // ST7735S typically needs these offsets
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
}
static void lvgl_init(void)
{
lv_init();
#if 1
const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = 4, // LVGL task priority
.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
};
ESP_ERROR_CHECK(lvgl_port_init(&lvgl_cfg));
#endif
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = io_handle,
.panel_handle = panel_handle,
// .buffer_size = LCD_H_RES * LCD_V_RES * 2,
.buffer_size = LCD_H_RES * 25 * (LCD_BITS_PER_PIXEL/8), // was full screen
.double_buffer = false,
.hres = LCD_H_RES,
.vres = LCD_V_RES,
.monochrome = false,
.rotation = {
.swap_xy = true,
.mirror_x = true,
.mirror_y = false,
},
.flags = {
//.buff_dma = true,
.swap_bytes = true,
}
};
disp = lvgl_port_add_disp(&disp_cfg);
//lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
}
static void createBubble(lv_obj_t * scr)
{
// 2) Create a bubble level of size 200×60, with range [30°, +30°], initial 0°:
lv_obj_t * level = bubble_create(scr, 150, 40, -10.0f, +10.0f, 0.0f);
lv_obj_align(level, LV_ALIGN_CENTER, 0, 0);
// 3) … Later, when you read your accelerometer or keypadderived angle …
float new_angle = 10.0f;
bubble_setValue(level, new_angle);
// 4) You can call bubble_level_set_value(level, …) as often as you like.
// Each call invalidates the object and LVGL will call the draw callback
// (usually on the next tick, or immediately if LVGL is idle).
_bubble = level;
}
void gui_start(void)
{
// Initialize LCD
lcd_init();
// Initialize LVGL
lvgl_init();
// Create UI
create_lvgl_demo();
keypad_start();
gpio_set_level(PIN_NUM_BK_LIGHT, 1);
xTaskCreate(gui_task, "gui_task", 8192, NULL, 5, NULL);
}
static uint32_t waitForKeyPress(void)
{
QueueHandle_t q = keypad_getQueue();
uint32_t ev = 0;
if (xQueueReceive(q, &ev, portMAX_DELAY) == pdTRUE)
{
return ev;
}
return 0;
}
static void handleMainMenu(void)
{
lvgl_port_lock(0);
// Create a screen with black background
lv_obj_t *scr = lv_scr_act();
//create_menu(scr);
lvgl_port_unlock();
}
// ───── MENU CONFIG ─────
#define ROW_H 20 // height of each row
static const char * items[] = { // your menu entries
"VMode Crossmade",
"Second Choice",
"Another Option",
"Yet Another",
"Yet Another",
"Last One"
};
#define ITEM_COUNT (sizeof(items)/sizeof(items[0]))
// ───── STATE & HELPERS ─────
static lv_obj_t * btn_array[ITEM_COUNT];
static int selected_idx = 0;
// 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();
// Push current page onto stack before navigating
menu_stack_push(_currentPage);
show_bt_device_list();
UNLOCK();
} else if (strcmp(txt, "Volume") == 0) {
LOCK();
// Push current page onto stack before navigating
menu_stack_push(_currentPage);
show_volume_control();
UNLOCK();
} else if (strcmp(txt, "Back") == 0) {
LOCK();
menu_go_back();
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
static void refresh_highlight(void) {
return;
lvgl_port_lock(0);
int index = 0;
lv_obj_t *page = _currentPage;
lv_obj_t * child = NULL;
lv_obj_t * next = lv_obj_get_child(page, index);
lv_obj_t * selected = NULL;
while(next) {
child = next;
ESP_LOGI(TAG, "Child: %p", child);
if (lv_obj_has_flag(child, LV_OBJ_FLAG_USER_1))
{
lv_obj_set_style_bg_color(child, lv_color_hex(0xFF8800), 0);
lv_obj_set_style_text_color(lv_obj_get_child(child,0),
lv_color_white(), 0);
selected = child;
ESP_LOGI(TAG, "Selected");
}
else
{
lv_obj_set_style_bg_color(child, lv_color_white(), 0);
lv_obj_set_style_text_color(lv_obj_get_child(child,0),
lv_color_black(), 0);
}
index++;
next = lv_obj_get_child(page, index);
}
if (selected)
{
lv_obj_scroll_to_view(selected, LV_ANIM_ON);
}
lvgl_port_unlock();
}
static void menuInc(int inc)
{
LOCK();
currentFocusIndex(&_menuContext);
ESP_LOGI(TAG, "Current Index: %d", _menuContext.selected);
// Safety check: if no focused object found, bail out
if (_menuContext.selected < 0 || _menuContext.obj == NULL) {
ESP_LOGW(TAG, "No focused object found, cannot navigate");
UNLOCK();
return;
}
lv_obj_t *next = NULL;
// check if we are at the first or last in the page
lv_obj_t *test = lv_obj_get_child(_currentPage, (inc > 0 ? -1 : 0));
if (_menuContext.obj != test)
{
next = lv_obj_get_child(_currentPage, _menuContext.selected + inc);
if (next) {
lv_obj_add_state(next, LV_STATE_FOCUSED);
lv_obj_clear_state(_menuContext.obj, LV_STATE_FOCUSED);
lv_obj_scroll_to_view(next, LV_ANIM_ON);
_menuContext.obj = next;
_menuContext.selected += inc;
}
}
UNLOCK();
}
static void menuNext(void)
{
menuInc(1);
}
static void menuPrevious(void)
{
menuInc(-1);
}
// Fire the “clicked” event on the selected row
static void activate_selected(void)
{
LOCK();
lv_obj_send_event(_menuContext.obj, LV_EVENT_CLICKED, NULL);
UNLOCK();
}
// Event handler to control label scrolling based on focus state
static void label_scroll_focus_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * btn = lv_event_get_target(e);
lv_obj_t * lbl = lv_obj_get_child(btn, 0); // Get the label child
if (!lbl) return;
if (code == LV_EVENT_FOCUSED) {
// Start scrolling when focused
lv_label_set_long_mode(lbl, LV_LABEL_LONG_SCROLL_CIRCULAR);
}
else if (code == LV_EVENT_DEFOCUSED) {
// Stop scrolling and reset position when defocused
lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP);
}
}
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);
lv_obj_add_state(btn, LV_STATE_DEFAULT);
// style it just like your old list
// lv_obj_set_style_bg_color(btn, lv_color_white(), 0);
lv_obj_set_style_radius(btn, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN);
lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
// 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_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP); // Start with clipping, scrolling only when focused
lv_obj_set_width(lbl, LV_PCT(95)); // Set width to allow scrolling
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);
lv_obj_set_style_anim_time(lbl, 3000, LV_PART_MAIN); // Set scrolling duration (3 seconds per cycle)
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0); // Center vertically, align left horizontally
// Add focus event handler to control scrolling
lv_obj_add_event_cb(btn, label_scroll_focus_cb, LV_EVENT_FOCUSED, NULL);
lv_obj_add_event_cb(btn, label_scroll_focus_cb, LV_EVENT_DEFOCUSED, NULL);
// click callback
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL);
return btn;
}
static int getSelectedIndex(lv_obj_t *page)
{
return -1;
}
static void currentFocusIndex(menu_context_t *ctx)
{
ctx->count = lv_obj_get_child_cnt(_currentPage);
ctx->obj = NULL;
ctx->selected = -1;
// return the index of the currently focused object
for(int i = 0; i < ctx->count; i++) {
lv_obj_t * child = lv_obj_get_child(_currentPage, i);
if (lv_obj_has_state(child, LV_STATE_FOCUSED))
{
ctx->obj = child;
ctx->selected = i;
return;
}
}
}
// ───── 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()); // black text
//lv_style_set_text_font(&_styleUnfocusedBtn, &lv_font_unscii_16); // larger font
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0x33,0x99,0xFF)); // blue bg
lv_style_set_text_color(&_styleFocusedBtn, lv_color_white()); // white text
//lv_style_set_text_font(&_styleFocusedBtn, &lv_font_unscii_16); // larger font
styles_initialized = true;
}
}
static lv_obj_t* create_menu_container(void) {
if (_menu == NULL) {
_menu = lv_menu_create(lv_scr_act());
menu_hide_headers(_menu);
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
// Hide the sidebar/header
lv_menu_set_sidebar_page(_menu, NULL);
lv_obj_set_style_pad_all(_menu, 0, LV_PART_MAIN);
// Disable the back button in header
lv_menu_set_mode_root_back_button(_menu, LV_MENU_ROOT_BACK_BUTTON_DISABLED);
// Find and hide the header container
// The menu has 2 children: child 0 is header, child 1 is content area
uint32_t menu_child_count = lv_obj_get_child_count(_menu);
ESP_LOGI(TAG, "Menu container has %d children", (int)menu_child_count);
if (menu_child_count >= 1) {
lv_obj_t* header = lv_obj_get_child(_menu, 0);
int32_t h = lv_obj_get_height(header);
int32_t w = lv_obj_get_width(header);
ESP_LOGI(TAG, " Header (child 0): %p, size=%dx%d", header, (int)w, (int)h);
// Hide the header completely
lv_obj_add_flag(header, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(header, 0);
ESP_LOGI(TAG, " Hidden header");
}
}
return _menu;
}
// ───── BLUETOOTH DEVICE LIST PAGE ─────
// Update the status item text (e.g., "Scanning...", "No devices found")
static void update_bt_status(const char* status_text) {
if (_bt_status_item) {
lv_obj_t* label = lv_obj_get_child(_bt_status_item, 0);
if (label) {
ESP_LOGI(TAG, "Updating BT status to: %s", status_text);
lv_label_set_text(label, status_text);
lv_obj_invalidate(label); // Force redraw
} else {
ESP_LOGW(TAG, "Status item has no label child");
}
} else {
ESP_LOGW(TAG, "Status item is NULL, cannot update");
}
}
// Clear all device items and action buttons from the container (keep status and Back only)
static void clear_bt_device_list(void) {
if (!_bt_device_container) return;
// Delete all children except status (index 0) and Back (last item)
uint32_t child_count = lv_obj_get_child_count(_bt_device_container);
ESP_LOGI(TAG, "Clearing BT device list, child_count=%d", (int)child_count);
// Work backwards to avoid index shifting issues
// Keep: status (0) and Back (last)
for (int i = child_count - 2; i > 0; i--) {
lv_obj_t* child = lv_obj_get_child(_bt_device_container, i);
if (child && child != _bt_status_item) {
ESP_LOGI(TAG, " Deleting child at index %d", i);
lv_obj_del(child);
}
}
ESP_LOGI(TAG, "Clear complete, remaining children: %d", (int)lv_obj_get_child_count(_bt_device_container));
}
// Populate the device list with current devices and add action buttons
static void populate_bt_device_list(void) {
if (!_bt_device_container) return;
// Clear old device list items first (keeps status and Back button)
clear_bt_device_list();
bt_device_list_t* device_list = bt_get_device_list();
if (!device_list) {
update_bt_status("Error getting devices");
return;
}
ESP_LOGI(TAG, "Populating BT list with %d devices", device_list->count);
// The page has: [status, Back]
// We'll add: [status, ...devices..., Refresh, Clear?, Back]
uint32_t insert_index = 1; // After status item
bool has_paired_devices = false;
if (device_list->count == 0) {
update_bt_status("No devices found");
// Still add Refresh button so user can try again
} else {
update_bt_status("Available Devices:");
// Add device items (limit to MAX_BT_DEVICES to avoid memory issues)
bool first = true;
int max_items = (device_list->count < MAX_BT_DEVICES) ? device_list->count : MAX_BT_DEVICES;
for (int i = 0; i < max_items; i++) {
char device_text[64];
const char* paired_prefix = device_list->devices[i].is_paired ? "* " : "";
snprintf(device_text, sizeof(device_text), "%s%s",
paired_prefix,
device_list->devices[i].name);
if (device_list->devices[i].is_paired) {
has_paired_devices = true;
}
lv_obj_t* btn = addMenuItem(_bt_device_container, device_text);
lv_obj_add_event_cb(btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
// Move to correct position (after status, before Back button)
lv_obj_move_to_index(btn, insert_index++);
if (first) {
lv_obj_add_state(btn, LV_STATE_FOCUSED);
first = false;
}
}
}
// Clear focus from all items first
uint32_t child_count = lv_obj_get_child_count(_bt_device_container);
for (uint32_t i = 0; i < child_count; i++) {
lv_obj_t* child = lv_obj_get_child(_bt_device_container, i);
if (child) {
lv_obj_clear_state(child, LV_STATE_FOCUSED);
}
}
// Always add Refresh button after devices (but before Back)
lv_obj_t* refresh_btn = addMenuItem(_bt_device_container, "Refresh");
lv_obj_add_event_cb(refresh_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
lv_obj_move_to_index(refresh_btn, insert_index++);
// Only add Clear Paired button if there are actually paired devices
if (has_paired_devices) {
lv_obj_t* clear_btn = addMenuItem(_bt_device_container, "Clear Paired");
lv_obj_add_event_cb(clear_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
lv_obj_move_to_index(clear_btn, insert_index++);
}
// Set focus on appropriate item
if (device_list->count == 0) {
// No devices found - focus on Refresh
lv_obj_add_state(refresh_btn, LV_STATE_FOCUSED);
} else {
// Devices found - focus on first device (already done above, but devices list was cleared)
lv_obj_t* first_device = lv_obj_get_child(_bt_device_container, 1); // After status
if (first_device) {
lv_obj_add_state(first_device, LV_STATE_FOCUSED);
}
}
// Update menu context to track the newly focused item
currentFocusIndex(&_menuContext);
}
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();
menu_go_back();
UNLOCK();
return;
} else if (strcmp(txt, "Refresh") == 0) {
LOCK();
// Update status first
update_bt_status("Scanning...");
_bt_scan_start_time = xTaskGetTickCount();
// Don't delete the current button while we're in its event handler!
// Instead, delete only the device items, keeping Refresh and Back buttons
uint32_t child_count = lv_obj_get_child_count(_bt_device_container);
ESP_LOGI(TAG, "Refresh clicked, clearing device items (not buttons), child_count=%d", (int)child_count);
// Delete children between status (0) and the last items
// Work backwards to avoid index shifting
// Keep: status (0), Refresh, Back - delete everything else (devices, Clear Paired)
for (int i = child_count - 2; i > 0; i--) {
lv_obj_t* child = lv_obj_get_child(_bt_device_container, i);
if (!child || child == _bt_status_item) {
continue;
}
// Get the button's text to decide if we should delete it
const char* child_text = NULL;
lv_obj_t* child_label = lv_obj_get_child(child, 0);
if (child_label) {
child_text = lv_label_get_text(child_label);
}
// Only keep Refresh and Back buttons
if (child_text) {
if (strcmp(child_text, "Refresh") != 0 && strcmp(child_text, "Back") != 0) {
ESP_LOGI(TAG, " Deleting item: %s", child_text);
lv_obj_del(child);
} else {
ESP_LOGI(TAG, " Keeping button: %s", child_text);
}
}
}
// Clear all focus states and set focus on Back button
child_count = lv_obj_get_child_count(_bt_device_container);
for (uint32_t i = 0; i < child_count; i++) {
lv_obj_t* child = lv_obj_get_child(_bt_device_container, i);
if (child) {
lv_obj_clear_state(child, LV_STATE_FOCUSED);
}
}
// Focus on Back button (last child)
lv_obj_t* back_btn = lv_obj_get_child(_bt_device_container, -1);
if (back_btn) {
lv_obj_add_state(back_btn, LV_STATE_FOCUSED);
}
// Update menu context to track the newly focused item
currentFocusIndex(&_menuContext);
bt_start_discovery();
UNLOCK();
return;
} else if (strcmp(txt, "Clear Paired") == 0) {
LOCK();
// Disconnect from any connected device first
bt_disconnect_current_device();
// Clear all paired devices from NVS and device list
esp_err_t ret = system_clearAllPairedDevices();
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Successfully cleared all paired devices from NVS");
// Also clear the in-memory device list used by GUI
bt_clear_all_devices();
// Immediately update GUI to show empty list
clear_bt_device_list();
update_bt_status("No devices found");
// Add back the Refresh button
lv_obj_t* refresh_btn = addMenuItem(_bt_device_container, "Refresh");
lv_obj_add_event_cb(refresh_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
lv_obj_move_to_index(refresh_btn, 1); // After status, before Back
// Focus on Refresh button
uint32_t child_count = lv_obj_get_child_count(_bt_device_container);
for (uint32_t i = 0; i < child_count; i++) {
lv_obj_t* child = lv_obj_get_child(_bt_device_container, i);
if (child) {
lv_obj_clear_state(child, LV_STATE_FOCUSED);
}
}
lv_obj_add_state(refresh_btn, LV_STATE_FOCUSED);
// Update menu context to track the newly focused item
currentFocusIndex(&_menuContext);
} else {
ESP_LOGE(TAG, "Failed to clear paired devices: %s", esp_err_to_name(ret));
}
UNLOCK();
return;
}
// Find which device was clicked by matching the device name
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;
}
// Extract device name from button text (remove "* " prefix if present)
char device_name[64];
const char* name_start = txt;
// Skip "* " prefix if present
if (txt[0] == '*' && txt[1] == ' ') {
name_start = txt + 2;
}
strncpy(device_name, name_start, sizeof(device_name) - 1);
device_name[sizeof(device_name) - 1] = '\0';
// Find the device index by name
for (int i = 0; i < device_list->count; i++) {
if (strcmp(device_list->devices[i].name, device_name) == 0) {
ESP_LOGI(TAG, "Requesting connection to device %d: %s", i, device_name);
system_requestBtConnect(i);
// Return to bubble mode after selection
_mode = GUI_BUBBLE;
show_bubble();
return;
}
}
ESP_LOGW(TAG, "Device not found in list: %s", device_name);
}
static lv_obj_t* create_bt_device_page(bool start_discovery) {
ESP_LOGI(TAG, "Creating Bluetooth device page (start_discovery=%d)", start_discovery);
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container");
return NULL;
}
// Only create the page structure once, or recreate if it was deleted
if (_bt_page == NULL) {
ESP_LOGI(TAG, "Creating new Bluetooth page structure");
_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_style_pad_all(_bt_page, 0, LV_PART_MAIN);
lv_obj_set_style_pad_row(_bt_page, 0, LV_PART_MAIN);
lv_obj_set_style_pad_column(_bt_page, 0, LV_PART_MAIN);
lv_obj_set_scrollbar_mode(_bt_page, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_size(_bt_page, lv_pct(100), lv_pct(100));
// Use the page itself as the container
_bt_device_container = _bt_page;
// Create status item (unselectable, shows scan state)
_bt_status_item = addMenuItem(_bt_device_container, "Initializing...");
lv_obj_add_state(_bt_status_item, LV_STATE_DISABLED);
lv_obj_clear_state(_bt_status_item, LV_STATE_FOCUSED);
// Create Back button (always visible)
lv_obj_t* back_btn = addMenuItem(_bt_device_container, "Back");
lv_obj_add_event_cb(back_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
// Set initial focus on Back button (will be moved to first device when they appear)
lv_obj_add_state(back_btn, LV_STATE_FOCUSED);
}
// Get device list first to decide if we should scan
bt_device_list_t* device_list = bt_get_device_list();
if (!device_list) {
ESP_LOGE(TAG, "Failed to get device list");
update_bt_status("Error");
return _bt_page;
}
// Only auto-start discovery if requested AND there are no paired devices
bool discovery_started = false;
if (start_discovery && device_list->count == 0) {
ESP_LOGI(TAG, "Starting Bluetooth discovery (no paired devices)");
update_bt_status("Scanning...");
_bt_scan_start_time = xTaskGetTickCount(); // Record when scan started
discovery_started = bt_start_discovery();
if (!discovery_started) {
ESP_LOGW(TAG, "Discovery not started");
update_bt_status("Scan failed");
_bt_scan_start_time = 0;
}
}
// Populate with current device list
if (device_list->count > 0) {
// Clear focus from Back button, will be set on first device
lv_obj_t* back_btn = lv_obj_get_child(_bt_device_container, 1); // Index 1 is Back (after status)
if (back_btn) {
lv_obj_clear_state(back_btn, LV_STATE_FOCUSED);
}
populate_bt_device_list();
} else if (!discovery_started) {
update_bt_status("No devices found");
// Update menu context since Back button has focus
currentFocusIndex(&_menuContext);
} else {
// Scanning in progress, Back button has focus
currentFocusIndex(&_menuContext);
}
return _bt_page;
}
static void show_bt_device_list(void) {
ESP_LOGI(TAG, "Showing Bluetooth device list with discovery");
// Start discovery when explicitly showing the BT device list
bool should_start_discovery = true;
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(should_start_discovery);
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
// Initialize menu context for this page
currentFocusIndex(&_menuContext);
menu_hide_headers(menu);
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");
}
static void refresh_bt_device_list(void) {
ESP_LOGI(TAG, "Refreshing Bluetooth device list with discovered devices");
// Clear old devices and repopulate with discovered devices
clear_bt_device_list();
populate_bt_device_list();
ESP_LOGI(TAG, "Bluetooth device list refreshed");
}
// ───── 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));
// Hide header
uint32_t child_count = lv_obj_get_child_count(_volume_page);
if (child_count >= 1) {
lv_obj_t* header = lv_obj_get_child(_volume_page, 0);
lv_obj_add_flag(header, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(header, 0);
}
// 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_set_style_text_color(title, lv_color_black(), 0);
//lv_obj_set_style_text_font(title, &lv_font_montserrat_8, 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);
#if 0
// 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);
#endif
// Add Back button
lv_obj_t* back_btn = addMenuItem(_volume_page, "Back");
lv_obj_add_event_cb(back_btn, btn_click_cb, LV_EVENT_CLICKED, NULL);
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
menu_hide_headers(menu);
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
ESP_LOGI(TAG, "Volume control displayed");
}
// Menu stack management functions
static void menu_stack_push(lv_obj_t *page) {
if (_menuStack.count < MAX_MENU_STACK_SIZE && page != NULL) {
_menuStack.pages[_menuStack.count++] = page;
ESP_LOGI(TAG, "Menu stack push: page=%p, count=%d", page, _menuStack.count);
} else if (_menuStack.count >= MAX_MENU_STACK_SIZE) {
ESP_LOGW(TAG, "Menu stack overflow, cannot push more pages");
}
}
static lv_obj_t* menu_stack_pop(void) {
if (_menuStack.count > 0) {
lv_obj_t *page = _menuStack.pages[--_menuStack.count];
ESP_LOGI(TAG, "Menu stack pop: page=%p, count=%d", page, _menuStack.count);
return page;
}
ESP_LOGI(TAG, "Menu stack is empty, cannot pop");
return NULL;
}
static void menu_stack_clear(void) {
_menuStack.count = 0;
ESP_LOGI(TAG, "Menu stack cleared");
}
static bool menu_stack_is_empty(void) {
return (_menuStack.count == 0);
}
static void menu_go_back(void) {
ESP_LOGI(TAG, "Menu go back requested");
if (menu_stack_is_empty()) {
ESP_LOGI(TAG, "Menu stack empty, returning to bubble mode");
_mode = GUI_BUBBLE;
show_bubble();
return;
}
lv_obj_t *previous_page = menu_stack_pop();
if (previous_page != NULL) {
ESP_LOGI(TAG, "Returning to previous menu page");
lv_obj_t* menu = create_menu_container();
if (menu) {
lv_menu_set_page(menu, previous_page);
_currentPage = previous_page;
_mode = GUI_MENU;
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
}
} else {
ESP_LOGI(TAG, "No previous page found, returning to bubble mode");
_mode = GUI_BUBBLE;
show_bubble();
}
}
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) {
lv_style_init(&_styleFocusedBtn);
lv_style_init(&_styleUnfocusedBtn);
lv_style_set_bg_color(&_styleUnfocusedBtn, lv_color_make(0xff,0xff,0xff)); // gray bg
lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_hex(0xFF8800));
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0x33,0x99,0xFF)); // blue bg
lv_style_set_text_color(&_styleUnfocusedBtn,lv_color_black());
// 2) Inside it, create the lv_menu and hide its sidebar/header
lv_obj_t *menu = lv_menu_create(lv_scr_act());
_menu = menu;
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_t * main = lv_menu_page_create(menu, NULL);
lv_obj_set_style_radius(main, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(main, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_scrollbar_mode(main, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_size(main, lv_pct(100), lv_pct(100));
// Hide header
uint32_t child_count = lv_obj_get_child_count(main);
if (child_count >= 1) {
lv_obj_t* header = lv_obj_get_child(main, 0);
lv_obj_add_flag(header, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(header, 0);
}
lv_menu_set_page(menu, main);
lv_obj_t * tmpObj;
lv_obj_t * calMenu = lv_menu_page_create(menu, NULL);
lv_obj_set_style_radius(calMenu, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(calMenu, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_scrollbar_mode(calMenu, LV_SCROLLBAR_MODE_AUTO);
// Hide header
child_count = lv_obj_get_child_count(calMenu);
if (child_count >= 1) {
lv_obj_t* header = lv_obj_get_child(calMenu, 0);
lv_obj_add_flag(header, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(header, 0);
}
tmpObj = addMenuItem(main, "Bluetooth");
lv_obj_add_state(tmpObj, LV_STATE_FOCUSED);
//lv_obj_add_flag(tmpObj, LV_OBJ_FLAG_USER_1);
tmpObj = addMenuItem(main, "Calibration");
lv_menu_set_load_page_event(menu, tmpObj, calMenu);
tmpObj = addMenuItem(main, "Volume");
addMenuItem(main, "About");
addMenuItem(main, "Exit");
addMenuItem(calMenu, "Calibrate Level");
addMenuItem(calMenu, "Reset Calibration");
addMenuItem(calMenu, "Exit");
_currentPage = main;
// 6) Initial highlight
selected_idx = 0;
refresh_highlight();
}
static void gui_task(void *pvParameters)
{
(void)pvParameters; // Unused parameter
system_subscribe(xTaskGetCurrentTaskHandle());
// Grab queue handle
QueueHandle_t q = keypad_getQueue();
uint32_t ev = 0;
LOCK();
_mode = GUI_BUBBLE;
lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
UNLOCK();
ESP_LOGI(TAG, "Start GUI Task...");
while (1)
{
if (xQueueReceive(q, &ev, pdMS_TO_TICKS(10)) == pdTRUE)
{
switch (ev) {
#if 0
case (KEY_UP << KEY_LONG_PRESS):
{
system_setZeroAngle();
break;
}
case (KEY_DOWN << KEY_LONG_PRESS):
{
system_clearZeroAngle();
break;
}
#endif
case (KEY_UP << KEY_SHORT_PRESS):
if (_mode == GUI_MENU)
{
// 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 {
menuPrevious();
}
}
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 {
menuNext();
}
}
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;
show_menu(); // Use lazy loading
}
else if (_mode == GUI_MENU)
{
activate_selected();
// After activation, check if we should stay in menu or exit
// (activation may have changed _currentPage or _mode)
if (_mode == GUI_BUBBLE) {
// Already handled by the menu item (e.g., "Exit" was selected)
} else if (_currentPage == _bt_page || _currentPage == _volume_page) {
// Stay in menu mode on these pages
}
// Note: For main menu items that navigate to subpages,
// the btn_click_cb handles the navigation and keeps _mode = GUI_MENU
}
UNLOCK();
ESP_LOGI(TAG, "MAIN: Button 0 LONG - Exit");
break;
}
case (KEY1 << KEY_LONG_PRESS):
{
LOCK();
if (_mode == GUI_MENU)
{
menu_go_back(); // Use menu stack navigation
}
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;
}
default:
break;
}
}
// Check for system events
uint32_t notifiedBits = 0;
xTaskNotifyWait(
0, // don't clear on entry
0xFFFFFFFF, // clear bits on exit
&notifiedBits,
pdMS_TO_TICKS(10));
if (notifiedBits & EM_EVENT_BT_DISCOVERY_COMPLETE) {
// Discovery completed - refresh the BT page if we're on it and scan has been running long enough
// Ignore spurious early "complete" events from stopping old discovery (must be > 500ms since start)
if (_mode == GUI_MENU && _currentPage == _bt_page && _bt_scan_start_time > 0) {
TickType_t elapsed = xTaskGetTickCount() - _bt_scan_start_time;
if (elapsed > pdMS_TO_TICKS(500)) {
ESP_LOGI(TAG, "Discovery complete after %dms, refreshing Bluetooth page", (int)pdTICKS_TO_MS(elapsed));
LOCK();
_bt_scan_start_time = 0; // Clear scan timestamp
// Recreate the BT page with updated device list (without starting new discovery)
refresh_bt_device_list();
UNLOCK();
} else {
ESP_LOGI(TAG, "Ignoring spurious discovery complete event (only %dms elapsed)", (int)pdTICKS_TO_MS(elapsed));
}
}
}
if (_mode == GUI_BUBBLE)
{
if (notifiedBits & EM_EVENT_NEW_DATA)
{
ImuData_t d = system_getImuData();
bubble_setValue(_bubble, -d.angle);
}
}
}
}