How to Automate a Street Barrier with ESP32 BLE and Beacon Detection

Introduction: A Smarter Way to Open Our Shared Barrier

In our quiet private street, 20 households share a single automated barrier gate. For years, we relied on outdated remotes, NFC keyfobs, or even phone calls to let people in — all of which felt increasingly clunky as smart technology became more accessible.

I wanted something better: a system that could detect when a trusted car was approaching and open the gate automatically, without the driver doing anything.

This tutorial walks you through how I built such a system using the ESP32-C6, a low-cost BLE beacon, and a bit of clever signal filtering. It opens the gate when it detects a beacon assigned to a known license plate approaching — no pairing, no apps, no internet required.

You’ll learn how to:

  • Set up BLE scanning on the ESP32
  • Filter and whitelist trusted beacons
  • Avoid false triggers from passing signals
  • Visually debug with a small TFT display
  • Install the system discreetly inside the barrier pillar

If you’re building smart infrastructure for shared driveways, delivery access, or gated communities, this project might be exactly what you’re looking for.

Table of Contents

  1. Hardware Used
  2. System Overview and Features
  3. Code Structure and Logic Breakdown
  4. BLE vs Bluetooth Classic
  5. Whitelist and License Plate Matching
  6. Debugging with Onboard Display
  7. Preventing False Triggers with RSSI Trend Detection
  8. Installation Inside the Barrier Pillar
  9. Final BLE-Only Algorithm
  10. Ideas for Future Improvements

Hardware Used

For this project, I selected an ultra-compact ESP32 board with integrated display and USB-C power, paired with a Bluetooth beacon and a simple relay module. Here are the components used:

  • ESP32-C6 Dev Board with Embedded Display (Makerfabs)
    This dev board features an ESP32-C6 chip and a built-in 1.9-inch ST7789 color display (320×170 resolution). It connects over SPI internally and is ideal for debugging without any external modules. It also includes USB-C, a user button, and a clean pinout for GPIO access.
    AliExpress link: https://www.aliexpress.com/item/1005008207509770.html
  • BLE Beacon Tag (18mm, CR2032-powered)
    A small, round Bluetooth Low Energy tag that broadcasts advertisements every few seconds. Used as a passive proximity identifier — no pairing needed.
    AliExpress link: https://www.aliexpress.com/item/1005007101436265.html
  • Relay Module (3.3V logic)
    A single-channel relay used to open the gate. Controlled directly by a GPIO pin from the ESP32.
  • 12V to 5V Buck Converter
    Since the barrier control box supplies 12V DC, I used a small buck converter to power the ESP32 safely and continuously.
ESP32-C6 Dev Board with Embedded Display bottom view
BLE Beacon TAG official Advertisement image

All components fit inside the gate’s existing pillar box. Since the display is already embedded in the ESP32 module, there’s no extra wiring or screen mounting needed — just plug in power and connect the relay.

System Overview and Features

The goal of this system is to automatically open a shared street barrier when a known car approaches — without pressing a button, using a remote, or triggering anything manually. The solution uses Bluetooth Low Energy (BLE) to detect a small beacon tag inside the vehicle and controls a relay to open the gate.

Here’s how it works:

  • The ESP32-C6 module continuously scans for nearby BLE devices.
  • It checks the detected MAC addresses against a whitelist stored in flash memory (using Preferences).
  • If a beacon is recognized and its signal strength is rising (indicating approach), the ESP32 activates a relay for a set period (e.g., 5 seconds).
  • A cooldown timer prevents repeat triggers while the vehicle remains in range.
  • The onboard display provides real-time feedback — showing signal strength, detected license plate, and gate status.
  • A Wi-Fi Access Point mode allows configuration from any phone or laptop, including adding/removing trusted beacons and toggling screen/system behavior.
  • OTA updates are supported using ArduinoOTA for future firmware tweaks.

The entire system runs locally — no cloud, no pairing, and no mobile apps. It was installed directly inside the metal pillar box of the barrier where it connects to the gate control wiring and shares its 12V power supply.

This compact setup delivers fast, silent, and seamless gate automation based entirely on passive BLE scanning.

Code Structure and Logic Breakdown

// ==== INCLUDES ====

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <ArduinoOTA.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

