Chapter 55: Bluetooth HFP Implementation
Chapter Objectives
Upon completing this chapter, you will be able to:
- Understand the fundamentals of the Bluetooth Hands-Free Profile (HFP).
- Differentiate between HFP Audio Gateway (AG) and Hands-Free (HF) roles.
- Describe the HFP connection establishment process, including Service Level Connection (SLC) and audio connections.
- Utilize ESP-IDF APIs to implement HFP functionality on ESP32 devices.
- Handle HFP events such as connection, disconnection, and audio state changes.
- Implement basic call control features using HFP.
- Understand the differences in HFP support across various ESP32 variants.
- Troubleshoot common issues in HFP implementations.
Introduction
In the previous chapters, we explored Bluetooth Classic architecture, the Serial Port Profile (SPP), and the Advanced Audio Distribution Profile (A2DP) for streaming music. Now, we turn our attention to another vital Bluetooth Classic profile: the Hands-Free Profile (HFP). HFP is designed to enable hands-free voice communication, most commonly seen in automotive hands-free systems and wireless headsets that allow users to make and receive phone calls without handling their mobile phones.
The ability to integrate voice call functionality into embedded devices opens up a wide range of applications, from custom intercom systems to voice-controlled assistants integrated with phone capabilities. The ESP32, with its robust Bluetooth Classic support, provides a powerful platform for developing HFP-enabled devices. This chapter will guide you through the theory behind HFP and demonstrate how to implement HFP functionality using ESP-IDF v5.x, enabling your ESP32 projects to interact with mobile phones for voice calls.
Theory
What is HFP?
The Hands-Free Profile (HFP) is a Bluetooth profile that defines the protocols and procedures for enabling a “Hands-Free” device (HF) to connect with an “Audio Gateway” (AG) device, typically a mobile phone. The primary purpose of HFP is to allow the HF device to place and receive phone calls, as well as control certain phone functions (like redialing, voice dialing, volume control) remotely.
HFP builds upon the Generic Access Profile (GAP) for connection management and the Serial Port Profile (SPP) for exchanging AT commands, which are used for control signaling. For audio, HFP uses Synchronous Connection-Oriented (SCO) or extended Synchronous Connection-Oriented (eSCO) links, which are specifically designed for time-sensitive voice data.
HFP Roles
There are two primary roles in an HFP interaction:
- Audio Gateway (AG): This is the device that acts as the gateway for audio input and output. Typically, this is a mobile phone or a device with cellular connectivity. The AG is responsible for managing the actual phone call over the cellular network.
- Hands-Free Unit (HF): This is the device that provides the user interface for hands-free operation. This could be a car kit, a wireless headset, or, in our case, an ESP32-based device. The HF device sends commands to the AG (e.g., dial number, answer call) and receives audio from/to the AG.
In most ESP32 applications, the ESP32 will act as the HF device, connecting to a user’s mobile phone (the AG).
HFP Connection Establishment
The HFP connection process involves several steps:
- Device Discovery and Pairing (if not already paired): The HF and AG devices must first discover each other and establish a paired relationship using standard Bluetooth procedures. This typically involves SDP (Service Discovery Protocol) to find HFP services on the AG.
- Service Level Connection (SLC) Establishment: Once pairing is complete (or if already paired), the HF device initiates an SLC to the AG. This connection is established over an RFCOMM channel (conceptually similar to SPP).
- The HF device queries the AG for its supported features.
- The AG responds with its features.
- Various parameters are exchanged, such as indicator statuses (battery level, signal strength, etc.).
- This SLC is used to exchange AT commands for call control and status updates.
- Audio Connection Establishment: When a call is active (incoming or outgoing), an audio connection is established.
- HFP uses SCO or eSCO links for audio. SCO links provide a fixed bandwidth, while eSCO links offer better quality and resilience to interference through retransmissions.
- The audio data is typically encoded using codecs like CVSD (Continuously Variable Slope Delta modulation) for basic quality or mSBC (modified Subband Codec) for wideband speech, if supported by both devices (HFP version 1.6 and later).
sequenceDiagram; %% HFP Connection Establishment Flow actor HF as "Hands-Free Unit (HF)<br>(e.g., ESP32)"; actor AG as "Audio Gateway (AG)<br>(e.g., Mobile Phone)"; %% Style definitions %% Primary/Start Nodes: fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 %% Process Nodes: fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF %% Success Nodes: fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 %% Decision Nodes: fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E participant HF; participant AG; opt Initial Setup (if not previously paired) HF->>AG: 1. Inquiry / Device Discovery; activate AG; AG-->>HF: Inquiry Response (BD_ADDR); deactivate AG; HF->>AG: 2. Pairing Request; activate AG; AG-->>HF: Pairing Response / Confirmation; deactivate AG; Note over HF,AG: Devices are now Paired; end rect rgb(219, 234, 254) /* Process Node Color */ HF->>AG: 3. SDP Query (for HFP Service UUIDs: HF or AG); activate AG; AG-->>HF: SDP Response (RFCOMM Channel for HFP); deactivate AG; end rect rgb(219, 234, 254) /* Process Node Color */ HF->>AG: 4. RFCOMM Connection Request (to discovered HFP channel); activate AG; AG-->>HF: RFCOMM Connection Accepted; deactivate AG; Note over HF,AG: Service Level Connection (SLC) Established; end rect rgb(254, 243, 199) /* Decision/Negotiation Node Color */ HF->>AG: 5. AT Command: Exchange Supported Features (e.g., AT+BRSF); activate AG; AG-->>HF: AG Features Response; deactivate AG; HF->>AG: AT Command: Query Indicators Status (e.g., AT+CIND?); activate AG; AG-->>HF: Indicators Status Response; deactivate AG; HF->>AG: AT Command: Enable Indicator Updates (e.g., AT+CMER); activate AG; AG-->>HF: OK (Response to AT+CMER); deactivate AG; Note over HF,AG: SLC Fully Initialized. Ready for Call Control.; end opt During a Call (Incoming or Outgoing) rect rgb(209, 250, 229) /* Success Node Color */ alt AG Initiates Audio AG->>HF: Request to Establish Audio Connection (SCO/eSCO); activate HF; HF-->>AG: Accept Audio Connection; deactivate HF; else HF Initiates Audio (e.g., after dialing) HF->>AG: Request to Establish Audio Connection (SCO/eSCO); activate AG; AG-->>HF: Accept Audio Connection; deactivate AG; end Note over HF,AG: SCO/eSCO Audio Connection Established for Voice; end end
Key HFP Operations and AT Commands
HFP relies heavily on AT commands (similar to modem commands) exchanged over the SLC for control and status. Some common operations and associated AT commands include:
AT Command (from HF) | Description | Typical Scenario |
---|---|---|
AT+BRSF=<HF_features> | Bluetooth Retrieve Supported Features. HF informs AG of its supported features. | During SLC establishment. |
AT+CIND? | Query current status of AG indicators (e.g., signal, battery). | During SLC establishment or periodically. |
AT+CIND=? | Query the list and range of indicators supported by AG. | During SLC establishment. |
AT+CMER=[<mode>[,<keyp>[,<disp>[,<ind>]]]] | Enable/disable event reporting for indicators from AG. (e.g., AT+CMER=3,0,0,1 to enable indicator updates). | During SLC establishment. |
ATA | Answer an incoming call. | When HF receives RING alert and call setup indication. |
AT+CHUP | Hang up call. Rejects an incoming call or terminates an active/outgoing call. | During an incoming call, active call, or outgoing call. |
ATD<number>; | Dial a phone number. (e.g., ATD1234567890;) | User initiates an outgoing call from HF. |
ATD><memory_location>; | Dial a number from a phonebook memory location. | User initiates an outgoing call from HF’s stored contacts. |
AT+BLDN | Redial last dialed number. | User initiates redial from HF. |
AT+BVRA=[0|1] | Activate (1) or deactivate (0) voice recognition on AG. | User wants to use voice commands via the phone. |
AT+VGS=<volume> | Set Speaker Gain (volume) on HF. HF reports this to AG. (0-15) | User adjusts speaker volume on HF. |
AT+VGM=<gain> | Set Microphone Gain on HF. HF reports this to AG. (0-15) | User adjusts microphone sensitivity on HF. |
AT+CNUM | Query Subscriber Number Information from AG. | To get the HF user’s own phone number. |
AT+NREC=[0|1] | Noise Reduction and Echo Canceling. Disable (0) or enable (1) on AG (if supported). | To improve call audio quality. |
AT+BTRH? | Query current Response and Hold status from AG. | Managing call hold states. |
AT+BTRH=[0|1|2] | Set Response and Hold status (0: Put incoming call on hold, 1: Accept held call, 2: Reject held call). | Managing call hold states. |
The ESP-IDF HFP component handles the low-level details of these AT command exchanges, providing higher-level APIs and callbacks for application developers.
graph TD %% HFP Roles Diagram %% Styles classDef ag fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef hf fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef link fill:#A78BFA,color:#1F2937; AG["Audio Gateway (AG)<br>e.g., Mobile Phone<br><br>Manages cellular calls<br>Gateway for audio I/O"] HF["Hands-Free Unit (HF)<br>e.g., ESP32 Device, Car Kit, Headset<br><br>Provides user interface<br>Controls AG for calls"] AG -- "SLC:<br>Service Level Connection<br>(RFCOMM for <br>AT Commands)" --> HF AG -- "AUDIO:<br>Audio Connection<br>(SCO/eSCO for Voice)" --> HF subgraph "HFP Interaction" AG HF end %% Style for links (referenced by index) linkStyle 0 stroke-width:2px,stroke:#D97706,stroke-dasharray:5 5 linkStyle 1 stroke-width:2px,stroke:#059669 class AG ag class HF hf %% Legend subgraph "Legend" direction LR L1["SLC:<br>Service Level Connection<br>RFCOMM for AT Commands"] L2["AUDIO:<br>Audio Connection<br>SCO/eSCO for Voice"] end style L1 fill:transparent,stroke:transparent,color:#92400E,font-style:italic style L2 fill:transparent,stroke:transparent,color:#065F46,font-style:italic
Audio Codecs
- CVSD (Continuously Variable Slope Delta Modulation): This is the mandatory codec for HFP, providing basic narrowband (8 kHz sampling rate) voice quality. It’s robust and computationally inexpensive.
- mSBC (modified Subband Codec): Introduced in HFP 1.6, mSBC enables Wideband Speech (WBS), offering significantly better audio quality (16 kHz sampling rate). If both the AG and HF support mSBC, they can negotiate its use. ESP-IDF supports mSBC.
Practical Examples
In this section, we’ll explore how to implement HFP HF functionality on an ESP32 device using ESP-IDF. The ESP32 will act as a Hands-Free unit, capable of connecting to a smartphone (Audio Gateway), answering calls, and initiating calls.
Prerequisites
- ESP-IDF v5.x installed and configured with VS Code.
- An ESP32 development board (e.g., ESP32-DevKitC, ESP32-S3-DevKitC).
- A smartphone with Bluetooth capability to act as the AG.
- (Optional but recommended for audio testing) An I2S audio codec chip (e.g., ES8388, MAX98357A) connected to the ESP32, or at least a DAC output pin for basic audio output and an ADC input pin for microphone input. For simplicity, this example will focus on the HFP control logic; audio routing to a codec is an advanced topic covered in I2S chapters.
ESP-IDF HFP Configuration
Before writing code, ensure HFP is enabled in your project’s sdkconfig
file.
- Open your ESP-IDF project in VS Code.
- Run
ESP-IDF: SDK Configuration Editor (menuconfig)
. - Navigate to
Component config
->Bluetooth
->Bluedroid Options
.- Ensure
Classic Bluetooth
is enabled. - Enable
Hands Free Profile (HFP)
. You might find options for HFP HF (Client) and HFP AG (Unit). For this example, we needHFP HF (Client) Role
.
- Ensure
- Navigate to
Component config
->Bluetooth
->Bluetooth Controller
.- Select
Bluetooth dual mode (BR/EDR + BLE)
.
- Select
- Save the configuration and exit. The build system will prompt for a re-build if necessary.
Example Code: Basic HFP Hands-Free Unit
This example demonstrates initializing HFP, handling connection events, and basic call control.
// main.c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_hf_client_api.h" // For HFP HF (Client) role
static const char *TAG = "HFP_HF_EXAMPLE";
// HFP event handler
static void hf_client_event_handler(esp_hf_client_cb_event_t event, esp_hf_client_cb_param_t *param);
// GAP callback handler (for Bluetooth classic events like authentication)
static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param);
// Main application entry point
void app_main(void)
{
esp_err_t ret;
// Initialize NVS - required for Bluetooth
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);
// Release memory reserved for BLE
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
// Initialize Bluetooth controller
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "%s initialize controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
// Enable Bluetooth controller in Classic BT mode
if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK) {
ESP_LOGE(TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
// Initialize Bluedroid stack
if ((ret = esp_bluedroid_init()) != ESP_OK) {
ESP_LOGE(TAG, "%s initialize bluedroid failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
// Enable Bluedroid stack
if ((ret = esp_bluedroid_enable()) != ESP_OK) {
ESP_LOGE(TAG, "%s enable bluedroid failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
// Register GAP callback function
if ((ret = esp_bt_gap_register_callback(bt_app_gap_cb)) != ESP_OK) {
ESP_LOGE(TAG, "%s gap register failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
// Initialize HFP HF (Client)
if ((ret = esp_hf_client_init()) != ESP_OK) {
ESP_LOGE(TAG, "%s hf client init failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
// Register HFP HF (Client) callback function
if ((ret = esp_hf_client_register_callback(hf_client_event_handler)) != ESP_OK) {
ESP_LOGE(TAG, "%s hf client register callback failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "HFP HF Example initialized. Waiting for connections.");
// Set device name
esp_bt_dev_set_device_name("ESP32_HFP_HF");
// Set discoverable and connectable mode
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
}
static void hf_client_event_handler(esp_hf_client_cb_event_t event, esp_hf_client_cb_param_t *param)
{
ESP_LOGI(TAG, "HFP Client Event: %s", esp_hf_client_event_str(event));
esp_hf_client_cb_param_t *hf_param = (esp_hf_client_cb_param_t *)param;
switch (event) {
case ESP_HF_CLIENT_CONNECTION_STATE_EVT:
ESP_LOGI(TAG, "Connection state: %s, peer_feat: 0x%x, chld_feat: 0x%x",
esp_hf_client_conn_state_str(hf_param->conn_stat.state),
hf_param->conn_stat.peer_feat,
hf_param->conn_stat.chld_feat);
if (hf_param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_CONNECTED) {
ESP_LOGI(TAG, "Connected to AG: %02x:%02x:%02x:%02x:%02x:%02x",
hf_param->conn_stat.remote_bda[0], hf_param->conn_stat.remote_bda[1],
hf_param->conn_stat.remote_bda[2], hf_param->conn_stat.remote_bda[3],
hf_param->conn_stat.remote_bda[4], hf_param->conn_stat.remote_bda[5]);
// You can query AG features or subscriber number here if needed
// esp_hf_client_query_current_calls(hf_param->conn_stat.remote_bda);
} else if (hf_param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED) {
ESP_LOGI(TAG, "Disconnected from AG.");
// Make device discoverable again if needed
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
}
break;
case ESP_HF_CLIENT_AUDIO_STATE_EVT:
ESP_LOGI(TAG, "Audio state: %s", esp_hf_client_audio_state_str(hf_param->audio_stat.state));
if (hf_param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_CONNECTED) {
ESP_LOGI(TAG, "Audio connection established.");
// Audio data will start flowing. If you have an I2S codec, configure it here.
// For mSBC, the audio path is automatically handled by Bluedroid if enabled.
} else if (hf_param->audio_stat.state == ESP_HF_CLIENT_AUDIO_STATE_DISCONNECTED) {
ESP_LOGI(TAG, "Audio connection terminated.");
if (esp_hf_client_is_audio_on()) { // Check if audio was on before disconnecting
ESP_LOGI(TAG, "Releasing audio resources.");
// If you were using I2S, stop/deinit it here.
}
}
break;
case ESP_HF_CLIENT_BVRA_EVT: // Voice Recognition Activation
ESP_LOGI(TAG, "Voice recognition: %s", hf_param->bvra.value == 1 ? "ON" : "OFF");
break;
case ESP_HF_CLIENT_CIND_CALL_EVT: // Call indicator
ESP_LOGI(TAG, "Call indicator: %s", hf_param->call.status == 1 ? "ACTIVE" : "INACTIVE");
break;
case ESP_HF_CLIENT_CIND_CALL_SETUP_EVT: // Call setup indicator
ESP_LOGI(TAG, "Call setup indicator: %s", esp_hf_client_call_setup_str(hf_param->call_setup.status));
if (hf_param->call_setup.status == ESP_HF_CALL_SETUP_STATE_INCOMING) {
ESP_LOGI(TAG, "Incoming call! Answering...");
// Example: Automatically answer incoming call
// esp_hf_client_answer_call(hf_param->call_setup.remote_bda);
}
break;
case ESP_HF_CLIENT_CIND_CALL_HELD_EVT: // Call held indicator
ESP_LOGI(TAG, "Call held indicator: %s", esp_hf_client_call_held_str(hf_param->call_held.status));
break;
case ESP_HF_CLIENT_CNUM_EVT: // Subscriber Number
ESP_LOGI(TAG, "Subscriber number: %s, type: %d", hf_param->cnum.number, hf_param->cnum.type);
break;
case ESP_HF_CLIENT_CLIP_EVT: // Calling Line Identification
ESP_LOGI(TAG, "Caller ID: %s, type: %d", hf_param->clip.number, hf_param->clip.type);
break;
case ESP_HF_CLIENT_RING_IND_EVT: // Ring indication
ESP_LOGI(TAG, "RING RING! from %02x:%02x:%02x:%02x:%02x:%02x",
hf_param->ring_ind.remote_bda[0], hf_param->ring_ind.remote_bda[1],
hf_param->ring_ind.remote_bda[2], hf_param->ring_ind.remote_bda[3],
hf_param->ring_ind.remote_bda[4], hf_param->ring_ind.remote_bda[5]);
// To answer: esp_hf_client_answer_call(hf_param->ring_ind.remote_bda);
// To reject: esp_hf_client_reject_call(hf_param->ring_ind.remote_bda);
break;
case ESP_HF_CLIENT_AT_RESPONSE_EVT:
ESP_LOGI(TAG, "AT response: code %d, cme_err %d", hf_param->at_response.code, hf_param->at_response.cme_err);
if (hf_param->at_response.code == ESP_HF_AT_RESPONSE_CODE_OK) {
ESP_LOGI(TAG, "AT command OK");
} else if (hf_param->at_response.code == ESP_HF_AT_RESPONSE_CODE_ERR) {
ESP_LOGE(TAG, "AT command ERROR: %d", hf_param->at_response.cme_err);
}
break;
// Add more cases as needed for other HFP events
// ESP_HF_CLIENT_BSIR_EVT (In-band Ring Tone)
// ESP_HF_CLIENT_BINP_EVT (Input Phone Number for Voice Tag)
// ESP_HF_CLIENT_BTRH_EVT (Bluetooth Response and Hold)
// ... and others
default:
ESP_LOGI(TAG, "Unhandled HFP Client Event: %d", event);
break;
}
}
static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
ESP_LOGI(TAG, "GAP Event: %d", event);
switch (event) {
case ESP_BT_GAP_AUTH_CMPL_EVT: {
if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
ESP_LOGI(TAG, "Authentication success: %s", param->auth_cmpl.device_name);
esp_log_buffer_hex(TAG, param->auth_cmpl.bda, ESP_BD_ADDR_LEN);
} else {
ESP_LOGE(TAG, "Authentication failed, status: %d", param->auth_cmpl.stat);
}
break;
}
case ESP_BT_GAP_PIN_REQ_EVT: {
ESP_LOGI(TAG, "ESP_BT_GAP_PIN_REQ_EVT min_16_digit: %d", param->pin_req.min_16_digit);
if (param->pin_req.min_16_digit) {
ESP_LOGI(TAG, "Input pin code: 0000 0000 0000 0000");
esp_bt_pin_code_t pin_code = {0};
esp_bt_gap_pin_reply(param->pin_req.bda, true, 16, pin_code);
} else {
ESP_LOGI(TAG, "Input pin code: 1234");
esp_bt_pin_code_t pin_code;
pin_code[0] = '1';
pin_code[1] = '2';
pin_code[2] = '3';
pin_code[3] = '4';
esp_bt_gap_pin_reply(param->pin_req.bda, true, 4, pin_code);
}
break;
}
#if (CONFIG_BT_SSP_ENABLED == true)
case ESP_BT_GAP_CFM_REQ_EVT:
ESP_LOGI(TAG, "ESP_BT_GAP_CFM_REQ_EVT Please compare the numeric value: %"PRIu32, param->cfm_req.num_val);
esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
break;
case ESP_BT_GAP_KEY_NOTIF_EVT:
ESP_LOGI(TAG, "ESP_BT_GAP_KEY_NOTIF_EVT passkey:%"PRIu32, param->key_notif.passkey);
break;
case ESP_BT_GAP_KEY_REQ_EVT:
ESP_LOGI(TAG, "ESP_BT_GAP_KEY_REQ_EVT Please enter passkey!");
break;
#endif
// Add other GAP events if needed, e.g., ESP_BT_GAP_MODE_CHG_EVT for scan mode changes
default: {
ESP_LOGI(TAG, "Unhandled GAP event: %d", event);
break;
}
}
}
// Helper functions to map enum values to strings (optional, for better logging)
const char *esp_hf_client_event_str(esp_hf_client_cb_event_t event) {
switch (event) {
case ESP_HF_CLIENT_CONNECTION_STATE_EVT: return "CONNECTION_STATE_EVT";
case ESP_HF_CLIENT_AUDIO_STATE_EVT: return "AUDIO_STATE_EVT";
case ESP_HF_CLIENT_BVRA_EVT: return "BVRA_EVT";
case ESP_HF_CLIENT_CIND_CALL_EVT: return "CIND_CALL_EVT";
case ESP_HF_CLIENT_CIND_CALL_SETUP_EVT: return "CIND_CALL_SETUP_EVT";
case ESP_HF_CLIENT_CIND_CALL_HELD_EVT: return "CIND_CALL_HELD_EVT";
case ESP_HF_CLIENT_CNUM_EVT: return "CNUM_EVT";
case ESP_HF_CLIENT_CLIP_EVT: return "CLIP_EVT";
case ESP_HF_CLIENT_RING_IND_EVT: return "RING_IND_EVT";
case ESP_HF_CLIENT_AT_RESPONSE_EVT: return "AT_RESPONSE_EVT";
// ... add other events
default: return "UNKNOWN_HF_CLIENT_EVENT";
}
}
const char *esp_hf_client_conn_state_str(esp_hf_client_connection_state_t state) {
switch (state) {
case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED: return "DISCONNECTED";
case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTING: return "CONNECTING";
case ESP_HF_CLIENT_CONNECTION_STATE_CONNECTED: return "CONNECTED";
case ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED: return "SLC_CONNECTED";
case ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTING: return "DISCONNECTING";
default: return "UNKNOWN_CONN_STATE";
}
}
const char *esp_hf_client_audio_state_str(esp_hf_client_audio_state_t state) {
switch (state) {
case ESP_HF_CLIENT_AUDIO_STATE_DISCONNECTED: return "AUDIO_DISCONNECTED";
case ESP_HF_CLIENT_AUDIO_STATE_CONNECTING: return "AUDIO_CONNECTING";
case ESP_HF_CLIENT_AUDIO_STATE_CONNECTED: return "AUDIO_CONNECTED";
case ESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC: return "AUDIO_CONNECTED_MSBC"; // For Wideband Speech
default: return "UNKNOWN_AUDIO_STATE";
}
}
const char *esp_hf_client_call_setup_str(esp_hf_call_setup_status_t status) {
switch (status) {
case ESP_HF_CALL_SETUP_STATE_NO_SETUP: return "NO_SETUP";
case ESP_HF_CALL_SETUP_STATE_INCOMING: return "INCOMING";
case ESP_HF_CALL_SETUP_STATE_OUTGOING_DIALING: return "OUTGOING_DIALING";
case ESP_HF_CALL_SETUP_STATE_OUTGOING_ALERTING: return "OUTGOING_ALERTING";
default: return "UNKNOWN_CALL_SETUP_STATE";
}
}
const char *esp_hf_client_call_held_str(esp_hf_call_held_status_t status) {
switch (status) {
case ESP_HF_CALL_HELD_STATE_NO_CALLS_HELD: return "NO_CALLS_HELD";
case ESP_HF_CALL_HELD_STATE_CALL_ON_HOLD_AND_ACTIVE_CALLS: return "CALL_ON_HOLD_AND_ACTIVE_CALLS";
case ESP_HF_CALL_HELD_STATE_CALL_ON_HOLD_NO_ACTIVE_CALLS: return "CALL_ON_HOLD_NO_ACTIVE_CALLS";
default: return "UNKNOWN_CALL_HELD_STATE";
}
}
Key ESP-IDF HFP Client (HF) Events
HFP Client Event (esp_hf_client_cb_event_t) | Description | Key Parameters in param union |
---|---|---|
ESP_HF_CLIENT_CONNECTION_STATE_EVT | HFP Service Level Connection (SLC) state with AG has changed (disconnected, connecting, connected, SLC connected). |
param->conn_stat.state param->conn_stat.remote_bda param->conn_stat.peer_feat (AG features) param->conn_stat.chld_feat (AG call handling features) |
ESP_HF_CLIENT_AUDIO_STATE_EVT | SCO/eSCO audio connection state has changed (disconnected, connecting, connected, connected_msbc). | param->audio_stat.state, param->audio_stat.remote_bda |
ESP_HF_CLIENT_BVRA_EVT | Voice Recognition activation state from AG changed. | param->bvra.value (0: off, 1: on) |
ESP_HF_CLIENT_CIND_CALL_EVT | Call indicator status from AG (0: no call, 1: call active). | param->call.status |
ESP_HF_CLIENT_CIND_CALL_SETUP_EVT | Call setup progress indicator from AG (no setup, incoming, outgoing dialing, outgoing alerting). | param->call_setup.status |
ESP_HF_CLIENT_CIND_CALL_HELD_EVT | Call hold status from AG. | param->call_held.status |
ESP_HF_CLIENT_CNUM_EVT | Subscriber Number information received from AG in response to AT+CNUM. | param->cnum.number, param->cnum.type |
ESP_HF_CLIENT_CLIP_EVT | Calling Line Identification Presentation (Caller ID) received from AG. | param->clip.number, param->clip.type |
ESP_HF_CLIENT_RING_IND_EVT | Incoming call “RING” indication from AG. | param->ring_ind.remote_bda |
ESP_HF_CLIENT_AT_RESPONSE_EVT | Response received from AG for an AT command sent by HF. |
param->at_response.code (e.g., ESP_HF_AT_RESPONSE_CODE_OK, ESP_HF_AT_RESPONSE_CODE_ERR) param->at_response.cme_err (CME error code if code is ERR) |
ESP_HF_CLIENT_VOLUME_CONTROL_EVT | Volume synchronization command from AG (either speaker gain or microphone gain). |
param->volume_control.type (ESP_HF_SPEAKER_VOLUME_CONTROL or ESP_HF_MICROPHONE_GAIN_CONTROL) param->volume_control.volume (0-15) |
ESP_HF_CLIENT_BSIR_EVT | In-band Ring Tone status changed on AG. | param->bsir.value (0: off, 1: on) |
CMakeLists.txt:
Ensure your CMakeLists.txt
includes the bt
component:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(hfp_hf_example C)
idf_component_register(SRCS "main.c"
INCLUDE_DIRS ".")
# Add bt component for Bluetooth functionality
# This is usually handled by ESP-IDF automatically if esp_bt.h etc. are included
# but explicitly adding it doesn't hurt.
# list(APPEND REQUIRES "bt") # For older IDF versions
# For IDF 5.x, component dependencies are usually auto-detected.
# Ensure your main component's CMakeLists.txt has:
# idf_component_get_property(my_main_component PRIV_REQUIRES priv_requires)
# list(APPEND priv_requires "bt")
# target_link_libraries(${my_main_component} INTERFACE ${priv_requires})
# However, typically just including the headers is enough for the build system to link correctly.
Usually, for ESP-IDF v5.x, you don’t need to explicitly add bt
to REQUIRES
in idf_component_register
if you are using the standard project structure, as the build system detects dependencies from included headers. If you encounter linking issues, you might need to add bt
to the PRIV_REQUIRES
list of your main component.
Build and Flash Instructions
- Connect your ESP32 board to your computer.
- Open the project in VS Code.
- Select the correct COM port for your ESP32 board (ESP-IDF extension:
ESP-IDF: Select Port to Use (COM, ttyS)
). - Select the target ESP32 variant (ESP-IDF extension:
ESP-IDF: Set Espressif Device Target
). - Build the project (ESP-IDF extension:
ESP-IDF: Build your project
). - Flash the project (ESP-IDF extension:
ESP-IDF: Flash your Device
). - Monitor the output (ESP-IDF extension:
ESP-IDF: Monitor your Device
).
Run and Observe
- After flashing, the ESP32 will initialize Bluetooth and HFP HF mode. It will set its device name to “ESP32_HFP_HF” and become discoverable.
- On your smartphone:
- Go to Bluetooth settings.
- Scan for new devices.
- You should see “ESP32_HFP_HF”.
- Tap on it to pair and connect. Your phone might ask for pairing confirmation.
- Observe the serial monitor output from the ESP32. You should see logs related to:
- GAP events (authentication).
- HFP connection state changes (e.g.,
ESP_HF_CLIENT_CONNECTION_STATE_CONNECTED
,ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED
).
- Test incoming call:
- Call your smartphone from another phone.
- You should see
ESP_HF_CLIENT_RING_IND_EVT
andESP_HF_CLIENT_CIND_CALL_SETUP_EVT
(status:INCOMING
) on the ESP32’s monitor. - The example code currently logs this. To answer, you would call
esp_hf_client_answer_call(remote_bda);
within theESP_HF_CLIENT_RING_IND_EVT
orESP_HF_CLIENT_CIND_CALL_SETUP_EVT
handler.
- Test audio connection:
- Once a call is active (either answered by the ESP32 or initiated from the phone while connected to ESP32), you should see
ESP_HF_CLIENT_AUDIO_STATE_EVT
indicating the audio connection is established (ESP_HF_CLIENT_AUDIO_STATE_CONNECTED
orESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC
). - If you have an audio output (like a speaker connected via I2S/DAC), you should hear the call audio. If you have a microphone, your voice should be sent to the phone. The provided code does not include I2S/DAC/ADC handling for brevity, but this is where you would integrate it.
- Once a call is active (either answered by the ESP32 or initiated from the phone while connected to ESP32), you should see
- Test ending a call:
- End the call from your smartphone or the other phone.
- The ESP32 should receive events indicating the call has ended and the audio connection is terminated.
- To end a call from ESP32, you would use
esp_hf_client_reject_call(remote_bda);
(which also serves as hang-up) oresp_hf_client_terminate_call(remote_bda);
.
Tip: The
remote_bda
(Bluetooth Device Address of the connected AG) is crucial for most HFP commands. It’s typically obtained from theconn_stat
parameter inESP_HF_CLIENT_CONNECTION_STATE_EVT
. Store it globally or pass it around as needed.
Variant Notes
The Hands-Free Profile (HFP) is a Bluetooth Classic profile. Therefore, its availability and functionality depend on the Bluetooth controller capabilities of the specific ESP32 variant.
- ESP32 (Original): Fully supports Bluetooth Classic, including HFP. The example code will work as described.
- ESP32-S2: Does not have a Bluetooth Classic radio. It only supports Bluetooth Low Energy (BLE). Therefore, HFP is not directly applicable to the ESP32-S2. You cannot use the ESP32-S2 as an HFP device on its own.
- ESP32-S3: Fully supports Bluetooth Classic (Dual Mode: Classic + BLE), including HFP. The example code will work as described. It’s a good successor to the original ESP32 for HFP applications.
- ESP32-C3: Does not have a Bluetooth Classic radio. It only supports Bluetooth Low Energy (BLE) and IEEE 802.15.4 (Thread/Zigbee). Therefore, HFP is not directly applicable to the ESP32-C3.
- ESP32-C6: Supports Bluetooth 5.3 (Dual Mode: Classic + BLE) and IEEE 802.15.4. Therefore, HFP is supported on the ESP32-C6. The example code should work, but always refer to the latest ESP-IDF documentation for any variant-specific configurations or limitations.
- ESP32-H2: Primarily designed for IEEE 802.15.4 (Thread/Zigbee) and Bluetooth Low Energy. It does not support Bluetooth Classic. Therefore, HFP is not directly applicable to the ESP32-H2.
Summary of HFP Applicability:
ESP32 Variant | Bluetooth Classic Support | HFP (HF/AG) Support | Key Notes for HFP |
---|---|---|---|
ESP32 (Original/Classic) | Yes | Yes | Full support for HFP roles. |
ESP32-S2 | No | No | Lacks Bluetooth Classic radio. |
ESP32-S3 | Yes (Dual Mode) | Yes | Full support for HFP roles. |
ESP32-C2 | No | No | BLE only. |
ESP32-C3 | No | No | BLE & 802.15.4 only. Lacks Bluetooth Classic. |
ESP32-C6 | Yes (Dual Mode) | Yes | Supports Bluetooth Classic, enabling HFP. |
ESP32-H2 | No | No | BLE & 802.15.4 only. Lacks Bluetooth Classic. |
When developing HFP applications, ensure you select an ESP32 variant that includes Bluetooth Classic capabilities (ESP32, ESP32-S3, ESP32-C6).
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
NVS Not Initialized |
Bluetooth stack initialization fails (e.g., esp_bluedroid_init). Pairing/bonding information not stored or retrieved correctly. |
1. Ensure nvs_flash_init() is called at the start of app_main(). 2. Check return code; if ESP_ERR_NVS_NO_FREE_PAGES or ESP_ERR_NVS_NEW_VERSION_FOUND, call nvs_flash_erase() then nvs_flash_init() again. |
Incorrect BT Controller/Bluedroid Init |
Controller or Bluedroid init functions return errors. HFP profile initialization fails or device is unstable. |
1. Call esp_bt_controller_mem_release(ESP_BT_MODE_BLE) if previously using BLE only. 2. Ensure esp_bt_controller_init(), esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT) (or DUAL_MODE), esp_bluedroid_init(), and esp_bluedroid_enable() are called in sequence and check all return codes. |
HFP Role Misconfiguration in menuconfig |
ESP32 fails to connect as HF, or phone fails to connect to ESP32 if configured as AG incorrectly. HFP events are not as expected for the intended role. |
1. Run idf.py menuconfig. 2. Navigate to Component config -> Bluetooth -> Bluedroid Options -> Hands Free Profile (HFP). 3. Enable [*] HFP HF (Client) Role if ESP32 is the hands-free device (connecting to a phone). 4. Disable HFP AG Role if not used. Rebuild project. |
Audio Routing / Codec Issues (No Audio) | HFP SLC and audio connection (SCO/eSCO) establish successfully (seen in logs), but no audio is heard on ESP32’s speaker or no audio from ESP32’s microphone reaches the phone. |
1. This example focuses on HFP control; actual audio requires I2S driver setup, codec (e.g., ES8388) initialization, and routing SCO data to/from the codec. This is complex and not covered by basic HFP init. 2. Check ESP_HF_CLIENT_AUDIO_STATE_EVT: if state is ESP_HF_CLIENT_AUDIO_STATE_CONNECTED_MSBC, wideband speech is active. If ESP_HF_CLIENT_AUDIO_STATE_CONNECTED, narrowband (CVSD) is active. Ensure your audio processing pipeline matches. 3. ESP-IDF’s HFP component can handle internal audio path for mSBC. For CVSD, or custom routing, you might need to handle SCO data packets from/to Bluetooth controller directly. |
Pairing or SLC Connection Failures |
ESP32 (HF) and phone (AG) fail to pair. Pairing succeeds, but Service Level Connection (SLC) doesn’t establish (no ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED event). |
1. Ensure ESP32 is discoverable: esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE). 2. Check GAP callback (bt_app_gap_cb) for authentication errors (ESP_BT_GAP_AUTH_CMPL_EVT). 3. Handle SSP correctly (e.g., esp_bt_gap_ssp_confirm_reply). 4. Clear bonding info on phone and ESP32 (erase NVS) and re-pair. 5. Ensure phone’s Bluetooth is on and not connected to too many other devices. |
AT Command Failures / No Response | Sending an AT command (e.g., esp_hf_client_dial_number) results in no action or an error response in ESP_HF_CLIENT_AT_RESPONSE_EVT. |
1. Ensure SLC is fully established (ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED) before sending AT commands. 2. Check the remote_bda used in API calls is correct for the connected AG. 3. Examine param->at_response.code and param->at_response.cme_err in the event for specific error reasons from the AG. 4. Some AGs might not support all AT commands or require specific sequences. |
Exercises
- Caller ID Display:
- Modify the example code to capture the caller’s phone number from the
ESP_HF_CLIENT_CLIP_EVT
event. - If you have an LCD or OLED display connected (from previous peripheral chapters), display the incoming caller ID on it. Otherwise, just log it clearly.
- Modify the example code to capture the caller’s phone number from the
- One-Button Call Initiation & Termination:
- Connect a GPIO button to your ESP32.
- Implement functionality where a short press on the button initiates a call to a predefined phone number (e.g.,
esp_hf_client_dial_number(remote_bda, "1234567890");
). You’ll need to store theremote_bda
of the connected AG. - Implement functionality where a long press on the same button terminates an active call or rejects an incoming call (
esp_hf_client_reject_call(remote_bda);
).
- Volume Control Indication:
- Implement functionality to send speaker volume changes from the ESP32 to the AG.
- Use two buttons for volume up and volume down. When pressed, increment/decrement a local volume variable (e.g., 0-15).
- Call
esp_hf_client_send_volume_control(remote_bda, ESP_HF_SPEAKER_VOLUME_CONTROL, volume_level);
to update the AG. - Log the volume level being sent. (Actually receiving volume changes from AG and applying them to a local speaker is more complex and involves
ESP_HF_CLIENT_VOLUME_CONTROL_EVT
).
Summary
- HFP enables hands-free voice communication between an Audio Gateway (AG, e.g., a phone) and a Hands-Free unit (HF, e.g., ESP32).
- Connection involves SLC establishment for AT command exchange and SCO/eSCO links for audio.
- ESP-IDF provides
esp_hf_client_api.h
for implementing HFP HF functionality. - Key operations include answering/rejecting calls, dialing, and receiving caller ID, managed via events and API calls.
- Audio codecs like CVSD (mandatory) and mSBC (for wideband speech) are used.
- HFP is a Bluetooth Classic profile, supported by ESP32, ESP32-S3, and ESP32-C6, but not by ESP32-S2, ESP32-C3, or ESP32-H2.
- Proper initialization of NVS, Bluetooth controller, Bluedroid, and HFP service is crucial.
- Event handlers are central to managing HFP states and interactions.
Further Reading
- ESP-IDF API Reference – Bluetooth Classic HFP Client:
- Bluetooth SIG – Hands-Free Profile Specification:
- https://www.bluetooth.com/specifications/specs/hands-free-profile-1-8/ (Or the latest version available). This provides the most in-depth technical details of the profile.
- ESP-IDF Bluetooth Examples on GitHub:
- Check the
examples/bluetooth/bluedroid/classic_bt/hfp_hf
directory in your ESP-IDF installation or on the Espressif GitHub repository for more complete examples. - https://github.com/espressif/esp-idf/tree/v5.4/examples/bluetooth/bluedroid/classic_bt/hfp_hf
- Check the
