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

15. Bluetooth Master Mode

Bluetooth Master Mode


Bluetooth Master Mode1. BLE Overview2. Core Concepts2.1 GAP — Generic Access Profile2.2 GATT — Generic Attribute Profile2.3 CCCD — Client Characteristic Configuration Descriptor2.4 Connection State Machine2.5 NimBLE Protocol Stack3. ESP32-S3 BLE Hardware Features4. Project Architecture and Flow5. Hardware Dependencies6. Source Code Analysis6.1 Header Files and Macro Configuration6.2 app_main() — Entry Point6.3 ble_central_init() — Initialization6.4 on_sync() — Sync Callback and start_scan()6.5 gap_event_cb() — GAP Event Callback6.6 GATT Discovery Chain — Callback Chain Calls6.7 vSendTask() — Periodic Send Task6.8 flush_timer_cb() — RX Accumulation Flush7. Operation Flow7.1 Build and Flash Flow7.2 Testing Method 1: Dual-Board Test7.3 Testing Method 2: With Phone Bluetooth Debugging Assistant7.4 Normal Communication Log (Dual-Board Test)7.5 Edge Cases8. Common Problems


1. BLE 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 connects faster (millisecond level) and consumes less power (coin battery can run for months).

Importance of BLE Master tutorial: BLE master (Central) plays the role of actively discovering and connecting to slaves, serving as the control end for data aggregation and command delivery. Phones, tablets, gateways and other central devices all run as master—mastering master programming means being able to build central nodes that actively scan, auto-connect, and read/write remote slave data. This is the other half of core capabilities for building complete BLE systems.

Typical application scenarios: Phone app scans nearby temperature sensors and reads data, industrial gateway periodically collects sensor values from multiple slave nodes, smart watch as master reads heart rate belt and running sensor data.

BLE role system:

RoleGAP NameResponsibilities
MasterCentralActively scans nearby devices, initiates connections, discovers services and characteristics, reads/writes data
SlavePeripheralAdvertises its presence, accepts connections, provides GATT service for master access

This project implements BLE Central (Master), responsible for scanning slave named "ESP-IDF", after connection automatically discovers GATT service, enables notifications, periodically 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:

OperationAPIDescription
Scanble_gap_disc()Actively scans nearby advertising, discovers slaves
Connectble_gap_connect()Initiates connection request to target address
Disconnection reconnectiongap_event_cb()Detects disconnection then automatically re-scans

2.2 GATT — Generic Attribute Profile

GATT (Generic Attribute Profile) defines how BLE devices organize and interact with data. Data is organized in Service → Characteristic → Descriptor three-level hierarchy.

This project's GATT custom service structure:

Service: 0xFFF0 (Custom primary service)
  ├─ Characteristic: 0xFFF1 (NOTIFY — Slave → Master reporting)
  │     └─ Descriptor: 0x2902 (CCCD — Client Characteristic Configuration Descriptor)
  └─ Characteristic: 0xFFF2 (WRITE — Master → Slave delivery)

2.3 CCCD — Client Characteristic Configuration Descriptor

CCCD (Client Characteristic Configuration Descriptor) UUID is fixed at 0x2902. Master writes CCCD to subscribe/unsubscribe to slave's notifications (Notify) or indications (Indicate).

ValueMeaning
0x0000Disable notifications and indications
0x0001Enable notifications (Notify)
0x0002Enable indications (Indicate)

This project writes 0x0001 to enable slave's 0xFFF1 notification.

2.4 Connection State Machine

Central device manages the entire lifecycle through a state machine, ensuring controllable behavior in async callbacks:

xxxxxxxxxx
STATE_IDLE ──start_scan()──► STATE_SCANNING
                                │ Matched target device
                                ▼
                           STATE_CONNECTING ──connection success──►  GATT discovery chain
                                │                        ├─ STATE_DISCOVER_SVC
                                │                        ├─ STATE_DISCOVER_NOTIFY_CHR
                                connection failed         ├─ STATE_DISCOVER_NOTIFY_DSC
                                ▼                        ├─ STATE_DISCOVER_WRITE_CHR
                           STATE_IDLE (re-scan)          └─ STATE_READY (send/receive ready)

2.5 NimBLE Protocol Stack

ESP32-S3 uses Apache NimBLE as BLE Host protocol stack. NimBLE runs on FreeRTOS, in independent task vNimbleHostTask, driving all BLE operations through nimble_port_run() event loop. NimBLE's advantage is small code size (~32KB ROM) and low memory footprint.


3. ESP32-S3 BLE Hardware Features

FeatureParameter
Wireless standardBLE 5.0
Max TX power+20 dBm
RX sensitivity-97 dBm (1Mbps)
Max MTU251 bytes (this project sets 256 negotiation upper limit)
AntennaOnboard PCB antenna
Concurrent connectionsSupported (this project single connection)
Protocol stackNimBLE (Apache open source)

BLE and WiFi share 2.4GHz antenna, both can coexist but use time division multiplexing.


4. Project Architecture and Flow

​x
app_main()
  ├─ nvs_flash_init()           ← Init NVS flash (stores pairing info)
  └─ ble_central_init()         ← BLE Central overall initialization
      ├─ nimble_port_init()              Init NimBLE protocol stack
      ├─ ble_hs_cfg callback config             Register sync/reset/store callbacks
      ├─ ble_svc_gap_init()              GAP service init
      ├─ ble_svc_gatt_init()             GATT service init
      ├─ ble_att_set_preferred_mtu(256)  Set max transmission unit
      ├─ xSemaphoreCreateBinary()        Create send sync semaphore
      ├─ xTimerCreate(flush)             Create RX flush timer
      ├─ xTaskCreate(vSendTask)          Create periodic send task
      └─ nimble_port_freertos_init()     Start NimBLE host task
          └─ on_sync()
              └─ start_scan()    ← Start BLE active scan

gap_event_cb()  ← GAP event callback (scan/connect/disconnect/receive notify)
  ├─ BLE_GAP_EVENT_DISC          Scan result → match "ESP-IDF" → initiate connection
  ├─ BLE_GAP_EVENT_CONNECT       Connection success → MTU negotiate → discover_service()
  ├─ BLE_GAP_EVENT_MTU            MTU negotiation complete
  ├─ BLE_GAP_EVENT_DISCONNECT    Disconnected → clear state → start_scan()
  └─ BLE_GAP_EVENT_NOTIFY_RX      Receive notify → buffer + flush timer

GATT discovery chain (callback chain calls):
  discover_service()             Discover 0xFFF0 service by UUID
    → discover_notify_characteristic()  Discover 0xFFF1 notify characteristic
      → discover_notify_cccd()          Discover CCCD descriptor
        → enable_notifications()        Write CCCD=0x0001 enable notify
          → discover_write_characteristic()  Discover 0xFFF2 write characteristic
            → mark_ready()              Mark STATE_READY + release semaphore

vSendTask()  ← Periodic send task
  while(1):
    ├─ xSemaphoreTake(g_send_sem)        Wait for semaphore (first=ready, subsequent=timeout)
    └─ ble_gattc_write_flat()            Write "Hello from ESP32 Master" to 0xFFF2

5. Hardware Dependencies

Hardware Connection: No external wiring, using ESP32-S3 onboard PCB antenna for BLE wireless communication.

Required components: nvs_flash, bt (NimBLE), FreeRTOS, esp_log

Companion device: Need another ESP32-S3 running BLE Slave routine as Peripheral, advertising name "ESP-IDF".


6. Source Code Analysis

Full source: Source Code → ESP-IDF → 5.Network_Protocol → 9.BLE → BLE_Master

6.1 Header Files and Macro Configuration

Master macros are symmetric with slave—service UUID, notify/write characteristic UUIDs must match on both sides for normal communication. State machine enum conn_state_t defines all 8 states from idle to ready, ensuring controllable behavior in async callbacks. g_send_sem semaphore synchronizes GATT discovery completion with periodic send task; g_target_addr caches slave address to support disconnection reconnection.

xxxxxxxxxx
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "host/ble_gatt.h"

#define TAG                     "BLE_MASTER"
// Target peripheral name
#define TARGET_DEVICE_NAME      "ESP-IDF"
// Target service UUID
#define BLE_SVC_UUID            0xFFF0
// Notify characteristic UUID
#define BLE_NOTIFY_CHR_UUID     0xFFF1
// Write characteristic UUID
#define BLE_WRITE_CHR_UUID      0xFFF2
// Periodic send interval (ms)
#define SEND_INTERVAL_MS        5000
// RX flush timeout (ms)
#define FLUSH_TIMEOUT_MS        50

// Connection state machine
typedef enum {
    STATE_IDLE,
    STATE_SCANNING,
    STATE_CONNECTING,
    STATE_DISCOVER_SVC,
    STATE_DISCOVER_NOTIFY_CHR,
    STATE_DISCOVER_NOTIFY_DSC,
    STATE_DISCOVER_WRITE_CHR,
    STATE_READY,
} conn_state_t;

// Global state variables
static conn_state_t      g_state;
// Connection handle
static uint16_t          g_conn_hdl;
// Service handle range
static uint16_t          g_svc_start, g_svc_end;
// Characteristic handles
static uint16_t          g_notify_hdl, g_write_hdl;
// Send semaphore
static SemaphoreHandle_t g_send_sem;
// Receive buffer
static uint8_t           g_rx_data[512];
// Receive data length
static uint16_t          g_rx_len;
// Flush timer
static TimerHandle_t     g_flush_timer;
// Target device address
static ble_addr_t        g_target_addr;

6.2 app_main() — Entry Point

Init NVS flash to store BLE pairing info, then call ble_central_init() to one-click start master-side Bluetooth. If NVS partition abnormal (first flash or partition corrupted) erase and rebuild.

xxxxxxxxxx
void app_main(void)
{
    // Init NVS flash
    esp_err_t ret = nvs_flash_init();
    // If NVS partition error
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
        ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // Erase and retry
        ESP_ERROR_CHECK(nvs_flash_erase());
        ESP_ERROR_CHECK(nvs_flash_init());
    }

    // Start BLE Central
    ble_central_init();
}

6.3 ble_central_init() — Initialization

ble_central_init() in order: init NimBLE protocol stack → register host callbacks → init GAP/GATT basic services → set MTU → create semaphore and timer → create send task → start host event loop. ble_att_set_preferred_mtu(256) negotiates larger MTU to avoid fragmentation of long data; xTimerCreate creates 50ms timer for RX accumulation—BLE notifications may arrive in fragments, accumulate together for one complete message.

xxxxxxxxxx
void ble_central_init(void)
{
    // Init NimBLE port
    ESP_ERROR_CHECK(nimble_port_init());

    // Register reset callback
    ble_hs_cfg.reset_cb = on_reset;
    // Register sync callback
    ble_hs_cfg.sync_cb  = on_sync;
    // Register GATT callback
    ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb;
    // Register store callback
    ble_hs_cfg.store_status_cb   = ble_store_util_status_rr;

    // Set device name
    ble_svc_gap_device_name_set("MASTER");
    // Init GAP service
    ble_svc_gap_init();
    // Init GATT service
    ble_svc_gatt_init();
    // Set MTU to 256
    ble_att_set_preferred_mtu(256);

    // Create send semaphore
    g_send_sem = xSemaphoreCreateBinary();
    configASSERT(g_send_sem != NULL);

    // Create flush timer
    g_flush_timer = xTimerCreate("flush", pdMS_TO_TICKS(FLUSH_TIMEOUT_MS),
                                 pdFALSE, NULL, flush_timer_cb);

    // Create send task
    ble_gatt_start_send_task();

    // Start NimBLE host task
    nimble_port_freertos_init(vNimbleHostTask);
}

Only call once. All BLE work is driven by NimBLE host task's event loop.

6.4 on_sync() — Sync Callback and start_scan()

After protocol stack syncs, on_sync() is automatically called, which calls start_scan() internally to start scanning nearby slaves. ble_gap_disc() starts active scanning with all default parameters, BLE_HS_FOREVER means continuous scanning without stopping. Scan results are returned via gap_event_cb() callback; after matching "ESP-IDF", automatically initiates connection.

xxxxxxxxxx
static void on_sync(void)
{
    ESP_LOGI(TAG, "Host ready, starting scan");
    g_state = STATE_IDLE;
    // Start BLE scan
    start_scan();
}

void start_scan(void)
{
    // Already scanning
    if (g_state == STATE_SCANNING) return;

    uint8_t own_addr_type;
    // Infer address type
    if (ble_hs_id_infer_auto(0, &own_addr_type) != 0) {
        ESP_LOGE(TAG, "ble_hs_id_infer_auto failed");
        return;
    }
    // Default scan params
    struct ble_gap_disc_params params = {
        .passive = 0, .filter_duplicates = 0, .limited = 0,
        .itvl = 0, .window = 0,
    };
    // Continuous scanning
    int rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER,
                          &params, gap_event_cb, NULL);
    if (rc != 0) {
        ESP_LOGE(TAG, "ble_gap_disc failed; rc=%d", rc);
        return;
    }

    g_state = STATE_SCANNING;
    ESP_LOGI(TAG, "Scanning for \"%s\" ...", TARGET_DEVICE_NAME);
}

6.5 gap_event_cb() — GAP Event Callback

gap_event_cb() is the master's central dispatcher, all BLE GAP events are dispatched and handled here:

Event TypeHandling Logic
BLE_GAP_EVENT_DISCDevice discovered → extract device name from advertising packet → name matches "ESP-IDF" then cache address and initiate connection
BLE_GAP_EVENT_CONNECTConnection success → record connection handle → negotiate MTU → start GATT service discovery
BLE_GAP_EVENT_MTUMTU negotiation complete → log
BLE_GAP_EVENT_DISCONNECTDisconnected → flush pending RX buffer → reset all handles → return to scan state
BLE_GAP_EVENT_NOTIFY_RXReceived slave notification → data accumulated to RX buffer → reset 50ms flush timer
xxxxxxxxxx
int gap_event_cb(struct ble_gap_event *event, void *arg)
{
    switch (event->type) {

    // Device discovered
    case BLE_GAP_EVENT_DISC: {
        struct ble_gap_disc_desc *d = &event->disc;
        char name[32] = {0};
        // Extract device name
        get_device_name(d, name, sizeof(name));
        if (name[0])
            ESP_LOGI(TAG, "Device: %-15s [%s] RSSI:%d",
                     name, addr_str(&d->addr), d->rssi);
        // Target matched
        if (strcmp(name, TARGET_DEVICE_NAME) == 0) {
            ESP_LOGI(TAG, "*** Target \"%s\" found, connecting ***",
                     TARGET_DEVICE_NAME);
            // Cache target address
            memcpy(&g_target_addr, &d->addr, sizeof(ble_addr_t));
            g_target_addr_type = d->addr.type;
            g_target_cached = true;
            // Initiate connection
            ble_central_connect(&g_target_addr, g_target_addr_type);
        }
        return 0;
    }

    // Connection event
    case BLE_GAP_EVENT_CONNECT:
        // Connected
        if (event->connect.status == 0) {
            // Save connection handle
            g_conn_hdl = event->connect.conn_handle;
            ESP_LOGI(TAG, "Connected (conn_handle=%d)", g_conn_hdl);
            // Exchange MTU
            ble_gattc_exchange_mtu(g_conn_hdl, NULL, NULL);
            // Discover GATT services
            discover_service();
        } else {
            ESP_LOGE(TAG, "Connection failed (status=%d)",
                     event->connect.status);
            g_state = STATE_IDLE;
            // Restart scan
            start_scan();
        }
        return 0;
    case BLE_GAP_EVENT_MTU:
        ESP_LOGI(TAG, "MTU negotiated: %d", event->mtu.value);
        return 0;

    // Disconnected
    case BLE_GAP_EVENT_DISCONNECT:
        ESP_LOGI(TAG, "Disconnected (reason=%d), re-scanning...",
                 event->disconnect.reason);
        // Flush pending buffer
        if (g_rx_len > 0) flush_timer_cb(NULL);
        // Reset all handles
        g_conn_hdl = g_svc_start = g_svc_end = 0;
        g_notify_hdl = g_write_hdl = 0;
        g_state = STATE_IDLE;
        // Restart scan
        start_scan();
        return 0;

    // Notification received
    case BLE_GAP_EVENT_NOTIFY_RX: {
        struct os_mbuf *om = event->notify_rx.om;
        // Notification data length
        uint16_t om_len = OS_MBUF_PKTLEN(om);
        // Flush before overflow
        if (g_rx_len + om_len > sizeof(g_rx_data) - 1) {
            ESP_LOGW(TAG, "RX overflow (%d+%d)", g_rx_len, om_len);
            flush_timer_cb(NULL);
        }
        uint16_t copied;
        // Copy data to buffer
        if (ble_hs_mbuf_to_flat(om, g_rx_data + g_rx_len,
                                sizeof(g_rx_data) - g_rx_len - 1,
                                &copied) != 0)
            return 0;
        g_rx_len += copied;
        // Reset flush timer
        xTimerReset(g_flush_timer, 0);
        return 0;
    }

    default:
        return 0;
    }
}

6.6 GATT Discovery Chain — Callback Chain Calls

Master discovers slave's GATT service structure through async callback chain—each step completion automatically triggers next step via callback, going all the way to the bottom:

xxxxxxxxxx
discover_service()                          ← Step 1: Find primary service 0xFFF0
  → gatt_disc_svc_cb()                      ← Found, record handle range
    → discover_notify_characteristic()       ← Step 2: Find notify characteristic 0xFFF1
      → gatt_disc_chr_notify_cb()              ← Found, record handle
        → discover_notify_cccd()             ← Step 3: Find CCCD descriptor (0x2902)
          → gatt_disc_dsc_cb()               ← Found, write 0x0001 to enable notify
            → enable_notifications()         ← Step 4: Notify enabled
              → discover_write_characteristic() ← Step 5: Find write characteristic 0xFFF2
                → gatt_disc_chr_write_cb()   ← Found, record handle
                  → mark_ready()             ← All ready! Release semaphore

Any step failing to find corresponding UUID logs an error and stops. enable_notifications() writes {0x01, 0x00} (i.e. 0x0001 little-endian) to CCCD, telling slave to enable notifications.

xxxxxxxxxx
static void enable_notifications(uint16_t cccd_handle)
{
    // 0x0001 = Enable notifications
    uint8_t val[2] = {0x01, 0x00};
    // Write CCCD
    if (ble_gattc_write_flat(g_conn_hdl, cccd_handle, val, sizeof(val),
                             NULL, NULL) != 0) {
        ESP_LOGE(TAG, "CCCD write failed");
        return;
    }
    ESP_LOGI(TAG, "Notification enabled (CCCD=0x%04X)", cccd_handle);

    // Continue pipeline: discover write char
    ble_uuid16_t chr_uuid = BLE_UUID16_INIT(BLE_WRITE_CHR_UUID);
    g_state = STATE_DISCOVER_WRITE_CHR;
    ble_gattc_disc_chrs_by_uuid(g_conn_hdl, g_svc_start, g_svc_end,
                                &chr_uuid.u, gatt_disc_chr_write_cb, NULL);
}

static void mark_ready(void)
{
    // Mark ready
    g_state = STATE_READY;
    ESP_LOGI(TAG, "GATT discovery complete, ready");
    ESP_LOGI(TAG, "  Notify:0x%04X  Write:0x%04X", g_notify_hdl, g_write_hdl);
    // Give semaphore
    xSemaphoreGive(g_send_sem);
}

6.7 vSendTask() — Periodic Send Task

vSendTask is an independent background task running in infinite loop. First time blocks on xSemaphoreTake() waiting for GATT discovery completion semaphore; subsequent times semaphore take returns due to 5-second timeout, achieving every 5 seconds auto-send. ble_gattc_write_flat() writes data to slave's 0xFFF2 characteristic.

xxxxxxxxxx
static void vSendTask(void *param)
{
    // Message to send
    const char *msg = "Hello from ESP32 Master";
    while (1) {
        // Wait for semaphore (first=GATT ready, subsequent=periodic trigger)
        if (xSemaphoreTake(g_send_sem,
                           pdMS_TO_TICKS(SEND_INTERVAL_MS)) == pdTRUE) { }
        // Ready & handle valid
        if (g_state == STATE_READY && g_write_hdl != 0) {
            // Write to slave
            int rc = ble_gattc_write_flat(g_conn_hdl, g_write_hdl,
                                          msg, strlen(msg), NULL, NULL);
            // Send OK
            if (rc == 0) ESP_LOGI(TAG, "Sent: %s", msg);
            // Send failed
            else ESP_LOGE(TAG, "Write failed; rc=%d", rc);
        }
    }
}

6.8 flush_timer_cb() — RX Accumulation Flush

BLE notifications from slave may arrive in multiple packets. flush_timer_cb() mechanism: each time data received, append to g_rx_data buffer and reset 50ms timer; if no new data within 50ms, message reception is considered complete, print complete content at once.

xxxxxxxxxx
void flush_timer_cb(TimerHandle_t timer)
{
    // No data, skip
    if (g_rx_len == 0) return;
    // Add terminator
    g_rx_data[g_rx_len] = '\0';
    // Print complete message
    ESP_LOGI(TAG, "Slave(%d bytes): %s", g_rx_len, g_rx_data);
    // Reset buffer
    g_rx_len = 0;
}

7. Operation Flow

7.1 Build and Flash Flow

ESP-IDF Flash Flow: See ESP-IDF → 1. Before Development → 3. Build and Flash.

Two ways to test master: ① Companion another ESP32-S3 flashed with BLE Slave routine for dual-device communication (section 7.2); ② Companion phone Bluetooth debugging assistant App to verify slave-side data (section 7.3).

7.2 Testing Method 1: Dual-Board Test

  1. First power on Slave board, confirm serial outputs Advertising as "ESP-IDF" ... indicating advertising started.

  2. Power on Master board, serial starts scanning and lists all nearby BLE devices. Intermixing non-target devices is normal.

Master Searching for Slave

  1. Master scans ESP-IDF and automatically connects. Serial outputs in sequence: Target "ESP-IDF" found → Connected → MTU negotiated → GATT discovery chain (Service 0xFFF0 → Notify char 0xFFF1 → CCCD → Notification enabled → Write char 0xFFF2) → GATT discovery complete - ready.

Found Target ESP-IDF Slave

Service Matching

Write Service

  1. After GATT discovery complete, Master immediately writes "Hello from ESP32 Master" via 0xFFF2. Serial outputs Sent: Hello from ESP32 Master.

  2. Slave pushes notification every 6 seconds. Master serial outputs Slave(26 bytes): Hello from ESP32 Slave [N] (first received count N depends on how long slave has been running, not necessarily 0).

Dual-Board Test — Master Version

  1. Both message types output alternately and independently, proving bidirectional data channel works.

7.3 Testing Method 2: With Phone Bluetooth Debugging Assistant

For only one development board, flash Master routine and pair with phone Bluetooth debugging assistant App to verify write and notification functions from phone side.

Common Apps: nRF Connect for Mobile (recommended), LightBlue, EFR Connect, BLE Scanner, all free.

Steps:

  1. Power on Master board, serial outputs Scanning for "ESP-IDF" ... indicating scan started.

Master Searching for Slave

  1. Master needs to find slave advertising as "ESP-IDF" to connect—so need to simultaneously run a BLE Slave (another board or another ESP32), or use phone App to simulate slave advertising (e.g., nRF Connect's Advertiser mode, advertising name set to "ESP-IDF").

Found ESP-IDF

  1. After Master connects to slave, serial outputs complete GATT discovery log (Service 0xFFF0 → Notify char 0xFFF1 → CCCD → Write char 0xFFF2), finally displays GATT discovery complete, ready.

  2. Master automatically writes to slave every 5 seconds "Hello from ESP32 Master". If slave receives normally, slave serial prints Master(23 bytes): Hello from ESP32 Master.

  1. Most phones don't support slave mode, slave mode mostly only used for receiving master's information, so dual-board test is recommended.

7.4 Normal Communication Log (Dual-Board Test)

FieldExampleMeaning
IIINFO level log
BLE_MASTERBLE_MASTERLog tag
Device:Device:Scanned device name
Sent:Sent:Write operation content sent by master
Slave(N bytes):Slave:Notification data from slave

Below is actual measured log (NimBLE protocol stack internal debug info filtered, only valid output with BLE_MASTER tag). Scan phase lists all nearby BLE devices; many non-target devices like earphones, home appliances mixed in is normal—Master only initiates connection when matching "ESP-IDF".

xxxxxxxxxx
I (483) BLE_MASTER: Scanning for "ESP-IDF" ...
I (483) main_task: Returned from app_main()
I (733) BLE_MASTER: Device: EDIFIER BLE     [CC:14:BC:02:85:7E] RSSI:-52
I (833) BLE_MASTER: Device: Baseus BowieE3  [41:AA:67:1B:51:AB] RSSI:-64
I (913) BLE_MASTER: Device: midea           [D8:34:1C:C2:48:FB] RSSI:-91
  ... (other non-target devices omitted) ...
I (2363) BLE_MASTER: Device: ESP-IDF         [A4:CB:8F:C6:B8:36] RSSI:-36
I (2363) BLE_MASTER: *** Target "ESP-IDF" found - connecting ***
I (2503) BLE_MASTER: Connected (conn_handle=1)
I (2553) BLE_MASTER: MTU negotiated: 256
I (2753) BLE_MASTER: Service 0xFFF0 [0x000E-0x0013]
I (2953) BLE_MASTER: Notify char 0xFFF1 (val=0x0010)
I (3053) BLE_MASTER: CCCD at 0x0010
I (3053) BLE_MASTER: Notification enabled (CCCD=0x0010)
I (3253) BLE_MASTER: Write char 0xFFF2 (val=0x0013)
I (3353) BLE_MASTER: GATT discovery complete - ready
I (3353) BLE_MASTER:   Notify:0x0010  Write:0x0013
I (3353) BLE_MASTER: Sent: Hello from ESP32 Master
I (3203) BLE_MASTER: Slave(26 bytes): Hello from ESP32 Slave [1]

7.5 Edge Cases

  • Only Master powered (no slave advertising): Master continuously scans. When Slave comes online, auto-connects.

  • Slave powers off first: Master detects disconnection then automatically returns to scan state. When Slave repowers, auto-reconnects.

  • Slave changed but same name: As long as advertising name still "ESP-IDF", MAC address change doesn't affect connection.


8. Common Problems

SymptomCauseSolution
Master can't find slaveSlave not powered / advertising name mismatchConfirm Slave flashed and powered, advertising name is "ESP-IDF"
Connection success but no dataGATT discovery chain interrupted (UUID mismatch)Confirm Slave service UUID is 0xFFF0, Master side UUID matches
Notifications not receivedCCCD not enabled / notify characteristic handle is 0Check slave 0xFFF1 characteristic flags include BLE_GATT_CHR_F_NOTIFY
Write failsWrite characteristic handle is 0 / connection disconnectedConfirm slave 0xFFF2 characteristic flags include BLE_GATT_CHR_F_WRITE
BLE_HS_EDONE errorService/characteristic/descriptor with corresponding UUID not foundCheck slave GATT service table definition, UUID matches Master macro definition
Doesn't reconnect after disconnectionstart_scan() re-entry protection causing state stuckConfirm BLE_GAP_EVENT_DISCONNECT correctly sets g_state = STATE_IDLE
NVS init errorNVS partition corrupted or unpartitionedIDE flash with "Erase flash" checked or run idf.py erase-flash
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/中文