Bluetooth BLE scanner iBeacon decoder

A Bluetooth BLE iBeacon observer, that scans for a list of available beacons, then for each iBeacon found, it reads and processes it’s gathered data.

The example presented builds using Platformio with the espressif arduino-esp32 core, and leverages the NimBLE-Arduino Bluetooth library.

Project source code

; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino


monitor_speed = 115200


lib_deps = h2zero/NimBLE-Arduino@^1.4.0
// File: main.cpp 
// ESP32_NimBLE_scanner_iBeacon_decoder.
// <green@bug-eyed.monster> MIT License.
//
// An iBeacon observer.
// Scan for servers and log each unique result. 
// Read each logged BLE beacon in turn, filter for
// iBeacons, then process their manufacturer_data.
// Repeat.

#include <Arduino.h>

#include "NimBLEBeacon.h"
#include "NimBLEDevice.h"


#define ENDIAN_CHANGE_U16(x) ((((x)&0xff00) >> 8) + (((x)&0xff) << 8))


void setup() { 

    Serial.begin(115200);

    // Initialise the NimBLE library, witholding any advertisable name.
    NimBLEDevice::init("");
    
    // Create a Scan(ner):
    // Get a pointer to a newly created Scan instance.
    NimBLEScan *pScan = NimBLEDevice::getScan();

    // Set active scanning, this will get more data from the advertiser.
    // Comment-out if not interested in iBeacon name.
    pScan->setActiveScan(true);

    // Block whilst scanning for advertising servers, storing
    // a list of all results, over a period given in seconds.
    Serial.printf("Scanning for BLE advertisers\n");
    NimBLEScanResults results = pScan->start(10);
    Serial.printf("Found %d BLE advertisers\n", results.getCount());
    
    // Iterate through the list of NimBLEAdvertisedDevice's stored
    // in the NimBLEScanResults scan results list.
    for(int i = 0; i < results.getCount(); i++) {

        // Get a pointer to the iterated Device instance.
        NimBLEAdvertisedDevice advertisedDevice = results.getDevice(i);

        // Retrieve the BLE beacon's manufacturer_data.
        std::string strManufacturerData = advertisedDevice.getManufacturerData();

        // Did we find any manufacturer_data?
        if (strManufacturerData != "")
        {
            // Look for Apple ID and iBeacon length
            if (strManufacturerData.length() == 25 &&
                strManufacturerData[0] == 0x4c &&
                strManufacturerData[1] == 0x00 &&
                strManufacturerData[2] == 0x02 &&
                strManufacturerData[3] == 0x15) 
            {
                // Leverage the NimBLEBeacon library 
                // to help process the iBeacon data.
                NimBLEBeacon iBeacon = NimBLEBeacon();

                // Load our NimBLEBeacon object with manufacturer_data.
                iBeacon.setData(strManufacturerData);

                // Process this found iBeacon's data
                Serial.printf("Name     : %s\n",
                        advertisedDevice.getName().c_str());
                Serial.printf("Address  : %s\n",
                        advertisedDevice.getAddress().toString().c_str());
                Serial.printf("UUID     : %s\n",
                        iBeacon.getProximityUUID().toString().c_str());
                Serial.printf("Major    : %d\n",
                        ENDIAN_CHANGE_U16(iBeacon.getMajor()));
                Serial.printf("Minor    : %d\n",
                        ENDIAN_CHANGE_U16(iBeacon.getMinor()));
                Serial.printf("TX power : %d dBm\n",
                        iBeacon.getSignalPower());
                Serial.printf("RSSI     : %d dBm\n",
                        advertisedDevice.getRSSI());
                Serial.println("-----------------------------------------------");
            }
        }
    }
}

void loop() {

    setup();
}

Serial Port Output

Output obtained when run in proximity to one BLE iBeacon device flashed with the companion Bluetooth BLE iBeacon on ESP32.

Scanning for BLE advertisers
Found 2 BLE advertisers
Name     : ESP32-iBeacon
Address  : 08:b6:1f:37:f3:92
UUID     : c6dd05d0-b428-4bd5-8831-4b62651e2b41
Major    : 1
Minor    : 1
TX power : -60 dBm
RSSI     : -64 dBm
-----------------------------------------------
Scanning for BLE advertisers

Suggested reading: https://www.elektor.com/products/develop-your-own-bluetooth-low-energy-applications (no affiliation).


Bluetooth BLE iBeacon on ESP32

An exploration of BLE_iBeacon.ino – A Bluetooth BLE iBeacon on an ESP32.

The example presented builds using Platformio with the espressif arduino-esp32 core, and leverages the NimBLE-Arduino Bluetooth library.

Project source code

; File: platformio.ini
; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino

monitor_speed=115200

lib_deps = h2zero/NimBLE-Arduino@^1.4.0
// File: main.cpp
/*
   Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleScan.cpp
   Ported to Arduino ESP32 by pcbreflux
   Changed and augmented by <green@bug-eyed.monster> - MIT License.
*/
// Create a BLE server that will send periodic iBeacon frames.
//   1. Create a BLE Server
//   2. Create advertising data
//   3. Start advertising.   


