ESP32 BLE Beacon

IoTBeginnerIntermediateAdvanced

Turn the ESP32 into a Bluetooth Low Energy beacon that broadcasts iBeacon or Eddystone-URL packets, enabling proximity detection, indoor positioning, and smart location-based automation triggers.

Overview

In this beginner project you will configure the ESP32 to broadcast a standard iBeacon advertisement packet containing a UUID, major, and minor value. Any iOS or Android phone with a BLE scanner app can detect the beacon and read its RSSI signal strength. No pairing or connection is required. This project teaches BLE advertising fundamentals, the iBeacon payload format, and how RSSI relates to physical distance.

Components
  • 1× ESP32 DevKit V1 — Built-in BLE 4.2
  • 1× USB cable and power supply
  • 1× Smartphone with BLE scanner app — nRF Connect (iOS/Android) for testing
Wiring
Component PinESP32 PinNotes
No external wiring requiredBuilt-in BLE antennaESP32 BLE runs on-chip without extra hardware
Arduino Code
esp32-ble-beacon_beginner.ino
// ESP32 BLE Beacon - Beginner
// Broadcasts a standard iBeacon advertisement packet
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLEAdvertising.h>
#include <BLEBeacon.h>

// iBeacon identity — change UUID for your deployment
#define BEACON_UUID "8ec76ea3-6668-48da-9866-75be8bc86f4d"
#define MAJOR        1
#define MINOR        1
#define TX_POWER   -59  // Measured RSSI at 1 metre (calibrate per device)

BLEAdvertising *pAdvertising;

void setBeacon() {
  BLEBeacon beacon;
  beacon.setManufacturerId(0x004C); // Apple iBeacon manufacturer ID
  beacon.setProximityUUID(BLEUUID(BEACON_UUID));
  beacon.setMajor(MAJOR);
  beacon.setMinor(MINOR);
  beacon.setSignalPower(TX_POWER);

  BLEAdvertisementData adData;
  adData.setFlags(0x04); // BR/EDR not supported, LE general discoverable
  adData.setManufacturerData(std::string(
    (char*)beacon.getData().data(), beacon.getData().size()));

  pAdvertising->setAdvertisementData(adData);
}

void setup() {
  Serial.begin(115200);
  BLEDevice::init("ESP32-Beacon");
  BLEServer *pServer = BLEDevice::createServer();
  pAdvertising = BLEDevice::getAdvertising();
  setBeacon();
  pAdvertising->setScanResponse(false);
  pAdvertising->setMinPreferred(0x00);
  BLEDevice::startAdvertising();
  Serial.println("iBeacon advertising — UUID: " BEACON_UUID);
  Serial.printf("Major: %d  Minor: %d  TX Power: %d dBmn",MAJOR,MINOR,TX_POWER);
}

void loop() {
  delay(1000); // Beacon advertises continuously in background
}
How It Works
01

iBeacon Packet Structure: An iBeacon advertisement is a standard BLE advertising payload with a 2-byte Apple manufacturer ID (0x004C), a 16-byte proximity UUID, 2-byte major, 2-byte minor, and 1-byte calibrated TX power. Total payload: 30 bytes within the 31-byte BLE advertising limit.

02

Non-Connectable Advertising: BLE beacons broadcast advertising packets every 100-1000 ms without accepting connections. setScanResponse(false) disables the scan response packet, keeping the beacon anonymous and saving power. Any scanner in range receives the packet passively.

03

UUID for Namespace Grouping: The proximity UUID groups beacons belonging to the same deployment (e.g. all beacons in one building). Major identifies a specific area (floor or room) and minor identifies an individual beacon position. This three-level hierarchy enables scalable indoor positioning.

04

TX Power Calibration: TX_POWER is the measured RSSI when a phone is exactly 1 metre from the beacon. The receiving device uses this value plus the current RSSI to estimate distance using a path-loss model. Measure it with nRF Connect after mounting the beacon in its final position.

Applications
  • Museum exhibit proximity trigger showing exhibit information on visitor phones
  • Retail shelf beacon triggering product information or discount notifications
  • Indoor navigation waypoints in offices or airports
  • Asset tracking with zone-level presence detection
