Practical case: ESP32 BLE Locator Beacon

Practical case: ESP32 BLE Locator Beacon — hero

case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
margin: 2.4rem 0;
padding: 1.45rem 1.55rem;
border: 1px solid rgba(148, 163, 184, 0.30);
border-radius: 14px;
background:
linear-gradient(135deg, rgba(30, 41, 59, 0.50), rgba(15, 23, 42, 0.18)),
rgba(17, 24, 39, 0.48);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.20);
}
.case-objective > h2:first-of-type,
.case-device-block-diagram > h2:first-of-type,
.prometeo-device-postcode-section > h2:first-of-type,
.prometeo-device-section-card > h2:first-of-type {
margin-top: 0;
}
.prometeo-device-section-card > :last-child,
.case-objective > :last-child,
.case-device-block-diagram > :last-child,
.prometeo-device-postcode-section > :last-child {
margin-bottom: 0;
}
.prometeo-device-section-card.prometeo-device-section-card-code {
border-left: 4px solid rgba(56, 189, 248, 0.86);
background:
linear-gradient(135deg, rgba(8, 47, 73, 0.42), rgba(15, 23, 42, 0.18)),
rgba(15, 23, 42, 0.58);
}
.prometeo-device-section-card.prometeo-device-section-card-compact {
padding: 1.2rem 1.35rem;
}
.prometeo-device-section-card pre,
.case-objective pre,
.case-device-block-diagram pre,
.prometeo-device-postcode-section pre {
max-width: 100%;
}
.prometeo-device-postcode-section .prometeo-device-flow-item {
background:
linear-gradient(135deg, rgba(15, 23, 42, 0.50), rgba(30, 41, 59, 0.28)),
rgba(15, 23, 42, 0.28);
}
@media print {
.case-objective,
.case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
background: #fff;
color: #111827;
box-shadow: none;
break-inside: avoid;
page-break-inside: avoid;
}
}

Objective and use case

What you’ll build: A Bluetooth Low Energy (BLE) tool finder beacon that attaches to equipment, enabling users to trigger a localized audible and visual alarm via smartphone to quickly locate missing items.

Why it matters / Use cases

  • Asset Management: Prevents the loss of expensive tools in busy workshops, reducing replacement costs and search time.
  • Inventory Auditing: Allows warehouse staff to instantly identify specific storage bins among visually identical containers.
  • BLE Foundation: Demonstrates practical implementation of GATT server concepts, including Services, Characteristics, and asynchronous callbacks.
  • Asynchronous Hardware Control: Manages non-blocking hardware alerts (e.g., 2Hz blinking LEDs and pulsing buzzers) while maintaining a stable, low-latency RF connection.

Expected outcome

  • The ESP32 broadcasts a BLE advertisement named “ToolFinder-Beacon” with a ~100ms advertising interval.
  • A smartphone BLE scanner application successfully discovers and connects to the device.
  • Writing a specific byte (0x01) to the custom BLE characteristic triggers the hardware alarm with <50ms latency.

Audience: Embedded developers and IoT engineers; Level: Intermediate

Architecture/flow: Smartphone (BLE Client) → RF Connection → ESP32 (BLE GATT Server) → Asynchronous Callback → Non-blocking GPIO Control (LED/Buzzer)

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

Local button

ESP32 BLE

Advertising packet

Status LED

Phone scanner

Conceptual flow: local configuration, BLE advertising and phone-side reading.

Validation path

Source code

PlatformIO build

Flash

Serial monitor

Conceptual summary of the tools used to check the published ESP32 project.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this ESP32 DevKitC profile, the project was checked as a PlatformIO project: the validator extracted platformio.ini and src/main.cpp, created a temporary project and ran pio run against platform = espressif32, board = esp32dev and framework = arduino. It also checked article structure, copy/paste-safe ASCII command options, and unsupported stacks such as direct ESP-IDF or non-scoped ESP32 boards.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 3 tables and 2 code blocks detected before publication.
  • Checked code: 1 PlatformIO config + 1 ESP32 source/pio run.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ESP32 DevKitC board, wiring, power supply and local WiFi environment.

Educational safety note

