Bluetooth LE: Control an ESP32 from Your Phone Without Wi-Fi
Turn the ESP32 into a BLE device with a UART service: send commands from your phone and get replies. Works where there's no router — in a car, on a bike, in a garage.
Why BLE
Wi-Fi needs a router; Bluetooth Low Energy only needs a phone nearby. The ESP32 has BLE built in, so no extra modules are required. We'll implement the popular Nordic UART Service (NUS) — ready-made terminal apps already support it, so you don't have to write your own app.
How it works:
- the phone writes text to the RX characteristic → the ESP32 runs the command;
- the ESP32 replies through the TX characteristic using notifications.
What you need
- ESP32 DevKit (on-board LED on GPIO 2, or your own LED + 330 Ω)
- A smartphone with an app:
- Android — Serial Bluetooth Terminal or nRF Connect;
- iOS — nRF Toolbox (UART mode) or Bluefruit Connect.
The BLE library ships with the arduino-esp32 core — nothing to install.
Code
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLE2902.h>
// Standard Nordic UART Service UUIDs
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
#define RX_UUID "6e400002-b5a3-f393-e0a9-e50e24dcca9e" // phone → ESP32
#define TX_UUID "6e400003-b5a3-f393-e0a9-e50e24dcca9e" // ESP32 → phone
const int LED_PIN = 2;
BLECharacteristic *txChar;
bool connected = false;
unsigned long lastNotify = 0;
void sendText(const String &text) {
txChar->setValue((uint8_t *)text.c_str(), text.length());
txChar->notify();
}
class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *server) override {
connected = true;
}
void onDisconnect(BLEServer *server) override {
connected = false;
BLEDevice::startAdvertising(); // allow reconnecting
}
};
class RxCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *c) override {
String cmd = String(c->getValue().c_str());
cmd.trim();
cmd.toLowerCase();
if (cmd == "on") {
digitalWrite(LED_PIN, HIGH);
} else if (cmd == "off") {
digitalWrite(LED_PIN, LOW);
} else if (cmd != "status") {
sendText("Commands: on, off, status\n");
return;
}
sendText("LED " + String(digitalRead(LED_PIN) ? "ON" : "OFF") + "\n");
}
};
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
BLEDevice::init("ESP32-LED");
BLEServer *server = BLEDevice::createServer();
server->setCallbacks(new ServerCallbacks());
BLEService *service = server->createService(SERVICE_UUID);
txChar = service->createCharacteristic(TX_UUID, BLECharacteristic::PROPERTY_NOTIFY);
txChar->addDescriptor(new BLE2902());
BLECharacteristic *rxChar = service->createCharacteristic(
RX_UUID, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
rxChar->setCallbacks(new RxCallbacks());
service->start();
BLEAdvertising *adv = BLEDevice::getAdvertising();
adv->addServiceUUID(SERVICE_UUID);
adv->setScanResponse(true);
BLEDevice::startAdvertising();
Serial.println("Advertising as ESP32-LED");
}
void loop() {
// Every 5 s send the phone a heartbeat with uptime
if (connected && millis() - lastNotify > 5000) {
lastNotify = millis();
sendText("uptime " + String(millis() / 1000) + " s\n");
}
}
Testing
- Flash the board. The Serial Monitor prints
Advertising as ESP32-LED. - Open the terminal app → Devices → Bluetooth LE → pick ESP32-LED.
- Send
on— the LED lights up and you getLED ONback. - Every 5 seconds
uptime …appears in the terminal.
You do not need to pair the device in your phone's Bluetooth settings — connect straight from the app.
Things to know
- Core 2.x vs 3.x.
getValue()returnsstd::stringin 2.x andStringin 3.x. Calling.c_str()keeps the code compatible with both. - Packet size. By default one message fits 20 bytes; split longer strings or raise the MTU.
- Security. Anyone nearby can connect. For real devices enable PIN-based encryption (
BLESecurity) or require a "password" as the first command. - BLE and Wi-Fi can run together but use a lot of RAM — for bigger projects look at the NimBLE-Arduino library.
Ideas
- Control a relay in a garage or car with no internet.
- A web page using the Web Bluetooth API (Chrome) that connects to the ESP32 straight from the browser.
- Wi-Fi provisioning over BLE instead of a captive portal — the way commercial devices do it.