ESP32 BLE Server and Client (Bluetooth Low Energy)

Learn how to make a BLE (Bluetooth Low Energy) connection between two ESP32 boards. One ESP32 is going to be the server, and the other ESP32 will be the client. The BLE server advertises characteristics that contain sensor readings that the client can read. The ESP32 BLE client reads the values of those characteristics (temperature and humidity) and displays them on an OLED display.

ESP32 BLE Server and Client BLuetooth Low Energy Arduino IDE

Recommended Reading: Getting Started with ESP32 Bluetooth Low Energy (BLE)

What is Bluetooth Low Energy?

Before going straight to the project, it is important to take a quick look at some essential BLE concepts so that you’re able to better understand the project later on. If you’re already familiar with BLE, you can skip to the Project Overview section.

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 that is always on, BLE remains in sleep mode constantly except for when a connection is initiated.

This makes it consume very low 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 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.

BLE Client Server Server Advertising

There are other possible communication modes like broadcast mode and mesh network (not covered in this tutorial).

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.

GATT Hierarchy ESP32 BLE Server Client Example
  • 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: describe how the characteristic value can be interacted with. For example: read, write, notify, broadcast, indicate, etc.

In our example, we’ll create a service with two characteristics. One for the temperature and another for the humidity. The actual temperature and humidity 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.

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 UUIDs for all types, services, and profiles specified in the SIG (Bluetooth Special Interest Group).

But 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.

For a more detailed introduction about BLE, read our getting started guide:

Project Overview

In this tutorial, you’re going to learn how to make a BLE connection between two ESP32 boards. One ESP32 is going to be the BLE server, and the other ESP32 will be the BLE client.

ESP32 BLE Client Server OLED Display Demonstration

The ESP32 BLE server is connected to a BME280 sensor and it updates its temperature and humidity characteristic values every 30 seconds.

The ESP32 client connects to the BLE server and it is notified of its temperature and humidity characteristic values. This ESP32 is connected to an OLED display and it prints the latest readings.

This project is divided into two parts:

Parts Required

Here’s a list of the parts required to follow this project:

ESP32 BLE Server:

ESP32 BLE Client:

You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!

1) ESP32 BLE Server

In this part, we’ll set up the BLE Server that advertises a service that contains two characteristics: one for temperature and another for humidity. Those characteristics have the Notify property to notify new values to the client.

ESP32 BLE Server Connected to the nRF Connect App

Schematic Diagram

The ESP32 BLE server will advertise characteristics with temperature and humidity from a BME280 sensor. You can use any other sensor as long as you add the required lines in the code.

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.

ESP32 Wiring Circuit to BME280 Schematic Diagram

Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?

Installing BME280 Libraries

As mentioned previously, we’ll advertise sensor readings from a BME280 sensor. So, you need to install the libraries to interface with the BME280 sensor.

You can install the libraries using the Arduino Library Manager. Go to Sketch Include Library > Manage Libraries and search for the library name.

Installing Libraries (VS Code + PlatformIO)

If you’re using VS Code with the PlatformIO extension, copy the following to the platformio.ini file to include the libraries.

lib_deps = adafruit/Adafruit Unified Sensor @ ^1.1.4
            adafruit/Adafruit BME280 Library @ ^2.1.2  

ESP32 BLE Server – Code

With the circuit ready and the required libraries installed, copy the following code to the Arduino IDE, or to the main.cpp file if you’re using VS Code.

