Back to Technology

ESP32 & IoT Development

April 1, 2026 Wasil Zafar 55 min read

Build connected IoT devices with ESP32 — WiFi and BLE connectivity, MQTT messaging, ESP-IDF and Arduino frameworks, sensor integration, deep sleep power management, OTA firmware updates, and cloud platform integration with AWS IoT and Home Assistant.

Table of Contents

  1. History of ESP32
  2. ESP32 Hardware
  3. Development Environment
  4. WiFi Connectivity
  5. BLE Connectivity
  6. MQTT & Cloud Integration
  7. Sensor Integration
  8. Power Management
  9. OTA Firmware Updates
  10. IoT Security
  11. Case Studies
  12. Exercises
  13. ESP32 IoT Project Plan Generator
  14. Conclusion & Resources

History of ESP32

Key Insight: The ESP32 family has shipped over 1 billion units since its introduction, making Espressif Systems one of the most successful fabless semiconductor companies in the IoT space. At $2-$4 per module with integrated WiFi and Bluetooth, it democratized wireless connectivity for embedded systems.

The ESP32 story begins with Espressif Systems, a semiconductor company founded in 2008 by Teo Swee Ann in Shanghai, China. Espressif's first breakthrough product was the ESP8266, released in 2014. Originally positioned as a simple WiFi-to-UART bridge for existing microcontrollers, the ESP8266 contained a Tensilica L106 processor that hackers and hobbyists quickly discovered could be programmed directly — eliminating the need for a separate MCU entirely.

The ESP8266 sold for under $2 on modules like the ESP-01, sparking an explosion of DIY IoT projects. But it had limitations: a single CPU core, limited GPIO, no Bluetooth, no hardware security, and only 80 KB of user RAM. Espressif addressed every one of these limitations with the ESP32, announced in September 2015 and shipping in September 2016.

The original ESP32 featured dual Xtensa LX6 cores at 240 MHz, 520 KB SRAM, integrated WiFi 802.11 b/g/n + Bluetooth 4.2 (Classic + BLE), 34 GPIO pins, multiple ADC/DAC channels, capacitive touch sensing, and a ULP (Ultra-Low-Power) coprocessor for deep sleep operation. At $3 for a module, it was a revelation.

The ESP32 Family Expands

Espressif has since expanded the ESP32 into a diverse family, each variant optimized for specific use cases:

  • ESP32-S2 (2019) — Single-core Xtensa LX7, WiFi only (no BLE), native USB, lower cost for WiFi-only applications
  • ESP32-S3 (2020) — Dual-core Xtensa LX7 at 240 MHz, WiFi + BLE 5.0, vector instructions for AI/ML, native USB-OTG, the premium variant for edge AI
  • ESP32-C3 (2020) — Single-core RISC-V at 160 MHz, WiFi + BLE 5.0, the first Espressif chip using RISC-V architecture, lowest cost with modern connectivity
  • ESP32-C6 (2021) — Single-core RISC-V at 160 MHz, WiFi 6 + BLE 5.3 + Thread/Zigbee (802.15.4), the first ESP with Thread/Matter protocol support
  • ESP32-H2 (2021) — RISC-V, BLE 5.3 + Thread/Zigbee only (no WiFi), designed as a Thread border router or Zigbee coordinator
Milestone Date Significance
Espressif founded2008Shanghai-based fabless semiconductor company
ESP8266 releasedAug 2014$2 WiFi module, ignited maker IoT movement
ESP32 announcedSep 2015Dual-core, WiFi+BT, addresses all ESP8266 limitations
ESP32 shippingSep 2016First modules available, ESP-IDF v1.0 released
ESP32-S22019Single-core, WiFi only, native USB, lower cost
ESP32-S32020AI/ML vector instructions, USB-OTG, BLE 5.0
ESP32-C32020First RISC-V based Espressif chip
ESP32-C62021WiFi 6 + Thread/Zigbee, Matter protocol ready
1 billion units shipped2023Espressif IPO on Shanghai STAR Market

ESP32 Hardware

Variant Comparison

Feature ESP32 ESP32-S2 ESP32-S3 ESP32-C3 ESP32-C6
CPUXtensa LX6 x2Xtensa LX7 x1Xtensa LX7 x2RISC-V x1RISC-V x1
Frequency240 MHz240 MHz240 MHz160 MHz160 MHz
SRAM520 KB320 KB512 KB400 KB512 KB
WiFi802.11 b/g/n802.11 b/g/n802.11 b/g/n802.11 b/g/n802.11ax (WiFi 6)
BluetoothClassic + BLE 4.2NoneBLE 5.0BLE 5.0BLE 5.3
Thread/ZigbeeNoNoNoNoYes (802.15.4)
USBNo nativeUSB-OTGUSB-OTGUSB Serial/JTAGUSB Serial/JTAG
GPIO3443452230
ADC18 ch (12-bit)20 ch (13-bit)20 ch (12-bit)6 ch (12-bit)7 ch (12-bit)
Secure Bootv1v2v2v2v2
Module Price~$3~$2.50~$3.50~$2~$3
Best ForGeneral IoTWiFi + USB HIDEdge AI, camerasLow-cost BLE+WiFiMatter/Thread
Key Insight: For new designs in 2026, the ESP32-C3 is the best choice for cost-optimized WiFi+BLE applications. The ESP32-S3 is optimal when you need camera support or AI inference. The ESP32-C6 is the right pick for Matter/Thread smart home devices. The original ESP32 is legacy — avoid it for new designs due to older security features and higher power consumption.

