yahboom
HomeLearn
Solution
Blog
Online Store
About Us
EN/中文
ESP32 Core Board📖
1. ESP32 Introduction
  • 1. ESP32S3 Overview
  • 2. Environment Setup Introduction
  • 3. Dev Board Introduction
2. ESP-IDF Development Tutorial
1. Before Development
  • 1. Environment Setup (Windows)
  • 2. Environment Setup (Linux)
  • 3. Build and Flash
2. Basic Peripherals
  • 1. LED Control
  • 2. Button Detection
  • 3. Peripheral Interrupts
  • 4. ESP_TIMER
  • 5. GPTIMER
  • 6. Watchdog Timer
  • 7. DMA
  • 8. ADC
  • 9. PWM Breathing LED
  • 10. RGB LED Effects
3. Serial Communication and Storage System
  • 1. Serial UART Communication Experiment
  • 2. I2C_OLED Display Experiment
  • 3. SPI Bus Communication Experiment
  • 4. PSRAM External Memory Experiment
  • 5. Flash Read and Write Experiment
  • 6. USB Emulated Mass Storage Experiment
4. FreeRTOS operating system
  • 1.FreeRTOS Core Concepts Theory
  • 2.Task Creation and Management
  • 3.Queue
  • 4.Binary Semaphore
  • 5.Mutex
  • 6.Event Groups
  • 7.Software Timer
  • 8.Task Scheduler
  • 9.Low Power Mode
  • 10.Deadlock
  • 11.Priority Inversion
5. Network Protocols and Wireless Communication
  • 1. WIFI Network Basics
  • 2. WIFI STA Mode
  • 3. WIFI AP Mode
  • 4. UDP Client Experiment
  • 5. UDP Server Experiment
  • 6. TCP Client Experiment
  • 7. TCP Server Experiment
  • 8. Socket Programming Basics
  • 9. HTTP Client Experiment
  • 10. HTTP Server Experiment
  • 11. MQTT IoT Communication Protocol
  • 12. WIFI OTA Upgrade
  • 13. Bluetooth Fundamentals
  • 14. Bluetooth Slave Mode
  • 15. Bluetooth Master Mode
6. Audio Applications
  • 1.I2S Microphone Recording and U-Disk Playback
  • 2.I2S Audio Playback Experiment
  • 3.Repeater Experiment
7. Xiaozhi AI Voice
  • 1. Burn Xiaozhi Firmware
  • 2. Network and Configuration
  • 3. AI Agent Settings
  • 4. Program Analysis
  • 5. Free Chat
3. Arduino Development Course
1. Before Development
  • 1. Environment Setup
  • 2. Build and Flash
2. Basic Peripherals
  • 1. LED Control
  • 2. Button Detection
  • 3. Peripheral Interrupts
  • 4. ESP_TIMER
  • 5. GPTIMER
  • 6. Watchdog Timer
  • 7. ADC
  • 8. PWM Breathing LED
  • 9. RGB LED Effects
3. Serial Communication and Storage System
  • 1. Serial UART Communication
  • 2. I2C OLED Display
  • 3. SPI Bus Communication
  • 4. Flash Read and Write
  • 5. USB Emulated Mass Storage
4. Network Protocols and Wireless Communication
  • 1. WIFI Network Basics
  • 2. WIFI STA Mode
  • 3. WIFI AP Mode
  • 4. UDP Server Experiment
  • 5. UDP Client Experiment
  • 6. TCP Client Experiment
  • 7. TCP Server Experiment
  • 8. HTTP Client Experiment
  • 9. HTTP Server Experiment
  • 10. MQTT IoT Communication Protocol
  • 11. WIFI OTA Upgrade
  • 12. Bluetooth Fundamentals
  • 13. Bluetooth Slave Mode
  • 14. Bluetooth Master Mode
5. Audio Applications
  • 1.I2S Microphone Recording and U-Disk Playback
  • 2.I2S Audio Playback Experiment
  • 3.Repeater Experiment
4. MicroPython Development Course
1. Before Development
  • 1. Environment Setup
  • 2. Build and Flash
  • 3. Thonny IDE
2. Basic Peripherals
  • 1. LED Control
  • 2. Button Detection
  • 3. Peripheral Interrupts
  • 4. TIMER_IT
  • 5. Watchdog Timer
  • 6. ADC
  • 7. PWM Breathing LED
  • 8. RGB LED Effects
3. Serial Communication and Storage System
  • 1. Serial UART Communication Experiment
  • 2. I2C_OLED Display Experiment
  • 3. SPI Bus Communication Experiment
  • 4. Flash Read and Write Experiment
  • 5. Print Storage
