Files
soundshot/main/gui.c
Brent Perteet a89fdc6843 Update GUI styling and font configuration
- Fix bt_app.h syntax error (remove stray "1")
- Improve menu item alignment and styling
- Enable additional LVGL fonts (Montserrat 8, Unscii 16)
- Clean up volume page layout, remove instruction labels
- Update default font to Montserrat 8

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-27 18:23:40 +00:00

1071 lines
31 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdio.h>
#include <math.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "lvgl.h"
#include "esp_lvgl_port.h"
#include "gui.h"
#include "gpio.h"
#include "keypad.h"
#include "bubble.h"
#include "system.h"
#include "bt_app.h"
#define DEVKIT
#undef DEVKIT
static const char *TAG = "gui";
#define UNLOCK() lvgl_port_unlock()
#define LOCK() lvgl_port_lock(0)
// LCD Pin Configuration
#define LCD_PIXEL_CLOCK_HZ (5 * 1000 * 1000)
#define LCD_SPI_HOST SPI2_HOST
#define PIN_NUM_nON 26
// ST7735S properties
#define LCD_H_RES 160
#define LCD_V_RES 80
#define LCD_CMD_BITS 8
#define LCD_PARAM_BITS 8
#define LCD_COLOR_SPACE ESP_LCD_COLOR_SPACE_RGB
#define LCD_BITS_PER_PIXEL 16
static esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_io_handle_t io_handle = NULL;
static lv_disp_t *disp = NULL;
static lv_obj_t *imu_label = NULL; // Label for IMU data
static lv_style_t style_mono8;
typedef struct
{
int selected;
int count;
lv_obj_t *obj;
} menu_context_t;
static void gui_task(void *pvParameters);
static void createBubble(lv_obj_t * scr);
static void build_scrollable_menu(void);
static void currentFocusIndex(menu_context_t *ctx);
static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text);
static lv_obj_t* create_menu_container(void);
static void show_bt_device_list(void);
static void show_volume_control(void);
static lv_obj_t* create_volume_page(void);
static void update_volume_display(int volume);
static void ensure_menu_styles(void);
#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;
/* 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;
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_scrollbar_mode(main_page, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_size(main_page, lv_pct(100), lv_pct(100));
// Add menu items
lv_obj_t* tmpObj;
tmpObj = addMenuItem(main_page, "Bluetooth");
lv_obj_add_state(tmpObj, LV_STATE_FOCUSED); // First item focused
addMenuItem(main_page, "Calibration");
addMenuItem(main_page, "Volume");
addMenuItem(main_page, "About");
addMenuItem(main_page, "Exit");
return main_page;
}
static void show_menu(void) {
lv_obj_t* menu = create_menu_container();
lv_obj_t* main_page = create_main_page();
lv_menu_set_page(menu, main_page);
_currentPage = main_page;
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
}
static void cleanup_menu(void) {
if (_menu) {
lv_obj_del(_menu);
_menu = NULL;
_currentPage = NULL;
ESP_LOGI(TAG, "Menu cleaned up to free memory");
}
}
static void show_bubble(void) {
cleanup_menu(); // Free menu memory when returning to bubble
lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
}
static void create_lvgl_demo(void)
{
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
};
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,
.double_buffer = false,
.hres = LCD_H_RES,
.vres = LCD_V_RES,
.monochrome = false,
.rotation = {
.swap_xy = true,
.mirror_x = true,
.mirror_y = false,
},
.flags = {
//.buff_dma = true,
.swap_bytes = true,
}
};
disp = lvgl_port_add_disp(&disp_cfg);
//lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
}
static void createBubble(lv_obj_t * scr)
{
// 2) Create a bubble level of size 200×60, with range [30°, +30°], initial 0°:
lv_obj_t * level = bubble_create(scr, 150, 40, -10.0f, +10.0f, 0.0f);
lv_obj_align(level, LV_ALIGN_CENTER, 0, 0);
// 3) … Later, when you read your accelerometer or keypadderived angle …
float new_angle = 10.0f;
bubble_setValue(level, new_angle);
// 4) You can call bubble_level_set_value(level, …) as often as you like.
// Each call invalidates the object and LVGL will call the draw callback
// (usually on the next tick, or immediately if LVGL is idle).
_bubble = level;
}
void gui_start(void)
{
// Initialize LCD
lcd_init();
// Initialize LVGL
lvgl_init();
// Create UI
create_lvgl_demo();
keypad_start();
gpio_set_level(PIN_NUM_BK_LIGHT, 1);
xTaskCreate(gui_task, "gui_task", 8192, NULL, 5, NULL);
}
static uint32_t waitForKeyPress(void)
{
QueueHandle_t q = keypad_getQueue();
uint32_t ev = 0;
if (xQueueReceive(q, &ev, portMAX_DELAY) == pdTRUE)
{
return ev;
}
return 0;
}
static void handleMainMenu(void)
{
lvgl_port_lock(0);
// Create a screen with black background
lv_obj_t *scr = lv_scr_act();
//create_menu(scr);
lvgl_port_unlock();
}
// ───── MENU CONFIG ─────
#define ROW_H 20 // height of each row
static const char * items[] = { // your menu entries
"VMode Crossmade",
"Second Choice",
"Another Option",
"Yet Another",
"Yet Another",
"Last One"
};
#define ITEM_COUNT (sizeof(items)/sizeof(items[0]))
// ───── STATE & HELPERS ─────
static lv_obj_t * btn_array[ITEM_COUNT];
static int selected_idx = 0;
// Called whenever a button is "activated" (Enter/Click)
static void btn_click_cb(lv_event_t * e) {
lv_obj_t * btn = lv_event_get_target(e);
const char * txt = lv_label_get_text(lv_obj_get_child(btn, 0));
ESP_LOGI(TAG, "Activated: %s\n", txt);
// Handle specific menu items
if (strcmp(txt, "Bluetooth") == 0) {
LOCK();
show_bt_device_list();
UNLOCK();
} else if (strcmp(txt, "Volume") == 0) {
LOCK();
show_volume_control();
UNLOCK();
} else if (strcmp(txt, "Exit") == 0) {
LOCK();
_mode = GUI_BUBBLE;
show_bubble();
UNLOCK();
}
// Add more menu handlers here as needed
ESP_LOGI(TAG, "End btn_click_cb");
}
// Repaint all rows so only btn_array[selected_idx] is highlighted
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();
}
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_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_align(lbl, LV_ALIGN_LEFT_MID, 0, 0); // Center vertically, align left horizontally
// 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());
lv_obj_set_style_radius(_menu, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(_menu, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_size(_menu, lv_pct(100), lv_pct(100));
lv_obj_center(_menu);
lv_obj_set_scrollbar_mode(_menu, LV_SCROLLBAR_MODE_AUTO);
lv_obj_add_flag(_menu, LV_OBJ_FLAG_HIDDEN); // Hidden by default
}
return _menu;
}
// ───── BLUETOOTH DEVICE LIST PAGE ─────
static lv_obj_t* _bt_page = NULL;
static int _bt_selected_device = 0;
// ───── VOLUME CONTROL PAGE ─────
static lv_obj_t* _volume_page = NULL;
static lv_obj_t* _volume_bar = NULL;
static lv_obj_t* _volume_label = NULL;
static int _current_volume = 50; // Default volume (0-100)
static void bt_device_click_cb(lv_event_t * e) {
if (!e) {
ESP_LOGE(TAG, "Null event in bt_device_click_cb");
return;
}
lv_obj_t * btn = lv_event_get_target(e);
if (!btn) {
ESP_LOGE(TAG, "Null button in bt_device_click_cb");
return;
}
lv_obj_t * child = lv_obj_get_child(btn, 0);
if (!child) {
ESP_LOGE(TAG, "Null child in bt_device_click_cb");
return;
}
const char * txt = lv_label_get_text(child);
if (!txt) {
ESP_LOGE(TAG, "Null text in bt_device_click_cb");
return;
}
// Handle special buttons
if (strcmp(txt, "Back") == 0) {
LOCK();
bt_stop_discovery();
_mode = GUI_MENU;
show_menu();
UNLOCK();
return;
} else if (strcmp(txt, "Refresh") == 0) {
LOCK();
// Use system event instead of direct BT call
system_requestBtRefresh();
UNLOCK();
return;
}
// Find which device was clicked
bt_device_list_t* device_list = bt_get_device_list();
if (!device_list) {
ESP_LOGE(TAG, "Null device list in bt_device_click_cb");
return;
}
if (!_bt_page) {
ESP_LOGE(TAG, "Null _bt_page in bt_device_click_cb");
return;
}
for (int i = 0; i < device_list->count; i++) {
lv_obj_t * child = lv_obj_get_child(_bt_page, i);
if (child == btn) {
ESP_LOGI(TAG, "Requesting connection to device %d: %s", i, device_list->devices[i].name);
// Use system event instead of direct BT call
system_requestBtConnect(i);
// Return to bubble mode after selection
_mode = GUI_BUBBLE;
show_bubble();
return;
}
}
}
static lv_obj_t* create_bt_device_page(void) {
ESP_LOGI(TAG, "Creating Bluetooth device page");
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container");
return NULL;
}
_bt_page = lv_menu_page_create(menu, NULL);
if (!_bt_page) {
ESP_LOGE(TAG, "Failed to create Bluetooth page");
return NULL;
}
lv_obj_set_style_radius(_bt_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(_bt_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_scrollbar_mode(_bt_page, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_size(_bt_page, lv_pct(100), lv_pct(100));
ESP_LOGI(TAG, "Starting Bluetooth discovery");
bool discovery_started = true;
#if 0
// Try to start discovery (may fail if BT stack is busy)
bool discovery_started = bt_start_discovery();
if (!discovery_started) {
ESP_LOGW(TAG, "Discovery not started - will show paired devices only");
}
#endif
// Get device list
ESP_LOGI(TAG, "Getting device list");
bt_device_list_t* device_list = bt_get_device_list();
if (!device_list) {
ESP_LOGE(TAG, "Failed to get device list");
return _bt_page;
}
if (device_list->count == 0) {
// Show appropriate message based on discovery status
const char* msg = discovery_started ? "Scanning for devices..." : "No devices found";
lv_obj_t* tmpObj = addMenuItem(_bt_page, msg);
lv_obj_add_state(tmpObj, LV_STATE_DISABLED);
} else {
// Add devices to the page
bool first = true;
for (int i = 0; i < device_list->count; i++) {
char device_text[64];
snprintf(device_text, sizeof(device_text), "%s%s",
device_list->devices[i].name,
device_list->devices[i].is_paired ? " (paired)" : "");
lv_obj_t* btn = addMenuItem(_bt_page, device_text);
lv_obj_add_event_cb(btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
if (first) {
lv_obj_add_state(btn, LV_STATE_FOCUSED);
first = false;
}
}
}
// Add back/refresh options
lv_obj_t* refresh_btn = addMenuItem(_bt_page, "Refresh");
lv_obj_add_event_cb(refresh_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t* back_btn = addMenuItem(_bt_page, "Back");
lv_obj_add_event_cb(back_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL);
return _bt_page;
}
static void show_bt_device_list(void) {
ESP_LOGI(TAG, "Showing Bluetooth device list");
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container for Bluetooth list");
return;
}
lv_obj_t* bt_page = create_bt_device_page();
if (!bt_page) {
ESP_LOGE(TAG, "Failed to create Bluetooth device page");
return;
}
lv_menu_set_page(menu, bt_page);
_currentPage = bt_page;
_mode = GUI_MENU; // Keep in menu mode
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
ESP_LOGI(TAG, "Bluetooth device list displayed");
}
// ───── VOLUME CONTROL PAGE ─────
static lv_obj_t* create_volume_page(void) {
ESP_LOGI(TAG, "Creating volume control page");
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container for volume control");
return NULL;
}
_volume_page = lv_menu_page_create(menu, NULL);
if (!_volume_page) {
ESP_LOGE(TAG, "Failed to create volume page");
return NULL;
}
lv_obj_set_style_radius(_volume_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_style_border_width(_volume_page, 0, LV_PART_MAIN | LV_STATE_ANY);
lv_obj_set_scrollbar_mode(_volume_page, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_size(_volume_page, lv_pct(100), lv_pct(100));
// Create title label
lv_obj_t* title = lv_label_create(_volume_page);
lv_label_set_text(title, "Volume Control");
lv_obj_set_style_text_align(title, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_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
return _volume_page;
}
static void show_volume_control(void) {
ESP_LOGI(TAG, "Showing volume control");
lv_obj_t* menu = create_menu_container();
if (!menu) {
ESP_LOGE(TAG, "Failed to create menu container for volume control");
return;
}
lv_obj_t* volume_page = create_volume_page();
if (!volume_page) {
ESP_LOGE(TAG, "Failed to create volume page");
return;
}
lv_menu_set_page(menu, volume_page);
_currentPage = volume_page;
_mode = GUI_MENU; // Keep in menu mode
lv_obj_remove_flag(menu, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
ESP_LOGI(TAG, "Volume control displayed");
}
static void update_volume_display(int volume) {
if (_volume_bar && _volume_label) {
LOCK();
lv_bar_set_value(_volume_bar, volume, LV_ANIM_ON);
char volume_text[16];
snprintf(volume_text, sizeof(volume_text), "%d%%", volume);
lv_label_set_text(_volume_label, volume_text);
UNLOCK();
_current_volume = volume;
ESP_LOGI(TAG, "Volume display updated to %d%%", volume);
}
}
// ───── BUILD THE MENU ─────
static void build_scrollable_menu(void) {
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));
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);
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 {
menuNext();
}
}
ESP_LOGI(TAG, "MAIN: Button UP SHORT");
break;
case (KEY_DOWN << KEY_SHORT_PRESS):
{
if (_mode == GUI_MENU)
{
// Check if we're on the volume control page
if (_currentPage == _volume_page) {
// Volume down
if (_current_volume > 0) {
_current_volume -= 5;
if (_current_volume < 0) _current_volume = 0;
update_volume_display(_current_volume);
system_requestVolumeDown();
}
} else {
menuPrevious();
}
}
ESP_LOGI(TAG, "MAIN: Button DOWN SHORT");
break;
}
case (KEY0 << KEY_LONG_PRESS):
{
ESP_LOGI(TAG, "MAIN: Button 0 LONG - Enter");
LOCK();
if (_mode != GUI_MENU)
{
_mode = GUI_MENU;
show_menu(); // Use lazy loading
}
else if (_mode == GUI_MENU)
{
activate_selected();
// Check if we're in special pages - don't auto-exit
if (_currentPage == _bt_page) {
// Don't automatically exit to bubble mode from Bluetooth page
} else if (_currentPage == _volume_page) {
// Don't automatically exit to bubble mode from Volume page
} else {
ESP_LOGI(TAG, "return to main");
// In main menu - activate selection and exit to bubble
activate_selected();
_mode = GUI_BUBBLE;
show_bubble(); // Cleanup menu and show bubble
}
}
UNLOCK();
ESP_LOGI(TAG, "MAIN: Button 0 LONG - Exit");
break;
}
case (KEY1 << KEY_LONG_PRESS):
{
LOCK();
if (_mode == GUI_MENU)
{
_mode = GUI_BUBBLE;
show_bubble(); // Cleanup menu and show bubble
}
else
{
// Power off on long press from bubble mode
gpio_set_level(PIN_NUM_nON, 0);
}
UNLOCK();
ESP_LOGI(TAG, "MAIN: Button 1 LONG - Back/Power");
break;
}
default:
break;
}
}
if (_mode == GUI_BUBBLE)
{
uint32_t notifiedBits = 0;
// clear on entry (first param), wait for any bit, block forever
xTaskNotifyWait(
0xFFFFFFFF, // clear any old bits on entry
0xFFFFFFFF, // clear bits on exit
&notifiedBits,
pdMS_TO_TICKS(100));
if (notifiedBits & EM_EVENT_NEW_DATA)
{
ImuData_t d = system_getImuData();
bubble_setValue(_bubble, -d.angle);
}
}
}
}