This project is a low-voltage educational BLE beacon, not a certified tracking, personal-safety, or critical-inventory system. Verify the pinout of your ESP32 DevKitC, keep GPIO signals at 3.3 V, use a suitable USB or battery supply, and respect privacy and local rules when broadcasting BLE identifiers in shared spaces.

Prerequisites

To successfully complete this tutorial, ensure you have the following software and background knowledge:
* PlatformIO IDE: Installed as an extension within Visual Studio Code, or the PlatformIO Core CLI available in your system path.
* BLE Scanner Application: A smartphone with Bluetooth enabled and a generic BLE debugging app installed. Recommended apps include “LightBlue” (Punch Through) or “BLE Scanner” (Bluepixel Technologies).
* Basic C++ Knowledge: Familiarity with variables, functions, and standard control structures.
* USB Drivers: Appropriate drivers for the ESP32 DevKitC USB-to-UART bridge (commonly Silicon Labs CP210x or WCH CH34x) installed on your operating system.

Materials

You will need the exact hardware components listed below to build this prototype:
* ESP32 DevKitC: The core microcontroller development board featuring Wi-Fi and Bluetooth capabilities.
* Pushbutton: A standard 4-pin or 2-pin tactile momentary switch.
* Piezo buzzer: A passive piezo buzzer capable of generating different tones via Pulse Width Modulation (PWM).
* Status LED: A standard 5mm light-emitting diode (e.g., Red or Blue) to provide visual feedback.
* Resistor (330Ω): One current-limiting resistor for the status LED.
* Breadboard: A standard half-size or full-size prototyping breadboard.
* Jumper Wires: Male-to-male jumper cables to establish connections between components.
* Micro-USB Cable: A high-quality data-capable USB cable to connect the ESP32 DevKitC to your computer.

Setup/Connection

The hardware setup requires connecting the pushbutton, piezo buzzer, and status LED to specific General Purpose Input/Output (GPIO) pins on the ESP32 DevKitC. We will utilize the ESP32’s internal pull-up resistor for the pushbutton to minimize the external component count.

Wiring Instructions:
1. Power Distribution: Connect the GND pin from the ESP32 DevKitC to the negative (blue/black) rail of your breadboard.
2. Status LED: Place the LED on the breadboard. Connect the anode (longer leg) to ESP32 GPIO 25. Connect the cathode (shorter leg) to one end of the 330Ω resistor. Connect the other end of the resistor to the breadboard’s GND rail.
3. Piezo Buzzer: Place the passive piezo buzzer on the breadboard. Connect its positive terminal to ESP32 GPIO 26. Connect its negative terminal to the GND rail.
4. Pushbutton: Insert the tactile pushbutton across the center trench of the breadboard. Connect one terminal of the button to ESP32 GPIO 27. Connect the diagonally opposite terminal (or the adjacent terminal on a 2-pin switch) directly to the GND rail.

Connection Summary Table:

ComponentESP32 DevKitC PinIntermediate ComponentDestination
Status LED (Anode)GPIO 25NoneLED Anode
Status LED (Cathode)N/A330Ω ResistorGND Rail
Piezo Buzzer (+)GPIO 26NoneBuzzer (+)
Piezo Buzzer (-)N/ANoneGND Rail
Pushbutton (Side A)GPIO 27NoneButton Terminal
Pushbutton (Side B)N/ANoneGND Rail

Validated Code

The project requires two files within your PlatformIO project structure: the configuration file (platformio.ini) and the main C++ source code (src/main.cpp).

PlatformIO Configuration

Create a new PlatformIO project for the ESP32 DevKitC and replace the contents of platformio.ini with the following configuration. This sets the framework, board, and serial monitor baud rate.

; platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

Main Application Source Code

Replace the contents of src/main.cpp with the code below. This code initializes the BLE server, creates a custom service and characteristic, and implements a non-blocking loop to handle the alarm state and physical button presses.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

// src/main.cpp
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Hardware Pin Definitions
const int LED_PIN = 25;
const int BUZZER_PIN = 26;
const int BUTTON_PIN = 27;

// LEDC PWM Settings for the passive buzzer
const int PWM_CHANNEL = 0;
const int PWM_FREQ = 2000;
const int PWM_RESOLUTION = 8;