// ==== DISPLAY SETTINGS ====

#define LCD_WIDTH   172
#define LCD_HEIGHT  320

#define PIN_SPI_MOSI 6
#define PIN_SPI_SCLK 7
#define PIN_SPI_CS   14
#define PIN_DC       15
#define PIN_RST      21
#define PIN_BLK      22

// ==== RELAY SETTINGS ====

#define RELAY_PIN 8
#define RELAY_ON_DURATION 5000           // Relay ON time (5s)
#define COOLDOWN_AFTER_TRIGGER 30000      // Cooldown after trigger (30s)

// ==== BLE SETTINGS ====

#define RSSI_BUFFER_SIZE 10
#define APPROACH_THRESHOLD 2
#define MIN_RSSI          -85

// ==== WIFI SETTINGS ====

const char* apSSID = "Barrier-Setup";
const char* apPassword = "12345678";

WebServer server(80);

// ==== GLOBAL OBJECTS ====

SPIClass spiCustom(FSPI);
Adafruit_ST7789 tft = Adafruit_ST7789(&spiCustom, PIN_SPI_CS, PIN_DC, PIN_RST);

BLEScan* pBLEScan;
Preferences preferences;

// ==== SYSTEM STATE ====

bool relayTriggered = false;
unsigned long relayStartTime = 0;
unsigned long cooldownUntil = 0;

bool systemEnabled = true;
bool screenEnabled = true;

String lastDisplayedStatus = "";

// ==== WHITELIST ====

struct WhitelistEntry {
  String mac;
  String license;
  int rssi;
  int lastRssi;
};

WhitelistEntry whitelistedDevices[10]; // Allow up to 10 devices
int numDevices = 0;

// PART 2

// ==== SETUP ====

void setup() {
  Serial.begin(115200);
  Serial.println("Booting Smart Barrier System...");

  // Init SPI and Display
  initDisplay();

  // Init Relay
  initRelay();

  // Init BLE
  initBLE();

  // Init WiFi AP
  initWiFi();

  // Init Web Server
  initWebServer();

  // Init OTA
  initOTA();

  // Load Preferences
  loadPreferences();
}

// Part 3
// ==== MAIN LOOP ====

void loop() {
  server.handleClient();
  if (systemEnabled) {
    scanBLE();
    updateRelayLogic();
    if (screenEnabled) {
      updateDisplay();
    }
  }

  
  // ArduinoOTA.handle();
}

// Part 4
// ==== INIT DISPLAY ====

void initDisplay() {
  spiCustom.begin(PIN_SPI_SCLK, -1, PIN_SPI_MOSI, PIN_SPI_CS);
  pinMode(PIN_BLK, OUTPUT);
  digitalWrite(PIN_BLK, HIGH);

  tft.init(LCD_WIDTH, LCD_HEIGHT);
  tft.setRotation(1);
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextColor(ST77XX_WHITE);
  tft.setTextSize(1);
}

// ==== INIT RELAY ====

void initRelay() {
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW);
}

// ==== INIT BLE ====

void initBLE() {
  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan();
  pBLEScan->setActiveScan(true);
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);
}

// ==== INIT WIFI (Access Point Mode) ====

void initWiFi() {
  WiFi.softAP(apSSID, apPassword);
  Serial.println("WiFi AP Started.");
  Serial.print("IP Address: ");
  Serial.println(WiFi.softAPIP());
}

// ==== INIT OTA ====

void initOTA() {
  ArduinoOTA.begin();
}

// Part 5

// ==== WEB SERVER SETUP ====

