BLE (Bluetooth Low Energy) Protocol

Technical reference for BLE — GAP advertising, GATT services and characteristics, UUIDs, and ESP32 BLE stack details

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

FeatureBLE (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 consumptionVery low ($10$--$50\,\text{mA}$ active, $\mu\text{A}$ sleeping)Higher ($30$--$100\,\text{mA}$ active)
Connection setupFast (~$3\,\text{ms}$ advertising interval possible)Slower (pairing/bonding handshake)
Max connectionsDepends on stack (ESP32: ~3--5 simultaneous)7 active devices
Pairing complexityOptional (many BLE devices skip pairing)Required
Audio streamingNot designed for it (LE Audio in BLE 5.2+)Yes (A2DP, HFP)
File transferPossible but slowYes (OBEX, FTP)
Use casesSensors, beacons, health monitors, smart homeAudio, 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

RoleDescriptionExamples
PeripheralAdvertises its presence and provides data via GATT servicesESP32 sensor node, heart rate monitor, smart lock
CentralScans for peripherals, initiates connections, reads/writes dataSmartphone, ESP32 gateway, laptop
BroadcasterAdvertises data without accepting connections (one-way)Beacon, temperature broadcast
ObserverScans for advertisements without connectingBeacon 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

LevelWhat it isExample
ProfileA collection of services for a use caseEnvironmental Sensing Profile
ServiceA group of related characteristicsEnvironmental Sensing Service (0x181A)
CharacteristicA single data point with propertiesTemperature (0x2A6E)
DescriptorMetadata about a characteristicClient 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, where xxxx is 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:

PropertyDescription
ReadCentral can read the value
WriteCentral can write a value (with acknowledgment)
Write Without ResponseCentral can write a value (no acknowledgment, faster)
NotifyPeripheral pushes value updates to the central (no acknowledgment)
IndicateLike 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

ServiceUUID (16-bit)Description
Generic Access0x1800Device name, appearance, connection parameters
Generic Attribute0x1801Service change indications
Device Information0x180AManufacturer, model, serial number, firmware version
Battery Service0x180FBattery level percentage
Heart Rate0x180DHeart rate measurement
Environmental Sensing0x181ATemperature, humidity, pressure, UV index
Automation IO0x1815Digital and analog I/O
User Data0x181CUser-specific data

🔗Common Standard Characteristic UUIDs

CharacteristicUUID (16-bit)FormatDescription
Temperature0x2A6Esint16 (0.01 C resolution)Temperature in Celsius multiplied by 100
Humidity0x2A6Fuint16 (0.01% resolution)Relative humidity multiplied by 100
Pressure0x2A6Duint32 (0.1 Pa resolution)Pressure in Pascals multiplied by 10
Battery Level0x2A19uint8 (0--100)Battery percentage
Heart Rate Measurement0x2A37CompoundFlags + heart rate value
Manufacturer Name0x2A29utf8sManufacturer name string
Model Number0x2A24utf8sModel number string
Firmware Revision0x2A26utf8sFirmware 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

  1. The peripheral broadcasts advertising packets at a set interval (typically $20\,\text{ms}$ to $10.24\,\text{s}$).
  2. Each packet contains up to $31\,\text{bytes}$ of data: device name, service UUIDs, TX power, manufacturer-specific data, or flags.
  3. An optional scan response provides an additional $31\,\text{bytes}$ when the central sends a scan request.
  4. The central scans on the three advertising channels (37, 38, 39) and collects advertising packets.
  5. The central can then connect to the peripheral or simply read the advertising data.

🔗Advertising Types

TypeConnectableScannableUse case
ADV_INDYesYesDefault -- allows connections and scan responses
ADV_DIRECT_INDYesNoDirected advertising to a specific central (fast reconnect)
ADV_SCAN_INDNoYesProvides data in scan response, no connections
ADV_NONCONN_INDNoNoBeacons -- broadcast-only, no interaction

🔗Advertising Data Format

Advertising data is a sequence of AD structures, each containing:

FieldSizeDescription
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 DataVariableThe actual data

Common AD types:

AD TypeCodeDescription
Flags0x01BLE capabilities (e.g., "LE General Discoverable" + "BR/EDR Not Supported")
Incomplete 16-bit UUIDs0x02Partial list of service UUIDs
Complete 16-bit UUIDs0x03Full list of service UUIDs
Complete Local Name0x09UTF-8 device name
TX Power Level0x0ATransmit power in dBm
Manufacturer Specific Data0xFFVendor 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 of 2350 means $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

MethodSecurity LevelUser InteractionUse case
Just WorksLow (no MITM protection)NoneSimple devices without a display or keyboard
Passkey EntryMediumUser enters a 6-digit codeDevices with a display or keyboard
Numeric ComparisonMedium-HighUser confirms matching numbers on both devicesDevices with displays on both sides
Out of Band (OOB)HighUses NFC or QR codeSpecial 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

ClassPurpose
BLEDeviceInitialize BLE, create server or client, set device name, MTU
BLEServerCreate a peripheral that other devices connect to
BLEClientConnect to another BLE peripheral as a central
BLEServiceCreate a service on the server
BLECharacteristicCreate a characteristic within a service
BLEAdvertisingConfigure and start advertising
BLEDescriptorAdd descriptors to characteristics
BLE2902CCCD descriptor (Client Characteristic Configuration -- required for notifications)
BLEScanScan for nearby BLE devices
BLEAdvertisedDeviceRepresents 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.

ConfigurationApproximate 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 Error messages 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

ProblemCauseFix
Device not found during scanPeripheral not advertising, or advertising on a different nameVerify BLEDevice::getAdvertising()->start() is called; check device name filter
Connection drops immediatelyMTU negotiation failure or incompatible parametersTry setting BLEDevice::setMTU(512) on both sides
Characteristic read returns emptyService not started before advertisingCall service->start() before starting advertising
Notifications not workingCCCD descriptor missing or not enabledAdd new BLE2902() to the characteristic; client must write 0x01 0x00 to CCCD
Only $20\,\text{bytes}$ receivedDefault MTU of 23 minus 3-byte ATT headerNegotiate a larger MTU: BLEDevice::setMTU(517)
Sketch too large to uploadBLE library increases binary size significantlyChange partition scheme to "No OTA" or "Huge APP"
Guru Meditation Error on BLE initOut of RAMReduce services/characteristics, disable WiFi, use ESP32 with PSRAM
Phone cannot find ESP32Advertising not set to discoverable, or phone BLE cache staleRestart advertising; on phone, toggle Bluetooth off/on or clear BLE cache
Reconnection fails after bondingBond keys corrupted or mismatchedClear bonding data: BLEDevice::init(""); BLEDevice::deinit(true); and re-pair
WiFi and BLE interfereBoth share the $2.4\,\text{GHz}$ radioAlternate between BLE and WiFi operations, or use connection intervals to reduce overlap

🔗Used In

The following pages on this site use BLE: