Skip to content
Tutorial
14 min read
October 17, 2026

ESP32 BLE Scanner: Find Nearby Bluetooth Devices, RSSI & iBeacons

Turn an ESP32 into a Bluetooth Low Energy scanner that lists every nearby device with its RSSI, manufacturer and iBeacon flag — complete Arduino code, the theory behind each line, and an honest look at why RSSI distance is only an estimate.

What You'll Build

An ESP32 Bluetooth Low Energy (BLE) scanner that listens for every BLE device around you — phones, fitness bands, earbuds, smart tags, beacons, other ESP32 boards — and prints a clean, sorted table to the Serial Monitor: address, signal strength (RSSI), a rough distance estimate, the manufacturer, and whether the device is an iBeacon.

No extra hardware is needed. The ESP32 has a Bluetooth radio built in, and the BLE library ships with the board package, so everything in this guide runs on a bare DevKit and a USB cable.

What you will learn:

  • How BLE advertising works, and why scanning is simply "listening" for those adverts
  • How to set up the Arduino IDE for ESP32 BLE and run a 25-line scanner
  • How to average RSSI, filter weak devices and de-duplicate repeated adverts
  • How to decode the manufacturer ID and spot an iBeacon in the raw advert bytes
  • Why RSSI-to-distance estimates are rough, and how to calibrate them
  • How to turn the scanner into a presence detector that switches an LED

Difficulty: Beginner | Build time: about 20 minutes | Board: any ESP32 with BLE (ESP32, S3, C3, C6)

Finished build — an ESP32 DevKit on a USB cable, with the Serial Monitor listing nearby BLE devices sorted by signal strength

What Is BLE, and How Is It Different from Classic Bluetooth?

"Bluetooth" is really two different radio systems that share a name and a frequency band (2.4 GHz):

Classic Bluetooth (BR/EDR)Bluetooth Low Energy (BLE)
Typical useAudio streaming, serial links, file transferSensors, beacons, wearables, small bursts of data
Data rateUp to about 2–3 Mbps125 kbps to 2 Mbps (BLE 5)
Power useHigherVery low — designed for coin cells
Connection timeSecondsA few milliseconds
Can broadcast without connectingNoYes — advertising

That last row is what makes a scanner possible. A BLE device does not need a connection to announce itself: it simply transmits a small packet — an advertisement — every so often, and anyone in range can read it. Your scanner never pairs with anything; it just listens.

Which ESP32 Boards Support BLE?

ChipClassic BluetoothBLE
ESP32 (original)Yes4.2
ESP32-S2NoNo Bluetooth at all
ESP32-S3No5
ESP32-C3No5
ESP32-C6No5
ESP32-H2No5

The code in this guide uses the standard Arduino BLE library and works on every chip that has BLE. The one board to avoid is the ESP32-S2, which has Wi-Fi but no Bluetooth radio.

How BLE Advertising Works

A BLE device that wants to be found cycles through a simple loop:

  1. It wakes up and sends the same advertisement on three dedicated advertising channels — 37, 38 and 39 (2402, 2426 and 2480 MHz). These three sit in the gaps between the busiest Wi-Fi channels so they interfere with each other as little as possible.
  2. It goes back to sleep for the advertising interval — anywhere from 20 ms to 10.24 s. A smart tag might advertise once a second to save its battery; a beacon in a shop might do it ten times a second.
  3. It repeats, forever.

What Is Inside an Advertisement?

A classic (legacy) advertisement carries at most 31 bytes, organised as a list of small records called AD structures. Each record is length, type, data. Common ones include:

  • Flags — whether the device is discoverable and whether it supports classic Bluetooth
  • Complete or shortened local name — the text you see in your phone's Bluetooth list
  • Service UUIDs — what the device offers, such as heart rate or battery
  • TX power level — the power the device claims to transmit at
  • Manufacturer-specific data — a 2-byte company ID followed by whatever the manufacturer wants to put there. Apple, Xiaomi and most beacon makers use this record.