/*********
  Rui Santos
  Complete instructions at https://RandomNerdTutorials.com/esp32-ble-server-client/
  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 <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius

//BLE server name
#define bleServerName "BME280_ESP32"

Adafruit_BME280 bme; // I2C

float temp;
float tempF;
float hum;

// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

bool deviceConnected = false;

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"

// Temperature Characteristic and Descriptor
#ifdef temperatureCelsius
  BLECharacteristic bmeTemperatureCelsiusCharacteristics("cba1d466-344c-4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor bmeTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
  BLECharacteristic bmeTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor bmeTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2902));
#endif

// Humidity Characteristic and Descriptor
BLECharacteristic bmeHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae-186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeHumidityDescriptor(BLEUUID((uint16_t)0x2903));

//Setup callbacks onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
  };
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
  }
};

void initBME(){
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

void setup() {
  // Start serial communication 
  Serial.begin(115200);

  // Init 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 Create a BLE Descriptor
  // Temperature
  #ifdef temperatureCelsius
    bmeService->addCharacteristic(&bmeTemperatureCelsiusCharacteristics);
    bmeTemperatureCelsiusDescriptor.setValue("BME temperature Celsius");
    bmeTemperatureCelsiusCharacteristics.addDescriptor(&bmeTemperatureCelsiusDescriptor);
  #else
    bmeService->addCharacteristic(&bmeTemperatureFahrenheitCharacteristics);
    bmeTemperatureFahrenheitDescriptor.setValue("BME temperature Fahrenheit");
    bmeTemperatureFahrenheitCharacteristics.addDescriptor(&bmeTemperatureFahrenheitDescriptor);
  #endif  

  // Humidity
  bmeService->addCharacteristic(&bmeHumidityCharacteristics);
  bmeHumidityDescriptor.setValue("BME humidity");
  bmeHumidityCharacteristics.addDescriptor(new BLE2902());
  
  // Start the service
  bmeService->start();

  // Start advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pServer->getAdvertising()->start();
  Serial.println("Waiting a client connection to notify...");
}

void loop() {
  if (deviceConnected) {
    if ((millis() - lastTime) > timerDelay) {
      // Read temperature as Celsius (the default)
      temp = bme.readTemperature();
      // Fahrenheit
      tempF = 1.8*temp +32;
      // Read humidity
      hum = bme.readHumidity();
  
      //Notify temperature reading from BME sensor
      #ifdef temperatureCelsius
        static char temperatureCTemp[6];
        dtostrf(temp, 6, 2, temperatureCTemp);
        //Set temperature Characteristic value and notify connected client
        bmeTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
        bmeTemperatureCelsiusCharacteristics.notify();
        Serial.print("Temperature Celsius: ");
        Serial.print(temp);
        Serial.print(" ºC");
      #else
        static char temperatureFTemp[6];
        dtostrf(tempF, 6, 2, temperatureFTemp);
        //Set temperature Characteristic value and notify connected client
        bmeTemperatureFahrenheitCharacteristics.setValue(temperatureFTemp);
        bmeTemperatureFahrenheitCharacteristics.notify();
        Serial.print("Temperature Fahrenheit: ");
        Serial.print(tempF);
        Serial.print(" ºF");
      #endif
      
      //Notify humidity reading from BME
      static char humidityTemp[6];
      dtostrf(hum, 6, 2, humidityTemp);
      //Set humidity Characteristic value and notify connected client
      bmeHumidityCharacteristics.setValue(humidityTemp);
      bmeHumidityCharacteristics.notify();   
      Serial.print(" - Humidity: ");
      Serial.print(hum);
      Serial.println(" %");
      
      lastTime = millis();
    }
  }
}

View raw code

You can upload the code, and it will work straight away advertising its service with the temperature and humidity characteristics. Continue reading to learn how the code works, or skip to the Client section.

There are several examples showing how to use BLE with the ESP32 in the Examples section. In your Arduino IDE, go to File > Examples > ESP32 BLE Arduino. This server sketch is based on the Notify example.

Importing Libraries

The code starts by importing the required libraries.

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Choosing Temperature Unit

By default, the ESP sends the temperature in Celsius degrees. You can comment the following line or delete it to send the temperature in Fahrenheit degrees.

//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius

BLE Server Name

The following line defines a name for our BLE server. Leave the default BLE server name. Otherwise, the server name in the client code also needs to be changed (because they have to match).

//BLE server name
#define bleServerName "BME280_ESP32"

BME280 Sensor

Create an Adafruit_BME280 object called bme on the default ESP32 I2C pins.

Adafruit_BME280 bme; // I2C

The temp, tempF and hum variables will hold the temperature in Celsius degrees, the temperature in Fahrenheit degrees, and the humidity read from the BME280 sensor.

float temp;
float tempF;
float hum;

Other Variables

The following timer variables define how frequently we want to write to the temperature and humidity characteristic. We set the timerDelay variable to 30000 milliseconds (30 seconds), but you can change it.

// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

The deviceConnected boolean variable allows us to keep track if a client is connected to the server.

bool deviceConnected = false;

BLE UUIDs

In the next lines, we define UUIDs for the service, for the temperature characteristic in celsius, for the temperature characteristic in Fahrenheit, and for the humidity.

// https://www.uuidgenerator.net/
#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"

// Temperature Characteristic and Descriptor
#ifdef temperatureCelsius
  BLECharacteristic bmeTemperatureCelsiusCharacteristics("cba1d466-344c-4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor bmeTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
  BLECharacteristic bmeTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
  BLEDescriptor bmeTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2901));
#endif

// Humidity Characteristic and Descriptor
BLECharacteristic bmeHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae-186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeHumidityDescriptor(BLEUUID((uint16_t)0x2903));

I recommend leaving all the default UUIDs. Otherwise, you also need to change the code on the client side—so the client can find the service and retrieve the characteristic values.

setup()

In the setup(), initialize the Serial Monitor and the BME280 sensor.

// Start serial communication 
Serial.begin(115200);

// Init 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 a callback function.

// Create the BLE Server
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

The callback function MyServerCallbacks() changes the boolean variable deviceConnected to true or false according to the current state of the BLE device. This means that if a client is connected to the server, the state is true. If the client disconnects, the boolean variable changes to false. Here’s the part of the code that defines the MyServerCallbacks() function.

//Setup callbacks onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
  };
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
  }
};

Start a BLE service with the service UUID defined earlier.

BLEService *bmeService = pServer->createService(SERVICE_UUID);

Then, create the temperature BLE characteristic. If you’re using Celsius degrees it sets the following characteristic and descriptor:

#ifdef temperatureCelsius
  bmeService->addCharacteristic(&bmeTemperatureCelsiusCharacteristics);
  bmeTemperatureCelsiusDescriptor.setValue("BME temperature Celsius");
  bmeTemperatureCelsiusCharacteristics.addDescriptor(new BLE2902());

Otherwise, it sets the Fahrenheit characteristic:

#else
  bmeService->addCharacteristic(&dhtTemperatureFahrenheitCharacteristics);
  bmeTemperatureFahrenheitDescriptor.setValue("BME temperature Fahrenheit");
  bmeTemperatureFahrenheitCharacteristics.addDescriptor(new BLE2902());
#endif  

After that, it sets the humidity characteristic:

// Humidity
bmeService->addCharacteristic(&bmeHumidityCharacteristics);
bmeHumidityDescriptor.setValue("BME humidity");
bmeHumidityCharacteristics.addDescriptor(new BLE2902());

Finally, you start the service, and the server starts the advertising so other devices can find it.

// Start the service
bmeService->start();

// Start advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pServer->getAdvertising()->start();
Serial.println("Waiting a client connection to notify...");

loop()

The loop() function is fairly straightforward. You constantly check if the device is connected to a client or not. If it’s connected, and the timerDelay has passed, it reads the current temperature and humidity.

if (deviceConnected) {
  if ((millis() - lastTime) > timerDelay) {
    // Read temperature as Celsius (the default)
    temp = bme.readTemperature();
    // Fahrenheit
    tempF = temp*1.8 +32;
    // Read humidity
    hum = bme.readHumidity();

If you’re using temperature in Celsius it runs the following code section. First, it converts the temperature to a char variable (temperatureCTemp variable). We must convert the temperature to a char variable type to use it in the setValue() function.

static char temperatureCTemp[6];
dtostrf(temp, 6, 2, temperatureCTemp);

Then, it sets the bmeTemperatureCelsiusCharacteristic value to the new temperature value (temperatureCTemp) using the setValue() function. After settings the new value, we can notify the connected client using the notify() function.

//Set temperature Characteristic value and notify connected client
bmeTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
bmeTemperatureCelsiusCharacteristics.notify();

We follow a similar procedure for the Temperature in Fahrenheit.

#else
    static char temperatureFTemp[6];
    dtostrf(f, 6, 2, temperatureFTemp);
    //Set temperature Characteristic value and notify connected client
    bmeTemperatureFahrenheitCharacteristics.setValue(tempF);
    bmeTemperatureFahrenheitCharacteristics.notify();
    Serial.print("Temperature Fahrenheit: ");
    Serial.print(tempF);
    Serial.print(" *F");
#endif

Sending the humidity also uses the same process.

//Notify humidity reading from DHT
static char humidityTemp[6];
dtostrf(hum, 6, 2, humidityTemp);
//Set humidity Characteristic value and notify connected client
bmeHumidityCharacteristics.setValue(humidityTemp);
bmeHumidityCharacteristics.notify();   
Serial.print(" - Humidity: ");
Serial.print(hum);
Serial.println(" %");

Testing the ESP32 BLE Server

Upload the code to your board and then, open the Serial Monitor. It will display a message as shown below.

ESP32 BLE Server Starts Serial Monitor

Then, you can test if the BLE server is working as expected by using a BLE scan application on your smartphone like nRF Connect. This application is available for Android and iOS.

After installing the application, enable Bluetooth on your smartphone. Open the nRF Connect app and click on the Scan button. It will find all Bluetooth nearby devices, including your BME280_ESP32 device (it is the BLE server name you defined on the code).

ESP32 BLE Server Scanner App

Connect to your BME280_ESP32 device and then, select the client tab (the interface might be slightly different). You can check that it advertises the service with the UUID we defined in the code, as well as the temperature and humidity characteristics. Notice that those characteristics have the Notify property.

ESP32 BLE Server Characteristics nRF Connect App

Your ESP32 BLE Server is ready!

Go to the next section to create an ESP32 client that connects to the server to get access to the temperature and humidity characteristics and get the readings to display them on an OLED display.


2) ESP32 BLE Client

In this section, we’ll create the ESP32 BLE client that will establish a connection with the ESP32 BLE server, and display the readings on an OLED display.

Schematic

The ESP32 BLE client is connected to an OLED display. The display shows the readings received via Bluetooth.

Wire your OLED display to the ESP32 by following the next schematic diagram. The SCL pin connects to GPIO 22 and the SDA pin to GPIO 21.

ESP32 Wiring Circuit to OLED SSD1306 Schematic Diagram

Installing the SSD1306, GFX and BusIO Libraries

You need to install the following libraries to interface with the OLED display:

To install the libraries, go SketchInclude Library > Manage Libraries, and search for the libraries’ names.

Installing Libraries (VS Code + PlatformIO)

If you’re using VS Code with the PlatformIO extension, copy the following to the platformio.ini file to include the libraries.

lib_deps = 
	adafruit/Adafruit GFX [email protected]^1.10.12
	adafruit/Adafruit [email protected]^2.4.6

ESP32 BLE Client – Code

Copy the BLE client Sketch to your Arduino IDE or to the main.cpp file if you’re using VS Code with PlatformIO.

/*********
  Rui Santos
  Complete instructions at https://RandomNerdTutorials.com/esp32-ble-server-client/
  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 <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>

//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius

//BLE Server name (the other ESP32 name running the server sketch)
#define bleServerName "BME280_ESP32"

/* UUID's of the service, characteristic that we want to read*/
// BLE Service
static BLEUUID bmeServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");

// BLE Characteristics
#ifdef temperatureCelsius
  //Temperature Celsius Characteristic
  static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
  //Temperature Fahrenheit Characteristic
  static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif

// Humidity Characteristic
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");

//Flags stating if should begin connecting and if the connection is up
static boolean doConnect = false;
static boolean connected = false;

//Address of the peripheral device. Address will be found during scanning...
static BLEAddress *pServerAddress;
 
//Characteristicd that we want to read
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;

//Activate notify
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t notificationOff[] = {0x0, 0x0};

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

//Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

//Variables to store temperature and humidity
char* temperatureChar;
char* humidityChar;

//Flags to check whether new temperature and humidity readings are available
boolean newTemperature = false;
boolean newHumidity = false;

//Connect to the BLE Server that has the name, Service, and Characteristics
bool connectToServer(BLEAddress pAddress) {
   BLEClient* pClient = BLEDevice::createClient();
 
  // Connect to the remove BLE Server.
  pClient->connect(pAddress);
  Serial.println(" - Connected to server");
 
  // Obtain a reference to the service we are after in the remote BLE server.
  BLERemoteService* pRemoteService = pClient->getService(bmeServiceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("Failed to find our service UUID: ");
    Serial.println(bmeServiceUUID.toString().c_str());
    return (false);
  }
 
  // Obtain a reference to the characteristics in the service of the remote BLE server.
  temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
  humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);

  if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
    Serial.print("Failed to find our characteristic UUID");
    return false;
  }
  Serial.println(" - Found our characteristics");
 
  //Assign callback functions for the Characteristics
  temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
  humidityCharacteristic->registerForNotify(humidityNotifyCallback);
  return true;
}

//Callback function that gets called, when another device's advertisement has been received
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    if (advertisedDevice.getName() == bleServerName) { //Check if the name of the advertiser matches
      advertisedDevice.getScan()->stop(); //Scan can be stopped, we found what we are looking for
      pServerAddress = new BLEAddress(advertisedDevice.getAddress()); //Address of advertiser is the one we need
      doConnect = true; //Set indicator, stating that we are ready to connect
      Serial.println("Device found. Connecting!");
    }
  }
};
 
//When the BLE Server sends a new temperature reading with the notify property
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, 
                                        uint8_t* pData, size_t length, bool isNotify) {
  //store temperature value
  temperatureChar = (char*)pData;
  newTemperature = true;
}

//When the BLE Server sends a new humidity reading with the notify property
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, 
                                    uint8_t* pData, size_t length, bool isNotify) {
  //store humidity value
  humidityChar = (char*)pData;
  newHumidity = true;
  Serial.print(newHumidity);
}

//function that prints the latest sensor readings in the OLED display
void printReadings(){
  
  display.clearDisplay();  
  // display temperature
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("Temperature: ");
  display.setTextSize(2);
  display.setCursor(0,10);
  display.print(temperatureChar);
  display.setTextSize(1);
  display.cp437(true);
  display.write(167);
  display.setTextSize(2);
  Serial.print("Temperature:");
  Serial.print(temperatureChar);
  #ifdef temperatureCelsius
    //Temperature Celsius
    display.print("C");
    Serial.print("C");
  #else
    //Temperature Fahrenheit
    display.print("F");
    Serial.print("F");
  #endif

  //display humidity 
  display.setTextSize(1);
  display.setCursor(0, 35);
  display.print("Humidity: ");
  display.setTextSize(2);
  display.setCursor(0, 45);
  display.print(humidityChar);
  display.print("%");
  display.display();
  Serial.print(" Humidity:");
  Serial.print(humidityChar); 
  Serial.println("%");
}

