Bluetooth

  • BLE
  • BT

What is the maximum number of service profiles in a GATT server, and how is it configured?

Set it in menuconfig under:

Component config -> Bluetooth -> Bluedroid Options -> Bluetooth Low Energy -> Include GATT server module (GATTS)

The configurable range is 1 to 32.

Why does a phone still show the old BLE device name after the name has been changed on ESP32?

This is normal. The main reason is that phones usually cache BLE advertising information.

In BLE, the device name is typically sent in the advertising packet or scan response packet. After a phone first discovers the device, it often caches the device name for faster display in later scans.

Even if the ESP32 side has already updated the device name, the phone may still display the old name if:

  • advertising was not restarted
  • the advertising payload did not change noticeably
  • the phone did not actively refresh its cache

Recommended handling:

  1. Stop and restart BLE advertising after changing the device name.
  2. Make sure the new name has been written to the advertising data or scan response data before advertising starts.
  3. Avoid changing the name dynamically while advertising is running unless advertising is restarted afterward.
  4. On the phone side, turn Bluetooth off and on again, or clear the system Bluetooth cache before scanning again.

Different phone vendors and OS versions may use different cache strategies, so behavior can vary.

How can BLE 2M PHY be enabled on ESP32-C2/C3/S3?

1. On ESP32-C3 with the Bluedroid stack, BLE PHY defaults to 1M.

To switch to 2M PHY under Bluedroid:

  • Call the following API after the ESP_GATTS_CONNECT_EVT callback to set the preferred PHY to 2M:
esp_ble_gap_set_prefered_default_phy(
    ESP_BLE_GAP_PHY_2M_PREF_MASK,
    ESP_BLE_GAP_PHY_2M_PREF_MASK
);
  • Supported in ESP-IDF 4.4 and 5.x.
  • This only sets the preferred PHY. Whether the PHY actually switches to 2M still depends on whether the peer also supports 2M PHY.

2. On ESP32-C3 with the NimBLE stack, 2M PHY can be enabled in menuconfig:

Component config -> Bluetooth -> NimBLE Options -> BLE 5.x Features -> Enable BLE 5 feature

menuconfig_Enable_2M_Phy

In the ble_spp_server example, both the phone app and ESP32-C2 set the MTU to 512. Why is a payload of about 200 bytes still split into 3 packets, and how can it be received as one complete packet?

In the uart_task implementation of the BLE SPP Server example, transparent transmission is handled segment by segment. A serial tool may send a large block of data at once, for example 500 bytes, but UART reception on the module side is triggered in chunks due to reasons such as buffer thresholds or timer-based events. Each UART event may therefore contain only tens to hundreds of bytes, and the example immediately forwards each received chunk to the app through esp_ble_gatts_send_indicate().

As a result, even if the MTU is large enough, the app can still receive multiple fragmented packets whenever the UART side is segmented. That is the root cause of the packet splitting behavior.

The recommended approach is to add packet aggregation on the module side. Buffer the consecutively received UART data until a specific condition is met, such as the buffer becoming full or an end character like \n being received, and then send the whole buffer to the app in a single BLE notification.

// 1. Add a flexible aggregation buffer and an end character
#define SPP_BLE_BUF_SIZE    600
#define SPP_BLE_END_CHAR    '\n'
static uint8_t spp_ble_cache[SPP_BLE_BUF_SIZE];
static uint16_t spp_ble_cache_len = 0;

// 2. Modify uart_task to aggregate data and send on the end character
void uart_task(void *pvParameters)
{
    uart_event_t event;

    for (;;) {
        if (xQueueReceive(spp_uart_queue, (void *)&event, (TickType_t)portMAX_DELAY)) {
            switch (event.type) {
            case UART_DATA:
                if (event.size && is_connected) {
                    uint8_t *temp = (uint8_t *)malloc(event.size);
                    if (!temp) {
                        ESP_LOGE(GATTS_TABLE_TAG, "%s malloc failed", __func__);
                        break;
                    }
                    uart_read_bytes(UART_NUM_0, temp, event.size, portMAX_DELAY);

                    // Aggregate packets flexibly
                    for (int i = 0; i < event.size; ++i) {
                        // Append to the aggregation buffer
                        if (spp_ble_cache_len < SPP_BLE_BUF_SIZE) {
                            spp_ble_cache[spp_ble_cache_len++] = temp[i];
                            // Notify BLE once the end character is seen
                            if (temp[i] == SPP_BLE_END_CHAR) {
                                // Check whether BLE notifications are enabled
                                if (enable_data_ntf) {
                                    ESP_LOGI(GATTS_TABLE_TAG, "Aggregated BLE packet, cache len=%d, MTU=%d", spp_ble_cache_len, spp_mtu_size);
                                    esp_ble_gatts_send_indicate(
                                        spp_gatts_if,
                                        spp_conn_id,
                                        spp_handle_table[SPP_IDX_SPP_DATA_NTY_VAL],
                                        spp_ble_cache_len,
                                        spp_ble_cache,
                                        false
                                    );
                                }
                                spp_ble_cache_len = 0; // Clear the cache after notification
                            }
                        } else {
                            ESP_LOGW(GATTS_TABLE_TAG, "BLE UART cache overflow, clearing automatically");
                            spp_ble_cache_len = 0;
                        }
                    }
                    free(temp);
                }
                break;
            default:
                break;
            }
        }
    }
    vTaskDelete(NULL);
}

How can NimBLE set the MTU at runtime, for example to 256?

Call the following API in code to set the preferred MTU:

ble_att_set_preferred_mtu(256);

This API sets the preferred MTU size in bytes. The actual MTU used during communication still depends on the smaller value negotiated by the two devices.

ble_att_set_preferred_mtu