Guides · 16 min read

ESP32 Bluetooth Tutorial: Classic vs BLE, LED Control and Sensors

ESP32 Bluetooth guide: control an LED from your phone with BluetoothSerial, build a BLE server with services and UUIDs, test with nRF Connect, send sensor data.

ESP32 Bluetooth uses the wireless radio built into the chip to connect your ESP32 to a phone, tablet or computer without any extra module. The original ESP32 supports two different kinds of Bluetooth at once: Bluetooth Classic, which behaves like a serial port, and Bluetooth Low Energy (BLE), which uses very little power. On an Arduino Uno you need a separate HC-05 module to talk to your phone; on the ESP32 a few lines of code are enough. In this guide you'll set up both methods step by step, switch an LED on from your phone, write your own BLE service and send sensor data to your phone.

Which method you choose depends on your phone and your board. If you have an Android phone and a classic ESP32 DevKit, the quickest start is the BluetoothSerial library. If you use an iPhone, or your board is an ESP32-S3 or ESP32-C3, Classic Bluetooth won't help you; you need BLE. Near the end you'll also find the same BLE project for the Raspberry Pi Pico W in MicroPython. If this is your first time with the ESP32, finish the Arduino IDE setup in What Is the ESP32? first.

ESP32 development board with Bluetooth and BLE
ESP32 development board: Wi-Fi, Bluetooth Classic and BLE all live inside the same chip, so you don't need an external Bluetooth module to connect to a phone.

What is ESP32 Bluetooth? Classic vs BLE

Bluetooth Classic is the older, familiar kind used in wireless headphones and modules like the HC-05. The ESP32 uses it through SPP (Serial Port Profile): bytes flow one after another as if there were an invisible serial cable between the phone and the board. You first pair the device in your phone's Bluetooth settings, then connect with a terminal app. BLE, on the other hand, is a separate system introduced with Bluetooth 4.0 in 2010 and designed for devices that run for months on a battery. BLE has no continuous data stream; the ESP32 keeps a small table of values (GATT), and the phone reads them, writes them or subscribes to be told when they change. Despite the similar names, the two can't talk to each other: an app written for BLE can't connect to a Classic Bluetooth device.

FeatureBluetooth Classic (SPP)Bluetooth Low Energy (BLE)
Arduino libraryBluetoothSerial (included in the ESP32 package)BLEDevice (included in the ESP32 package) or NimBLE-Arduino
Data modelContinuous byte stream, like a serial portGATT table made of services and characteristics
Amount of dataHigh, suited to continuous streamsSmall packets; 20 bytes per notification by default
Power consumptionConstantly high while connectedVery low; can run for months on a battery
PairingDone in the phone's Bluetooth settingsNot needed; the connection is made inside the app
AndroidSupportedSupported
iPhone (iOS)Not supportedSupported
Supported ESP32sOnly the original ESP32 (WROOM-32, WROVER)ESP32, S3, C3, C6 and H2 (not S2)
Typical useSerial terminal, robot car, HC-05 replacementSensors, wearables, smart home, beacons

Here's a practical rule: if you want to send and receive text from an Android phone and see results fast, use Classic; if you need iPhone support, low power or one of the newer ESP32 boards, use BLE. If you're starting a new project, BLE is worth learning, because it works on both kinds of phone and is found in every new Espressif chip with Bluetooth. If you've worked with the HC-05 before, you'll see that the command logic from HC-05 Bluetooth Module is exactly the same with BluetoothSerial.

Which ESP32 boards have which Bluetooth?

Not every chip in the ESP32 family has the same Bluetooth hardware. The most common mistake is trying to upload a BluetoothSerial example found online to an ESP32-S3 or ESP32-C3 board. Espressif put Classic Bluetooth only in the original ESP32 chip; the later chips that have Bluetooth support BLE only. You can tell which chip your board carries from the text on the module's metal shield (for example ESP32-WROOM-32 or ESP32-S3-WROOM-1).

Board / chipBluetooth ClassicBLENote
ESP32 (WROOM-32, WROVER, DevKit V1)YesYes (4.2)The only ESP32 family that runs BluetoothSerial
ESP32-S2NoNoWi-Fi only
ESP32-S3NoYes (Bluetooth 5)BluetoothSerial won't compile, use BLE
ESP32-C3NoYes (Bluetooth 5)Small, cheap BLE boards
ESP32-C6NoYes (Bluetooth 5)Also has Wi-Fi 6, Zigbee and Thread
ESP32-H2NoYes (Bluetooth 5)No Wi-Fi; focused on Zigbee and Thread
ESP8266 (NodeMCU, Wemos D1)NoNoNeeds an HC-05 or HM-10 for Bluetooth
Raspberry Pi Pico W / Pico 2 WIn hardwareYesMicroPython supports BLE only
Raspberry Pi Pico / Pico 2NoNoNeeds an external Bluetooth module
If you compile BluetoothSerial code with an ESP32-S3, C3, C6 or H2 selected in the board menu, the check line in Espressif's examples (and in our code below) stops the build with "Serial Bluetooth not available or not enabled. It is only available for the ESP32 chip." This isn't an installation or library problem; the chip simply has no Classic Bluetooth hardware. If you have one of these boards, go straight to the BLE (Nordic UART) example below.