4. Network Protocols and Wireless Communication
  • 1. WIFI Network Basics
  • 2. WIFI STA Mode
  • 3. WIFI AP Mode
  • 4. UDP Client Experiment
  • 5. UDP Server Experiment
  • 6. TCP Client Experiment
  • 7. TCP Server Experiment
  • 8. HTTP Client Experiment
  • 9. HTTP Server Experiment
  • 10. MQTT
  • 11. Bluetooth Fundamentals
  • 12. Bluetooth Slave Mode
  • 13. Bluetooth Master Mode
5. Audio Applications
  • 1. I2S Audio Playback Experiment
  • 2. Repeater Experiment
Download
  • Code
  • Hardware_Info
  • 3D_Model_File
  • Software_Tools
  • VMware
  • VM_File
Download ZIP

Welcome to ESP32 Core Board repository

14. Bluetooth Master Mode

Bluetooth Master Mode


Bluetooth Master Mode1. Bluetooth Master Overview2. Core Concepts2.1 GAP — Generic Access Profile2.2 Scan Modes2.3 GATT — Generic Attribute Profile2.4 Notify and Write2.5 CCCD — Client Characteristic Configuration Descriptor2.6 NimBLE-Arduino Library Architecture (Master Perspective)3. Hardware Dependencies3.1 ESP32-S3 BLE Hardware Specifications3.2 Required Libraries3.3 Companion Devices4. Project Architecture and Flow5. Source Code Details5.1 Header Files and Global Variables5.2 Receiving Slave Push — notifyCallback5.3 Connection/Disconnection Event Callbacks — MyClientCallbacks5.4 Scanning and Connecting — scanAndConnect5.5 FreeRTOS Timed Write — sendTask5.6 setup() — Initialize BLE Master5.7 loop() — Auto Reconnect Loop6. Operation Flow6.1 Build and Flash6.2 Method 1: Dual-Board Communication Test (Requires Two Boards)6.3 Method 2: Phone Simulated Slave Test6.4 Expected Output — Dual-Board Communication6.5 Log Field Description6.6 Edge Cases7. FAQ


1. Bluetooth Master 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 Master Tutorial: The BLE master (Central) plays the role of actively discovering and connecting to slaves, serving as the control end for data aggregation and command distribution. Phones, tablets, gateways, and other central devices all operate as masters—mastering master programming means being able to build central nodes that actively scan, auto-connect, and read/write data from remote slaves, which is the other core capability for building a complete BLE system.