Memory Architecture

ESP32 memory is divided into several regions, each with different speed and access characteristics. Understanding this is critical for performance optimization:

  • IRAM (Instruction RAM, ~200 KB) — Fastest execution, used for interrupt handlers and time-critical code. Attribute: IRAM_ATTR
  • DRAM (Data RAM, ~320 KB) — General-purpose data storage, stack, heap. Attribute: DRAM_ATTR
  • RTC Memory (8 KB fast + 8 KB slow) — Survives deep sleep. Used by ULP coprocessor. Attribute: RTC_DATA_ATTR
  • PSRAM (optional, up to 8 MB) — External SPI RAM for large buffers, camera frames, ML models
  • Flash (4-16 MB) — Code storage, SPIFFS/LittleFS filesystems, OTA partitions

Development Environment

Framework Comparison

Framework Language Complexity Best For
ESP-IDFC/C++HighProduction firmware, full hardware access, maximum performance
Arduino (ESP32)C++LowRapid prototyping, hobbyist projects, rich library ecosystem
MicroPythonPythonLowScripting, education, rapid iteration without compilation
CircuitPythonPythonLowBeginner-friendly, Adafruit ecosystem, drag-and-drop deployment
Rust (esp-rs)RustMediumMemory safety, async/await, growing ecosystem

ESP-IDF Setup

# Install ESP-IDF v5.x on Linux/macOS
mkdir -p ~/esp
cd ~/esp
git clone -b v5.2 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32,esp32s3,esp32c3,esp32c6

# Activate ESP-IDF environment
. ~/esp/esp-idf/export.sh

# Create a new project from template
idf.py create-project my_iot_device
cd my_iot_device

# Configure for ESP32-S3
idf.py set-target esp32s3

# Open configuration menu
idf.py menuconfig

# Build, flash, and monitor
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

# Useful commands
idf.py size          # Show memory usage
idf.py size-components  # Per-component memory usage
idf.py erase-flash   # Erase entire flash

Arduino IDE Setup

# Arduino CLI setup (alternative to IDE)
arduino-cli config init
arduino-cli config add board_manager.additional_urls \
    https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli core update-index
arduino-cli core install esp32:esp32

# Compile and upload
arduino-cli compile --fqbn esp32:esp32:esp32s3 MySketch/
arduino-cli upload --fqbn esp32:esp32:esp32s3 -p /dev/ttyUSB0 MySketch/

WiFi Connectivity

WiFi is the ESP32's primary connectivity feature. The chip supports 802.11 b/g/n in station mode (client), access point mode (AP), or both simultaneously (AP+STA). Understanding the WiFi stack is essential for building reliable IoT devices.

Station Mode with ESP-IDF

/* wifi_station.c - ESP-IDF WiFi station mode with reconnection logic */

#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include <string.h>

static const char *TAG = "wifi_sta";
static int s_retry_num = 0;
#define MAX_RETRY 10

static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                                int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT &&
               event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry_num < MAX_RETRY) {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGW(TAG, "Retry %d/%d to connect...", s_retry_num, MAX_RETRY);
        } else {
            ESP_LOGE(TAG, "Failed to connect after %d attempts", MAX_RETRY);
        }
    } else if (event_base == IP_EVENT &&
               event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
        ESP_LOGI(TAG, "Connected! IP: " IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
    }
}

void wifi_init_sta(const char *ssid, const char *password)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(
        IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL));

    wifi_config_t wifi_config = {
        .sta = {
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
            .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
        },
    };
    strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid));
    strncpy((char *)wifi_config.sta.password, password,
            sizeof(wifi_config.sta.password));

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "WiFi station initialized. Connecting to %s...", ssid);
}

HTTP Server with WebSocket

/* Simple HTTP server with WebSocket for real-time sensor data */

#include "esp_http_server.h"
#include "esp_log.h"

static const char *TAG = "httpd";

/* Serve the main page */
static esp_err_t root_handler(httpd_req_t *req)
{
    const char *html =
        "<!DOCTYPE html><html><body>"
        "<h1>ESP32 Sensor Dashboard</h1>"
        "<div id='data'>Connecting...</div>"
        "<script>"
        "var ws = new WebSocket('ws://' + location.host + '/ws');"
        "ws.onmessage = function(e) {"
        "  document.getElementById('data').innerHTML = e.data;"
        "};"
        "</script></body></html>";
    httpd_resp_set_type(req, "text/html");
    return httpd_resp_send(req, html, strlen(html));
}

/* WebSocket handler */
static esp_err_t ws_handler(httpd_req_t *req)
{
    if (req->method == HTTP_GET) {
        ESP_LOGI(TAG, "WebSocket handshake done");
        return ESP_OK;
    }
    /* Receive frame */
    httpd_ws_frame_t ws_pkt;
    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;
    esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
    if (ret != ESP_OK) return ret;

    ESP_LOGI(TAG, "Received WS frame: %.*s", ws_pkt.len, ws_pkt.payload);
    return ESP_OK;
}

httpd_handle_t start_webserver(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    httpd_handle_t server = NULL;

    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_uri_t root = {
            .uri = "/", .method = HTTP_GET, .handler = root_handler
        };
        httpd_uri_t ws = {
            .uri = "/ws", .method = HTTP_GET, .handler = ws_handler,
            .is_websocket = true
        };
        httpd_register_uri_handler(server, &root);
        httpd_register_uri_handler(server, &ws);
        ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);
    }
    return server;
}

BLE Connectivity

Bluetooth Low Energy (BLE) is ideal for short-range communication with mobile phones, wearables, and other BLE devices. The ESP32 supports both the GATT server role (peripheral — advertising services that clients connect to) and the GATT client role (central — scanning and connecting to peripherals).

BLE GATT Server (Environmental Sensor)

/* BLE Environmental Sensing Service - ESP-IDF NimBLE stack
 * Advertises temperature and humidity via standard BLE ESS profile.
 */

#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "esp_log.h"

static const char *TAG = "ble_ess";
static uint16_t conn_handle;
static bool connected = false;

/* BLE UUIDs - Environmental Sensing Service */
#define ESS_UUID         0x181A  /* Environmental Sensing */
#define TEMP_CHAR_UUID   0x2A6E  /* Temperature */
#define HUMID_CHAR_UUID  0x2A6F  /* Humidity */

static int16_t current_temp = 2350;   /* 23.50 C (in 0.01 C units) */
static uint16_t current_humid = 5500; /* 55.00 % (in 0.01% units) */

/* Read temperature characteristic */
static int temp_access_cb(uint16_t conn_handle, uint16_t attr_handle,
                           struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
        os_mbuf_append(ctxt->om, &current_temp, sizeof(current_temp));
        ESP_LOGI(TAG, "Temperature read: %.2f C", current_temp / 100.0);
    }
    return 0;
}

/* Read humidity characteristic */
static int humid_access_cb(uint16_t conn_handle, uint16_t attr_handle,
                            struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
        os_mbuf_append(ctxt->om, &current_humid, sizeof(current_humid));
        ESP_LOGI(TAG, "Humidity read: %.2f %%", current_humid / 100.0);
    }
    return 0;
}

/* GATT service definition */
static const struct ble_gatt_svc_def gatt_svcs[] = {
    {
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = BLE_UUID16_DECLARE(ESS_UUID),
        .characteristics = (struct ble_gatt_chr_def[]) {
            {
                .uuid = BLE_UUID16_DECLARE(TEMP_CHAR_UUID),
                .access_cb = temp_access_cb,
                .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
            },
            {
                .uuid = BLE_UUID16_DECLARE(HUMID_CHAR_UUID),
                .access_cb = humid_access_cb,
                .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
            },
            { 0 }, /* Terminate */
        },
    },
    { 0 }, /* Terminate */
};
Key Insight: NimBLE is the recommended BLE stack for ESP32 — it uses approximately 50% less flash and 60% less RAM compared to the Bluedroid stack. Enable it with idf.py menuconfig under Component config -> Bluetooth -> Host -> NimBLE.

MQTT & Cloud Integration

MQTT (Message Queuing Telemetry Transport) is the dominant IoT messaging protocol. Designed by Andy Stanford-Clark (IBM) and Arlen Nipper in 1999 for oil pipeline telemetry over satellite links, it provides reliable pub/sub messaging with minimal bandwidth overhead — perfect for constrained IoT devices.

MQTT QoS Levels

QoS Name Delivery Use Case
0At most onceFire and forget, no ACKFrequent sensor data where occasional loss is acceptable
1At least onceACK required, may duplicateImportant events, commands — most common for IoT
2Exactly once4-step handshake, no duplicatesFinancial transactions, billing — rarely needed for IoT

ESP-MQTT Client with TLS

/* MQTT client with TLS, LWT, and automatic reconnection */

#include "mqtt_client.h"
#include "esp_log.h"
#include "esp_tls.h"

static const char *TAG = "mqtt";

/* Embed the CA certificate at compile time */
extern const uint8_t ca_cert_pem_start[] asm("_binary_ca_cert_pem_start");
extern const uint8_t ca_cert_pem_end[]   asm("_binary_ca_cert_pem_end");

static void mqtt_event_handler(void *args, esp_event_base_t base,
                                int32_t event_id, void *event_data)
{
    esp_mqtt_event_handle_t event = event_data;
    esp_mqtt_client_handle_t client = event->client;

    switch (event_id) {
    case MQTT_EVENT_CONNECTED:
        ESP_LOGI(TAG, "Connected to MQTT broker");
        /* Subscribe to command topics */
        esp_mqtt_client_subscribe(client, "device/esp32-001/cmd/#", 1);
        /* Publish online status (retained) */
        esp_mqtt_client_publish(client, "device/esp32-001/status",
                                "online", 0, 1, 1);
        break;

    case MQTT_EVENT_DATA:
        ESP_LOGI(TAG, "Received: topic=%.*s data=%.*s",
                 event->topic_len, event->topic,
                 event->data_len, event->data);
        /* Handle commands */
        if (strstr(event->topic, "/cmd/reboot")) {
            ESP_LOGW(TAG, "Reboot command received!");
            esp_restart();
        }
        break;

    case MQTT_EVENT_DISCONNECTED:
        ESP_LOGW(TAG, "Disconnected from MQTT broker");
        break;

    case MQTT_EVENT_ERROR:
        ESP_LOGE(TAG, "MQTT error type: %d", event->error_handle->error_type);
        break;
    }
}

void mqtt_app_start(void)
{
    esp_mqtt_client_config_t mqtt_cfg = {
        .broker = {
            .address.uri = "mqtts://mqtt.example.com:8883",
            .verification.certificate = (const char *)ca_cert_pem_start,
        },
        .credentials = {
            .username = "esp32-001",
            .authentication.password = "secure_password_here",
            .client_id = "esp32-001",
        },
        .session = {
            .last_will = {
                .topic = "device/esp32-001/status",
                .msg = "offline",
                .qos = 1,
                .retain = 1,
            },
            .keepalive = 120,
        },
        .network.reconnect_timeout_ms = 5000,
    };

    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID,
                                    mqtt_event_handler, NULL);
    esp_mqtt_client_start(client);
}

Home Assistant Auto-Discovery

{
    "name": "ESP32 Living Room Temperature",
    "unique_id": "esp32_001_temperature",
    "state_topic": "home/livingroom/esp32/sensor",
    "value_template": "{{ value_json.temperature }}",
    "unit_of_measurement": "°C",
    "device_class": "temperature",
    "device": {
        "identifiers": ["esp32-001"],
        "name": "ESP32 Living Room Sensor",
        "model": "ESP32-C3 DevKit",
        "manufacturer": "Espressif",
        "sw_version": "1.2.0"
    },
    "availability": {
        "topic": "device/esp32-001/status"
    }
}
Key Insight: Home Assistant's MQTT auto-discovery protocol lets your ESP32 register itself automatically — no manual configuration in Home Assistant's YAML files. Publish a JSON config message to homeassistant/sensor/esp32_001/temperature/config with the discovery payload, and HA creates the entity automatically.

Sensor Integration

I2C BME280 with ESP-IDF

/* BME280 I2C driver for ESP-IDF */

#include "driver/i2c.h"
#include "esp_log.h"

#define I2C_MASTER_NUM    I2C_NUM_0
#define I2C_SDA_GPIO      21
#define I2C_SCL_GPIO      22
#define I2C_FREQ_HZ       400000
#define BME280_ADDR       0x76

static const char *TAG = "bme280";

esp_err_t i2c_master_init(void)
{
    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = I2C_SDA_GPIO,
        .scl_io_num = I2C_SCL_GPIO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = I2C_FREQ_HZ,
    };
    ESP_ERROR_CHECK(i2c_param_config(I2C_MASTER_NUM, &conf));
    return i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}

esp_err_t bme280_read_raw(int32_t *temp, int32_t *press, int32_t *humid)
{
    uint8_t data[8];
    uint8_t reg = 0xF7;

    /* Trigger measurement: forced mode, oversample x1 */
    uint8_t ctrl_meas[] = {0xF4, 0x25};
    i2c_master_write_to_device(I2C_MASTER_NUM, BME280_ADDR,
                                ctrl_meas, 2, pdMS_TO_TICKS(100));
    vTaskDelay(pdMS_TO_TICKS(50)); /* Wait for measurement */

    /* Read 8 bytes starting from 0xF7 */
    i2c_master_write_read_device(I2C_MASTER_NUM, BME280_ADDR,
                                  &reg, 1, data, 8, pdMS_TO_TICKS(100));

    *press = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
    *temp  = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
    *humid = (data[6] << 8)  | data[7];

    ESP_LOGI(TAG, "Raw: T=%ld P=%ld H=%ld", *temp, *press, *humid);
    return ESP_OK;
}