Bluetooth Classic: controlling an LED from your phone with BluetoothSerial

The BluetoothSerial library presents the ESP32 to your phone like an HC-05 module and uses the same commands as the Serial object: available(), read(), print() and println(). The library comes with the ESP32 board package, so there's nothing extra to install. For the circuit, the blue built-in LED wired to GPIO 2 on most DevKit boards is enough; if you use an external LED, connect it to GPIO 2 through a 220 Ω resistor. You can check the resistor value with the LED resistor calculator and see the ESP32's other safe pins in the Pinout tool. Make sure ESP32 Dev Module is selected under Tools > Board and upload this code:

C++ (Arduino IDE)
1#include "BluetoothSerial.h"
2
3// Klasik Bluetooth yalnızca ilk ESP32'de var / Classic Bluetooth exists only on the original ESP32
4#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
5#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
6#endif
7#if !defined(CONFIG_BT_SPP_ENABLED)
8#error Serial Bluetooth not available or not enabled. It is only available for the ESP32 chip.
9#endif
10
11BluetoothSerial SerialBT;
12const int LED_PIN = 2;                 // DevKit dahili LED / DevKit onboard LED
13bool ledAcik = false;
14
15void ledAyarla(bool acik) {
16  ledAcik = acik;
17  digitalWrite(LED_PIN, acik ? HIGH : LOW);
18  SerialBT.println(acik ? "LED acik / LED on" : "LED kapali / LED off");
19}
20
21void setup() {
22  Serial.begin(115200);
23  pinMode(LED_PIN, OUTPUT);
24  SerialBT.begin("BlueGrays-ESP32");   // telefonda görünecek ad / name shown on the phone
25  Serial.println("Eslesmeye hazir / Ready to pair");
26}
27
28void loop() {
29  if (SerialBT.available()) {
30    String komut = SerialBT.readStringUntil('\n');  // satır sonuna kadar oku / read until newline
31    komut.trim();                                   // CR ve boşlukları sil / strip CR and spaces
32    komut.toLowerCase();
33
34    if (komut == "1" || komut == "ac") {
35      ledAyarla(true);
36    } else if (komut == "0" || komut == "kapat") {
37      ledAyarla(false);
38    } else if (komut == "durum") {
39      SerialBT.println(ledAcik ? "LED acik / LED on" : "LED kapali / LED off");
40    } else if (komut.length() > 0) {
41      SerialBT.print("Bilinmeyen komut / Unknown command: ");
42      SerialBT.println(komut);
43    }
44  }
45
46  if (Serial.available()) {            // Seri Monitör'den telefona / Serial Monitor to phone
47    SerialBT.write(Serial.read());
48  }
49}

The code reads every line coming from the phone as a command. readStringUntil() waits until the newline character arrives, and trim() removes the carriage return (CR) the phone adds as well as any spaces; if you forget this line, sending "1" actually delivers "1" plus an invisible CR character and no command matches. Because the commands are converted to lower case, "AC" and "ac" do the same thing. The two lines at the bottom forward everything you type in the Serial Monitor to the phone, so you can hold a wireless chat between your computer and your phone. The #error checks at the top make the build stop with a clear message if the wrong board is selected.

Connecting from your phone with Serial Bluetooth Terminal

  1. After uploading, open the Serial Monitor at 115200 baud and look for the "Eslesmeye hazir / Ready to pair" message.
  2. On your Android phone, scan for new devices under Settings > Bluetooth and pair with BlueGrays-ESP32. By default the ESP32 pairs without asking for a PIN; if the phone shows a confirmation code, accept it.
  3. Install the Serial Bluetooth Terminal app by Kai Morich from Google Play.
  4. In the app, open Devices from the side menu and pick BlueGrays-ESP32 on the Bluetooth Classic tab.
  5. Tap the plug icon in the top bar; once "Connected" appears on screen, you're connected.
  6. Type 1 in the box at the bottom and send it: the LED lights up and the reply "LED acik / LED on" appears. 0 switches it off, and durum tells you its current state.
Long-press the M1 and M2 macro buttons at the bottom of Serial Bluetooth Terminal and save the commands "1" and "0" to switch the LED with a single tap. The Send > Newline option in the app's settings is CR+LF by default; trim() in the code already removes the extra CR. Connect a relay instead of the LED and the same code can switch a lamp or a socket; see What Is a Relay Module? for the details.

How does BLE work? Services, characteristics and UUIDs