This experiment uses the NimBLE protocol stack (saves ~50% RAM compared to Arduino's default Bluedroid) to scan for the target slave "ESP-IDF", auto-connect and discover GATT services, subscribe to Notify to receive slave push data, and also write data to the slave every 5 seconds via a FreeRTOS task.

After completing this tutorial, you will have: An ESP32-S3 development board that automatically scans surrounding BLE devices upon power-on, auto-connects to a slave named ESP-IDF, subscribes to notifications, sends data every 5 seconds, and prints all received slave push data on the serial port—a complete bidirectional BLE communication master.

Typical Application Scenarios:

Application TypeExample
Mobile AppScan and connect to bracelet, watch, and other sensor peripherals
Data CollectionMaster collects data from multiple slave sensors and uploads to cloud
Door Lock ControlPhone (master) connects to door lock slave, sends unlock command

BLE Role System:

RoleGAP NameResponsibility
SlavePeripheralAdvertises its presence, accepts connections, provides GATT services for master access
MasterCentralActively scans surrounding 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 discover and connect to each other.

GAP operations involved in this master:

OperationArduino APIDescription
ScanNimBLEDevice::getScan()->start()Actively scan surrounding advertisements, collect device list
ConnectpClient->connect(pDev)Initiate connection request to target slave
Disconnect RescanClientCallbacks::onDisconnect() → loop() auto rescanWhen disconnect is detected, loop() detects connected=false and automatically rescans and reconnects

2.2 Scan Modes

ModeAPIBehaviorUse Case
Active ScansetActiveScan(true)Send Scan Request after receiving advertisement to get Scan ResponseNeed complete device name or more advertisement data
Passive ScansetActiveScan(false)Silently collect advertisement packets only, send no requestsLow power listening, beacon collection

This project uses active scan (setActiveScan(true)) to ensure obtaining the complete device name from the slave's Scan Response.

2.3 GATT — Generic Attribute Profile

GATT (Generic Attribute Profile) defines the data organization structure of BLE devices.

This project's custom GATT service architecture (defined on slave side, discovered by master)

Service: 0xFFF0 (Custom Primary Service)
  ├─ Characteristic: 0xFFF1
  │     Properties: READ | NOTIFY (Readable + Slave actively notifies master)
  │     Direction: Slave → Master
  │     Central Action: subscribe(true, callback) — Write CCCD to enable notification
  │     Purpose: Slave pushes data to master via Notify every 2 seconds
  │                      Slave pushes data to central via Notify every 2 seconds
  │
  └─ Characteristic: 0xFFF2
        Properties: WRITE | WRITE_NR (Supports both write with response and write without response)
        Direction: Master → Slave
        Central Action: writeValue(data, false) — Write data to slave
        Purpose: Master sends data to slave every 5 seconds
                        Master sends data to slave every 5 seconds

2.4 Notify and Write

OperationInitiatorTargetAck RequiredArduino APITypical Scenario
NotifySlave → Master0xFFF1Nosubscribe(true, callback) receiveSensor data reporting, heartbeat count
WriteMaster → Slave0xFFF2WRITE: Yes; WRITE_NR: NowriteValue(data, false) sendControl command distribution, parameter configuration

In this project, the master writes "Hello from ESP32 Master" to 0xFFF2 every 5 seconds; the slave pushes data via 0xFFF1 Notify every 2 seconds, and the master receives and prints it immediately in notifyCallback. Both operations are asynchronous and independent, non-blocking each other.

2.5 CCCD — Client Characteristic Configuration Descriptor

CCCD (Client Characteristic Configuration Descriptor) UUID is fixed at 0x2902. Notify is disabled by default—the master must write 0x0001 to the slave's CCCD to enable notifications.

CCCD ValueMeaningEffect
0x0000Disable notifications and indicationsSlave does not push data
0x0001Enable NotifySlave starts pushing Notify to master
0x0002Enable IndicateSlave starts pushing Indicate to master (with acknowledgment)
xxxxxxxxxx
// subscribe(true, callback) automatically completes two steps internally:
// 1. Write 0x0001 to slave CCCD (0x2902)
// 2. Register notifyCallback as the notification receive callback
pNotify->subscribe(true, notifyCallback);

subscribe(true, callback) encapsulates the CCCD write process, so developers don't need to manually operate the descriptor. The second parameter is a callback function pointer for when Notify is received.

2.6 NimBLE-Arduino Library Architecture (Master Perspective)

ESP32-S3 uses Apache NimBLE as the BLE Host protocol stack. Master-side core class hierarchy:

xxxxxxxxxx
NimBLEDevice               ← Static class, stack initialization + global control / Static class; stack init + global control
  ├─ NimBLEScan            ← Scan controller, manages scan params + results / Scan controller, manages scan params + results
  │    └─ NimBLEScanResults / NimBLEAdvertisedDevice  ← Scan results / Scan results
  ├─ NimBLEClient          ← GATT client, manages connection + service discovery / GATT client, manages connection + service discovery
  │    ├─ NimBLEClientCallbacks      ← User overrides onConnect/onDisconnect
  │    └─ NimBLERemoteService        ← Remote service reference / Remote service reference
  │         └─ NimBLERemoteCharacteristic  ← Remote characteristic reference, read/write/subscribe / Remote char ref, read/write/subscribe
  └─ (NimBLEServer / NimBLEAdvertising  ← Not used by master / Not used by central)

Core differences between master and slave:

ComponentSlave (Peripheral)Master (Central)
Core ObjectNimBLEServer — Provides GATT servicesNimBLEClient — Discovers and accesses remote GATT services
Characteristic TypeNimBLECharacteristic — Local characteristicNimBLERemoteCharacteristic — Remote characteristic reference
Active Operationsnotify() push / wait for writessubscribe() subscribe / writeValue() write
AdvertisementNimBLEAdvertising send advertisementsNimBLEScan scan advertisements

3. Hardware Dependencies

3.1 ESP32-S3 BLE Hardware Specifications

SpecificationParameter
Wireless StandardBLE 5.0
Max TX Power+20 dBm
Receive Sensitivity-97 dBm (1Mbps mode)
Max MTU251 bytes (Auto-negotiated by NimBLE-Arduino)
AntennaOn-board PCB antenna
Scan IntervalSet to 10ms window in this tutorial
Protocol StackNimBLE (Apache open source) via NimBLE-Arduino
Arduino TX Power SettingNimBLEDevice::setPower(ESP_PWR_LVL_P9) — Set to +9dBm (max) in this tutorial

3.2 Required Libraries

LibraryPurposeInclude Method
NimBLEDevice.hNimBLE BLE protocol stack Arduino wrapper#include <NimBLEDevice.h>

The NimBLEDevice library 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).

3.3 Companion Devices

You need a companion device with BLE slave program flashed onto ESP32-S3, or use a phone Bluetooth debugging assistant app to simulate a slave.