One-Wire DS18B20 Temperature Sensor

/* DS18B20 one-wire temperature sensor using ESP-IDF RMT driver */

#include "onewire_bus.h"
#include "ds18b20.h"
#include "esp_log.h"

#define ONEWIRE_GPIO  4
static const char *TAG = "ds18b20";

void ds18b20_task(void *pvParameters)
{
    /* Install one-wire bus */
    onewire_bus_handle_t bus;
    onewire_bus_config_t bus_config = { .bus_gpio_num = ONEWIRE_GPIO };
    onewire_bus_rmt_config_t rmt_config = { .max_rx_bytes = 10 };
    ESP_ERROR_CHECK(onewire_new_bus_rmt(&bus_config, &rmt_config, &bus));

    /* Search for DS18B20 devices */
    onewire_device_iter_handle_t iter;
    onewire_device_t device;
    ESP_ERROR_CHECK(onewire_new_device_iter(bus, &iter));

    ds18b20_device_handle_t ds18b20 = NULL;
    while (onewire_device_iter_get_next(iter, &device) == ESP_OK) {
        ds18b20_config_t ds_cfg = {};
        if (ds18b20_new_device(&device, &ds_cfg, &ds18b20) == ESP_OK) {
            ESP_LOGI(TAG, "Found DS18B20: %016llX", device.address);
            break;
        }
    }
    onewire_del_device_iter(iter);

    if (!ds18b20) {
        ESP_LOGE(TAG, "No DS18B20 found!");
        vTaskDelete(NULL);
        return;
    }

    /* Set 12-bit resolution (750ms conversion time) */
    ds18b20_set_resolution(ds18b20, DS18B20_RESOLUTION_12B);

    while (1) {
        float temperature;
        ds18b20_trigger_temperature_conversion(ds18b20);
        vTaskDelay(pdMS_TO_TICKS(800)); /* Wait for conversion */
        ds18b20_get_temperature(ds18b20, &temperature);
        ESP_LOGI(TAG, "Temperature: %.2f C", temperature);
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}
Warning: The ESP32's built-in ADC is notoriously non-linear, especially at the extremes. For analog sensors requiring accuracy better than 5%, use an external ADC (ADS1115 via I2C or MCP3008 via SPI). If using the internal ADC, apply ESP-IDF's calibration API (adc_cali_create_scheme_curve_fitting()) to compensate for per-chip variations.

Power Management

Power management is the single most critical factor for battery-powered IoT devices. An ESP32 running WiFi continuously draws 80-240 mA — a 3000 mAh battery would last barely 12-37 hours. With deep sleep, the same battery can last months or years.

Sleep Mode Comparison

Mode Current Wake Time Preserved Wake Sources
Active (WiFi TX)160-240 mAN/AEverythingN/A
Active (WiFi RX)80-100 mAN/AEverythingN/A
Modem Sleep20-30 mAInstantCPU, RAM, WiFi associationWiFi beacon interval
Light Sleep0.8 mA<1 msCPU state, RAM, WiFiTimer, GPIO, UART, touch
Deep Sleep10 uA~300 msRTC memory only (8 KB)Timer, GPIO (RTC), touch, ULP
Hibernation5 uA~300 msNothingRTC timer, RTC GPIO only

Deep Sleep with Timer Wake

/* Deep sleep example: wake every 5 minutes, read sensor, publish MQTT */

#include "esp_sleep.h"
#include "esp_log.h"
#include "driver/rtc_io.h"

#define SLEEP_DURATION_US   (5 * 60 * 1000000ULL)  /* 5 minutes */

/* RTC memory survives deep sleep */
RTC_DATA_ATTR static int boot_count = 0;
RTC_DATA_ATTR static float last_temperature = 0.0;

void app_main(void)
{
    boot_count++;
    ESP_LOGI("main", "Boot count: %d (last temp: %.1f C)",
             boot_count, last_temperature);

    /* Determine wake cause */
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    switch (cause) {
    case ESP_SLEEP_WAKEUP_TIMER:
        ESP_LOGI("main", "Woke up from timer");
        break;
    case ESP_SLEEP_WAKEUP_EXT0:
        ESP_LOGI("main", "Woke up from GPIO interrupt");
        break;
    default:
        ESP_LOGI("main", "Initial boot (not from sleep)");
        break;
    }

    /* Read sensor */
    float temperature = read_bme280_temperature(); /* Your sensor read */
    last_temperature = temperature;

    /* Connect WiFi and publish (only if data changed significantly) */
    if (boot_count == 1 ||
        fabs(temperature - last_temperature) > 0.5) {
        wifi_init_sta("MySSID", "MyPassword");
        mqtt_publish_temperature(temperature);
        wifi_disconnect();
    }

    /* Configure wake sources */
    esp_sleep_enable_timer_wakeup(SLEEP_DURATION_US);

    /* Optional: GPIO wake (e.g., motion sensor on GPIO 33) */
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 1); /* Wake on HIGH */

    /* Isolate unused GPIOs to save power */
    rtc_gpio_isolate(GPIO_NUM_12);
    rtc_gpio_isolate(GPIO_NUM_15);

    ESP_LOGI("main", "Entering deep sleep for %llu seconds...",
             SLEEP_DURATION_US / 1000000ULL);
    esp_deep_sleep_start();
    /* Execution never reaches here */
}