BLE has two roles. The ESP32 plays the server (peripheral): it holds the values and announces "I'm here" by sending small advertising packets of at most 31 bytes at short intervals (every 20–40 ms by default on the ESP32). The phone plays the client (central): it scans the surroundings, finds the device and connects. Once connected, the phone reads the ESP32's GATT (Generic Attribute Profile) table. Think of this table as a filing cabinet: each drawer is a service, and each file in a drawer is a characteristic. A characteristic carries a single value (such as a temperature or an LED command) plus properties that say what can be done with it.

TermMeaningExample in this guide
AdvertisingShort packet in which the device announces its name and servicesAdvertising under the name "BlueGrays-BLE"
ServiceGroup that holds related characteristics togetherNordic UART service, Environmental Sensing (0x181A)
CharacteristicA single value that is read or writtenLED command, temperature (0x2A6E)
READThe phone reads the value whenever it wantsLast measured temperature
WRITE / WRITE_NRThe phone writes a value (with or without response)Writing "1" lights the LED
NOTIFYThe ESP32 tells the phone by itself when the value changesA new temperature every 2 seconds
Descriptor 0x2902 (CCCD)Switch that turns notifications on and offTriple-arrow button in nRF Connect
UUIDID number of services and characteristics6E400001-B5A3-F393-E0A9-E50E24DCCA9E

Every service and characteristic is identified by a UUID. Standard features defined by the Bluetooth SIG use short 16-bit numbers, such as 0x180F for the battery service and 0x2A6E for the temperature characteristic. For your own data with no standard equivalent, generate a random 128-bit UUID; in Python, import uuid; print(uuid.uuid4()) is all it takes. When choosing your own UUID, don't use a value matching the pattern xxxxxxxx-0000-1000-8000-00805F9B34FB, because that pattern is reserved for the Bluetooth SIG's 16-bit numbers. If you want to work smoothly with existing apps, it's even more practical to use UUIDs everyone recognises:

Service / characteristicUUIDData format
Battery Service / Battery Level0x180F / 0x2A191 byte, percent (0–100)
Environmental Sensing / Temperature0x181A / 0x2A6E2-byte signed integer, 0.01 °C
Environmental Sensing / Humidity0x181A / 0x2A6F2-byte unsigned integer, 0.01 %
Heart Rate / Heart Rate Measurement0x180D / 0x2A37Flags byte + heart rate value
Nordic UART Service (NUS)6E400001-B5A3-F393-E0A9-E50E24DCCA9ERX 6E400002 (write), TX 6E400003 (notify)

ESP32 BLE server: controlling an LED from your phone (Nordic UART)

In our first BLE project we turn the ESP32 into a Nordic UART Service (NUS) server. NUS is a "serial port over BLE" service defined by Nordic Semiconductor that has become a de facto standard: the phone writes to the RX characteristic and the ESP32 sends notifications from the TX characteristic. Its advantage is that many ready-made apps recognise it; nRF Connect, the Bluetooth LE tab of Serial Bluetooth Terminal and Bluefruit Connect on the iPhone all work with the same code. So the code in this section works with both Android and iPhone, and also on BLE-only boards like the ESP32-S3 and C3. The BLE library is part of the ESP32 board package, so there's nothing extra to install.