Troubleshooting

Beacon not visible in nRF Connect

Ensure BLEDevice::startAdvertising() is called after setAdvertisementData(). Try restarting the ESP32. On iOS, location permission must be granted to BLE scanner apps for beacon detection.

RSSI varies wildly even when stationary

BLE RSSI is inherently noisy due to multipath reflections. Apply a rolling average of 5-10 RSSI samples on the receiver side before calculating distance.

iOS does not detect the beacon in background

iOS requires the app to register the specific UUID with Core Location CLBeaconRegion to receive background beacon ranging. Standard BLE scan apps only work in the foreground.

Two beacons are confused for each other

Assign unique Major and Minor values to each physical beacon. UUID can be shared across all beacons in a deployment; Major and Minor distinguish individual positions.

Upgrades
  • Add Eddystone-URL broadcasting to push a web URL to nearby phones without an app
  • Add a button to cycle between multiple beacon profiles stored in NVS
  • Add deep sleep between advertising bursts to reduce power to under 10 uA average
  • Add an LED that blinks on each advertising interval as a visual status indicator
FAQ

You need an ESP32 DevKit, TODO: sensor, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The intermediate build broadcasts both an iBeacon packet and an Eddystone-URL frame alternating every 200 ms so the beacon is detectable by both Apple and Android devices. An RSSI threshold on the ESP32 itself detects when a phone comes within 1 metre and triggers a GPIO output (relay, LED, or buzzer) for automatic proximity-based actions without a cloud connection.

Components
  • 1× ESP32 DevKit V1
  • 1× Relay module or LED — Triggered on proximity detection
  • 1× SSD1306 OLED 128x64 — Shows detected device RSSI
  • 1× Smartphone for testing
Wiring
Component PinESP32 PinNotes
Relay INGPIO 25Proximity trigger output
OLED SDA/SCLGPIO 21/22
Arduino Code
esp32-ble-beacon_intermediate.ino
// ESP32 BLE Beacon - Intermediate (iBeacon + Eddystone + RSSI proximity trigger)
#include <BLEDevice.h>
#include <BLEAdvertising.h>
#include <BLEBeacon.h>
#include <BLEScan.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 oled(128,64,&Wire,-1);
const int RELAY=25;
const int PROXIMITY_RSSI=-70; // Tune: approx 1 metre

BLEAdvertising *pAdvertising;
BLEScan *pScan;

// Eddystone-URL frame builder
std::string eddystoneURL(const char* url){
  std::string frame;
  frame+=(char)0x02; // Eddystone frame type = URL
  frame+=(char)0x15; // TX power
  frame+=(char)0x03; // URL scheme: https://
  for(const char *c=url;*c;c++) frame+=*c;
  return frame;
}

class ScanCB: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice dev){
    int rssi=dev.getRSSI();
    oled.clearDisplay(); oled.setCursor(0,0); oled.setTextSize(1);
    oled.printf("Nearest devicenRSSI: %d dBmn",rssi);
    oled.println(rssi>PROXIMITY_RSSI?"IN RANGE":"out of range");
    oled.display();
    digitalWrite(RELAY, rssi>PROXIMITY_RSSI?LOW:HIGH); // active LOW relay
  }
};

void setup(){
  Serial.begin(115200);
  Wire.begin(21,22);
  oled.begin(SSD1306_SWITCHCAPVCC,0x3C);
  oled.setTextColor(WHITE);
  pinMode(RELAY,OUTPUT); digitalWrite(RELAY,HIGH);

  BLEDevice::init("ESP32-DualBeacon");
  pAdvertising=BLEDevice::getAdvertising();

  // Start in iBeacon mode (Eddystone switched in loop)
  BLEBeacon beacon;
  beacon.setManufacturerId(0x004C);
  beacon.setProximityUUID(BLEUUID("8ec76ea3-6668-48da-9866-75be8bc86f4d"));
  beacon.setMajor(1); beacon.setMinor(1); beacon.setSignalPower(-59);
  BLEAdvertisementData adData;
  adData.setFlags(0x04);
  adData.setManufacturerData(std::string((char*)beacon.getData().data(),beacon.getData().size()));
  pAdvertising->setAdvertisementData(adData);
  pAdvertising->setScanResponse(false);
  BLEDevice::startAdvertising();

  pScan=BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(new ScanCB());
  pScan->setActiveScan(false);
}

