Bluetooth Low Energy (BLE) is a wireless protocol designed for short-range communication with minimal power consumption. It was introduced as part of Bluetooth 4.0 in 2010 and has since become the dominant standard for wearables, beacons, sensor tags, and IoT devices. The ESP32 has a built-in BLE radio alongside its classic Bluetooth and WiFi radios, making it a versatile platform for BLE projects.
BLE is fundamentally different from Bluetooth Classic. It was designed from the ground up for devices that send small amounts of data infrequently and need to run on coin cell batteries for months or years. Where Bluetooth Classic streams audio or transfers files, BLE excels at reading a temperature sensor every few seconds or toggling an LED from your phone.
🔗BLE vs Bluetooth Classic
| Feature | BLE (Bluetooth Low Energy) | Bluetooth Classic |
|---|---|---|
| Range | ~$100\,\text{m}$ (open air) | ~$100\,\text{m}$ (open air) |
| Data rate | ~$1\,\text{Mbps}$ (BLE 4.x), ~$2\,\text{Mbps}$ (BLE 5.0) | $1$--$3\,\text{Mbps}$ |
| Typical throughput | $10$--$100\,\text{kbps}$ (application-level) | $200$--$2000\,\text{kbps}$ |
| Power consumption | Very low ($10$--$50\,\text{mA}$ active, $\mu\text{A}$ sleeping) | Higher ($30$--$100\,\text{mA}$ active) |
| Connection setup | Fast (~$3\,\text{ms}$ advertising interval possible) | Slower (pairing/bonding handshake) |
| Max connections | Depends on stack (ESP32: ~3--5 simultaneous) | 7 active devices |
| Pairing complexity | Optional (many BLE devices skip pairing) | Required |
| Audio streaming | Not designed for it (LE Audio in BLE 5.2+) | Yes (A2DP, HFP) |
| File transfer | Possible but slow | Yes (OBEX, FTP) |
| Use cases | Sensors, beacons, health monitors, smart home | Audio, file transfer, input devices |
The ESP32 supports both BLE and Bluetooth Classic. However, they cannot run simultaneously in all configurations. Most ESP32 projects pick one or the other. For sensor data and control applications, BLE is almost always the better choice due to its lower power consumption and simpler data model.
Note: The ESP32-S3 supports BLE 5.0. The original ESP32 supports BLE 4.2. The ESP32-C3 supports BLE 5.0 but does not support Bluetooth Classic at all.
🔗How BLE Works
BLE communication revolves around two roles: a peripheral (the device that advertises and provides data) and a central (the device that scans, connects, and consumes data). In most ESP32 projects, the ESP32 acts as the peripheral and a smartphone or another ESP32 acts as the central.
sequenceDiagram
participant P as Peripheral (ESP32)
participant C as Central (Phone/ESP32)
P->>P: Start advertising
C->>C: Start scanning
P-->>C: Advertising packet
C->>P: Connection request
P->>C: Connection accepted
C->>P: Discover services
P->>C: Service list
C->>P: Read characteristic
P->>C: Characteristic value
C->>P: Enable notifications
P-->>C: Notify (value changed)
P-->>C: Notify (value changed)
C->>P: Disconnect🔗Roles Explained
| Role | Description | Examples |
|---|---|---|
| Peripheral | Advertises its presence and provides data via GATT services | ESP32 sensor node, heart rate monitor, smart lock |
| Central | Scans for peripherals, initiates connections, reads/writes data | Smartphone, ESP32 gateway, laptop |
| Broadcaster | Advertises data without accepting connections (one-way) | Beacon, temperature broadcast |
| Observer | Scans for advertisements without connecting | Beacon scanner, presence detector |
A single device can switch roles or even act as both central and peripheral (though this is advanced and resource-intensive on the ESP32).
🔗Connection vs Connectionless
BLE supports two modes of operation:
- Connectionless (advertising only): The peripheral broadcasts data in its advertising packets. Any observer can pick it up. No connection is formed. This is how beacons (iBeacon, Eddystone) work. Data is limited to $31\,\text{bytes}$ per advertising packet ($255\,\text{bytes}$ with BLE 5.0 extended advertising).
- Connection-based: A central connects to the peripheral and can then read, write, and subscribe to characteristics. This enables two-way communication, larger data transfers, and notification-based updates.
🔗GATT: Services and Characteristics
GATT (Generic Attribute Profile) defines how data is structured and exchanged over a BLE connection. It organizes data into a hierarchy:
graph TD
PR[Profile] --> S1[Service<br>e.g. Environmental Sensing]
PR --> S2[Service<br>e.g. Battery]
S1 --> C1[Characteristic<br>e.g. Temperature]
S1 --> C2[Characteristic<br>e.g. Humidity]
S2 --> C3[Characteristic<br>e.g. Battery Level]
C1 --> D1[Descriptor<br>e.g. CCCD]
C1 --> D2[Descriptor<br>e.g. User Description]
C2 --> D3[Descriptor<br>e.g. CCCD]🔗Hierarchy Breakdown
| Level | What it is | Example |
|---|---|---|
| Profile | A collection of services for a use case | Environmental Sensing Profile |
| Service | A group of related characteristics | Environmental Sensing Service (0x181A) |
| Characteristic | A single data point with properties | Temperature (0x2A6E) |
| Descriptor | Metadata about a characteristic | Client Characteristic Configuration (CCCD) -- enables/disables notifications |
🔗UUIDs
Every service and characteristic is identified by a UUID. There are two types:
- 16-bit UUIDs are assigned by the Bluetooth SIG for standard, well-known services and characteristics. They are shorthand for a full 128-bit UUID:
0000xxxx-0000-1000-8000-00805F9B34FB, wherexxxxis the 16-bit value. - 128-bit UUIDs are used for custom (vendor-specific) services and characteristics. You generate your own. Use a random UUID generator -- do not invent one manually.
🔗Characteristic Properties
Each characteristic declares what operations it supports:
| Property | Description |
|---|---|
| Read | Central can read the value |
| Write | Central can write a value (with acknowledgment) |
| Write Without Response | Central can write a value (no acknowledgment, faster) |
| Notify | Peripheral pushes value updates to the central (no acknowledgment) |
| Indicate | Like Notify, but the central must acknowledge receipt |
A single characteristic can have multiple properties. For example, a temperature characteristic might support both Read and Notify -- the central can read it on demand and also subscribe to automatic updates.
🔗Common Standard Service UUIDs
| Service | UUID (16-bit) | Description |
|---|---|---|
| Generic Access | 0x1800 | Device name, appearance, connection parameters |
| Generic Attribute | 0x1801 | Service change indications |
| Device Information | 0x180A | Manufacturer, model, serial number, firmware version |
| Battery Service | 0x180F | Battery level percentage |
| Heart Rate | 0x180D | Heart rate measurement |
| Environmental Sensing | 0x181A | Temperature, humidity, pressure, UV index |
| Automation IO | 0x1815 | Digital and analog I/O |
| User Data | 0x181C | User-specific data |
🔗Common Standard Characteristic UUIDs
| Characteristic | UUID (16-bit) | Format | Description |
|---|---|---|---|
| Temperature | 0x2A6E | sint16 (0.01 C resolution) | Temperature in Celsius multiplied by 100 |
| Humidity | 0x2A6F | uint16 (0.01% resolution) | Relative humidity multiplied by 100 |
| Pressure | 0x2A6D | uint32 (0.1 Pa resolution) | Pressure in Pascals multiplied by 10 |
| Battery Level | 0x2A19 | uint8 (0--100) | Battery percentage |
| Heart Rate Measurement | 0x2A37 | Compound | Flags + heart rate value |
| Manufacturer Name | 0x2A29 | utf8s | Manufacturer name string |
| Model Number | 0x2A24 | utf8s | Model number string |
| Firmware Revision | 0x2A26 | utf8s | Firmware version string |
Tip: You do not have to use standard UUIDs. For custom ESP32 projects, generating your own 128-bit UUIDs for services and characteristics is perfectly normal and often simpler. Standard UUIDs matter most when you want interoperability with existing apps (like phone health apps reading heart rate data).
🔗GAP: Advertising and Discovery
GAP (Generic Access Profile) controls how BLE devices discover each other. Before any GATT communication can happen, the central must find the peripheral through advertising.
🔗Advertising Flow
- The peripheral broadcasts advertising packets at a set interval (typically $20\,\text{ms}$ to $10.24\,\text{s}$).
- Each packet contains up to $31\,\text{bytes}$ of data: device name, service UUIDs, TX power, manufacturer-specific data, or flags.
- An optional scan response provides an additional $31\,\text{bytes}$ when the central sends a scan request.
- The central scans on the three advertising channels (37, 38, 39) and collects advertising packets.
- The central can then connect to the peripheral or simply read the advertising data.
🔗Advertising Types
| Type | Connectable | Scannable | Use case |
|---|---|---|---|
| ADV_IND | Yes | Yes | Default -- allows connections and scan responses |
| ADV_DIRECT_IND | Yes | No | Directed advertising to a specific central (fast reconnect) |
| ADV_SCAN_IND | No | Yes | Provides data in scan response, no connections |
| ADV_NONCONN_IND | No | No | Beacons -- broadcast-only, no interaction |
🔗Advertising Data Format
Advertising data is a sequence of AD structures, each containing:
| Field | Size | Description |
|---|---|---|
| Length | $1\,\text{byte}$ | Length of the AD structure (excluding the length byte itself) |
| AD Type | $1\,\text{byte}$ | Type code (e.g., 0x01 for Flags, 0x09 for Complete Local Name) |
| AD Data | Variable | The actual data |
Common AD types:
| AD Type | Code | Description |
|---|---|---|
| Flags | 0x01 | BLE capabilities (e.g., "LE General Discoverable" + "BR/EDR Not Supported") |
| Incomplete 16-bit UUIDs | 0x02 | Partial list of service UUIDs |
| Complete 16-bit UUIDs | 0x03 | Full list of service UUIDs |
| Complete Local Name | 0x09 | UTF-8 device name |
| TX Power Level | 0x0A | Transmit power in dBm |
| Manufacturer Specific Data | 0xFF | Vendor ID + custom data |
🔗Data Format
BLE characteristics exchange raw bytes. How those bytes are interpreted depends on the characteristic's format:
- Standard characteristics follow the Bluetooth SIG's defined formats. For example, the Temperature characteristic (
0x2A6E) is a signed 16-bit integer representing temperature in 0.01 degree Celsius increments. A value of2350means $23.50\,\text{°C}$. - Custom characteristics can use any format you define. Common approaches:
- Raw struct: Pack values into a C struct and send the bytes directly (fast, but both sides must agree on the layout).
- JSON string: Human-readable but wastes bandwidth (BLE MTU is typically $23$--$517\,\text{bytes}$).
- Packed integers: Multiply floats by a factor (e.g., temperature * 100), send as integers. Efficient and portable.
Note: The default BLE MTU (Maximum Transmission Unit) is $23\,\text{bytes}$, which means $20\,\text{bytes}$ of usable payload per characteristic read/write (3 bytes are ATT header). The ESP32 can negotiate a larger MTU (up to $517\,\text{bytes}$) with
BLEDevice::setMTU(517), but the other device must also support it.
🔗Security
BLE security is optional and configurable. Many simple ESP32 projects skip security entirely, especially during development.
🔗Pairing Methods
| Method | Security Level | User Interaction | Use case |
|---|---|---|---|
| Just Works | Low (no MITM protection) | None | Simple devices without a display or keyboard |
| Passkey Entry | Medium | User enters a 6-digit code | Devices with a display or keyboard |
| Numeric Comparison | Medium-High | User confirms matching numbers on both devices | Devices with displays on both sides |
| Out of Band (OOB) | High | Uses NFC or QR code | Special devices with NFC support |
🔗Bonding
Bonding stores the pairing keys so that reconnections skip the pairing process. The ESP32 can store bonded device information in flash memory. Without bonding, the pairing process repeats on every connection.
Note: The basic BLE examples in this site's tutorials skip security for simplicity. For any project exposed to the real world (smart locks, garage door openers), you should implement at least passkey pairing and encrypted characteristics.
🔗ESP32 BLE Stack
The ESP32 includes a full BLE stack accessible through the ESP32 BLE Arduino library (included with the ESP32 Arduino core -- no separate installation needed).
🔗Key Classes
| Class | Purpose |
|---|---|
BLEDevice | Initialize BLE, create server or client, set device name, MTU |
BLEServer | Create a peripheral that other devices connect to |
BLEClient | Connect to another BLE peripheral as a central |
BLEService | Create a service on the server |
BLECharacteristic | Create a characteristic within a service |
BLEAdvertising | Configure and start advertising |
BLEDescriptor | Add descriptors to characteristics |
BLE2902 | CCCD descriptor (Client Characteristic Configuration -- required for notifications) |
BLEScan | Scan for nearby BLE devices |
BLEAdvertisedDevice | Represents a device found during scanning |
🔗Typical Server Setup Flow
BLEDevice::init("MyESP32"); // Initialize BLE with device name
BLEServer* server = BLEDevice::createServer();
BLEService* service = server->createService(SERVICE_UUID);
BLECharacteristic* characteristic = service->createCharacteristic(
CHAR_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
characteristic->addDescriptor(new BLE2902()); // Enable notifications
service->start();
BLEDevice::getAdvertising()->start(); // Begin advertising🔗Typical Client Setup Flow
BLEDevice::init("");
BLEScan* scan = BLEDevice::getScan();
scan->setActiveScan(true);
BLEScanResults results = scan->start(5); // Scan for 5 seconds
// Connect to a found device
BLEClient* client = BLEDevice::createClient();
client->connect(device);
BLERemoteService* remoteService = client->getService(SERVICE_UUID);
BLERemoteCharacteristic* remoteChar = remoteService->getCharacteristic(CHAR_UUID);
std::string value = remoteChar->readValue();🔗Memory Considerations
BLE is memory-hungry on the ESP32. The BLE stack alone consumes approximately $170\,\text{KB}$ of RAM. On an ESP32 with $520\,\text{KB}$ total SRAM, that leaves roughly $350\,\text{KB}$ for your application, WiFi (if used simultaneously), and heap.
| Configuration | Approximate RAM usage |
|---|---|
| BLE only | ~$170\,\text{KB}$ |
| BLE + WiFi | ~$290\,\text{KB}$ |
| Available for application (BLE only) | ~$350\,\text{KB}$ |
| Available for application (BLE + WiFi) | ~$230\,\text{KB}$ |
Warning: If you see crashes, random reboots, or
Guru Meditation Errormessages after enabling BLE, you are likely running out of RAM. Reduce the number of services/characteristics, avoid large string operations, or choose between BLE and WiFi rather than running both.
🔗Partition Scheme
If your compiled sketch is too large to upload after enabling BLE, change the partition scheme in Arduino IDE under Tools > Partition Scheme to one with more app space. "No OTA (2MB APP/2MB SPIFFS)" or "Huge APP (3MB No OTA/1MB SPIFFS)" are common choices for BLE projects.
🔗Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Device not found during scan | Peripheral not advertising, or advertising on a different name | Verify BLEDevice::getAdvertising()->start() is called; check device name filter |
| Connection drops immediately | MTU negotiation failure or incompatible parameters | Try setting BLEDevice::setMTU(512) on both sides |
| Characteristic read returns empty | Service not started before advertising | Call service->start() before starting advertising |
| Notifications not working | CCCD descriptor missing or not enabled | Add new BLE2902() to the characteristic; client must write 0x01 0x00 to CCCD |
| Only $20\,\text{bytes}$ received | Default MTU of 23 minus 3-byte ATT header | Negotiate a larger MTU: BLEDevice::setMTU(517) |
| Sketch too large to upload | BLE library increases binary size significantly | Change partition scheme to "No OTA" or "Huge APP" |
Guru Meditation Error on BLE init | Out of RAM | Reduce services/characteristics, disable WiFi, use ESP32 with PSRAM |
| Phone cannot find ESP32 | Advertising not set to discoverable, or phone BLE cache stale | Restart advertising; on phone, toggle Bluetooth off/on or clear BLE cache |
| Reconnection fails after bonding | Bond keys corrupted or mismatched | Clear bonding data: BLEDevice::init(""); BLEDevice::deinit(true); and re-pair |
| WiFi and BLE interfere | Both share the $2.4\,\text{GHz}$ radio | Alternate between BLE and WiFi operations, or use connection intervals to reduce overlap |
🔗Used In
The following pages on this site use BLE:
- BLE Server: Expose Sensor Data -- set up the ESP32 as a BLE peripheral that exposes sensor readings
- BLE Client: Read Data from Nearby Devices -- connect to a BLE peripheral and read characteristic values