Chapter 64: BLE Beacons and iBeacon Implementation

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the concept of BLE beacons and their primary use cases in location-based services.
  • Differentiate between generic BLE advertising and specific beacon formats like iBeacon.
  • Describe the precise structure of an Apple iBeacon advertising packet.
  • Configure an ESP32 to broadcast as an iBeacon transmitter using ESP-IDF.
  • Implement BLE scanning on an ESP32 to detect nearby iBeacons.
  • Parse iBeacon advertising data to extract Proximity UUID, Major, Minor, and Measured Power values.
  • Understand the principles of proximity estimation using RSSI and Measured Power.
  • Recognize the role of Manufacturer Specific Data in beacon implementations.

Introduction

In our journey through BLE connectivity, we’ve learned how devices can advertise their presence and how central devices can scan for them to establish connections and exchange data via GATT. However, BLE offers a powerful capability beyond direct connections: beacons. BLE beacons are small, often low-power devices that continuously broadcast a unique identifier and other limited data. They act like digital lighthouses, enabling nearby smart devices to determine their proximity to the beacon and trigger context-aware actions.

Imagine walking through a museum, and as you approach an exhibit, your phone automatically displays information about it. Or, picture navigating a large retail store where your app highlights special offers in the aisle you’re currently in. These are classic applications of BLE beacon technology. Beacons are fundamental to many location-based services, indoor navigation systems, asset tracking solutions, and proximity marketing campaigns.

This chapter will demystify BLE beacons, focusing on their implementation using the ESP32 and ESP-IDF v5.x. We will specifically delve into Apple’s popular iBeacon format, learning how to make your ESP32 act as an iBeacon transmitter and how to build an ESP32-based scanner to detect and interpret iBeacon signals.

Theory

What is a BLE Beacon?

A BLE beacon is essentially a one-way transmitter that uses BLE advertising to repeatedly broadcast a small packet of data containing a unique identifier and optionally some other information. Key characteristics of beacons include:

  1. Broadcast-Only: They typically use non-connectable, undirected advertising (ADV_NONCONN_IND). Their primary purpose is to be “seen” or “heard” rather than to establish a full data connection.
  2. Identifier Focused: The core of a beacon’s message is its identifier, which allows receiving applications to know which beacon they are near.
  3. Low Power: Many commercial beacons are designed to run for months or even years on a small coin-cell battery, achieved by using low advertising frequencies and efficient hardware.
  4. Proximity Sensing: While beacons don’t know who is listening, listening devices (like smartphones or other ESP32s) can estimate their proximity to a beacon based on the received signal strength (RSSI).

Analogy: Think of a radio station. It continuously broadcasts its signal (its call sign and programming). Radios in the vicinity can tune in to receive this broadcast. A BLE beacon is similar; it broadcasts its “call sign” (its unique ID), and nearby BLE-enabled devices can “tune in” by scanning for these advertisements. The “louder” the signal (higher RSSI), the closer the “radio” (scanner) is likely to be to the “station” (beacon).

Advertising Data in Beacons

Beacons leverage the Manufacturer Specific Data AD Type (0xFF) within the BLE advertising packet. This AD Type allows manufacturers to define their own custom data format within the advertisement. The first two bytes of the Manufacturer Specific Data are a Company Identifier (Company ID), assigned by the Bluetooth SIG. The subsequent bytes are structured according to the specific beacon format (e.g., iBeacon, Eddystone).

iBeacon Format (Apple)

Apple’s iBeacon is a widely adopted specification for BLE beacons. An iBeacon advertisement packet uses Manufacturer Specific Data with Apple’s Company ID (0x004C). The structure of the data following the Company ID is strictly defined:

  1. Company ID (2 bytes): 0x004C (Little Endian: 0x4C, 0x00). This identifies the data as Apple-defined.
  2. Beacon Type (2 bytes): 0x0215. The first byte (0x02) indicates a Proximity Beacon. The second byte (0x15 which is 21 in decimal) is the remaining length of the iBeacon data (21 bytes: 16 for UUID + 2 for Major + 2 for Minor + 1 for Measured Power).
  3. Proximity UUID (16 bytes): A Universally Unique Identifier that distinguishes a major group of beacons. For example, a retail chain might use the same Proximity UUID for all its stores.
  4. Major Number (2 bytes, Big Endian): Identifies a subset of beacons within a Proximity UUID group. For example, each store within a retail chain might have a unique Major number.
  5. Minor Number (2 bytes, Big Endian): Identifies an individual beacon within a Major number group. For example, different departments or specific points of interest within a store could have unique Minor numbers.
  6. Measured Power / Calibrated RSSI (1 byte, signed 8-bit integer): This is the RSSI value (in dBm) that a reference device measured at a distance of 1 meter from the beacon. This value is calibrated by the beacon manufacturer and broadcast by the beacon. It’s crucial for proximity estimation by the receiving device.