4. Project Architecture and Flow

File Structure (BLE_Master.ino, complete source ~150 lines):

xxxxxxxxxx
BLE_Master.ino
├─ #include + Macro definitions (TARGET_DEVICE_NAME, SERVICE_UUID, CHAR_*_UUID)
├─ Global variables (pClient, pWriteChar, connected)
├─ void notifyCallback()        ← Receive slave Notify push (Section 5.2)
├─ class MyClientCallbacks      ← Connection/disconnection events (Section 5.3)
├─ void scanAndConnect()        ← Scan→match name→connect→discover service→subscribe (Section 5.4)
├─ void sendTask()              ← FreeRTOS timed write (Section 5.5)
├─ void setup()                 ← BLE master initialization (Section 5.6)
└─ void loop()                  ← Auto scan reconnect when disconnected (Section 5.7)

Program Flow:

​x
setup()
  ├─ NimBLEDevice::init("MASTER")          // Initialize stack
  └─ xTaskCreate(sendTask)                 // Create FreeRTOS timed send task

loop()
  └─ if (!connected) → scanAndConnect()    // Continuously scan and reconnect when disconnected

scanAndConnect():
  ├─ Scan 500ms, iterate results
  ├─ Match device name "ESP-IDF"
  │    └─ When name is empty: manually parse AD Structure to extract device name
  ├─ createClient() + connect()            // Create client and initiate connection
  ├─ getService(0xFFF0)                    // Get target service
  │    ├─ getCharacteristic(0xFFF1) → subscribe(notifyCallback)
  │    └─ getCharacteristic(0xFFF2) → Save pWriteChar reference

sendTask (FreeRTOS):
  └─ Every 5s: writeValue("Hello from ESP32 Master", false)  // WRITE_NR

notifyCallback():
  └─ Print slave push data

MyClientCallbacks:
  ├─ onConnect()    → connected = true, print log
  └─ onDisconnect() → connected = false, pWriteChar = nullptr, print log

5. Source Code Details

Source code: Source code → Arduino → 3.Network Communication → BLE_Master → BLE_Master.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 of the NimBLE protocol stack. The UUID values in macros must exactly match the slave side. Three global variables—client pointer, remote Write characteristic reference, and connection status flag—persist throughout the program's lifecycle. pWriteChar is set to nullptr when disconnected to prevent wild pointer access to invalid remote characteristic references.

xxxxxxxxxx
// NimBLE protocol stack / NimBLE BLE stack
#include <NimBLEDevice.h>

// Target slave advertising name / Target slave advertising name
#define TARGET_DEVICE_NAME   "ESP-IDF"
// Primary service UUID / Primary service UUID
#define SERVICE_UUID         NimBLEUUID((uint16_t)0xFFF0)
// Notify char — slave→central / Notify char — slave→central
#define CHAR_NOTIFY_UUID     NimBLEUUID((uint16_t)0xFFF1)
// Write char — central→slave / Write char — central→slave
#define CHAR_WRITE_UUID      NimBLEUUID((uint16_t)0xFFF2)

// BLE GATT client pointer / BLE GATT client pointer
NimBLEClient* pClient = nullptr;
// Reference to slave's write characteristic / Reference to slave's write characteristic
NimBLERemoteCharacteristic* pWriteChar = nullptr;
// BLE connection status flag / BLE connection status flag
bool connected = false;

5.2 Receiving Slave Push — notifyCallback

notifyCallback is the callback function for the master to receive slave Notify data. Each time the slave calls notify() to push data, the NimBLE protocol stack automatically calls this function. data points to the raw byte array, len is the byte count, and isNotify is true for Notify (false for Indicate). Serial.write(data, len) directly outputs raw bytes without string conversion to avoid \0 truncation issues.

xxxxxxxxxx
// Slave notify data callback / Slave notify data callback
void notifyCallback(NimBLERemoteCharacteristic* pChar, uint8_t* data, size_t len, bool isNotify) {
  // Print label / Print label
  Serial.print("Slave:");
  // Output raw bytes / Output raw bytes
  Serial.write(data, len);
  Serial.println();
}

5.3 Connection/Disconnection Event Callbacks — MyClientCallbacks

Connection and disconnection are two key events in the BLE master lifecycle. Inherit NimBLEClientCallbacks and override onConnect and onDisconnect: set the status flag when connected for the send task to use; when disconnected, besides clearing the flag, must set pWriteChar to nullptr—remote characteristic references are invalid after connection disconnect, and the next reconnection requires re-obtaining through GATT discovery, otherwise write operations will access invalid pointers.

