When designing a wireless node around the Espressif ESP32, choosing between WiFi, Bluetooth Low Energy (BLE), and Classic Bluetooth dictates your power budget, range, and hardware layout. The direct verdict: Use WiFi (802.11) for high-bandwidth, mains-powered applications requiring direct IP routing. Use BLE 5.0 for battery-operated sensor nodes transmitting small payloads over medium distances. Use Classic Bluetooth strictly for legacy audio (A2DP) or continuous serial streams (SPP). If you need low-latency, long-range mesh without the overhead of IP, look into the proprietary ESP-NOW protocol.

Unlike wired buses (I2C, SPI) where the physical layer is just copper traces and pull-up resistors, wireless protocols demand strict RF layout rules and aggressive power supply decoupling. This primer breaks down the ESP32's wireless stacks from the physical layer up to the application exchange.

RF Bus Mechanics: Speed, Range, and Addressing

To select the right protocol, you need to look past the marketing PHY (physical layer) speeds and examine real-world throughput and addressing limits. The table below maps the core wireless protocols available on the standard ESP32-WROOM-32 and newer variants like the ESP32-C3.

ESP32 Wireless Protocol Specification Matrix
Protocol Max PHY Rate Real-World Throughput Addressing Scheme Max Range (Open / Indoor) Peak TX Current
WiFi 802.11n (2.4GHz) 72 Mbps ~15-20 Mbps (TCP) MAC / IPv4 / IPv6 80m / 25m ~500 mA
BLE 5.0 2 Mbps ~100 kbps (GATT) UUID / 48-bit MAC 100m / 30m ~130 mA
BT Classic (EDR) 3 Mbps ~2.1 Mbps (SPP) MAC / PIN Pairing 40m / 15m ~300 mA
ESP-NOW (Proprietary) 1 Mbps ~700 kbps (UDP-like) MAC Address Only 150m / 40m ~350 mA
Decision Framework:
  • Device Count: WiFi routers typically choke past 30-50 connected ESP32s due to DHCP and ARP table limits. BLE central devices struggle past 7 simultaneous connections. For 100+ nodes, use ESP-NOW or WiFi with MQTT and static IPs.
  • Distance vs Speed: If you need 100+ meters and only send 50 bytes of sensor data, BLE or ESP-NOW wins. If you need to stream 1080p video (requiring an ESP32-S3 with PSRAM), WiFi is the only option.

Physical Layer: Decoupling, Antennas, and Keep-Out Zones

In wired protocols, a missing I2C pull-up resistor causes a bus hang. In ESP32 wireless design, the equivalent 'physical layer' failure is a power brownout during RF transmission. When the ESP32 fires up the WiFi PA (Power Amplifier), it draws up to 500mA in microseconds. If your 3.3V LDO cannot respond fast enough, the core voltage dips below 2.7V, triggering a hardware brownout reset.

The Mandatory Power Decoupling Rule

Never rely solely on the LDO's output capacitor. You must place a 10µF bulk ceramic capacitor and a 100nF high-frequency bypass capacitor as close to the ESP32's VDD33 and GND pins as physically possible. According to the Espressif Hardware Design Guidelines, the power supply must be capable of delivering 500mA peak current with a voltage ripple of less than 50mV.

RF Antenna Keep-Out Zones

If you are designing a custom PCB using the ESP32's onboard inverted-F antenna (PCB trace antenna), the area directly beneath and immediately surrounding the antenna must be completely clear of copper, ground planes, and traces on all layers. Placing a ground plane under the antenna detunes the impedance from 50 ohms, converting your RF energy into heat rather than radiated signal. If your enclosure is metal or you need external routing, specify an ESP32 module with a U.FL (IPEX) connector and use a certified 2.4GHz external antenna.

Sniffing and Debugging the Invisible Bus

Debugging wireless protocols requires looking at the airwaves, not just the serial monitor. Here is how to diagnose the classic failures for each protocol.

Classic Failure 1: WiFi IP Clashes and DHCP Timeouts

Symptom: The ESP32 connects to the AP, gets an IP, but drops offline every few minutes, or two devices fight for the same IP.

Fix: Do not use default DHCP for static IoT deployments. Assign static IPs in your router's DHCP reservation table based on the ESP32's MAC address. To debug packet drops, use Wireshark with a secondary ESP32 flashed with the esp_wifi_sniffer example from the ESP-IDF. Put the sniffer ESP32 into promiscuous mode to capture raw 802.11 management frames and see exactly when the AP sends a Deauthentication frame.

Classic Failure 2: BLE GATT Connection Drops

Symptom: Your phone connects to the ESP32 BLE server, but disconnects after exactly 30 seconds, or fails to receive notifications.

Fix: This is almost always a missing 'Connection Parameter Update' or an MTU (Maximum Transmission Unit) mismatch. By default, BLE 4.2 MTU is 23 bytes (3 bytes overhead = 20 bytes payload). If your ESP32 tries to send a 50-byte string without negotiating a higher MTU, the stack drops the connection. Use the nRF Connect mobile app to sniff the GATT exchange. It will show you the exact negotiated MTU and allow you to manually trigger connection parameter updates to test stability.

Minimal Working Exchange: BLE GATT Server

Below is a minimal, robust BLE GATT server implementation for the ESP32 using the Arduino core. This code sets up a service, exposes a characteristic, and handles the physical layer status via an onboard LED.

Hardware Context: This code assumes an ESP32-DevKitV1. Wire a standard LED to GPIO 2 (the default boot LED) to visualize connection state. Ensure your USB cable is rated for data and power, and that your PC's USB port can supply at least 500mA to prevent brownouts during BLE advertising spikes.

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

// UUIDs for the Service and Characteristic (Generate unique ones for production)
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

const int STATUS_LED = 2; // GPIO 2 on most ESP32 DevKits
bool deviceConnected = false;

class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    digitalWrite(STATUS_LED, HIGH); // LED ON when connected
  }

  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    digitalWrite(STATUS_LED, LOW);  // LED OFF when disconnected
    // Restart advertising on disconnect
    pServer->startAdvertising(); 
  }
};

void setup() {
  Serial.begin(115200);
  pinMode(STATUS_LED, OUTPUT);
  
  // Initialize BLE with device name
  BLEDevice::init("Flux-Sensor-Node-01");
  
  // Create the BLE Server
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

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

  // Create a BLE Characteristic (Read | Write | Notify)
  BLECharacteristic *pCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_WRITE |
    BLECharacteristic::PROPERTY_NOTIFY
  );

  // Set initial value
  pCharacteristic->setValue("System Ready");
  
  // Start the service and advertising
  pService->start();
  pServer->getAdvertising()->start();
  Serial.println("Waiting for BLE connection...");
}

void loop() {
  if (deviceConnected) {
    // Example: Read a sensor and notify the client
    // In a real build, read an ADC pin or I2C sensor here
    int mockSensorValue = analogRead(34); 
    String payload = "ADC:" + String(mockSensorValue);
    
    // pCharacteristic->setValue(payload.c_str());
    // pCharacteristic->notify();
    
    delay(1000); // Throttle notifications to avoid stack overflow
  }
  delay(10);
}

Verifying the Exchange

To test this without writing a companion mobile app, download nRF Connect for Mobile (iOS/Android). Scan for 'Flux-Sensor-Node-01', connect, and expand the generic attribute service. You can write hex values to the characteristic and read the 'System Ready' string. If the ESP32 fails to advertise, check your serial monitor for E (xxxx) BT: bta_dm_act BTA_DM_PM_BTIMER_ERR—this usually indicates the RF calibration failed at boot due to a noisy power supply or an obstructed antenna.