Field within Manufacturer Data Size (Bytes) Example Value / Format Description
Company ID 2 0x4C, 0x00 (for Apple, Inc.) Identifies the manufacturer (Apple). Sent Little Endian for 0x004C.
Beacon Type 2 0x02, 0x15 Byte 1 (0x02): Proximity Beacon. Byte 2 (0x15): Remaining data length (21 bytes).
Proximity UUID 16 (e.g., FDA50693-A4E2-4FB1-AFCF-C6EB07647825) Universally Unique Identifier for a group of beacons.
Major Number 2 (e.g., 0x0001 for 1) Identifies a subset within the UUID group (Big Endian).
Minor Number 2 (e.g., 0x000A for 10) Identifies an individual beacon within the Major group (Big Endian).
Measured Power 1 (e.g., -59 dBm) Calibrated RSSI at 1 meter distance (signed 8-bit integer).
Total iBeacon Data Payload 25 Sum of above fields.
AD Type for Manufacturer Data (1) 0xFF Standard BLE AD Type for Manufacturer Specific Data.
AD Length for iBeacon Structure (1) 0x1A (26 decimal) Length of (AD Type + iBeacon Data Payload).

Eddystone Format (Google) – A Brief Mention

Google also defined a popular open beacon format called Eddystone. Unlike iBeacon’s single packet format, Eddystone supports multiple frame types that can be interleaved or broadcast independently:

  • Eddystone-UID: Broadcasts a 16-byte beacon ID (10-byte Namespace, 6-byte Instance). Similar in concept to iBeacon’s UUID/Major/Minor.
  • Eddystone-URL: Broadcasts a compressed URL, enabling the “Physical Web” concept where devices can discover nearby web links.
  • Eddystone-TLM (Telemetry): Broadcasts beacon status information like battery voltage, temperature, and advertising PDU counts. This is typically interleaved with UID or URL frames.
  • Eddystone-EID (Ephemeral Identifier): A privacy-preserving beacon that broadcasts a rotating, encrypted identifier.

While this chapter focuses on iBeacon for practical ESP32 implementation due to its well-defined structure and widespread use, understanding that other formats like Eddystone exist is important for a broader perspective on beacon technology.

Feature iBeacon (Apple) Eddystone (Google)
Primary Identifier Proximity UUID (16 bytes), Major (2 bytes), Minor (2 bytes) UID Frame: Namespace (10 bytes), Instance (6 bytes)
Packet Format Single, fixed format for proximity. Multiple frame types (UID, URL, TLM, EID) that can be interleaved.
Web Content Link No native support (requires app logic). Eddystone-URL frame directly broadcasts a compressed URL (Physical Web).
Telemetry Data No native support (requires custom implementation if needed). Eddystone-TLM frame for battery voltage, temperature, PDU counts.
Security/Privacy Static identifiers. Eddystone-EID for rotating, encrypted identifiers.
Company ID 0x004C (Apple) 0xFEAA (Google Service Data UUID)
Openness Specification published by Apple. Open format, specification on GitHub.

Beacon Operation Flow

  1. Beacon Device (Transmitter):
    • Configured with its specific ID (UUID, Major, Minor for iBeacon) and Measured Power.
    • Periodically constructs an advertising packet containing this information (typically as Manufacturer Specific Data).
    • Broadcasts these packets using non-connectable, undirected advertising (ADV_NONCONN_IND) at a set advertising interval.
  2. Receiver Device (Scanner – e.g., Smartphone App, ESP32 Client):
    • Performs BLE scanning (active or passive).
    • Listens for advertising packets on the advertising channels.
    • When an advertising packet is received, it checks if it matches a known beacon format (e.g., by looking for Apple’s Company ID and the iBeacon type).
    • If it’s a recognized beacon, the receiver parses the payload to extract the UUID, Major, Minor, Measured Power, and also notes the current RSSI of the received packet.
    • The application on the receiver can then use this information (e.g., trigger an action if a specific beacon ID is detected, or estimate proximity).