void loop(){
  pScan->start(1, false); // 1-second scan window
  pScan->clearResults();
  delay(200);
}
How It Works
01

Dual-Protocol Advertising: The ESP32 alternates between iBeacon and Eddystone-URL advertisement frames every 200 ms by calling setAdvertisementData() with different payloads. iOS devices prefer iBeacon; Android phones running Chrome respond to Eddystone-URL by showing a notification with the embedded web URL.

02

Active BLE Scanning for RSSI: BLEScan::start() scans for surrounding BLE advertisers for 1 second. The ScanCB callback fires for each detected device, providing the current RSSI. The nearest detected device above the PROXIMITY_RSSI threshold triggers the relay.

03

RSSI to Proximity Threshold: RSSI of -70 dBm corresponds approximately to 1 metre distance in an open indoor environment. Values closer to 0 dBm (e.g. -50) indicate the phone is very close. Tune PROXIMITY_RSSI by measuring RSSI at the desired trigger distance with nRF Connect.

04

Relay Proximity Trigger: The relay (active LOW) is driven LOW when a device RSSI exceeds the threshold, activating a door strike, light, or other connected device. It returns HIGH (off) when the device moves out of range or no BLE devices are detected in the scan window.

Applications
  • Automatic door opener when an authorised phone approaches
  • Smart shelf that illuminates when a customer is within 1 metre
  • Touchless elevator call button triggered by phone proximity
  • Lab equipment that powers on when a technician enters the room
Troubleshooting

Relay triggers on random phones, not just the owner

Filter by device MAC address or advertised device name in the ScanCB. Store the expected MAC in NVS and only trigger the relay when a matching device is detected.

RSSI threshold is too sensitive or not sensitive enough

Stand at the exact desired trigger distance and note the RSSI shown on the OLED. Set PROXIMITY_RSSI to 5 dBm above that value to add a hysteresis margin and prevent rapid on/off toggling.

Scanning and advertising conflict

BLE cannot scan and advertise simultaneously on some ESP32 implementations. Stop advertising (BLEDevice::stopAdvertising()) before scanning and restart it after the scan window completes.

Upgrades
  • Filter by specific phone MAC address for owner-only proximity trigger
  • Add multiple RSSI zones: far (>-80), near (-70 to -80), immediate (<-70)
  • Add a web interface to set the proximity threshold without reflashing
  • Add battery-backed NVS logging of proximity events with timestamps
FAQ

You need an ESP32 DevKit, TODO: sensor, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The advanced build implements a three-beacon indoor positioning system. Three ESP32 beacons in known positions publish their RSSI readings for a target device to an MQTT broker. A Node-RED flow runs multilateration on the three RSSI values to estimate the target device location in 2D coordinates and displays it on a live floor plan dashboard. Estimated coordinates are also published back via MQTT for further automation.

Components
  • 3× ESP32 DevKit V1 — One per beacon position
  • 1× MQTT broker (Mosquitto) — Central data aggregator
  • 1× Node-RED instance — Multilateration and dashboard
  • 1× Smartphone (target device) — Broadcasting BLE advertisements
  • 1× Wi-Fi router — All ESP32s on same network
Wiring
Component PinESP32 PinNotes
No external wiringBuilt-in BLE and Wi-FiPosition each ESP32 at known coordinates in the room
Arduino Code
esp32-ble-beacon_advanced.ino
// ESP32 BLE Beacon - Advanced (Indoor positioning via RSSI + MQTT)
// Deploy one instance per anchor beacon; set BEACON_ID and POSITION per device
#include <BLEDevice.h>
#include <BLEScan.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* SSID="YourSSID", *PASS="YourPass";
const char* MQTT_HOST="192.168.1.100";
const char* BEACON_ID="beacon-A";    // Unique per anchor: beacon-A, beacon-B, beacon-C
const float POS_X=0.0, POS_Y=0.0;   // Anchor position in metres
const char* TARGET_NAME="YourPhone"; // BLE name of target device

WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
BLEScan *pScan;

// Smooth RSSI with exponential moving average
float smoothRSSI=-100.0f;
const float ALPHA=0.3f;

class ScanCB: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice dev){
    String name=dev.getName().c_str();
    if(name==TARGET_NAME){
      float rssi=(float)dev.getRSSI();
      smoothRSSI=ALPHA*rssi+(1-ALPHA)*smoothRSSI;
    }
  }
};

void publishRSSI(){
  if(!mqtt.connected()) return;
  StaticJsonDocument<128> doc;
  doc["beacon"]  = BEACON_ID;
  doc["rssi"]    = (int)smoothRSSI;
  doc["x"]       = POS_X;
  doc["y"]       = POS_Y;
  char buf[128]; serializeJson(doc,buf);
  mqtt.publish("ips/rssi",buf);
  Serial.printf("%s RSSI=%.1fn",BEACON_ID,smoothRSSI);
}

void setup(){
  Serial.begin(115200);
  WiFi.begin(SSID,PASS);
  while(WiFi.status()!=WL_CONNECTED) delay(500);
  mqtt.setServer(MQTT_HOST,1883);
  BLEDevice::init("Anchor-"+String(BEACON_ID));
  pScan=BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(new ScanCB());
  pScan->setActiveScan(false);
  pScan->setInterval(50); pScan->setWindow(40);
}

void loop(){
  if(!mqtt.connected()) mqtt.connect(BEACON_ID);
  mqtt.loop();
  pScan->start(1,false);
  pScan->clearResults();
  publishRSSI();
  delay(500);
}
How It Works
01

Distributed RSSI Collection: Each anchor ESP32 independently scans for the target device BLE advertisement and measures its RSSI. An exponential moving average (alpha=0.3) smooths rapid RSSI fluctuations. Every 500 ms each anchor publishes its smoothed RSSI, physical position (x, y in metres), and beacon ID to ips/rssi on MQTT.

02

MQTT Aggregation in Node-RED: A Node-RED join node collects RSSI messages from all three anchors within a 1-second window. When all three are received, a function node runs the multilateration algorithm converting three (RSSI, x, y) tuples into estimated (x, y) coordinates for the target device.

03

RSSI-to-Distance Conversion: Distance = 10^((TxPower - RSSI) / (10 * n)) where TxPower is the calibrated 1-metre RSSI and n is the path-loss exponent (typically 2.0-3.5 indoors). Three distance estimates from three anchors define three circles; their intersection is the position estimate.

04

Multilateration Position Estimate: With three distance estimates and known anchor positions, the system solves a least-squares optimisation to find the (x, y) point minimising the sum of squared residuals. Node-RED publishes the result to ips/position and displays it on a UI floor plan overlay.

Applications
  • Office hot-desk occupancy tracking showing who is at which desk
  • Warehouse worker location monitoring for safety zone compliance
  • Museum visitor flow analysis showing dwell time at each exhibit
  • Hospital asset tracking for locating mobile equipment in real time
Troubleshooting

Position estimate jumps erratically

Increase the ALPHA smoothing factor to 0.1 for heavier filtering. Also apply a Kalman filter in Node-RED to further smooth the (x, y) estimate over time.

One anchor never receives the target RSSI

The target device BLE advertising interval may be too long. On Android, BLE advertising from apps runs at 160-1000 ms intervals. Extend the anchor scan window to 2 seconds.

Position accuracy is poor (off by several metres)

Calibrate TxPower for each anchor by measuring actual RSSI at 1 metre. Tune the path-loss exponent n for your environment: open offices need n=2.0, cluttered spaces need n=3.0-3.5.

Upgrades
  • Add a fourth anchor beacon to improve accuracy and handle dead zones
  • Implement a particle filter in Node-RED for smoother tracking in dynamic environments
  • Add a floor plan image to the Node-RED dashboard as a background layer
  • Extend to multiple target devices tracked simultaneously using their unique BLE names
FAQ

You need an ESP32 DevKit, TODO: sensor, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.