Bluetooth
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:
- Stop and restart BLE advertising after changing the device name.
- Make sure the new name has been written to the advertising data or scan response data before advertising starts.
- Avoid changing the name dynamically while advertising is running unless advertising is restarted afterward.
- 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_EVTcallback to set the preferred PHY to2M:
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.4and5.x. - This only sets the preferred PHY. Whether the PHY actually switches to
2Mstill depends on whether the peer also supports2M 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

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.