Battery Life Calculator

#!/usr/bin/env python3
"""ESP32 battery life calculator for deep sleep duty cycles."""

def calculate_battery_life(
    battery_mah,
    active_current_ma,
    active_duration_s,
    sleep_current_ua,
    sleep_duration_s,
    battery_efficiency=0.8
):
    """Calculate expected battery life for a duty-cycled IoT device."""
    cycle_total_s = active_duration_s + sleep_duration_s
    duty_cycle = active_duration_s / cycle_total_s

    # Average current (in mA)
    avg_current_ma = (
        (active_current_ma * active_duration_s +
         (sleep_current_ua / 1000) * sleep_duration_s)
        / cycle_total_s
    )

    # Usable capacity
    usable_mah = battery_mah * battery_efficiency

    # Life in hours
    life_hours = usable_mah / avg_current_ma
    life_days = life_hours / 24

    print(f"=== ESP32 Battery Life Estimate ===")
    print(f"Battery: {battery_mah} mAh (efficiency: {battery_efficiency*100}%)")
    print(f"Active: {active_current_ma} mA for {active_duration_s}s")
    print(f"Sleep:  {sleep_current_ua} uA for {sleep_duration_s}s")
    print(f"Duty cycle: {duty_cycle*100:.2f}%")
    print(f"Average current: {avg_current_ma:.3f} mA")
    print(f"Expected life: {life_days:.0f} days ({life_hours:.0f} hours)")
    return life_days

# Example: Wake every 5 minutes, active for 10 seconds (WiFi connect + publish)
calculate_battery_life(
    battery_mah=3000,
    active_current_ma=150,     # WiFi TX average
    active_duration_s=10,      # Connect + read + publish
    sleep_current_ua=10,       # Deep sleep
    sleep_duration_s=290       # 5 min - 10s active = 290s sleep
)
# Output: ~400+ days on a single 3000 mAh battery

OTA Firmware Updates

Over-The-Air (OTA) firmware updates are essential for deployed IoT devices. Without OTA, updating firmware requires physical access to every device — impractical for hundreds or thousands of field-deployed sensors.

ESP-IDF OTA Dual-Partition Scheme

The ESP32 uses a dual-partition OTA scheme. The flash is divided into two application partitions (ota_0 and ota_1) plus an otadata partition that tracks which application is active. During an OTA update, the new firmware is written to the inactive partition while the current firmware continues running. After verification, the device reboots into the new partition.

# Custom partition table for OTA (partitions.csv)
# Name,     Type,  SubType,  Offset,   Size,    Flags
nvs,        data,  nvs,      0x9000,   0x6000,
otadata,    data,  ota,      0xf000,   0x2000,
phy_init,   data,  phy,      0x11000,  0x1000,
ota_0,      app,   ota_0,    0x20000,  0x1E0000,
ota_1,      app,   ota_1,    0x200000, 0x1E0000,
storage,    data,  spiffs,   0x3E0000, 0x20000,
/* HTTPS OTA update with certificate verification and rollback */

#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "esp_log.h"

static const char *TAG = "ota";

extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start");
extern const uint8_t server_cert_pem_end[]   asm("_binary_ca_cert_pem_end");

