BLE vs Bluetooth Classic
Bluetooth Low Energy (BLE) is not a lower-speed version of Bluetooth Classic — it is a completely different protocol stack optimised for sending small bursts of data infrequently. A BLE temperature sensor can run on a coin cell for months. The tradeoff: BLE throughput is limited to ~27 Kbps effective (BLE 4.2) vs ~300 Kbps for Classic SPP. BLE is also the only option for iOS connectivity.
GATT Concepts
- Server — the ESP32; holds the data (services and characteristics)
- Client — the phone; connects to read/write characteristics and subscribe to notifications
- Service UUID — identifies a group of related data (e.g.
4fafc201-1fb5-459e-8fcc-c5c9c3319000) - Characteristic UUID — identifies one value within a service (e.g.
beb5483e-36e1-4688-b7f5-ea07361b26a8)
Temperature Notification Server
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c3319000"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
BLECharacteristic* pChar;
bool deviceConnected = false;
class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer) { deviceConnected = true; Serial.println("BLE: client connected"); }
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
Serial.println("BLE: client disconnected — restarting advertising");
pServer->getAdvertising()->start();
}
};
void setup() {
Serial.begin(115200);
BLEDevice::init("ESP32-Temp");
BLEServer* pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
BLEService* pService = pServer->createService(SERVICE_UUID);
pChar = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
);
pChar->addDescriptor(new BLE2902()); // enables notify subscription
pService->start();
BLEAdvertising* pAdv = BLEDevice::getAdvertising();
pAdv->addServiceUUID(SERVICE_UUID);
pAdv->setScanResponse(true);
BLEDevice::startAdvertising();
Serial.println("BLE advertising as 'ESP32-Temp'");
}
void loop() {
if (deviceConnected) {
float temp = 22.5 + (random(-5, 5) / 10.0); // replace with real sensor
char buf[8];
dtostrf(temp, 4, 1, buf);
pChar->setValue(buf);
pChar->notify();
Serial.printf("Notified: %s°C\n", buf);
}
delay(2000);
}
Receiving Write Commands from a Phone
#define CMD_UUID "a1234567-0000-1000-8000-00805f9b34fb"
const int LED = 2;
class CmdCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* pC) {
String val = pC->getValue().c_str();
val.trim();
if (val == "ON") { digitalWrite(LED, HIGH); Serial.println("LED ON"); }
if (val == "OFF") { digitalWrite(LED, LOW); Serial.println("LED OFF"); }
}
};
// In setup(), after createService():
BLECharacteristic* pCmd = pService->createCharacteristic(
CMD_UUID,
BLECharacteristic::PROPERTY_WRITE
);
pCmd->setCallbacks(new CmdCallbacks());
Read-Only Characteristic
#define UPTIME_UUID "b2345678-0001-1000-8000-00805f9b34fb"
BLECharacteristic* pUptime = pService->createCharacteristic(
UPTIME_UUID,
BLECharacteristic::PROPERTY_READ
);
// Update before each read (use a callback to stay current):
class UptimeCallbacks : public BLECharacteristicCallbacks {
void onRead(BLECharacteristic* pC) {
unsigned long s = millis() / 1000;
char buf[16];
snprintf(buf, sizeof(buf), "%lus", s);
pC->setValue(buf);
}
};
pUptime->setCallbacks(new UptimeCallbacks());
Using nRF Connect to Test
- Install nRF Connect for Mobile (Android/iOS) — free from Nordic Semiconductor
- Open the app → Scan → find “ESP32-Temp”
- Connect → expand the Unknown Service
- Tap the notification icon (three arrows down) to subscribe to notifications
- Watch temperature values arrive every 2 seconds
- Tap the write icon on the command characteristic → type “ON” → send
NimBLE for Lower Memory Usage
The default Bluedroid BLE stack uses ~100 KB of heap. For projects that also use Wi-Fi, switch to NimBLE-Arduino which uses ~50 KB:
// Replace all BLEDevice.h imports with: #include <NimBLEDevice.h> // API is nearly identical — NimBLEServer, NimBLEService, NimBLECharacteristic // No BLE2902 descriptor needed — NimBLE handles notifications automatically // Connections: up to 9 simultaneous clients (vs 3 with Bluedroid)