C++ (Arduino IDE)
1#include <BLEDevice.h>
2#include <BLEServer.h>
3#include <BLEUtils.h>
4#include <BLE2902.h>
5
6// Nordic UART Service UUID'leri / Nordic UART Service UUIDs
7#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
8#define RX_UUID      "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"  // telefon -> ESP32 / phone -> ESP32
9#define TX_UUID      "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"  // ESP32 -> telefon / ESP32 -> phone
10
11const int LED_PIN = 2;
12
13BLEServer *sunucu = nullptr;
14BLECharacteristic *txKarakteristik = nullptr;
15volatile bool bagli = false;
16bool oncekiBagli = false;
17volatile int bekleyenKomut = -1;          // -1: yeni komut yok / no new command
18
19class SunucuOlaylari : public BLEServerCallbacks {
20  void onConnect(BLEServer *s) { bagli = true; }
21  void onDisconnect(BLEServer *s) { bagli = false; }
22};
23
24class RxOlaylari : public BLECharacteristicCallbacks {
25  void onWrite(BLECharacteristic *c) {
26    String deger = c->getValue().c_str(); // çekirdek 2.x ve 3.x ile uyumlu / works on core 2.x and 3.x
27    deger.trim();
28    if (deger == "1") bekleyenKomut = 1;
29    else if (deger == "0") bekleyenKomut = 0;
30  }
31};
32
33void cevapGonder(const char *mesaj) {
34  txKarakteristik->setValue((uint8_t *)mesaj, strlen(mesaj));
35  txKarakteristik->notify();              // telefona bildirim / notify the phone
36}
37
38void setup() {
39  Serial.begin(115200);
40  pinMode(LED_PIN, OUTPUT);
41
42  BLEDevice::init("BlueGrays-BLE");       // yayında görünecek ad / advertised name
43  sunucu = BLEDevice::createServer();
44  sunucu->setCallbacks(new SunucuOlaylari());
45
46  BLEService *servis = sunucu->createService(SERVICE_UUID);
47
48  txKarakteristik = servis->createCharacteristic(TX_UUID, BLECharacteristic::PROPERTY_NOTIFY);
49  txKarakteristik->addDescriptor(new BLE2902());   // bildirim anahtarı (CCCD) / notification switch (CCCD)
50
51  BLECharacteristic *rx = servis->createCharacteristic(
52    RX_UUID, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
53  rx->setCallbacks(new RxOlaylari());
54
55  servis->start();
56
57  BLEAdvertising *yayin = BLEDevice::getAdvertising();
58  yayin->addServiceUUID(SERVICE_UUID);
59  yayin->setScanResponse(true);
60  yayin->setMinPreferred(0x06);           // iPhone bağlantısına yardımcı / helps iPhone connections
61  yayin->setMaxPreferred(0x12);
62  BLEDevice::startAdvertising();
63  Serial.println("BLE yayini basladi / BLE advertising started");
64}
65
66void loop() {
67  if (bekleyenKomut != -1) {              // telefondan komut geldi / command received
68    int komut = bekleyenKomut;
69    bekleyenKomut = -1;
70    digitalWrite(LED_PIN, komut == 1 ? HIGH : LOW);
71    if (bagli) cevapGonder(komut == 1 ? "LED acik" : "LED kapali");
72  }
73
74  if (!bagli && oncekiBagli) {            // bağlantı koptu / disconnected
75    delay(500);                           // yığına zaman tanı / give the stack time
76    sunucu->startAdvertising();           // yeniden görünür ol / advertise again
77    Serial.println("Yeniden yayin / Advertising again");
78    oncekiBagli = false;
79  }
80  if (bagli && !oncekiBagli) {            // yeni bağlantı / new connection
81    Serial.println("Telefon baglandi / Phone connected");
82    oncekiBagli = true;
83  }
84}

The most important part of the code is the callback classes. onConnect and onDisconnect track the connection state, and onWrite catches the value the phone writes to the RX characteristic. These functions run in the Bluetooth stack's own task, so instead of doing long jobs inside them we only change a variable; loop() does the actual work. The BLE2902 descriptor added to the TX characteristic is needed so the phone can switch notifications on. When the connection drops, the ESP32 doesn't restart advertising on its own; without the startAdvertising() line in loop(), the device can't be found again after the first connection. The setMinPreferred and setMaxPreferred lines add the preferred connection interval to the advertisement, which makes connecting with iPhones easier.

The code in this article was compiled with version 2.0.11 of the ESP32 Arduino core (board package). In this version BLE functions such as getValue() return std::string, and most examples online use that type too. With core 3.0 the BLE functions switched to Arduino's String type, so code written with std::string fails on 3.x with "conversion from 'String' to non-scalar type 'std::string' requested". If you assign the result to a String variable and add .c_str() at the end, as in our code, the line compiles on both the 2.x and 3.x cores.

Testing BLE with nRF Connect step by step

  1. Install Nordic Semiconductor's free nRF Connect for Mobile app on your phone; it's available for Android and iPhone.
  2. On Android the app asks for Bluetooth and Nearby devices permission (on Android 11 and older, location permission with location services switched on); without it the scan list stays empty.
  3. On the Scanner tab, press SCAN and find BlueGrays-BLE in the list. The RSSI value next to it is the signal strength: -50 dBm means very close, -90 dBm is the edge of the range.
  4. Tap CONNECT. The screen that opens shows the Nordic UART Service; if you had used UUIDs you generated yourself, it would say "Unknown Service" here.
  5. Tap the triple down-arrow icon next to the TX characteristic (6E400003) to switch notifications on.
  6. Tap the up-arrow icon next to the RX characteristic (6E400002), choose Text (UTF-8) as the data type, type 1 and send it. The LED lights up and "LED acik" appears on the TX line.

This test is the quickest way to prove the ESP32 side works before you write a phone app. If something goes wrong, try nRF Connect first: if the device isn't in the list, the problem is the advertising; if it connects but the service doesn't show, the service's start() call is missing; if you write a value and the LED stays off, check the comparison inside onWrite. You can also try the same code from the Bluetooth LE tab of Serial Bluetooth Terminal; the app recognises the NUS service and you chat just as with Classic Bluetooth. If you'd like to build your own Android app, take a look at App Inventor Arduino Bluetooth.

Broadcasting sensor data over BLE (DHT22)

DHT22 temperature and humidity sensor
The DHT22 measures temperature with 0.1 °C resolution; since it runs on 3.3 V, it connects directly to the ESP32.
DHT22 pinESP32Note
VCC (+)3V3Works from 3.3 to 5 V
DATA (OUT)GPIO 4On a bare sensor, add a 10 kΩ resistor between DATA and 3V3
GND (−)GNDCommon ground

In this example we use the standard Environmental Sensing service (0x181A) and its temperature (0x2A6E) and humidity (0x2A6F) characteristics. The beauty of standard UUIDs is that apps like nRF Connect can recognise the value and show it directly as "23.45 °C". The standard asks for values to be sent not as text but as integers multiplied by 100: a temperature of 23.45 °C becomes the number 2345 and is sent as 2 bytes, low byte first (little-endian). To read the sensor we use Adafruit's DHT sensor library, which we covered in the DHT11 guide; you can install it from the Library Manager.

C++ (Arduino IDE)
1#include <BLEDevice.h>
2#include <BLEServer.h>
3#include <BLE2902.h>
4#include <DHT.h>
5
6#define DHT_PIN 4
7DHT dht(DHT_PIN, DHT22);
8
9BLEServer *sunucu = nullptr;
10BLECharacteristic *sicaklikKar = nullptr;
11BLECharacteristic *nemKar = nullptr;
12volatile bool bagli = false;
13bool oncekiBagli = false;
14unsigned long sonOlcum = 0;
15
16class SunucuOlaylari : public BLEServerCallbacks {
17  void onConnect(BLEServer *s) { bagli = true; }
18  void onDisconnect(BLEServer *s) { bagli = false; }
19};
20
21BLECharacteristic *karakteristikEkle(BLEService *servis, uint16_t uuid) {
22  BLECharacteristic *k = servis->createCharacteristic(
23    BLEUUID(uuid), BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
24  k->addDescriptor(new BLE2902());        // bildirim için gerekli / needed for notify
25  return k;
26}
27
28void setup() {
29  Serial.begin(115200);
30  dht.begin();
31
32  BLEDevice::init("BlueGrays-Sensor");
33  sunucu = BLEDevice::createServer();
34  sunucu->setCallbacks(new SunucuOlaylari());
35
36  BLEService *servis = sunucu->createService(BLEUUID((uint16_t)0x181A));  // Environmental Sensing
37  sicaklikKar = karakteristikEkle(servis, 0x2A6E);  // sıcaklık, 0,01 °C / temperature, 0.01 °C
38  nemKar = karakteristikEkle(servis, 0x2A6F);       // nem, %0,01 / humidity, 0.01 %
39  servis->start();
40
41  BLEAdvertising *yayin = BLEDevice::getAdvertising();
42  yayin->addServiceUUID(BLEUUID((uint16_t)0x181A));
43  yayin->setScanResponse(true);
44  BLEDevice::startAdvertising();
45}
46
47void loop() {
48  if (millis() - sonOlcum >= 2000) {      // DHT22 en fazla 2 sn'de bir / DHT22 at most every 2 s
49    sonOlcum = millis();
50    float t = dht.readTemperature();
51    float h = dht.readHumidity();
52    if (!isnan(t) && !isnan(h)) {
53      int16_t t100 = (int16_t)lroundf(t * 100);    // 23,45 °C -> 2345
54      uint16_t h100 = (uint16_t)lroundf(h * 100);  // %41,2 -> 4120
55      sicaklikKar->setValue((uint8_t *)&t100, 2);  // 2 bayt, düşük bayt önce / 2 bytes, little-endian
56      nemKar->setValue((uint8_t *)&h100, 2);
57      if (bagli) {
58        sicaklikKar->notify();
59        nemKar->notify();
60      }
61      Serial.printf("%.2f C  %.2f %%\n", t, h);
62    }
63  }
64
65  if (!bagli && oncekiBagli) {            // bağlantı koptu / disconnected
66    delay(500);
67    sunucu->startAdvertising();           // yeniden yayın / advertise again
68    oncekiBagli = false;
69  }
70  if (bagli && !oncekiBagli) oncekiBagli = true;
71}

The code takes a reading every two seconds, because the DHT22 can't be read more often. The values are first written to the characteristic with setValue(), so the phone also gets the latest measurement when it reads (READ) without waiting for a notification. If the phone is connected, notify() sends the new value straight away. If nRF Connect can't decode the value, you'll see the raw bytes: for 23.45 °C the screen shows (0x) 29-09. Reverse the bytes and you get 0x0929, which is 2345 in decimal; divide by 100 and you have the temperature. You'll need to do the same calculation when you write your own app. Sending the value as text may look easier, but the standard format uses fewer bytes and works with existing apps.

It's also possible to send data without a connection: the ESP32 can put the reading straight into the manufacturer data field of its advertising packet, and every phone or gateway nearby can read it without connecting. This method is called a beacon, and the open BTHome format recognised by Home Assistant works the same way. If the ESP32 goes into deep sleep after each reading, battery life stretches to months; see ESP32 Deep Sleep for how.

ESP32 Bluetooth with the iPhone (iOS)

The question iPhone users ask most is why the ESP32 doesn't show up in the Bluetooth settings. Apple opens the Classic Bluetooth serial port profile (SPP) only to MFi-certified accessories, so you can't connect to an ESP32 running BluetoothSerial from an iPhone. The solution is BLE, and the NUS and sensor examples above work fine on the iPhone. There are a few things to keep in mind:

  • Searching in Settings: BLE devices aren't listed in iOS Bluetooth settings; make the connection inside an app such as nRF Connect, LightBlue or Bluefruit Connect.
  • Cache: iOS remembers the device's name and service list. If you still see the old list after changing the services in your code, switch the phone's Bluetooth off and on again.
  • No MAC address: iOS doesn't show apps the device's MAC address and gives each device an identifier of its own; if you use several ESP32s, give each one a different name.
  • Connection interval: don't delete the setMinPreferred(0x06) and setMaxPreferred(0x12) lines; Espressif's examples also recommend them against iPhone connection issues.
  • Your own app: on iOS you use Core Bluetooth with Swift; no pairing is needed, and the app scans directly for the service UUID.

BLE with the Raspberry Pi Pico W and MicroPython

Raspberry Pi Pico board
In the Raspberry Pi Pico family, Bluetooth is only found on the boards with a W in their name (Pico W, Pico 2 W); the standard Pico has no wireless connection.

In the Raspberry Pi Pico family, Bluetooth is only available on the Pico W and Pico 2 W. The on-board CYW43439 chip supports both Classic Bluetooth and BLE in hardware, but MicroPython only uses BLE (from version 1.20 onwards). The code below is the MicroPython equivalent of the ESP32 NUS example; because it uses the same UUIDs, you test it the same way with nRF Connect and Serial Bluetooth Terminal. You must have flashed the MicroPython build made for the Pico W. The code also reads the board's internal temperature sensor and sends it to the connected phone every 2 seconds.

MicroPython
1import bluetooth
2import struct
3import time
4from machine import Pin, ADC
5from micropython import const
6
7_IRQ_CENTRAL_CONNECT = const(1)
8_IRQ_CENTRAL_DISCONNECT = const(2)
9_IRQ_GATTS_WRITE = const(3)
10_FLAG_READ = const(0x0002)
11_FLAG_WRITE_NO_RESPONSE = const(0x0004)
12_FLAG_WRITE = const(0x0008)
13_FLAG_NOTIFY = const(0x0010)
14
15# Nordic UART Service: ESP32 örneğiyle aynı UUID'ler / same UUIDs as the ESP32 example
16NUS_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
17TX = (bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), _FLAG_READ | _FLAG_NOTIFY)
18RX = (bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), _FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE)
19
20led = Pin("LED", Pin.OUT)       # Pico W dahili LED / Pico W onboard LED
21sensor = ADC(4)                 # dahili sıcaklık sensörü / internal temperature sensor
22
23ble = bluetooth.BLE()
24ble.active(True)
25((tx_h, rx_h),) = ble.gatts_register_services(((NUS_UUID, (TX, RX)),))
26baglanti = None
27
28
29def alan(tur, veri):            # uzunluk + tür + veri / length + type + data
30    return struct.pack("BB", len(veri) + 1, tur) + veri
31
32
33YAYIN = alan(0x01, b"\x06") + alan(0x09, b"BlueGrays-Pico")  # bayraklar + ad / flags + name
34YANIT = alan(0x07, bytes(NUS_UUID))                          # 128 bit UUID / 128-bit UUID
35
36
37def yayinla():
38    ble.gap_advertise(100000, adv_data=YAYIN, resp_data=YANIT)  # 100 ms aralık / interval
39
40
41def olay(event, data):
42    global baglanti
43    if event == _IRQ_CENTRAL_CONNECT:
44        baglanti = data[0]
45    elif event == _IRQ_CENTRAL_DISCONNECT:
46        baglanti = None
47        yayinla()                                 # yeniden görünür ol / advertise again
48    elif event == _IRQ_GATTS_WRITE and data[1] == rx_h:
49        komut = ble.gatts_read(rx_h).decode().strip()
50        if komut == "1":
51            led.on()
52        elif komut == "0":
53            led.off()
54
55
56ble.irq(olay)
57yayinla()
58
59while True:
60    if baglanti is not None:
61        volt = sensor.read_u16() * 3.3 / 65535
62        sicaklik = 27 - (volt - 0.706) / 0.001721   # veri sayfası formülü / datasheet formula
63        try:
64            ble.gatts_notify(baglanti, tx_h, "T:{:.1f}\n".format(sicaklik).encode())
65        except OSError:
66            pass                                    # bağlantı yeni koptu / just disconnected
67    time.sleep(2)

