How to connect to your ESP32 - Now we're talking

Connectivity methods with code examples.

February 2, 2026
12 min read
0 views
#ESP32#IoT#Connectivity#WiFi#Bluetooth#BLE

I recently worked on a project using the ESP32 microcontroller. It needed to read values from sensors and ship them off to other devices. After trying a handful of connectivity methods to see what fits which scenario, I documented the patterns so others can drop them into their own projects.

A quick note on chip variants - radio support differs across the family:

  • Original ESP32: WiFi + Bluetooth Classic + BLE
  • ESP32-S3 / C3: WiFi + BLE only (no Classic Bluetooth)
  • ESP32-S2: WiFi only (no Bluetooth at all)

The WiFi sections below work on every variant. The Bluetooth Classic section needs the original ESP32. The BLE section works on every BLE-enabled variant.

Overview of Connectivity Methods

  • Router WiFi: Joins an existing network to talk to local servers or the internet
  • Firebase: Cloud-based real-time database with cross-device sync
  • Hotspot Mode: ESP32 hosts its own access point — clients connect directly
  • Bluetooth Classic: Reliable short-range link to a paired host
  • Bluetooth Low Energy (BLE): Energy-efficient pub/sub-style notifications

Quick Decision Guide

Pick by where the receiving end lives and what infrastructure you have:

MethodRangeInfrastructureBest for
Router WiFiWhatever your Wi-Fi coversExisting router + a serverSending to a local or internal server
FirebaseAnywhere with internetInternet + Firebase projectCloud dashboards, multi-device sync
Hotspot (AP)Typical Wi-Fi rangeNoneStandalone setups, no router around
Bluetooth Classic~10 m typicalPaired host (PC/phone)Streaming serial-style data to a PC
BLE~10 m typical, longer with BT5 LE Coded PHYBLE central (phone/PC)Phone apps, battery-powered nodes

The Power column was deliberately dropped - every WiFi-based option uses the same radio at similar instantaneous draw, so the practical differences come from your duty cycle (sleep schedule), not the connectivity choice. BLE is the genuine outlier here: lower peak draw and more aggressive sleep states.

Required Libraries

WiFi (ESP32 built-in)
WebServer (ESP32 built-in)
HTTPClient (ESP32 built-in)
BluetoothSerial (ESP32 built-in, original ESP32 only)
BLEDevice / BLEServer / BLEUtils (ESP32 built-in, BLE-enabled variants)
FirebaseClient by Mobizt (v2.1.5)
ArduinoJson by Benoit Blanchon (v7.4.2)

Python requirements (for the host-side examples):

pip install flask pyserial firebase-admin bleak

1. Router WiFi Connection

The ESP32 joins your existing Wi-Fi network and POSTs JSON to a server you control.

ESP32 Implementation

#include <WiFi.h>
#include <HTTPClient.h>

const char *ssid = "YOUR_WIFI_SSID";
const char *password = "YOUR_WIFI_PASSWORD";
const char *serverURL = "http://X.X.X.X:5000/data";

WiFiClient client;
HTTPClient http;

void setup() {
    Serial.begin(115200);

    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(1000);
        Serial.println("Connecting to WiFi...");
    }

    Serial.println("WiFi connected");
    Serial.print("IP: ");
    Serial.println(WiFi.localIP());
}

void sendData() {
    if (WiFi.status() == WL_CONNECTED) {
        http.begin(client, serverURL);
        http.addHeader("Content-Type", "application/json");

        String payload = "{";
        payload += "\"device\":\"ESP32-001\",";
        payload += "\"value\":42,"; // Replace with your sensor reading
        payload += "\"timestamp\":" + String(millis());
        payload += "}";

        int httpCode = http.POST(payload);

        if (httpCode == 200) {
            Serial.println("HTTP Success: " + String(httpCode));
        } else {
            Serial.println("HTTP Failed: " + String(httpCode));
        }

        http.end();
    }
}

PC Server Application (Flask)

from flask import Flask, request, jsonify
from datetime import datetime

app = Flask(__name__)
received_data = []

@app.route('/data', methods=['POST'])
def receive_data():
    try:
        data = request.get_json()
        timestamp = datetime.now().strftime('%H:%M:%S')
        data['received_at'] = timestamp

        received_data.append(data)
        print(f"[{timestamp}] Received: {data}")

        # Keep only last 100 readings
        if len(received_data) > 100:
            received_data.pop(0)

        return jsonify({"status": "success"}), 200

    except Exception as e:
        return jsonify({"status": "error", "message": str(e)}), 400

@app.route('/status', methods=['GET'])
def get_status():
    return jsonify({
        "status": "running",
        "total_received": len(received_data),
        "latest": received_data[-1] if received_data else None
    })

if __name__ == '__main__':
    # debug=True is for development only — disable in production
    app.run(host='0.0.0.0', port=5000)

Security note: plain HTTP exposes your payload to anyone on the network. For anything beyond a lab, swap WiFiClient for WiFiClientSecure and use HTTPS, validate the inbound JSON server-side, and don't ship Wi-Fi credentials in the binary.

2. Firebase Cloud Integration

Firebase Realtime Database gives you device → cloud → multi-client sync without standing up your own server.

ESP32 Firebase Implementation

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

#define WIFI_SSID "YOUR_WIFI_SSID"
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD"
#define API_KEY "YOUR_FIREBASE_API_KEY"
#define DATABASE_URL "YOUR_DATABASE_URL"

void sendToFirebase() {
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        String path = "/readings/" + String(millis()) + ".json";
        String url = DATABASE_URL + path;

        http.begin(url);
        http.addHeader("Content-Type", "application/json");

        String payload = "{";
        payload += "\"device\":\"ESP32-001\",";
        payload += "\"value\":42,"; // Replace with your sensor reading
        payload += "\"timestamp\":" + String(millis());
        payload += "}";

        int httpCode = http.PUT(payload);

        if (httpCode > 0) {
            Serial.println("Firebase Success: " + String(httpCode));
        } else {
            Serial.println("Firebase Failed: " + String(httpCode));
        }

        http.end();
    }
}

Python Firebase Monitor

import firebase_admin
from firebase_admin import credentials, db
import time

cred = credentials.Certificate("path/to/serviceAccountKey.json")
firebase_admin.initialize_app(cred, {
    'databaseURL': 'YOUR_DATABASE_URL'
})

def listen_for_data():
    ref = db.reference('/readings')

    def listener(event):
        if event.data:
            timestamp = time.strftime('%H:%M:%S')
            print(f"[{timestamp}] New data received:")
            print(f"  Device: {event.data.get('device', 'Unknown')}")
            print(f"  Value:  {event.data.get('value', 'N/A')}")
            # Process additional fields here

    ref.listen(listener)

if __name__ == '__main__':
    print("Firebase monitor started...")
    listen_for_data()
    input("Press Enter to stop...")

Security note: Firebase databases ship with permissive defaults — {"rules": {".read": true, ".write": true}} lets anyone with the URL read or wipe your data. At minimum, lock writes to authenticated clients (Firebase Auth + ID-token verification) and scope /readings to a UID path. Treat the database URL and service-account key like passwords.

3. Hotspot Mode (Access Point)

The ESP32 hosts its own Wi-Fi network. Clients connect directly - no router needed.

ESP32 Hotspot Implementation

#include <WiFi.h>
#include <WebServer.h>

const char *ssid = "ESP32-Sensor";
const char *password = "esp32pass";

WebServer server(80);
unsigned long readingCount = 0;

void setup() {
    Serial.begin(115200);

    WiFi.softAP(ssid, password);
    Serial.println("AP Started");
    Serial.print("IP: ");
    Serial.println(WiFi.softAPIP());

    server.on("/", handleRoot);
    server.on("/data", handleData);
    server.begin();
}

void handleRoot() {
    server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");

    String html = "<!DOCTYPE html><html><head>";
    html += "<title>ESP32 Monitor</title>";
    html += "<meta http-equiv='refresh' content='2'>";
    html += "</head><body>";
    html += "<h1>ESP32 Data</h1>";
    html += "<p>Value: 42</p>"; // Replace with your sensor reading
    html += "<p>Reading Count: " + String(readingCount) + "</p>";
    html += "</body></html>";

    server.send(200, "text/html", html);
}

void handleData() {
    String json = "{";
    json += "\"value\":42,"; // Replace with your sensor reading
    json += "\"readingCount\":" + String(readingCount);
    json += "}";

    server.send(200, "application/json", json);
}

void loop() {
    server.handleClient();
    readingCount++;
    delay(1000); // Adjust to match your sampling rate
}

Browser-Based Monitoring

  1. Connect to Wi-Fi network: "ESP32-Sensor"
  2. Enter password: "esp32pass"
  3. Navigate to the IP printed on the serial monitor
  4. The page auto-refreshes every 2 seconds

