Fix Bluetooth pairing, menu crashes, and improve UX

Major fixes and improvements to Bluetooth device management and menu navigation:

**Bluetooth Device Pairing**
- Fixed discovered devices not being saved as paired after connection
- Only save devices to NVS when successfully connected (not just discovered)
- Auto-pair discovered devices on successful A2DP connection
- Update device list to show paired status immediately after connection

**Critical Bug Fixes**
- Fixed dangling pointer crash in NVS request/response mechanism
  - Was sending pointer to stack variable, now sends result value directly
  - Prevents crash when connecting to Bluetooth devices
- Fixed use-after-free crash when clicking menu items after dynamic updates
  - Menu context now properly synchronized after adding/removing items
  - Prevents InstructionFetchError crashes in menu navigation
- Fixed memory exhaustion by reducing MAX_BT_DEVICES from 20 to 8
  - Prevents heap allocation failures when populating device list

**Menu & UX Improvements**
- "Clear Paired" button now properly disconnects active connections
- "Clear Paired" button always visible when paired devices exist
- GUI updates immediately after clearing paired devices
- Paired devices marked with asterisk prefix (* Device Name)
- Removed redundant "(paired)" suffix text
- Long device names scroll smoothly when selected (3-second animation)
- Refresh button preserved during menu updates to prevent crashes
- Menu focus state properly maintained across all dynamic updates

**Technical Details**
- bt_add_discovered_device() no longer saves to NVS
- Added currentFocusIndex() calls after all menu modifications
- Improved clear_bt_device_list() to avoid deleting active buttons
- Added bt_disconnect_current_device() for clean disconnections
- Fixed NVS notification mechanism to avoid stack variable pointers

🤖 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-14 11:39:58 -06:00
parent 115105c032
commit b8a3a09e9f
5 changed files with 480 additions and 184 deletions

View File

@@ -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");