ESP32: advertise as a BLE peripheral with the GATT server pattern
Make the ESP32 show up as a Bluetooth device that a phone can see and connect to. The foundation for any BLE-based IoT project.
The ESP32 has Bluetooth Low Energy built in. You do not need a module add-on. You can make the ESP32 show up as a Bluetooth device that any phone or laptop can scan for and connect to.
This is the foundation for any BLE-based IoT project: a sensor that publishes data over BLE, a configurable device that a phone app can adjust, a remote control that triggers events. The ESP32 is the “peripheral” (the device exposing data), and the phone is the “central” (the device reading data).
This tutorial covers the GATT server pattern: services, characteristics, and notifications. The GATT client pattern is in the next tutorial.
What BLE is (in one paragraph)
BLE is a low-power version of Bluetooth designed for IoT. It works like this:
- A peripheral advertises its presence. Phones can see it in the Bluetooth scan list.
- A phone or laptop as the central connects to the peripheral.
- Once connected, they exchange data through the GATT (Generic Attribute Profile), which is a tree of services and characteristics.
A service is a logical grouping (e.g. “Battery service”). A characteristic is a single data point (e.g. “battery level” is a single characteristic with a read value). The peripheral can also notify the central when a characteristic changes, which is how you stream live sensor data over BLE without polling.
What you need
- ESP32 dev board
- A phone or laptop with BLE (any phone made after 2015 has BLE)
- The Arduino ESP32 board package, version 2.x or later
The code: a basic BLE peripheral
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#define SERVICE_UUID "4fafc201-1bc5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Starting BLE...");
BLEDevice::init("ESP32-BLE-Example");
BLEServer *server = BLEDevice::createServer();
BLEService *service = server->createService(SERVICE_UUID);
BLECharacteristic *characteristic = service->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
characteristic->setValue("Hello from ESP32!");
characteristic->addDescriptor(new BLE2902());
service->start();
BLEAdvertising *advertising = BLEDevice::getAdvertising();
advertising->addServiceUUID(SERVICE_UUID);
advertising->setScanResponse(true);
advertising->setMinPreferred(0x06);
advertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("BLE peripheral ready. Scan with your phone.");
}
void loop() {
delay(1000);
}
Upload. Open the Serial Monitor. Look at your phone’s Bluetooth scan list. You should see “ESP32-BLE-Example.” Connect with the nRF Connect app (free, available on iOS and Android). You can read the characteristic value (“Hello from ESP32!”).
The service and characteristic UUIDs
The UUIDs 4fafc201-... and beb5483e-... are random. You generate your
own. The convention is to use UUIDs that are unique to your project so
your device does not collide with someone else’s.
Use https://www.uuidgenerator.net or uuidgen on macOS/Linux. The
standard BLE services (battery, heart rate, etc.) have assigned short
UUIDs; you do not need those for custom data.
Adding a writable characteristic
Read-only is not enough for most projects. Add a writable characteristic for sending commands from the phone to the ESP32:
BLECharacteristic *commandChar = service->createCharacteristic(
COMMAND_CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_WRITE);
commandChar->setCallbacks(new CommandCallback());
Where CommandCallback is a class that handles writes:
class CommandCallback : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *characteristic) {
String value = characteristic->getValue();
Serial.print("Received: ");
Serial.println(value);
if (value == "ON") {
digitalWrite(LED_BUILTIN, HIGH);
} else if (value == "OFF") {
digitalWrite(LED_BUILTIN, LOW);
}
}
};
Now your phone app can write “ON” or “OFF” to the characteristic, and the ESP32 turns the LED on or off in response.
Notifying when a value changes
Read-once is fine for static data, but most sensor data changes over time. Notifications let the peripheral push updates to the central without being polled:
BLECharacteristic *sensorChar = service->createCharacteristic(
SENSOR_CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
sensorChar->addDescriptor(new BLE2902());
float lastReading = 0;
void loop() {
float reading = analogRead(34) * 3.3 / 4095.0; // some sensor
if (reading != lastReading) {
sensorChar->setValue(reading);
sensorChar->notify();
lastReading = reading;
}
delay(100);
}
The phone app subscribes to notifications on SENSOR_CHARACTERISTIC_UUID,
and the ESP32 pushes updates whenever the value changes. This is how you
stream live sensor data over BLE.
The
BLE2902descriptor is the standard “Client Characteristic Configuration Descriptor.” It tells the phone app that this characteristic supports notifications. Without it, notifications do not work.
The disconnect and reconnect problem
If the phone moves out of range or the user disables Bluetooth, the connection drops. The peripheral needs to handle that and accept new connections:
class ServerCallback : public BLEServerCallbacks {
void onConnect(BLEServer *server) {
Serial.println("Connected");
}
void onDisconnect(BLEServer *server) {
Serial.println("Disconnected");
BLEDevice::startAdvertising(); // resume advertising
}
};
void setup() {
// ... after BLEDevice::createServer() ...
server->setCallbacks(new ServerCallback());
}
Without startAdvertising() on disconnect, the ESP32 stops being
discoverable after the first connection drops. Phone apps cannot find it
again.
Power consumption
BLE on the ESP32 uses about 30 mA when actively connected. Compare to Wi-Fi at 80-200 mA. For battery-powered projects, BLE is the right call when the phone does not need to be online (most sensor monitoring apps).
For even lower power, the ESP32 can advertise and only wake up on connection. The deep sleep + BLE tutorial covers this in the book ESP32 Low Power.
What you learned
- The ESP32 is a BLE peripheral out of the box. No module needed.
- Services are containers, characteristics are the actual data points.
- Read, write, and notify are the three operations a characteristic supports.
- Notifications let you stream data without polling.
When something breaks
- Phone does not see the ESP32. The phone BLE is off (check Settings), the ESP32 is not advertising (Serial Monitor should show “BLE peripheral ready”), or the advertisement is being filtered by the OS (rare on phones, common on some laptops).
- Phone connects but cannot read. Wrong characteristic UUID in the app. Check the Serial Monitor output.
- Notifications do not arrive. Missing
BLE2902descriptor on the characteristic. Or the phone app is not subscribed. - ESP32 stops being discoverable after one connection. Missing
startAdvertising()on disconnect callback.
What to build next
- The BLE central tutorial makes the ESP32 read from other BLE devices (heart rate straps, temperature sensors, beacons).
- The ESP32 MQTT tutorial combines with this for a sensor that publishes over both BLE and Wi-Fi.
- The book ESP32 IoT Projects covers custom BLE apps for iOS and Android using React Native.