void initWebServer() {
  server.on("/", HTTP_GET, []() {
    String html = "<h1>Barrier Config</h1><form action='/save' method='POST'>";
    html += "System Enabled: <input type='checkbox' name='system' ";
    if (systemEnabled) html += "checked";
    html += "><br>Screen Enabled: <input type='checkbox' name='screen' ";
    if (screenEnabled) html += "checked";
    html += "><br><br><h3>Add Beacon</h3>";
    html += "MAC Address: <input name='mac'><br>License Plate: <input name='license'><br><br>";
    html += "<input type='submit' value='Save'>";
    html += "</form>";
    server.send(200, "text/html", html);
  });

  server.on("/save", HTTP_POST, []() {
    if (server.hasArg("system")) systemEnabled = true; else systemEnabled = false;
    if (server.hasArg("screen")) screenEnabled = true; else screenEnabled = false;
    if (server.hasArg("mac") && server.hasArg("license") && server.arg("mac") != "" && server.arg("license") != "") {
      if (numDevices < 10) {
        whitelistedDevices[numDevices].mac = server.arg("mac");
        whitelistedDevices[numDevices].license = server.arg("license");
        whitelistedDevices[numDevices].rssi = -100;
        whitelistedDevices[numDevices].lastRssi = -100;
        numDevices++;
      }
    }
    savePreferences();
    server.sendHeader("Location", "/");
    server.send(303);
  });

  server.begin();
}

// Party 6
// ==== BLE SCAN ====

void scanBLE() {
  Serial.println("\n--- BLE Scan Start ---");
  BLEScanResults* results = pBLEScan->start(1, false);

  // Reset previous RSSI
  for (int i = 0; i < numDevices; i++) {
    whitelistedDevices[i].lastRssi = whitelistedDevices[i].rssi;
    whitelistedDevices[i].rssi = -100;
  }

  // Process results
  for (int i = 0; i < results->getCount(); i++) {
    BLEAdvertisedDevice dev = results->getDevice(i);
    String mac = dev.getAddress().toString().c_str();
    int rssi = dev.getRSSI();

    int index = findDeviceIndex(mac);
    if (index != -1) {
      Serial.printf("Detected Whitelisted Device: %s (%d dBm)\n", mac.c_str(), rssi);
      whitelistedDevices[index].rssi = rssi;
    }
  }
}

// ==== RELAY LOGIC ====

void updateRelayLogic() {
  int bestRSSI = -100;
  String bestLicense = "";
  int trend = 0;

  // Find best device
  for (int i = 0; i < numDevices; i++) {
    if (whitelistedDevices[i].rssi > bestRSSI) {
      bestRSSI = whitelistedDevices[i].rssi;
      bestLicense = whitelistedDevices[i].license;
      trend = whitelistedDevices[i].rssi - whitelistedDevices[i].lastRssi;
    }
  }

  Serial.printf("Best Device: %s RSSI=%d Trend=%d\n", bestLicense.c_str(), bestRSSI, trend);

  if (relayTriggered) {
    // Do nothing — relay is ON
  } else if ((millis() > cooldownUntil) && (trend > APPROACH_THRESHOLD) && (bestRSSI > MIN_RSSI)) {
    Serial.println("Triggering Relay...");
    triggerRelay();
  }

  // Turn off relay after ON duration
  if (relayTriggered && (millis() - relayStartTime > RELAY_ON_DURATION)) {
    Serial.println("Relay timeout, turning OFF");
    digitalWrite(RELAY_PIN, LOW);
    relayTriggered = false;
  }
}

// ==== DISPLAY ====

void updateDisplay() {
  if (relayTriggered) {
    showBarrierOpenScreen();
  } else {
    showWaitingScreen();
  }
}


// Part 7
void showWaitingScreen() {
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextColor(ST77XX_WHITE);
  tft.setTextSize(2);
  tft.setCursor(10, 10);
  tft.println("Waiting...");

  tft.setTextSize(1);

  for (int i = 0; i < numDevices; i++) {
    tft.setCursor(10, 60 + i * 30);
    tft.print(whitelistedDevices[i].license);
    tft.print(" -> ");
    if (whitelistedDevices[i].rssi > -100) {
      tft.print(whitelistedDevices[i].rssi);
      tft.print(" dBm (");
      int trend = whitelistedDevices[i].rssi - whitelistedDevices[i].lastRssi;
      if (trend >= 0) tft.print("+");
      tft.print(trend);
      tft.print(")");
    } else {
      tft.print("-- dBm");
    }
  }
}

void showBarrierOpenScreen() {
  tft.fillScreen(tft.color565(0, 100, 0));  // Dark green
  tft.setTextColor(ST77XX_WHITE);
  tft.setTextSize(2);
  tft.setCursor(10, 60);
  tft.println("Barrier Open!");
}

