Bluetooth Low Energy (BLE)

The earlier lessons were about networks that use IP addresses. Bluetooth Low Energy (BLE) is different: it is a short-range radio protocol that lets a small, battery-powered device such as a heart rate strap, a temperature sensor or a smart lock talk to a phone or a microcontroller, without Wi-Fi and without a router. This lesson explains how devices find each other, how they organise their data, and how a phone reads a value from your own ESP32.

What Is BLE?

BLE was introduced with Bluetooth 4.0 and is part of every newer Bluetooth version. It works in the 2.4 GHz band and is designed for small bursts of data and very low power use. A BLE sensor can spend almost all of its time asleep, wake up for a few milliseconds to send a reading, and typically run for a long time on a coin cell. You will find it in wearables, beacons, door locks, fitness equipment and many hobby sensor projects.

Classic Bluetooth (BR/EDR)Bluetooth Low Energy
Designed forContinuous streamsSmall, occasional messages
Power useHigherVery low
Data patternA steady flow, like a cableShort bursts, often a few bytes at a time
Typical usesWireless headphones and speakers, serial-style linksSensors, wearables, beacons, locks, remote controls

The two are not compatible with each other: a BLE-only device cannot talk to a Classic-only device. Phones and dual-mode chips such as the ESP32 contain both radios, so they can do either (the ESP32 side is covered in Bluetooth & BLE on the ESP32).

Radio Basics

  • 40 channels, 2 MHz apart, spread across the 2.4 GHz band.
  • 3 advertising channels (numbers 37, 38 and 39) are used for being discovered and for starting a connection.
  • 37 data channels (numbers 0 to 36) are used once two devices are connected.
  • Frequency hopping. During a connection both sides jump to a different data channel for each connection event, following a pattern they agreed on. This helps because 2.4 GHz is crowded with Wi-Fi and other radios: if one channel is noisy, only a few events suffer.

BLE 5 added a few options, all of which need support on both devices. A 2 Mbit/s PHY sends bits twice as fast as the original 1 Mbit/s. A Coded PHY adds error-correction data to trade speed for better reach. Extended advertising allows much bigger advertising payloads. Real range and throughput depend heavily on the chip, the antenna, walls, interference and the phone, so treat any single number you read as a best case.

Roles and the Life of a Connection

Two pairs of names are used, and they describe different moments:

  • Before a connection: the advertiser (also called a broadcaster) sends small packets announcing itself, and the scanner (or observer) listens for them.
  • After a connection: the device that advertised and accepted the connection is the peripheral; the one that started it, usually your phone, is the central.
The life of a BLE connection A peripheral sends advertising packets on channels 37, 38 and 39 while a phone scans. The phone sends a connect request. The two devices then exchange data packets once per connection interval, hopping between data channels. Either side can end the connection. Peripheral for example a sensor Central for example a phone Advertising Advertising packet on channel 37 Advertising packet on channel 38 Advertising packet on channel 39 repeated after every advertising interval while the phone scans Connecting Connect request sent by the phone, in answer to an advertising packet Connected Packets both ways on data channel 12 One connection interval later, channel 25 reads, writes and notifications travel inside these packets Closing Disconnect (either side can end it) the peripheral can then start advertising again Before connecting: advertiser and scanner. After connecting: peripheral and central.
A device is discovered through its advertising packets. The connect request turns the broadcast into a two-way link that hops across data channels. The channel numbers 12 and 25 are only examples.

The time between two connection events is the connection interval. It is negotiated and typically ranges from a few milliseconds to a few seconds: a short interval means lower delay but more power, a long one saves power. A sensor that reports once a minute does not need a fast interval.

Advertising

An advertising packet is a tiny broadcast that anyone in range can hear. It is made of a few labelled fields: typically some flags, the device name, a list of service UUIDs the device offers, and sometimes manufacturer data (a few custom bytes, such as a sensor reading). In classic (legacy) advertising the payload is limited to 31 bytes, and a device may add a second 31-byte block, the scan response, that a scanner can ask for. Space goes quickly: one full 128-bit UUID alone takes 16 bytes. BLE 5 extended advertising lifts this limit.

Two settings pull in opposite directions. Advertising often makes the device appear quickly in a scan but uses more battery; advertising rarely saves power but makes discovery slow. A device can also advertise without allowing connections at all. Such an advertising-only device is called a beacon: it just broadcasts an identifier or a small reading, and phones react to it. iBeacon and Eddystone are two well-known beacon formats.

GATT: How the Data Is Organised

Once connected, devices need an agreed way to describe their data. BLE uses GATT (Generic Attribute Profile). The device that holds the data is the GATT server and the device that reads it is the GATT client. Usually the peripheral is the server and the phone is the client, but the two pairs of roles are separate ideas. The server arranges its data as a tree:

  • A service groups related data, for example everything about a battery.
  • A characteristic is one piece of data inside a service, with a value, a UUID and a set of allowed operations.
  • A descriptor belongs to a characteristic and holds extra information or a setting.
  • A profile is an agreement about which services a kind of device must offer, for example the Heart Rate profile.
