ESP32: How to use Bluetooth Low Energy (BLE) Server Communication

ESP32 Tutorial: Bluetooth Low Energy (BLE) Server Communication

Abstract

The ESP32 is a powerful dual-mode chip, excelling in Bluetooth Low Energy (BLE) applications. Unlike the older Bluetooth Classic (BR/EDR), BLE is optimized for low power consumption and uses the Generic Attribute Profile (GATT) architecture. This guide details how to set up the ESP32 as a BLE Server, making it discoverable by modern smartphones and capable of exchanging data using custom Services and Characteristics.

1. Introduction to BLE Server Communication

The ESP32 is a versatile microcontroller, and its dual-mode capability for Bluetooth is one of its most powerful features. This tutorial focuses specifically on Bluetooth Low Energy (BLE), the modern standard optimized for low power consumption and short, bursty data transmission—ideal for battery-operated sensors and remote control.

Unlike the older Bluetooth Classic (which behaves much like a simple serial connection), BLE uses the structured Generic Attribute Profile (GATT) architecture. This architecture requires you to define the structure of the data your device offers.

In this guide, the ESP32 is configured as a BLE Server, which means it hosts the data structure and accepts connections. The data structure is hierarchical:

  1. Service: A collection of related data points (e.g., a “Temperature Service”).

  2. Characteristic: A single data point within a service (e.g., the actual “Temperature Reading”).

By setting up a custom Service and Characteristic, the ESP32 becomes a robust, discoverable device capable of two-way data exchange with a client device, typically a modern smartphone running an application like nRF Connect.

2. Bluetooth Classic vs. BLE

It’s essential to understand the difference between the two main Bluetooth flavors supported by the ESP32:

Feature

Bluetooth Classic (BR/EDR)

Bluetooth Low Energy (BLE)

Data Rate

High (up to 3 Mbps)

Low (up to 1 Mbps)

Power

Higher (suitable for streaming/audio)

Very Low (ideal for battery sensors)

Profile

SPP (Serial Port Profile)

GATT (Generic Attribute Profile)

Pairing

Mandatory for connection

Optional (can be connectionless)

Programming

Simpler (like Serial/UART)

More complex (services/characteristics)

We will focus on Bluetooth Low Energy (BLE), using the necessary libraries: BLEDevice.h, BLEServer.h, BLEUtils.h, and BLE2902.h.

3. Core ESP32 BLE Server Code (GATT Structure)

The ESP32 acts as a BLE Server, defining the structure of the data, which a Client (like a smartphone app) can read from or write to. This structure is defined by Services and Characteristics.

3.1 Setup and Initialization

The setup involves four main steps: Initialize the device, create the server, define the GATT structure (Services and Characteristics), and start advertising

The setup() function performs the crucial steps necessary to establish the ESP32 as a visible BLE server.

  • Initialization: BLEDevice::init("MyCoolESP32_BLE_Device") initializes the Bluetooth radio and sets the human-readable advertising name that client devices will see.

  • Server Creation: pServer = BLEDevice::createServer() instantiates the core BLE Server object.

  • Service and Characteristic:

    • pService->createService(SERVICE_UUID) creates the custom Service layer, uniquely identified by a 128-bit UUID.

    • pCharacteristic = pService->createCharacteristic(...) creates the Characteristic (the data point) within the service, also using a unique UUID.

  • Properties: The characteristic is configured with BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE, which tells the client device that it is allowed to both read the characteristic’s current value and write a new value to it.

  • Advertising: BLEDevice::startAdvertising() makes the ESP32 discoverable by broadcasting its name and service UUID, allowing client devices to find and connect to it.

				
					// --- UUID Definitions ---
#define SERVICE_UUID        "4E22A700-AA7C-4B79-9945-8C7957731778"
#define CHARACTERISTIC_UUID "A64BBF3B-F4E7-474F-A186-F98CC5A49603"

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE Server...");

  // 1. Initialize the BLE Device
  BLEDevice::init("MyCoolESP32_BLE_Device"); // The name seen by clients

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

  // 3. Create the Service and Characteristic
  BLEService *pService = pServer->createService(SERVICE_UUID);

  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ |
                      BLECharacteristic::PROPERTY_WRITE // Set properties for R/W
                    );

  pCharacteristic->setValue("Hello World"); // Initial value
  pService->start();
  
  // 4. Start Advertising (Makes the device discoverable)
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID); // Advertise the custom service
  BLEDevice::startAdvertising();

  Serial.println("Characteristic defined! Now advertising...");
}

				
			

3.2 Monitoring Connections and Data Exchange

Data exchange and connection status are managed using Callback classes, which are executed when specific events occur.

3.2.1 Server Callbacks (Connection Management)

The MyServerCallbacks class monitors when a phone connects or disconnects. A crucial part of this is restarting Advertising after a disconnect, allowing the device to be found again.