Because MicroPython's bluetooth module is low-level, we build the advertising packet ourselves. Each field consists of a length byte, a type byte and the data: 0x01 is the flags, 0x09 the device's complete name and 0x07 the 128-bit service UUID. Since an advertising packet holds at most 31 bytes, we put the 18-byte UUID field in the scan response (resp_data). gatts_register_services() returns the characteristics' handles; we use these handles to read the value the phone wrote and to send notifications. If you'd like shorter code, you can install the asyncio-based aioble library with mip. To run the code on the ESP32 under MicroPython, just change the LED line to Pin(2, Pin.OUT) and read, for example, a DHT11 instead of the internal temperature sensor.

Wi-Fi and Bluetooth together: memory, range and power

On the ESP32, Wi-Fi and Bluetooth share the same 2.4 GHz radio. The chip runs both by time-sharing, so you can use them at the same time, but each gets a little slower, and Classic Bluetooth combined with heavy Wi-Fi traffic is the toughest scenario. The real problem is usually memory, though: the Bluetooth stack is large, and together with Wi-Fi the program can exceed the default 1.2 MB (1,310,720 bytes) app partition; even the BLE example in this guide fills about 80% of that space on its own. If the build ends with "Sketch too big" or "text section exceeds available space in board" and you don't use over-the-air updates (OTA), choose Huge APP (3MB No OTA/1MB SPIFFS) under Tools > Partition Scheme. The NimBLE-Arduino library, which uses less flash and RAM, is a good alternative too.

