- Add calibration submenu with three options: Cancel (default), Calibrate, and Clear - Cancel returns to main menu to prevent accidental calibration - Calibrate runs system_setZeroAngle() and displays "Calibrated" confirmation - Clear runs system_clearZeroAngle() and displays "Cleared" confirmation - Use LVGL timer for non-blocking 1-second confirmation messages - Center confirmation messages both horizontally and vertically - Use larger font (montserrat_16) for better readability - Remove "Back" button from volume control page - Center volume control elements vertically on screen - Improve overall menu navigation and visual consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1781 lines
57 KiB
C
1781 lines
57 KiB
C
#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);
|
||
static void show_calibration_menu(void);
|
||
static lv_obj_t* create_calibration_page(void);
|
||
static void calibration_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)
|
||
|
||
// Calibration page state
|
||
static lv_obj_t* _calibration_page = NULL;
|
||
static lv_timer_t* _calibration_timer = NULL;
|
||
|
||
// 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;
|
||
_calibration_page = NULL;
|
||
_volume_page = NULL;
|
||
_volume_bar = NULL;
|
||
_volume_label = NULL;
|
||
if (_calibration_timer) {
|
||
lv_timer_del(_calibration_timer);
|
||
_calibration_timer = 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)
|
||
{
|
||
|
||
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 keypad‐derived 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, "Calibration") == 0) {
|
||
LOCK();
|
||
// Push current page onto stack before navigating
|
||
menu_stack_push(_currentPage);
|
||
show_calibration_menu();
|
||
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)
|
||
{
|
||
int search_index = _menuContext.selected + inc;
|
||
next = lv_obj_get_child(_currentPage, search_index);
|
||
|
||
// Skip disabled/non-clickable items
|
||
while (next && (!lv_obj_has_flag(next, LV_OBJ_FLAG_CLICKABLE) || lv_obj_has_state(next, LV_STATE_DISABLED))) {
|
||
search_index += inc;
|
||
next = lv_obj_get_child(_currentPage, search_index);
|
||
}
|
||
|
||
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 = search_index;
|
||
}
|
||
}
|
||
|
||
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) {
|
||
ESP_LOGI(TAG, "Updating BT status to: %s", status_text);
|
||
lv_label_set_text(_bt_status_item, status_text);
|
||
lv_obj_invalidate(_bt_status_item); // Force redraw
|
||
} else {
|
||
ESP_LOGW(TAG, "Status item is NULL, cannot update");
|
||
}
|
||
}
|
||
|
||
// Clear all device items and action buttons from the container (keep Back only)
|
||
static void clear_bt_device_list(void) {
|
||
if (!_bt_device_container) return;
|
||
|
||
// Delete all children except 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: 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) {
|
||
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
|
||
// is_scanning: true if currently scanning (show Cancel instead of Back, hide Refresh)
|
||
static void populate_bt_device_list(bool is_scanning) {
|
||
if (!_bt_device_container) return;
|
||
|
||
// Clear old device list items first (keeps 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 (is_scanning=%d)", device_list->count, is_scanning);
|
||
|
||
// The container has: [Back/Cancel]
|
||
// We'll add: [...devices..., Refresh?, Clear?, Back/Cancel]
|
||
uint32_t insert_index = 0; // Start at beginning
|
||
bool has_paired_devices = false;
|
||
|
||
if (device_list->count == 0 && !is_scanning) {
|
||
update_bt_status("No devices found");
|
||
} else if (!is_scanning) {
|
||
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 (before action buttons)
|
||
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);
|
||
}
|
||
}
|
||
|
||
lv_obj_t* focus_target = NULL;
|
||
|
||
// Only add Refresh button if NOT scanning
|
||
if (!is_scanning) {
|
||
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++);
|
||
|
||
if (device_list->count == 0) {
|
||
focus_target = refresh_btn; // Focus on Refresh if no devices
|
||
}
|
||
}
|
||
|
||
// Only add Clear Paired button if there are actually paired devices and NOT scanning
|
||
if (has_paired_devices && !is_scanning) {
|
||
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 (!focus_target) {
|
||
if (device_list->count > 0) {
|
||
// Devices found - focus on first device
|
||
focus_target = lv_obj_get_child(_bt_device_container, 0);
|
||
} else {
|
||
// Scanning with no devices yet, or no devices and no refresh - focus on Back/Cancel
|
||
focus_target = lv_obj_get_child(_bt_device_container, -1); // Last item (Back/Cancel)
|
||
}
|
||
}
|
||
|
||
if (focus_target) {
|
||
lv_obj_add_state(focus_target, 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 || strcmp(txt, "Cancel") == 0) {
|
||
LOCK();
|
||
bt_stop_discovery();
|
||
if (strcmp(txt, "Cancel") == 0) {
|
||
// Cancelled scanning - repopulate with current devices (not scanning)
|
||
populate_bt_device_list(false);
|
||
} else {
|
||
// Back button - go back to previous menu
|
||
menu_go_back();
|
||
}
|
||
UNLOCK();
|
||
return;
|
||
} else if (strcmp(txt, "Refresh") == 0) {
|
||
LOCK();
|
||
// Update status first
|
||
update_bt_status("Scanning...");
|
||
_bt_scan_start_time = xTaskGetTickCount();
|
||
|
||
// Change Back button to Cancel and hide Refresh
|
||
// Find and update the Back button text
|
||
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_t* child_label = lv_obj_get_child(child, 0);
|
||
if (child_label) {
|
||
const char* child_text = lv_label_get_text(child_label);
|
||
if (child_text && strcmp(child_text, "Back") == 0) {
|
||
lv_label_set_text(child_label, "Cancel");
|
||
ESP_LOGI(TAG, "Changed Back button to Cancel");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Repopulate the list in scanning mode (hides Refresh, shows Cancel)
|
||
populate_bt_device_list(true);
|
||
|
||
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();
|
||
|
||
// Repopulate with empty list (not scanning)
|
||
populate_bt_device_list(false);
|
||
} 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_OFF);
|
||
lv_obj_set_size(_bt_page, lv_pct(100), lv_pct(100));
|
||
lv_obj_set_flex_flow(_bt_page, LV_FLEX_FLOW_COLUMN);
|
||
|
||
// Create fixed status label (header, non-scrolling)
|
||
_bt_status_item = lv_label_create(_bt_page);
|
||
lv_label_set_text(_bt_status_item, "Devices:");
|
||
lv_obj_set_width(_bt_status_item, lv_pct(100));
|
||
lv_obj_set_height(_bt_status_item, 16); // Thinner header
|
||
lv_obj_set_style_bg_color(_bt_status_item, lv_color_make(0xE0, 0xE0, 0xE0), 0);
|
||
lv_obj_set_style_bg_opa(_bt_status_item, LV_OPA_COVER, 0);
|
||
lv_obj_set_style_text_color(_bt_status_item, lv_color_black(), 0);
|
||
lv_obj_set_style_pad_left(_bt_status_item, 5, 0);
|
||
lv_obj_set_style_pad_top(_bt_status_item, 2, 0);
|
||
lv_obj_set_style_text_align(_bt_status_item, LV_TEXT_ALIGN_LEFT, 0);
|
||
lv_obj_set_style_radius(_bt_status_item, 0, LV_PART_MAIN);
|
||
lv_obj_clear_flag(_bt_status_item, LV_OBJ_FLAG_SCROLLABLE);
|
||
|
||
// Create scrollable container for menu items (below header)
|
||
_bt_device_container = lv_obj_create(_bt_page);
|
||
lv_obj_set_width(_bt_device_container, lv_pct(100));
|
||
lv_obj_set_flex_grow(_bt_device_container, 1); // Take remaining space
|
||
lv_obj_set_style_radius(_bt_device_container, 0, LV_PART_MAIN);
|
||
lv_obj_set_style_radius(_bt_device_container, 0, LV_STATE_DEFAULT);
|
||
lv_obj_set_style_radius(_bt_device_container, 0, 0);
|
||
lv_obj_set_style_border_width(_bt_device_container, 0, LV_PART_MAIN);
|
||
lv_obj_set_style_outline_width(_bt_device_container, 0, LV_PART_MAIN);
|
||
lv_obj_set_style_pad_all(_bt_device_container, 0, LV_PART_MAIN);
|
||
lv_obj_set_style_pad_row(_bt_device_container, 0, LV_PART_MAIN);
|
||
lv_obj_set_style_bg_color(_bt_device_container, lv_color_white(), LV_PART_MAIN);
|
||
lv_obj_set_style_bg_opa(_bt_device_container, LV_OPA_COVER, LV_PART_MAIN);
|
||
lv_obj_set_flex_flow(_bt_device_container, LV_FLEX_FLOW_COLUMN);
|
||
lv_obj_set_scrollbar_mode(_bt_device_container, LV_SCROLLBAR_MODE_AUTO);
|
||
|
||
// Create Back button (always visible, will change to Cancel during scanning)
|
||
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 (pass scanning state)
|
||
if (discovery_started) {
|
||
// Change Back to Cancel
|
||
lv_obj_t* back_btn = lv_obj_get_child(_bt_device_container, 0);
|
||
if (back_btn) {
|
||
lv_obj_t* label = lv_obj_get_child(back_btn, 0);
|
||
if (label) {
|
||
lv_label_set_text(label, "Cancel");
|
||
}
|
||
}
|
||
populate_bt_device_list(true); // Scanning mode
|
||
} else {
|
||
populate_bt_device_list(false); // Not scanning
|
||
}
|
||
|
||
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);
|
||
// Store the page in _bt_page for later reference, but use container for navigation
|
||
_currentPage = _bt_device_container; // Point to the scrollable container for navigation
|
||
_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");
|
||
|
||
// Change Cancel back to Back
|
||
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_t* child_label = lv_obj_get_child(child, 0);
|
||
if (child_label) {
|
||
const char* child_text = lv_label_get_text(child_label);
|
||
if (child_text && strcmp(child_text, "Cancel") == 0) {
|
||
lv_label_set_text(child_label, "Back");
|
||
ESP_LOGI(TAG, "Changed Cancel button back to Back");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Repopulate with discovered devices (not scanning anymore)
|
||
populate_bt_device_list(false);
|
||
|
||
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));
|
||
lv_obj_set_style_pad_all(_volume_page, 0, LV_PART_MAIN);
|
||
|
||
// 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 a container to center everything vertically
|
||
lv_obj_t* container = lv_obj_create(_volume_page);
|
||
lv_obj_set_size(container, lv_pct(100), LV_SIZE_CONTENT);
|
||
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0);
|
||
lv_obj_set_style_border_width(container, 0, 0);
|
||
lv_obj_set_style_pad_all(container, 0, 0);
|
||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);
|
||
lv_obj_set_flex_align(container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||
lv_obj_center(container);
|
||
|
||
// Create title label
|
||
lv_obj_t* title = lv_label_create(container);
|
||
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_pad_bottom(title, 8, 0);
|
||
|
||
// Create volume bar (progress bar)
|
||
_volume_bar = lv_bar_create(container);
|
||
lv_obj_set_size(_volume_bar, 120, 20);
|
||
lv_bar_set_range(_volume_bar, 0, 100);
|
||
lv_bar_set_value(_volume_bar, _current_volume, LV_ANIM_OFF);
|
||
lv_obj_set_style_pad_top(_volume_bar, 5, 0);
|
||
lv_obj_set_style_pad_bottom(_volume_bar, 5, 0);
|
||
|
||
// Create volume percentage label
|
||
_volume_label = lv_label_create(container);
|
||
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_set_style_pad_top(_volume_label, 5, 0);
|
||
|
||
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) {
|
||
// Stop any ongoing Bluetooth discovery when leaving BT page
|
||
if (_currentPage == _bt_device_container) {
|
||
bt_stop_discovery();
|
||
}
|
||
|
||
lv_menu_set_page(menu, previous_page);
|
||
_currentPage = previous_page; // For main menu, page and currentPage are the same
|
||
_mode = GUI_MENU;
|
||
|
||
// Re-initialize menu context for the restored page
|
||
currentFocusIndex(&_menuContext);
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// ───── CALIBRATION PAGE ─────
|
||
static void calibration_timer_cb(lv_timer_t * timer) {
|
||
// Return to bubble mode after showing message
|
||
LOCK();
|
||
_mode = GUI_BUBBLE;
|
||
show_bubble();
|
||
if (_calibration_timer) {
|
||
lv_timer_del(_calibration_timer);
|
||
_calibration_timer = NULL;
|
||
}
|
||
UNLOCK();
|
||
}
|
||
|
||
static void calibration_click_cb(lv_event_t * e) {
|
||
if (!e) {
|
||
ESP_LOGE(TAG, "Null event in calibration_click_cb");
|
||
return;
|
||
}
|
||
|
||
lv_obj_t * btn = lv_event_get_target(e);
|
||
if (!btn) {
|
||
ESP_LOGE(TAG, "Null button in calibration_click_cb");
|
||
return;
|
||
}
|
||
|
||
lv_obj_t * child = lv_obj_get_child(btn, 0);
|
||
if (!child) {
|
||
ESP_LOGE(TAG, "Null child in calibration_click_cb");
|
||
return;
|
||
}
|
||
|
||
const char * txt = lv_label_get_text(child);
|
||
if (!txt) {
|
||
ESP_LOGE(TAG, "Null text in calibration_click_cb");
|
||
return;
|
||
}
|
||
|
||
ESP_LOGI(TAG, "Calibration menu item clicked: %s", txt);
|
||
|
||
if (strcmp(txt, "Cancel") == 0) {
|
||
LOCK();
|
||
menu_go_back();
|
||
UNLOCK();
|
||
} else if (strcmp(txt, "Calibrate") == 0) {
|
||
// Run calibration
|
||
system_setZeroAngle();
|
||
|
||
// Delete all menu items
|
||
if (_calibration_page) {
|
||
lv_obj_clean(_calibration_page);
|
||
|
||
// Create a container to help with centering
|
||
lv_obj_t* container = lv_obj_create(_calibration_page);
|
||
lv_obj_set_size(container, lv_pct(100), lv_pct(100));
|
||
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0);
|
||
lv_obj_set_style_border_width(container, 0, 0);
|
||
lv_obj_set_style_pad_all(container, 0, 0);
|
||
|
||
// Show "Calibrated" message centered
|
||
lv_obj_t* label = lv_label_create(container);
|
||
lv_label_set_text(label, "Calibrated");
|
||
lv_obj_set_style_text_color(label, lv_color_black(), 0);
|
||
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
|
||
lv_obj_center(label);
|
||
}
|
||
|
||
// Create a one-shot timer to return to bubble mode after 1 second
|
||
if (_calibration_timer) {
|
||
lv_timer_del(_calibration_timer);
|
||
}
|
||
_calibration_timer = lv_timer_create(calibration_timer_cb, 2000, NULL);
|
||
lv_timer_set_repeat_count(_calibration_timer, 1);
|
||
} else if (strcmp(txt, "Clear") == 0) {
|
||
// Clear calibration
|
||
system_clearZeroAngle();
|
||
|
||
// Delete all menu items
|
||
if (_calibration_page) {
|
||
lv_obj_clean(_calibration_page);
|
||
|
||
// Create a container to help with centering
|
||
lv_obj_t* container = lv_obj_create(_calibration_page);
|
||
lv_obj_set_size(container, lv_pct(100), lv_pct(100));
|
||
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0);
|
||
lv_obj_set_style_border_width(container, 0, 0);
|
||
lv_obj_set_style_pad_all(container, 0, 0);
|
||
|
||
// Show "Cleared" message centered
|
||
lv_obj_t* label = lv_label_create(container);
|
||
lv_label_set_text(label, "Cleared");
|
||
lv_obj_set_style_text_color(label, lv_color_black(), 0);
|
||
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
|
||
lv_obj_center(label);
|
||
}
|
||
|
||
// Create a one-shot timer to return to bubble mode after 1 second
|
||
if (_calibration_timer) {
|
||
lv_timer_del(_calibration_timer);
|
||
}
|
||
_calibration_timer = lv_timer_create(calibration_timer_cb, 2000, NULL);
|
||
lv_timer_set_repeat_count(_calibration_timer, 1);
|
||
}
|
||
}
|
||
|
||
static lv_obj_t* create_calibration_page(void) {
|
||
ESP_LOGI(TAG, "Creating calibration page");
|
||
|
||
lv_obj_t* menu = create_menu_container();
|
||
if (!menu) {
|
||
ESP_LOGE(TAG, "Failed to create menu container for calibration");
|
||
return NULL;
|
||
}
|
||
|
||
_calibration_page = lv_menu_page_create(menu, NULL);
|
||
if (!_calibration_page) {
|
||
ESP_LOGE(TAG, "Failed to create calibration page");
|
||
return NULL;
|
||
}
|
||
|
||
lv_obj_set_style_radius(_calibration_page, 0, LV_PART_MAIN | LV_STATE_ANY);
|
||
lv_obj_set_style_border_width(_calibration_page, 0, LV_PART_MAIN | LV_STATE_ANY);
|
||
lv_obj_set_style_pad_all(_calibration_page, 0, LV_PART_MAIN);
|
||
lv_obj_set_style_pad_row(_calibration_page, 0, LV_PART_MAIN);
|
||
lv_obj_set_style_pad_column(_calibration_page, 0, LV_PART_MAIN);
|
||
lv_obj_set_scrollbar_mode(_calibration_page, LV_SCROLLBAR_MODE_AUTO);
|
||
lv_obj_set_size(_calibration_page, lv_pct(100), lv_pct(100));
|
||
|
||
// Add menu items in order: Cancel, Calibrate, Clear
|
||
lv_obj_t* cancel_btn = addMenuItem(_calibration_page, "Cancel");
|
||
lv_obj_add_event_cb(cancel_btn, calibration_click_cb, LV_EVENT_CLICKED, NULL);
|
||
lv_obj_add_state(cancel_btn, LV_STATE_FOCUSED); // First item focused
|
||
|
||
lv_obj_t* calibrate_btn = addMenuItem(_calibration_page, "Calibrate");
|
||
lv_obj_add_event_cb(calibrate_btn, calibration_click_cb, LV_EVENT_CLICKED, NULL);
|
||
|
||
lv_obj_t* clear_btn = addMenuItem(_calibration_page, "Clear");
|
||
lv_obj_add_event_cb(clear_btn, calibration_click_cb, LV_EVENT_CLICKED, NULL);
|
||
|
||
return _calibration_page;
|
||
}
|
||
|
||
static void show_calibration_menu(void) {
|
||
ESP_LOGI(TAG, "Showing calibration menu");
|
||
|
||
lv_obj_t* menu = create_menu_container();
|
||
if (!menu) {
|
||
ESP_LOGE(TAG, "Failed to create menu container for calibration");
|
||
return;
|
||
}
|
||
|
||
lv_obj_t* calibration_page = create_calibration_page();
|
||
if (!calibration_page) {
|
||
ESP_LOGE(TAG, "Failed to create calibration page");
|
||
return;
|
||
}
|
||
|
||
lv_menu_set_page(menu, calibration_page);
|
||
_currentPage = calibration_page;
|
||
_mode = GUI_MENU; // Keep in menu mode
|
||
|
||
// 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);
|
||
|
||
ESP_LOGI(TAG, "Calibration menu displayed");
|
||
}
|
||
|
||
// ───── 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
|
||
¬ifiedBits,
|
||
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_device_container && _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);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
} |