void setup() {
  //OLED display setup
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE,0);
  display.setCursor(0,25);
  display.print("BLE Client");
  display.display();
  
  //Start serial communication
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");

  //Init BLE device
  BLEDevice::init("");
 
  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 30 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->start(30);
}

void loop() {
  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer(*pServerAddress)) {
      Serial.println("We are now connected to the BLE Server.");
      //Activate the Notify property of each Characteristic
      temperatureCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
      humidityCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
      connected = true;
    } else {
      Serial.println("We have failed to connect to the server; Restart your device to scan for nearby BLE server again.");
    }
    doConnect = false;
  }
  //if new temperature readings are available, print in the OLED
  if (newTemperature && newHumidity){
    newTemperature = false;
    newHumidity = false;
    printReadings();
  }
  delay(1000); // Delay a second between loops.
}

View raw code

Continue reading to learn how the code works or skip to the Demonstration section.

Importing libraries

You start by importing the required libraries:

#include "BLEDevice.h"
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>

Choosing temperature unit

By default the client will receive the temperature in Celsius degrees, if you comment the following line or delete it, it will start receiving the temperature in Fahrenheit degrees.

//Default Temperature is in Celsius
//Comment the next line for Temperature in Fahrenheit
#define temperatureCelsius

BLE Server Name and UUIDs

Then, define the BLE server name that we want to connect to and the service and characteristic UUIDs that we want to read. Leave the default BLE server name and UUIDs to match the ones defined in the server sketch.

//BLE Server name (the other ESP32 name running the server sketch)
#define bleServerName "BME280_ESP32"

/* UUID's of the service, characteristic that we want to read*/
// BLE Service
static BLEUUID bmeServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");

// BLE Characteristics
#ifdef temperatureCelsius
  //Temperature Celsius Characteristic
  static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
  //Temperature Fahrenheit Characteristic
  static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif

// Humidity Characteristic
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");

Declaring variables

Then, you need to declare some variables that will be used later with Bluetooth to check whether we’re connected to the server or not.

//Flags stating if should begin connecting and if the connection is up
static boolean doConnect = false;
static boolean connected = false;

Create a variable of type BLEAddress that refers to the address of the server we want to connect. This address will be found during scanning.

//Address of the peripheral device. Address will be found during scanning...
static BLEAddress *pServerAddress;

Set the characteristics we want to read (temperature and humidity).

//Characteristicd that we want to read
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;

OLED Display

You also need to declare some variables to work with the OLED. Define the OLED width and height:

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

Instantiate the OLED display with the width and height defined earlier.

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire);

Temperature and Humidity Variables

Define char variables to hold the temperature and humidity values received by the server.

//Variables to store temperature and humidity
char* temperatureChar;
char* humidityChar;

The following variables are used to check whether new temperature and humidity readings are available and if it is time to update the OLED display.

//Flags to check whether new temperature and humidity readings are available
boolean newTemperature = false;
boolean newHumidity = false;

printReadings()

We created a function called printReadings() that displays the temperature and humidity readings on the OLED display.

void printReadings(){
  
  display.clearDisplay();  
  // display temperature
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("Temperature: ");
  display.setTextSize(2);
  display.setCursor(0,10);
  display.print(temperatureChar);
  display.print(" ");
  display.setTextSize(1);
  display.cp437(true);
  display.write(167);
  display.setTextSize(2);
  Serial.print("Temperature:");
  Serial.print(temperatureChar);
  #ifdef temperatureCelsius
    //Temperature Celsius
    display.print("C");
    Serial.print("C");
  #else
    //Temperature Fahrenheit
    display.print("F");
    Serial.print("F");
  #endif

  //display humidity
  display.setTextSize(1);
  display.setCursor(0, 35);
  display.print("Humidity: ");
  display.setTextSize(2);
  display.setCursor(0, 45);
  display.print(humidityChar);
  display.print("%");
  display.display();
  Serial.print(" Humidity:");
  Serial.print(humidityChar); 
  Serial.println("%");
}

Recommended reading: ESP32 OLED Display with Arduino IDE

setup()

In the setup(), start the OLED display.

//OLED display setup
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
  Serial.println(F("SSD1306 allocation failed"));
  for(;;); // Don't proceed, loop forever
}

Then, print a message in the first line saying “BME SENSOR”.

display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE,0);
display.setCursor(0,25);
display.print("BLE Client");
display.display();

Start the serial communication at a baud rate of 115200.

Serial.begin(115200);

And initialize the BLE device.

//Init BLE device
BLEDevice::init("");

Scan nearby devices

The following methods scan for nearby devices.

// Retrieve a Scanner and set the callback we want to use to be informed when we
// have detected a new device.  Specify that we want active scanning and start the
// scan to run for 30 seconds.
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(30);

MyAdvertisedDeviceCallbacks() function

Note that the MyAdvertisedDeviceCallbacks() function, upon finding a BLE device, checks if the device found has the right BLE server name. If it has, it stops the scan and changes the doConnect boolean variable to true. This way we know that we found the server we’re looking for, and we can start establishing a connection.

//Callback function that gets called, when another device's advertisement has been received
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    if (advertisedDevice.getName() == bleServerName) { //Check if the name of the advertiser matches
      advertisedDevice.getScan()->stop(); //Scan can be stopped, we found what we are looking for
      pServerAddress = new BLEAddress(advertisedDevice.getAddress()); //Address of advertiser is the one we need
      doConnect = true; //Set indicator, stating that we are ready to connect
      Serial.println("Device found. Connecting!");
    }
  }
};

Connect to the server

If the doConnect variable is true, it tries to connect to the BLE server. The connectToServer() function handles the connection between the client and the server.