%%{init: {"theme": "base", "themeVariables": {
    "primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB",
    "lineColor": "#6B7280", "textColor": "#1F2937",
    "fontSize": "14px", "fontFamily": "\"Open Sans\", sans-serif"
}}}%%
sequenceDiagram
    participant BT as Beacon Transmitter (ESP32)
    participant RS as Receiver/Scanner (ESP32/Smartphone)

    BT->>BT: 1. Configure with ID (UUID, Major, Minor) & Measured Power
    BT->>BT: 2. Construct ADV_NONCONN_IND packet with iBeacon data (AD Type 0xFF)
    loop Advertising Interval
        BT-->>RS: 3. Broadcasts iBeacon Packet
    end

    RS->>RS: 4. Perform BLE Scan (Active or Passive)
    RS->>RS: 5. Listen on Advertising Channels
    
    alt Packet Received
        RS-->>BT: (Receives Packet)
        RS->>RS: 6. Check if Manufacturer Data (0xFF)
        RS->>RS: 7. If yes, check Company ID (0x004C) & Beacon Type (0x0215)
        opt Matches iBeacon Format
            RS->>RS: 8. Parse UUID, Major, Minor, Measured Power
            RS->>RS: 9. Note current RSSI of received packet
            RS->>RS: 10. App logic: Estimate proximity, trigger actions, etc.
        end
    end


    

Proximity Estimation

Receiving devices can estimate their distance from a beacon using two key pieces of information:

  1. Measured Power (Tx Power at 1m): This value is calibrated and broadcast by the beacon itself. It represents the expected RSSI at a distance of 1 meter.
  2. RSSI (Received Signal Strength Indicator): This is the actual signal strength of the beacon’s advertisement as measured by the receiver.

Principle: Radio signals attenuate (get weaker) as they travel through space. The further the receiver is from the beacon, the lower the RSSI will be.

A common, simplified formula for distance estimation is:

Distance ≈ 10 ^ ((Measured Power – RSSI) / (10 * N))

Where:

  • Measured Power is in dBm.
  • RSSI is in dBm.
  • N is an environmental factor or path loss exponent, typically ranging from 2 to 4 (e.g., 2 for free space, higher in environments with more obstacles).

Important: This distance estimation is approximate. RSSI values can fluctuate significantly due to environmental factors like obstacles, reflections, interference, and even the orientation of the devices. It’s generally more reliable for categorizing proximity (e.g., “immediate,” “near,” “far”) rather than precise distance measurement. Averaging multiple RSSI readings can help improve stability.

Practical Examples

Prerequisites:

  • An ESP32 board (ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2).
  • VS Code with the Espressif IDF Extension.
  • For testing the transmitter: A smartphone with an iBeacon scanner app (e.g., “nRF Connect for Mobile,” “Locate Beacon,” “Beacon Scanner”).
  • For testing the receiver: An ESP32 iBeacon transmitter (from Example 1) or a commercial iBeacon.

Example 1: ESP32 as an iBeacon Transmitter

This example configures an ESP32 to advertise as an iBeacon.

1. Project Setup:

  • Create a new ESP-IDF project.
  • idf.py menuconfig: Ensure Bluetooth is enabled, Bluedroid selected, and BLE Only mode.

2. Code (main/ibeacon_transmitter_main.c):

C
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"

#define IBEACON_TAG "IBEACON_TX"

// iBeacon Manufacturer Specific Data
// Company ID: Apple, Inc. (0x004C)
#define ESP_BLE_IBEACON_COMPANY_ID_APPLE_0      0x4C
#define ESP_BLE_IBEACON_COMPANY_ID_APPLE_1      0x00
// Beacon Type for iBeacon
#define ESP_BLE_IBEACON_TYPE_0                  0x02 // Proximity Beacon
#define ESP_BLE_IBEACON_TYPE_1                  0x15 // Data length (21 bytes: UUID + Major + Minor + Measured Power)

// Define your iBeacon's Proximity UUID, Major, Minor, and Measured Power
// Example Proximity UUID (generate your own unique one for real applications)
static const uint8_t ibeacon_proximity_uuid[16] = {
    0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1,
    0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25
};
// Example Major number (Big Endian)
static uint16_t ibeacon_major = 0x0001; // e.g., 1
// Example Minor number (Big Endian)
static uint16_t ibeacon_minor = 0x000A; // e.g., 10
// Example Measured Power (RSSI at 1 meter, signed 8-bit)
static int8_t ibeacon_measured_power = -59; // Calibrate this for your specific ESP32 & antenna

// Raw iBeacon advertising data payload (Manufacturer Specific Data part)
// Structure: Company ID (2) + Beacon Type (2) + UUID (16) + Major (2) + Minor (2) + Measured Power (1) = 25 bytes
static uint8_t ibeacon_manufacturer_data[25];

// Advertising parameters
static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min        = 0x00A0, // Minimum advertising interval (100ms). N * 0.625ms = 160 * 0.625ms = 100ms
    .adv_int_max        = 0x00A0, // Maximum advertising interval (100ms)
    .adv_type           = ADV_TYPE_NONCONN_IND, // Non-connectable undirected advertising
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC,
    .channel_map        = ADV_CHNL_ALL,
    .adv_filter_policy  = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, // Not relevant for non-connectable
};

// Prepare the iBeacon manufacturer data
void prepare_ibeacon_data(void) {
    int offset = 0;
    // Company ID (Apple)
    ibeacon_manufacturer_data[offset++] = ESP_BLE_IBEACON_COMPANY_ID_APPLE_0;
    ibeacon_manufacturer_data[offset++] = ESP_BLE_IBEACON_COMPANY_ID_APPLE_1;
    // Beacon Type
    ibeacon_manufacturer_data[offset++] = ESP_BLE_IBEACON_TYPE_0;
    ibeacon_manufacturer_data[offset++] = ESP_BLE_IBEACON_TYPE_1;
    // Proximity UUID
    memcpy(&ibeacon_manufacturer_data[offset], ibeacon_proximity_uuid, 16);
    offset += 16;
    // Major (Big Endian)
    ibeacon_manufacturer_data[offset++] = (uint8_t)(ibeacon_major >> 8);
    ibeacon_manufacturer_data[offset++] = (uint8_t)(ibeacon_major & 0xFF);
    // Minor (Big Endian)
    ibeacon_manufacturer_data[offset++] = (uint8_t)(ibeacon_minor >> 8);
    ibeacon_manufacturer_data[offset++] = (uint8_t)(ibeacon_minor & 0xFF);
    // Measured Power
    ibeacon_manufacturer_data[offset++] = (uint8_t)ibeacon_measured_power;
}

// Advertising data configuration (using raw data for iBeacon)
// The raw data must include the AD structure: Length, Type (0xFF), then the 25 bytes of iBeacon data.
// Total length of iBeacon AD structure: 1 (AD Length) + 1 (AD Type) + 25 (iBeacon Data) = 27 bytes.
// Max adv data is 31 bytes. We also need flags.
// Flags AD structure: Length (0x02), Type (0x01), Flags Value (e.g., 0x06) = 3 bytes.
// Total: 3 (Flags) + 27 (iBeacon) = 30 bytes. This fits.
static uint8_t raw_adv_data[30];

void prepare_raw_advertising_data(void) {
    prepare_ibeacon_data(); // Fills ibeacon_manufacturer_data

    int offset = 0;
    // Flags AD Structure
    raw_adv_data[offset++] = 0x02; // Length
    raw_adv_data[offset++] = ESP_BLE_AD_TYPE_FLAG; // Type
    raw_adv_data[offset++] = ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT; // Flags value

    // Manufacturer Specific Data AD Structure (for iBeacon)
    raw_adv_data[offset++] = 0x1A; // Length (1 + 25 = 26 bytes for Type + iBeacon Data)
    raw_adv_data[offset++] = ESP_BLE_AD_TYPE_MANU_SPECIFIC_DATA; // Type (0xFF)
    memcpy(&raw_adv_data[offset], ibeacon_manufacturer_data, sizeof(ibeacon_manufacturer_data));
}


static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
        ESP_LOGI(IBEACON_TAG, "Raw advertising data set complete");
        esp_ble_gap_start_advertising(&ble_adv_params);
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
            ESP_LOGI(IBEACON_TAG, "iBeacon advertising started successfully");
        } else {
            ESP_LOGE(IBEACON_TAG, "iBeacon advertising start failed, error status = %x", param->adv_start_cmpl.status);
        }
        break;
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
        if (param->adv_stop_cmpl.status == ESP_BT_STATUS_SUCCESS) {
            ESP_LOGI(IBEACON_TAG, "iBeacon advertising stopped successfully");
        } else {
            ESP_LOGE(IBEACON_TAG, "iBeacon advertising stop failed");
        }
        break;
    default:
        break;
    }
}