xxxxxxxxxx
// BLE connection callbacks class / BLE connection callbacks class
class MyClientCallbacks : public NimBLEClientCallbacks {
  void onConnect(NimBLEClient* pClient) {
    // Set connection flag / Set connection flag
    connected = true;
    Serial.println("Connected successfully!");
  }

  void onDisconnect(NimBLEClient* pClient, int reason) {
    // Clear connection flag / Clear connection flag
    connected = false;
    // Key: nullify stale remote reference after disconnect / Key: nullify stale remote reference after disconnect
    pWriteChar = nullptr;
    Serial.println("Disconnect and rescan...");
  }
} clientCB;  // Global instance / Global instance

5.4 Scanning and Connecting — scanAndConnect

scanAndConnect is the master's most core function, completing the entire process of scan → match → connect → service discovery → subscribe.

Scan parameter configuration: Active scan + 10ms window + 500ms duration + don't filter duplicate advertisements, to discover all devices in a short time as much as possible.

Device name extraction: pDev->getName() sometimes returns an empty string (the device name in the slave advertisement packet may be in the Scan Response rather than the advertisement packet body), the code manually parses the Type 0x09 (Complete Local Name) or 0x08 (Shortened Local Name) field in the AD Structure to extract the name. This is a common pitfall in BLE master development—do not rely solely on getName() to determine the device.

Post-connection GATT discovery: After finding the target slave, four steps are performed in sequence: create Client → connect → get Service → get two Characteristics. Call subscribe(true, notifyCallback) on the Notify characteristic to enable notifications; save pWriteChar reference for the Write characteristic for the send task to use.

xxxxxxxxxx
void scanAndConnect() {
  Serial.println("\nScan BLE devices...");

  // Get scanner / Get scanner
  NimBLEScan* pScan = NimBLEDevice::getScan();
  // Active scan / Active scan
  pScan->setActiveScan(true);
  // Scan interval 10ms (16 × 0.625ms) / Scan interval 10ms
  pScan->setInterval(16);
  // Scan window 10ms / Scan window 10ms
  pScan->setWindow(16);
  // Don't filter duplicates / Don't filter duplicates
  pScan->setDuplicateFilter(false);
  // Scan 500ms, non-blocking / Scan 500ms, non-blocking
  pScan->start(500, false);
  // Wait for scan to finish / Wait for scan to finish
  delay(500);

  // Get scan results / Get scan results
  NimBLEScanResults results = pScan->getResults();
  int count = results.getCount();

  Serial.print("Number of discovered devices: ");
  Serial.println(count);

  // Iterate all discovered devices / Iterate all discovered devices
  for (int i = 0; i < count; i++) {
    const NimBLEAdvertisedDevice* pDev = results.getDevice(i);
    String name = pDev->getName().c_str();
    // BD_ADDR (6-byte Bluetooth device address) / BD_ADDR (6-byte Bluetooth device address)
    String mac  = pDev->getAddress().toString().c_str();

    // ============================================
    // Key: getName() may be empty; must parse AD Structure manually
    // Key: getName() may be empty; must parse AD Structure manually
    // ============================================
    if (name.isEmpty()) {
      // Get raw advertising payload bytes / Get raw advertising payload bytes
      std::vector<uint8_t> raw = pDev->getPayload();
      String payload = String((const char*)raw.data(), raw.size());
      size_t pos = 0;
      while (pos + 2 < payload.length()) {
        // Field total length (includes self) / Field total length (includes self)
        uint8_t fieldLen  = (uint8_t)payload[pos];
        // AD Type identifier / AD Type identifier
        uint8_t fieldType = (uint8_t)payload[pos + 1];
        // length=0 means end of advertising data / length=0 means end of advertising data
        if (fieldLen == 0) break;
        // AD Type 0x09 = Complete Local Name, 0x08 = Shortened Local Name
        if (fieldType == 0x09 || fieldType == 0x08) {
          // Extract name / Extract name
          name = payload.substring(pos + 2, pos + 2 + fieldLen - 1);
          break;
        }
        // Skip to next AD field / Skip to next AD field
        pos += fieldLen + 1;
      }
    }

    // Print discovered device info / Print discovered device info
    Serial.print("Equipment: ");
    if (name.isEmpty()) {
      Serial.print("(unknown) ");
      Serial.print(mac);
    } else {
      Serial.print(name);
      Serial.print(" [");
      Serial.print(mac);
      Serial.print("]");
    }
    Serial.println();

    // Match the preset target device name / Match the preset target device name
    if (name == TARGET_DEVICE_NAME) {
      Serial.println("Target slave device found!");
      // Stop scanning once target found / Stop scanning once target found
      pScan->stop();

      // Create BLE GATT client / Create BLE GATT client
      pClient = NimBLEDevice::createClient();
      // Register connection status callbacks / Register connection status callbacks
      pClient->setClientCallbacks(&clientCB);

      // Initiate BLE connection / Initiate BLE connection
      if (pClient->connect(pDev)) {
        // Get the target service / Get the target service
        NimBLERemoteService* pSvc = pClient->getService(SERVICE_UUID);
        if (pSvc) {
          Serial.println("Service 0xFFF0 found");

          // Get notify characteristic and subscribe / Get notify characteristic and subscribe
          NimBLERemoteCharacteristic* pNotify = pSvc->getCharacteristic(CHAR_NOTIFY_UUID);
          if (pNotify) {
            // Write CCCD to enable Notify / Write CCCD to enable Notify
            pNotify->subscribe(true, notifyCallback);
            Serial.println("Notification subscription succeeded");
          }

          // Get write characteristic / Get write characteristic
          pWriteChar = pSvc->getCharacteristic(CHAR_WRITE_UUID);
          if (pWriteChar) Serial.println("Writable features found");
        }
      }
      return;
    }
  }
}