Range depends on the board's antenna design and the environment. With an ESP32 that has an on-board PCB antenna, expect 10–20 metres indoors depending on the walls, more in open space; modules with an external antenna connector (for example the ESP32-WROOM-32U) reach even further. Covering the antenna with a metal box, a battery or cables cuts the range dramatically. Because the current briefly rises when the radio switches on, a weak USB cable can make the board restart with the message "Brownout detector was triggered"; you'll find the fixes in ESP32 Brownout and Guru Meditation.

ESP32 Bluetooth project ideas

  • Bluetooth robot car: pass the F, B, L and R commands received via BluetoothSerial to an L298N motor driver; for the mechanics, see Building an Arduino Robot Car.
  • Smart socket: replace the LED in the NUS example with a relay module and switch a lamp on from your iPhone.
  • Wireless thermometer: add an OLED display to the DHT22 example so the value appears both on the screen and on your phone.
  • Colourful mood light: change the colour of a WS2812B NeoPixel strip with R, G and B values sent from your phone.
  • Your own Android app: build a remote-control app with buttons and sliders in App Inventor in a few hours.
  • Communication between two ESP32s: to make two boards talk without a phone, try the simpler and faster ESP-NOW instead of Bluetooth.

Common ESP32 Bluetooth problems and fixes

