← All projects
Wireless Intermediate

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.

Difficulty
Intermediate
Time
1 h
Category
Wireless

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

  1. Flash the board. The Serial Monitor prints Advertising as ESP32-LED.
  2. Open the terminal app → Devices → Bluetooth LE → pick ESP32-LED.
  3. Send on — the LED lights up and you get LED ON back.
  4. 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() returns std::string in 2.x and String in 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.