esp_err_t perform_ota_update(const char *url)
{
    ESP_LOGI(TAG, "Starting OTA from: %s", url);

    esp_http_client_config_t http_config = {
        .url = url,
        .cert_pem = (const char *)server_cert_pem_start,
        .timeout_ms = 30000,
        .keep_alive_enable = true,
    };

    esp_https_ota_config_t ota_config = {
        .http_config = &http_config,
    };

    esp_https_ota_handle_t ota_handle = NULL;
    esp_err_t err = esp_https_ota_begin(&ota_config, &ota_handle);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "OTA begin failed: %s", esp_err_to_name(err));
        return err;
    }

    /* Download and write firmware in chunks */
    while (1) {
        err = esp_https_ota_perform(ota_handle);
        if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) break;

        int image_size = esp_https_ota_get_image_size(ota_handle);
        int read_size = esp_https_ota_get_image_len_read(ota_handle);
        ESP_LOGI(TAG, "Progress: %d / %d bytes (%.1f%%)",
                 read_size, image_size,
                 (float)read_size / image_size * 100);
    }

    if (err != ESP_OK) {
        esp_https_ota_abort(ota_handle);
        ESP_LOGE(TAG, "OTA failed: %s", esp_err_to_name(err));
        return err;
    }

    /* Verify and finalize */
    err = esp_https_ota_finish(ota_handle);
    if (err == ESP_OK) {
        ESP_LOGI(TAG, "OTA update successful! Rebooting...");
        esp_restart();
    } else {
        ESP_LOGE(TAG, "OTA finish failed: %s", esp_err_to_name(err));
    }
    return err;
}

/* After reboot, validate the new firmware works */
void validate_ota_image(void)
{
    esp_ota_img_states_t state;
    const esp_partition_t *running = esp_ota_get_running_partition();
    esp_ota_get_state_partition(running, &state);

    if (state == ESP_OTA_IMG_PENDING_VERIFY) {
        ESP_LOGI(TAG, "New firmware booted. Running self-test...");
        /* Run your validation tests here */
        bool tests_passed = run_self_tests();
        if (tests_passed) {
            esp_ota_mark_app_valid_cancel_rollback();
            ESP_LOGI(TAG, "Firmware validated and confirmed.");
        } else {
            ESP_LOGE(TAG, "Self-test failed! Rolling back...");
            esp_ota_mark_app_invalid_rollback_and_reboot();
        }
    }
}
Warning: Always implement a rollback mechanism for OTA updates. A firmware update that breaks WiFi connectivity will brick the device permanently if there is no fallback. ESP-IDF's esp_ota_mark_app_valid_cancel_rollback() should only be called after the new firmware has verified it can connect to the network and perform its core functions.

IoT Security

IoT security is not optional. A compromised ESP32 in your network can serve as a pivot point for attackers, participate in botnets (as seen in the Mirai botnet attack of 2016), or leak sensitive sensor data. The ESP32's hardware security features — when properly configured — provide a strong foundation.

Security Checklist

  • TLS 1.2+ for all network traffic — Never use plain MQTT (port 1883) or HTTP in production. Always use MQTTS (8883) and HTTPS.
  • Mutual TLS (mTLS) — Both server and device authenticate each other with certificates. Prevents unauthorized devices from connecting.
  • Secure Boot v2 — Ensures only signed firmware can execute. Available on ESP32-S2/S3/C3/C6.
  • Flash Encryption — Encrypts firmware and data in flash. Prevents reading firmware via UART or JTAG.
  • NVS Encryption — Encrypts WiFi credentials and API keys stored in Non-Volatile Storage.
  • Disable JTAG in production — Prevent hardware debugging of deployed devices.
  • Certificate rotation — Implement a mechanism to update device certificates before they expire.
# Enable Secure Boot v2 and Flash Encryption via menuconfig
idf.py menuconfig

# Security features -> Enable Secure Boot v2
# Security features -> Enable Flash Encryption (Release mode)

# Generate signing key (keep this key SECURE - loss = bricked devices)
espsecure.py generate_signing_key --version 2 secure_boot_signing_key.pem

# Sign firmware
espsecure.py sign_data --version 2 --keyfile secure_boot_signing_key.pem \
    build/my_app.bin

# Flash encrypted firmware (one-time operation - IRREVERSIBLE in Release mode)
idf.py -p /dev/ttyUSB0 encrypted-flash monitor
Warning: Enabling Flash Encryption in Release mode is a one-way operation — it permanently burns eFuses that cannot be undone. If you lose the encryption key, the device is permanently bricked with no recovery path. Always start with Development mode and only switch to Release for final production programming.

Case Studies

Case Study 1: Smart Agriculture at Libelium (Spain)

Libelium, a Spanish IoT company, deployed thousands of ESP32-based sensor nodes across agricultural fields in Spain's Ebro Valley. Each node measures soil moisture (capacitive sensors), air temperature/humidity (SHT31), solar radiation (pyranometer via ADC), and wind speed (pulse-counting anemometer). The ESP32-C3 was chosen for its low cost, BLE for local configuration via a farmer's smartphone app, and WiFi for data upload to Libelium's cloud platform.

The nodes operate on solar panels with LiFePO4 batteries, using ESP32 deep sleep to wake every 15 minutes for sensor readings and every hour for WiFi data upload. Average power consumption is under 0.5 mA, giving a theoretical battery life exceeding one year even without solar input. The system reduced irrigation water usage by 30% by enabling precision watering based on actual soil moisture data rather than fixed schedules.

