How to Build a BLE Asset Tracker with ESP32

ESP32 microcontroller board with Bluetooth antenna for wireless asset tracking

Commercial asset trackers like Tile and AirTag have a dirty secret: they’re deliberately crippled. The hardware inside a $30 Tile is worth maybe $3, but you’re paying for the ecosystem lock-in, the subscription upsells, and the privilege of not knowing how your own devices work. Meanwhile, you’ve got ESP32 boards in your parts drawer that pack the same Bluetooth Low Energy radio, and you can make them do exactly what you want.

Here’s what we’re building: a two-device system where one ESP32 acts as a beacon (the tag you attach to your keys) and another acts as a scanner (the receiver that tells you when your keys are nearby). No phone app required. No cloud subscription. No mystery about what data goes where. Just two microcontrollers talking to each other over BLE, and complete control over every parameter.

You’ll need basic ESP32 experience: flashing firmware, navigating the Arduino IDE or PlatformIO. BLE concepts get explained as we go.

How BLE Asset Tracking Actually Works

Forget everything you know about Bluetooth pairing. BLE asset tracking doesn’t use connections at all.

Instead, your beacon continuously shouts into the void: “I’m here! My name is KEYS-BEACON! My UUID is 12345!” It does this dozens of times per second, never caring whether anyone’s listening. This is called advertising.

Your scanner, meanwhile, constantly listens for these shouts. When it hears one matching your beacon’s identity, it logs the detection and measures the RSSI (Received Signal Strength Indicator), a rough proxy for distance. Signal strong? Beacon’s close. Signal weak? It’s farther away or behind obstacles.

The critical pieces:

  • Advertising packets: Small data bursts (31 bytes max) containing device identity
  • UUID: A unique identifier so your scanner ignores the thousands of other BLE devices nearby
  • RSSI: Measured in dBm, typically -30 (very close) to -90 (far away or obstructed)

ESP32 handles both roles well. Its integrated BLE radio supports all standard advertising modes, and the Arduino BLE library abstracts away the painful parts.

Hardware You’ll Need

ComponentPurposeApproximate Cost
2× ESP32 dev boardsBeacon + Scanner$6–10 each
USB cablesPower and programming~$0 (you have these)
LiPo battery + charger board (optional)Portable beacon$5–8
LED + 220Ω resistor (optional)Visual feedback~$0.10

Total cost: $12–30 depending on what’s already in your parts bin.

Any ESP32 variant with BLE works: DevKitC, NodeMCU-32S, or the compact ESP32-C3 boards. For the beacon, smaller is better since it’ll live in a bag or attach to keys. The scanner can be any size since it stays at your base station.

Battery note: The beacon needs portability. A small LiPo (500mAh) powers an advertising-only ESP32 for 20+ hours with default settings, or days if you implement deep sleep cycles between advertising bursts.

Setting Up the Beacon

The beacon’s job is simple: advertise continuously with a unique identity. Here’s the complete, flash-ready code:

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// Define unique identifiers for your beacon
#define BEACON_NAME "KEYS-TRACKER"
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"

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

  // Initialize the BLE stack with our device name
  BLEDevice::init(BEACON_NAME);
  
  // Create a BLE server (required even though we're just advertising)
  BLEServer *pServer = BLEDevice::createServer();
  
  // Create a service with our UUID - this is what the scanner will filter for
  BLEService *pService = pServer->createService(SERVICE_UUID);
  pService->start();

  // Configure advertising parameters
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  
  // Make beacon discoverable by scanners (not just devices looking to pair)
  pAdvertising->setScanResponse(true);
  
  // Helps with iPhone detection if you ever add mobile support
  pAdvertising->setMinPreferred(0x06);
  pAdvertising->setMinPreferred(0x12);
  
  // Start broadcasting - beacon is now live
  BLEDevice::startAdvertising();
  
  Serial.println("Beacon active. Broadcasting as: " + String(BEACON_NAME));
  Serial.println("Service UUID: " + String(SERVICE_UUID));
}

void loop() {
  // Beacon runs entirely in background - nothing needed here
  // Add LED blink or deep sleep logic for battery optimization
  delay(2000);
}

Why the UUID matters: That long hexadecimal string is your beacon’s fingerprint. Your scanner will filter for this exact UUID, ignoring every other BLE device in range. Generate your own unique UUID at uuidgenerator.net. Don’t use the example above in production, or you’ll detect other people following this tutorial.

Upload this code to your first ESP32. Open Serial Monitor at 115200 baud. You should see confirmation that advertising has started. The beacon is now live, even if nothing’s listening yet.

Building the Scanner