// ==== RELAY TRIGGER ====

void triggerRelay() {
  digitalWrite(RELAY_PIN, HIGH);
  relayTriggered = true;
  relayStartTime = millis();
  cooldownUntil = millis() + COOLDOWN_AFTER_TRIGGER;
}

// Part 8

void savePreferences() {
  preferences.begin("barrier", false);
  preferences.putBool("systemEnabled", systemEnabled);
  preferences.putBool("screenEnabled", screenEnabled);
  preferences.putInt("numDevices", numDevices);

  for (int i = 0; i < numDevices; i++) {
    String keyMac = "mac" + String(i);
    String keyLicense = "license" + String(i);
    preferences.putString(keyMac.c_str(), whitelistedDevices[i].mac);
    preferences.putString(keyLicense.c_str(), whitelistedDevices[i].license);
  }

  preferences.end();
}

void loadPreferences() {
  preferences.begin("barrier", true);
  systemEnabled = preferences.getBool("systemEnabled", true);
  screenEnabled = preferences.getBool("screenEnabled", true);
  numDevices = preferences.getInt("numDevices", 0);

  if (numDevices > 10) numDevices = 0;

  for (int i = 0; i < numDevices; i++) {
    String keyMac = "mac" + String(i);
    String keyLicense = "license" + String(i);
    whitelistedDevices[i].mac = preferences.getString(keyMac.c_str(), "");
    whitelistedDevices[i].license = preferences.getString(keyLicense.c_str(), "");
    whitelistedDevices[i].rssi = -100;
    whitelistedDevices[i].lastRssi = -100;
  }

  preferences.end();
}

// ==== FIND DEVICE ====

int findDeviceIndex(String mac) {
  for (int i = 0; i < numDevices; i++) {
    if (mac.equalsIgnoreCase(whitelistedDevices[i].mac)) {
      return i;
    }
  }
  return -1;
}

To keep development manageable and the project scalable, I divided the code into logical parts. This modular approach made debugging easier and allows for individual components (like display or web config) to be turned on or off later without breaking functionality.

Part 1: Global Settings and Includes
This part defines all the libraries, BLE parameters, relay timing values, and screen configuration. It also includes global structures like the whitelist array for storing known beacon MAC addresses with their associated license plates.

Part 2: Setup Function
The setup() routine initializes all critical components:

  • SPI and screen (displayed messages, colors, font sizes)
  • Relay pin as output
  • BLE scanning in active mode
  • Wi-Fi Access Point with predefined SSID/password
  • Web server endpoints
  • OTA update service
  • Loading saved preferences (like system state, screen toggle, and whitelist)

Part 3: Main Loop
The loop() checks for BLE activity, display updates, OTA connections, and web server requests. It also enforces cooldowns and relay timeouts based on system state.

Part 4: BLE Scanning
The ESP32 starts a BLE scan every second. For each detected device, it compares the MAC address to the whitelist. If a match is found:

  • It logs the current RSSI (signal strength)
  • Stores the last known RSSI for comparison (to detect trends)
  • Updates the screen with RSSI info

Part 5: Relay Control Logic
To prevent false triggers, the system checks:

  • Is the detected RSSI above the minimum threshold (e.g., -85 dBm)?
  • Is the signal getting stronger (meaning the car is approaching)?
  • Is the system not currently in cooldown?

If all conditions are true, the relay is activated for a set time (e.g., 5 seconds) to open the barrier. After that, a cooldown period (e.g., 30 seconds) prevents retriggers.

Part 6: Web Configuration UI
A minimal HTML form served over Wi-Fi allows the user to:

  • Enable or disable the system
  • Turn the display on or off
  • Add new MAC addresses and license labels
  • View and manage the whitelist

All changes are stored using Preferences so they persist across reboots.

Part 7: Display Output
The onboard display is used to:

  • Show real-time RSSI values for each detected and whitelisted beacon
  • Indicate “Barrier Open” when the relay is triggered
  • Help with tuning thresholds and placement during installation

Part 8: EEPROM-Like Preferences
The ESP32 uses the Preferences library to store system state, screen settings, and whitelist data. On boot, it automatically loads saved data and initializes the array of trusted beacon MACs.