// BLE UUIDs - Generated unique identifiers for our custom service
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

// Global State Variables
bool deviceConnected = false;
bool alarmActive = false;

// Timing variables for non-blocking alarm pattern
unsigned long previousMillis = 0;
const long interval = 250; // Toggle every 250 milliseconds
bool toggleState = false;

// BLE Server Callbacks: Handle connection and disconnection events
class MyServerCallbacks : public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
        deviceConnected = true;
        Serial.println("BLE Device Connected.");
    }
    void onDisconnect(BLEServer* pServer) {
        deviceConnected = false;
        Serial.println("BLE Device Disconnected. Restarting advertising...");
        // Restart advertising so the device can be found again
        pServer->startAdvertising();
    }
};

// BLE Characteristic Callbacks: Handle incoming write requests from the smartphone
class MyCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
        std::string value = pCharacteristic->getValue();

        if (value.length() > 0) {
            Serial.print("Received Value: ");
            for (int i = 0; i < value.length(); i++) {
                Serial.print(value[i], HEX);
                Serial.print(" ");
            }
            Serial.println();

            // Check if the received byte is 0x01 (Start Alarm) or 0x00 (Stop Alarm)
            if (value[0] == 0x01) {
                alarmActive = true;
                Serial.println("Alarm TRIGGERED via BLE!");
            } else if (value[0] == 0x00) {
                alarmActive = false;
                Serial.println("Alarm STOPPED via BLE.");
                // Ensure hardware is turned off immediately
                digitalWrite(LED_PIN, LOW);
                ledcWriteTone(PWM_CHANNEL, 0); 
            }
        }
    }
};
// ...

🔒 This content is premium. With the monthly subscription (7-day free trial) you can unlock the full didactic material and the print-ready PDF pack.🔓 Unlock it — 7-day free trial
// src/main.cpp
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Hardware Pin Definitions
const int LED_PIN = 25;
const int BUZZER_PIN = 26;
const int BUTTON_PIN = 27;

// LEDC PWM Settings for the passive buzzer
const int PWM_CHANNEL = 0;
const int PWM_FREQ = 2000;
const int PWM_RESOLUTION = 8;

// BLE UUIDs - Generated unique identifiers for our custom service
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

// Global State Variables
bool deviceConnected = false;
bool alarmActive = false;

// Timing variables for non-blocking alarm pattern
unsigned long previousMillis = 0;
const long interval = 250; // Toggle every 250 milliseconds
bool toggleState = false;

// BLE Server Callbacks: Handle connection and disconnection events
class MyServerCallbacks : public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
        deviceConnected = true;
        Serial.println("BLE Device Connected.");
    }
    void onDisconnect(BLEServer* pServer) {
        deviceConnected = false;
        Serial.println("BLE Device Disconnected. Restarting advertising...");
        // Restart advertising so the device can be found again
        pServer->startAdvertising();
    }
};

// BLE Characteristic Callbacks: Handle incoming write requests from the smartphone
class MyCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
        std::string value = pCharacteristic->getValue();

        if (value.length() > 0) {
            Serial.print("Received Value: ");
            for (int i = 0; i < value.length(); i++) {
                Serial.print(value[i], HEX);
                Serial.print(" ");
            }
            Serial.println();

            // Check if the received byte is 0x01 (Start Alarm) or 0x00 (Stop Alarm)
            if (value[0] == 0x01) {
                alarmActive = true;
                Serial.println("Alarm TRIGGERED via BLE!");
            } else if (value[0] == 0x00) {
                alarmActive = false;
                Serial.println("Alarm STOPPED via BLE.");
                // Ensure hardware is turned off immediately
                digitalWrite(LED_PIN, LOW);
                ledcWriteTone(PWM_CHANNEL, 0); 
            }
        }
    }
};

void setup() {
    Serial.begin(115200);
    Serial.println("Starting Tool Finder Beacon...");

    // Initialize Hardware Pins
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);

    // Configure Pushbutton with internal pull-up resistor
    pinMode(BUTTON_PIN, INPUT_PULLUP);

    // Configure PWM for the passive buzzer
    ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
    ledcAttachPin(BUZZER_PIN, PWM_CHANNEL);
    ledcWriteTone(PWM_CHANNEL, 0); // Ensure buzzer is quiet on boot

    // Initialize BLE Device
    BLEDevice::init("ToolFinder-Beacon");

    // Create BLE Server
    BLEServer *pServer = BLEDevice::createServer();
    pServer->setCallbacks(new MyServerCallbacks());

    // Create BLE Service
    BLEService *pService = pServer->createService(SERVICE_UUID);

    // Create BLE Characteristic (Write capability)
    BLECharacteristic *pCharacteristic = pService->createCharacteristic(
                                           CHARACTERISTIC_UUID,
                                           BLECharacteristic::PROPERTY_WRITE
                                         );

    pCharacteristic->setCallbacks(new MyCallbacks());

    // Start the service
    pService->start();

    // Start advertising
    BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setScanResponse(true);
    pAdvertising->setMinPreferred(0x06);  // Functions that help with iPhone connections
    pAdvertising->setMinPreferred(0x12);
    BLEDevice::startAdvertising();

    Serial.println("BLE Advertising Started. Waiting for connections...");
}

void loop() {
    // 1. Check physical pushbutton to clear the alarm locally
    // Since we use INPUT_PULLUP, a pressed button reads as LOW
    if (digitalRead(BUTTON_PIN) == LOW) {
        if (alarmActive) {
            alarmActive = false;
            Serial.println("Alarm STOPPED via physical button press.");
            // Immediately turn off the hardware
            digitalWrite(LED_PIN, LOW);
            ledcWriteTone(PWM_CHANNEL, 0);

            // Simple debounce delay
            delay(300); 
        }
    }

    // 2. Handle the Alarm Pattern (Non-blocking)
    if (alarmActive) {
        unsigned long currentMillis = millis();

        if (currentMillis - previousMillis >= interval) {
            previousMillis = currentMillis;
            toggleState = !toggleState;

            if (toggleState) {
                digitalWrite(LED_PIN, HIGH);
                ledcWriteTone(PWM_CHANNEL, 2500); // 2.5 kHz tone
            } else {
                digitalWrite(LED_PIN, LOW);
                ledcWriteTone(PWM_CHANNEL, 0);    // Silence
            }
        }
    } else {
        // Ensure hardware remains off when alarm is inactive
        digitalWrite(LED_PIN, LOW);
        ledcWriteTone(PWM_CHANNEL, 0);
    }

    // Small delay to yield to FreeRTOS background tasks (BLE stack)
    delay(10);
}

Build/Flash/Run commands

To compile, upload, and monitor the code on your ESP32 DevKitC, use the PlatformIO Core CLI. Ensure your terminal is navigated to the root directory of your project (where platformio.ini is located).

CommandPurpose
pio runCompiles the project to ensure there are no syntax or dependency errors.
pio run --target uploadCompiles and flashes the built firmware to the connected ESP32 over USB.
pio device monitorOpens the serial monitor to view print statements and debug logs.

Execution Workflow:
1. Connect the ESP32 DevKitC to your computer via the micro-USB cable.
2. Open your terminal or the PlatformIO CLI window.
3. Execute pio run to verify the code compiles cleanly.
4. Execute pio run --target upload to flash the device. If the upload process pauses at “Connecting…”, you may need to press and hold the “BOOT” button on the ESP32 until the flashing begins.
5. Execute pio device monitor to observe the serial output. You should see “Starting Tool Finder Beacon…” followed by “BLE Advertising Started.”

Step-by-step Validation