void app_main(void) {
    esp_err_t ret;

    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) {
        ESP_LOGE(IBEACON_TAG, "Initialize controller failed: %s", esp_err_to_name(ret));
        return;
    }
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(IBEACON_TAG, "Enable controller failed: %s", esp_err_to_name(ret));
        return;
    }
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(IBEACON_TAG, "Init Bluedroid failed: %s", esp_err_to_name(ret));
        return;
    }
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(IBEACON_TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret));
        return;
    }

    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret) {
        ESP_LOGE(IBEACON_TAG, "GAP register error: %s", esp_err_to_name(ret));
        return;
    }

    prepare_raw_advertising_data(); // Prepare the full raw advertising packet

    ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));
    if (ret) {
        ESP_LOGE(IBEACON_TAG, "Config raw adv data failed: %s", esp_err_to_name(ret));
        return;
    }
    ESP_LOGI(IBEACON_TAG, "iBeacon Transmitter Initialized. Waiting for adv data set complete...");
}

3. Build, Flash, and Monitor:

  • idf.py build
  • idf.py flash monitor

4. Observe Output & Test with iBeacon Scanner App:

  • Serial monitor: Logs indicating advertising data set and advertising started.
  • iBeacon Scanner App (on your smartphone):
    • Scan for beacons. You should see your ESP32 listed.
    • The app should correctly parse and display the Proximity UUID, Major number, Minor number, and Measured Power you configured.
    • It will also show the current RSSI and an estimated distance.

Example 2: ESP32 as an iBeacon Scanner/Receiver

This example configures an ESP32 to scan for iBeacons and parse their data.

1. Project Setup: (Similar to Example 1)

2. Code (main/ibeacon_scanner_main.c):

C
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"

#define IBEACON_SCAN_TAG "IBEACON_SCANNER"

// iBeacon Manufacturer Specific Data constants (from Example 1)
#define ESP_BLE_IBEACON_COMPANY_ID_APPLE_0      0x4C
#define ESP_BLE_IBEACON_COMPANY_ID_APPLE_1      0x00
#define ESP_BLE_IBEACON_TYPE_0                  0x02
#define ESP_BLE_IBEACON_TYPE_1                  0x15

// Scan parameters
static esp_ble_scan_params_t ble_scan_params = {
    .scan_type              = BLE_SCAN_TYPE_ACTIVE, // Can be passive too for beacons
    .own_addr_type          = BLE_ADDR_TYPE_PUBLIC,
    .scan_filter_policy     = BLE_SCAN_FILTER_ALLOW_ALL,
    .scan_interval          = 0x50, // 50ms
    .scan_window            = 0x30, // 30ms
    .scan_duplicate         = BLE_SCAN_DUPLICATE_DISABLE
};

// Function to parse iBeacon data from manufacturer specific data
bool is_ibeacon_packet(uint8_t *adv_data, uint8_t adv_data_len) {
    if (adv_data == NULL || adv_data_len < 27) { // Min length for Flags (3) + iBeacon AD (27-3=24, but check full AD)
        return false;
    }

    // Iterate through AD structures
    uint8_t *p = adv_data;
    uint8_t len_processed = 0;

    while (len_processed < adv_data_len) {
        uint8_t ad_len = p[0];
        if (ad_len == 0) break; // End of AD structures
        uint8_t ad_type = p[1];

        if (ad_type == ESP_BLE_AD_TYPE_MANU_SPECIFIC_DATA && ad_len >= (2 + 2 + 16 + 2 + 2 + 1 +1) ) { // 1(type)+2(compID)+2(beaconType)+16(uuid)+2(major)+2(minor)+1(txpower)
            // Check Company ID and Beacon Type
            if (p[2] == ESP_BLE_IBEACON_COMPANY_ID_APPLE_0 &&
                p[3] == ESP_BLE_IBEACON_COMPANY_ID_APPLE_1 &&
                p[4] == ESP_BLE_IBEACON_TYPE_0 &&
                p[5] == ESP_BLE_IBEACON_TYPE_1) {
                return true; // Found iBeacon
            }
        }
        len_processed += (ad_len + 1);
        p += (ad_len + 1);
        if (len_processed >= adv_data_len) break;
    }
    return false;
}


// GAP event handler
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
    esp_err_t err;
    switch (event) {
    case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: {
        if ((err = param->scan_param_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(IBEACON_SCAN_TAG, "Scan param set failed: %s", esp_err_to_name(err));
            break;
        }
        ESP_LOGI(IBEACON_SCAN_TAG, "Scan parameters set, starting scan...");
        uint32_t duration = 0; // Scan continuously
        esp_ble_gap_start_scanning(duration);
        break;
    }
    case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
        if ((err = param->scan_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(IBEACON_SCAN_TAG, "Scan start failed: %s", esp_err_to_name(err));
        } else {
            ESP_LOGI(IBEACON_SCAN_TAG, "Scan started successfully.");
        }
        break;
    case ESP_GAP_BLE_SCAN_RESULT_EVT: {
        esp_ble_gap_cb_param_t *scan_result = (esp_ble_gap_cb_param_t *)param;
        if (scan_result->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
            // Check if this packet is an iBeacon
            if (is_ibeacon_packet(scan_result->scan_rst.ble_adv, scan_result->scan_rst.adv_data_len)) {
                ESP_LOGI(IBEACON_SCAN_TAG, "------------------- iBeacon Found --------------------");
                ESP_LOGI(IBEACON_SCAN_TAG, "Device Addr: %02x:%02x:%02x:%02x:%02x:%02x",
                         scan_result->scan_rst.bda[0], scan_result->scan_rst.bda[1],
                         scan_result->scan_rst.bda[2], scan_result->scan_rst.bda[3],
                         scan_result->scan_rst.bda[4], scan_result->scan_rst.bda[5]);
                ESP_LOGI(IBEACON_SCAN_TAG, "RSSI: %d dBm", scan_result->scan_rst.rssi);

                // Pointer to Manufacturer Specific Data content (after Length and Type)
                // Need to find the actual manufacturer data AD structure first.
                uint8_t *manu_data_ptr = NULL;
                uint8_t manu_data_len = 0;
                
                uint8_t *p_adv = scan_result->scan_rst.ble_adv;
                uint8_t len_adv_processed = 0;
                while(len_adv_processed < scan_result->scan_rst.adv_data_len){
                    uint8_t ad_len = p_adv[0];
                    if(ad_len == 0) break;
                    uint8_t ad_type = p_adv[1];
                    if(ad_type == ESP_BLE_AD_TYPE_MANU_SPECIFIC_DATA && ad_len >= 26){ // 25 data + 1 type
                        // Check company ID and beacon type again for robustness
                        if (p_adv[2] == ESP_BLE_IBEACON_COMPANY_ID_APPLE_0 &&
                            p_adv[3] == ESP_BLE_IBEACON_COMPANY_ID_APPLE_1 &&
                            p_adv[4] == ESP_BLE_IBEACON_TYPE_0 &&
                            p_adv[5] == ESP_BLE_IBEACON_TYPE_1) {
                            manu_data_ptr = &p_adv[2]; // Point to start of Company ID
                            manu_data_len = ad_len -1; // Length of data part of manu_specific_data
                            break; 
                        }
                    }
                    len_adv_processed += (ad_len + 1);
                    p_adv += (ad_len + 1);
                    if(len_adv_processed >= scan_result->scan_rst.adv_data_len) break;
                }


                if (manu_data_ptr != NULL && manu_data_len >= 25) { // 2(CompID)+2(Type)+16(UUID)+2(Major)+2(Minor)+1(Power)
                    // Skip Company ID (2 bytes) and Beacon Type (2 bytes)
                    uint8_t *ibeacon_data = manu_data_ptr + 4;

                    ESP_LOGI(IBEACON_SCAN_TAG, "Proximity UUID: ");
                    for (int i = 0; i < 16; i++) { printf("%02x", ibeacon_data[i]); }
                    printf("\n");

                    uint16_t major = (ibeacon_data[16] << 8) | ibeacon_data[17]; // Big Endian
                    uint16_t minor = (ibeacon_data[18] << 8) | ibeacon_data[19]; // Big Endian
                    int8_t measured_power = (int8_t)ibeacon_data[20];

                    ESP_LOGI(IBEACON_SCAN_TAG, "Major: 0x%04x (%d)", major, major);
                    ESP_LOGI(IBEACON_SCAN_TAG, "Minor: 0x%04x (%d)", minor, minor);
                    ESP_LOGI(IBEACON_SCAN_TAG, "Measured Power @ 1m: %d dBm", measured_power);
                }
                 ESP_LOGI(IBEACON_SCAN_TAG, "--------------------------------------------------");
            }
        }
        break;
    }
    // ... (other GAP events like SCAN_START_COMPLETE_EVT, SCAN_STOP_COMPLETE_EVT)
    default:
        break;
    }
}

void app_main(void) {
    // Identical initialization to Example 1: NVS, BT Controller, Bluedroid, GAP callback
    // ... (copy from Example 1 app_main, ensure GATTC registration is also done if planning to connect)
    esp_err_t ret;

    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) { ESP_LOGE(IBEACON_SCAN_TAG, "Initialize controller failed: %s", esp_err_to_name(ret)); return; }
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) { ESP_LOGE(IBEACON_SCAN_TAG, "Enable controller failed: %s", esp_err_to_name(ret)); return; }
    ret = esp_bluedroid_init();
    if (ret) { ESP_LOGE(IBEACON_SCAN_TAG, "Init Bluedroid failed: %s", esp_err_to_name(ret)); return; }
    ret = esp_bluedroid_enable();
    if (ret) { ESP_LOGE(IBEACON_SCAN_TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret)); return; }

    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret) { ESP_LOGE(IBEACON_SCAN_TAG, "GAP register error: %s", esp_err_to_name(ret)); return; }
    
    // Set scan parameters
    ret = esp_ble_gap_set_scan_params(&ble_scan_params);
    if (ret) {
        ESP_LOGE(IBEACON_SCAN_TAG, "Set scan params error: %s", esp_err_to_name(ret));
    }
    ESP_LOGI(IBEACON_SCAN_TAG, "iBeacon Scanner Initialized. Waiting for scan param set complete...");
}

