From 115105c03279ad3735a8100c4cf4523974b2ca8b Mon Sep 17 00:00:00 2001 From: Brent Perteet Date: Thu, 13 Nov 2025 19:55:32 -0600 Subject: [PATCH] Fix menu navigation issues with Bluetooth and initial focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- main/gui.c | 343 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 277 insertions(+), 66 deletions(-) diff --git a/main/gui.c b/main/gui.c index 33ec22b..4175ccd 100644 --- a/main/gui.c +++ b/main/gui.c @@ -116,6 +116,39 @@ 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); @@ -127,10 +160,14 @@ 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)); @@ -138,25 +175,30 @@ static lv_obj_t* create_main_page(void) { 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, "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); } @@ -359,6 +401,8 @@ static const char * items[] = { // your menu entries 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); @@ -487,22 +531,41 @@ static void activate_selected(void) 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); @@ -513,7 +576,7 @@ static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text) // 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_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); @@ -524,12 +587,19 @@ static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text) 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_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); @@ -587,13 +657,41 @@ static void ensure_menu_styles(void) { 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; } @@ -643,8 +741,23 @@ static void bt_device_click_cb(lv_event_t * e) { return; } else if (strcmp(txt, "Refresh") == 0) { LOCK(); - // Use system event instead of direct BT call - system_requestBtRefresh(); + // 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; } @@ -676,36 +789,47 @@ static void bt_device_click_cb(lv_event_t * e) { } } -static lv_obj_t* create_bt_device_page(void) { - ESP_LOGI(TAG, "Creating Bluetooth device page"); - +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)); - - 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"); + + 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"); + } } -#endif + // Get device list ESP_LOGI(TAG, "Getting device list"); bt_device_list_t* device_list = bt_get_device_list(); @@ -716,9 +840,10 @@ static lv_obj_t* create_bt_device_page(void) { if (device_list->count == 0) { // Show appropriate message based on discovery status - const char* msg = discovery_started ? "Scanning for devices..." : "No devices found"; + 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; @@ -738,41 +863,92 @@ static lv_obj_t* create_bt_device_page(void) { } } - // Add back/refresh options + // 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"); - + 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(); + + 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"); @@ -788,12 +964,20 @@ static lv_obj_t* create_volume_page(void) { 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"); @@ -856,6 +1040,8 @@ static void show_volume_control(void) { 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); @@ -963,11 +1149,20 @@ static void build_scrollable_menu(void) { 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; @@ -975,10 +1170,19 @@ static void build_scrollable_menu(void) { 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); @@ -1056,7 +1260,7 @@ static void gui_task(void *pvParameters) system_requestVolumeUp(); } } else { - menuNext(); + menuPrevious(); } } ESP_LOGI(TAG, "MAIN: Button UP SHORT"); @@ -1076,7 +1280,7 @@ static void gui_task(void *pvParameters) system_requestVolumeDown(); } } else { - menuPrevious(); + menuNext(); } } ESP_LOGI(TAG, "MAIN: Button DOWN SHORT"); @@ -1095,19 +1299,15 @@ static void gui_task(void *pvParameters) 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 + // 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"); @@ -1136,17 +1336,28 @@ static void gui_task(void *pvParameters) } + // 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) { - 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 - ¬ifiedBits, - pdMS_TO_TICKS(100)); - - if (notifiedBits & EM_EVENT_NEW_DATA) + if (notifiedBits & EM_EVENT_NEW_DATA) { ImuData_t d = system_getImuData(); bubble_setValue(_bubble, -d.angle);