This example demonstrates how to implement a Bluetooth BLE (Low Energy) server (peripheral / GATT Server) on the ESP32. The program initializes the BLE device, creates a GATT server, and starts advertising. A BLE debugging app on a mobile phone (such as nRF Connect, LightBlue, etc.) can scan for the device and establish a connection. Once connected, the phone sends string data to the ESP32 by "writing the characteristic value". The ESP32 displays the received data on the TFT LCD screen and sends the data back to the phone via notify, enabling two-way communication between the phone and the ESP32.
Blue_Server_test.ino fileBLEDevice::init("ESP32_BT_BLE"); // Change to your desired BLE device name (shown when the phone scans)
#define SERVICE_UUID "DFCD0001-36E1-4688-B7F5-EA07361B26A8"
#define CHARACTERISTIC1_UUID "DFCD000A-36E1-4688-B7F5-EA07361B26A8"
pCharacteristic->setValue("ESP32 BT BLE"); // Content returned when the phone reads this characteristic
Blue_Server_test.ino in the Arduino IDEESP32 Dev Module (for ESP32-WROOM-32E)BLE server start.: the server has started and is advertisingreceive data: is shown: the data receiving area is readyESP32_BT_BLE in the scan listBLE device connected.DFCD0001-... in the service listDFCD000A-...READ, WRITE, and NOTIFYBLE device disconnected., ready for the next connection⚠️ Key Notes:
READ, WRITE, and NOTIFY simultaneously; NOTIFY requires enabling notification subscription on the app side to receive the echoed dataloop() only contains delay(3000); actual data sending/receiving is asynchronously driven by BLE callbacks and does not need to be handled in the main loopRecommended mobile apps:
Understanding the service and characteristic structure (GATT):
READ: the phone can read the current content of the characteristicWRITE: the phone can write data to the characteristic (i.e. "send" data to the ESP32)NOTIFY: the ESP32 can actively "push" data to the phone (the phone must subscribe first)Purpose of the BLE2902 descriptor:
pCharacteristic->addDescriptor(new BLE2902()) adds the CCCD (Client Characteristic Configuration Descriptor)0x0001 to this descriptorData send/receive flow:
onWrite callback → screen display + serial print + notify echo → phone's notification area receives the dataTo make it convenient for users to test this example directly on a computer, a web-based BLE testing tool Blue_Server_test_web.html is provided. The tool is based on the browser-native Web Bluetooth API and requires no mobile app or desktop software. Using Chrome / Edge, you can connect to the ESP32 and send/receive data via the computer's Bluetooth.
| Item | Description |
|---|---|
| File location | 26_BLE_server_V3.0/Blue_Server_test_web.html |
| Technology | Web Bluetooth API (W3C standard, browser-native) |
| Browser requirement | Chrome / Edge (version 70+); Firefox / Safari not supported |
| Runtime environment | Requires a "secure context" (HTTPS or localhost); opening via file:// may be restricted |
| Corresponding firmware | Blue_Server_test.ino (device name and UUIDs are aligned) |
| Interface language | English |
The web interface is divided into four card areas, with the following functions:
Connection
Disconnected (not connected) / Connected: ESP32_BT_BLE (connected)Connect Device, DisconnectSend Data to ESP32
Send button: sends the text in the input boxRead Value button: reads the current characteristic value (initial value is "ESP32 BT BLE")Sent: N shows the number of sent messagesReal-time Log
Clear Log button: clears the logBottom hint area
Open the web file (double-click)
Blue_Server_test_web.html file in the 26_BLE_server_V3.0 directoryConnect the device
Connect Device button on the web pageESP32_BT_BLE in the list → click "Pair"Send data
Hello) → click Send or press EnterSending: "Hello" and Sent successfully (5 bytes)Observe the ESP32 response
Received ESP32 echo: "Hello" (notify echo)Disconnect
Disconnect button, or directly close the browser/pageBLE device disconnected., ready for the next connectionAll log output of the web page is in English. The following table lists the common log messages and their meanings:
| Log message | Meaning |
|---|---|
Tool ready. Click "Connect Device" to start testing. |
The tool is ready; click "Connect Device" to start testing |
Note: The browser will show a device picker when connecting... |
Note: the browser will show a device picker when connecting |
Requesting device (select "ESP32_BT_BLE" in the popup)... |
Requesting the device (please select ESP32_BT_BLE in the popup) |
Device selected: ESP32_BT_BLE |
Device selected |
Connecting to GATT server... |
Connecting to the GATT server |
GATT server connected |
GATT server connected |
Getting service (...uuid...)... |
Getting the service |
Service obtained |
Service obtained |
Getting characteristic (...uuid...)... |
Getting the characteristic |
Characteristic obtained |
Characteristic obtained |
Subscribing to NOTIFY... |
Subscribing to NOTIFY |
NOTIFY subscription enabled (ESP32 echo data will appear here) |
NOTIFY subscription enabled (ESP32 echo data will appear here) |
Connected! You can now send data. |
Connected! You can now send data |
Sending: "Hello" |
Sending: "Hello" |
Sent successfully (5 bytes) |
Sent successfully (5 bytes) |
Received ESP32 echo: "Hello" |
Received ESP32 echo: "Hello" |
Reading current characteristic value... |
Reading the current characteristic value |
Read success: "ESP32 BT BLE" |
Read success: "ESP32 BT BLE" |
Device disconnected. |
Device disconnected |
Connection failed: ... |
Connection failed: ... |
Send failed: ... |
Send failed: ... |
Note: Device not found. Make sure the ESP32 is powered on... |
Note: device not found; make sure the ESP32 is powered on and within broadcast range |
Note: Device selection was cancelled by the user. |
Note: device selection was cancelled by the user |
Current browser does not support Web Bluetooth API... |
The current browser does not support the Web Bluetooth API |
Please enter text to send. |
Please enter text to send |
Characteristic not ready, please connect first. |
The characteristic is not ready; please connect first |
Log cleared. |
Log cleared |
http://localhost is recommended; opening directly via file:// may not work in some browsersPROPERTY_WRITE (Write with Response); the web side uses writeValue() to match; if the firmware is changed to PROPERTY_WRITE_NR, the web side must use writeValueWithoutResponse()Current browser does not support Web Bluetooth API, switch to Chrome or Edge (version 70+)This example code is based on the ESP32-WROOM-32E board. The program creates a BLE GATT server, advertises, waits for a phone to connect and write data to the characteristic, and displays the received data on the TFT screen and echoes it back via notify.
The parameters that can be adjusted in the program are as follows:
#define SERVICE_UUID "DFCD0001-36E1-4688-B7F5-EA07361B26A8" // Service UUID
#define CHARACTERISTIC1_UUID "DFCD000A-36E1-4688-B7F5-EA07361B26A8" // Characteristic UUID
BLEDevice::init("ESP32_BT_BLE"); // BLE device advertising name
pCharacteristic->setValue("ESP32 BT BLE"); // Characteristic initial content
⚠️ Optional parameters to modify:
#include <TFT_eSPI.h>
#include <BLEDevice.h> // Bluetooth BLE device base library
#include <BLEServer.h> // BLE server (GATT Server) library
#include <BLEUtils.h> // BLE utility library
#include <BLE2902.h> // BLE2902 descriptor library (required for notify)
#define SERVICE_UUID "DFCD0001-36E1-4688-B7F5-EA07361B26A8"
#define CHARACTERISTIC1_UUID "DFCD000A-36E1-4688-B7F5-EA07361B26A8"
bool deviceConnected = false;
BLEServer *pServer;
BLEService *pService;
BLECharacteristic* pCharacteristic;
int i = 0;
TFT_eSPI my_lcd = TFT_eSPI();
TFT_eSPI.h: TFT screen driver libraryBLEDevice.h: ESP32 BLE device base library, provides BLE device initialization, server creation, advertising, etc.BLEServer.h: BLE server library, provides creation and management of GATT server, services, and characteristicsBLEUtils.h: BLE utility library, provides BLE-related helper functionsBLE2902.h: BLE2902 descriptor library, CCCD (Client Characteristic Configuration Descriptor), required to enable notifySERVICE_UUID / CHARACTERISTIC1_UUID: 128-bit UUID constants for the service and characteristicdeviceConnected: connection status flag, true means a phone is connectedpServer: BLE server object pointerpService: BLE service object pointerpCharacteristic: BLE characteristic object pointer, used for reading and writing datai: screen data display line counter, auto-resets to 0 after reaching 28 (i.e. i>27)my_lcd: TFT screen objectMyServerCallbacks inherits from BLEServerCallbacks. When a phone connects to or disconnects from the ESP32 BLE server, onConnect() or onDisconnect() is automatically called, which displays the connection status in the screen's top status bar and updates the deviceConnected flag.
class MyServerCallbacks: public BLEServerCallbacks
{
void onConnect(BLEServer* pServer)
{
my_lcd.fillRect(10, 0, my_lcd.width()-1, 20,TFT_WHITE);
my_lcd.drawString("BLE device connected.", 10, 0);
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer)
{
my_lcd.fillRect(10, 0, my_lcd.width()-1, 20,TFT_WHITE);
my_lcd.drawString("BLE device disconnected.", 10, 0);
deviceConnected = false;
}
};
pServer: pointer to the BLEServer object that triggered the callback; can be used to further operate the server in the callback (e.g. restart advertising)onConnect: triggered when the phone's BLE app clicks "CONNECT" and the GATT connection is successfully establishedonDisconnect: triggered when the phone actively disconnects, turns off Bluetooth, or the connection drops due to exceeding communication rangemy_lcd.fillRect(10, 0, width-1, 20, TFT_WHITE): clears the screen's top status bar area (height 20) to avoid overlap of old and new status textmy_lcd.drawString(...): draws the connect/disconnect status text at position (10, 0)deviceConnected = true/false: updates the global connection status flag (not used in the main loop in this example, but kept for extension)⚠️ Notes:
MyCallbacks inherits from BLECharacteristicCallbacks. When the phone writes data to the characteristic, the onWrite() method is automatically called, which displays the received data on the screen, prints it to the serial port, and sends the data back to the phone via notify.
class MyCallbacks: public BLECharacteristicCallbacks
{
void onWrite(BLECharacteristic *pCharacteristic)
{
String value = pCharacteristic->getValue();
if (value.length() > 0)
{
if(i>27)
{
i = 0;
my_lcd.fillRect(0, 32, my_lcd.width()-1, my_lcd.height()-32,TFT_WHITE);
}
my_lcd.drawString(value.c_str(),5,32+16*i);
Serial.println(value.c_str());
pCharacteristic->notify();
i++;
}
}
};
pCharacteristic: pointer to the characteristic object that triggered the callback; the phone's written content can be read via getValue()The program displays the following information on the screen:
pCharacteristic->getValue(): gets the characteristic content written by the phone, returns a Stringvalue.length() > 0: checks whether the written content is non-empty; empty writes do not trigger displayi>27: checks whether the line index exceeds 27 (i.e. 28 lines have been displayed); if so, resets to 0 and clears the receiving areamy_lcd.fillRect(0, 32, width-1, height-32, TFT_WHITE): clears the entire data receiving area from y=32 to the bottom of the screenmy_lcd.drawString(value.c_str(), 5, 32+16*i): displays the received string at position (5, 32+16*i)Serial.println(value.c_str()): prints the received string to the serial portpCharacteristic->notify(): pushes the current characteristic content to the subscribed phone via notifyi++: increments the line index, preparing for the next write⚠️ Notes:
onWrite is only triggered when the phone performs a WRITE operation; READ and NOTIFY do not trigger this callbacknotify(), make sure the phone has enabled notification subscription (click the notify icon in the app); otherwise the phone will not receive the echoed datanotify() is the current value of the characteristic, which in this example is the value just written by the phone, so it behaves as an "echo"i is a global variable that keeps accumulating across multiple writes and auto-resets and clears the screen after reaching 28The setupBLE() function completes all BLE-related setup, including BLE device initialization, server/service/characteristic creation, property configuration, and advertising startup. It is the core configuration function of this example.
void setupBLE()
{
BLEDevice::init("ESP32_BT_BLE");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC1_UUID,
BLECharacteristic::PROPERTY_READ|
BLECharacteristic::PROPERTY_NOTIFY|
BLECharacteristic::PROPERTY_WRITE);
pCharacteristic->setCallbacks(new MyCallbacks());
pCharacteristic->addDescriptor(new BLE2902());
pCharacteristic->setValue("ESP32 BT BLE");
pService->start();
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();
}
Initialize the BLE device and set its name
BLEDevice::init("ESP32_BT_BLE");
Create the BLE server and register the callback
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
Create the service
pService = pServer->createService(SERVICE_UUID);
Create the characteristic and configure properties
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC1_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_WRITE);
PROPERTY_READ: allows the phone to read this characteristicPROPERTY_NOTIFY: allows the ESP32 to actively push data to the phonePROPERTY_WRITE: allows the phone to write data to this characteristicRegister the characteristic callback and add the descriptor
pCharacteristic->setCallbacks(new MyCallbacks());
pCharacteristic->addDescriptor(new BLE2902());
Set the initial value and start the service
pCharacteristic->setValue("ESP32 BT BLE");
pService->start();
Start advertising
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();
⚠️ Notes:
BLEDevice::init() must be called before using any other BLE functionsaddDescriptor(new BLE2902()) must be called after the characteristic is created and before the service is started; otherwise notify will not workpService->start()pAdvertising->start() starts advertisingnew MyServerCallbacks() and new MyCallbacks() are managed by the BLE library and do not need to be released manuallyThe setup() function completes the initialization of the serial port and TFT screen, calls setupBLE() to start the BLE server, and displays the startup prompt on the screen.
void setup()
{
Serial.begin(115200);
my_lcd.begin();
my_lcd.setRotation(0);
my_lcd.fillScreen(TFT_WHITE);
my_lcd.setTextFont(2);
my_lcd.setTextColor(TFT_RED);
setupBLE();
my_lcd.drawString("BLE server start.", 10, 0);
my_lcd.drawString("receive data :",10,16,2);
}
Serial.begin(115200);
my_lcd.begin();
my_lcd.setRotation(0);
my_lcd.fillScreen(TFT_WHITE);
Serial.begin(115200): initializes serial communication with a baud rate of 115200 (used to print received data)my_lcd.begin(): initializes the TFT screenmy_lcd.setRotation(0): sets the screen rotation to 0 (portrait mode)my_lcd.fillScreen(TFT_WHITE): clears the screen and fills it with a white background⚠️ Note:
my_lcd.setTextFont(2);
my_lcd.setTextColor(TFT_RED);
my_lcd.setTextFont(2): sets the font to font 2 (a medium-sized font suitable for displaying text)my_lcd.setTextColor(TFT_RED): sets the text color to redsetupBLE();
my_lcd.drawString("BLE server start.", 10, 0);
my_lcd.drawString("receive data :",10,16,2);
setupBLE(): calls the function defined earlier to complete all BLE configurationdrawString("BLE server start.", 10, 0): displays the server startup prompt at (10, 0)drawString("receive data :", 10, 16, 2): displays the data receiving area label at (10, 16) using font 2⚠️ Notes:
setupBLE(), because the BLE callbacks operate on the screenThe loop() function only performs a delay; actual data sending/receiving is entirely asynchronously driven by BLE callbacks, and the main loop is not involved in the business logic.
void loop()
{
delay(3000);
}
delay(3000): delays for 3 seconds, idly waiting for BLE eventsonWrite callback in the BLE stack task⚠️ Notes:
loop() in this example is almost empty, which is a typical pattern for callback-driven BLE programsloop(), as this may compete with the stack taskpCharacteristic->setValue(...) + pCharacteristic->notify(), and it is best to check the deviceConnected status firstdelay(3000) only reduces CPU usage; its value does not affect the real-time performance of BLE communicationThis example implements two-way communication between the ESP32 BLE server (peripheral) and a phone through the following steps:
onWrite callback → screen display + serial print + notify echoKey functions used in the program include:
BLEDevice::init(): initialize the BLE device and set its nameBLEDevice::createServer(): create the BLE serverpServer->setCallbacks(): register the server callbackpServer->createService(): create the servicepService->createCharacteristic(): create the characteristic and configure propertiespCharacteristic->setCallbacks(): register the characteristic callbackpCharacteristic->addDescriptor(): add the descriptor (BLE2902)pCharacteristic->setValue(): set the characteristic contentpCharacteristic->getValue(): get the characteristic content (read the phone's written data in onWrite)pCharacteristic->notify(): push notification data to the phonepService->start(): start the servicepServer->getAdvertising(): get the advertising objectpAdvertising->start(): start advertisingmy_lcd.drawString(): display text on the screenmy_lcd.fillRect(): fill a rectangular area (clear screen/clear line)If you need to modify the code for different application scenarios, you can adjust the following aspects:
BLEDevice::init("My_ESP32_BLE"); // Change to a custom name
#define SERVICE_UUID "0000180F-0000-1000-8000-00805F9B34FB" // Standard Battery Service UUID
#define CHARACTERISTIC1_UUID "00002A19-0000-1000-8000-00805F9B34FB" // Standard Battery Level UUID
Note: the UUID must be in the standard 8-4-4-4-12 hexadecimal format; after modification, the phone side (if manual UUID entry is required) must be updated accordingly.
BLECharacteristic* pChar2 = pService->createCharacteristic(
"DFCD000B-36E1-4688-B7F5-EA07361B26A8",
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
pChar2->setValue("channel2");
// If notify is needed, also call addDescriptor(new BLE2902())
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC1_UUID,
BLECharacteristic::PROPERTY_READ); // Only allow the phone to read, no writing
void loop()
{
if (deviceConnected)
{
String msg = "uptime:" + String(millis() / 1000) + "s";
pCharacteristic->setValue(msg.c_str());
pCharacteristic->notify();
}
delay(3000);
}
if(i > 10) // Change to a maximum of 11 lines (i=0~10)
{
i = 0;
my_lcd.fillRect(0, 32, my_lcd.width()-1, my_lcd.height()-32, TFT_WHITE);
}
Note: when modifying the number of lines, the line height (16) or the starting Y coordinate (32) must be adjusted accordingly to avoid overlapping display or going off-screen.
my_lcd.drawString(value.c_str(), 5, 32 + 20 * i); // Change line height to 20
// The clear area and maximum number of lines must also be updated:
// screen height / 20 is the maximum number of lines
By default, the ESP32 continues to advertise after disconnection, but if reconnection fails, you can manually restart advertising in onDisconnect:
void onDisconnect(BLEServer* pServer)
{
my_lcd.fillRect(10, 0, my_lcd.width()-1, 20, TFT_WHITE);
my_lcd.drawString("BLE device disconnected.", 10, 0);
deviceConnected = false;
pServer->getAdvertising()->start(); // Restart advertising
}
void onWrite(BLECharacteristic *pCharacteristic)
{
String value = pCharacteristic->getValue();
if (value.length() > 0)
{
String reply = "Recv:" + value; // Concatenate a custom prefix
pCharacteristic->setValue(reply.c_str());
pCharacteristic->notify(); // Echo back "Recv:xxx"
Serial.println(reply);
// ... screen display
}
}
The phone cannot scan for the "ESP32_BT_BLE" device
pAdvertising->start() is executedThe phone disconnects immediately after connecting
The screen does not display after writing data
onWrite has a value.length() > 0 checki has reached 28; at this point the screen is cleared and reset, and new data should appear from the first lineThe phone does not receive notify echo data
PROPERTY_NOTIFY and addDescriptor(new BLE2902()) has been callednotify()Garbled text or abnormal font on the screen
setRotation(0) is correct; this example uses portrait modeTFT_eSPI library font configuration is correct; setTextFont(2) requires the corresponding font fileChinese characters show as blocks or garbled text
loadFont() to load a Chinese font file, or use a UTF-8-capable fontThe "receive data:" label disappears
The screen does not refresh after more than 28 writes
i>27 check and the clear logic; after 28 writes, the receiving area should be cleared and the display restarted from the first linemy_lcd.fillRect area parameters are correct (y starts at 32, height is screen height minus 32)Multiple phones fail to connect at the same time
No output in the Serial Monitor
Serial.begin(115200)onWrite is triggered (when the phone writes data); no output when not writing is normal