Part 9: Helper Functions
Small functions are used to:

  • Trigger and reset the relay
  • Compare MAC addresses to whitelist entries
  • Save and retrieve structured data
  • Handle form submission and redirects from the web UI

BLE vs Bluetooth Classic

Although both BLE (Bluetooth Low Energy) and Classic Bluetooth are available on many devices, only BLE is suitable for this project — especially when you’re building a passive detection system that operates without pairing or manual interaction.

Here’s how the two compare in the context of an ESP32-based gate automation system:

Power Consumption
BLE was designed for ultra-low-power devices like beacons, fitness trackers, and sensors. It sends small bursts of data and then goes back to sleep. A BLE beacon can run for months (or even over a year) on a single CR2032 battery. Classic Bluetooth, on the other hand, draws significantly more power and isn’t ideal for something that’s always broadcasting.

Connection Requirements
BLE uses advertising packets — broadcasts that can be picked up by nearby devices without ever connecting. This is what allows our ESP32 to scan and detect beacons passively. Classic Bluetooth requires pairing and maintaining a connection, which is overkill for simply recognizing a device’s presence.

Latency and Setup Time
BLE scanning happens fast and doesn’t require the device to be visible, discoverable, or user-activated. In contrast, Classic Bluetooth often takes seconds to pair and handshake — unacceptable when you’re pulling up to a gate and expecting it to open immediately.

Range and Stability
While Classic Bluetooth can sometimes offer longer range, BLE’s range is more than sufficient — especially when tuned with proper RSSI thresholds. Plus, BLE’s modern design makes it more resilient to signal collisions and dropouts in dense environments.

Use Case Alignment

  • BLE: Ideal for smart locks, proximity sensors, keyless entry systems
  • Classic Bluetooth: Ideal for wireless headphones, keyboards, and file transfers

In short, BLE fits this application perfectly: it allows the beacon to simply exist and advertise, while the ESP32-C6 listens in the background — silently and efficiently — and reacts only when a trusted signal is approaching.

Whitelist and License Plate Matching

To ensure that the barrier only opens for authorized vehicles, the system uses a MAC address whitelist combined with optional license plate tags for human-friendly reference. This whitelist is stored persistently using the ESP32’s built-in flash memory via the Preferences library.

How It Works

Each BLE beacon has a unique MAC address. When the ESP32-C6 scans for BLE devices, it compares the MAC of each detected advertisement packet against the whitelist. If a match is found, the system continues processing that beacon’s RSSI value to determine proximity and approach.

Every trusted device in the system is stored in a simple data structure:

  • MAC address (e.g., A4:C1:38:XX:YY:ZZ)
  • License plate string (e.g., "B-123-XYZ") — used only for display/debugging
  • RSSI (signal strength from current scan)
  • Last RSSI (from previous scan, used to detect approach trend)

Adding New Devices

New devices can be added through the onboard web interface:

  • Connect to the ESP32’s Wi-Fi Access Point (e.g., Barrier-Setup)
  • Open the browser at the displayed IP (usually 192.168.4.1)
  • Enter the beacon’s MAC address and optional license plate label
  • Click “Save” to update the whitelist

Once saved, the ESP32 stores this data using Preferences, so it survives restarts or power cycles.

Limiting Access

The code currently supports up to 10 whitelisted beacons, but this can be expanded with more complex storage structures (like SPIFFS or LittleFS) if needed.

By using the MAC + RSSI combination, the system ensures that:

  • Only known tags can trigger the relay
  • And only when within expected proximity (via RSSI filtering)

This provides a basic yet robust access control mechanism without the need for a central server, cloud authentication, or pairing.

Debugging with Onboard Display

One of the key advantages of using the Makerfabs ESP32-C6 dev board is that it includes a built-in 1.9-inch ST7789 color display (320×170 pixels). This makes field debugging far easier — no need to connect serial monitors or external screens.

What the Display Shows

When enabled, the screen provides real-time insight into the system’s behavior, including:

  • System Status – Whether the gate is in “waiting” or “triggered” mode
  • Detected Devices – A list of whitelisted beacon MACs and their associated license plate labels
  • Signal Strength (RSSI) – For each beacon, the current and previous RSSI are displayed
  • Trend Direction – Shows whether a signal is increasing (approaching) or decreasing (moving away)