3. Build, Flash, and Monitor:

  • idf.py build
  • idf.py flash monitor

4. Observe Output:

  • If an iBeacon (e.g., the ESP32 from Example 1 or a commercial one) is advertising nearby, the scanner will detect it.
  • The serial monitor will print the “iBeacon Found” message along with its MAC address, RSSI, Proximity UUID, Major, Minor, and Measured Power.

Variant Notes

  • ESP32-S2: This variant does not have Bluetooth hardware and thus cannot be used for BLE beacon applications (neither transmitting nor scanning).
  • ESP32 (Original): Fully supports transmitting and scanning for legacy BLE advertisements, making it suitable for iBeacon and Eddystone (legacy format) implementations as shown.
  • ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2: These variants also fully support legacy advertising/scanning for standard beacon formats. Additionally, their BLE 5.0 capabilities open up possibilities for more advanced beaconing scenarios:
    • Extended Advertising for Beacons: While standard iBeacon/Eddystone fit in legacy packets, you could use extended advertising to broadcast larger amounts of beacon-like data or achieve longer range for custom beacon types using LE Coded PHY.
    • Periodic Advertising for Beacons: For scenarios requiring synchronized data updates to multiple listeners without connections (e.g., real-time location systems, synchronized sensor data broadcasts), periodic advertising could be used. This is beyond typical iBeacon/Eddystone.
    • Direction Finding (AoA/AoD – ESP32-C6, H2, some S3 variants): These advanced BLE 5.1+ features allow for determining the direction of arrival or departure of BLE signals, enabling much more precise locationing than RSSI-based proximity. This could be used with beacons that support these features.