//Connect to the BLE Server that has the name, Service, and Characteristics
bool connectToServer(BLEAddress pAddress) {
   BLEClient* pClient = BLEDevice::createClient();
 
  // Connect to the remove BLE Server.
  pClient->connect(pAddress);
  Serial.println(" - Connected to server");
 
  // Obtain a reference to the service we are after in the remote BLE server.
  BLERemoteService* pRemoteService = pClient->getService(bmeServiceUUID);
  if (pRemoteService == nullptr) {
    Serial.print("Failed to find our service UUID: ");
    Serial.println(bmeServiceUUID.toString().c_str());
    return (false);
  }
 
  // Obtain a reference to the characteristics in the service of the remote BLE server.
  temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
  humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);

  if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
    Serial.print("Failed to find our characteristic UUID");
    return false;
  }
  Serial.println(" - Found our characteristics");
 
  //Assign callback functions for the Characteristics
  temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
  humidityCharacteristic->registerForNotify(humidityNotifyCallback);
  return true;
}

It also assigns a callback function responsible to handle what happens when a new value is received.

//Assign callback functions for the Characteristics
temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
humidityCharacteristic->registerForNotify(humidityNotifyCallback);

After the BLE client is connected to the server, you need to active the notify property for each characteristic. For that, use the writeValue() method on the descriptor.

if (connectToServer(*pServerAddress)) {
  Serial.println("We are now connected to the BLE Server.");
  //Activate the Notify property of each Characteristic
  temperatureCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
  humidityCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);

Notify new values

When the client receives a new notify value, it will call these two functions: temperatureNotifyCallback() and humidityNotifyCallback() that are responsible for retrieving the new value, update the OLED with the new readings and print them on the Serial Monitor.

//When the BLE Server sends a new temperature reading with the notify property
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, 
                                        uint8_t* pData, size_t length, bool isNotify) {
  //store temperature value
  temperatureChar = (char*)pData;
  newTemperature = true;
}
//When the BLE Server sends a new humidity reading with the notify property
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, 
                                    uint8_t* pData, size_t length, bool isNotify) {
  //store humidity value
  humidityChar = (char*)pData;
  newHumidity = true;
  Serial.print(newHumidity);
}

These two previous functions are executed every time the BLE server notifies the client with a new value, which happens every 30 seconds. These functions save the values received on the temperatureChar and humidityChar variables. These also change the newTemperature and newHumidity variables to true, so that we know we’ve received new readings.

Display new temperature and humidity readings

In the loop(), there is an if statement that checks if new readings are available. If there are new readings, we se the newTemperature and newHumidity variables to false, so that we are able to receive new readings later on. Then, we call the printReadings() function to display the readings on the OLED.

//if new temperature readings are available, print in the OLED
if (newTemperature && newHumidity){
  newTemperature = false;
  newHumidity = false;
  printReadings();
}

Testing the Project

That’s it for the code. You can upload it to your ESP32 board.

Once the code is uploaded. Power the ESP32 BLE server, then power the ESP32 with the client sketch. The client starts scanning nearby devices, and when it finds the other ESP32, it establishes a Bluetooth connection. Every 30 seconds, it updates the display with the latest readings.

ESP32 BLE Client Server OLED Display Demonstration

Important: don’t forget to disconnect your smartphone from the BLE server. Otherwise, the ESP32 BLE Client won’t be able to connect to the server.

ESP32 BLE Client Connected to ESP32 BLE Server Serial Monitor

Wrapping Up

In this tutorial, you learned how to create a BLE Server and a BLE Client with the ESP32. You learned how to set new temperature and humidity values on the BLE server characteristics. Then, other BLE devices (clients) can connect to that server and read those characteristic values to get the latest temperature and humidity values. Those characteristics have the notify property, so that the client is notified whenever there’s a new value.

Using BLE is another communication protocol you can use with the ESP32 boards besides Wi-Fi. We hope you found this tutorial useful. We have tutorials for other communication protocols that you may find useful.

Learn more about the ESP32 with our resources:



Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »

Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »


Enjoyed this project? Stay updated by subscribing our newsletter!