Follow these checkpoints to verify the functionality of your BLE Tool Finder Beacon.

  1. Checkpoint 1: Boot and Initialization
    • Action: Open the serial monitor (pio device monitor) and press the “EN” (Reset) button on the ESP32.
    • Expected Observation: The terminal outputs “Starting Tool Finder Beacon…” and “BLE Advertising Started. Waiting for connections…”.
    • Pass Condition: The ESP32 boots without crashing and successfully starts the BLE advertising stack.
  2. Checkpoint 2: BLE Discovery
    • Action: Open your smartphone BLE app (e.g., LightBlue) and start scanning for devices.
    • Expected Observation: A device named “ToolFinder-Beacon” appears in the list of available devices.
    • Pass Condition: The smartphone successfully detects the ESP32’s BLE advertisement.
  3. Checkpoint 3: Connection Establishment
    • Action: Tap “Connect” next to “ToolFinder-Beacon” in the app.
    • Expected Observation: The app connects, displays the Custom Service UUID (4fafc201...), and the serial monitor outputs “BLE Device Connected.”
    • Pass Condition: The GATT connection is negotiated successfully.
  4. Checkpoint 4: Triggering the Alarm remotely
    • Action: In the app, expand the Custom Service, find the Characteristic (beb5483e...), tap the “Write” icon (usually an upward arrow), select “Byte Array” or “UINT8”, enter 01, and send.
    • Expected Observation: The serial monitor outputs “Alarm TRIGGERED via BLE!”. The status LED begins blinking, and the piezo buzzer pulses a 2.5 kHz tone.
    • Pass Condition: The ESP32 successfully receives the BLE payload and triggers the non-blocking hardware alert pattern.
  5. Checkpoint 5: Physical Reset
    • Action: While the alarm is actively sounding, press the physical pushbutton on the breadboard.
    • Expected Observation: The serial monitor outputs “Alarm STOPPED via physical button press.” The LED turns off, and the buzzer is silenced immediately.
    • Pass Condition: The local GPIO interrupt/polling logic successfully overrides the alarm state without requiring a BLE disconnect.

Troubleshooting

If you encounter issues during the build or validation phases, consult the following table for common problems and their solutions.

SymptomLikely causeFix
Upload fails / “Connecting…” timeoutESP32 is not automatically entering bootloader mode.Press and hold the “BOOT” button on the ESP32 DevKitC when the terminal displays “Connecting…”.
No serial output visibleIncorrect baud rate or missing CP210x/CH34x drivers.Ensure monitor_speed = 115200 in platformio.ini. Verify USB drivers are installed in your OS Device Manager.
Device not showing in BLE scanSmartphone BLE cache issue or ESP32 power instability.Toggle Bluetooth off and on again on your smartphone. Ensure the ESP32 is powered by a capable USB port.
App disconnects immediatelyGATT server crash or timeout.Reset the ESP32. Ensure you are writing to the Characteristic, not the Service descriptor.
Buzzer emits continuous noise, no pulsingBlocking code (e.g., delay()) used inside the BLE callback.Verify that the loop() uses millis() for timing as provided in the Validated Code, avoiding delay() loops.

Improvements

Once the basic prototype is functioning, consider the following enhancements to move toward a production-ready device:

  • Power Optimization:
    • Implement ESP32 Deep Sleep mode. Configure the device to wake up periodically, advertise its presence, and go back to sleep to save battery.
    • Transition the hardware from a DevKitC to a bare ESP32 module powered by a CR2032 coin cell or a small LiPo battery.
  • Advanced BLE Features:
    • Implement BLE Security (Pairing/Bonding) to ensure that only an authorized smartphone can trigger the tool finder alarm.
    • Add a “Battery Level” standard BLE service (UUID 0x180F) to report the device’s remaining power to the smartphone.
  • Form Factor and Enclosure:
    • Design and 3D print a rugged, compact enclosure that can be zip-tied or adhered to a drill, wrench, or toolbox.
    • Replace the breadboard components with a custom PCB or perfboard for physical durability.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary purpose of the BLE tool finder beacon described in the article?




Question 2: How does the tool finder beacon assist in Asset Management?




Question 3: Which BLE concept does this project demonstrate as part of its foundation?




Question 4: What is the blinking frequency of the LED alert managed by the asynchronous hardware control?




Question 5: What is the expected name of the BLE advertisement broadcasted by the ESP32?




Question 6: What is the approximate advertising interval for the BLE broadcast?




Question 7: What specific byte must be written to the custom BLE characteristic to trigger the hardware alarm?




Question 8: How does the beacon assist with Inventory Auditing?




Question 9: What type of hardware alerts are managed asynchronously in this project?




Question 10: What device is used to discover and connect to the ESP32 beacon?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top