Example display layout:

ESP32 with embedded display showing detection screen in automatic BLE barrier detection system
Waiting...
B-123-XYZ -> -69 dBm (+5)
G-987-KLM -> -- dBm

When a trigger occurs, the screen switches to a clear confirmation message:

ESP32 with embedded display showing success vehicle detected screen in automatic BLE barrier detection system
Barrier Open!

This visual feedback helps fine-tune RSSI thresholds, placement of the ESP32 board inside the pillar, and verify the beacon’s responsiveness in real time.

Optional: Disabling the Display

If you plan to run the system long-term without visual output, the display can be turned off in two ways:

  • Via the web interface (toggle screen off)
  • In code, by setting a screenEnabled flag to false

Disabling the display slightly reduces power usage and heat, although with the screen embedded and powered over SPI, the impact is minimal.

In development and testing phases, however, the screen is a huge time-saver — especially when you’re working solo and need confirmation that the system is recognizing your tag as you approach.

Preventing False Triggers with RSSI Trend Detection

One of the biggest challenges with BLE-based detection systems is dealing with false positives — when a valid beacon is detected but the system misinterprets it as an approach. This can happen due to signal bouncing, reflection from surfaces, or even the beacon being present but stationary inside a parked car.

To improve reliability, I implemented a simple yet effective filtering strategy based on RSSI trend detection.

Why RSSI Alone Isn’t Enough

Received Signal Strength Indicator (RSSI) tells us how “strong” the BLE signal is. While a higher RSSI often means the device is closer, it’s not always reliable on its own. BLE signals can fluctuate rapidly due to environmental noise, interference, or physical obstacles.

Without filtering, the gate might open every time the car’s beacon is detected — even if the car isn’t moving.

The Trend-Based Approach

Instead of just checking if RSSI is above a threshold (e.g., -85 dBm), the system also compares current RSSI with last RSSI. If the signal is getting stronger over time, it likely means the beacon is approaching the ESP32.

Here’s how the logic works:

  1. Scan BLE devices every second
  2. For each whitelisted beacon:
    • Record the current RSSI
    • Compare it with the previous RSSI
    • Calculate the trend (difference)
  3. If the trend is positive (e.g., +5 dBm) and current RSSI is above the threshold:
    • Trigger the relay
    • Start a cooldown period to avoid repeated activation

This eliminates most cases where the beacon is nearby but not actually approaching — such as when someone is parked near the gate or walking by with a tag in their pocket.

Adjustable Parameters

You can fine-tune these values in code:

#define MIN_RSSI -85                // Minimum strength to consider triggering
#define APPROACH_THRESHOLD 2 // Minimum RSSI increase to count as 'approaching'
#define RELAY_ON_DURATION 5000 // Relay active time in milliseconds
#define COOLDOWN_AFTER_TRIGGER 30000 // Time before another trigger is allowed

These values worked well in my setup, but depending on your environment (e.g., concrete walls, metal enclosures), you might need to adjust them slightly.

By combining signal strength with directional trend, the system becomes much smarter — opening the barrier only when it’s actually needed.

Installation Inside the Barrier Pillar

Unlike outdoor IoT installations that require custom enclosures or waterproofing, this system benefits from being mounted directly inside the existing barrier’s pillar box — where the gate electronics and power supply are already located.

Why Install Inside the Pillar?

  • Stable Power Source
    The barrier box already provides 12V DC, which can be stepped down using a compact buck converter to safely power the ESP32-C6 board.
  • Physical Protection
    The internal housing protects the ESP32 module from sunlight, rain, wind, and accidental tampering — no need for additional weatherproofing.
  • Wiring Convenience
    Since the relay is used to simulate a button press or gate trigger, placing the ESP32 near the barrier control board makes wiring short and simple.
  • Good BLE Range
    Despite being enclosed, the pillar box often includes air vents, plastic covers, or non-metallic access panels. BLE signal from the beacon can still be received reliably within a 5–8 meter range, especially when the beacon is inside a car dashboard or glove box.