24 thoughts on “ESP32 BLE Server and Client (Bluetooth Low Energy)”

      • I’m afraid I noticed a couple of other typos in the server sketch. You apparently switched from dht to bme sensors but missed one reference in line 93:
        bmeService->addCharacteristic(&dhtTemperatureFahrenheitCharacteristics);

        You also created new descriptors when, I’m sure, you intended to point to the ones created earlier. Lines 95 and 101:
        bmeTemperatureFahrenheitCharacteristics.addDescriptor(new BLE2902());
        bmeHumidityCharacteristics.addDescriptor(new BLE2902());
        should be:
        bmeTemperatureCharacteristics.addDescriptor(&bmeTemperatureDescriptor);
        bmeHumidityCharacteristics.addDescriptor(&bmeHumidityDescriptor);

        Thanks for all the work you do. You are always the first source I go to when I have a problem with ESP microcontrollers.

        Reply
        • Hi.
          You’re right. Thanks for pointing that out.
          I’ll fix it soon.
          I guess I need a vacation. I should have seen that.
          Regards,
          Sara

          Reply
          • Don’t feel too bad. When I looked for more documentation, every example I found created a new Descriptor. None explained how to modify that Descriptor after it was created. I applaud you for putting together such a good sketch when documentation is apparently so hard to find. That is why I always go to RNT first.

  1. Hey.
    Can you explain this rule to me in more detail?
    BLECharacteristic bmeTemperatureCelsiusCharacteristics(“cba1d466-344c-4be3-ab3f-189f80dd7518”, BLECharacteristic::PROPERTY_NOTIFY);

    I almost understand how everything works but where does that long number come from or what does it do.
    Greetings old man Bert

    Reply
    • Hi.
      Basically, you need to set UUIDs for your characteristics to identify them. You can create UUIDs (use the UUID generator website: https://www.uuidgenerator.net/) or use predefined UUIDs (https://www.bluetooth.com/specifications/assigned-numbers/).

      The UUIDs are used to identify the characteristics. There are predefined UUIDs for the most common characteristics used by BLE devices, for example the Battery Level has a default UUID. This is useful because other devices that connect, know exactly what to search for to get the information they want.

      We also define that we want the property of that characteristic to be notify. The property defines how the client can interact with those characteristics, it can be read, write, notify, and others.

      I hope this is clear.

      Regards,
      Sara

      Reply
      • Hey.
        Very clear and understandable.
        Sometimes there is a translation problem because I can’t write english.
        Certain concepts are then not clear.
        You explain everything you do very well and it is very instructive.
        I thank you for that.
        I’m going to work on it.
        Greetings Old man Bert

        Reply
          • You once had a wish list.
            If you still have it I’d love to
            Esp32 Web server Hosting files from MicroSd Card.
            Turns into an Esp32 captive portal Hosting Files from MicroSd Card.
            But I have no idea if that’s possible.
            I do have a version here that does that without SDcart, but that is very inconvenient if you want to change the html.

  2. I found a way to make the client much more stable if the connection is broken and reconnected.

    Make pClient global by moving the following statement to the top area outside all loops and functions:

    BLEClient* pClient = BLEDevice::createClient();

    Then add this to the top of the main loop:

    bool status = pClient->isConnected(); // check BLE connection
    if (!status) {
    doConnect = true;
    temperatureChar[0] = 0; humidityChar[0] = 0; // purge stale data
    Serial.println(“Reconnecting BLE…”);
    printReadings(); // redraw display so it does not display stale data
    }

    It will then connect and reconnect as necessary.

    Reply
  3. Thanks RNT crew,
    … for doing the BLE thing for esp32… and everything else.!

    I am trying to incorporate [AsyncElegantOTA]
    and am confused as to the order of the [#includes].
    — I am only guessing that there is a correct “pecking order” ! ?

    Is there a location where this issue is documented?

    Currently I have:
    #include <BLEDevice.h>
    #include <BLEServer.h>
    #include <BLEUtils.h>
    #include <BLE2902.h>
    #include <Wire.h>

    #include <Arduino.h>
    #include “WiFi.h”
    #include “AsyncTCP.h”
    #include “ESPAsyncWebServer.h”
    #include “AsyncElegantOTA.h”

    #include <Adafruit_Sensor.h>
    #include <Adafruit_BME280.h>

    But haven’t even tested that yet.
    …before I proceed only to find headaches that I can’t identify the cause of.

    I would appreciate someone with more experience would mind checking that and advise whether OK. If not, please re-order.

    Any other pitfalls of trying to do OTA with BLE.
    My server is going outside to a hard-to-reach spot.

    TIA

    Reply

Leave a Comment

Download our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.