The ESP32 is one of the most versatile microcontrollers available today, and its built-in Bluetooth support is a major reason why. In this tutorial, you will build a complete BLE remote control system: an ESP32 that acts as a BLE server, letting you toggle LEDs and read live temperature/humidity data from your phone — no app development required.
We will cover everything from BLE fundamentals to a working Web Bluetooth dashboard you can open in Chrome.
Bluetooth Classic vs BLE: Which One Do You Need?
The ESP32 supports both Bluetooth Classic and Bluetooth Low Energy (BLE). They are fundamentally different protocols that share the same radio hardware.
| Feature | Bluetooth Classic | BLE (Bluetooth Low Energy) |
|---|---|---|
| Data rate | Up to 3 Mbps | Up to 2 Mbps (BLE 5.0) |
| Power consumption | High (continuous connection) | Very low (intermittent bursts) |
| Range | ~10-30 m | ~10-50 m (up to 200 m with BLE 5.0) |
| Connection model | Continuous stream | Event-driven, short bursts |
| Best for | Audio streaming, serial data | Sensors, remote control, beacons |
| Profiles | SPP, A2DP, HFP | GATT-based custom profiles |
| Phone support | Android (limited on iOS) | Android + iOS + Web Bluetooth |
Use Bluetooth Classic when you need continuous high-throughput data — streaming audio to a speaker, or replacing a serial cable with SPP (Serial Port Profile).
Use BLE for almost everything else: sensor readings, remote control, status notifications, proximity detection. BLE is the better choice for battery-powered projects and has broader platform support, including the Web Bluetooth API in Chrome.
For this project, we are using BLE because it gives us phone compatibility across platforms and lets us build a browser-based controller.
BLE Core Concepts
Before writing code, you need to understand how BLE organizes data. Unlike Bluetooth Classic's stream-based model, BLE uses a structured database called GATT (Generic Attribute Profile).
The GATT Hierarchy
BLE Server (ESP32)
└── Service (e.g., "LED Control Service")
├── Characteristic (e.g., "LED State")
│ ├── Value: 0x01
│ └── Descriptor (optional metadata)
└── Characteristic (e.g., "Sensor Data")
├── Value: "25.3,60.1"
└── Descriptor
Server: The device that holds the data (our ESP32). Client: The device that reads/writes data (your phone or browser). Service: A logical grouping of related data. Think of it as a folder. Characteristic: A single data point within a service. This is where actual values live. Descriptor: Optional metadata about a characteristic (units, description, etc.).
UUIDs
Every service and characteristic is identified by a UUID (Universally Unique Identifier). Bluetooth SIG defines standard UUIDs for common services (heart rate, battery level, etc.), but for custom projects you generate your own 128-bit UUIDs.
You can generate UUIDs at uuidgenerator.net. We will use these throughout the tutorial:
- Service UUID:
4fafc201-1fb5-459e-8fcc-c5c9c331914b - LED Characteristic UUID:
beb5483e-36e1-4688-b7f5-ea07361b26a8 - Sensor Characteristic UUID:
1c95d5e3-d8f7-413a-bf3d-7a2e5d7be87e
Advertising
Before a client can connect, the BLE server must advertise its presence. The server periodically broadcasts small packets containing its name and available services. Clients scan for these advertisements and initiate a connection when they find a device they want to talk to.
Characteristic Properties
Each characteristic declares what operations it supports:
| Property | Direction | Description |
|---|---|---|
| Read | Client reads from server | Client requests the current value |
| Write | Client writes to server | Client sends a new value |
| Notify | Server pushes to client | Server sends updates without client asking |
| Indicate | Server pushes to client | Like Notify, but client must acknowledge |
Our LED characteristic will be Read + Write (phone sends ON/OFF commands). Our sensor characteristic will be Read + Notify (ESP32 pushes temperature updates).
ESP32 BLE Capabilities
The ESP32's Bluetooth stack is impressively capable:
- Dual mode: Supports Bluetooth Classic and BLE simultaneously
- Simultaneous WiFi + BLE: Run a web server and BLE server at the same time
- Server and Client: ESP32 can act as both BLE server and client
- Multiple connections: Handle several BLE clients at once (typically up to 3-4)
- BLE 4.2 compliant: Supports LE Secure Connections, Data Length Extension
- Custom services: Define any GATT structure you need
This means you could have an ESP32 that serves sensor data over BLE to a phone while simultaneously uploading that data to a cloud server over WiFi.
Hardware Setup
Components Needed
| Component | Quantity | Purpose |
|---|---|---|
| ESP32 DevKit (any variant) | 1 | Main controller |
| LED (any color) | 1 | Visual output |
| 220-ohm resistor | 1 | Current limiting for LED |
| DHT22 temperature/humidity sensor | 1 | Sensor input |
| 10K-ohm resistor | 1 | Pull-up for DHT22 data line |
| Breadboard | 1 | Prototyping |
| Jumper wires | Several | Connections |
Wiring
ESP32 GPIO 2 ----[220 ohm]---- LED Anode (+)
LED Cathode (-) ---- GND
ESP32 GPIO 4 ---- DHT22 Data Pin
ESP32 3.3V ---- DHT22 VCC
ESP32 GND ---- DHT22 GND
10K resistor between DHT22 VCC and Data Pin (pull-up)
GPIO 2 is used for the LED because it is connected to the onboard LED on most ESP32 DevKit boards, giving you a visual indicator even without an external LED.
Installing Libraries
In the Arduino IDE, install these libraries via Library Manager:
- DHT sensor library by Adafruit (search "DHT sensor library")
- Adafruit Unified Sensor (dependency, install when prompted)
The BLE library is included with the ESP32 board package — no separate installation needed.
ESP32 BLE Server Code
This is the complete working code. Upload it to your ESP32.
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <DHT.h>
// --- Pin definitions ---
#define LED_PIN 2
#define DHT_PIN 4
#define DHT_TYPE DHT22
// --- BLE UUIDs ---
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define LED_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define SENSOR_CHAR_UUID "1c95d5e3-d8f7-413a-bf3d-7a2e5d7be87e"
// --- Global objects ---
DHT dht(DHT_PIN, DHT_TYPE);
BLEServer* pServer = nullptr;
BLECharacteristic* pLedCharacteristic = nullptr;
BLECharacteristic* pSensorCharacteristic = nullptr;
bool deviceConnected = false;
bool oldDeviceConnected = false;
unsigned long lastSensorUpdate = 0;
const unsigned long SENSOR_INTERVAL = 2000; // 2 seconds
// --- Connection callbacks ---
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
Serial.println("Client connected");
}
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("Client disconnected");
}
};
// --- LED write callback ---
class LedCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* pCharacteristic) {
String value = pCharacteristic->getValue().c_str();
if (value.length() > 0) {
Serial.print("Received LED command: ");
Serial.println(value);
if (value == "1" || value == "ON") {
digitalWrite(LED_PIN, HIGH);
Serial.println("LED ON");
} else if (value == "0" || value == "OFF") {
digitalWrite(LED_PIN, LOW);
Serial.println("LED OFF");
}
}
}
};
void setup() {
Serial.begin(115200);
Serial.println("Starting BLE Remote Control...");
// Initialize hardware
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
dht.begin();
// Initialize BLE
BLEDevice::init("ESP32-Remote");
// Create BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Create BLE Service
BLEService* pService = pServer->createService(SERVICE_UUID);
// Create LED Characteristic (Read + Write)
pLedCharacteristic = pService->createCharacteristic(
LED_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pLedCharacteristic->setCallbacks(new LedCallbacks());
pLedCharacteristic->setValue("0");
// Create Sensor Characteristic (Read + Notify)
pSensorCharacteristic = pService->createCharacteristic(
SENSOR_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
// BLE2902 descriptor is required for notifications to work
pSensorCharacteristic->addDescriptor(new BLE2902());
pSensorCharacteristic->setValue("0.0,0.0");
// Start the service
pService->start();
// Start advertising
BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
// Helps with iPhone connection issues
pAdvertising->setMinPreferred(0x06);
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("BLE server ready. Waiting for connections...");
}
void loop() {
// Send sensor data via notifications
if (deviceConnected && (millis() - lastSensorUpdate >= SENSOR_INTERVAL)) {
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
if (!isnan(temperature) && !isnan(humidity)) {
// Format: "temperature,humidity"
String sensorData = String(temperature, 1) + "," + String(humidity, 1);
pSensorCharacteristic->setValue(sensorData.c_str());
pSensorCharacteristic->notify();
Serial.print("Sent sensor data: ");
Serial.println(sensorData);
}
lastSensorUpdate = millis();
}
// Handle reconnection — restart advertising after disconnect
if (!deviceConnected && oldDeviceConnected) {
delay(500); // Give the stack time to get ready
pServer->startAdvertising();
Serial.println("Restarted advertising");
oldDeviceConnected = deviceConnected;
}
// Detect new connection
if (deviceConnected && !oldDeviceConnected) {
oldDeviceConnected = deviceConnected;
}
}
How the Code Works
- Setup: The ESP32 initializes as a BLE server named "ESP32-Remote" with one service containing two characteristics.
- LED Characteristic: When a client writes "1"/"ON" or "0"/"OFF", the callback fires and toggles GPIO 2.
- Sensor Characteristic: Every 2 seconds, the ESP32 reads the DHT22 and pushes the data to any subscribed client via
notify(). - Reconnection handling: After a client disconnects, advertising restarts so new clients can find the device.
Testing with nRF Connect
nRF Connect is a free BLE debugging app by Nordic Semiconductor, available on both Android and iOS. It is the single most useful tool for BLE development.
Step-by-Step Testing
- Upload the code to your ESP32 and open the Serial Monitor at 115200 baud.
- Install nRF Connect from Google Play or the App Store.
- Open the app and tap Scan. You should see "ESP32-Remote" in the device list.
- Tap Connect.
- You will see the service UUID
4fafc201-.... Tap it to expand. - You will see both characteristics listed.
To control the LED:
- Find the LED characteristic (
beb5483e-...). - Tap the upload arrow (write) icon.
- Select Text format and type
1. Tap Send. The LED turns on. - Write
0to turn it off.
To read sensor data:
- Find the Sensor characteristic (
1c95d5e3-...). - Tap the triple down arrow icon to enable notifications.
- You will see values updating every 2 seconds in the format
25.3,60.1.
If the device does not appear in the scan list, check that Bluetooth and Location permissions are enabled on your phone.
Web Bluetooth: Control ESP32 from Chrome
The Web Bluetooth API lets a web page communicate directly with BLE devices. No app installation needed — just open a page in Chrome and connect. This works on Chrome for Android, Chrome for desktop (Windows, macOS, Linux), and Edge.
Note: Web Bluetooth does not work on iOS Safari or Firefox. Use nRF Connect on iOS instead.
Complete Web BLE Controller
Save this as an HTML file and open it in Chrome. You can serve it from any web server, or even open it as a local file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 BLE Remote Control</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0F1A2E;
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
max-width: 400px;
width: 100%;
padding: 24px;
}
h1 { font-size: 1.5rem; margin-bottom: 24px; text-align: center; }
.card {
background: #1a2744;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
}
.card h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; color: #64748B; margin-bottom: 12px; }
.btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.btn:hover { opacity: 0.9; }
.btn-connect { background: #FF6B35; color: #fff; }
.btn-disconnect { background: #dc3545; color: #fff; }
.btn-led { background: #1E3A5F; color: #fff; margin-top: 8px; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.status { text-align: center; font-size: 0.85rem; color: #64748B; margin-top: 8px; }
.sensor-value { font-size: 2rem; font-weight: 700; color: #FF6B35; }
.sensor-label { font-size: 0.85rem; color: #64748B; }
.sensor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.sensor-item { text-align: center; }
#log {
background: #0B1423;
border-radius: 8px;
padding: 12px;
font-family: monospace;
font-size: 0.75rem;
max-height: 150px;
overflow-y: auto;
color: #64748B;
}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 BLE Remote</h1>
<div class="card">
<h2>Connection</h2>
<button class="btn btn-connect" id="connectBtn" onclick="toggleConnection()">
Connect to ESP32
</button>
<p class="status" id="statusText">Disconnected</p>
</div>
<div class="card">
<h2>LED Control</h2>
<button class="btn btn-led" id="ledOnBtn" onclick="sendLedCommand('1')" disabled>
LED ON
</button>
<button class="btn btn-led" id="ledOffBtn" onclick="sendLedCommand('0')" disabled>
LED OFF
</button>
</div>
<div class="card">
<h2>Sensor Data</h2>
<div class="sensor-grid">
<div class="sensor-item">
<div class="sensor-value" id="temperature">--</div>
<div class="sensor-label">Temperature (C)</div>
</div>
<div class="sensor-item">
<div class="sensor-value" id="humidity">--</div>
<div class="sensor-label">Humidity (%)</div>
</div>
</div>
</div>
<div class="card">
<h2>Log</h2>
<div id="log"></div>
</div>
</div>
<script>
// BLE UUIDs — must match the ESP32 code
const SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b';
const LED_CHAR_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26a8';
const SENSOR_CHAR_UUID = '1c95d5e3-d8f7-413a-bf3d-7a2e5d7be87e';
let bleDevice = null;
let bleServer = null;
let ledCharacteristic = null;
let sensorCharacteristic = null;
function log(msg) {
const logEl = document.getElementById('log');
const time = new Date().toLocaleTimeString();
logEl.innerHTML += `[${time}] ${msg}\n`;
logEl.scrollTop = logEl.scrollHeight;
}
async function toggleConnection() {
if (bleDevice && bleDevice.gatt.connected) {
disconnect();
} else {
await connect();
}
}
async function connect() {
try {
log('Requesting BLE device...');
// This opens the browser's device picker dialog
bleDevice = await navigator.bluetooth.requestDevice({
filters: [{ name: 'ESP32-Remote' }],
optionalServices: [SERVICE_UUID]
});
bleDevice.addEventListener('gattserverdisconnected', onDisconnected);
log('Connecting to GATT server...');
bleServer = await bleDevice.gatt.connect();
log('Getting service...');
const service = await bleServer.getPrimaryService(SERVICE_UUID);
// Get LED characteristic
log('Getting LED characteristic...');
ledCharacteristic = await service.getCharacteristic(LED_CHAR_UUID);
// Get Sensor characteristic and subscribe to notifications
log('Getting Sensor characteristic...');
sensorCharacteristic = await service.getCharacteristic(SENSOR_CHAR_UUID);
await sensorCharacteristic.startNotifications();
sensorCharacteristic.addEventListener(
'characteristicvaluechanged',
onSensorData
);
// Update UI
updateUI(true);
log('Connected successfully!');
} catch (error) {
log('Error: ' + error.message);
updateUI(false);
}
}
function disconnect() {
if (bleDevice && bleDevice.gatt.connected) {
bleDevice.gatt.disconnect();
}
}
function onDisconnected() {
log('Device disconnected');
updateUI(false);
}
function onSensorData(event) {
// Decode the characteristic value (comes as a DataView)
const decoder = new TextDecoder('utf-8');
const value = decoder.decode(event.target.value);
const parts = value.split(',');
if (parts.length === 2) {
document.getElementById('temperature').textContent = parts[0];
document.getElementById('humidity').textContent = parts[1];
log(`Sensor: ${parts[0]}C, ${parts[1]}%`);
}
}
async function sendLedCommand(state) {
if (!ledCharacteristic) return;
try {
const encoder = new TextEncoder();
await ledCharacteristic.writeValue(encoder.encode(state));
log(`LED command sent: ${state === '1' ? 'ON' : 'OFF'}`);
} catch (error) {
log('Write error: ' + error.message);
}
}
function updateUI(connected) {
const btn = document.getElementById('connectBtn');
const status = document.getElementById('statusText');
if (connected) {
btn.textContent = 'Disconnect';
btn.className = 'btn btn-disconnect';
status.textContent = 'Connected to ESP32-Remote';
document.getElementById('ledOnBtn').disabled = false;
document.getElementById('ledOffBtn').disabled = false;
} else {
btn.textContent = 'Connect to ESP32';
btn.className = 'btn btn-connect';
status.textContent = 'Disconnected';
document.getElementById('ledOnBtn').disabled = true;
document.getElementById('ledOffBtn').disabled = true;
document.getElementById('temperature').textContent = '--';
document.getElementById('humidity').textContent = '--';
}
}
</script>
</body>
</html>
How Web Bluetooth Works
navigator.bluetooth.requestDevice()opens a browser dialog where the user selects the BLE device. This is a security requirement — a web page cannot silently connect to BLE devices.device.gatt.connect()establishes the GATT connection.service.getCharacteristic()gets references to specific characteristics by UUID.characteristic.writeValue()sends data to the ESP32 (LED control).characteristic.startNotifications()subscribes to push updates from the ESP32 (sensor data).
The TextEncoder and TextDecoder convert between JavaScript strings and the ArrayBuffer format that BLE uses.
Running the Web Controller
You need to serve the HTML file over HTTPS (or from localhost) for Web Bluetooth to work. The simplest approach:
# Using Python's built-in server
python3 -m http.server 8080
# Then open http://localhost:8080 in Chrome
Chrome on Android also supports Web Bluetooth. Host the file on any HTTPS server and open it on your phone's Chrome browser.
BLE Security: Pairing and Bonding
For production projects, you should secure your BLE communications.
Pairing is the process of establishing a temporary encrypted connection. Bonding saves the pairing keys so reconnection is automatic and encrypted without repeating the pairing process.
To enable pairing on the ESP32, add this before starting the service:
// In setup(), after BLEDevice::init()
BLESecurity* pSecurity = new BLESecurity();
pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
pSecurity->setCapability(ESP_IO_CAP_NONE);
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
This enables Secure Connections with bonding. The ESP_IO_CAP_NONE setting uses "Just Works" pairing (no PIN entry) since the ESP32 has no display or keyboard. For higher security, you could implement a static passkey:
pSecurity->setCapability(ESP_IO_CAP_OUT);
pSecurity->setStaticPIN(123456);
The phone will then prompt the user to enter the PIN when connecting.
Power Consumption: Tuning BLE Advertising
BLE power consumption is dominated by advertising interval — how frequently the device broadcasts its presence. Shorter intervals mean faster discovery but higher power draw.
| Advertising Interval | Discovery Time | Current Draw (approx.) |
|---|---|---|
| 20 ms | Very fast (<1 s) | ~10 mA average |
| 100 ms (default) | Fast (~1-2 s) | ~3-5 mA average |
| 500 ms | Moderate (~3-5 s) | ~1-2 mA average |
| 1000 ms | Slow (~5-10 s) | ~0.5-1 mA average |
| 2500 ms | Very slow | ~0.2-0.5 mA average |
To change the advertising interval:
BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
// Set interval in units of 0.625 ms
// 160 = 100 ms, 800 = 500 ms, 1600 = 1000 ms
pAdvertising->setMinInterval(800); // 500 ms
pAdvertising->setMaxInterval(1600); // 1000 ms
For a battery-powered remote control, use 500-1000 ms intervals. For a mains-powered device where fast discovery matters, keep the default 100 ms.
You can also use ESP32 light sleep between advertising events for even lower power consumption. The BLE stack handles wake-up automatically.
Range and Performance
Typical Indoor Range
| Condition | Expected Range |
|---|---|
| Open room, line of sight | 15-30 metres |
| Through one wall | 8-15 metres |
| Through two walls | 3-8 metres |
| Through floor/ceiling | 5-10 metres |
Factors Affecting Signal
- Antenna orientation: The ESP32's PCB antenna is directional. Performance is best when the antenna faces the client.
- Body absorption: Holding the ESP32 in your hand reduces range significantly. Mount it elevated and unobstructed.
- WiFi interference: Both WiFi and BLE use the 2.4 GHz band. The ESP32 handles coexistence internally, but heavy WiFi traffic can affect BLE throughput.
- TX power: Increase the transmission power for better range at the cost of higher power consumption.
// Set TX power (default is ESP_PWR_LVL_P3 = +3 dBm)
// Options: N12, N9, N6, N3, N0, P3, P6, P9
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); // +9 dBm max
Common Issues and Solutions
Connection Drops
BLE connections can drop for several reasons:
- Supervision timeout: If no data is exchanged within the timeout period, the connection drops. The ESP32 default is 20 seconds. Sending periodic notifications (like our sensor data) keeps the connection alive.
- WiFi coexistence: If running WiFi and BLE simultaneously, ensure your WiFi operations do not block the main loop for too long. Use non-blocking WiFi calls.
- Distance: Moving out of range causes a clean disconnect. The
onDisconnectcallback fires, and you should restart advertising.
MTU Size
The default BLE Maximum Transmission Unit is 23 bytes (20 bytes usable payload). For larger data transfers, negotiate a bigger MTU:
// In setup(), after BLEDevice::init()
BLEDevice::setMTU(517); // Request up to 512 bytes payload
The actual MTU is negotiated between server and client — both must support the requested size.
Characteristic Value Size Limits
A single characteristic value can hold up to 512 bytes (with MTU negotiation). For larger data, you need to implement chunked transfers or use multiple characteristics. For our sensor data ("25.3,60.1"), this is more than sufficient.
iPhone Connection Issues
iPhones can be finicky with BLE. Two common fixes:
- Set the minimum connection interval hint in advertising (already included in our code).
- Include the service UUID in the advertising data (already included).
If the device still does not appear on iPhone, ensure the service UUID is in the advertising packet, not just the scan response.
Advanced: BLE Mesh Networking
BLE Mesh allows multiple ESP32 devices to form a network where messages can be relayed from node to node, extending range far beyond a single BLE connection.
Key concepts:
- Nodes: Each ESP32 in the mesh is a node.
- Relay nodes: Nodes that forward messages from other nodes, extending coverage.
- Provisioning: The process of adding a new node to the mesh.
- Publish/Subscribe: Nodes publish messages to groups, and other nodes subscribe to those groups.
The ESP-IDF (ESP32's native SDK) includes a BLE Mesh stack. A basic mesh setup involves:
#include "esp_ble_mesh_defs.h"
#include "esp_ble_mesh_common_api.h"
#include "esp_ble_mesh_networking_api.h"
#include "esp_ble_mesh_provisioning_api.h"
#include "esp_ble_mesh_config_model_api.h"
#include "esp_ble_mesh_generic_model_api.h"
// Mesh requires ESP-IDF, not Arduino framework
// See Espressif's BLE Mesh examples for full implementation
BLE Mesh is an advanced topic that requires the ESP-IDF framework rather than Arduino. Espressif provides well-documented examples in their ESP-IDF BLE Mesh repository.
Project Ideas
Now that you have the fundamentals working, here are projects that build on this BLE remote control.
BLE-Controlled Robot
Replace the LED with motor driver pins. Map BLE characteristic values to motor commands:
// In the write callback:
if (value == "F") { forward(); }
else if (value == "B") { backward(); }
else if (value == "L") { turnLeft(); }
else if (value == "R") { turnRight(); }
else if (value == "S") { stop(); }
Use the Web Bluetooth page with on-screen directional buttons for a browser-based robot controller.
Wireless Sensor Network
Deploy multiple ESP32 boards with different sensors (temperature, humidity, light, soil moisture). Each runs as a BLE server with a unique name. A central ESP32 acts as a BLE client, scanning and connecting to each sensor node in round-robin fashion, collecting data, and forwarding it to a server over WiFi.
Proximity-Based Automation
Use BLE advertising and RSSI (Received Signal Strength Indicator) to detect when a phone is nearby:
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
if (advertisedDevice.getName() == "MyPhone") {
int rssi = advertisedDevice.getRSSI();
if (rssi > -60) {
// Phone is nearby — unlock door, turn on lights
digitalWrite(RELAY_PIN, HIGH);
}
}
}
};
This lets you build presence detection for home automation: lights turn on when you enter a room, doors unlock as you approach.
Wrapping Up
The ESP32's BLE capabilities open up a huge range of wireless projects without the complexity of WiFi setup and cloud dependencies. With the GATT model, you have a clean, structured way to expose data and accept commands from phones, browsers, and other microcontrollers.
The complete project from this tutorial gives you a solid foundation: a BLE server with writable and notify characteristics, tested with nRF Connect, and controlled from a web browser. From here, you can add more characteristics, implement security, reduce power consumption, and build increasingly sophisticated wireless systems.
All the hardware components used in this tutorial — ESP32 DevKit boards, DHT22 sensors, LEDs, and resistors — are available at wavtron.in.



