In this guide, you’ll learn how to set up the ESP32 as a BLE Peripheral (or BLE Server) with an Environmental Sensing Service. This service exposes measurement data from environmental sensors and supports a wide range of environmental parameters like temperature, humidity, pressure, and others. As an example, we’ll use the measurements from a BME280 sensor, but this can be applied to any other sensor. This is a great tutorial to help you understand the BLE protocol.
New to the ESP32? Start here: Getting Started with the ESP32 Development Board.
Table of Contents:
- Introducing Bluetooth Low Energy (BLE)
- Project Overview
- Preparing your Smartphone
- Building the Circuit
- ESP32 – Creating an Environmental Sensing BLE Service (Arduino sketch)
Introducing Bluetooth Low Energy (BLE)
The ESP32 comes not only with Wi-Fi but also with Bluetooth and Bluetooth Low Energy (BLE).
Bluetooth Low Energy, BLE for short, is a power-conserving variant of Bluetooth. BLE’s primary application is short-distance transmission of small amounts of data (low bandwidth). Unlike Bluetooth which is always on, BLE remains in sleep mode constantly except for when a connection is initiated.
This makes it consume very little power. BLE consumes approximately 100x less power than Bluetooth (depending on the use case). You can check the main differences between Bluetooth and Bluetooth Low Energy here.
BLE Server and Client
With Bluetooth Low Energy, there are two types of devices: the server (also called peripheral) and the client. The ESP32 can act either as a client or as a server.
The server advertises its existence, so it can be found by other devices and contains data that the client can read. The client scans the nearby devices, and when it finds the server it is looking for, it establishes a connection and listens for incoming data. This is called point-to-point communication and this is the communication mode we’ll use with the ESP32.
GATT
GATT stands for Generic Attributes and it defines a hierarchical data structure that is exposed to connected BLE devices. This means that GATT defines the way that two BLE devices send and receive standard messages. Understanding this hierarchy is important because it will make it easier to understand how to use BLE with the ESP32.
- Profile: standard collection of services for a specific use case;
- Service: collection of related information, like sensor readings, battery level, heart rate, etc. ;
- Characteristic: it is where the actual data is saved on the hierarchy (value);
- Descriptor: metadata about the data;
- Properties: describes how the characteristic value can be interacted with. For example: read, write, notify, broadcast, indicate, etc.
For a more in-depth introduction to BLE with the ESP32, read the following guide: Getting Started with ESP32 Bluetooth Low Energy (BLE) on Arduino IDE
UUID
Each service, characteristic, and descriptor have a UUID (Universally Unique Identifier). A UUID is a unique 128-bit (16 bytes) number. For example:
55072829-bc9e-4c53-938a-74a6d4c78776
There are shortened default UUIDs for all types, services, and profiles specified in the SIG (Bluetooth Special Interest Group). We’ll use the default UUIDs for the Environmental Sensing Service, and for the temperature, humidity, and pressure characteristics.
If your application needs its own UUID, you can generate it using this UUID generator website.
In summary, the UUID is used for uniquely identifying information. For example, it can identify a particular service provided by a Bluetooth device.
Project Overview
In our example, we’ll create an Environmental Sensing Service with three characteristics. One for the temperature, another for the humidity, and another for the pressure.
The actual temperature, humidity, and pressure readings are saved on the value under their characteristics. Each characteristic has the notify property so that it notifies the client whenever the values change.
We’re going to use the default UUIDs for the Environmental Sensing Profile and corresponding characteristics.
If you go to this page and open the Assigned Numbers Document (PDF), you’ll find all the default assigned UUID numbers. If you search for the Environmental Sensing Service, you’ll find all the permitted characteristics that you can use with that service. You can see that it supports, temperature, humidity, and pressure.
There’s a table with the UUIDs for all services. You can see that the UUID for the Environmental Sensing service is 0x181A.
Then, search for the temperature, humidity, and pressure characteristics UUIDs. You’ll find a table with the values for all characteristics. The UUIDs for the temperature, humidity, and pressure are:
- pressure: 0x2A6D
- temperature: 0x2A6E
- humidity: 0x246F
Preparing your Smartphone
To check if the ESP32 BLE Server was created properly and receive temperature, humidity, and pressure notifications, we’ll use an app on the smartphone.
Most modern smartphones should have BLE capabilities. You can search for your smartphone specifications to check if it has BLE or not.
Note: the smartphone can act as a client or as a server. In this case, it will be the client that connects to the ESP32 BLE server.
For our tests, we’ll be using a free app called nRF Connect for Mobile from Nordic. It works on Android (Google Play Store) and iOS (App Store). Go to Google Play Store or App Store, search for “nRF Connect for Mobile” and install the app.
Parts Required
For this tutorial, you’ll need the following parts:
- ESP32 Board – read ESP32 Development Boards Review and Comparison
- BME280 sensor module – check the BME280 getting started guide with the ESP32
- Breadboard
- Jumper wires
For this example, we’ll use a BME280 sensor, but you can easily modify the code to use any other sensor you’re familiar with.
Building the Circuit
For this particular example, we’ll use a BME280 sensor. So, you need to wire a BME280 sensor to your ESP32. You can also use any other sensor you’re familiar with.
Schematic Diagram
We’re going to use I2C communication with the BME280 sensor module. For that, wire the sensor to the default ESP32 SCL (GPIO 22) and SDA (GPIO 21) pins, as shown in the following schematic diagram.
Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
ESP32 – Creating an Environmental Sensing BLE Service
We’ll program the ESP32 using Arduino IDE. So, you need to have the ESP32 boards installed on the IDE. Follow the next tutorial if you haven’t already:
Here are the steps to create an ESP32 BLE peripheral with an Environmental Sensing BLE service with temperature, humidity, and pressure, characteristics:
- Create a BLE device (server) with a name of your choice (we’ll call it ESP32_BME2820, but you can call it any other name).
- Create an Environmental Sensing service (UUID: 0x181A).
- Add characteristics to that service:
- pressure: 0x2A6D
- temperature: 0x2A6E
- humidity: 0x246F
- Add descriptors to the characteristics.
- Start the BLE server.
- Start advertising so BLE clients can connect and read the characteristics.
- Once a connection is established with a client, it will write new values on the characteristics and will notify the client, every time there’s a change.
Copy the following code to the Arduino IDE and upload it to your board.
/*
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-ble-server-environmental-sensing-service/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
//BLE server name
#define bleServerName "ESP32_BME280"
// Default UUID for Environmental Sensing Service
// https://www.bluetooth.com/specifications/assigned-numbers/
#define SERVICE_UUID (BLEUUID((uint16_t)0x181A))
// Temperature Characteristic and Descriptor (default UUID)
// Check the default UUIDs here: https://www.bluetooth.com/specifications/assigned-numbers/
BLECharacteristic temperatureCharacteristic(BLEUUID((uint16_t)0x2A6E), BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor temperatureDescriptor(BLEUUID((uint16_t)0x2902));
// Humidity Characteristic and Descriptor (default UUID)
BLECharacteristic humidityCharacteristic(BLEUUID((uint16_t)0x2A6F), BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor humidityDescriptor(BLEUUID((uint16_t)0x2902));
// Pressure Characteristic and Descriptor (default UUID)
BLECharacteristic pressureCharacteristic(BLEUUID((uint16_t)0x2A6D), BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor pressureDescriptor(BLEUUID((uint16_t)0x2902));
// Create a sensor object
Adafruit_BME280 bme;
// Init BME280
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
bool deviceConnected = false;
//Setup callbacks onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("Device Connected");
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("Device Disconnected");
}
};
void setup() {
// Start serial communication
Serial.begin(115200);
// Start BME sensor
initBME();
// Create the BLE Device
BLEDevice::init(bleServerName);
// Create the BLE Server
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Create the BLE Service
BLEService *bmeService = pServer->createService(SERVICE_UUID);
// Create BLE Characteristics and corresponding Descriptors
bmeService->addCharacteristic(&temperatureCharacteristic);
temperatureCharacteristic.addDescriptor(&temperatureDescriptor);
bmeService->addCharacteristic(&humidityCharacteristic);
humidityCharacteristic.addDescriptor(&humidityDescriptor);
bmeService->addCharacteristic(&pressureCharacteristic);
pressureCharacteristic.addDescriptor(&pressureDescriptor);
// Start the service
bmeService->start();
// Start advertising
pServer->getAdvertising()->start();
Serial.println("Waiting a client connection to notify...");
}
void loop() {
if (deviceConnected) {
// Read temperature as Celsius (the default)
float t = bme.readTemperature();
// Read humidity
float h = bme.readHumidity();
// Read pressure
float p = bme.readPressure()/100.0F;
//Notify temperature reading
uint16_t temperature = (uint16_t)t;
//Set temperature Characteristic value and notify connected client
temperatureCharacteristic.setValue(temperature);
temperatureCharacteristic.notify();
Serial.print("Temperature Celsius: ");
Serial.print(t);
Serial.println(" ºC");
//Notify humidity reading
uint16_t humidity = (uint16_t)h;
//Set humidity Characteristic value and notify connected client
humidityCharacteristic.setValue(humidity);
humidityCharacteristic.notify();
Serial.print("Humidity: ");
Serial.print(h);
Serial.println(" %");
//Notify pressure reading
uint16_t pressure = (uint16_t)p;
//Set humidity Characteristic value and notify connected client
pressureCharacteristic.setValue(pressure);
pressureCharacteristic.notify();
Serial.print("Pressure: ");
Serial.print(p);
Serial.println(" hPa");
delay(10000);
}
}
How does the Code Work?
Continue reading to learn how the code works or skip to the Demonstration section.
Importing libraries
You start by importing the required libraries: the libraries to use BLE and the libraries to interface with the BME280 sensor.
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
BLE server name
In the following line, you can define the name of your BLE device. We’ll call it ESP32_BME2820, but you can call it any other name.
//BLE server name
#define bleServerName "ESP32_BME280"
Bluetooth UUIDs
We’re using default UUIDs for services and characteristics already defined in the SIG (Bluetooth Special Interest Group). To report temperature, humidity, and pressure there’s a service called Environmental Sensing that supports temperature, humidity, and pressure characteristics as we’ve seen previously.
You can find all the default UUIDs for profiles and characteristics in the following document: https://www.bluetooth.com/specifications/assigned-numbers/ .
We create an Environmental Sensing Service on the following line:
#define SERVICE_UUID (BLEUUID((uint16_t)0x181A))
Then, we create the temperature characteristic as follows:
BLECharacteristic temperatureCharacteristic(BLEUUID((uint16_t)0x2A6E), BLECharacteristic::PROPERTY_NOTIFY);
This line of code is creating a BLE characteristic named temperatureCharacteristic with a UUID of 0x2A6E (representing the “Temperature” characteristic) and configuring it to support notifications (PROPERTY_NOTIFY) – this will allow other BLE devices to subscribe to and receive notifications when the temperature value changes on the ESP32.
Then, we create a descriptor for the temperature characteristic. A descriptor provides additional information about a characteristic and how it should be accessed or interpreted. Our descriptor has the default UUID 0x2902 and it represents the “Client Characteristic Configuration” descriptor.
BLEDescriptor temperatureDescriptor(BLEUUID((uint16_t)0x2902));
The “Client Characteristic Configuration” descriptor, with UUID 0x2902, is commonly used in BLE to configure how notifications and indications are handled by the client (usually a central device like a smartphone) when it subscribes to a characteristic’s notifications.
This descriptor allows the client to configure how it wants to receive updates (e.g., notifications) from the associated characteristic, giving more control over the communication between the ESP32 and the connected BLE devices.
We proceed in a similar way to create the temperature and pressure characteristics and corresponding descriptors.
// Humidity Characteristic and Descriptor (default UUID)
BLECharacteristic humidityCharacteristic(BLEUUID((uint16_t)0x2A6F), BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor humidityDescriptor(BLEUUID((uint16_t)0x2902));
// Pressure Characteristic and Descriptor (default UUID)
BLECharacteristic pressureCharacteristic(BLEUUID((uint16_t)0x2A6D), BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor pressureDescriptor(BLEUUID((uint16_t)0x2902));
Initialize the BME280 Sensor
The following line creates an Adafruit_BME280 object to refer to the sensor called bme.
// Create a sensor object
Adafruit_BME280 bme;
The initBME() function initializes the sensor. It will be called later in the setup().
// Init BME280
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
}
Learn more about the BME280 sensor: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).
BLE Callback functions
Then, we need to set up callback functions for when the BLE device connects (onConnect) or disconnects (onDisconnect).
//Setup callbacks onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("Device Connected");
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("Device Disconnected");
}
};
We create a boolean variable called deviceConnected that keeps track of whether a Bluetooth device is currently connected to the ESP32.
bool deviceConnected = false;
The onConnect function changes the deviceConnected variable to true.
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("Device Connected");
};
And the onDisconnect, changes it to false.
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("Device Disconnected");
}
setup()
In the setup(), start the serial port at a baud rate of 115200:
Serial.begin(115200);
Initialize the BME280 sensor by calling the initBME() function we created previously.
// Start BME sensor
initBME();
Create a new BLE device with the BLE server name you’ve defined earlier:
// Create the BLE Device
BLEDevice::init(bleServerName);
Set the BLE device as a server and assign the callback functions.
// Create the BLE Server
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
Creating the BLE Service and Characteristics
Continuing with the setup(), start a BLE service with the service UUID defined earlier.
// Create the BLE Service
BLEService *bmeService = pServer->createService(SERVICE_UUID);
Then, create the temperature, humidity, and pressure BLE characteristics using the UUIDs you defined earlier and assign the corresponding descriptors.
// Create BLE Characteristics and corresponding Descriptors
bmeService->addCharacteristic(&temperatureCharacteristic);
temperatureCharacteristic.addDescriptor(&temperatureDescriptor);
bmeService->addCharacteristic(&humidityCharacteristic);
humidityCharacteristic.addDescriptor(&humidityDescriptor);
bmeService->addCharacteristic(&pressureCharacteristic);
pressureCharacteristic.addDescriptor(&pressureDescriptor);
Starting the Service and the Advertising
Finally, you start the service, and the server begins the advertising so other devices can find it.
// Start the service
bmeService->start();
// Start advertising
pServer->getAdvertising()->start();
loop()
The loop() function is fairly straightforward. You constantly check if the device is connected or not. If it’s connected, it reads the current temperature, humidity, and pressure.
if (deviceConnected) {
// Read temperature as Celsius
float t = bme.readTemperature();
// Read humidity
float h = bme.readHumidity();
// Read pressure
float p = bme.readPressure()/100.0F;
Then, convert the readings to uint16_t format (unsigned 16-bit integer), a suitable format to use in BLE.
uint16_t temperature = (uint16_t)t;
The following two lines update the current characteristic value (using .setValue()) and send it to the connected client (using .notify()).
//Set temperature Characteristic value and notify connected client
temperatureCharacteristic.setValue(temperature);
temperatureCharacteristic.notify();
There are also three lines to print the temperature in the Serial Monitor for debugging purposes.
Serial.print("Temperature Celsius: ");
Serial.print(t);
Serial.println(" ºC");
Sending the humidity and pressure uses the same process.
//Notify humidity reading
uint16_t humidity = (uint16_t)h;
//Set humidity Characteristic value and notify connected client
humidityCharacteristic.setValue(humidity);
humidityCharacteristic.notify();
Serial.print("Humidity: ");
Serial.print(h);
Serial.println(" %");
//Notify pressure reading
uint16_t pressure = (uint16_t)p;
//Set humidity Characteristic value and notify connected client
pressureCharacteristic.setValue(pressure);
pressureCharacteristic.notify();
Serial.print("Pressure: ");
Serial.print(p);
Serial.println(" hPa");
The delay function waits 10 seconds between readings.
delay(10000);
Demonstration
Upload the code to your board. After uploading, open the Serial Monitor, and restart the ESP32 by pressing the RST/EN button. You should get a similar message in the Serial Monitor.
This means everything is working as expected and the ESP32 is waiting for a BLE client to connect.
Then, go to your smartphone, open the nRF Connect app from Nordic, and start scanning for new devices. You should find a device called ESP32_BME280—this is the BLE server name you defined earlier.
Connect to it. You’ll see that it displays the Environmental Sensing service with the temperature, humidity, and pressure characteristics. Click on the arrows to activate the notifications.
Then, click on the second icon at the left to change the format. You can change to unsigned int for all characteristics. You’ll start seeing the temperature, humidity, and pressure values being reported every 10 seconds.
You should also get the readings on the Serial Monitor.
Congratulations! You’ve successfully created an ESP32 BLE Peripheral that advertises the Environmental Sensing Service. Now, you can develop an app, or program another ESP32 to interface with the ESP32 BLE device.
Wrapping Up
In this tutorial, you learned how to create a BLE device with the ESP32 with the default UUIDs defined by the SIG. As an example, we created an Environmental Sensing Service with temperature, humidity, and pressure characteristics. The Environmental Sensing Service also supports many other characteristics. So, you can easily modify this project to work with other sensors. Or you can also create a different Service to advertise other types of characteristics—the workflow is the same.
We hope you found this tutorial useful and that it has helped you understand more about BLE protocol with the ESP32.
We have other BLE and Bluetooth Classic tutorials that you may find useful:
- Getting Started with ESP32 Bluetooth Low Energy (BLE) on Arduino IDE
- ESP32 BLE Server and Client (Bluetooth Low Energy)
- ESP32 Bluetooth Classic with Arduino IDE – Getting Started
Learn more about the ESP32 with our resources:
- Learn ESP32 with Arduino IDE (eBook)
- Build Web Servers with ESP32 and ESP8266 (eBook)
- Firebase Web App with ESP32 and ESP8266 (eBook)
- SMART HOME with Raspberry Pi, ESP32, and ESP8266
- Free ESP32 Projects and Tutorials…
Thanks for reading.
Hi two the both of you at RNT!
I’m wondering where to actually insert isFahrenheit = true, as commented in the code.
Hi.
I’m sorry. That was a copy/paste error on my part.
That’s only available on the DHT sensor.
For the BME280 to display in Fahrenheit you need to do the conversion yourself in the code.
tempF=1.8 * bme.readTemperature() + 32;
Regards,
Sara
Great new project! It worked right away. Plug and Play! I didn’t even bother reading most of your article. Maybe I should: the temperature and humidity readings in the app are only 10 percent of the expected values, and I get no pressure reading. I promise I will read the article and start all over. 🙂
Your code appears to be mostly working. However I get the following error message once connected: Incorrect data length (16bit expected) (0x) 00
I am using a Samsung Galaxy 5 smart phone.
Your instructions say “Then, click on the second icon at the left to change the format.”
I don’t see those icons on my version of nRF Connect.
is therre some other data format I can use besides int16_t ??
Hi.
I’m not sure… but I think that option might not be available on Android smartphone :/
Regards,
Sara
Where/how are the units for the temperatureCharacteristic.setValue() defined? If I search for examples with 2A6E, it seems the default is degrees C with DecimalExponent -2 (meaning units are .01degC). I wasn’t able to see where the DecimalExponent is defined. An older xml viewer seems to have the -2 exponent associated with 2A6E. I came across an alternate API for BLEDescriptor with an argument for units with DecimalExponent, but didn’t see documentation on the default.
Also, shouldn’t the temperature be int16_t?
Very good tutorial! Keep the good work, Rui! There are a tutorial on this subject with micropython? TY!
Hi.
At the moment, we don’t have micropython tutorials with BLE.
Bue, we’ll add that to our to-do list.
Regards,
Rui
Very good article. Ofcourse the Nordc app is handy to start with, but when I get around to it, i will build a dedicated app that just shows the sensor values without all the unnecessary ‘noise’. But your article already is a great start
Hi.
We’ll do that in a future tutorial that we’ll publish next month.
This is just a quick introduction to the topic.
Regards,
Sara
Great!
Hi !
Thanks for all the interistng projects – I have learnt very much also for my BLE project. I’m trying to setup a battery driven sensor. To save battery I must include deep-sleep at the server, however sometimes, the client lost the connection.
I’m wondering wether you could include this topic in your project?
Best regards Franz
Great project very usefull tips. Thanks for the effort.