Chapter 65: BLE Battery Service Implementation
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the purpose and structure of the standard BLE Battery Service (BAS).
- Identify the UUIDs for the Battery Service and its Battery Level characteristic.
- Configure the Battery Level characteristic with appropriate properties (Read, Notify).
- Implement the Characteristic Presentation Format Descriptor for the Battery Level.
- Add and manage the Client Characteristic Configuration Descriptor (CCCD) for notifications.
- Develop an ESP32 GATT server application that exposes the Battery Service.
- Simulate battery level changes and send notifications to subscribed clients.
- Develop an ESP32 GATT client application to discover, read, and receive notifications for the Battery Level.
Introduction
Many battery-powered BLE devices need a standardized way to report their battery status to connected clients, such as smartphones or other microcontrollers. The Bluetooth SIG has defined a standard service for this exact purpose: the Battery Service (BAS). Implementing this standard service ensures interoperability, allowing generic clients to easily understand and display your device’s battery level without needing custom parsing logic.
Imagine a wireless headphone, a fitness tracker, or a remote sensor. All these devices benefit from reporting their battery levels. If they all implement the standard Battery Service, a single mobile application or central device can monitor the battery status of multiple, diverse peripherals in a consistent manner.
This chapter will walk you through the implementation of the BLE Battery Service on an ESP32 using ESP-IDF v5.x. You will learn how to set up the service and its mandatory Battery Level characteristic, including its properties and descriptors. We will cover both the server-side implementation (making your ESP32 report its battery level) and a client-side example to read and subscribe to battery level updates.
Theory
The Battery Service is one of the simplest and most widely adopted GATT-based services defined by the Bluetooth SIG. Its primary function is to expose the battery level of a device.
Battery Service (BAS) Overview
Aspect | Details | Notes |
---|---|---|
Service Name | Battery Service (BAS) | Standardized by Bluetooth SIG. |
Service UUID | 0x180F |
16-bit UUID, officially assigned. |
Primary Purpose | To expose the battery level and status of a device to a connected client. | Enhances interoperability for battery monitoring. |
Dependencies | None | Self-contained service. |
Instances | Typically one instance per device. Multiple instances possible if a device has multiple independent batteries. | Most common use case is a single BAS instance. |
Mandatory Characteristic | Battery Level (0x2A19 ) |
This is the core of the service. |
A device can have one or more instances of the Battery Service if it contains multiple independent batteries (though a single instance is most common).
Battery Level Characteristic
The Battery Service has one mandatory characteristic:
Property | Value / Description |
---|---|
Characteristic Name | Battery Level |
Characteristic UUID | 0x2A19 (Bluetooth SIG adopted) |
Data Type | uint8_t (unsigned 8-bit integer) |
Value Range | 0-100 (representing percentage: 0% = empty, 100% = full). Values outside this range may indicate errors or unknown state. |
Mandatory Properties | READ (ESP_GATT_CHAR_PROP_BIT_READ ) |
Optional Properties | NOTIFY (ESP_GATT_CHAR_PROP_BIT_NOTIFY ) – Highly recommended for dynamic updates. |
Permissions (Typical) | ESP_GATT_PERM_READ (or ESP_GATT_PERM_READ_ENCRYPTED if security needed). |
Descriptors for Battery Level Characteristic
- Client Characteristic Configuration Descriptor (CCCD – UUID
0x2902
):- Presence: Mandatory if the
NOTIFY
property is supported for the Battery Level characteristic. - Purpose: Allows a client to enable or disable notifications for the Battery Level.
- Value: A 2-byte bitfield. Writing
0x0001
enables notifications;0x0000
disables them. - Permissions: Typically
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE
(or encrypted versions).
- Presence: Mandatory if the
- Characteristic Presentation Format Descriptor (CPF – UUID
0x2904
):- Presence: Optional, but highly recommended for clarity and interoperability.
- Purpose: Describes the format, unit, and other properties of the characteristic’s value.
- Value for Battery Level (7 bytes):
- Format (1 byte):
0x04
(unsigned 8-bit integer –uint8
). - Exponent (1 byte):
0x00
(exponent of 0, meaning the value is taken as is). - Unit (2 bytes):
0x27AD
(Bluetooth SIG unit for “percentage”). Little Endian:0xAD, 0x27
. - Namespace (1 byte):
0x01
(Bluetooth SIG Assigned Numbers namespace). - Description (2 bytes):
0x0000
(no specific description from this namespace).
- Format (1 byte):
- Permissions: Typically
ESP_GATT_PERM_READ
.
%%{init: {"theme": "base", "themeVariables": { "primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937", "fontSize": "14px", "fontFamily": "\"Open Sans\", sans-serif" }}}%% graph TD subgraph BAS_Service ["Battery Service (0x180F)"] direction TB BLC["Battery Level Characteristic (0x2A19)<br><b>Properties:</b> READ, NOTIFY (opt)<br><b>Value:</b> uint8 (0-100%)"]:::char subgraph Descriptors_for_BLC [Descriptors] direction TB CCCD["CCCD (0x2902)<br><b>Purpose:</b> Enable/Disable Notifications<br><b>Value:</b> 0x0000 / 0x0001<br>(Mandatory if NOTIFY)"]:::desc CPF["Char. Presentation Format (0x2904)<br><b>Purpose:</b> Describe Value<br><b>Value:</b> uint8, %, etc.<br>(Recommended)"]:::desc end BLC --> CCCD BLC --> CPF end style BAS_Service fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 classDef char fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF classDef desc fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E %% Notes: %% - The Battery Service (BAS) exposes battery information. %% - Its core is the Battery Level Characteristic. %% - CCCD is vital for enabling notifications from client. %% - CPF helps clients correctly interpret the battery level value.
Operation Flow
- Server (ESP32 exposing BAS):
- Initializes the GATT server.
- Creates the Battery Service (
0x180F
). - Adds the Battery Level characteristic (
0x2A19
) to this service withREAD
(and optionallyNOTIFY
) properties. - If
NOTIFY
is supported, adds a CCCD to the Battery Level characteristic. - Optionally, adds a Characteristic Presentation Format descriptor.
- Starts the service.
- Manages the actual battery level value (e.g., by reading an ADC connected to a battery voltage divider, or simulating it).
- Handles read requests for the Battery Level from connected clients.
- If notifications are enabled by a client (via CCCD write), sends notifications when the battery level changes significantly.
%%{init: {"theme": "base", "themeVariables": { "primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937", "fontSize": "14px", "fontFamily": "\"Open Sans\", sans-serif" }}}%% graph TD A[/"<b>Start: Init BLE & GATTS</b><br>Register GATTS Callback<br>Register GATTS App ID"\]:::start B["Create Service (BAS 0x180F)<br><code class='code-mermaid'>esp_ble_gatts_create_service()</code>"]:::process C{"Service Created?<br>(<code class='code-mermaid'>ESP_GATTS_CREATE_EVT</code>)"}:::decision D["Add Battery Level Char (0x2A19)<br>Properties: READ, NOTIFY<br><code class='code-mermaid'>esp_ble_gatts_add_char()</code>"]:::process E{"Char Added?<br>(<code class='code-mermaid'>ESP_GATTS_ADD_CHAR_EVT</code>)"}:::decision F["Add CCCD (0x2902)<br>Permissions: R/W<br><code class='code-mermaid'>esp_ble_gatts_add_char_descr()</code>"]:::process G{"CCCD Added?<br>(<code class='code-mermaid'>ESP_GATTS_ADD_CHAR_DESCR_EVT</code>)"}:::decision H["Add CPF Descriptor (0x2904)<br>Permissions: R<br><code class='code-mermaid'>esp_ble_gatts_add_char_descr()</code>"]:::process I{"CPF Added?<br>(<code class='code-mermaid'>ESP_GATTS_ADD_CHAR_DESCR_EVT</code>)"}:::decision J["Start Service<br><code class='code-mermaid'>esp_ble_gatts_start_service()</code>"]:::process K{"Service Started?<br>(<code class='code-mermaid'>ESP_GATTS_START_EVT</code>)"}:::decision L["Start Advertising"]:::process M[/"<b>Service Ready & Advertising</b>"\]:::success subgraph Client_Interaction ["Client Interaction (Post-Connection)"] direction TB R1["Client Reads Battery Level<br>(<code class='code-mermaid'>ESP_GATTS_READ_EVT</code>)"]:::process_client R2["Server Responds with Value<br>(Auto or <code class='code-mermaid'>esp_ble_gatts_send_response()</code>)"]:::process W1["Client Writes to CCCD<br>(<code class='code-mermaid'>ESP_GATTS_WRITE_EVT</code>)"]:::process_client W2["Server Updates Notification State<br>Responds if <code class='code-mermaid'>need_rsp</code>"]:::process N1["Battery Level Changes"]:::process N2{"Notifications Enabled?"}:::decision_client N3["Send Notification<br><code class='code-mermaid'>esp_ble_gatts_send_indicate()</code>"]:::process end M --> R1 M --> W1 M --> N1 N1 --> N2 N2 -- Yes --> N3 N2 -- No --> N1 A --> B B --> C C -- OK --> D D --> E E -- OK --> F F --> G G -- OK --> H H --> I I -- OK --> J J --> K K -- OK --> L L --> M C -- Fail --> Z{Error Handling}:::error E -- Fail --> Z G -- Fail --> Z I -- Fail --> Z K -- Fail --> Z classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B classDef process_client fill:#FFF7ED,stroke:#F97316,color:#9A3412 classDef decision_client fill:#FFFBEB,stroke:#F59E0B,color:#B45309
- Client (e.g., Smartphone, another ESP32):
- Scans and connects to the server.
- Discovers services, looking for the Battery Service UUID (
0x180F
). - If BAS is found, discovers characteristics within it, looking for the Battery Level UUID (
0x2A19
). - Reads the Battery Level characteristic value.
- Optionally, discovers descriptors for the Battery Level. If CCCD is present and notifications are desired:
- Writes
0x0001
to the CCCD to enable notifications. - Registers locally to handle incoming notifications.
- Writes
- Receives and processes Battery Level notifications.
%%{init: {"theme": "base", "themeVariables": { "primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937", "fontSize": "14px", "fontFamily": "\"Open Sans\", sans-serif" }}}%% graph TD A[/"<b>Start: Init BLE & GATTC</b><br>Register GATTC Callback<br>Register GATTC App ID"\]:::start B["Scan for Devices"]:::process C{"Device Found?"}:::decision D["Connect to Device<br><code class='code-mermaid'>esp_ble_gattc_open()</code>"]:::process E{"Connected?<br>(<code class='code-mermaid'>ESP_GATTC_OPEN_EVT</code>)"}:::decision F["Discover Services (Filter for BAS 0x180F)<br><code class='code-mermaid'>esp_ble_gattc_search_service()</code>"]:::process G{"BAS Found?<br>(<code class='code-mermaid'>ESP_GATTC_SEARCH_RES_EVT</code> & <code class='code-mermaid'>ESP_GATTC_SEARCH_CMPL_EVT</code>)"}:::decision H["Discover Battery Level Char (0x2A19)<br><code class='code-mermaid'>esp_ble_gattc_get_char_by_uuid()</code>"]:::process I{"Char Found?<br>(<code class='code-mermaid'>ESP_GATTC_GET_CHAR_EVT</code>)"}:::decision J["Read Battery Level<br><code class='code-mermaid'>esp_ble_gattc_read_char()</code>"]:::process K{"Read Complete?<br>(<code class='code-mermaid'>ESP_GATTC_READ_CHAR_EVT</code>)"}:::decision L["(Optional) Discover CCCD for Battery Level (0x2902)<br><code class='code-mermaid'>esp_ble_gattc_get_descr_by_uuid()</code>"]:::process M{"CCCD Found?<br>(<code class='code-mermaid'>ESP_GATTC_GET_DESCR_EVT</code>)"}:::decision N["Write 0x0001 to CCCD<br><code class='code-mermaid'>esp_ble_gattc_write_char_descr()</code>"]:::process O{"Write Complete?<br>(<code class='code-mermaid'>ESP_GATTC_WRITE_DESCR_EVT</code>)"}:::decision P["Register for Notifications<br><code class='code-mermaid'>esp_ble_gattc_register_for_notify()</code>"]:::process Q[/"<b>Receiving Notifications</b><br>(<code class='code-mermaid'>ESP_GATTC_NOTIFY_EVT</code>)"\]:::success A --> B B --> C C -- Yes --> D D --> E E -- OK --> F F --> G G -- Yes --> H H --> I I -- Yes --> J J --> K K -- Value Received --> L L --> M M -- Yes --> N N --> O O -- OK --> P P --> Q C -- No / Timeout --> B E -- Fail --> B G -- No --> F I -- No --> F K -- Fail --> J M -- No --> J O -- Fail --> N classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B
Practical Examples
Prerequisites:
- An ESP32 board (ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2).
- VS Code with the Espressif IDF Extension.
- For testing: A BLE scanner app (e.g., “nRF Connect for Mobile”) or another ESP32 to act as a client.
Example 1: ESP32 as a GATT Server with Battery Service
This example creates a GATT server that exposes the Battery Service. The battery level will be simulated and will decrease over time, sending notifications if a client is subscribed.
1. Project Setup:
- Create a new ESP-IDF project.
idf.py menuconfig
: Ensure Bluetooth is enabled, Bluedroid selected, and BLE Only mode.
2. Code (main/bas_server_main.c
):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_gatt_common_api.h"
#define BAS_TAG "BAS_SERVER"
#define DEVICE_NAME "ESP32_BAS_Device"
// Battery Service
#define GATTS_SERVICE_UUID_BAS 0x180F
// Battery Level Characteristic
#define GATTS_CHAR_UUID_BATTERY_LEVEL 0x2A19
// Characteristic Presentation Format Descriptor
#define GATTS_DESCR_UUID_CHAR_FORMAT 0x2904
// Client Characteristic Configuration Descriptor
#define GATTS_DESCR_UUID_CCC 0x2902
#define PROFILE_NUM 1
#define PROFILE_BAS_APP_ID 0
#define SVC_INST_ID 0 // Service instance ID
// Attribute handles
enum {
BAS_IDX_SVC, // Battery Service Declaration
BAS_IDX_BATT_LVL_CHAR, // Battery Level Characteristic Declaration
BAS_IDX_BATT_LVL_VAL, // Battery Level Characteristic Value
BAS_IDX_BATT_LVL_CCC, // Battery Level CCCD
BAS_IDX_BATT_LVL_PRES_FMT, // Battery Level Presentation Format Descriptor
BAS_IDX_NB,
};
uint16_t bas_handle_table[BAS_IDX_NB];
// Current battery level (0-100%)
static uint8_t current_battery_level = 100;
// Timer to simulate battery discharge
TimerHandle_t battery_sim_timer;
// Client connection information
typedef struct {
esp_gatt_if_t gatt_if;
uint16_t conn_id;
bool notifications_enabled;
} client_conn_info_t;
static client_conn_info_t connected_client = {0}; // Simplified for one client
// Characteristic Presentation Format value for Battery Level (percentage)
static const uint8_t batt_level_pres_fmt[7] = {
0x04, // Format: uint8
0x00, // Exponent: 0
0xAD, // Unit: Percentage (0x27AD LSB first)
0x27, // Unit: Percentage (MSB)
0x01, // Namespace: Bluetooth SIG Assigned Numbers
0x00, // Description: LSB
0x00 // Description: MSB
};
// 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,
};
// Advertising data
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,
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};
// Function to send battery level notification
static void send_battery_level_notification(void) {
if (connected_client.conn_id != 0 && connected_client.notifications_enabled) {
ESP_LOGI(BAS_TAG, "Sending battery level notification: %d%%", current_battery_level);
esp_ble_gatts_send_indicate(connected_client.gatt_if,
connected_client.conn_id,
bas_handle_table[BAS_IDX_BATT_LVL_VAL],
sizeof(current_battery_level),
¤t_battery_level,
false); // false for notification, true for indication
}
}
// Timer callback to simulate battery discharge
void battery_simulation_timer_cb(TimerHandle_t xTimer) {
if (current_battery_level > 0) {
current_battery_level--;
} else {
current_battery_level = 100; // Reset for demo
}
ESP_LOGI(BAS_TAG, "Simulated Battery Level: %d%%", current_battery_level);
send_battery_level_notification();
}
static void gatts_profile_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(BAS_TAG, "REG_EVT, status %d, app_id %d, gatts_if %d", param->reg.status, param->reg.app_id, gatts_if);
if (param->reg.status == ESP_GATT_OK) {
connected_client.gatt_if = gatts_if; // Store for this profile
} else {
ESP_LOGE(BAS_TAG, "GATTS App registration failed"); return;
}
esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(DEVICE_NAME);
if (set_dev_name_ret){ ESP_LOGE(BAS_TAG, "set device name failed, error code = %x", set_dev_name_ret); }
esp_err_t adv_config_ret = esp_ble_gap_config_adv_data(&adv_data);
if (adv_config_ret){ ESP_LOGE(BAS_TAG, "config adv data failed, error code = %x", adv_config_ret); }
// Create Battery Service
esp_gatt_srvc_id_t service_id;
service_id.is_primary = true;
service_id.id.uuid.len = ESP_UUID_LEN_16;
service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_BAS;
esp_ble_gatts_create_service(gatts_if, &service_id, BAS_IDX_NB); // BAS_IDX_NB is num_handles
break;
case ESP_GATTS_CREATE_EVT:
ESP_LOGI(BAS_TAG, "CREATE_SVC_EVT, status %d, service_handle %d", param->create.status, param->create.service_handle);
if (param->create.status == ESP_GATT_OK) {
bas_handle_table[BAS_IDX_SVC] = param->create.service_handle;
// Add Battery Level Characteristic
esp_bt_uuid_t char_uuid;
char_uuid.len = ESP_UUID_LEN_16;
char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_BATTERY_LEVEL;
esp_attr_value_t char_val = {
.attr_max_len = sizeof(current_battery_level),
.attr_len = sizeof(current_battery_level),
.attr_value = ¤t_battery_level, // Initial value
};
// Properties: Read + Notify. Permissions: Read.
esp_ble_gatts_add_char(param->create.service_handle, &char_uuid,
ESP_GATT_PERM_READ,
ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_NOTIFY,
&char_val, NULL); // Auto response for read
}
break;
case ESP_GATTS_ADD_CHAR_EVT:
ESP_LOGI(BAS_TAG, "ADD_CHAR_EVT, status %d, attr_handle %d, service_handle %d, char_uuid %04x",
param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle, param->add_char.char_uuid.uuid.uuid16);
if (param->add_char.status == ESP_GATT_OK) {
if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_BATTERY_LEVEL) {
bas_handle_table[BAS_IDX_BATT_LVL_CHAR] = param->add_char.attr_handle; // Char decl handle
// The value handle is typically attr_handle + 1 if no other descriptors are between decl and value
// However, ESP-IDF examples often use the attr_handle from ADD_CHAR_EVT as the value handle for notifications
// For clarity, let's assume ADD_CHAR_EVT's attr_handle IS the value handle for simple cases
// or more robustly, you'd use the handle from a separate ADD_CHAR_VAL event if that existed,
// or manage handles more explicitly if you build the table manually.
// For esp_ble_gatts_send_indicate, the value handle is needed.
// The stack creates the value attribute implicitly. The handle from ADD_CHAR_EVT is for the declaration.
// Let's assume the value handle is param->add_char.attr_handle + 1 for now.
// A more robust way is to query handles or use a predefined table structure.
// For simplicity, we'll use a fixed offset logic for this example,
// assuming value is right after declaration.
bas_handle_table[BAS_IDX_BATT_LVL_VAL] = param->add_char.attr_handle; // This is usually the characteristic declaration handle
// The actual value handle is typically +1
// Add CCCD for Battery Level
esp_bt_uuid_t cccd_uuid;
cccd_uuid.len = ESP_UUID_LEN_16;
cccd_uuid.uuid.uuid16 = GATTS_DESCR_UUID_CCC;
esp_attr_value_t cccd_val = {
.attr_max_len = 2, // CCCD is 2 bytes
.attr_len = 2,
.attr_value = (uint8_t[]){0x00, 0x00} // Initial: notifications disabled
};
esp_ble_gatts_add_char_descr(param->add_char.service_handle, &cccd_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
&cccd_val, NULL); // Auto response for read/write
}
}
break;
case ESP_GATTS_ADD_CHAR_DESCR_EVT:
ESP_LOGI(BAS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d, descr_uuid %04x",
param->add_char_descr.status, param->add_char_descr.attr_handle,
param->add_char_descr.service_handle, param->add_char_descr.descr_uuid.uuid.uuid16);
if (param->add_char_descr.status == ESP_GATT_OK) {
if (param->add_char_descr.descr_uuid.uuid.uuid16 == GATTS_DESCR_UUID_CCC) {
bas_handle_table[BAS_IDX_BATT_LVL_CCC] = param->add_char_descr.attr_handle;
// Add Characteristic Presentation Format Descriptor for Battery Level
esp_bt_uuid_t pres_fmt_uuid;
pres_fmt_uuid.len = ESP_UUID_LEN_16;
pres_fmt_uuid.uuid.uuid16 = GATTS_DESCR_UUID_CHAR_FORMAT;
esp_attr_value_t pres_fmt_val = {
.attr_max_len = sizeof(batt_level_pres_fmt),
.attr_len = sizeof(batt_level_pres_fmt),
.attr_value = (uint8_t*)batt_level_pres_fmt,
};
esp_ble_gatts_add_char_descr(param->add_char_descr.service_handle, &pres_fmt_uuid,
ESP_GATT_PERM_READ,
&pres_fmt_val, NULL); // Auto response
} else if (param->add_char_descr.descr_uuid.uuid.uuid16 == GATTS_DESCR_UUID_CHAR_FORMAT) {
bas_handle_table[BAS_IDX_BATT_LVL_PRES_FMT] = param->add_char_descr.attr_handle;
// All attributes for BAS added, now start the service
esp_ble_gatts_start_service(bas_handle_table[BAS_IDX_SVC]);
}
}
break;
case ESP_GATTS_START_EVT:
ESP_LOGI(BAS_TAG, "SERVICE_START_EVT, status %d, service_handle %d", param->start.status, param->start.service_handle);
break;
case ESP_GATTS_CONNECT_EVT:
ESP_LOGI(BAS_TAG, "CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:, gatts_if %d",
param->connect.conn_id,
param->connect.remote_bda[0], param->connect.remote_bda[1], param->connect.remote_bda[2],
param->connect.remote_bda[3], param->connect.remote_bda[4], param->connect.remote_bda[5],
gatts_if);
connected_client.conn_id = param->connect.conn_id;
connected_client.gatt_if = gatts_if; // Update gatt_if for this connection
connected_client.notifications_enabled = false;
// Stop advertising on connection if desired, or allow multiple connections
// esp_ble_gap_stop_advertising();
break;
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(BAS_TAG, "DISCONNECT_EVT, conn_id %d, reason %d", param->disconnect.conn_id, param->disconnect.reason);
connected_client.conn_id = 0;
connected_client.notifications_enabled = false;
// Restart advertising to allow new connections
esp_ble_gap_start_advertising(&adv_params);
break;
case ESP_GATTS_READ_EVT:
ESP_LOGI(BAS_TAG, "READ_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d",
param->read.conn_id, param->read.trans_id, param->read.handle);
// For Battery Level value, if auto_rsp is not used, respond here:
if (param->read.handle == bas_handle_table[BAS_IDX_BATT_LVL_VAL]) {
// This example uses auto_rsp for the value, so stack handles it.
// If manual:
// esp_gatt_rsp_t rsp;
// memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
// rsp.attr_value.handle = param->read.handle;
// rsp.attr_value.len = sizeof(current_battery_level);
// rsp.attr_value.value[0] = current_battery_level;
// esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp);
}
break;
case ESP_GATTS_WRITE_EVT:
ESP_LOGI(BAS_TAG, "WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d, len %d, value:",
param->write.conn_id, param->write.trans_id, param->write.handle, param->write.len);
esp_log_buffer_hex(BAS_TAG, param->write.value, param->write.len);
if (param->write.handle == bas_handle_table[BAS_IDX_BATT_LVL_CCC] && param->write.len == 2) {
uint16_t cccd_val = (param->write.value[1] << 8) | param->write.value[0];
if (cccd_val == 0x0001) {
ESP_LOGI(BAS_TAG, "Notifications ENABLED for Battery Level");
connected_client.notifications_enabled = true;
// Send initial notification
send_battery_level_notification();
} else if (cccd_val == 0x0000) {
ESP_LOGI(BAS_TAG, "Notifications DISABLED for Battery Level");
connected_client.notifications_enabled = false;
} else {
ESP_LOGE(BAS_TAG, "Invalid CCCD value: 0x%04x", cccd_val);
}
}
// Send response if needed (this example uses auto_rsp for CCCD)
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;
case ESP_GATTS_CONF_EVT: // Confirmation for indication
ESP_LOGI(BAS_TAG, "CONF_EVT, status %d, handle %d", param->conf.status, param->conf.handle);
break;
default:
break;
}
}
static void 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(BAS_TAG, "Advertising data set complete");
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(BAS_TAG, "Advertising started successfully");
} else {
ESP_LOGE(BAS_TAG, "Advertising start failed, error status = %x", param->adv_start_cmpl.status);
}
break;
// ... other GAP events ...
default:
break;
}
}
void app_main(void) {
esp_err_t ret;
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);
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret) { ESP_LOGE(BAS_TAG, "Initialize controller failed: %s", esp_err_to_name(ret)); return; }
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) { ESP_LOGE(BAS_TAG, "Enable controller failed: %s", esp_err_to_name(ret)); return; }
ret = esp_bluedroid_init();
if (ret) { ESP_LOGE(BAS_TAG, "Init Bluedroid failed: %s", esp_err_to_name(ret)); return; }
ret = esp_bluedroid_enable();
if (ret) { ESP_LOGE(BAS_TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret)); return; }
ret = esp_ble_gatts_register_callback(gatts_profile_event_handler);
if (ret) { ESP_LOGE(BAS_TAG, "GATTS register error: %s", esp_err_to_name(ret)); return; }
ret = esp_ble_gap_register_callback(gap_event_handler);
if (ret) { ESP_LOGE(BAS_TAG, "GAP register error: %s", esp_err_to_name(ret)); return; }
ret = esp_ble_gatts_app_register(PROFILE_BAS_APP_ID);
if (ret) { ESP_LOGE(BAS_TAG, "GATTS app register error: %s", esp_err_to_name(ret)); return; }
// Create and start battery simulation timer (e.g., every 5 seconds)
battery_sim_timer = xTimerCreate("BatterySimTimer", pdMS_TO_TICKS(5000), pdTRUE, (void *)0, battery_simulation_timer_cb);
if (battery_sim_timer == NULL || xTimerStart(battery_sim_timer, 0) != pdPASS) {
ESP_LOGE(BAS_TAG, "Failed to start battery simulation timer");
} else {
ESP_LOGI(BAS_TAG, "Battery simulation timer started.");
}
ESP_LOGI(BAS_TAG, "BAS Server Initialized. Advertising will start after GATTS registration.");
}
Note on Handle Management: The
bas_handle_table
and its indexing (BAS_IDX_SVC
, etc.) are crucial. The order ofesp_ble_gatts_add_char
andesp_ble_gatts_add_char_descr
calls determines the actual handles assigned by the stack. The example assumes a specific order. In more complex scenarios with multiple characteristics or services, robust handle management is key. Theattr_handle
inESP_GATTS_ADD_CHAR_EVT
is for the characteristic declaration. The characteristic value handle is implicitly created by the stack, usually immediately after the declaration handle. For notifications, you use the characteristic value handle. The example simplifies this by usingbas_handle_table[BAS_IDX_BATT_LVL_VAL]
which should ideally point to the value handle. A common pattern isvalue_handle = char_declaration_handle + 1
.
3. Build, Flash, and Monitor:
idf.py build
idf.py flash monitor
4. Observe Output & Test with BLE Scanner App:
- Serial monitor: Logs for GATTS registration, service/characteristic/descriptor creation, advertising start, and simulated battery level changes.
- BLE Scanner App (e.g., nRF Connect for Mobile):
- Scan and connect to “ESP32_BAS_Device”.
- Discover services. You should find the Battery Service (UUID
0x180F
). - Discover characteristics. You should find Battery Level (UUID
0x2A19
).- Read its value. It should show the current simulated percentage.
- Inspect its descriptors. You should see the CCCD and Characteristic Presentation Format. The CPF should indicate it’s a uint8 percentage.
- Enable notifications for the Battery Level characteristic by writing
0100
to its CCCD. - You should start receiving notifications on your scanner app as the simulated battery level on the ESP32 decreases. The ESP32’s serial monitor will also log these notification attempts.
Example 2: ESP32 as a GATT Client Reading Battery Service
This example shows an ESP32 client connecting to a device (like the server from Example 1) and interacting with its Battery Service.
(This client example would be similar in structure to the GATT client in Chapter 59. Key steps specific to BAS:
- Scan for devices advertising the Battery Service UUID (
0x180F
) or connect to a known device. - Upon connection (
ESP_GATTC_OPEN_EVT
), initiate service discovery for BAS (esp_ble_gattc_search_service
with UUID0x180F
). - In
ESP_GATTC_SEARCH_RES_EVT
, store the service start/end handles. - In
ESP_GATTC_SEARCH_CMPL_EVT
, if BAS was found, discover the Battery Level characteristic (esp_ble_gattc_get_char_by_uuid
with UUID0x2A19
) within the service’s handle range. - In
ESP_GATTC_GET_CHAR_EVT
, store the Battery Level characteristic’s value handle and properties. Check if it supportsREAD
andNOTIFY
. - Read the initial Battery Level:
esp_ble_gattc_read_char()
using its value handle. Process the result inESP_GATTC_READ_CHAR_EVT
. - If
NOTIFY
is supported, discover its CCCD handle (esp_ble_gattc_get_descr_by_uuid
with UUID0x2902
). - In
ESP_GATTC_GET_DESCR_EVT
, store the CCCD handle. - Write
0x0001
to the CCCD handle usingesp_ble_gattc_write_char_descr()
. - In
ESP_GATTC_WRITE_DESCR_EVT
(for CCCD write success), callesp_ble_gattc_register_for_notify()
with the Battery Level characteristic’s value handle. - Handle incoming notifications in ESP_GATTC_NOTIFY_EVT, parse the uint8_t value, and display the battery percentage.Due to the complexity and length, a full runnable client code is omitted here but follows the structure from Chapter 59, adapted for BAS.)
Variant Notes
- ESP32-S2: This variant does not have Bluetooth hardware and cannot implement BLE services or act as a BLE client.
- ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2: All these variants fully support the implementation of standard BLE services like the Battery Service, both as a server and as a client. The ESP-IDF APIs for GATT server and client operations are consistent across these BLE-enabled chips. The underlying BLE controller and host stack handle the standard profile requirements.
The implementation of standard services like BAS is primarily a software concern at the host stack level, making it portable across ESP32 variants that have BLE capabilities.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Incorrect UUIDs Used | Standard clients (e.g., nRF Connect, OS Bluetooth settings) do not recognize the service as “Battery Service” or the characteristic as “Battery Level”. May show “Unknown Service/Characteristic”. | Ensure correct Bluetooth SIG adopted UUIDs: – Battery Service: 0x180F – Battery Level Char.: 0x2A19 – CCCD: 0x2902 – Char. Presentation Format: 0x2904 . |
Missing CCCD for Notifiable Characteristic | Battery Level characteristic has NOTIFY property, but clients cannot enable notifications (e.g., “Enable Notifications” option is missing or writing to CCCD fails). | If the Battery Level characteristic supports NOTIFY, a Client Characteristic Configuration Descriptor (CCCD, UUID 0x2902 ) must be added to it. Ensure its permissions are READ and WRITE. |
Incorrect CCCD Write Handling (Server) | Client writes 0x0001 to CCCD, but server doesn’t start sending notifications, or doesn’t stop when 0x0000 is written. |
In ESP_GATTS_WRITE_EVT on the server: 1. Check if param->write.handle matches the CCCD handle. 2. Check if param->write.len is 2. 3. Interpret param->write.value (e.g., (value[1] << 8) | value[0] ) as 0x0001 (enable) or 0x0000 (disable). 4. Store this notification state per client connection ( conn_id , gatts_if ). |
Incorrect Characteristic Presentation Format (CPF) Value | Client reads battery level but displays it incorrectly (e.g., as signed, wrong unit, or "Unknown Format"). | Ensure the 7-byte value for the CPF descriptor (UUID 0x2904 ) is correct for Battery Level: [0x04, 0x00, 0xAD, 0x27, 0x01, 0x00, 0x00] . (Format: uint8, Exponent: 0, Unit: Percentage (0x27AD), Namespace: SIG, Desc: 0). Remember unit is LSB first (AD, 27). |
Client Not Calling esp_ble_gattc_register_for_notify() |
Client writes to CCCD successfully, server is ready to send notifications, but client's ESP_GATTC_NOTIFY_EVT is never triggered. |
After the client successfully writes to the CCCD (confirmed by ESP_GATTC_WRITE_DESCR_EVT ), it must call esp_ble_gattc_register_for_notify(gattc_if, server_bda, char_handle) for the Battery Level characteristic's value handle. |
Handle Mismatch for Notifications/Indications (Server) | Server calls esp_ble_gatts_send_indicate() (or notify) but uses the characteristic declaration handle instead of the value handle. Error occurs or notifications not received. |
Use the correct attribute handle for the characteristic's value when sending notifications/indications. This is typically char_declaration_handle + 1 if no other descriptors are between declaration and value. Manage handles carefully using your attribute table (e.g., bas_handle_table[BAS_IDX_BATT_LVL_VAL] should point to the value handle). |
Attribute Table Handle Mismanagement | Service/characteristic/descriptor creation fails, or wrong handles are used for operations, leading to unexpected behavior or errors. | Carefully define your attribute handle enumeration (e.g., BAS_IDX_SVC , etc.) and ensure bas_handle_table is populated correctly in the respective GATTS events (ESP_GATTS_CREATE_EVT , ESP_GATTS_ADD_CHAR_EVT , ESP_GATTS_ADD_CHAR_DESCR_EVT ). The order of adding attributes matters for implicit handle assignment. |
Forgetting to Start Service | Service and characteristics are created, but clients cannot discover them. | After all characteristics and descriptors for a service are added, call esp_ble_gatts_start_service(service_handle) . This is often done in the event handler for the last descriptor added. |
Exercises
- Client-Side Battery Level Display:
- Implement the ESP32 GATT client part (conceptualized in Example 2) to connect to your ESP32 BAS server.
- Read the initial battery level.
- Subscribe to notifications.
- Print the battery level to the serial monitor whenever it's read or a notification is received.
- Multiple Battery Service Instances (Advanced Conceptual):
- Conceptually design how you would modify the server to expose two instances of the Battery Service (e.g., for a device with a main battery and a backup battery).
- How would a client differentiate between them? (Hint: Service handles would be different. Clients discover all instances).
- Note: This is not typical for BAS, as BAS is usually for the device's primary battery, but it's a good exercise in understanding service instances. You don't need to fully implement, just outline the GATTS calls and client discovery logic.
Summary
- The BLE Battery Service (BAS, UUID
0x180F
) is a standard service for reporting a device's battery level. - It contains one mandatory characteristic: Battery Level (UUID
0x2A19
), auint8_t
value from 0-100%. - The Battery Level characteristic must be readable and can optionally support notifications.
- If notifications are supported, a Client Characteristic Configuration Descriptor (CCCD, UUID
0x2902
) is mandatory. - A Characteristic Presentation Format Descriptor (CPF, UUID
0x2904
) is recommended to describe the value as auint8
percentage. - Implementing standard services like BAS promotes interoperability with generic BLE clients.
- Both server (exposing BAS) and client (consuming BAS) sides involve specific GATT operations and event handling.
Further Reading
- Bluetooth SIG - Battery Service Specification (BAS): https://www.bluetooth.com/specifications/specs/bas-1-1/
- Bluetooth SIG - Core Specification Supplement: (On bluetooth.com) Part B, Section 1.1 ("Format Types") and Part D ("Units") for understanding CPF values: https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-54/out/en/index-en.html
- ESP-IDF API Reference -
esp_gatts_api.h
andesp_gattc_api.h
: https://github.com/espressif/esp-idf/blob/master/components/bt/host/bluedroid/api/include/api/esp_gatts_api.h - ESP-IDF
gatt_server_service_table
example: Can be adapted to include the Battery Service. https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/bluedroid/ble/gatt_server_service_table/tutorial/Gatt_Server_Service_Table_Example_Walkthrough.md - ESP-IDF
gatt_client_demo
example: Can be adapted to discover and read the Battery Service.