Symptom / errorCauseFix
"Serial Bluetooth not available or not enabled"The selected board is an ESP32-S3, C3, C6 or H2, which has no Classic BluetoothUse a classic ESP32 or switch to the BLE (NUS) example
"conversion from 'String' to non-scalar type 'std::string' requested"Code written for core 2.x (std::string) compiled on a 3.x coreAssign getValue().c_str() to a String variable
"Sketch too big"Bluetooth plus Wi-Fi exceeded the app partitionPartition Scheme: choose Huge APP or use NimBLE-Arduino
The iPhone can't find the ESP32Classic Bluetooth (SPP) isn't supported on iOSUse BLE and connect from inside an app
The device disappears after the first connectionAdvertising wasn't restarted after the disconnectCall startAdvertising() after a disconnect
No notifications arriveNotifications weren't enabled on the phone, or BLE2902 is missingTap the triple arrow in nRF Connect; add BLE2902 to the characteristic
"read failed, socket might closed or timeout"Stale pairing data, or another phone is already connectedForget the device on the phone and pair again; SPP accepts only one connection at a time
The end of a long message is cut offDefault MTU is 23 bytes, i.e. 20 bytes of data per notificationSplit the message into 20-byte chunks or allow a larger MTU with BLEDevice::setMTU(185)
Service changes don't show on the phoneThe phone keeps the old GATT table in its cacheSwitch the phone's Bluetooth off and on; on Android, forget the device
When troubleshooting connection problems, choose Debug or Verbose under Tools > Core Debug Level. The Serial Monitor then shows the Bluetooth stack's connection, disconnection and error logs, which quickly reveal whether the problem is on the phone or on the ESP32. Repeating the same test on the phone with nRF Connect also tells you whether your own app is the cause.

With ESP32 Bluetooth, turning your phone into a remote and the ESP32 into a wireless sensor or smart device takes just a few lines of code. BluetoothSerial is the right choice for a quick start with Android, and BLE for iPhone support and low power. If you'd like to keep going with wireless ESP32 projects, the BlueGrays app has projects such as LEDs, RGB LEDs, temperature measurement with the DHT22 and a Bluetooth-controlled car, each with a step-by-step circuit diagram and ready-to-use code.

Frequently asked questions

Does the ESP32 have Bluetooth?

