If you want to detect drones flying in your airspace, you don't need a $5,000 RF spectrum analyzer. Under the FAA's Remote ID mandate, nearly all commercial and hobby drones over 250g must continuously broadcast their identity, location, and operator position via Bluetooth Low Energy (BLE) or Wi-Fi. By building an ESP32 RemoteID scanner, you can passively sniff these Open Drone ID (ODID) packets and log local drone traffic.
This guide targets the ESP32-S3-DevKitC-1 (specifically the WROOM-1 module variant). While the older ESP32-WROOM-32 works, the S3 variant features Bluetooth 5.0 with improved RF sensitivity and a dual-core 240MHz processor, which is critical for handling the high-volume BLE advertisement bursts without dropping packets or triggering watchdog resets.
Hardware & Pin Mapping
To keep this build self-contained, we are pairing the ESP32-S3 with a standard 128x64 I2C OLED. This allows you to walk around your property and see drone detections in real-time without being tethered to a serial monitor.
Parts List
- MCU: ESP32-S3-DevKitC-1 (WROOM-1 variant with PCB antenna)
- Display: 1.3" or 0.96" 128x64 I2C OLED (SSD1306 or SH1106 driver)
- Wiring: 4x silicone jumper wires (female-to-female)
- Power: USB-C cable and a 5V/1A power bank for mobile scanning
Pin Mapping Table
The ESP32-S3 has a different default I2C pinout than the original ESP32. Wire your OLED exactly as shown below to match the code definitions.
| OLED Pin | ESP32-S3 GPIO | Function | Notes |
|---|---|---|---|
| VCC | 3V3 | Power | Do not use 5V; S3 logic is 3.3V tolerant |
| GND | GND | Ground | Common ground required |
| SCL | GPIO 9 | I2C Clock | Default S3 I2C clock pin |
| SDA | GPIO 8 | I2C Data | Default S3 I2C data pin |
Open Drone ID (ODID) Protocol Breakdown
Before writing the scanner, you need to understand what you are looking for. The Open Drone ID specification (which forms the basis of ASTM F3411 and the FAA rule) structures its BLE advertisements into specific message types. Drones broadcast these using the ODID Service UUID 0000fffa-0000-1000-8000-00805f9b34fb.
Here is the data-dense breakdown of the ODID message payload structure. When you eventually parse the raw hex payloads in advanced builds, these are the type identifiers you will filter for.
| Msg Type | Hex ID | Name | Payload Size | Key Data Fields |
|---|---|---|---|---|
| 0 | 0x00 | Basic ID | 20 bytes | UA Type, ID Type, UAS ID (Serial/MAVLink) |
| 1 | 0x10 | Location/Vector | 46 bytes | Lat/Lon, Altitude, Speed, Pitch/Roll |
| 2 | 0x20 | Authentication | 23 bytes | Auth Type, Page Number, Auth Data |
| 3 | 0x30 | Self ID | 23 bytes | Description Type, Free-text description |
| 4 | 0x40 | System | 18 bytes | Operator Lat/Lon, Area Ceiling/Count |
| 5 | 0x50 | Operator ID | 20 bytes | Operator ID Type, Operator ID String |
The Complete ESP32 RemoteID Scanner Code
This code uses the official Espressif BLEDevice library. It initializes the I2C display, sets up a continuous BLE scan, and filters incoming advertisements for the Open Drone ID service UUID.
Prerequisites: Install the Adafruit SSD1306 and Adafruit GFX libraries via the Arduino Library Manager before compiling.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
// --- Pin & Hardware Definitions ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
#define I2C_SDA 8
#define I2C_SCL 9
// Open Drone ID Service UUID (ASTM F3411)
static BLEUUID odidUUID("0000fffa-0000-1000-8000-00805f9b34fb");
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
BLEScan* pBLEScan;
int droneCount = 0;
// --- BLE Callback Handler ---
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
// Filter for ODID Service UUID
if(advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(odidUUID)) {
droneCount++;
Serial.printf("[DRONE] MAC: %s | RSSI: %d dBm\n",
advertisedDevice.getAddress().toString().c_str(),
advertisedDevice.getRSSI());
}
}
};
void setup() {
Serial.begin(115200);
// Initialize I2C with explicit S3 pins
Wire.begin(I2C_SDA, I2C_SCL);
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed. Check I2C wiring."));
for(;;); // Halt if display fails
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("ESP32 RemoteID Scanner");
display.println("Init BLE...");
display.display();
// Initialize BLE Stack
BLEDevice::init("ESP32_ODID_Scanner");
pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true); // Active scan requests scan responses
pBLEScan->setInterval(100);
pBLEScan->setWindow(99); // Window must be <= Interval
}
void loop() {
// Start scan for 5 seconds, non-blocking
BLEScanResults foundDevices = pBLEScan->start(5, false);
// Update OLED
display.clearDisplay();
display.setCursor(0,0);
display.setTextSize(2);
display.printf("Drones: %d", droneCount);
display.setTextSize(1);
display.setCursor(0,30);
display.println("Scanning 2.4GHz BLE...");
display.display();
// CRITICAL: Clear results to free heap memory and prevent OOM crash
pBLEScan->clearResults();
delay(500); // Brief pause between scan cycles
}
Debugging: First Three Things to Check When It Fails
BLE scanning on the ESP32 is notoriously fragile if you don't manage the hardware stack correctly. If your scanner reboots randomly or freezes, check these three things in order:
1. The BLE Out-Of-Memory (OOM) Crash
Exact Error String: E (12345) BT_BLE: bta_dm_act BLE_SCAN_OUT_OF_MEMORY followed by a Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout).
The Fix: This is the most common ESP32 BLE failure. The internal BLE stack caches every advertisement it sees. If you don't explicitly clear the cache, the heap fragments and overflows within 3 minutes. Ensure pBLEScan->clearResults(); is called at the end of every loop iteration (as shown in the code above).
2. I2C OLED Initialization Hang
Symptom: Serial monitor prints SSD1306 allocation failed and the board halts, or the screen stays completely black.
The Fix: Verify your I2C address. While 0x3C is standard for 0.96" displays, many 1.3" SH1106 displays default to 0x3D. Run an I2C scanner sketch first. Also, confirm you are using GPIO 8 and 9; if you are using an original ESP32-WROOM-32 instead of the S3, you must change these to GPIO 21 (SDA) and 22 (SCL).
3. Zero Detections Despite Drones Flying
Symptom: Code compiles and runs, screen says "Scanning", but drone count stays at 0.
The Fix: The drone might be broadcasting via Wi-Fi Neighbor Awareness Networking (NAN) rather than BLE, which the ESP32 cannot sniff passively. Alternatively, the drone might be using Wi-Fi Direct. Ensure the drone is configured for "Standard Remote ID" via BLE in its companion app, and verify you are within 100 feet (BLE range drops off heavily past 30 meters).
Extending and Simplifying the Build
Once you have the basic scanner running on your bench, you can adapt it to fit your specific deployment needs.
How to Simplify (Headless Mode)
If you want to mount this in a weatherproof enclosure on your roof and power it via solar, ditch the OLED. Remove all Adafruit_SSD1306 and Wire includes, delete the display update blocks in the loop(), and rely entirely on the UART serial output. This drops the active RAM usage by about 15% and eliminates I2C bus lockups caused by long wire runs.
How to Extend (MQTT & NimBLE)
For a permanent smart-home integration, you should swap the legacy BLEDevice library for the NimBLE-Arduino library. NimBLE reduces the BLE stack memory footprint by nearly 40%, freeing up enough heap space to run a Wi-Fi client simultaneously.
With Wi-Fi active, you can add the PubSubClient library to push drone detections via MQTT to Home Assistant. Create an automation that flashes your porch lights red and sends a push notification to your phone whenever a drone enters a 50-meter radius of your property. To calculate the distance, use the RSSI value from the BLE payload and apply a standard log-distance path loss model, keeping in mind that 2.4GHz RF attenuates heavily through residential siding and trees.
Disclaimer: This project is for educational and passive monitoring purposes. Always consult the FAA Remote ID guidelines and local privacy laws regarding the logging and tracking of aircraft and operator data.






