Chapter 69: Dual-mode Bluetooth Applications (Classic + LE)
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand what dual-mode Bluetooth means and which ESP32 variants support it.
- Initialize the ESP32 Bluetooth controller for both Bluetooth Classic (BR/EDR) and BLE operations.
- Manage and register callbacks for both Bluetooth Classic profiles (e.g., SPP, A2DP) and BLE GATT services.
- Implement an application that simultaneously advertises BLE services and is discoverable for Bluetooth Classic connections.
- Understand potential resource sharing and coexistence considerations between Classic and LE modes.
- Develop a basic ESP32 application demonstrating concurrent SPP (Classic) and GATT (BLE) functionality.
Introduction
Bluetooth technology has evolved significantly, leading to two main branches: Bluetooth Classic (often referred to by its underlying technologies, Basic Rate/Enhanced Data Rate or BR/EDR) and Bluetooth Low Energy (BLE). Bluetooth Classic is well-suited for streaming applications like audio (A2DP) or continuous data transfer (SPP). BLE, on the other hand, excels in low-power applications, short data bursts, and beaconing.
Feature | Bluetooth Classic (BR/EDR) | Bluetooth Low Energy (BLE) | Relevance in Dual-Mode |
---|---|---|---|
Primary Use Case | Streaming (audio, continuous data) | Low power, short data bursts, beacons | Combines high throughput with power efficiency for versatile applications. |
Data Throughput | Higher (e.g., ~1-3 Mbps for EDR) | Lower (e.g., up to 2 Mbps in BLE 5) | Allows selection of appropriate mode based on data needs. |
Power Consumption | Higher | Significantly Lower | Enables battery-powered devices to use Classic for demanding tasks and BLE for idle/low-data states. |
Connection Topology | Connection-oriented (piconets, scatternets) | Connection-oriented and connectionless (advertising) | Supports diverse interaction models, from continuous streams to periodic updates. |
Common Profiles/Services | SPP, A2DP, HFP, HID | GATT-based services (e.g., Heart Rate, Proximity) | Can interact with legacy Classic devices and modern BLE peripherals simultaneously. |
Discovery | Inquiry / Page | Advertising / Scanning | Device can be discoverable and connectable using both methods. |
Pairing/Bonding | Legacy Pairing, Secure Simple Pairing (SSP) | LE Legacy Pairing, LE Secure Connections | Security managed for both, potentially with cross-transport key derivation in some cases. |
A “dual-mode” Bluetooth chip is one that can support both Bluetooth Classic and BLE functionalities concurrently. This capability offers tremendous versatility, allowing a device to connect to legacy Bluetooth Classic devices while also leveraging the power efficiency and modern features of BLE. For instance, a smart speaker might use A2DP (Classic) for audio streaming from a phone and simultaneously use BLE to communicate with low-power sensors or provide a configuration interface via a mobile app.
%%{init: {'theme': 'base', 'themeVariables': {'fontFamily': 'Open Sans'}}}%% graph TD; subgraph Single Chip direction LR SC[Shared Radio/Antenna Hardware] --> BC(Bluetooth Controller); end BC --> CS["Bluetooth Classic Stack <br> (BR/EDR)"]; BC --> BS["Bluetooth Low Energy Stack <br> (BLE)"]; CS --> CP[Classic Profiles <br> e.g., SPP, A2DP]; BS --> BP[BLE Services <br> e.g., GATT based]; subgraph Applications APP1["Application Logic 1 <br> (e.g., Audio Streaming)"]; APP2["Application Logic 2 <br> (e.g., Sensor Data)"]; end CP --> APP1; BP --> APP2; classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px,font-family:'Open Sans'; classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef app fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1px,color:#0369A1; class SC,BC primary; class CS,BS,CP,BP process; class APP1,APP2 app;
This chapter focuses on developing dual-mode Bluetooth applications on the ESP32. It is crucial to note that only the original ESP32 series supports Bluetooth Classic. Newer variants like the ESP32-S3, -C3, -C6, and -H2 are BLE-only or combine BLE with other radio technologies like IEEE 802.15.4, but they do not have Bluetooth Classic capabilities. Thus, the practical implementations discussed here are specific to the original ESP32. We will explore how to initialize the ESP32 for dual-mode operation and manage both protocol stacks within a single application.
Theory
What is Dual-Mode Bluetooth?
A dual-mode Bluetooth device integrates two distinct radio modes on a single chip, managed by a shared Bluetooth controller:
- Bluetooth Classic (BR/EDR): This is the “original” Bluetooth, designed for relatively high-throughput, connection-oriented communication. Common profiles include:
- SPP (Serial Port Profile): Emulates a serial cable connection for data exchange.
- A2DP (Advanced Audio Distribution Profile): For streaming stereo audio.
- HFP (Hands-Free Profile): For voice calls.
- Bluetooth Low Energy (BLE): Designed for very low power consumption, suitable for applications that require short bursts of data or operate for long periods on small batteries. It uses the GATT (Generic Attribute Profile) for data exchange.
A dual-mode chip can, for example, maintain a BLE connection with a heart rate sensor while simultaneously streaming music to Bluetooth Classic headphones.
Architecture and Resource Sharing
On an ESP32 (original series), a single physical radio antenna and parts of the Bluetooth controller hardware are shared between Classic and LE operations. The Bluetooth host stack (Bluedroid in ESP-IDF) manages this sharing and the scheduling of radio time for each mode.
- Controller: The Link Controller (LC) manages the low-level communication, including scheduling packet transmission and reception for both BR/EDR and LE links.
- Host Stack (Bluedroid): Provides APIs for both Classic profiles and BLE GATT services. It handles the higher-level protocol logic.
- Coexistence: The controller implements mechanisms to manage coexistence, attempting to schedule Classic and LE activities to minimize interference. However, intense activity in one mode can potentially impact the performance (latency, throughput) of the other.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% graph TD A["Application Layer <br> (User Code)"] --> HS; subgraph ESP32 Hardware & Low-Level Control RFA[Shared Radio & Antenna] --> CTRL["Shared Bluetooth Link Controller <br> (Manages BR/EDR & LE Links, Coexistence)"]; end CTRL --> HS[Bluedroid Host Stack]; subgraph Bluedroid Host Stack direction LR subgraph "Classic Bluetooth (BR/EDR)" GAP_C[Classic GAP] --> SPP[SPP Profile]; GAP_C --> A2DP[A2DP Profile]; GAP_C --> HFP[HFP Profile]; GAP_C --> Other_C[Other Classic Profiles...]; end subgraph "Bluetooth Low Energy (BLE)" GAP_L["BLE GAP <br> (Advertising, Scanning)"] --> GATT["GATT Layer <br> (Server/Client)"]; GATT --> GS[GATT Services & Characteristics]; end HS --> GAP_C; HS --> GAP_L; end classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px,font-family:'Open Sans'; classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; %% Radio, Controller classDef hostStack fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF; %% Bluedroid, GAP_C, GAP_L, GATT classDef profile fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46; %% SPP, A2DP, HFP, Other_C, GS classDef appLayer fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#92400E; %% Application Layer class RFA,CTRL primary; class HS,GAP_C,GAP_L,GATT hostStack; class SPP,A2DP,HFP,Other_C,GS profile; class A appLayer;
Use Cases for Dual-Mode
- Smart Hubs/Gateways: Connecting to legacy Classic sensors or devices while also managing a network of BLE sensors.
- Audio Devices: A speaker playing audio via A2DP (Classic) while offering BLE control for settings or receiving notifications.
- Automotive: A car infotainment system connecting to a phone via HFP/A2DP (Classic) for calls/music, and using BLE for keyless entry or diagnostics.
- Transitional Devices: Products that need to support older Classic-only devices while also being future-proof with BLE.
- Development/Prototyping: Allowing a single ESP32 to interact with a wide range of Bluetooth peripherals during development.
Use Case Category | Bluetooth Classic Role (BR/EDR) | Bluetooth Low Energy (BLE) Role | Example Scenario |
---|---|---|---|
Smart Hubs / Gateways | Connect to legacy Classic sensors/devices (e.g., older weather stations via SPP). | Manage a network of modern BLE sensors (e.g., temperature, motion). Provide BLE configuration interface. | An ESP32 hub collects data from both old Classic and new BLE environmental sensors. |
Audio Devices | Streaming audio (A2DP), voice calls (HFP). | Device settings control (e.g., EQ, volume), notifications, firmware updates, pairing assistance. | Smart speaker streams music from a phone (A2DP) and uses a BLE app for settings. |
Automotive Infotainment | Hands-free calls (HFP), music streaming (A2DP) from phone. | Keyless entry, tire pressure monitoring, diagnostics, short-range data exchange with phone apps. | Car system connects to phone for calls/music (Classic) and uses BLE for passive keyless entry. |
Transitional Devices | Maintain compatibility with existing Classic-only peripherals. | Offer modern BLE connectivity for new features and future-proofing. | A medical device supports data transfer to older monitoring systems via SPP (Classic) while also offering BLE connectivity to new mobile health apps. |
Development & Prototyping | Interact with a wide range of Classic peripherals for testing. | Interact with a wide range of BLE peripherals for testing. | An ESP32 development board used to test communication with both a Classic Bluetooth GPS module and a BLE heart rate sensor. |
Wearables / Health Tech | Potentially for higher bandwidth data transfer if needed (though less common now). | Primary communication for sensor data, device status, notifications, configuration. | A smartwatch might (theoretically) use Classic for a specific high-data task while relying on BLE for continuous health tracking and phone notifications. (Most smartwatches are heavily BLE-focused). |
ESP-IDF Support for Dual-Mode
The ESP-IDF, through its Bluedroid host stack, provides support for dual-mode operation on the original ESP32.
- Initialization: The Bluetooth controller can be initialized in a mode that supports both BR/EDR and LE (
ESP_BT_MODE_BTDM
). - API Separation: APIs for Bluetooth Classic (e.g.,
esp_spp_api.h
,esp_a2dp_api.h
) are distinct from BLE APIs (esp_gatts_api.h
,esp_gattc_api.h
,esp_gap_ble_api.h
). - Event Handling: Your application will need to register and handle callbacks for events from both Classic profiles and BLE GAP/GATT events.
%%{init: {"theme": "base", "themeVariables": { "fontFamily": "Open Sans" }}}%% graph TD Start(("Start: app_main")) --> NVS["Initialize NVS <br> <span class='mono'>nvs_flash_init()</span>"]; NVS --> RelMem["Optional: Release Classic BT memory <br> <span class='mono'>esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)</span> <br> (if not already BTDM or switching modes)"]; RelMem --> BTCfg["Configure BT Controller <br> <span class='mono'>esp_bt_controller_config_t</span>"]; BTCfg --> BTInit["Initialize BT Controller <br> <span class='mono'>esp_bt_controller_init(&bt_cfg)</span>"]; BTInit --> BTEnable{"Enable BT Controller <br> in Dual Mode? <br> <span class='mono'>esp_bt_controller_enable(ESP_BT_MODE_BTDM)</span>"}; BTEnable -- "Success" --> BDInit["Initialize Bluedroid Stack <br> <span class='mono'>esp_bluedroid_init()</span>"]; BDInit --> BDEnable{"Enable Bluedroid Stack? <br> <span class='mono'>esp_bluedroid_enable()</span>"}; BDEnable -- "Success" --> RegBLE["Register BLE Callbacks <br> (GAP & GATTS) <br> <span class='mono'>esp_ble_gap_register_callback()</span> <br> <span class='mono'>esp_ble_gatts_register_callback()</span>"]; RegBLE --> RegBLEApp["Register BLE Application Profile <br> <span class='mono'>esp_ble_gatts_app_register()</span>"]; RegBLEApp --> RegClassic["Register Classic Callbacks <br> (e.g., SPP, A2DP) <br> <span class='mono'>esp_spp_register_callback()</span>"]; RegClassic --> InitClassic["Initialize Classic Profiles <br> (e.g., SPP) <br> <span class='mono'>esp_spp_init()</span>"]; InitClassic --> PostInit["Post-Initialization Steps <br> (e.g., Set BLE MTU, Start BLE Adv, Set Classic Discoverable)"]; PostInit --> Ready(("Dual Mode Ready")); BTEnable -- "Failure" --> Error1["Handle BT Controller Enable Error"]; BDEnable -- "Failure" --> Error2["Handle Bluedroid Enable Error"]; Error1 --> End(("End")); Error2 --> End(("End")); classDef mono fill:none,color:inherit,font-family:monospace,font-size:0.9em; classDef startEnd fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; class Start,Ready startEnd; class NVS,RelMem,BTCfg,BTInit,BDInit,RegBLE,RegBLEApp,RegClassic,InitClassic,PostInit process; class BTEnable,BDEnable decision; class Error1,Error2 error; class End error;
Discovery and Connection in Dual-Mode
A dual-mode ESP32 can simultaneously:
- Be discoverable for Bluetooth Classic: Using GAP inquiry scan, allowing other Classic devices to find and pair/connect with it (e.g., for SPP or A2DP).
- Advertise BLE services: Using BLE GAP advertising, allowing BLE central devices to discover its GATT services and connect.
It can also initiate connections in both modes:
- Initiate Classic connections: Discover and connect to other Classic devices.
- Initiate BLE connections: Scan for and connect to BLE peripherals.
Security Considerations
Security mechanisms for Classic Bluetooth (pairing, link keys) and BLE (pairing, bonding, LE Secure Connections) are distinct but managed by the same underlying security manager in Bluedroid. When a device pairs over one transport (Classic or LE), bonding information might sometimes be shared or leveraged for the other transport if both devices are dual-mode and support cross-transport key derivation, but this is an advanced topic. Generally, you manage security for each mode somewhat independently at the application level.
Security Aspect | Bluetooth Classic (BR/EDR) | Bluetooth Low Energy (BLE) | Dual-Mode Considerations |
---|---|---|---|
Pairing Methods | Legacy Pairing (PIN code), Secure Simple Pairing (SSP) using methods like Just Works, Numeric Comparison, Passkey Entry. | LE Legacy Pairing (similar to Classic PIN but for LE), LE Secure Connections (using Elliptic Curve Diffie-Hellman, ECDH). | Devices may pair separately over each transport. Bluedroid manages keys. |
Encryption | Based on E0 stream cipher (up to 128-bit). Strength depends on pairing method. | AES-CCM (128-bit). LE Secure Connections significantly enhances key exchange. | Encryption is transport-specific. |
Bonding | Storing link keys for future connections without re-pairing. | Storing Long Term Keys (LTK) and other security information. | Bonding information is generally managed per transport. Cross-transport key derivation is possible if both devices support it, allowing a bond on one transport to secure the other. |
Authentication | Verifying the identity of the connecting device, often part of pairing. | Verifying identity, strengthened by LE Secure Connections. Signed data can also be used. | Authentication procedures are distinct for Classic and LE. |
Privacy | Less emphasis on privacy in older specifications. Device address (BD_ADDR) is static. | Resolvable Private Addresses (RPAs) to prevent tracking. Identity Resolving Key (IRK) used. | BLE privacy features (RPAs) do not apply to Classic Bluetooth communication. |
ESP-IDF Management | Managed by Bluedroid stack. Security modes and pairing parameters configurable. | Managed by Bluedroid stack. Security parameters (IOCAP, auth_req) configurable for GAP and GATTS. | Application needs to configure security for both Classic profiles (e.g., SPP security mode) and BLE services appropriately. |
Practical Examples
Prerequisites:
- An original ESP32 board (e.g., ESP32-WROOM-32, ESP32-DevKitC). This chapter’s code will NOT work on ESP32-S3, C3, C6, H2 as they lack Bluetooth Classic.
- VS Code with the Espressif IDF Extension.
- ESP-IDF v5.x.
- A peer device capable of Bluetooth Classic (e.g., a smartphone or PC for SPP testing) AND a peer device capable of BLE (e.g., a smartphone with a BLE scanner app like nRF Connect). Often, one smartphone can serve both roles.
Example 1: Basic Dual-Mode Initialization and Operation (SPP Server + BLE GATT Server)
This example demonstrates initializing the ESP32 in dual-mode, setting up an SPP server for Classic Bluetooth communication, and a simple GATT server for BLE communication.
1. Project Setup:
- Create a new ESP-IDF project:
idf.py create-project dual_mode_example
cd dual_mode_example
idf.py menuconfig
:Component config
->Bluetooth
->Bluetooth
(Enable)Component config
->Bluetooth
->Bluetooth Host
->Bluedroid
(Select)Component config
->Bluetooth
->Bluetooth Controller
->Bluetooth controller mode (BR/EDR/BLE/DUALMODE)
-> SelectDUAL MODE (BR/EDR + BLE)
Component config
->Bluetooth
->Bluedroid Options
->Classic Bluetooth
(Enable)Component config
->Bluetooth
->Bluedroid Options
->SPP
(Enable)Component config
->Bluetooth
->BLE Only
(Ensure this is disabled if DUAL MODE is selected above, or ensure the controller mode is indeed DUAL MODE)Component config
->Bluetooth
->GATT
->Enable GATT server
(Enable)
Menuconfig Path | Option | Recommended Setting for Dual-Mode | Purpose |
---|---|---|---|
Component config → Bluetooth → Bluetooth | Bluetooth (Enable) | [*] (Enabled) | Globally enables Bluetooth functionality. |
Component config → Bluetooth → Bluetooth Host | Bluetooth Host | (X) Bluedroid | Selects the Bluedroid host stack, which supports dual-mode. |
Component config → Bluetooth → Bluetooth Controller | Bluetooth controller mode (BR/EDR/BLE/DUALMODE) | (X) DUAL MODE (BR/EDR + BLE) | Crucial: Configures the controller to support both Classic and BLE operations. (e.g., CONFIG_BT_CONTROLLER_MODE_BTDM=y) |
Component config → Bluetooth → Bluedroid Options | Classic Bluetooth | [*] (Enable) | Enables Classic Bluetooth (BR/EDR) protocol stack within Bluedroid. |
Component config → Bluetooth → Bluedroid Options | SPP | [*] (Enable) (If using SPP) | Enables the Serial Port Profile for Classic Bluetooth. |
Component config → Bluetooth → Bluedroid Options | A2DP | [*] (Enable) (If using A2DP) | Enables the Advanced Audio Distribution Profile. |
Component config → Bluetooth → Bluedroid Options | BLE Only | [ ] (Ensure Disabled) | This should be disabled if DUAL MODE is selected for the controller. Enabling this would conflict. |
Component config → Bluetooth → GATT | Enable GATT server | [*] (Enable) (If providing BLE services) | Enables the GATT server functionality for BLE. |
Component config → Bluetooth → GATT | Enable GATT client | [*] (Enable) (If connecting to other BLE devices) | Enables the GATT client functionality for BLE. |
2. Code (main/dual_mode_main.c):
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_spp_api.h" // For Classic SPP
#include "esp_bt_device.h" // For esp_bt_dev_set_device_name
#define DUAL_MODE_TAG "DUAL_MODE_APP"
#define ESP_BLE_DEVICE_NAME "ESP32_DUAL_BLE"
#define ESP_CLASSIC_DEVICE_NAME "ESP32_DUAL_SPP"
// BLE GATT Service and Characteristic UUIDs (example)
#define GATTS_SERVICE_UUID_TEST 0x00FF
#define GATTS_CHAR_UUID_TEST_A 0xFF01
#define GATTS_NUM_HANDLE_TEST 4
// BLE Variables
static uint8_t char1_str[] = {0x11, 0x22, 0x33};
static esp_gatt_char_prop_t test_a_property = 0;
static esp_attr_value_t gatts_demo_char1_val = {
.attr_max_len = sizeof(char1_str),
.attr_len = sizeof(char1_str),
.attr_value = char1_str,
};
static uint16_t gatts_handle_table[GATTS_NUM_HANDLE_TEST];
// SPP Variables
#define SPP_SERVER_NAME "SPP_SERVER_DUAL"
static uint32_t spp_handle = 0; // Store SPP connection handle
// BLE Advertising Parameters
static esp_ble_adv_params_t adv_params = {
.adv_int_min = 0x20,
.adv_int_max = 0x40,
.adv_type = ADV_TYPE_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
static esp_ble_adv_data_t adv_data = {
.set_scan_rsp = false,
.include_name = true,
.include_txpower = true,
.min_interval = 0x0006,
.max_interval = 0x0010,
.appearance = 0x00,
.manufacturer_len = 0,
.p_manufacturer_data = NULL,
.service_data_len = 0,
.p_service_data = NULL,
.service_uuid_len = 0, // Set later if needed
.p_service_uuid = NULL,
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), // BREDR_NOT_SPT is important for dual mode
};
/* BLE GATTS Event Handler */
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_REG_EVT:
ESP_LOGI(DUAL_MODE_TAG, "BLE GATTS REG_EVT, status %d, app_id %d", param->reg.status, param->reg.app_id);
esp_ble_gap_set_device_name(ESP_BLE_DEVICE_NAME); // Set BLE device name
esp_ble_gap_config_adv_data(&adv_data);
esp_ble_gatts_create_service(gatts_if, &((esp_gatt_srvc_id_t){.is_primary = true, .id = {.uuid = {.len = ESP_UUID_LEN_16, .uuid = {.uuid16 = GATTS_SERVICE_UUID_TEST}} }}), GATTS_NUM_HANDLE_TEST);
break;
case ESP_GATTS_CREATE_EVT:
ESP_LOGI(DUAL_MODE_TAG, "BLE GATTS CREATE_EVT, status %d, service_handle %d", param->create.status, param->create.service_handle);
gatts_handle_table[0] = param->create.service_handle; // SVC_IDX
esp_ble_gatts_start_service(gatts_handle_table[0]);
test_a_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY;
esp_ble_gatts_add_char(gatts_handle_table[0], &((esp_bt_uuid_t){.len = ESP_UUID_LEN_16, .uuid = {.uuid16 = GATTS_CHAR_UUID_TEST_A}}),
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
test_a_property,
&gatts_demo_char1_val, NULL);
break;
case ESP_GATTS_ADD_CHAR_EVT:
ESP_LOGI(DUAL_MODE_TAG, "BLE GATTS ADD_CHAR_EVT, status %d, attr_handle %d, service_handle %d",
param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);
gatts_handle_table[1] = param->add_char.attr_handle; // CHAR_A_IDX
// Add CCCD for notifications
esp_ble_gatts_add_char_descr(gatts_handle_table[0], &((esp_bt_uuid_t){.len = ESP_UUID_LEN_16, .uuid = {.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG}}),
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, NULL, NULL);
break;
case ESP_GATTS_ADD_CHAR_DESCR_EVT:
ESP_LOGI(DUAL_MODE_TAG, "BLE GATTS ADD_DESCR_EVT, status %d, attr_handle %d", param->add_char_descr.status, param->add_char_descr.attr_handle);
gatts_handle_table[2] = param->add_char_descr.attr_handle; // CCCD_A_IDX
break;
case ESP_GATTS_CONNECT_EVT:
ESP_LOGI(DUAL_MODE_TAG, "BLE GATTS CONNECT_EVT, conn_id %d", param->connect.conn_id);
esp_ble_gap_stop_advertising(); // Stop BLE advertising on connection
break;
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(DUAL_MODE_TAG, "BLE GATTS DISCONNECT_EVT, reason %d", param->disconnect.reason);
esp_ble_gap_start_advertising(&adv_params); // Restart BLE advertising
break;
case ESP_GATTS_WRITE_EVT:
ESP_LOGI(DUAL_MODE_TAG, "BLE GATTS WRITE_EVT, handle %d, value len %d:", param->write.handle, param->write.len);
esp_log_buffer_hex(DUAL_MODE_TAG, param->write.value, param->write.len);
// Handle writes to characteristic or CCCD
if (param->write.handle == gatts_handle_table[2]) { // CCCD for Char A
uint16_t cccd_val = (param->write.value[1] << 8) | param->write.value[0];
if (cccd_val == 0x0001) ESP_LOGI(DUAL_MODE_TAG, "Char A notifications ENABLED");
else ESP_LOGI(DUAL_MODE_TAG, "Char A notifications DISABLED");
}
if (param->write.need_rsp) {
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
}
break;
// ... other GATTS events
default:
break;
}
}
/* BLE GAP Event Handler */
static void ble_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
ESP_LOGI(DUAL_MODE_TAG, "BLE ADV_DATA_SET_COMPLETE, starting advertising");
esp_ble_gap_start_advertising(&adv_params);
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
ESP_LOGI(DUAL_MODE_TAG, "BLE Advertising started");
} else {
ESP_LOGE(DUAL_MODE_TAG, "BLE Advertising start failed");
}
break;
// ... other BLE GAP events
default:
break;
}
}
/* Classic Bluetooth SPP Event Handler */
static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
switch (event) {
case ESP_SPP_INIT_EVT:
ESP_LOGI(DUAL_MODE_TAG, "SPP_INIT_EVT");
esp_bt_dev_set_device_name(ESP_CLASSIC_DEVICE_NAME); // Set Classic BT device name
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE); // Make discoverable
esp_spp_start_srv(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE, 0, SPP_SERVER_NAME);
break;
case ESP_SPP_SRV_OPEN_EVT: // Server connection open
ESP_LOGI(DUAL_MODE_TAG, "SPP_SRV_OPEN_EVT, handle %"PRIu32, param->srv_open.handle);
spp_handle = param->srv_open.handle;
// Example: Send a welcome message
char *welcome_msg = "Welcome to ESP32 SPP Dual Mode!\r\n";
esp_spp_write(spp_handle, strlen(welcome_msg), (uint8_t *)welcome_msg);
break;
case ESP_SPP_DATA_EVT: // Data received
ESP_LOGI(DUAL_MODE_TAG, "SPP_DATA_EVT, len %d, handle %"PRIu32, param->data_ind.len, param->data_ind.handle);
esp_log_buffer_hex(DUAL_MODE_TAG, param->data_ind.data, param->data_ind.len);
// Echo data back
esp_spp_write(param->data_ind.handle, param->data_ind.len, param->data_ind.data);
break;
case ESP_SPP_CLOSE_EVT:
ESP_LOGI(DUAL_MODE_TAG, "SPP_CLOSE_EVT, handle %"PRIu32, param->close.handle);
spp_handle = 0;
break;
case ESP_SPP_START_EVT:
ESP_LOGI(DUAL_MODE_TAG, "SPP_START_EVT");
break;
// ... other SPP events
default:
break;
}
}
void app_main(void) {
esp_err_t ret;
// Initialize NVS
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// Initialize Bluetooth Controller in DUAL MODE
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); // Release classic BT memory if not using BTDM
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
// IMPORTANT: Set mode to DUAL (BR/EDR + BLE) in menuconfig.
// If configured for DUAL_MODE, this init will enable it.
ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "Initialize controller failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BTDM); // Enable DUAL MODE
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "Enable controller failed: %s", esp_err_to_name(ret));
return;
}
// Initialize Bluedroid Host Stack
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "Init Bluedroid failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret));
return;
}
// Register BLE GAP and GATTS callbacks
ret = esp_ble_gap_register_callback(ble_gap_event_handler);
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "BLE GAP register failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_ble_gatts_register_callback(gatts_event_handler);
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "BLE GATTS register failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_ble_gatts_app_register(0); // Single BLE app profile
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "BLE GATTS app register failed: %s", esp_err_to_name(ret));
return;
}
// Initialize and register SPP (Classic Bluetooth)
ret = esp_spp_register_callback(esp_spp_cb);
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "SPP register failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_spp_init(ESP_SPP_MODE_CB); // Callback mode
if (ret) {
ESP_LOGE(DUAL_MODE_TAG, "SPP init failed: %s", esp_err_to_name(ret));
return;
}
// Set MTU for BLE
esp_ble_gatt_set_local_mtu(500);
ESP_LOGI(DUAL_MODE_TAG, "Dual-mode application initialized. SPP server and BLE GATT server started.");
// BLE advertising will start after GATTS_REG_EVT.
// SPP server will start after SPP_INIT_EVT.
}
3. Build, Flash, and Observe:
idf.py build
idf.py -p (PORT) flash monitor
(Replace(PORT)
with your ESP32’s serial port)- Testing BLE:
- Use a BLE scanner app (e.g., nRF Connect for Mobile).
- Scan for devices. You should see “ESP32_DUAL_BLE”.
- Connect to it, discover services/characteristics. You should find service
0x00FF
and characteristic0xFF01
. - Try reading/writing to the characteristic and enabling notifications.
- Testing Classic SPP:
- Use a Bluetooth terminal app on your smartphone or PC (e.g., Serial Bluetooth Terminal on Android).
- Scan for Classic Bluetooth devices. You should see “ESP32_DUAL_SPP”.
- Pair and connect to it.
- Once connected, you should receive “Welcome to ESP32 SPP Dual Mode!” from the ESP32.
- Send some data from your terminal; the ESP32 should echo it back.
- Observe Serial Monitor: You will see logs from both BLE and SPP event handlers, indicating activities in both modes.
Tip: The
ESP_BLE_ADV_FLAG_BREDR_NOT_SPT
flag in BLE advertising data is important. It signals to a scanning dual-mode central device that this peripheral, while advertising LE, does not support BR/EDR discovery or connection over the LE physical channel (which is a specific feature). For general dual-mode operation where Classic and LE are somewhat independent, this flag is appropriate.
Variant Notes
ESP32 Variant | Bluetooth Classic (BR/EDR) Support | Bluetooth Low Energy (BLE) Support | Dual-Mode (Classic + LE) Capable | Other Radio Tech |
---|---|---|---|---|
ESP32 (Original Series, e.g., WROOM-32, WROVER) | Yes | Yes | Yes | Wi-Fi |
ESP32-S2 | No | No (Wi-Fi only) | No | Wi-Fi |
ESP32-S3 | No | Yes (BLE 5.0) | No (BLE only for Bluetooth) | Wi-Fi |
ESP32-C3 | No | Yes (BLE 5.0) | No (BLE only for Bluetooth) | Wi-Fi |
ESP32-C6 | No | Yes (BLE 5.3) | No (BLE only for Bluetooth) | Wi-Fi, IEEE 802.15.4 (Thread/Zigbee) |
ESP32-H2 | No | Yes (BLE 5.3) | No (BLE only for Bluetooth) | IEEE 802.15.4 (Thread/Zigbee) |
In summary, if your application requires Bluetooth Classic profiles, you must use the original ESP32 series. For applications needing only BLE or BLE combined with 802.15.4 protocols, the newer variants (S3, C3, C6, H2) are suitable.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Incorrect Controller Mode Configuration | Only one mode (Classic or BLE) works, or initialization fails for the other mode (e.g., errors like “controller enable failed”). Bluedroid init might fail. |
Ensure DUAL MODE (BR/EDR + BLE) is selected in menuconfig:
Component config → Bluetooth → Bluetooth Controller → Bluetooth controller mode. This sets CONFIG_BT_CONTROLLER_MODE_BTDM=y. In code, use esp_bt_controller_enable(ESP_BT_MODE_BTDM);. |
Using Dual-Mode Code on Non-Dual-Mode ESP32 Variants | Compilation errors (missing Classic BT APIs like esp_spp_api.h). Runtime errors or crashes when trying to initialize Classic components on ESP32-S3, -C3, -C6, -H2. |
Verify your ESP32 variant. Bluetooth Classic profiles (SPP, A2DP) are only supported on the original ESP32 series (e.g., ESP32-WROOM-32).
Refer to the “Variant Notes” section or the ESP32 Variant Capabilities table. |
Callback and Event Handling Confusion | Events for one mode are not received, incorrect handlers are triggered, or unexpected behavior during operations (e.g., SPP data not handled, BLE connection events missed). |
Maintain separate callback functions for BLE GAP/GATTS and Classic Bluetooth profiles.
Register them correctly: – esp_ble_gap_register_callback(), esp_ble_gatts_register_callback() for BLE. – esp_spp_register_callback() for SPP, etc. Ensure event switch-cases within handlers are comprehensive. |
Resource Conflicts / Performance Issues (Advanced) | Increased latency, reduced throughput in one or both modes. Unstable connections, especially under heavy load (e.g., simultaneous SPP data streaming and BLE advertising/data exchange). |
Be mindful of the shared radio. Bluedroid manages coexistence, but intense, concurrent activity can strain resources.
Test thoroughly under expected load. Consider prioritizing tasks or scheduling activities to reduce simultaneous peak demand. Check for sufficient task stack sizes for Bluetooth tasks. |
Incorrect Device Naming | Device appears with a generic name (e.g., “ESP32”) or an incorrect/unexpected name during Classic Bluetooth discovery or BLE scanning. |
Set device names separately for each mode:
– BLE: esp_ble_gap_set_device_name(“BLE_NAME”); (typically after ESP_GATTS_REG_EVT or ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT). – Classic BT: esp_bt_dev_set_device_name(“CLASSIC_NAME”); (typically after ESP_SPP_INIT_EVT or Bluedroid init). |
Forgetting nvs_flash_init() | Bluetooth initialization may fail, often with errors related to NVS (Non-Volatile Storage). ESP32 may reboot or behave erratically. |
Always initialize NVS at the beginning of app_main():
esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { … } |
BLE Advertising Flag ESP_BLE_ADV_FLAG_BREDR_NOT_SPT | If this flag is missing or incorrect in a dual-mode scenario, it might cause confusion for some central devices about how to connect or discover BR/EDR services. | For typical dual-mode where Classic and LE are independent, include (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT) in your BLE advertising data flags. This signals that BR/EDR is not supported *over the LE physical channel* for discovery/connection, guiding dual-mode centrals correctly. |
Memory Allocation Failures | Failures during esp_bt_controller_init(), esp_bluedroid_init(), or when creating services/profiles. “Out of memory” errors. |
Ensure enough heap memory is available.
In menuconfig: Component config → ESP System Settings → Memory protection → Enable memory protection (set to None if debugging memory issues, but be cautious). Check CONFIG_BT_CONTROLLER_HCI_MODE_VHCI. Release memory from unused modes if not using BTDM from the start: esp_bt_controller_mem_release(ESP_BT_MODE_BLE) or (ESP_BT_MODE_CLASSIC_BT) before initializing the controller in a different mode (though for dual-mode, you want both). |
Exercises
- Dual-Mode Data Exchange:
- Extend Example 1. When data is received over SPP (Classic), send a BLE notification containing that data (or a summary) to a connected BLE client.
- Conversely, when data is written to a specific BLE characteristic, send that data out over an active SPP connection.
- Simultaneous Connections:
- Modify Example 1 to allow multiple SPP connections if supported by the profile (SPP typically handles one connection per server instance, but you could explore client roles).
- Ensure the BLE part can still handle connections while an SPP link is active. Observe resource usage and stability.
- Mode-Specific Control:
- Add a BLE characteristic that allows a connected BLE client to enable or disable the discoverability of the Classic Bluetooth SPP service (i.e., programmatically call
esp_bt_gap_set_scan_mode
). This demonstrates controlling one mode’s behavior via the other.
- Add a BLE characteristic that allows a connected BLE client to enable or disable the discoverability of the Classic Bluetooth SPP service (i.e., programmatically call
Summary
- Dual-mode Bluetooth allows a single device (like the original ESP32) to support both Bluetooth Classic (BR/EDR) and BLE simultaneously.
- Only the original ESP32 series supports Bluetooth Classic and thus true Classic+LE dual-mode operation. ESP32-S3, C3, C6, H2 do not.
- ESP-IDF’s Bluedroid stack manages both modes, requiring separate initialization and event handling for Classic profiles (e.g., SPP) and BLE GATT services.
- The Bluetooth controller must be initialized in
ESP_BT_MODE_BTDM
. - Dual-mode devices can be concurrently discoverable and connectable over both Classic Bluetooth and BLE.
- Careful management of callbacks, device naming, and understanding variant capabilities are crucial for successful dual-mode application development.
Further Reading
- ESP-IDF API Reference: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/esp_bt_device.html
- ESP-IDF Bluetooth Examples: https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/bluedroid/coex/a2dp_gatts_coex
- Bluetooth Core Specification: https://www.bluetooth.com/specifications/specs/