Welcome to ESP32 Core Board repository
13. Bluetooth Slave Mode
Bluetooth Slave Mode
Bluetooth Slave Mode1. Bluetooth Slave Overview2. Core Concepts2.1 GAP — Generic Access Profile2.2 GATT — Generic Attribute Profile2.3 Notify vs Write2.4 NimBLE-Arduino Library Architecture2.5 Notify vs Indicate Implementation Differences in Arduino3. Hardware Dependencies3.1 ESP32-S3 BLE Hardware Specifications3.2 Required Libraries4. Project Architecture and Flow5. Source Code Analysis5.1 Header Files and Global Variables5.2 Receiving Master Data — WriteCallbacks5.3 Connect/Disconnect Event Callbacks — ServerCallbacks5.4 FreeRTOS Push Task — sendTask5.5 setup() — Initialize BLE Slave5.6 loop() — Empty Loop6. Operation Procedures6.1 Build and Flash6.2 Method 1: Phone Bluetooth Debug6.3 Method 2: Dual-Board Communication Test (Requires Two Boards)6.4 Expected Output — Phone Debug6.5 Expected Output — Dual-Board Communication6.6 Log Field Description6.7 Edge Cases7. FAQ
1. Bluetooth Slave Overview
BLE (Bluetooth Low Energy) is a short-range wireless communication protocol designed for low power consumption and low bandwidth scenarios. Compared to Classic Bluetooth, BLE connections are faster (millisecond-level) and consume less power (coin batteries can run for months).
Importance of BLE Slave Tutorial: BLE Slave (Peripheral) is the most common role for IoT devices — sensor nodes, wearable devices, smart home peripherals, etc. Most scenarios run as slaves. Mastering slave programming means you can build smart devices that can be discovered and connected by phones, tablets, or gateways. This is a core skill for embedded BLE development.
This experiment uses the NimBLE protocol stack (saves ~50% RAM compared to Arduino's default Bluedroid), creating a FreeRTOS task that pushes data to the master every 2 seconds, while also receiving data written by the master.
After completing this tutorial, you will have: An ESP32-S3 development board broadcasting with device name ESP-IDF. Phones or another ESP32-S3 can scan, connect, receive pushed data every 2 seconds, and send arbitrary text commands to the slave — a complete bidirectional BLE communication slave.
Typical Application Scenarios:
| Application Type | Example |
|---|---|
| Sensor Slave | Thermometer broadcasts and pushes readings |
| Wearable Device | Fitness band pushes heart rate data to phone |
| Beacon | iBeacon location broadcast |
BLE Role System:
| Role | GAP Name | Responsibility |
|---|---|---|
| Slave | Peripheral | Broadcasts presence, accepts connections, provides GATT services for master access |
| Master | Central | Actively scans nearby devices, initiates connections, discovers services and characteristics, reads/writes data |
2. Core Concepts
2.1 GAP — Generic Access Profile
GAP (Generic Access Profile) defines how BLE devices are scanned and connected.
GAP operations for this slave:
| Operation | Arduino API | Description |
|---|---|---|
| Advertising | NimBLEDevice::getAdvertising()->start() | Periodically sends advertising packets containing device name and service UUID |
| Connection Response | ServerCallbacks::onConnect() | Accepts master connection request, updates connection status |
| Re-advertising on Disconnect | ServerCallbacks::onDisconnect() → NimBLEDevice::startAdvertising() | Automatically restarts advertising after disconnection |
Advertising Packet Structure
Advertising Packet├─ Flags: General discoverable + BR/EDR not supported├─ Name: "ESP-IDF" (Complete)│ Complete device name; central matches on this└─ Service UUID: 0xFFF0 (Complete List of 16-bit Service UUIDs)Custom service UUID; central can determine available services during scan
2.2 GATT — Generic Attribute Profile
GATT (Generic Attribute Profile) defines the data organization structure of BLE devices.
GATT Custom Service Architecture for This Project
xxxxxxxxxxService: 0xFFF0 (Custom Primary Service)├─ Characteristic: 0xFFF1│ Properties: READ | NOTIFY (Readable + Slave notifies master actively)│ Direction: Slave → Master│ Purpose: Slave pushes data to central via Notify every 2 seconds│└─ Characteristic: 0xFFF2Properties: WRITE | WRITE_NR (Supports both acknowledged and unacknowledged writes)Direction: Master → SlavePurpose: Receives data from central, processed in onWrite() callback
2.3 Notify vs Write
| Operation | Initiator | Target | Acknowledgment | Typical Scenario |
|---|---|---|---|---|
| Notify | Slave → Master | 0xFFF1 | No ACK | Sensor data reporting, heartbeat counting |
| Write | Master → Slave | 0xFFF2 | WRITE: Requires link layer ACK; WRITE_NR: No ACK | Control command delivery, parameter configuration |
| Indicate | Slave → Master | (Not used) | Requires master ACK | Critical data reporting, guaranteed delivery |
In this project, the slave sends a fixed message via Notify on 0xFFF1 every 2 seconds; the master can write arbitrary data to 0xFFF2 at any time, and the slave receives and prints it immediately in
onWrite(). Both operations are asynchronous and independent, non-blocking.
2.4 NimBLE-Arduino Library Architecture
ESP32-S3 uses Apache NimBLE as the BLE Host protocol stack.
Core Class Hierarchy
xxxxxxxxxxNimBLEDevice ← Static class, stack initialization + global control├─ NimBLEServer ← GATT server, manages service list│ ├─ NimBLEService ← GATT service, contains multiple characteristics│ │ └─ NimBLECharacteristic ← GATT characteristic, data carrier + callback binding│ └─ NimBLEServerCallbacks ← User overrides onConnect/onDisconnect├─ NimBLEAdvertising ← Advertising parameter config and control└─ NimBLECharacteristicCallbacks ← User overrides onWrite/onRead
Compared to ESP-IDF native NimBLE's callback-style API (
gap_event_cb,gatt_svr_access_cb), the Arduino wrapper maps each event type to independent C++ virtual methods implemented through class inheritance. This makes code logic more focused and eliminates the need forswitch/casedispatching in a single callback function.
2.5 Notify vs Indicate Implementation Differences in Arduino
In Arduino NimBLEDevice library, the difference between notify and indicate is determined by characteristic properties and send methods together.
| Operation | Property Declaration | Send Method | Master Acknowledgment |
|---|---|---|---|
| Notify | NIMBLE_PROPERTY::NOTIFY | pChar->notify() | No |
| Indicate | NIMBLE_PROPERTY::INDICATE | pChar->indicate() | Yes |
x// Notify — No ACK, suitable for high-frequency sensor datapCharNotify = pService->createCharacteristic( CHAR_NOTIFY_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY // Declare NOTIFY property);pCharNotify->setValue("Hello");pCharNotify->notify(); // Send ATT Handle Value Notification
// Indicate — Requires ACK, suitable for critical control commands// pChar->indicate(); // Send ATT Handle Value Indication3. Hardware Dependencies
3.1 ESP32-S3 BLE Hardware Specifications
| Specification | Parameter |
|---|---|
| Wireless Standard | BLE 5.0 |
| Max TX Power | +20 dBm |
| Receiver Sensitivity | -97 dBm (1Mbps mode) |
| Max MTU | 251 bytes (Auto-negotiated by NimBLE-Arduino) |
| Antenna | On-board PCB antenna |
| Advertising Interval | Protocol stack default (~100ms) |
| Protocol Stack | NimBLE (Apache open source) via NimBLE-Arduino |
| Arduino TX Power Setting | NimBLEDevice::setPower(ESP_PWR_LVL_P3) — Set to +3dBm in this tutorial |
BLE and Wi-Fi share the 2.4GHz RF front-end and can coexist through time-division multiplexing.
3.2 Required Libraries
| Library | Purpose | Include Method |
|---|---|---|
NimBLEDevice.h | NimBLE BLE protocol stack Arduino wrapper | #include <NimBLEDevice.h> |
NimBLEDevicelibrary needs to be installed via Arduino IDE library manager: Tools → Manage Libraries → Search "NimBLE" → Install "NimBLE-Arduino" (Author: h2zero). Do not confuse with ESP32's built-in "BLE" library (based on Bluedroid).
4. Project Architecture and Flow
File Structure ([BLE_Slave.ino](../../../Arduino/3.Network%20Communication/BLE_Slave/BLE_Slave.ino), ~90 lines total):
xxxxxxxxxxBLE_Slave.ino├─ #include + Macros (DEVICE_NAME, SERVICE_UUID, CHAR_*_UUID)├─ Global variables (pServer, pCharNotify, g_connected)├─ class WriteCallbacks ← Receives master writes (5.2)├─ class ServerCallbacks ← Connect/disconnect events (5.3)├─ void sendTask() ← FreeRTOS periodic push (5.4)├─ void setup() ← BLE slave initialization six-step method (5.5)└─ void loop() ← Empty loop (5.6)
Program Flow:
xxxxxxxxxxsetup()├─ NimBLEDevice::init("ESP-IDF") // Initialize stack├─ createServer() + setCallbacks(serverCb) // Create GATT server├─ createService(0xFFF0)│ ├─ createCharacteristic(0xFFF1, READ|NOTIFY) // Notify characteristic│ └─ createCharacteristic(0xFFF2, WRITE|WRITE_NR) // Write characteristic + bind callback├─ service.start()└─ startAdvertising() // Start advertisingsendTask (FreeRTOS):└─ Every 2s: setValue + notify // Push dataWriteCallbacks::onWrite():└─ Print data received from master
5. Source Code Analysis
Source code: Source code → Arduino → 3.Network Communication → BLE_Slave → BLE_Slave.ino
5.1 Header Files and Global Variables
The top of the file contains #include, macro definitions, and global variables. NimBLEDevice.h is the only header file, which internally aggregates all C++ wrapper classes for the NimBLE protocol stack. Macros use NimBLEUUID((uint16_t)0x...) to wrap 16-bit UUIDs into BLE standard 128-bit UUIDs. Three global variables — server pointer, Notify characteristic pointer, and connection status flag — persist throughout the program lifecycle.
xxxxxxxxxx// NimBLE BLE stack
// Advertising device name// Primary service UUID// Notify char — slave→central// Write char — central→slave
// BLE GATT server pointerNimBLEServer* pServer = nullptr;// Notify char pointerNimBLECharacteristic* pCharNotify = nullptr;// BLE connection status flagbool g_connected = false;5.2 Receiving Master Data — WriteCallbacks
WriteCallbacks is responsible for receiving data sent by the master through the Write characteristic (0xFFF2). It inherits from NimBLECharacteristicCallbacks and overrides onWrite(), which NimBLE protocol stack automatically calls each time a master write completes. getValue() returns complete data (std::string), no manual mbuf operation or packet aggregation timer needed.
xxxxxxxxxx// Callback class for receiving central write dataclass WriteCallbacks : public NimBLECharacteristicCallbacks { void onWrite(NimBLECharacteristic* pChar, NimBLEConnInfo& connInfo) override { // Get data central just wrote std::string recvData = pChar->getValue(); Serial.println("=================================="); Serial.printf("Host Message: %s\n", recvData.c_str()); Serial.println("=================================="); }} writeCb; // Global instance, bound when creating characteristic5.3 Connect/Disconnect Event Callbacks — ServerCallbacks
Connect and disconnect are two critical events in the BLE slave lifecycle. Inherit from NimBLEServerCallbacks and override onConnect and onDisconnect: on connect, set the status flag for the send task; on disconnect, clear the flag first then call startAdvertising() to resume advertising — otherwise the device will be "silent" and the master can never scan it again.
xxxxxxxxxx// BLE connect/disconnect callbacks classclass ServerCallbacks : public NimBLEServerCallbacks { void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override { // Set connection flag g_connected = true; Serial.println("Bluetooth connected"); }
void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override { // Clear connection flag g_connected = false; Serial.println("Bluetooth disconnected"); // Key: restart advertisi