A GATT tree with three standard services Under a profile there are three services. Battery Service 0x180F contains the Battery Level characteristic 0x2A19 with Read and Notify and a CCCD descriptor 0x2902. Heart Rate Service 0x180D contains the Heart Rate Measurement characteristic 0x2A37 with Notify and a CCCD descriptor. Device Information 0x180A contains Manufacturer Name String 0x2A29 and Model Number String 0x2A24, both Read. Profile: a use case built from services, such as Heart Rate Battery Service UUID 0x180F Battery Level UUID 0x2A19 Read, Notify CCCD, 0x2902 switches notify on Heart Rate Service UUID 0x180D Heart Rate Measurement UUID 0x2A37 Notify CCCD, 0x2902 switches notify on Device Information UUID 0x180A Manufacturer Name String UUID 0x2A29 Read Model Number String UUID 0x2A24 Read Profile Service Characteristic (the data) Descriptor (settings or extra info) A client discovers this tree after connecting, then reads, writes or subscribes to individual characteristics. The UUID tells it what each one means.
Three real standard services as a GATT server could offer them. Every box is found by its UUID; a client does not need to know anything else about the device in advance.

UUIDs

Every service and characteristic has a UUID. Standard ones are defined by the Bluetooth SIG and written as short 16-bit numbers such as 0x180F. They are really shorthand inside a fixed base: 0000xxxx-0000-1000-8000-00805F9B34FB. For your own custom services you generate a full 128-bit UUID (any UUID generator will do), so it cannot clash with a standard one.

What a Characteristic Can Do

Each characteristic declares which operations it allows:

PropertyMeaning
ReadThe client asks for the current value and gets one answer
WriteThe client sets the value and the server confirms it
Write without responseThe client sets the value without waiting for a confirmation, which is faster and good for things like a stream of control commands
NotifyThe server pushes a new value whenever it wants, without being asked and without any confirmation
IndicateLike notify, but the client must acknowledge each value, so the server knows it arrived

Notify and indicate do not start on their own. The client must first switch them on by writing to a descriptor called the Client Characteristic Configuration Descriptor (CCCD, UUID 0x2902). Writing 0x0001 enables notifications, 0x0002 enables indications and 0x0000 switches both off. This is the step people forget most often.

Read Versus Notify

A client that wants a battery level can read it, which means one request and one answer. To follow a value that changes, such as a heart rate, polling with reads would waste power and add delay. Instead the client subscribes once and the server sends a notification each time the value changes.

Reading a characteristic versus subscribing to notifications The client sends one read request for Battery Level and gets one read response. Then the client writes 0x0001 to the CCCD, the server confirms, and the server sends three notifications on its own. Finally the client writes 0x0000 to the CCCD to switch notifications off. GATT client for example a phone GATT server for example a sensor Read Read request: Battery Level (0x2A19) Read response: 0x57 (87 percent) one request, one answer; to see a change the client must ask again Subscribe Write 0x0001 to the CCCD (0x2902) Write response (OK) notifications are now switched on for this characteristic Notify Notification: 0x56 Notification: 0x55 Notification: 0x54 sent by the server whenever the value changes, with no further requests Stop Write 0x0000 to the CCCD Indications work the same way (the client writes 0x0002), but it must confirm each one.
A read is one request and one answer. A notification costs the client one write to the CCCD, after which the server decides when to send.

How Much Fits in One Notification

Attribute messages travel in packets limited by the ATT MTU (maximum transmission unit). Every connection starts with a default MTU of 23 bytes. A notification spends 3 of those bytes on its own header, which leaves about 20 bytes of your data. Either side can ask for a bigger MTU (up to 517 bytes) and both agree on a value they both support; many phones and libraries do this automatically, but you should not rely on it. If you have more data than fits, send it in several pieces or negotiate a larger MTU first.

The BLE Protocol Stack

Like TCP/IP, BLE is built in layers. Your application only sees the top of the stack, but knowing what the other layers do explains most error messages and settings. On many chips the lower two layers (the controller) run on the radio part of the chip and the upper layers (the host) run on the main processor, linked by a standard interface called HCI.

The BLE protocol stack From top to bottom: the application; GAP and GATT; ATT and the Security Manager; L2CAP; then below the HCI boundary the Link Layer and the PHY radio. Application Your code: show the heart rate, update the battery level GAP roles, advertising, scanning, and making connections GATT the data model: services, characteristics and descriptors ATT the read, write and notify messages that move attribute values SMP (Security Manager) pairing and the exchange of encryption keys L2CAP Carries ATT and SMP, splits and reassembles large packets host above, controller below (joined by HCI) Link Layer Advertising, connections, channel hopping, retransmission PHY (radio) The 2.4 GHz radio itself: 1 Mbit/s, 2 Mbit/s or Coded PHY
Everything you do with GATT is carried by ATT, which rides on L2CAP, which the Link Layer sends over the radio.

