diff --git a/main/bt_app.c b/main/bt_app.c index 7738552..8b98144 100644 --- a/main/bt_app.c +++ b/main/bt_app.c @@ -327,33 +327,15 @@ static esp_err_t bt_add_discovered_device(esp_bd_addr_t bda, const char *name) if (!bda || !name) { return ESP_ERR_INVALID_ARG; } - - // Check if device is already known - if (system_isDeviceKnown(bda)) { - char bda_str[18]; - ESP_LOGD(BT_AV_TAG, "Device %s (%s) already known, skipping", - name, bda2str(bda, bda_str, sizeof(bda_str))); - return ESP_OK; - } - - // Create paired_device_t structure for discovered device - paired_device_t device; - memcpy(device.bda, bda, ESP_BD_ADDR_LEN); - strncpy(device.name, name, DEVICE_NAME_MAX_LEN - 1); - device.name[DEVICE_NAME_MAX_LEN - 1] = '\0'; - device.last_connected = 0; // Never connected, set to 0 - - // Save to NVS - esp_err_t ret = system_savePairedDevice(&device); - if (ret == ESP_OK) { - char bda_str[18]; - ESP_LOGI(BT_AV_TAG, "Added discovered device to NVS: %s (%s)", - device.name, bda2str(device.bda, bda_str, sizeof(bda_str))); - } else { - ESP_LOGE(BT_AV_TAG, "Failed to save discovered device: %s", esp_err_to_name(ret)); - } - - return ret; + + // Don't save discovered devices to NVS - they're not paired yet! + // They will only be saved to NVS when successfully connected. + // Just log that we discovered this device. + char bda_str[18]; + ESP_LOGI(BT_AV_TAG, "Discovered device: %s (%s)", + name, bda2str(bda, bda_str, sizeof(bda_str))); + + return ESP_OK; } static esp_err_t __attribute__((unused)) nvs_update_connection_timestamp(esp_bd_addr_t bda) @@ -538,13 +520,13 @@ static void filter_inquiry_scan_result(esp_bt_gap_cb_param_t *param) esp_bt_gap_dev_prop_t *p; /* handle the discovery results */ - + for (int i = 0; i < param->disc_res.num_prop; i++) { p = param->disc_res.prop + i; switch (p->type) { case ESP_BT_GAP_DEV_PROP_COD: cod = *(uint32_t *)(p->val); - + break; case ESP_BT_GAP_DEV_PROP_RSSI: rssi = *(int8_t *)(p->val); @@ -559,9 +541,14 @@ static void filter_inquiry_scan_result(esp_bt_gap_cb_param_t *param) } } + // Log device details for debugging + ESP_LOGI(BT_AV_TAG, " CoD: 0x%"PRIx32", Valid: %d, RSSI: %"PRId32", EIR: %p", + cod, esp_bt_gap_is_valid_cod(cod), rssi, eir); + /* search for device with MAJOR service class as "rendering" in COD */ if (!esp_bt_gap_is_valid_cod(cod) || !(esp_bt_gap_get_cod_srvc(cod) & ESP_BT_COD_SRVC_RENDERING)) { + ESP_LOGI(BT_AV_TAG, " Device filtered out - not an audio rendering device"); return; } @@ -592,9 +579,13 @@ static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *pa switch (event) { /* when device discovered a result, this event comes */ case ESP_BT_GAP_DISC_RES_EVT: { - if (s_a2d_state == APP_AV_STATE_DISCOVERING) { - filter_inquiry_scan_result(param); - } + // Log ALL discovered devices for debugging + char bda_str[18]; + ESP_LOGI(BT_AV_TAG, "*** Device discovered: %s (A2DP state: %d)", + bda2str(param->disc_res.bda, bda_str, 18), s_a2d_state); + + // Process the result regardless of A2DP state + filter_inquiry_scan_result(param); break; } /* when discovery state changed, this event comes */ @@ -602,6 +593,10 @@ static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *pa if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) { ESP_LOGI(BT_AV_TAG, "Device discovery stopped."); s_device_list.discovery_active = false; + + // Notify GUI that discovery is complete so it can refresh the display + system_notifyAll(EM_EVENT_BT_DISCOVERY_COMPLETE); + // Don't automatically connect - wait for user selection // Only connect if we're in DISCOVERED state (manually triggered by bt_connect_device) if (s_a2d_state == APP_AV_STATE_DISCOVERED) { @@ -1103,9 +1098,29 @@ static void bt_app_av_state_connecting_hdlr(uint16_t event, void *param) ESP_LOGI(BT_AV_TAG, "a2dp connected"); s_a2d_state = APP_AV_STATE_CONNECTED; s_media_state = APP_AV_MEDIA_STATE_IDLE; - - // Update connection timestamp for this device - system_updateConnectionTimestamp(s_peer_bda); + + // Check if device is already paired, if not, add it as paired + if (!system_isDeviceKnown(s_peer_bda)) { + ESP_LOGI(BT_AV_TAG, "Device not in paired list, adding: %s", s_peer_bdname); + paired_device_t new_device; + memcpy(new_device.bda, s_peer_bda, ESP_BD_ADDR_LEN); + strncpy(new_device.name, (char*)s_peer_bdname, DEVICE_NAME_MAX_LEN - 1); + new_device.name[DEVICE_NAME_MAX_LEN - 1] = '\0'; + new_device.last_connected = (uint32_t)(esp_timer_get_time() / 1000000); + system_savePairedDevice(&new_device); + + // Update the device in the GUI list to show it as paired + for (int i = 0; i < s_device_list.count; i++) { + if (memcmp(s_device_list.devices[i].bda, s_peer_bda, ESP_BD_ADDR_LEN) == 0) { + s_device_list.devices[i].is_paired = true; + ESP_LOGI(BT_AV_TAG, "Marked device as paired in GUI list: %s", s_peer_bdname); + break; + } + } + } else { + // Update connection timestamp for this device + system_updateConnectionTimestamp(s_peer_bda); + } } else if (a2d->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) { ESP_LOGI(BT_AV_TAG, "Connection failed."); // If device was previously connected (known device), don't retry automatically @@ -1445,6 +1460,7 @@ static void bt_app_task_handler(void *arg) if (notifiedBits & EM_EVENT_BT_REFRESH) { ESP_LOGI(BT_AV_TAG, "BT Refresh event received"); bt_clear_discovered_devices(); + bt_start_discovery(); // Start new discovery after clearing // Notify GUI that refresh is done - could add completion event if needed } if (notifiedBits & EM_EVENT_BT_CONNECT) { @@ -1788,7 +1804,7 @@ bool bt_connect_device(int device_index) { void bt_clear_discovered_devices(void) { int new_count = 0; - + // Keep only paired devices for (int i = 0; i < s_device_list.count; i++) { if (s_device_list.devices[i].is_paired) { @@ -1798,11 +1814,38 @@ void bt_clear_discovered_devices(void) { new_count++; } } - + s_device_list.count = new_count; ESP_LOGI(BT_AV_TAG, "Cleared discovered devices, kept %d paired devices", new_count); } +void bt_clear_all_devices(void) { + s_device_list.count = 0; + ESP_LOGI(BT_AV_TAG, "Cleared all devices from device list"); +} + +void bt_disconnect_current_device(void) { + if (s_a2d_state == APP_AV_STATE_CONNECTED) { + ESP_LOGI(BT_AV_TAG, "Disconnecting from current device"); + + // Stop media first if playing + if (s_media_state == APP_AV_MEDIA_STATE_STARTED) { + esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_SUSPEND); + s_media_state = APP_AV_MEDIA_STATE_STOPPING; + } + + // Disconnect A2DP + esp_a2d_source_disconnect(s_peer_bda); + s_a2d_state = APP_AV_STATE_DISCONNECTING; + } else if (s_a2d_state == APP_AV_STATE_CONNECTING) { + ESP_LOGI(BT_AV_TAG, "Cancelling connection attempt"); + // Cancel connection attempt + s_a2d_state = APP_AV_STATE_UNCONNECTED; + } else { + ESP_LOGI(BT_AV_TAG, "No device connected (state: %d)", s_a2d_state); + } +} + void bt_volume_up(void) { if (!s_volume_control_available) { ESP_LOGW(BT_AV_TAG, "Volume control not available"); diff --git a/main/bt_app.h b/main/bt_app.h index c698fb9..31de09e 100644 --- a/main/bt_app.h +++ b/main/bt_app.h @@ -69,7 +69,7 @@ void bt_app_task_shut_down(void); void bt_app_init(void); /* Bluetooth device management for GUI */ -#define MAX_BT_DEVICES 20 +#define MAX_BT_DEVICES 8 // Reduced from 20 to save memory #define MAX_BT_NAME_LEN 32 typedef struct { @@ -100,6 +100,12 @@ bool bt_connect_device(int device_index); /* Clear discovered devices (keep paired devices) */ void bt_clear_discovered_devices(void); +/* Clear all devices from the device list (paired and discovered) */ +void bt_clear_all_devices(void); + +/* Disconnect from currently connected device */ +void bt_disconnect_current_device(void); + /* Volume control functions */ void bt_volume_up(void); void bt_volume_down(void); diff --git a/main/gui.c b/main/gui.c index 4175ccd..597d36c 100644 --- a/main/gui.c +++ b/main/gui.c @@ -71,6 +71,7 @@ static void show_volume_control(void); static lv_obj_t* create_volume_page(void); static void update_volume_display(int volume); static void ensure_menu_styles(void); +static void bt_device_click_cb(lv_event_t * e); // Menu stack management functions static void menu_stack_push(lv_obj_t *page); @@ -116,6 +117,18 @@ static lv_style_t _styleFocusedBtn; static menu_context_t _menuContext; +// Bluetooth page state +static lv_obj_t* _bt_page = NULL; +static lv_obj_t* _bt_status_item = NULL; // Top status item (unselectable) +static lv_obj_t* _bt_device_container = NULL; // Container for device list items +static TickType_t _bt_scan_start_time = 0; // When scan was started + +// Volume page state +static lv_obj_t* _volume_page = NULL; +static lv_obj_t* _volume_bar = NULL; +static lv_obj_t* _volume_label = NULL; +static int _current_volume = 50; // Default volume (0-100) + // Hide *all* headers/back buttons that LVGL may show for this menu static void menu_hide_headers(lv_obj_t *menu) { if (!menu) return; @@ -208,6 +221,10 @@ static void cleanup_menu(void) { lv_obj_del(_menu); _menu = NULL; _currentPage = NULL; + _bt_page = NULL; + _bt_status_item = NULL; + _bt_device_container = NULL; + _bt_scan_start_time = 0; ESP_LOGI(TAG, "Menu cleaned up to free memory"); } } @@ -493,6 +510,13 @@ static void menuInc(int inc) ESP_LOGI(TAG, "Current Index: %d", _menuContext.selected); + // Safety check: if no focused object found, bail out + if (_menuContext.selected < 0 || _menuContext.obj == NULL) { + ESP_LOGW(TAG, "No focused object found, cannot navigate"); + UNLOCK(); + return; + } + lv_obj_t *next = NULL; // check if we are at the first or last in the page @@ -501,14 +525,16 @@ static void menuInc(int inc) 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); + if (next) { + lv_obj_add_state(next, LV_STATE_FOCUSED); + lv_obj_clear_state(_menuContext.obj, LV_STATE_FOCUSED); + lv_obj_scroll_to_view(next, LV_ANIM_ON); - _menuContext.obj = next; - _menuContext.selected += inc; + _menuContext.obj = next; + _menuContext.selected += inc; + } } - + UNLOCK(); } @@ -593,7 +619,7 @@ static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text) 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_set_style_anim_time(lbl, 3000, LV_PART_MAIN); // Set scrolling duration (3 seconds per cycle) lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0); // Center vertically, align left horizontally // Add focus event handler to control scrolling @@ -699,14 +725,132 @@ static lv_obj_t* create_menu_container(void) { // ───── 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) +// Update the status item text (e.g., "Scanning...", "No devices found") +static void update_bt_status(const char* status_text) { + if (_bt_status_item) { + lv_obj_t* label = lv_obj_get_child(_bt_status_item, 0); + if (label) { + ESP_LOGI(TAG, "Updating BT status to: %s", status_text); + lv_label_set_text(label, status_text); + lv_obj_invalidate(label); // Force redraw + } else { + ESP_LOGW(TAG, "Status item has no label child"); + } + } else { + ESP_LOGW(TAG, "Status item is NULL, cannot update"); + } +} + +// Clear all device items and action buttons from the container (keep status and Back only) +static void clear_bt_device_list(void) { + if (!_bt_device_container) return; + + // Delete all children except status (index 0) and Back (last item) + uint32_t child_count = lv_obj_get_child_count(_bt_device_container); + ESP_LOGI(TAG, "Clearing BT device list, child_count=%d", (int)child_count); + + // Work backwards to avoid index shifting issues + // Keep: status (0) and Back (last) + for (int i = child_count - 2; i > 0; i--) { + lv_obj_t* child = lv_obj_get_child(_bt_device_container, i); + if (child && child != _bt_status_item) { + ESP_LOGI(TAG, " Deleting child at index %d", i); + lv_obj_del(child); + } + } + ESP_LOGI(TAG, "Clear complete, remaining children: %d", (int)lv_obj_get_child_count(_bt_device_container)); +} + +// Populate the device list with current devices and add action buttons +static void populate_bt_device_list(void) { + if (!_bt_device_container) return; + + // Clear old device list items first (keeps status and Back button) + clear_bt_device_list(); + + bt_device_list_t* device_list = bt_get_device_list(); + if (!device_list) { + update_bt_status("Error getting devices"); + return; + } + + ESP_LOGI(TAG, "Populating BT list with %d devices", device_list->count); + + // The page has: [status, Back] + // We'll add: [status, ...devices..., Refresh, Clear?, Back] + uint32_t insert_index = 1; // After status item + bool has_paired_devices = false; + + if (device_list->count == 0) { + update_bt_status("No devices found"); + // Still add Refresh button so user can try again + } else { + update_bt_status("Available Devices:"); + + // Add device items (limit to MAX_BT_DEVICES to avoid memory issues) + bool first = true; + int max_items = (device_list->count < MAX_BT_DEVICES) ? device_list->count : MAX_BT_DEVICES; + for (int i = 0; i < max_items; i++) { + char device_text[64]; + const char* paired_prefix = device_list->devices[i].is_paired ? "* " : ""; + snprintf(device_text, sizeof(device_text), "%s%s", + paired_prefix, + device_list->devices[i].name); + + if (device_list->devices[i].is_paired) { + has_paired_devices = true; + } + + lv_obj_t* btn = addMenuItem(_bt_device_container, device_text); + lv_obj_add_event_cb(btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL); + + // Move to correct position (after status, before Back button) + lv_obj_move_to_index(btn, insert_index++); + + if (first) { + lv_obj_add_state(btn, LV_STATE_FOCUSED); + first = false; + } + } + } + + // Clear focus from all items first + uint32_t child_count = lv_obj_get_child_count(_bt_device_container); + for (uint32_t i = 0; i < child_count; i++) { + lv_obj_t* child = lv_obj_get_child(_bt_device_container, i); + if (child) { + lv_obj_clear_state(child, LV_STATE_FOCUSED); + } + } + + // Always add Refresh button after devices (but before Back) + lv_obj_t* refresh_btn = addMenuItem(_bt_device_container, "Refresh"); + lv_obj_add_event_cb(refresh_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL); + lv_obj_move_to_index(refresh_btn, insert_index++); + + // Only add Clear Paired button if there are actually paired devices + if (has_paired_devices) { + lv_obj_t* clear_btn = addMenuItem(_bt_device_container, "Clear Paired"); + lv_obj_add_event_cb(clear_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL); + lv_obj_move_to_index(clear_btn, insert_index++); + } + + // Set focus on appropriate item + if (device_list->count == 0) { + // No devices found - focus on Refresh + lv_obj_add_state(refresh_btn, LV_STATE_FOCUSED); + } else { + // Devices found - focus on first device (already done above, but devices list was cleared) + lv_obj_t* first_device = lv_obj_get_child(_bt_device_container, 1); // After status + if (first_device) { + lv_obj_add_state(first_device, LV_STATE_FOCUSED); + } + } + + // Update menu context to track the newly focused item + currentFocusIndex(&_menuContext); +} static void bt_device_click_cb(lv_event_t * e) { if (!e) { @@ -741,20 +885,95 @@ static void bt_device_click_cb(lv_event_t * e) { return; } else if (strcmp(txt, "Refresh") == 0) { LOCK(); - // Recreate the BT page with discovery enabled to show "Scanning..." - show_bt_device_list(); + // Update status first + update_bt_status("Scanning..."); + _bt_scan_start_time = xTaskGetTickCount(); + + // Don't delete the current button while we're in its event handler! + // Instead, delete only the device items, keeping Refresh and Back buttons + uint32_t child_count = lv_obj_get_child_count(_bt_device_container); + ESP_LOGI(TAG, "Refresh clicked, clearing device items (not buttons), child_count=%d", (int)child_count); + + // Delete children between status (0) and the last items + // Work backwards to avoid index shifting + // Keep: status (0), Refresh, Back - delete everything else (devices, Clear Paired) + for (int i = child_count - 2; i > 0; i--) { + lv_obj_t* child = lv_obj_get_child(_bt_device_container, i); + if (!child || child == _bt_status_item) { + continue; + } + + // Get the button's text to decide if we should delete it + const char* child_text = NULL; + lv_obj_t* child_label = lv_obj_get_child(child, 0); + if (child_label) { + child_text = lv_label_get_text(child_label); + } + + // Only keep Refresh and Back buttons + if (child_text) { + if (strcmp(child_text, "Refresh") != 0 && strcmp(child_text, "Back") != 0) { + ESP_LOGI(TAG, " Deleting item: %s", child_text); + lv_obj_del(child); + } else { + ESP_LOGI(TAG, " Keeping button: %s", child_text); + } + } + } + + // Clear all focus states and set focus on Back button + child_count = lv_obj_get_child_count(_bt_device_container); + for (uint32_t i = 0; i < child_count; i++) { + lv_obj_t* child = lv_obj_get_child(_bt_device_container, i); + if (child) { + lv_obj_clear_state(child, LV_STATE_FOCUSED); + } + } + // Focus on Back button (last child) + lv_obj_t* back_btn = lv_obj_get_child(_bt_device_container, -1); + if (back_btn) { + lv_obj_add_state(back_btn, LV_STATE_FOCUSED); + } + + // Update menu context to track the newly focused item + currentFocusIndex(&_menuContext); + + bt_start_discovery(); UNLOCK(); return; } else if (strcmp(txt, "Clear Paired") == 0) { LOCK(); + // Disconnect from any connected device first + bt_disconnect_current_device(); + // Clear all paired devices from NVS and device list esp_err_t ret = system_clearAllPairedDevices(); if (ret == ESP_OK) { ESP_LOGI(TAG, "Successfully cleared all paired devices from NVS"); // Also clear the in-memory device list used by GUI bt_clear_all_devices(); - // Refresh the device list to show updated state - system_requestBtRefresh(); + + // Immediately update GUI to show empty list + clear_bt_device_list(); + update_bt_status("No devices found"); + + // Add back the Refresh button + lv_obj_t* refresh_btn = addMenuItem(_bt_device_container, "Refresh"); + lv_obj_add_event_cb(refresh_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL); + lv_obj_move_to_index(refresh_btn, 1); // After status, before Back + + // Focus on Refresh button + uint32_t child_count = lv_obj_get_child_count(_bt_device_container); + for (uint32_t i = 0; i < child_count; i++) { + lv_obj_t* child = lv_obj_get_child(_bt_device_container, i); + if (child) { + lv_obj_clear_state(child, LV_STATE_FOCUSED); + } + } + lv_obj_add_state(refresh_btn, LV_STATE_FOCUSED); + + // Update menu context to track the newly focused item + currentFocusIndex(&_menuContext); } else { ESP_LOGE(TAG, "Failed to clear paired devices: %s", esp_err_to_name(ret)); } @@ -762,31 +981,39 @@ static void bt_device_click_cb(lv_event_t * e) { return; } - // Find which device was clicked + // Find which device was clicked by matching the device name bt_device_list_t* device_list = bt_get_device_list(); if (!device_list) { ESP_LOGE(TAG, "Null device list in bt_device_click_cb"); return; } - - if (!_bt_page) { - ESP_LOGE(TAG, "Null _bt_page in bt_device_click_cb"); - return; + + // Extract device name from button text (remove "* " prefix if present) + char device_name[64]; + const char* name_start = txt; + + // Skip "* " prefix if present + if (txt[0] == '*' && txt[1] == ' ') { + name_start = txt + 2; } - + + strncpy(device_name, name_start, sizeof(device_name) - 1); + device_name[sizeof(device_name) - 1] = '\0'; + + // Find the device index by name for (int i = 0; i < device_list->count; i++) { - 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 + if (strcmp(device_list->devices[i].name, device_name) == 0) { + ESP_LOGI(TAG, "Requesting connection to device %d: %s", i, device_name); system_requestBtConnect(i); - + // Return to bubble mode after selection _mode = GUI_BUBBLE; show_bubble(); return; } } + + ESP_LOGW(TAG, "Device not found in list: %s", device_name); } static lv_obj_t* create_bt_device_page(bool start_discovery) { @@ -798,86 +1025,79 @@ static lv_obj_t* create_bt_device_page(bool start_discovery) { 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; - } + // Only create the page structure once, or recreate if it was deleted + if (_bt_page == NULL) { + ESP_LOGI(TAG, "Creating new Bluetooth page structure"); - _bt_page = lv_menu_page_create(menu, NULL); - if (!_bt_page) { - ESP_LOGE(TAG, "Failed to create Bluetooth page"); - return NULL; - } - - lv_obj_set_style_radius(_bt_page, 0, LV_PART_MAIN | LV_STATE_ANY); - lv_obj_set_style_border_width(_bt_page, 0, LV_PART_MAIN | LV_STATE_ANY); - lv_obj_set_style_pad_all(_bt_page, 0, LV_PART_MAIN); - lv_obj_set_style_pad_row(_bt_page, 0, LV_PART_MAIN); - lv_obj_set_style_pad_column(_bt_page, 0, LV_PART_MAIN); - lv_obj_set_scrollbar_mode(_bt_page, LV_SCROLLBAR_MODE_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"); + _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)); + + // Use the page itself as the container + _bt_device_container = _bt_page; + + // Create status item (unselectable, shows scan state) + _bt_status_item = addMenuItem(_bt_device_container, "Initializing..."); + lv_obj_add_state(_bt_status_item, LV_STATE_DISABLED); + lv_obj_clear_state(_bt_status_item, LV_STATE_FOCUSED); + + // Create Back button (always visible) + lv_obj_t* back_btn = addMenuItem(_bt_device_container, "Back"); + lv_obj_add_event_cb(back_btn, bt_device_click_cb, LV_EVENT_CLICKED, NULL); + // Set initial focus on Back button (will be moved to first device when they appear) + lv_obj_add_state(back_btn, LV_STATE_FOCUSED); } - // Get device list - ESP_LOGI(TAG, "Getting device list"); + // Get device list first to decide if we should scan bt_device_list_t* device_list = bt_get_device_list(); if (!device_list) { ESP_LOGE(TAG, "Failed to get device list"); + update_bt_status("Error"); return _bt_page; } - - 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; - } + + // Only auto-start discovery if requested AND there are no paired devices + bool discovery_started = false; + if (start_discovery && device_list->count == 0) { + ESP_LOGI(TAG, "Starting Bluetooth discovery (no paired devices)"); + update_bt_status("Scanning..."); + _bt_scan_start_time = xTaskGetTickCount(); // Record when scan started + discovery_started = bt_start_discovery(); + + if (!discovery_started) { + ESP_LOGW(TAG, "Discovery not started"); + update_bt_status("Scan failed"); + _bt_scan_start_time = 0; } } - - // 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); + // Populate with current device list + if (device_list->count > 0) { + // Clear focus from Back button, will be set on first device + lv_obj_t* back_btn = lv_obj_get_child(_bt_device_container, 1); // Index 1 is Back (after status) + if (back_btn) { + lv_obj_clear_state(back_btn, LV_STATE_FOCUSED); + } + populate_bt_device_list(); + } else if (!discovery_started) { + update_bt_status("No devices found"); + // Update menu context since Back button has focus + currentFocusIndex(&_menuContext); + } else { + // Scanning in progress, Back button has focus + currentFocusIndex(&_menuContext); } - 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; } @@ -903,6 +1123,9 @@ static void show_bt_device_list(void) { _currentPage = bt_page; _mode = GUI_MENU; // Keep in menu mode + // Initialize menu context for this page + currentFocusIndex(&_menuContext); + menu_hide_headers(menu); @@ -916,35 +1139,11 @@ static void show_bt_device_list(void) { } 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, "Refreshing Bluetooth device list with discovered devices"); + // Clear old devices and repopulate with discovered devices + clear_bt_device_list(); + populate_bt_device_list(); ESP_LOGI(TAG, "Bluetooth device list refreshed"); } @@ -1345,13 +1544,20 @@ static void gui_task(void *pvParameters) 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(); + // Discovery completed - refresh the BT page if we're on it and scan has been running long enough + // Ignore spurious early "complete" events from stopping old discovery (must be > 500ms since start) + if (_mode == GUI_MENU && _currentPage == _bt_page && _bt_scan_start_time > 0) { + TickType_t elapsed = xTaskGetTickCount() - _bt_scan_start_time; + if (elapsed > pdMS_TO_TICKS(500)) { + ESP_LOGI(TAG, "Discovery complete after %dms, refreshing Bluetooth page", (int)pdTICKS_TO_MS(elapsed)); + LOCK(); + _bt_scan_start_time = 0; // Clear scan timestamp + // Recreate the BT page with updated device list (without starting new discovery) + refresh_bt_device_list(); + UNLOCK(); + } else { + ESP_LOGI(TAG, "Ignoring spurious discovery complete event (only %dms elapsed)", (int)pdTICKS_TO_MS(elapsed)); + } } } diff --git a/main/system.c b/main/system.c index 944b1d6..d1a0430 100644 --- a/main/system.c +++ b/main/system.c @@ -22,12 +22,13 @@ void system_init(void) _systemState.zeroAngle = 0.0f; _systemState.primaryAxis = PRIMARY_AXIS; _systemState.pairedDeviceCount = 0; + _systemState.isCharging = false; _systemEvent = xEventGroupCreate(); _eventManager.count = 0; _eventManager.mutex = xSemaphoreCreateMutex(); - + system_initNvsService(); } @@ -48,13 +49,29 @@ float system_getAngle(void) xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); - + angle = _systemState.imu.raw[_systemState.primaryAxis] - _systemState.zeroAngle; xSemaphoreGive(_eventManager.mutex); - return angle; + return angle; +} + +void system_setChargeStatus(bool charging) +{ + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + _systemState.isCharging = charging; + xSemaphoreGive(_eventManager.mutex); +} + +bool system_getChargeStatus(void) +{ + bool charging; + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + charging = _systemState.isCharging; + xSemaphoreGive(_eventManager.mutex); + return charging; } void system_setZeroAngle(void) @@ -349,16 +366,16 @@ void system_processNvsRequests(void) { } break; } - + default: request.result = ESP_ERR_INVALID_ARG; break; } xSemaphoreGive(_eventManager.mutex); - - request.response_ready = true; + + // Send the result directly as the notification value (not a pointer) if (request.requestor) { - xTaskNotify(request.requestor, (uint32_t)&request, eSetValueWithOverwrite); + xTaskNotify(request.requestor, (uint32_t)request.result, eSetValueWithOverwrite); } } } @@ -396,8 +413,8 @@ esp_err_t system_savePairedDevice(const paired_device_t *device) { if (xQueueSend(_nvsRequestQueue, &request, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) { uint32_t notification; if (xTaskNotifyWait(0, UINT32_MAX, ¬ification, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) { - nvs_request_t *response = (nvs_request_t*)notification; - return response->result; + // notification contains the result directly (not a pointer) + return (esp_err_t)notification; } } return ESP_ERR_TIMEOUT; @@ -425,8 +442,8 @@ esp_err_t system_removePairedDevice(esp_bd_addr_t bda) { if (xQueueSend(_nvsRequestQueue, &request, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) { uint32_t notification; if (xTaskNotifyWait(0, UINT32_MAX, ¬ification, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) { - nvs_request_t *response = (nvs_request_t*)notification; - return response->result; + // notification contains the result directly (not a pointer) + return (esp_err_t)notification; } } return ESP_ERR_TIMEOUT; @@ -464,8 +481,8 @@ esp_err_t system_updateConnectionTimestamp(esp_bd_addr_t bda) { if (xQueueSend(_nvsRequestQueue, &request, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) { uint32_t notification; if (xTaskNotifyWait(0, UINT32_MAX, ¬ification, pdMS_TO_TICKS(NVS_TIMEOUT_MS)) == pdTRUE) { - nvs_request_t *response = (nvs_request_t*)notification; - return response->result; + // notification contains the result directly (not a pointer) + return (esp_err_t)notification; } } return ESP_ERR_TIMEOUT; @@ -473,10 +490,26 @@ esp_err_t system_updateConnectionTimestamp(esp_bd_addr_t bda) { const paired_device_t* system_getPairedDevices(size_t *count) { if (!count) return NULL; - + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); *count = _systemState.pairedDeviceCount; xSemaphoreGive(_eventManager.mutex); - + return (const paired_device_t*)_systemState.pairedDevices; +} + +esp_err_t system_clearAllPairedDevices(void) { + // Directly clear in-memory state and save to NVS + xSemaphoreTake(_eventManager.mutex, portMAX_DELAY); + _systemState.pairedDeviceCount = 0; + esp_err_t ret = nvs_save_devices_internal(_systemState.pairedDevices, 0); + xSemaphoreGive(_eventManager.mutex); + + if (ret == ESP_OK) { + ESP_LOGI("system", "Cleared all paired devices from NVS"); + } else { + ESP_LOGE("system", "Failed to clear paired devices: %s", esp_err_to_name(ret)); + } + + return ret; } \ No newline at end of file diff --git a/main/system.h b/main/system.h index 3e6e029..cdc87e2 100644 --- a/main/system.h +++ b/main/system.h @@ -47,11 +47,14 @@ typedef struct SystemState_s // BT event data int btDeviceIndex; - + // NVS cached data paired_device_t pairedDevices[MAX_PAIRED_DEVICES]; size_t pairedDeviceCount; + // Charge status + bool isCharging; + } SystemState_t; @@ -67,6 +70,7 @@ typedef struct SystemState_s #define EM_EVENT_BT_CONNECT (1UL<<3) #define EM_EVENT_VOLUME_UP (1UL<<4) #define EM_EVENT_VOLUME_DOWN (1UL<<5) +#define EM_EVENT_BT_DISCOVERY_COMPLETE (1UL<<6) // …add more event bit-masks here… typedef struct { @@ -83,6 +87,9 @@ ImuData_t system_getImuData(void); int system_getPrimaryAxis(void); float system_getAngle(void); +void system_setChargeStatus(bool charging); +bool system_getChargeStatus(void); + void system_setZeroAngle(void); void system_clearZeroAngle(void); float system_getZeroAngle(void); @@ -133,6 +140,7 @@ bool system_isDeviceKnown(esp_bd_addr_t bda); esp_err_t system_getKnownDeviceCount(size_t *count); esp_err_t system_updateConnectionTimestamp(esp_bd_addr_t bda); const paired_device_t* system_getPairedDevices(size_t *count); +esp_err_t system_clearAllPairedDevices(void); #define NVS_TIMEOUT_MS 5000