- Initialize menu context when showing menu to properly track focused item - Fix double-activation bug that caused immediate exit from Bluetooth menu - Add separate refresh_bt_device_list() to avoid infinite discovery loop - Delete old BT page before creating new one to prevent memory leaks - Add "Clear Paired" option to Bluetooth menu - Improve Bluetooth menu refresh behavior to show "Scanning..." message 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1371 lines
40 KiB
C
1371 lines
40 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);
|
||
|
||
// 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;
|
||
|
||
// 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;
|
||
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, "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);
|
||
|
||
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);
|
||
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, 4000, LV_PART_MAIN); // Set scrolling duration (4 seconds per cycle - slower)
|
||
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 ─────
|
||
static lv_obj_t* _bt_page = NULL;
|
||
static int _bt_selected_device = 0;
|
||
|
||
// ───── VOLUME CONTROL PAGE ─────
|
||
static lv_obj_t* _volume_page = NULL;
|
||
static lv_obj_t* _volume_bar = NULL;
|
||
static lv_obj_t* _volume_label = NULL;
|
||
static int _current_volume = 50; // Default volume (0-100)
|
||
|
||
static void bt_device_click_cb(lv_event_t * e) {
|
||
if (!e) {
|
||
ESP_LOGE(TAG, "Null event in bt_device_click_cb");
|
||
return;
|
||
}
|
||
|
||
lv_obj_t * btn = lv_event_get_target(e);
|
||
if (!btn) {
|
||
ESP_LOGE(TAG, "Null button in bt_device_click_cb");
|
||
return;
|
||
}
|
||
|
||
lv_obj_t * child = lv_obj_get_child(btn, 0);
|
||
if (!child) {
|
||
ESP_LOGE(TAG, "Null child in bt_device_click_cb");
|
||
return;
|
||
}
|
||
|
||
const char * txt = lv_label_get_text(child);
|
||
if (!txt) {
|
||
ESP_LOGE(TAG, "Null text in bt_device_click_cb");
|
||
return;
|
||
}
|
||
|
||
// Handle special buttons
|
||
if (strcmp(txt, "Back") == 0) {
|
||
LOCK();
|
||
bt_stop_discovery();
|
||
menu_go_back();
|
||
UNLOCK();
|
||
return;
|
||
} else if (strcmp(txt, "Refresh") == 0) {
|
||
LOCK();
|
||
// Recreate the BT page with discovery enabled to show "Scanning..."
|
||
show_bt_device_list();
|
||
UNLOCK();
|
||
return;
|
||
} else if (strcmp(txt, "Clear Paired") == 0) {
|
||
LOCK();
|
||
// 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();
|
||
// Refresh the device list to show updated state
|
||
system_requestBtRefresh();
|
||
} else {
|
||
ESP_LOGE(TAG, "Failed to clear paired devices: %s", esp_err_to_name(ret));
|
||
}
|
||
UNLOCK();
|
||
return;
|
||
}
|
||
|
||
// Find which device was clicked
|
||
bt_device_list_t* device_list = bt_get_device_list();
|
||
if (!device_list) {
|
||
ESP_LOGE(TAG, "Null device list in bt_device_click_cb");
|
||
return;
|
||
}
|
||
|
||
if (!_bt_page) {
|
||
ESP_LOGE(TAG, "Null _bt_page in bt_device_click_cb");
|
||
return;
|
||
}
|
||
|
||
for (int i = 0; i < device_list->count; i++) {
|
||
lv_obj_t * child = lv_obj_get_child(_bt_page, i);
|
||
if (child == btn) {
|
||
ESP_LOGI(TAG, "Requesting connection to device %d: %s", i, device_list->devices[i].name);
|
||
// Use system event instead of direct BT call
|
||
system_requestBtConnect(i);
|
||
|
||
// Return to bubble mode after selection
|
||
_mode = GUI_BUBBLE;
|
||
show_bubble();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
static lv_obj_t* create_bt_device_page(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;
|
||
}
|
||
|
||
// Delete old BT page if it exists to prevent memory leaks
|
||
if (_bt_page) {
|
||
ESP_LOGI(TAG, "Deleting old Bluetooth page");
|
||
lv_obj_del(_bt_page);
|
||
_bt_page = NULL;
|
||
}
|
||
|
||
_bt_page = lv_menu_page_create(menu, NULL);
|
||
if (!_bt_page) {
|
||
ESP_LOGE(TAG, "Failed to create Bluetooth page");
|
||
return NULL;
|
||
}
|
||
|
||
lv_obj_set_style_radius(_bt_page, 0, LV_PART_MAIN | LV_STATE_ANY);
|
||
lv_obj_set_style_border_width(_bt_page, 0, LV_PART_MAIN | LV_STATE_ANY);
|
||
lv_obj_set_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));
|
||
|
||
bool discovery_started = false;
|
||
if (start_discovery) {
|
||
ESP_LOGI(TAG, "Starting Bluetooth discovery");
|
||
// Try to start discovery (may fail if BT stack is busy)
|
||
discovery_started = bt_start_discovery();
|
||
|
||
if (!discovery_started) {
|
||
ESP_LOGW(TAG, "Discovery not started - will show paired devices only");
|
||
}
|
||
}
|
||
|
||
// Get device list
|
||
ESP_LOGI(TAG, "Getting device list");
|
||
bt_device_list_t* device_list = bt_get_device_list();
|
||
if (!device_list) {
|
||
ESP_LOGE(TAG, "Failed to get device list");
|
||
return _bt_page;
|
||
}
|
||
|
||
if (device_list->count == 0) {
|
||
// Show appropriate message based on discovery status
|
||
const char* msg = discovery_started ? "Scanning..." : "No devices found";
|
||
lv_obj_t* tmpObj = addMenuItem(_bt_page, msg);
|
||
lv_obj_add_state(tmpObj, LV_STATE_DISABLED);
|
||
lv_obj_clear_state(tmpObj, LV_STATE_FOCUSED); // Remove focus from disabled item
|
||
} else {
|
||
// Add devices to the page
|
||
bool first = true;
|
||
for (int i = 0; i < device_list->count; i++) {
|
||
char device_text[64];
|
||
snprintf(device_text, sizeof(device_text), "%s%s",
|
||
device_list->devices[i].name,
|
||
device_list->devices[i].is_paired ? " (paired)" : "");
|
||
|
||
lv_obj_t* btn = addMenuItem(_bt_page, device_text);
|
||
lv_obj_add_event_cb(btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
|
||
|
||
if (first) {
|
||
lv_obj_add_state(btn, LV_STATE_FOCUSED);
|
||
first = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add back/refresh/clear options
|
||
lv_obj_t* refresh_btn = addMenuItem(_bt_page, "Refresh");
|
||
lv_obj_add_event_cb(refresh_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
|
||
|
||
// If no devices were listed, focus on Refresh button
|
||
if (device_list->count == 0) {
|
||
lv_obj_add_state(refresh_btn, LV_STATE_FOCUSED);
|
||
}
|
||
|
||
lv_obj_t* clear_btn = addMenuItem(_bt_page, "Clear Paired");
|
||
lv_obj_add_event_cb(clear_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
|
||
|
||
lv_obj_t* back_btn = addMenuItem(_bt_page, "Back");
|
||
lv_obj_add_event_cb(back_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
|
||
|
||
return _bt_page;
|
||
}
|
||
|
||
static void show_bt_device_list(void) {
|
||
ESP_LOGI(TAG, "Showing Bluetooth device list 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
|
||
|
||
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 without new discovery");
|
||
|
||
// Don't start new discovery, just refresh the display with current devices
|
||
bool should_start_discovery = false;
|
||
|
||
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
|
||
|
||
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 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
|
||
¬ifiedBits,
|
||
pdMS_TO_TICKS(10));
|
||
|
||
if (notifiedBits & EM_EVENT_BT_DISCOVERY_COMPLETE) {
|
||
// Discovery completed - refresh the BT page if we're on it
|
||
if (_mode == GUI_MENU && _currentPage == _bt_page) {
|
||
ESP_LOGI(TAG, "Discovery complete, refreshing Bluetooth page");
|
||
LOCK();
|
||
// Recreate the BT page with updated device list (without starting new discovery)
|
||
refresh_bt_device_list();
|
||
UNLOCK();
|
||
}
|
||
}
|
||
|
||
if (_mode == GUI_BUBBLE)
|
||
{
|
||
if (notifiedBits & EM_EVENT_NEW_DATA)
|
||
{
|
||
ImuData_t d = system_getImuData();
|
||
bubble_setValue(_bubble, -d.angle);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
} |