Security note: WPA2 is the floor - never run an open AP outside a controlled environment. The password lives in source here for clarity, but in real builds load it from NVS or a config file you don't commit.

4. Bluetooth Classic Communication

Reliable short-range link to a paired PC. Note: Bluetooth Classic works on the original ESP32.

ESP32 Bluetooth Implementation

#include "BluetoothSerial.h"

BluetoothSerial SerialBT;

unsigned long lastReading = 0;
unsigned long readingCount = 0;

void setup() {
    Serial.begin(115200);
    SerialBT.begin("ESP32-Sensor");
    Serial.println("Bluetooth Started! Device name: ESP32-Sensor");
    Serial.println("Ready to pair...");
}

void sendBluetoothData() {
    unsigned long readingAge = millis() - lastReading;

    String payload = "{";
    payload += "\"device\":\"ESP32-001\",";
    payload += "\"reading\":" + String(readingCount) + ",";
    payload += "\"value\":42,"; // Replace with your sensor reading
    payload += "\"timestamp\":" + String(lastReading) + ",";
    payload += "\"age\":" + String(readingAge);
    payload += "}\n";

    SerialBT.print(payload);
    Serial.println("BT sent: " + payload);
}

void loop() {
    lastReading = millis();
    readingCount++;
    sendBluetoothData();
    delay(1000);
}

Python Bluetooth Receiver

import serial.tools.list_ports
import json
import time
from datetime import datetime

class BluetoothReceiver:
    def __init__(self):
        self.connection = None
        self.received_data = []

    def list_bluetooth_ports(self):
        ports = serial.tools.list_ports.comports()
        return [p for p in ports if "bluetooth" in p.description.lower()]

    def connect(self, port_name, baudrate=115200):
        try:
            import serial
            self.connection = serial.Serial(port_name, baudrate, timeout=1)
            print(f"Connected to {port_name}")
            return True
        except Exception as e:
            print(f"Failed to connect: {e}")
            return False

    def listen_for_data(self):
        print("Listening for Bluetooth data...")

        try:
            while True:
                if self.connection.in_waiting > 0:
                    line = self.connection.readline().decode('utf-8').strip()

                    if line.startswith('{') and line.endswith('}'):
                        try:
                            data = json.loads(line)
                            timestamp = datetime.now().strftime('%H:%M:%S')

                            print(f"[{timestamp}] Received:")
                            print(f"  Device:  {data.get('device', 'Unknown')}")
                            print(f"  Reading: {data.get('reading', 0)}")
                            print(f"  Value:   {data.get('value', 'N/A')}")
                            # Process additional fields here

                        except json.JSONDecodeError as e:
                            print(f"JSON decode error: {e}")

                time.sleep(0.1)

        except KeyboardInterrupt:
            print("\nStopping listener...")

if __name__ == '__main__':
    receiver = BluetoothReceiver()

    bt_ports = receiver.list_bluetooth_ports()
    if bt_ports:
        print("Available Bluetooth ports:")
        for i, port in enumerate(bt_ports):
            print(f"{i}: {port.device} - {port.description}")

        if receiver.connect(bt_ports[0].device):
            receiver.listen_for_data()
    else:
        print("No Bluetooth ports found")

Security note: BluetoothSerial traffic is unencrypted by default and the SPP profile pairs without a strong PIN unless you configure one. Treat the channel as eavesdroppable - don't push credentials, tokens, or anything sensitive over it without app-level encryption.

5. Bluetooth Low Energy (BLE)

BLE is the right call when you want a phone or laptop to subscribe to live notifications from a battery-powered device. The ESP32 hosts a GATT server with one service and a notify-capable characteristic; the host subscribes and reads as values change.

ESP32 BLE Implementation

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

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLEServer *pServer = nullptr;
BLECharacteristic *pCharacteristic = nullptr;
volatile bool deviceConnected = false;

unsigned long lastReading = 0;
unsigned long readingCount = 0;

class ServerCallbacks : public BLEServerCallbacks {
    void onConnect(BLEServer *pServer) override {
        deviceConnected = true;
        Serial.println("BLE client connected");
    }
    void onDisconnect(BLEServer *pServer) override {
        deviceConnected = false;
        Serial.println("BLE client disconnected — restarting advertising");
        pServer->getAdvertising()->start();
    }
};

void setup() {
    Serial.begin(115200);

    BLEDevice::init("ESP32-BLE");
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new ServerCallbacks());

    BLEService *pService = pServer->createService(SERVICE_UUID);
    pCharacteristic = pService->createCharacteristic(
        CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
    );
    pCharacteristic->setValue("ready");
    pService->start();

    BLEAdvertising *pAdvertising = pServer->getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->start();

    Serial.println("BLE advertising as \"ESP32-BLE\"");
}