#include <Arduino.h>
#include "NimBLEDevice.h"
#include "NimBLEBeacon.h"


// Freshly generated beacon UUID:
// https://www.uuidgenerator.net/
// or linux tool uuidgen
// msb nsb Address type:
//  0   0  private random non-resolvable
//  0   1  private random resolvable
//  1   1  random static address
// Make 2 most significant bits 1 to indicate a random static address for iBeacon.
#define BEACON_UUID "C6dd05d0-b428-4bd5-8831-4b62651e2b41" // Beacon UUID 128-Bit 

// Comment-out to disable iBeacon naming.
#define NAMED_iBEACON

// Select iBeacon name to advertise.
#define iBEACON_NAME "ESP32-iBeacon"

// Choose a transmit power.
// -12dBmW - 0.0625mW
// -9dBmW  - 0.125mW
// -6dBmW  - 0.250mW
// -3dBmW  - 0.500mW
//  0dBmW  - 1.000mW
//  3dBmW  - 2.000mW
//  6dBmW  - 4.000mW
//  9dBmW  - 8.000mW
#define iBEACON_POWER  ESP_PWR_LVL_N0  // 1.000mW

// Average received transmission power in dBmW @ 1 meter. 
// Measure and record RSSI at 1 meter over 10-sec then record the average.
#define MEASURED_POWER -60 


// Global Advertising(er) object used to control the Advertisement.
BLEAdvertising *pAdvertising;


void setup() {

  Serial.begin(115200);

  Serial.printf("start %s\n", iBEACON_NAME);
  
  // Initialise the NimBLE library, witholding any advertisable name for now.
  BLEDevice::init("");

  // Set transmitter power.
  BLEDevice::setPower(iBEACON_POWER); 

  // Get a pointer to an Advertising(er) instance, for subsequent population.
  pAdvertising = BLEDevice::getAdvertising();
  
  // Create and initialise AdvertisementData object.
  BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
#ifdef NAMED_iBEACON
  // Create and initialise ScanResponseData object.
  BLEAdvertisementData oScanResponseData = BLEAdvertisementData();

  // Populate ScanResponse with iBeacon's name.
  oScanResponseData.setName(iBEACON_NAME);
#endif

  // Set Advertisement flags to indicate capabilities and features of
  // this Bluetooth device. iBeacon is a generally discoverable BLE device.
  //   LE Limited Discoverable Mode         0x01
  //   LE General Discoverable Mode         0x02
  //   BR/ERD Not Supported                 0x04   (device only supports BLE)
  //   LE and BR/EDR Capable (Controller)   0x08
  //   LE and BR/EDR Capable (Host)         0x0F
  oAdvertisementData.setFlags(0x06); // LE General Discoverable Mode 0x02 | BR/ERD Not Supported 0x04

  // Assemble the manufacturer_data.
  BLEBeacon oBeacon = BLEBeacon();
  oBeacon.setManufacturerId(0x4C00); // Use Apple 0x004C ID without license, as no iPhone App
  oBeacon.setProximityUUID(BLEUUID(BEACON_UUID));
  oBeacon.setMajor(1);
  oBeacon.setMinor(1);
  oBeacon.setSignalPower((char)MEASURED_POWER); // Add the measured power value
  
  // Add manufacturer_data to AdvertisementData.
  std::string strServiceData = "";
  strServiceData += (char)(26);     // Length of the manufacturer_data
  strServiceData += (char)0xFF;     // Indicates that type is manufacturer_data
  strServiceData += oBeacon.getData(); 
  oAdvertisementData.addData(strServiceData);

  // Add AdvertisementData to Advertising(er).
  pAdvertising->setAdvertisementData(oAdvertisementData);
#ifdef NAMED_iBEACON
  // Add ScanResponseData to Advertising(er).
  pAdvertising->setScanResponseData(oScanResponseData);
#endif

  // Choose an Advertising mode for the Advertising(er).
  // iBeacon is a non-connectable, undirected broadcaster,
  // set the appropriate PDU type in the packet header.
  //   BLE_GAP_CONN_MODE_NON (non-connectable; 3.C.9.3.2).
  //   BLE_GAP_CONN_MODE_DIR (directed-connectable; 3.C.9.3.3).
  //   BLE_GAP_CONN_MODE_UND (undirected-connectable; 3.C.9.3.4).
  pAdvertising->setAdvertisementType(BLE_GAP_CONN_MODE_NON);

   // Start advertising
  pAdvertising->start();
  Serial.println("Advertising started...");
}

void loop() {

}

Nordic nRF Connect mobile app screenshots

passive scan of nameless build

active scan of build with name

Prototypical reference: https://github.com/h2zero/NimBLE-Arduino/blob/master/examples/Refactored_original_examples/BLE_iBeacon/BLE_iBeacon.ino

Suggested reading: https://www.elektor.com/products/develop-your-own-bluetooth-low-energy-applications (no affiliation).