Add dark color scheme, backlight timeout, and L/R swap feature

- Invert UI colors: black background with white text, yellow highlights
- Add 30-second backlight timeout with auto-wake on button press
- Add Swap L/R menu option with checkbox to swap audio channels
- Update volume page styling (yellow bar, full-screen container)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Brent Perteet
2025-11-21 09:08:12 -06:00
parent 31e0e3a148
commit 3bce9e772c
4 changed files with 206 additions and 27 deletions

View File

@@ -65,6 +65,8 @@ 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 * addMenuItemWithCheckbox(lv_obj_t *page, const char *text, bool checked, lv_obj_t **checkbox_out);
static void update_checkbox_focus_style(lv_obj_t *cb, bool focused);
static lv_obj_t* create_menu_container(void);
static void show_bt_device_list(void);
static void show_volume_control(void);
@@ -103,6 +105,19 @@ static lv_obj_t *_currentPage = NULL;
static GuiMode_t _mode = GUI_BUBBLE;
// Backlight timeout
#define BACKLIGHT_TIMEOUT_MS 30000
static TickType_t _lastButtonPress = 0;
static bool _backlightOn = true;
static void backlight_reset_timeout(void) {
_lastButtonPress = xTaskGetTickCount();
if (!_backlightOn) {
gpio_set_level(PIN_NUM_BK_LIGHT, 1);
_backlightOn = true;
}
}
// Menu navigation stack
#define MAX_MENU_STACK_SIZE 8
typedef struct {
@@ -137,6 +152,9 @@ static int _current_volume = 50; // Default volume (0-100)
static lv_obj_t* _calibration_page = NULL;
static lv_timer_t* _calibration_timer = NULL;
// Swap L/R checkbox
static lv_obj_t* _swapLR_checkbox = 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;
@@ -200,6 +218,7 @@ static lv_obj_t* create_main_page(void) {
addMenuItem(main_page, "Calibration");
addMenuItem(main_page, "Volume");
addMenuItemWithCheckbox(main_page, "Swap L/R", system_getSwapLR(), &_swapLR_checkbox);
//addMenuItem(main_page, "About");
addMenuItem(main_page, "Exit");
@@ -238,6 +257,7 @@ static void cleanup_menu(void) {
_volume_page = NULL;
_volume_bar = NULL;
_volume_label = NULL;
_swapLR_checkbox = NULL;
if (_calibration_timer) {
lv_timer_del(_calibration_timer);
_calibration_timer = NULL;
@@ -257,7 +277,9 @@ static void create_lvgl_demo(void)
lvgl_port_lock(0);
// Create a screen with black background
lv_obj_t *scr = lv_scr_act();
lv_obj_set_style_bg_color(scr, lv_color_black(), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
createBubble(scr);
// Menu will be created lazily when needed
@@ -462,6 +484,14 @@ static void btn_click_cb(lv_event_t * e) {
menu_stack_push(_currentPage);
show_volume_control();
UNLOCK();
} else if (strcmp(txt, "Swap L/R") == 0) {
// Toggle the swap L/R state
system_toggleSwapLR();
// Update the checkbox visual (now a label showing "X" or empty)
if (_swapLR_checkbox) {
lv_label_set_text(_swapLR_checkbox, system_getSwapLR() ? "X" : "");
}
ESP_LOGI(TAG, "Swap L/R toggled to: %s", system_getSwapLR() ? "ON" : "OFF");
} else if (strcmp(txt, "Back") == 0) {
LOCK();
menu_go_back();
@@ -498,18 +528,18 @@ static void refresh_highlight(void) {
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_bg_color(child, lv_color_hex(0xFFFF00), 0);
lv_obj_set_style_text_color(lv_obj_get_child(child,0),
lv_color_white(), 0);
lv_color_black(), 0);
selected = child;
ESP_LOGI(TAG, "Selected");
}
else
else
{
lv_obj_set_style_bg_color(child, lv_color_white(), 0);
lv_obj_set_style_bg_color(child, lv_color_black(), 0);
lv_obj_set_style_text_color(lv_obj_get_child(child,0),
lv_color_black(), 0);
lv_color_white(), 0);
}
index++;
@@ -560,6 +590,12 @@ static void menuInc(int inc)
// Update scroll on old focused item (stop scrolling)
update_label_scroll(_menuContext.obj, false);
// Update checkbox style on old focused item (if it has one)
lv_obj_t *old_cb = lv_obj_get_user_data(_menuContext.obj);
if (old_cb) {
update_checkbox_focus_style(old_cb, false);
}
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);
@@ -567,6 +603,12 @@ static void menuInc(int inc)
// Update scroll on new focused item (start scrolling)
update_label_scroll(next, true);
// Update checkbox style on new focused item (if it has one)
lv_obj_t *new_cb = lv_obj_get_user_data(next);
if (new_cb) {
update_checkbox_focus_style(new_cb, true);
}
_menuContext.obj = next;
_menuContext.selected = search_index;
}
@@ -626,6 +668,83 @@ static void label_scroll_focus_cb(lv_event_t * e)
}
}
static lv_obj_t * addMenuItemWithCheckbox(lv_obj_t *page, const char *text, bool checked, lv_obj_t **checkbox_out)
{
if (!page || !text) {
ESP_LOGE(TAG, "Null parameters in addMenuItemWithCheckbox");
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 addMenuItemWithCheckbox");
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);
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 on the left (same as regular menu item)
lv_obj_t * lbl = lv_label_create(btn);
if (!lbl) {
ESP_LOGE(TAG, "Failed to create label in addMenuItemWithCheckbox");
lv_obj_del(btn);
return NULL;
}
lv_label_set_text(lbl, text);
lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP);
lv_obj_set_width(lbl, LV_PCT(80));
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);
// Label on the right to act as checkbox (shows "X" when checked, empty when not)
lv_obj_t * cb = lv_label_create(btn);
lv_label_set_text(cb, checked ? "X" : "");
lv_obj_set_style_text_color(cb, lv_color_white(), 0); // Unfocused: white
lv_obj_align(cb, LV_ALIGN_RIGHT_MID, -4, 0);
if (checkbox_out) {
*checkbox_out = cb;
}
// Add focus event handler to control scrolling
lv_obj_add_event_cb(btn, label_scroll_focus_cb, LV_EVENT_ALL, NULL);
// click callback
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL);
// Store checkbox in user data for focus styling updates
lv_obj_set_user_data(btn, cb);
return btn;
}
// Update checkbox label color based on parent focus state
static void update_checkbox_focus_style(lv_obj_t *cb, bool focused) {
if (!cb) return;
if (focused) {
// Focused: black text (on yellow background)
lv_obj_set_style_text_color(cb, lv_color_black(), 0);
} else {
// Unfocused: white text (on black background)
lv_obj_set_style_text_color(cb, lv_color_white(), 0);
}
}
static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text)
{
if (!page || !text) {
@@ -719,12 +838,12 @@ static void ensure_menu_styles(void) {
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_bg_color(&_styleUnfocusedBtn, lv_color_make(0x00,0x00,0x00)); // black bg
lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_white()); // white 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_bg_color(&_styleFocusedBtn, lv_color_make(0xFF,0xFF,0x00)); // bright yellow bg
lv_style_set_text_color(&_styleFocusedBtn, lv_color_black()); // black text
//lv_style_set_text_font(&_styleFocusedBtn, &lv_font_unscii_16); // larger font
styles_initialized = true;
@@ -1066,9 +1185,9 @@ static lv_obj_t* create_bt_device_page(bool start_discovery) {
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_color(_bt_status_item, lv_color_make(0x00, 0x00, 0x00), 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_text_color(_bt_status_item, lv_color_white(), 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);
@@ -1086,7 +1205,7 @@ static lv_obj_t* create_bt_device_page(bool start_discovery) {
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_color(_bt_device_container, lv_color_black(), 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);
@@ -1235,9 +1354,11 @@ static lv_obj_t* create_volume_page(void) {
// 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_size(container, lv_pct(100), lv_pct(100));
lv_obj_set_style_bg_color(container, lv_color_black(), 0);
lv_obj_set_style_bg_opa(container, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(container, 0, 0);
lv_obj_set_style_radius(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);
@@ -1247,7 +1368,7 @@ static lv_obj_t* create_volume_page(void) {
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_text_color(title, lv_color_white(), 0);
lv_obj_set_style_pad_bottom(title, 8, 0);
// Create volume bar (progress bar)
@@ -1255,6 +1376,8 @@ static lv_obj_t* create_volume_page(void) {
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_bg_color(_volume_bar, lv_color_make(0x40, 0x40, 0x40), LV_PART_MAIN); // Dark gray background
lv_obj_set_style_bg_color(_volume_bar, lv_color_make(0xFF, 0xFF, 0x00), LV_PART_INDICATOR); // Yellow indicator
lv_obj_set_style_pad_top(_volume_bar, 5, 0);
lv_obj_set_style_pad_bottom(_volume_bar, 5, 0);
@@ -1264,6 +1387,7 @@ static lv_obj_t* create_volume_page(void) {
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_text_color(_volume_label, lv_color_white(), 0);
lv_obj_set_style_pad_top(_volume_label, 5, 0);
return _volume_page;
@@ -1438,7 +1562,7 @@ static void calibration_click_cb(lv_event_t * e) {
// 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_color(label, lv_color_white(), 0);
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
lv_obj_center(label);
}
@@ -1467,7 +1591,7 @@ static void calibration_click_cb(lv_event_t * e) {
// 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_color(label, lv_color_white(), 0);
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
lv_obj_center(label);
}
@@ -1556,11 +1680,11 @@ 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(&_styleUnfocusedBtn, lv_color_make(0x00,0x00,0x00)); // black bg
lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_white());
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0x33,0x99,0xFF)); // blue bg
lv_style_set_text_color(&_styleUnfocusedBtn,lv_color_black());
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0xFF,0xFF,0x00)); // bright yellow bg
lv_style_set_text_color(&_styleFocusedBtn, lv_color_black());
// 2) Inside it, create the lv_menu and hide its sidebar/header
@@ -1653,13 +1777,28 @@ static void gui_task(void *pvParameters)
lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
UNLOCK();
// Initialize backlight timeout
_lastButtonPress = xTaskGetTickCount();
_backlightOn = true;
ESP_LOGI(TAG, "Start GUI Task...");
while (1)
{
// Check backlight timeout
if (_backlightOn) {
TickType_t elapsed = xTaskGetTickCount() - _lastButtonPress;
if (elapsed > pdMS_TO_TICKS(BACKLIGHT_TIMEOUT_MS)) {
gpio_set_level(PIN_NUM_BK_LIGHT, 0);
_backlightOn = false;
ESP_LOGI(TAG, "Backlight off due to timeout");
}
}
if (xQueueReceive(q, &ev, pdMS_TO_TICKS(10)) == pdTRUE)
if (xQueueReceive(q, &ev, pdMS_TO_TICKS(10)) == pdTRUE)
{
// Reset backlight timeout on any button press
backlight_reset_timeout();
switch (ev) {
#if 0
case (KEY_UP << KEY_LONG_PRESS):