BLE connection and disconnection events are handled asynchronously using Callback functions defined within the MyServerCallbacks class, which inherits from BLEServerCallbacks.

  • onConnect: This is called when a client (e.g., a smartphone) successfully connects. It sets the global deviceConnected flag to true.

  • onDisconnect: This is called when the client disconnects. After setting deviceConnected to false, it immediately calls pServer->startAdvertising() to restart the advertising process. This is essential because most BLE servers stop advertising once a connection is established, and they must be told to start again to be rediscovered by other devices.

				
					class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println("Client connected!");
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("Client disconnected! Restarting advertising...");
      pServer->startAdvertising(); // Restart visibility
    }
};

				
			

3.2.2 Data Exchange in loop()

Since the characteristic was created with the PROPERTY_READ and PROPERTY_WRITE attributes, the client can now read the current value or write a new value. The loop() function can be used to periodically update the characteristic value if the device is connected (e.g., sending sensor data).

				
					void loop() {
  if (deviceConnected) {
    // Example: Read a sensor value and notify the client
    // pCharacteristic->setValue("New Sensor Data");
    // pCharacteristic->notify(); 
  }
  delay(1000); 
}

				
			
  • Monitoring: By checking the deviceConnected flag, the code can execute logic only when a client is active.

  • Reading/Writing: The client can initiate READ operations at any time. For the server to actively push data to a subscribed client (known as Notifications), the characteristic’s value must be updated using pCharacteristic->setValue() and then broadcast using pCharacteristic->notify(). Since this feature requires a specific characteristic property and client subscription, it is commented out but shows the key pattern for sending live data like sensor readings.

4. Source Code

				
					#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h> // For the standard BLE descriptors

// The service UUID is a unique identifier for the type of service your device offers.
// You can use a generic one or generate your own.
#define SERVICE_UUID        "4E22A700-AA7C-4B79-9945-8C7957731778"
// Characteristic UUID (for a specific data point/functionality within the service)
#define CHARACTERISTIC_UUID "A64BBF3B-F4E7-474F-A186-F98CC5A49603"

BLEServer *pServer = NULL;
BLECharacteristic *pCharacteristic = NULL;
bool deviceConnected = false;

// Callback class to monitor connection events
class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println("Client connected!");
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("Client disconnected! Restarting advertising...");
      // Restart advertising so it can be re-discovered
      pServer->startAdvertising();
    }
};

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE Server...");

  // 1. Initialize the BLE Device (Name is what your phone will see!)
  BLEDevice::init("MyCoolESP32_BLE_Device");

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

  // 3. Create a BLE Service
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // 4. Create a BLE Characteristic within the Service
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ |
                      BLECharacteristic::PROPERTY_WRITE
                    );

  // Set an initial value for the characteristic (e.g., a simple status)
  pCharacteristic->setValue("Hello World");

  // 5. Start the Service
  pService->start();

  // 6. Start Advertising (This is the key step for visibility!)
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);  // Set minimum advertising interval
  pAdvertising->setMinPreferred(0x12);  // Set maximum advertising interval
 
  BLEDevice::startAdvertising();
  Serial.println("Characteristic defined! Now advertising...");
}

void loop() {
  // Put your main loop code here.
  // You might want to update the characteristic value here if needed.
  if (deviceConnected) {
    // Example: send a '1' to the characteristic every second while connected
    // pCharacteristic->setValue("1");
    // pCharacteristic->notify();
  }
  delay(1000);
}

				
			

5. Testing the Communication (GATT Client)

To test this server sketch, you cannot use a generic Bluetooth Serial Terminal app, as those are for Bluetooth Classic (SPP). You must use a dedicated BLE Client/Scanner app that can browse GATT Services and Characteristics, such as:

  • Android: nRF Connect or BLE Scanner
  • iOS: LightBlue

5.1 Connection Steps

Upload the Code: Upload the complete sketch to your ESP32 board.

Scan for Devices: Open your BLE client app and scan. You should see the device named MyCoolESP32_BLE_Device.

Connect: Tap the device name to connect. The Arduino Serial Monitor should print: Client connected!.

Explore GATT: Once connected, the app will show the custom Service UUID and the custom Characteristic UUID.

Test Read: Tap the Read button on the characteristic in the app. The app should display the initial value: Hello World.

Test Write: Use the app to write a new value to the characteristic. While this code doesn’t implement an onWrite handler, the characteristic property allows the data to be sent.

6. Lab Recap

You’ve learned the fundamentals of ESP32 Bluetooth Low Energy (BLE) server communication:

  • BLE uses the GATT structure, requiring Services and Characteristics.
  • The key to visibility is BLEDevice::init(“Name”) and BLEDevice::startAdvertising().
  • Connection status is managed via BLEServerCallbacks.
  • Data is read/written to the defined BLECharacteristic

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top