Case Study 2: Industrial Vibration Monitoring at Augury

Augury, an industrial IoT company valued at over $1 billion, uses ESP32-S3 modules in their vibration monitoring sensors for predictive maintenance. The ESP32-S3's vector instructions accelerate FFT (Fast Fourier Transform) computation on vibration data from MEMS accelerometers, allowing the device to perform on-edge analysis before transmitting only anomaly summaries via WiFi.

Each sensor monitors rotating equipment (motors, pumps, compressors) in manufacturing plants. When vibration patterns deviate from learned baselines — indicating bearing wear, misalignment, or impeller damage — the sensor alerts the maintenance team via MQTT to AWS IoT Core. Augury reports that their system detects 92% of mechanical failures 1-3 months before they occur, reducing unplanned downtime by 50%.

Case Study 3: Shelly Smart Home Devices

Shelly (by Allterco Robotics, Bulgaria) is one of the most successful consumer ESP32 product companies. Their smart relays, dimmers, and sensors — all powered by ESP32 variants — have sold millions of units globally. The Shelly Plus series uses ESP32-C3 modules, while the Shelly Pro series uses ESP32 modules with Ethernet.

What makes Shelly notable is their commitment to local control: every Shelly device runs a complete HTTP server and MQTT client locally, functioning without any cloud dependency. They support Home Assistant, MQTT, and their own cloud — giving users full flexibility. Their firmware demonstrates production-grade ESP32 development: OTA updates, mDNS discovery, CoAP for device configuration, and robust WiFi reconnection handling.

Exercises

Exercise 1 Beginner

WiFi Weather Display

Using an ESP32 DevKit and an SSD1306 OLED display (I2C), build a WiFi-connected weather station that fetches weather data from the OpenWeatherMap API every 10 minutes and displays current temperature, humidity, and conditions on the OLED. Implement WiFi reconnection logic and display a "No WiFi" indicator when disconnected. Use the Arduino framework for rapid prototyping. Bonus: add a button that cycles through current weather, 3-hour forecast, and system info (uptime, WiFi RSSI, free heap).

WiFi HTTP Client JSON Parsing I2C OLED Arduino
Exercise 2 Intermediate

BLE-to-MQTT Bridge

Build an ESP32 gateway that scans for BLE environmental sensors (Xiaomi Mijia LYWSD03MMC or similar), reads their temperature/humidity advertisements, and publishes the data to an MQTT broker with Home Assistant auto-discovery. The gateway should support at least 5 concurrent BLE devices, handle duplicate advertisements gracefully, implement a 30-second scan window with 10-second intervals, and maintain persistent MQTT connections with LWT (Last Will and Testament) for offline detection. Deploy on an ESP32-C3 and measure power consumption.

BLE Scanner MQTT Home Assistant Gateway ESP-IDF
Exercise 3 Advanced

Secure OTA Fleet Management

Design and implement a fleet management system for 10+ ESP32 devices. Each device should: (1) connect to AWS IoT Core using mutual TLS with per-device certificates provisioned via the ESP32's secure element or eFuse key storage, (2) report telemetry (sensor data, firmware version, heap usage, WiFi RSSI) every 60 seconds, (3) accept OTA update commands via an MQTT shadow document, (4) download signed firmware from S3 via HTTPS, verify the signature, and roll back automatically if self-tests fail after update. Build a simple web dashboard (Node.js or Python Flask) that shows device status and triggers OTA rollouts to specific device groups.

AWS IoT Core mTLS Secure Boot OTA Fleet Management Dashboard

ESP32 IoT Project Plan Generator

Use this tool to document your ESP32 IoT project plan — chip selection, connectivity options, MQTT configuration, sensor inventory, power strategy, and OTA update approach. Download as Word, Excel, PDF, or PowerPoint for project documentation, hardware procurement, or team review.

ESP32 IoT Project Plan Generator

Document your ESP32 IoT project configuration for review and export. All data stays in your browser — nothing is sent to any server.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

Conclusion & Resources

The ESP32 has become the backbone of the IoT revolution. From a $2 WiFi chip to a diverse family of processors powering billions of connected devices, Espressif's platform provides everything needed to build production-quality IoT systems — wireless connectivity, security features, power management, and over-the-air updates.

Key Takeaways:
  • Choose the right ESP32 variant — C3 for cost, S3 for AI/cameras, C6 for Matter/Thread
  • Use ESP-IDF for production firmware; Arduino for rapid prototyping
  • MQTT with TLS is the standard IoT messaging protocol — implement QoS 1 and LWT
  • Deep sleep is non-negotiable for battery devices — plan your power budget early
  • OTA with rollback prevents bricked devices in the field
  • Security (Secure Boot, Flash Encryption, mTLS) must be designed in from day one

Further Resources

Technology