5.5 FreeRTOS Timed Write — sendTask

sendTask is an independent FreeRTOS task that checks connection status every 5 seconds in an infinite loop, and if online, writes data to the slave via the Write characteristic (0xFFF2). The second parameter false in writeValue(data, false) means using WRITE_NR (Write Without Response) mode—no slave acknowledgment required, suitable for periodic data distribution. If you need to ensure the slave receives it, change the second parameter to true (Write With Response, requires link layer acknowledgment).

xxxxxxxxxx
// FreeRTOS send task / FreeRTOS send task
void sendTask(void* param) {
  while (1) {
    // Send only when connected and char is valid / Send only when connected and char is valid
    if (connected && pWriteChar) {
      const char* sendMsg = "Hello from ESP32 Master";
      // Write Without Response / Write Without Response
      pWriteChar->writeValue(sendMsg, false);
      Serial.printf("Sent:%s\n", sendMsg);
    }
    // Block 5 seconds / Block 5 seconds
    vTaskDelay(pdMS_TO_TICKS(5000));
  }
}

5.6 setup() — Initialize BLE Master

The master's setup() is more concise than the slave's—no need to create GATT services, only need to initialize the stack and create the send task. The name in NimBLEDevice::init("MASTER") is the master's own identifier (not mandatory, not exposed during scan phase). TX power is set to +9dBm (ESP_PWR_LVL_P9), the master should use max power to ensure scan and connection range covers the slave.

xxxxxxxxxx
void setup() {
  // Init serial + wait for USB enumeration / Init serial + wait for USB enumeration
  Serial.begin(115200);
  delay(2000);
  Serial.println("=========================");
  Serial.println("      BLE Host Start");
  Serial.println("=========================");

  // Init BLE stack / Init BLE stack
  NimBLEDevice::init("MASTER");
  // TX power +9dBm (max) / TX power +9dBm (max)
  NimBLEDevice::setPower(ESP_PWR_LVL_P9);

  // Create FreeRTOS send task / Create FreeRTOS send task
  xTaskCreate(sendTask, "send", 4096, NULL, 1, NULL);
}

BLE Master and Slave Initialization Comparison

StepSlave (Peripheral)Master (Central)
1. Initialize stackNimBLEDevice::init(DEVICE_NAME)NimBLEDevice::init("MASTER")
2. Create server/clientcreateServer() + setCallbacks()createClient() + setClientCallbacks() (in scanAndConnect)
3. Create servicecreateService() + createCharacteristic()No need to create — Discover slave services via GATT discovery
4. Start advertising/scanningstartAdvertising()getScan()->start() (in scanAndConnect)
5. Create taskxTaskCreate(sendTask)xTaskCreate(sendTask)

Core difference: Slave builds local GATT service table (createService + createCharacteristic) for master to discover; Master discovers and references slave's remote services via getService + getCharacteristic.

5.7 loop() — Auto Reconnect Loop

loop() executes in Arduino's main task in a loop, checking connection status once every 1 second. When not connected, it calls scanAndConnect() to initiate scanning—this is a one-time scan+connection flow; if the slave is found and connected, connected is set to true, and loop() enters an empty loop waiting; if the scan doesn't find the target or connection fails, it rescans after delay(1000). This forms a robust "auto reconnect on disconnect" mechanism—no reset needed to recover from any state.

xxxxxxxxxx
void loop() {
  // If disconnected: scan and connect / If disconnected: scan and connect
  if (!connected) {
    scanAndConnect();
  }
  // Check again after 1 second / Check again after 1 second
  delay(1000);
}

Execution Flow Distribution

