ESP32/Bluetooth/60. BLE server and client
Your chip
Your language
Bluetooth · 60 of 81

BLE server and client

BLE is not a wireless serial port, and expecting one is the whole learning curve. You publish a tree of named values, a phone reads or subscribes to them, and the choice between read and notify decides your battery life.

/esp32/ble-server-and-client · arduino · S3

What a phone actually sees

What a phone actually sees
1.80 mA average
Average current
1.80 mA
Readings can be lost
yes
Payload per packet
20 B by default
The board sends when the value changes and nobody acknowledges. Cheapest, and a packet lost in a noisy room is lost for good. Use it for a live value where the next one is along in a second anyway — a temperature, a heart rate. Not for a command or a count. And the 20-byte default is real — ask for a larger MTU if you are sending more, or split it yourself.

Server or client

Server — the board holds the values and a phone connects to it. This is almost always what you want: a sensor, a lock, a light.

Client — the board connects to something else, such as a commercial heart rate strap or a Bluetooth thermometer. Same UUIDs, opposite direction, and the code is a scan followed by a connect.

NimBLE, not Bluedroid

The BLE library that ships with the Arduino core is Bluedroid and it is enormous — well over a megabyte of flash. NimBLE-Arduino does the same job in around half of that with a third of the RAM, and the API is close enough to port in an afternoon. On a 4 MB board with OTA enabled, that difference decides whether the sketch fits.

On your S3
ChipXtensa LX7 · 2 × 240 MHz
Board settingESP32S3 Dev Module
Default I2CSDA 8 · SCL 9
Watch out forThe port vanishes after upload

The code

One service, one characteristic, notify enabled. The UUIDs are the names - a phone finds values by UUID, never by position, so they have to be stable.

ble_sensor.ino
#include <NimBLEDevice.h>

NimBLECharacteristic *temp;

void setup() {
  Serial.begin(115200);
  NimBLEDevice::init("Greenhouse");

  NimBLEServer *server = NimBLEDevice::createServer();
  NimBLEService *svc = server->createService("181A");   // Environmental

  temp = svc->createCharacteristic(
           "2A6E",                                      // Temperature
           NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
  svc->start();

  NimBLEAdvertising *adv = NimBLEDevice::getAdvertising();
  adv->addServiceUUID("181A");
  adv->start();
}

void loop() {
  int16_t hundredths = 2140;          // 21.40 C, as the spec defines it
  temp->setValue((uint8_t *)&hundredths, 2);
  temp->notify();
  delay(1000);
}

Use the standard 16-bit UUIDs where one exists, such as 0x2A6E for temperature. Generic apps and health platforms already know how to read those.

When it does not work

The phone sees the device and no data

You advertised without including the service UUID, so generic apps do not know to look. Add it to the advertising payload, not only to the GATT table.

Notifications stop after a few minutes

The phone dropped the connection to save power, or your connection interval is too long. Reconnect on disconnect - a BLE server should always restart advertising when a client goes.

Only 20 bytes of my payload arrives

That is the default MTU. Request a larger one after connecting, or split the value yourself. There is no automatic fragmentation for a characteristic.

The sketch does not fit

The Bluedroid stack is large. NimBLE does the same job in roughly half the flash and a third of the RAM, and it is a drop-in for most projects.

Where this goes next

A service exists. Putting a number in it a phone can read — and then the one property that stops the phone having to ask.

A value a phone can read

Edit this page — content/esp32/ble-server-and-client.mdx

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse ESP32 on the forum