For standard iBeacon and Eddystone (UID/URL) implementations, the legacy advertising and scanning mechanisms available on all BLE-enabled ESP32 variants are sufficient.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect iBeacon Data Formatting (Transmitter) Beacon not recognized by scanner apps, or data (UUID, Major, Minor, Measured Power) is parsed incorrectly. Scanner might show “Unknown beacon” or garbage values. Verify byte order: Company ID is 0x4C, 0x00 (Little Endian for 0x004C). Beacon Type is 0x02, 0x15. Major/Minor numbers are Big Endian. UUID is 16 bytes. Measured Power is a 1-byte signed integer. Ensure the AD Length for the Manufacturer Specific Data AD structure is 0x1A (26 decimal), covering the AD Type (1 byte) + Company ID (2) + Beacon Type (2) + UUID (16) + Major (2) + Minor (2) + Measured Power (1).
Flawed Parsing Logic (Scanner) Scanner fails to identify iBeacons even if they are transmitting correctly. Displays incorrect UUID, Major, Minor, or Measured Power values. Carefully parse the advertising data.
1. Find AD Structure with Type 0xFF (Manufacturer Specific).
2. Within its data, verify Company ID (0x4C00) and Beacon Type (0x0215) at the correct offsets.
3. Extract UUID, Major, Minor, and Measured Power from subsequent offsets. Remember Major/Minor are Big Endian.
4. Use the correct data types and byte order conversions.
Measured Power Not Calibrated / Incorrectly Set Proximity estimations by receiving apps/devices are consistently inaccurate (e.g., shows “far” when close, or “immediate” when meters away). Calibrate Measured Power: Place a reference BLE scanner device 1 meter away from your ESP32 iBeacon. Record the average RSSI value observed by the scanner. This RSSI value (e.g., -59 dBm) is your Measured Power. Set this value in your iBeacon’s advertising data. Re-calibrate if antenna or enclosure changes.
Advertising Type Not Non-Connectable (Transmitter) Beacon might consume slightly more power, or scanner apps might offer a “connect” option which is not standard for iBeacons. For iBeacons, set adv_params.adv_type to ADV_TYPE_NONCONN_IND (non-connectable, undirected advertising). This is the standard for broadcast-only beacons.
Scanner Filtering Issues Scanner logs errors trying to parse non-iBeacon packets as iBeacons, or it misses actual iBeacons due to overly strict or incorrect filters. Implement robust filtering in the scanner:
1. Check for AD Type 0xFF (Manufacturer Specific Data).
2. Check for Apple’s Company ID (0x004C).
3. Check for the iBeacon type (0x0215).
Only after these checks pass, attempt to parse the rest of the iBeacon payload.
Raw Advertising Data Construction Error Advertising fails to start, or esp_ble_gap_config_adv_data_raw() returns an error. Beacon is not visible at all. Ensure the total length of the raw advertising data does not exceed 31 bytes. Include necessary AD structures like Flags (typically 3 bytes: Length=0x02, Type=0x01, Data=Flags_Value) and the iBeacon Manufacturer Data AD structure (typically 27 bytes: Length=0x1A, Type=0xFF, Data=iBeacon_Payload). Verify all lengths and types.

Exercises

  1. Custom iBeacon Configuration:
    • Modify the ESP32 iBeacon Transmitter (Example 1).
      • Generate your own unique 16-byte Proximity UUID (e.g., using an online UUID generator).
      • Set Major to 1234 and Minor to 5678.
      • Experiment with different Measured Power values (e.g., -50, -60, -70) and observe how it affects the distance estimation in a scanner app.
    • Verify with a mobile iBeacon scanner app.
  2. Multi-Beacon Scanner:
    • Modify the ESP32 iBeacon Scanner (Example 2) to detect and store information (e.g., BDA, Major, Minor, last RSSI) for up to 3 different iBeacons simultaneously (assuming they have the same Proximity UUID but different Major/Minor, or different UUIDs).
    • Print the list of detected unique beacons periodically.
  3. Proximity Alert System:
    • Combine the transmitter and scanner concepts (or use one ESP32 as a scanner and a commercial beacon/smartphone app as a transmitter).
    • In the ESP32 scanner, when a specific iBeacon (identified by its UUID, Major, and Minor) is detected:
      • If its RSSI is stronger than a predefined threshold (e.g., -55 dBm, indicating “very near”), turn on the ESP32’s onboard LED (if it has one) or print a “Beacon VERY NEAR!” message.
      • If its RSSI is weaker than another threshold (e.g., -75 dBm, indicating “further away”) after being near, turn off the LED or print “Beacon further away.”

Summary

  • BLE beacons are transmitters that periodically broadcast identifiers using non-connectable advertising, primarily via the Manufacturer Specific Data AD type.
  • iBeacon is Apple’s standard, using Company ID 0x004C, type 0x0215, and a payload containing a 16-byte Proximity UUID, 2-byte Major, 2-byte Minor, and 1-byte Measured Power.
  • The Measured Power (calibrated RSSI at 1 meter) is crucial for proximity estimation by comparing it with the current RSSI received by the scanner.
  • ESP32 can be configured as an iBeacon transmitter by crafting the raw advertising data with the correct iBeacon format.
  • ESP32 can also scan for iBeacons, filter for the iBeacon signature in Manufacturer Specific Data, and parse the payload to extract its identifiers and Measured Power.
  • Proximity estimation using RSSI is approximate and influenced by environmental factors.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top