Execution FlowResponsible ForRuns On
NimBLE Host taskProtocol stack event loop, handles scan results, connection callbacks, GATT discovery, Notify callbacksAuto-created inside NimBLEDevice::init()
sendTaskSends data to slave via Write every 5 secondsUser xTaskCreate created
loop()Continuously monitors connection status, triggers scanAndConnect() to reconnect on disconnectArduino main task

6. Operation Flow

There are two ways to test the master, choose one: Method 1 (Dual-board communication) requires two ESP32-S3 boards with Master and Slave sketches flashed respectively; Method 2 (Phone simulated slave) is suitable when you only have one development board.

6.1 Build and Flash

Prerequisites:

  1. NimBLE-Arduino library installed (Tools → Manage Libraries → Search "NimBLE" → Install, author h2zero).

    Slave device ready — Another ESP32-S3 flashed with BLE Slave program, or prepare to use a phone app as the slave.

    Open library

Install NimBLE-Arduino library

Arduino flashing flow: See Arduino → 1. Before Development → 2. Build and Flash.

6.2 Method 1: Dual-Board Communication Test (Requires Two Boards)

Applicable scenario: Two ESP32-S3 boards, one as BLE Master, one as BLE Slave, implementing complete two-board communication.

StepOperationExpected Result
1Slave board powered onSlave serial outputs Waiting for host connection...
2Master board powered onMaster scans for ESP-IDF and auto-connects
3Wait for connectionMaster serial outputs Connected successfully!
4Wait ~2 secondsMaster serial prints Slave: Hello from ESP32 Slave
5Wait ~5 secondsMaster serial prints Sent: Hello from ESP32 Master
6Disconnect Slave powerMaster serial outputs Disconnect and rescan..., automatically rescans
7Slave re-powered onMaster rediscovering and reconnecting, communication resumes

6.3 Method 2: Phone Simulated Slave Test

Applicable scenario: Only one ESP32-S3, using a phone app as the BLE slave, verifying the master's scan, connection, and write functions.

Scanning for slave devices:

Scan slave devices

Connection successful

Connect to slave device success

Phone settings:

①:Switch to device mode

Switch to device mode

②Modify your Bluetooth ID and service numbers

Connection interaction

③Serial monitor actual interaction:

Master and phone debug device interaction

Recommended Apps: nRF Connect for Mobile (most feature-rich), LightBlue, EFR Connect, BLE Scanner, all free.

nRF Connect's built-in Advertising function can simulate BLE slave:

Operation Steps:

StepOperationExpected Result
1Open phone Bluetooth debugging tool → Switch to ADVERTISER tabEnter advertisement configuration interface
2Click + to create new advertisement → Set device name to ESP-IDFAdvertisement configuration ready
3Add Service UUID 0xFFF0 → Turn on advertisement switchPhone starts simulating slave advertisement
4Master board powered onScans for phone-simulated ESP-IDF and auto-connects
5Master writes to 0xFFF2 every 5sCan see Hello from ESP32 Master on nRF Connect
6Turn off phone advertisementMaster serial outputs Disconnect and rescan...