Yes. The original ESP32 chip supports both Classic Bluetooth (BR/EDR) and BLE with Bluetooth 4.2 and needs no extra module. The ESP32-S3, C3, C6 and H2 support BLE only, and the ESP32-S2 has no Bluetooth at all.

What is the range of ESP32 Bluetooth?

With an ESP32 that has an on-board PCB antenna you usually get 10–20 metres indoors; walls, metal objects and the antenna's orientation affect this distance. The range grows in open space, and modules with an external antenna (for example the WROOM-32U) reach even further.

Can the ESP32 connect to an iPhone over Bluetooth?

Yes over BLE, but not over Classic Bluetooth (BluetoothSerial), because iOS opens the serial port profile (SPP) only to MFi-certified accessories. On the iPhone you can connect to the ESP32's BLE service with an app such as nRF Connect, LightBlue or Bluefruit Connect.

Does BluetoothSerial work on the ESP32-S3 and ESP32-C3?

No. These chips have no Classic Bluetooth hardware, so BluetoothSerial won't compile. To get the same serial-port logic over BLE you can use the Nordic UART Service (NUS); apps like Serial Bluetooth Terminal recognise it.

Can the ESP32 use Wi-Fi and Bluetooth at the same time?

Yes, both work together by time-sharing the same 2.4 GHz radio. Speed drops a little and the program gets bigger, though; if you get a "Sketch too big" error, choose Huge APP under Partition Scheme or use the lighter NimBLE-Arduino library.

Do I need an HC-05 with the ESP32?

Usually not. The BluetoothSerial library on the classic ESP32 does the HC-05's job internally and works with the same terminal apps. You only need an HC-05 on boards without Bluetooth (Arduino Uno, ESP8266, the standard Pico, ESP32-S2) or if a project needs Classic Bluetooth and you're using a BLE-only ESP32.

See all projects
ESP32 Built-in LED Blink
#106Beginner

ESP32 Built-in LED Blink

Blinking the built-in blue LED on the ESP32 DevKit V1. No external components required! The first project you should try when you buy an ESP32. GPIO 2 is connected to the built-in LED.

4.8(29)
53
LED Blink with ESP32
#103Beginner

LED Blink with ESP32

Teaches basic GPIO output usage with ESP32. We achieve a blinking effect by turning an LED on and off from a digital pin. ESP32 version of the classic blink project on Arduino UNO; the basic blink commands in the Arduino environment are the same, but the ESP32's pin layout and 3.3 V GPIO levels differ from those of the Arduino UNO.

4.7(37)
99
ESP32 LED Control with Pushbutton (Toggle)
#105Beginner

ESP32 LED Control with Pushbutton (Toggle)

Each time the button is pressed the state of the LED changes: if it is on it turns off, if it is off it turns on. Classic toggle pattern. With debounce we clean up the mechanical noise of the pushbutton to ensure a single toggle with a single press.

4.6(39)
57
Control an LED from Your Phone via Bluetooth
#34Beginner
Premium

Control an LED from Your Phone via Bluetooth

A small rehearsal for switching the lights in your room on and off from your phone. The Bluetooth module passes the letters sent from your phone to the Arduino; one letter turns on the red LED, anothe

4.8(68)
89
Bluetooth Controlled Car with L298N
#80Advanced
Premium

Bluetooth Controlled Car with L298N

Turn your phone into a remote and drive your own robot car. The Arduino receives commands from a remote-control app on your phone through a Bluetooth module and spins the wheels with a motor driver; t

5.0(38)
65
ESP32 RGB LED Control
#111Beginner

ESP32 RGB LED Control

Unlimited color generation with a single RGB LED instead of 3 separate LEDs. As in Arduino, we make a rainbow transition by setting the R/G/B values with analogWrite(). Color theory (such as red+green=yellow) is seen practically.

4.6(29)
51
ESP32 DHT22 Temperature and Humidity (with LCD Display)
#117Intermediate

ESP32 DHT22 Temperature and Humidity (with LCD Display)

This project measures ambient temperature and humidity using a DHT22 sensor and displays the data in real time on a 16x2 I2C LCD. Data is read from the sensor using the one-wire protocol via the Adafruit DHT library. This project serves as the foundation for IoT weather stations, smart home automation, or environmental monitoring systems in greenhouses. Communication with the DHT22 is established via the ESP32's GPIO 4 pin.

4.8(38)
56
ESP32 WS2812B NeoPixel Strip Control
#126Intermediate
Premium

ESP32 WS2812B NeoPixel Strip Control

WS2812B (NeoPixel) addressable LEDs are smart RGB LEDs that can be controlled in a daisy-chain configuration via a single data pin. In this project, we will control an 8-LED WS2812B strip using the Adafruit_NeoPixel library to create a smooth rainbow animation. While the ESP32’s 3.3V logic level works seamlessly with most WS2812B components, we’ll demonstrate a solution using a 470Ω series resistor and a 1000µF capacitor in case of instability. The animation is achieved by shifting the hue value

4.9(19)
30