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 for | Continuous streams | Small, occasional messages |
| Power use | Higher | Very low |
| Data pattern | A steady flow, like a cable | Short bursts, often a few bytes at a time |
| Typical uses | Wireless headphones and speakers, serial-style links | Sensors, 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 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.
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:
| Property | Meaning |
|---|---|
| Read | The client asks for the current value and gets one answer |
| Write | The client sets the value and the server confirms it |
| Write without response | The client sets the value without waiting for a confirmation, which is faster and good for things like a stream of control commands |
| Notify | The server pushes a new value whenever it wants, without being asked and without any confirmation |
| Indicate | Like 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.
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.
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.
#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);
}
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
- Install a free BLE explorer such as nRF Connect or LightBlue on your phone.
- Scan. Find
ESP32-Sensorin the list (the advertising packet carries the name or the service UUID). - Connect, then open the unknown service with the UUID from the sketch.
- 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.