void publishIfConnected() {
    if (!deviceConnected || pCharacteristic == nullptr) return;

    String payload = "{";
    payload += "\"reading\":" + String(readingCount) + ",";
    payload += "\"value\":42,"; // Replace with your sensor reading
    payload += "\"timestamp\":" + String(lastReading);
    payload += "}";

    pCharacteristic->setValue((uint8_t *)payload.c_str(), payload.length());
    pCharacteristic->notify();
}

void loop() {
    lastReading = millis();
    readingCount++;
    publishIfConnected();
    delay(1000);
}

Python BLE Receiver (Bleak)

import asyncio
import json
import signal
from datetime import datetime
from typing import Optional

from bleak import BleakScanner, BleakClient, BleakError

SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
CHAR_UUID    = "beb5483e-36e1-4688-b7f5-ea07361b26a8"
DEVICE_NAME  = "ESP32-BLE"

class BLEReceiver:
    def __init__(self, device_name: str = DEVICE_NAME):
        self.device_name = device_name
        self.client: Optional[BleakClient] = None
        self._stop = asyncio.Event()

    async def find_device(self):
        print("Scanning for BLE devices (5s)...")
        devices = await BleakScanner.discover(timeout=5.0)

        # Prefer exact name match
        for d in devices:
            if (d.name or "").strip() == self.device_name:
                print(f"Found {d.name} [{d.address}]")
                return d

        # Fallback: match advertised service UUID
        for d in devices:
            uuids = set((d.metadata or {}).get("uuids", []) or [])
            if SERVICE_UUID.lower() in {u.lower() for u in uuids}:
                print(f"Found by service UUID: {d.address}")
                return d

        print("Device not found")
        return None

    def _handle_notification(self, _: int, data: bytearray):
        text = data.decode("utf-8", errors="ignore").strip()
        if not (text.startswith("{") and text.endswith("}")):
            return
        try:
            msg = json.loads(text)
            stamp = datetime.now().strftime('%H:%M:%S')
            print(f"[{stamp}] Reading {msg.get('reading')}: value={msg.get('value')}")
            # Process additional fields here
        except json.JSONDecodeError:
            print(f"Invalid JSON: {text}")

    async def connect_and_listen(self):
        device = await self.find_device()
        if not device:
            return

        try:
            self.client = BleakClient(device.address, timeout=10.0)
            await self.client.connect()
            print("BLE connected — subscribing to notifications. Ctrl+C to stop.")

            await self.client.start_notify(CHAR_UUID, self._handle_notification)

            while not self._stop.is_set() and self.client.is_connected:
                await asyncio.sleep(0.2)

        except BleakError as e:
            print(f"BLE error: {e}")
        finally:
            if self.client and self.client.is_connected:
                try:
                    await self.client.stop_notify(CHAR_UUID)
                except Exception:
                    pass
                await self.client.disconnect()
                print("Disconnected.")

    def stop(self, *_):
        self._stop.set()

async def main():
    rx = BLEReceiver()
    loop = asyncio.get_running_loop()
    for sig in (signal.SIGINT, signal.SIGTERM):
        try:
            loop.add_signal_handler(sig, rx.stop)
        except NotImplementedError:
            pass  # Windows doesn't support add_signal_handler
    await rx.connect_and_listen()

if __name__ == '__main__':
    asyncio.run(main())

Security note: by default, BLE notifications go out unencrypted - anyone in range with a sniffer can read them. For sensitive data, mark the characteristic as encrypted (PROPERTY_READ_ENC / PROPERTY_WRITE_ENC) and bond on first connect so the link uses an established LTK.

Conclusion

If you're choosing between methods, here's the short version:

  • Cloud dashboard you'll check from anywhere? Firebase.
  • Local PC or server on the same network? Router WiFi → Flask.
  • No router on site? Hotspot mode, with the ESP32 as the AP.
  • Pairing with a phone or running on battery? BLE.
  • Streaming serial-style data to a PC and you're on the original ESP32? Bluetooth Classic.
  • Peer-to-peer between ESP32s with no infrastructure? Look at ESP-NOW (not covered here, but worth a search).

In real projects you'll often layer two - for example, BLE for setup/configuration plus Wi-Fi for steady state data - so don't feel locked into one.

Published on February 2, 2026
0 views