Pairing, Bonding and Privacy

A plain BLE connection is not encrypted. Pairing is the process in which two devices prove who they are to a certain degree and agree on keys to encrypt the link. How strong that is depends on the method:

  • Just Works: no user input; the devices simply exchange keys. It is convenient, but it does not protect against an active attacker who sits between the two devices while they pair.
  • Passkey entry: one device shows a 6-digit number and the user types it into the other. A person is involved in the check, so a man-in-the-middle is much harder.
  • Numeric comparison: both screens show a number and the user confirms that they match. It is available with LE Secure Connections, the more modern pairing scheme built on elliptic-curve key exchange.

Bonding means the devices store the keys from pairing, so on the next connection they can encrypt immediately without asking the user again. A characteristic can require an encrypted (or even an authenticated) link; if the data is sensitive, such as a door lock command or health data, make the server demand it rather than trusting that nobody is listening.

Privacy: if a device always advertised the same address, anyone could track it. Many devices therefore use random private addresses that change from time to time. A bonded partner can still recognise the device, because it holds a key that lets it resolve the changing address. Do not assume a BLE device keeps one fixed address.

Building Something: An ESP32 BLE Server

Here is the smallest useful example: an ESP32 offering one custom service with one characteristic that can be read and also notifies every second. It uses the Arduino BLE library for the ESP32. The basic flow (init, server, service, characteristic, advertise) is the one described in the ESP32 BLE lesson; here we add notify and a disconnect handler.

This page's code is a preview, not something you can run right on this website - this server runs PHP, not the ESP32's own firmware. Upload it from the Arduino IDE to your own board.
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLECharacteristic *pCharacteristic;
bool deviceConnected = false;
uint8_t counter = 0;

class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer *server) {
    deviceConnected = true;
  }
  void onDisconnect(BLEServer *server) {
    deviceConnected = false;
    server->getAdvertising()->start();   // become visible again
  }
};

void setup() {
  BLEDevice::init("ESP32-Sensor");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
  );
  pCharacteristic->addDescriptor(new BLE2902());   // the CCCD, see below
  pCharacteristic->setValue(&counter, 1);

  pService->start();
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->start();
}

void loop() {
  if (deviceConnected) {
    counter++;
    pCharacteristic->setValue(&counter, 1);
    pCharacteristic->notify();
  }
  delay(1000);
}
Note: The ESP32 BLE library has changed between Arduino-ESP32 core versions, and there is also a separate NimBLE-based library. Class names, the way the CCCD descriptor is created (BLE2902 is needed in older cores and deprecated in newer ones, which add it for you) and callback signatures depend on the core version you installed. If the sketch does not compile, check the examples that ship with your own core and see the ESP32 BLE lesson.

Test It with a Phone App

  1. Install a free BLE explorer such as nRF Connect or LightBlue on your phone.
  2. Scan. Find ESP32-Sensor in the list (the advertising packet carries the name or the service UUID).
  3. Connect, then open the unknown service with the UUID from the sketch.
  4. Use read on the characteristic to see the current byte, then enable notifications (the app writes the CCCD for you). The value should now change every second.

If you write your own Android app instead, scanning and connecting need runtime permissions: BLUETOOTH_SCAN and BLUETOOTH_CONNECT on Android 12 and newer, and a location permission on older versions. The steps are in Bluetooth on Android.

Common Mistakes

  • Forgetting that notifications must be enabled. The server can call notify() all day; nothing is sent until the client writes to the CCCD.
  • Sending more than the MTU allows. With the default MTU only about 20 bytes fit in one notification. Split the data or negotiate a larger MTU.
  • Choosing a bad advertising interval. Too frequent drains the battery; too rare makes the device look missing in a scan.
  • Assuming a fixed device address. Private addresses change. Identify a device by its name, service UUID or bonding, not by a remembered address alone.
  • Using Classic Bluetooth settings or tools. A BLE-only device cannot be used with Classic serial-port (SPP) apps, and a phone's normal Bluetooth settings screen may not list it at all. Use a BLE scanner and an app that speaks GATT.
  • Not handling disconnects. Connections drop when a device moves away. The server should start advertising again, and the client should be ready to reconnect and re-subscribe, because notification settings do not always survive a new connection unless the devices are bonded.
Recap: BLE is a low-power 2.4 GHz radio protocol for small bursts of data and is not compatible with Classic Bluetooth. A peripheral advertises on channels 37, 38 and 39; a central scans, sends a connect request, and the two then hop across the data channels at a negotiated connection interval. Data is organised by GATT into services, characteristics and descriptors found by UUID. A client can read a value or switch on notifications through the CCCD, and notifications are limited by the ATT MTU (about 20 bytes by default). Pairing and bonding add encryption, and anything sensitive should require an encrypted link.
Next steps: Go back to Protocols Home to compare all the protocols, or put BLE to work by reading real sensors in ADC & Reading Sensors.