Practical Mounting Notes

  • The Makerfabs ESP32-C6 board includes onboard mounting holes. You can screw it to a spacer or clip it inside a small utility box within the pillar.
  • Keep the ESP32 module away from high-voltage terminals inside the barrier — route power and relay control through the low-voltage section.
  • The onboard display is helpful during installation — you can immediately see whether the system detects your beacon and how strong the signal is.

Tip: During installation, park the car at different distances and observe the RSSI values on-screen. This helps you set the correct detection threshold and avoid early or delayed openings.

Final BLE-Only Algorithm

After several rounds of testing, I refined the logic to a minimal, reliable algorithm that uses only BLE scanning, a whitelist check, RSSI filtering, and trend detection — with no need for Wi-Fi, cloud services, or external apps.

Here’s a summary of how the final system works in real-time:

Core Loop Behavior

  1. Every 1 second, the ESP32 starts a BLE scan.
  2. For each discovered device:
    • Check if the MAC address matches an entry in the whitelist.
    • Record the current RSSI and compare it with the previous value.
  3. If the device is whitelisted and:
    • The RSSI is above the minimum threshold (e.g., -85 dBm)
    • The RSSI is rising (trend is positive)
    • The system is not in cooldown
  4. Then:
    • Trigger the relay (simulate gate open)
    • Display “Barrier Open!” on screen
    • Start a 30-second cooldown timer to prevent repeat activations

Example Pseudocode Summary

if (isWhitelisted(mac)) {
if (rssi > MIN_RSSI && (rssi - lastRssi) > APPROACH_THRESHOLD) {
if (millis() > cooldownUntil) {
triggerRelay();
cooldownUntil = millis() + COOLDOWN_AFTER_TRIGGER;
}
}
}

Key Advantages

  • Offline Operation
    No internet connection, no pairing — just passive detection and reaction.
  • Minimal Power Draw
    Wi-Fi is disabled entirely. The display can also be toggled off, reducing heat.
  • Quick and Seamless Trigger
    From the time the beacon gets in range, the gate opens in less than 2 seconds.
  • Customizable Settings
    All key parameters (thresholds, timers) can be changed easily via code or the web UI.

This algorithm keeps things fast, lightweight, and highly reliable — exactly what you want when automating something as critical as access control.

Ideas for Future Improvements

While the current BLE-based gate automation system has been running reliably for weeks, there are several features that could make it even more robust, scalable, or user-friendly — depending on your environment and technical needs.

1. Serial-Based Whitelist Management

For those who prefer not to use a web interface, adding a simple serial console menu (via USB-C) could allow administrators to:

  • View current whitelist entries
  • Add or remove MAC addresses
  • Reset preferences to defaults

This would be especially useful during installation or debugging, even without a Wi-Fi device nearby.

2. MQTT or Logging Support

If Wi-Fi were to be re-enabled selectively (without overheating), the ESP32 could publish:

  • Detection events (e.g., “Beacon ABC123 triggered gate”)
  • RSSI logs for tuning
  • Uptime statistics or self-checks

These could be sent via MQTT to Home Assistant, Node-RED, or a simple local dashboard.

3. Over-the-Air Whitelist Syncing

In a shared neighborhood scenario, syncing a common whitelist across multiple gates could improve consistency. This would require a small backend or shared storage (Google Sheets, Firebase, or even GitHub JSON files).

4. Environmental Sensing

Adding a temperature or voltage sensor could help the system:

  • Avoid overheating (throttle scanning or display)
  • Detect brownout or unstable power
  • Schedule restarts if anomalies are detected

5. Mobile App or NFC Alternative

While BLE beacons are ideal for passive operation, you could offer an optional “tap to open” feature using:

  • NFC tags scanned by a phone
  • BLE beacon toggles via a mobile app
  • CarPlay or Android Auto integrations (for higher-end users)

These would be fallback methods, not replacements for the core logic.

6. Expanded Relay Modes

In some installations, users might want:

  • Momentary vs. latching relay behavior
  • Trigger based on multiple beacons arriving simultaneously (e.g., for group access)
  • Delayed closing or integration with magnetic lock sensors

These logic paths can be added modularly with minimal changes to the existing structure.

1 thought on “How to Automate a Street Barrier with ESP32 BLE and Beacon Detection”

Leave a Comment