The scanner continuously sweeps for BLE advertisements and reports when it detects your beacon. This code filters by UUID and reports RSSI:

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

// Must match your beacon's UUID exactly
#define TARGET_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"

BLEScan* pBLEScan;
bool beaconFound = false;

// Callback class - handles each discovered device
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    
    // Check if this device is advertising our target service UUID
    if (advertisedDevice.haveServiceUUID() && 
        advertisedDevice.isAdvertisingService(BLEUUID(TARGET_SERVICE_UUID))) {
      
      beaconFound = true;
      
      Serial.println("========== BEACON DETECTED ==========");
      Serial.print("Device Name: ");
      Serial.println(advertisedDevice.getName().c_str());
      
      Serial.print("RSSI: ");
      Serial.print(advertisedDevice.getRSSI());
      Serial.println(" dBm");
      
      // Rough distance estimation based on RSSI
      int rssi = advertisedDevice.getRSSI();
      if (rssi > -50) {
        Serial.println("Proximity: IMMEDIATE (< 1m)");
      } else if (rssi > -70) {
        Serial.println("Proximity: NEAR (1-3m)");
      } else if (rssi > -85) {
        Serial.println("Proximity: FAR (3-10m)");
      } else {
        Serial.println("Proximity: VERY FAR (>10m or obstructed)");
      }
      Serial.println("======================================");
      Serial.println();
    }
  }
};

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE Scanner...");
  Serial.println("Looking for UUID: " + String(TARGET_SERVICE_UUID));
  Serial.println();

  // Initialize BLE
  BLEDevice::init("ASSET-SCANNER");
  
  // Create scanner instance
  pBLEScan = BLEDevice::getScan();
  
  // Attach our callback to handle discovered devices
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  
  // Active scanning gets more data but uses more power
  pBLEScan->setActiveScan(true);
  
  // Scan window in seconds
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);
}

void loop() {
  beaconFound = false;
  
  Serial.println("Scanning...");
  
  // Run a 5-second scan
  BLEScanResults foundDevices = pBLEScan->start(5, false);
  
  if (!beaconFound) {
    Serial.println("Beacon not detected in this scan cycle.");
    Serial.println();
  }
  
  // Clear results to free memory before next scan
  pBLEScan->clearResults();
  
  // Brief pause between scan cycles
  delay(1000);
}

Upload to your second ESP32. Open Serial Monitor. You’ll see “Scanning…” followed by detection reports whenever your beacon is in range.

Understanding the output: RSSI values fluctuate. That’s normal. BLE signals bounce off walls, get absorbed by bodies, and vary with antenna orientation. Don’t expect laboratory precision. The proximity categories in the code are rough guidelines, not guarantees.

Testing and Troubleshooting

Power both ESP32 boards. The scanner’s Serial Monitor should start reporting detections within seconds.

Verification tests:

  1. Place beacon next to scanner. RSSI should be around -30 to -50 dBm.
  2. Walk the beacon to another room. Watch RSSI drop to -70 to -90 dBm.
  3. Put the beacon in a metal box. Signal may disappear entirely (useful to know).

Common issues:

Beacon not detected: Verify UUIDs match exactly (copy-paste both). Ensure the beacon’s Serial Monitor shows “Beacon active.” Restart both devices.

Extremely weak signal: ESP32 PCB antennas are directional. Rotate the beacon 90 degrees. Metal objects nearby absorb signal; move away from filing cabinets and refrigerators.

Detections are intermittent: Increase scan duration in the scanner code (change pBLEScan->start(5, false) to 10 seconds). WiFi interference on the same 2.4GHz band can also cause issues. Try a different location.

Extending Your Build

This foundation opens up several possibilities:

Multiple beacons: Assign different UUIDs to each beacon. Modify the scanner callback to maintain a list of tracked devices with individual last-seen timestamps.

Visual feedback: Add an OLED display (SSD1306) to the scanner showing beacon name, RSSI, and proximity in real-time. No computer required.

Out-of-range alerts: Track time since last detection. Trigger a piezo buzzer when a beacon hasn’t been seen for 30 seconds. Never leave your bag at a coffee shop again.

Range logging: Write timestamped RSSI data to an SD card. Analyze patterns later to understand your beacon’s typical environment.

Power optimization: Implement deep sleep on the beacon, waking every few seconds to advertise briefly. Battery life jumps from hours to weeks.

You’ve now built something commercial trackers deliberately prevent: a system you fully understand and control. The ESP32 in your bag isn’t phoning home to corporate servers or nagging you about subscriptions. It’s just doing exactly what you told it to do.


Want your DIY tracker to work beyond Bluetooth range? Hubble connects BLE devices directly to satellites—no gateways, no infrastructure. See how it works →