A device can also answer a scan request with a second 31-byte packet, the scan response. Many devices keep their name in the scan response to leave room for other data in the main advert.

Active vs Passive Scanning

A passive scanner only listens. An active scanner listens and, when it hears an advert, sends back a scan request to ask for the scan response. Active scanning gives you names and extra data that passive scanning would miss, at the cost of a little more airtime and power. The sketches below use active scanning.

Scan Interval and Window

The scanner does not have to listen continuously. It listens for a window (say 99 ms) once every interval (say 100 ms). If the window equals the interval the radio listens all the time; a shorter window saves power but you will miss more adverts. The window can never be longer than the interval.

Timing diagram — a device advertises on channels 37, 38 and 39 once per advertising interval, while the scanner listens for one scan window per scan interval
Anatomy of a 31-byte advertisement — Flags, Complete Local Name and Manufacturer Specific Data, with the 2-byte company ID highlighted

Components

4 items
NameQtyLink
ESP32 DevKit (ESP32, S3, C3 or C6 — any board with BLE)×1—
USB cable (data-capable, with the right connector for your board)×1—
Smartphone with the free nRF Connect app (to create a test beacon) — optional×1—
BLE beacon, fitness band or smart tag to experiment with — optional×1—

Setting Up the Arduino IDE

If you have never programmed an ESP32 with the Arduino IDE, add the board package first:

  1. Open File → Preferences and paste the Espressif board manager URL into Additional boards manager URLs (the current link is in Espressif's Arduino-ESP32 documentation).
  2. Open Tools → Board → Boards Manager, search for esp32 and install esp32 by Espressif Systems. Use version 3.0 or newer — the sketches here are written for the 3.x core.
  3. Select your board under Tools → Board (for a generic DevKit, ESP32 Dev Module) and the correct port under Tools → Port.

You do not need to install a BLE library. BLEDevice.h and friends are part of the ESP32 package.

If you get "Sketch too big": the BLE stack is large. In Tools → Partition Scheme pick Huge APP (3MB No OTA/1MB SPIFFS) and upload again.

Make a Test Beacon with Your Phone

Scanning an empty room is boring. On an Android phone, install nRF Connect for Mobile, open the Advertiser tab, create a new advertiser with a recognisable local name such as SolderHubTest, and switch it on. Your phone is now a BLE beacon the ESP32 can find. A fitness band or a smart tag works just as well.

Sketch 1: The Minimal Scanner

Start with the smallest sketch that does something useful. It scans for five seconds, prints every device it hears, clears the result list, waits two seconds and repeats.

C++

What Each Setting Does

CallWhat it doesValue used
BLEDevice::init("")Starts the Bluetooth stack. The name only matters if you advertise, which a scanner does not.empty
setAdvertisedDeviceCallbacks(cb)Runs your onResult() function every time an advert arrivesyour class
setActiveScan(true)Sends scan requests so devices reply with names and extra datatrue
setInterval(100)Time between the start of two scan windows, in ms100
setWindow(99)How long the radio listens inside each interval, in ms (must be ≤ interval)99
start(5, false)Scans for 5 seconds. It blocks until the scan finishes.5
clearResults()Frees the memory the library uses to remember devicesafter every scan

Always call clearResults(). Without it the library keeps every device it has ever seen, and a scanner left running in a busy place will eventually run out of RAM.

Open the Serial Monitor at 115200 baud. Within a few seconds you should see a stream of lines, one per device. Names are often missing and addresses look random — that is normal, and the second sketch makes sense of it.

Serial Monitor at 115200 baud showing the raw output of the minimal scanner — one line per advertisement, with names missing on many devices

Sketch 2: A Proper Scanner — RSSI, Vendor and iBeacon Table

The minimal sketch prints every advert as it arrives, which quickly becomes a wall of text: one phone may advertise ten times in five seconds. This version keeps a small table in RAM and does four useful things:

  • De-duplicates by address, so each device appears once
  • Averages the RSSI over every advert heard during the scan, which smooths out the jitter
  • Decodes the manufacturer ID from the raw bytes and names the common ones
  • Flags iBeacons and whether the device is connectable

At the end of each scan it sorts the table strongest-first and prints it.

C++

How the Code Works

Why a Fixed Table and char Arrays?

The scanner callback runs on a different task from loop(), and it can fire dozens of times a second. Using fixed-size arrays and plain char buffers instead of String objects avoids repeated heap allocation, which keeps memory stable for hours and avoids fragmentation. The table holds up to 40 devices; raise MAX_DEVICES if you scan somewhere crowded.

setAdvertisedDeviceCallbacks(cb, true)

The second argument is wantDuplicates. With false the library calls you only for the first advert from each address. With true it calls you for every advert — which is what we want, because averaging many RSSI readings is how we get a stable number.

Finding the Manufacturer

getManufacturerData() returns the raw bytes of the manufacturer-specific record. The first two bytes are the Bluetooth SIG company identifier, stored low byte first (little-endian), so Apple's 0x004C arrives as the bytes 4C 00. The line

s.company = (uint8_t)md[0] | ((uint8_t)md[1] << 8);

rebuilds the 16-bit value. The (uint8_t) casts matter: a plain char is signed on the ESP32, and a byte above 0x7F would otherwise turn negative and corrupt the result.

Spotting an iBeacon

An iBeacon is just a manufacturer-specific record with a fixed layout, which the next section describes. The sketch checks for Apple's ID, a type byte of 0x02 and a length byte of 0x15 (21), plus at least 25 bytes in total.

Sorting and the Flags Column

sortByRssi() is a small insertion sort — more than fast enough for 40 entries. In the FLAGS column, C means the device is connectable (you could open a connection to it) and B means iBeacon. ADVS is how many adverts were heard during the scan, a handy hint at the device's advertising interval.

Example Output

This is what the Serial Monitor looks like. Yours will differ, because it depends entirely on what is around you.

=== 4 devices in 5 s ===
 #  ADDRESS            RSSI  ~DIST  ADVS  VENDOR       FLAGS  NAME
 1  aa:bb:cc:00:11:02   -55    0.7     1  Xiaomi       C-     Mi Band
 2  aa:bb:cc:00:11:03   -60    1.1     1  Apple        -B     (no name)
 3  aa:bb:cc:00:11:01   -72    3.3     2  Apple        C-     (no name)
 4  aa:bb:cc:00:11:05   -80    6.9     1  -            C-     NoMfr

Many devices will show (no name). That is not a bug: plenty of phones and tags never put a name in their adverts.

The sorted scan table in the Serial Monitor — address, averaged RSSI, estimated distance, advert count, vendor and flags for each device

RSSI and Distance: Useful, But Never Exact

RSSI (Received Signal Strength Indicator) is how strongly the scanner received an advert, in dBm. It is always negative; closer to zero is stronger.

RSSISignalTypical meaning
−30 to −50 dBmExcellentWithin about a metre
−50 to −70 dBmGoodSame room
−70 to −85 dBmFairAcross a room, or one wall away
−85 to −95 dBmPoorEdge of range, adverts get missed
Below −95 dBmUnusablePractically out of range

Turning RSSI into Distance

A common approximation is the log-distance path-loss model:

distance (m) = 10 ^ ((RSSI at 1 m − measured RSSI) / (10 × n))

RSSI at 1 m is the signal you measure with the device one metre away, and n is the path-loss exponent: about 2 in open air, 2.5–4 indoors. With the sketch's defaults (−59 dBm and n = 2.5) you get:

Measured RSSIn = 2.0n = 2.5n = 3.0
−50 dBm0.4 m0.4 m0.5 m
−60 dBm1.1 m1.1 m1.1 m
−70 dBm3.5 m2.8 m2.3 m
−80 dBm11.2 m6.9 m5.0 m
−90 dBm35.5 m17.4 m10.8 m

Notice how much the answer swings with n at weak signals: −90 dBm could mean 11 m or 35 m. That is the honest reality of RSSI ranging.

Why RSSI Lies

  • Multipath fading. 2.4 GHz waves bounce off walls, floors and furniture and add up or cancel out at the antenna. Moving a few centimetres can change RSSI by 10 dB.
  • People. Water-filled bodies absorb 2.4 GHz. A phone in a pocket reads weaker than the same phone on a table.
  • Different transmit powers. Two devices at the same distance can differ by 10 dB or more, because each one transmits at its own power.
  • Antenna orientation and interference. Wi-Fi, microwave ovens and other BLE devices all raise the noise.

How to Get a Better Number

  1. Calibrate RSSI_AT_1M. Place your beacon exactly one metre from the ESP32, run the scanner for a minute and use the average as RSSI_AT_1M.
  2. Average. The table already averages every advert in a scan. For a calmer reading, also average across several scans.
  3. Use zones, not metres. "Near", "in the room" and "far" are reliable. "3.2 m" is not.
Graph — measured RSSI at 0.5, 1, 2, 3, 5 and 8 metres for one beacon, with the log-distance curve (n = 2.5) fitted over the points
Graph — RSSI of a beacon that never moves, over 60 seconds: the raw readings jump by 10 dB or more while the 5-second average stays almost flat

Reading Manufacturer Data and iBeacons

Every manufacturer-specific record starts with a 2-byte company ID. Here are the ones the sketch recognises (the complete list is the Bluetooth SIG's Assigned Numbers document):

Company IDCompanyTypically seen on
0x004CAppleiPhones, AirPods, AirTags, Apple Watch
0x0006MicrosoftWindows laptops and Surface devices
0x0075SamsungGalaxy phones, watches, SmartTags
0x00E0GoogleAndroid and Nest devices
0x0059Nordic SemiconductorMany beacons and dev kits
0x02E5EspressifESP32-based products
0x0087GarminWatches and bike computers
0x038FXiaomiMi Band and other wearables

The iBeacon Layout

An iBeacon is Apple's beacon format. It is nothing more than a manufacturer-specific record with 25 bytes:

BytesValueMeaning
0–14C 00Apple company ID
202iBeacon type
315Remaining length: 21 bytes
4–1916 bytesProximity UUID — identifies your deployment
20–212 bytesMajor — a group, such as a floor or a shop
22–232 bytesMinor — an individual beacon
241 signed byteMeasured power at 1 m, used for distance estimates

Byte 24 is the beacon's own calibration value, and it is a better RSSI_AT_1M than a guess — if you want to extend the sketch, read it and use it. Retail beacons from many makers follow this layout, which is why the scanner can flag them.

Project: A BLE Presence Detector

Here is a practical use for a scanner: switch an LED on while a particular beacon is nearby, and off when it has gone for ten seconds. Think of a "welcome home" light, or a reminder that you have left your keys tag behind.

The sketch does not need the table. It runs a short scan, remembers when it last saw a device with the target name, and compares the signal strength with a threshold. Change TARGET_NAME to the name your beacon advertises — the SolderHubTest advertiser from your phone works perfectly.

C++

Tuning the Detector

  • NEAR_RSSI sets the "close" radius. Raise it toward −55 for a small zone; lower it toward −80 for a whole room. Test with the beacon in your pocket, since your body weakens the signal.
  • TIMEOUT_MS stops the LED flickering when a few adverts are missed. Ten seconds is a good starting point.
  • Name matching works when the beacon advertises a fixed name. To match by address instead, compare dev.getAddress().toString().c_str() with a fixed string — but read the next section first.

Why Your Phone Is Hard to Track: Address Privacy

You may notice that your phone's address changes between scans, or that the same phone appears under several addresses. This is deliberate. Modern phones, earbuds and many wearables use random private addresses that rotate every few minutes (often around 15) so that strangers cannot follow them around by logging a fixed address. Only devices that already share a secret key with the phone — the "identity resolving key" — can recognise it again.

What that means for your projects:

  • Beacons and tags with a fixed address or name are easy to detect, which is why presence detectors use them.
  • Phones are unreliable targets. If you need to know whether your phone is near, make it advertise a fixed name with an app, or use a dedicated tag.
  • Be thoughtful. A scanner only reads what devices broadcast openly, but logging and following people's devices without their knowledge is a privacy problem — and illegal in many places. Use your scanner to learn BLE and to build things for yourself, not to track other people.

Troubleshooting

ProblemLikely causeFix
Sketch too big errorBLE stack fills the default partitionTools → Partition Scheme → Huge APP (3MB No OTA/1MB SPIFFS)
Compile errors about BLEScan or onResultOld ESP32 coreUpdate esp32 by Espressif Systems to 3.0 or newer in the Boards Manager
Nothing is ever foundESP32-S2 board, or no BLE devices nearbyConfirm your chip has BLE (not the S2); switch on your test beacon
Names are all missingPassive scanningKeep setActiveScan(true)
The same device shows up many timesRandom address rotation, or you used the minimal sketchUse the table sketch; remember phones change address
Only a few devices appearScan window much shorter than the intervalUse setInterval(100) and setWindow(99)
Distance numbers look wrongUncalibrated RSSI_AT_1M or wrong nCalibrate at 1 m and treat the result as a zone, not a measurement
Board resets or hangs after a whileResults never cleared, or a weak power supplyKeep clearResults(); use a good USB cable and port
Wi-Fi gets slow while scanningWi-Fi and Bluetooth share the same 2.4 GHz radioScan in shorter bursts, or pause scanning during heavy Wi-Fi traffic
Serial Monitor shows garbageWrong baud rateSet the Serial Monitor to 115200

Ideas to Extend the Project

  • Send results to the network. Publish the table to an MQTT broker and chart it in Home Assistant.
  • Count nearby devices over the day for a simple occupancy trend — counts only, with no logging of individual addresses.
  • Log to an SD card with timestamps for a signal-strength survey of a building.
  • Filter by service UUID to find just heart-rate straps or just beacons from your own deployment.
  • Read the iBeacon UUID, major and minor and show them in the table.
  • Combine with a sensor. A BLE scanner on one ESP32 can collect readings that other ESP32 boards advertise in their manufacturer data — a neat way to move small values without Wi-Fi or pairing.
  • Try the NimBLE-Arduino library, an alternative BLE stack that uses noticeably less RAM and flash. The scanning ideas in this guide carry over directly.

Frequently Asked Questions

Can an ESP32 scan for Bluetooth and Wi-Fi at the same time? Yes, the radio is shared between them, so both work but each gets less airtime. Expect some slowdown, especially during heavy Wi-Fi traffic.

Can this scanner see classic Bluetooth devices such as speakers and headsets? Only if they are also advertising over BLE. Classic Bluetooth discovery uses a different mechanism that this library does not use.

What is the maximum BLE range? Typically 10–30 m indoors and up to roughly 100 m in open air with a good line of sight. BLE 5's Coded PHY can go much further, but this scanner listens for the standard advertisements.

Why does my phone not appear in the list? Many phones only advertise when Bluetooth is on and an app or system service is using it, and they use random addresses. Use the nRF Connect advertiser to make your phone visible with a fixed name.

Is RSSI the same on every device? No. Different chips, antennas and transmit powers give different readings for the same physical distance, which is why calibration matters.

Can I connect to a device I find? Yes — the library has a BLEClient class for connecting to a device's services. That is the next step after scanning, and a good follow-up project.

Does scanning drain the ESP32's battery? Continuous scanning uses a fairly high current, because the radio is listening almost all the time. For battery projects, scan in short bursts and use deep sleep in between.

Do I need to pair with a device to read its advert? No. Advertisements are broadcast openly, so no pairing or connection is involved.

Signal In

Built from what makers search for

The lookups piling up in SolderHub's search bar every day shape what we publish next — new component pages, board guides, and firmware notes, sent out as they go live.

By subscribing you agree to our Privacy Policy. Unsubscribe anytime.