Note: The phone only simulates advertisement+connection, may not actively push Notify data (depends on the app's GATT Server functionality), so Master-side Slave: log may not appear. This method mainly verifies the master's scan connection and write functions.

6.4 Expected Output — Dual-Board Communication

Master serial:

xxxxxxxxxx
=========================
      BLE Host Start
=========================

Scan BLE devices...
Number of discovered devices: 3
Equipment: ESP-IDF [AA:BB:CC:DD:EE:FF]
Target slave device found!
Connected successfully!
Service 0xFFF0 found
Notification subscription succeeded
Writable features found
Slave:Hello from ESP32 Slave
Sent:Hello from ESP32 Master
Slave:Hello from ESP32 Slave
Slave:Hello from ESP32 Slave
Sent:Hello from ESP32 Master
...
Disconnect and rescan...

Scan BLE devices...

Power-on, scanning, connecting:

Serial print, search and connect

Interaction with slave:

Dual-board interaction with slave

Output rhythm difference: Slave sendTask pushes Notify at fixed 2-second interval, serial prints one Sent: every 2 seconds. Master sendTask writes at fixed 5-second interval. The different print frequencies on both ends are determined by their respective task cycles and don't affect BLE transmission—every notification and write is delivered at the NimBLE underlying layer.

6.5 Log Field Description

LogMeaning
BLE Host StartProgram started, initialization complete
Scan BLE devices...Start scanning surrounding BLE devices
Number of discovered devices: NNumber of devices discovered during scan
Equipment: ESP-IDF [MAC]Scanned device name and Bluetooth MAC address
Target slave device found!Target slave matched, stop scanning
Connected successfully!BLE connection established successfully
Service 0xFFF0 foundGATT service discovery successful, found slave's primary service
Notification subscription succeededCCCD write successful, subscribed to slave Notify
Writable features foundWrite characteristic reference obtained successfully, ready to send data
Slave: <content>Received Notify data pushed by slave via 0xFFF1
Sent: <content>Master wrote data to slave via 0xFFF2
Disconnect and rescan...Connection disconnected, about to rescan

6.6 Edge Cases

ScenarioBehavior
Master powered on first (no slave online)Attempts to scan 500ms every 1 second, repeatedly prints Scan BLE devices... until slave appears
Slave loses power (master online)onDisconnect triggered → connected=false, pWriteChar=nullptr → loop() detects disconnect, rescans
Slave powered on againMaster matches ESP-IDF in next scan cycle, auto reconnects, re-discovers services, re-subscribes
Multiple devices with same name ESP-IDF in area during scanConnects to first matched device in scan list (sorted by RSSI, strongest signal first)
getName() returns empty stringManually parse AD Type 0x09/0x08 from raw advertisement payload to extract device name
Master resets after disconnectpWriteChar set to nullptr in onDisconnect, next reconnect scanAndConnect re-obtains getCharacteristic for new reference
sendTask executes during disconnectconnected=false and pWriteChar=nullptr, condition connected && pWriteChar is false, skip this send

7. FAQ

SymptomCauseSolution
Compilation error NimBLEDevice.h not foundNimBLE-Arduino library not installed, or wrong BLE library installed (Bluedroid)Library manager search "NimBLE-Arduino" (author h2zero) to install, confirm board selected ESP32S3 Dev Module, restart IDE
No serial outputUSB CDC On Boot not enabled, or baud rate mismatchTools → USB CDC On Boot → Enabled; serial monitor set to 115200; press RST to reset
Cannot scan for slaveSlave not running, device name mismatch, too far or signal interferenceConfirm slave program flashed and powered on, DEVICE_NAME = "ESP-IDF" matches; move within 5 meters; away from 2.4GHz interference sources
Device scanned but shows (unknown)Slave advertisement packet doesn't contain complete device name (name in Scan Response)Normal — Code already handles AD Structure manual parsing, doesn't affect connection; if still not matching, confirm slave side setName() was called
connect() returns falseToo far, signal interference, or slave already connected by another deviceMove closer to slave; reduce metal obstacles; confirm slave not connected by phone or other master (only one connection allowed at a time)
Cannot receive Notify after subscribeSlave characteristic doesn't have NOTIFY property, CCCD write failedConfirm slave 0xFFF1 characteristic declares NIMBLE_PROPERTY::NOTIFY; confirm slave vNotifyTask started
writeValue() write failedSlave characteristic doesn't have WRITE propertyConfirm slave 0xFFF2 characteristic declares NIMBLE_PROPERTY::WRITE or WRITE_NR
Disconnects immediately after connectionIncompatible connection parameters, slave-side onConnect timeoutNormal — loop() automatically rescans and reconnects; check distance and interference if persistent
getName() still empty after connectiongetPayload() returns raw advertisement packet data, AD Structure parsing may have errorsCheck if slave side simultaneously calls addServiceUUID() and setName(); use nRF Connect packet capture to confirm advertisement data
Slave/Master output frequency inconsistentBoth ends' prints driven by their respective task cycles, Slave 2s, Master 5sNormal — doesn't affect BLE transmission, every notification and write delivered at NimBLE underlying layer
Can Wi-Fi and BLE be used simultaneously?ESP32-S3 supports time-division multiplexing, Arduino core and NimBLE auto-coordinateInclude both #include <WiFi.h> and <NimBLEDevice.h>; note throughput decreases for both, audio streaming not recommended coexisting
Can Master connect to multiple slaves simultaneously?NimBLE supports multiple connections, but requires additional management of multiple Client instancesCreate multiple NimBLEClient objects, each with independent connect() + getService(); note memory overhead and scheduling
Bluetooth Slave Mode
I2S Microphone Recording and U-Disk Playback

COMPANY

  • About us
  • Team Style
  • Blog

LEARNING & EXPLORATION

  • Tutorials
  • Edge AI hardware
  • Educational robots
  • Embedded intelligent hardware

WHERE TO BUY

  • Yahboom Online Store
  • Amazon
  • Alibaba
  • AliExpress
  • Mercado

TECHNOLOGY SUPPORT

  • Email: support@yahboom.com

BE A DISTRIBUTOR

  • Email: sales@yahboom.com
QR Code

Copyright © 2017-2026 Yahboom. All Rights Reserved.

EN/中文