ESP-IDF: ESP32 Web Server – Output Control

This guide shows how to build a local web server with the ESP32 programmed with ESP-IDF that serves a simple HTML web page with two buttons to control one GPIO on/off. This web page can then be accessed on your local network with any device with a web browser.

ESP-IDF ESP32 Web Server Output Control

Recommended reading: ESP-IDF ESP32 Web Server – Basic HTTP Authentication (Login/Logout)

Prerequisites

Before following this guide, you need to install the ESP-IDF extension on VS Code IDE (Microsoft Visual Studio Code). Follow the next guide to install it, if you haven’t already:

You will also need an ESP32 development board model of your choice.

Project Overview

Let’s take a quick look at the features of the project we’ll build, you can read our guide about building web server with ESP-IDF.

ESP-IDF ESP32 Output Control Web Server Full Project Overview
  • In this tutorial, you’ll learn how to build a web server that serves an HTML page;
  • When you try to access the web server page on the ESP32 IP address, a web page loads with two buttons;
  • You can click the TURN ON / TURN OFF buttons to control a GPIO of the ESP32.

Creating an ESP-IDF Template App Project for the ESP32

The ESP-IDF extension provides an easy way to create a project from scratch with all the required files and configurations generated automatically.

To create a new ESP-IDF project on VS Code, follow these steps:

  1. Open the ESP-IDF Espressif extension and expand the “Advanced” menu
  2. Click the “New Project Wizard” option
  3. Choose the “Use ESP-IDF v6.X” to select the ESP-IDF framework version
VS Code Open ESP-IDF Extension New Project Wizard Select ESP-IDF Version

A new window opens, you need to fill in these fields:

  • Project Name: type the desired project name;
  • Enter Project Directory: click the folder icon and select the target folder to save all your project files. You can use any directory. Note: do NOT use a Google Drive / One Drive / Dropbox folder, because it will write/create many files during the building process—if it’s on a cloud folder, this process might be extremely slow;
  • ESP-IDF Target: select the target device chip, I’m using an ESP32 with the esp32s3 chip;
  • ESP-IDF Board: for the esp32s3 chip, I also need to select the configuration: ESP32-S chip (via builtin USB-JTAG);
  • Serial Port: while having your ESP32 board connected to your computer, select the correct COM port number that refers to your ESP32;
  • Choose Template: click the blue button to create a new project using a template.
ESP-IDF ESP32 Create Open New Project Wizard Menu Select Directory Board Template

In the menu, select the “ESP-IDF Templates” sample project and press the “Create project using template sample project” button.

ESP-IDF ESP32 Create New Sample Project using IDF Template

Opening the ESP-IDF Project on VS Code

After a few seconds, a notification will appear in a new window in VS Code. You can click “Open Project” to open the newly created ESP-IDF sample project template.

ESP-IDF ESP32 Open New Project Sample

IMPORTANT: if you didn’t see the notification that allows you to automatically open the ESP-IDF project on VS Code, you can easily do it by following these instructions:

Go to File > Open Folder…

ESP-IDF ESP32 Open Project Folder VS Code File Menu

Browse on your computer for the esp-idf-project folder (your project folder name that you’ve previously defined) and “Select Folder“.

ESP-IDF ESP32 Open Project VS Code Select Folder

That’s it! Your new ESP-IDF project template has been successfully created and opened.

ESP-IDF generates many files, folders, and subfolders for your project. For this guide, I recommend keeping all the default files unchanged; we will only modify the main.c file.

The example code will be written in the main.c file. To open it, follow these instructions:

  1. Open the project explorer by clicking the first icon on the left sidebar.
  2. Select your project folder name, in my case, it’s “ESP-IDF-PROJECT“.
  3. Expand the “main” folder.
  4. Click the “main.c” file.
  5. The default main.c template file loads in the code window.
ESP-IDF ESP32 Open Project in a VS Code Browse to Main C File

Create the Wi-Fi Config Options File – Kconfig.projbuild

The Kconfig.projbuild file in ESP-IDF is a configuration file placed in a project’s main folder to define custom options in the SDK Configuration Editor (menuconfig tool). This allows users to set specific settings, like strings or values that will be used during build configuration.

To create the Kconfig.projbuild file, go to the File Explorer left sidebar and right-click on top of the main folder. Then, select the “New File…” option:

Create new file main folder Kconfig projbuild

Name the new file exactly as follows: Kconfig.projbuild and copy the content (shown after the following screenshot) to your newly created file.

Kconfig projbuild Wi-Fi Configuration File
menu "Wi-Fi Configuration"
    config ESP_WIFI_SSID
        string "WiFi SSID"
        help
            SSID (network name) you want to connect to.

    config ESP_WIFI_PASSWORD
        string "WiFi Password"
        help
            Password of the Wi-Fi network you want to connect to.
endmenu

Save the file content. This will create a new menu called “Wi-Fi Configuration” in the SDK Configuration Editor where you can type you SSID and Password, so the ESP can connect to your network.

Having the Kconfig.projbuild file prepared, click the gear icon to open the SDK Configuration Editor (menuconfig).

Open SDK Configuration Editor Menuconfig Wi-Fi Configuration

Now, instead of typing your network credentials inside the main.c file, you can configure them in the menuconfig tool. Follow these next steps:

  1. Search for “wifi“;
  2. Type your SSID and Password in the fields;
  3. Press the “Save” button to apply the changes.
SDK Configuration Editor Menuconfig Kconfig projbuild Wi-Fi Configuration SSID Password

Code: ESP32 Web Server to Control Outputs using ESP-IDF

Copy the following code to the main.c file. This code serves a basic HTML web page hosted on an ESP32 that allows you to control the ESP32 GPIO 5.

/*  
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details: https://RandomNerdTutorials.com/esp-idf-esp32-web-server-output/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "driver/gpio.h"
#include "sdkconfig.h"

#define MY_ESP_WIFI_SSID      CONFIG_ESP_WIFI_SSID
#define MY_ESP_WIFI_PASS      CONFIG_ESP_WIFI_PASSWORD

static const char *TAG = "web_server";

// Define the GPIO pin for the LED
#define LED_PIN GPIO_NUM_5

// HTML web page to serve the root / page
static const char *html_root_page = 
    "<!DOCTYPE html>"
    "<html>"
    "<head>"
    "<title>ESP-IDF: Output Web Server</title>"
    "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
    "<style>"
    "body { font-family: Arial, sans-serif; background-color: #f0f0f0; text-align: center; padding: 40px; margin: 0; }"
    "h1 { color: #333; margin-bottom: 10px; }"
    "p { font-size: 20px; color: #666; }"
    ".status { font-weight: bold; margin: 20px 0; }"
    ".status-on { color: #28a745; }"
    ".status-off { color: #dc3545; }"
    ".button {"
    "    display: inline-block; padding: 15px 32px; margin: 15px;"
    "    font-size: 20px; font-weight: bold; cursor: pointer; border: none;"
    "    border-radius: 8px; color: white; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"
    "}"
    ".on { background-color: #28a745; }"
    ".off { background-color: #dc3545; }"
    ".button:hover { opacity: 0.9; transform: translateY(-2px); }"
    ".button:active { transform: translateY(1px); }"
    "</style>"
    "</head>"
    "<body>"
    "<h1>ESP-IDF: Output Web Server</h1>"
    "<p>GPIO 5 Status: <span id=\"status\" class=\"status %s\">%s</span></p>"
    "<button onclick=\"setOutput(1)\" class=\"button on\">TURN ON</button>"
    "<button onclick=\"setOutput(0)\" class=\"button off\">TURN OFF</button>"
    "<script>"
    "function updateStatus() {"
    "    fetch('/status?gpio=5')"
    "        .then(r => r.text())"
    "        .then(text => {"
    "            const state = parseInt(text.trim());"
    "            const el = document.getElementById('status');"
    "            if (state === 1) {"
    "                el.className = 'status status-on';"
    "                el.textContent = 'ON';"
    "            } else {"
    "                el.className = 'status status-off';"
    "                el.textContent = 'OFF';"
    "            }"
    "        })"
    "        .catch(() => {});"
    "}"
    "function setOutput(state) {"
    "    fetch(`/output?gpio=5&state=${state}`)"
    "        .then(() => updateStatus())"
    "        .catch(() => {});"
    "}"
    "setInterval(updateStatus, 10000);"
    "window.onload = updateStatus;"
    "</script>"
    "</body>"
    "</html>";

// HTTP GET handler for root "/" - send main HTML page
static esp_err_t root_get_handler(httpd_req_t *req)
{
    int gpio_state = gpio_get_level(LED_PIN);
    const char *status_class = gpio_state ? "status-on" : "status-off";
    const char *status_text  = gpio_state ? "ON" : "OFF";

    char response[2048];
    snprintf(response, sizeof(response), html_root_page, status_class, status_text);

    httpd_resp_set_hdr(req, "Cache-Control", "no-cache, no-store, must-revalidate");
    httpd_resp_set_hdr(req, "Pragma", "no-cache");
    httpd_resp_set_hdr(req, "Expires", "0");
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, response, HTTPD_RESP_USE_STRLEN);

    return ESP_OK;
}

// Return current status as plain text "1" or "0"
static esp_err_t status_get_handler(httpd_req_t *req)
{
    char buf[100];
    size_t buf_len = httpd_req_get_url_query_len(req) + 1;

    int gpio = LED_PIN;

    if (buf_len > 1 && httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
        char param[16];
        if (httpd_query_key_value(buf, "gpio", param, sizeof(param)) == ESP_OK) {
            gpio = atoi(param);
        }
    }

    if (gpio < 0 || gpio >= 40) {
        gpio = LED_PIN;   // Fallback to default if invalid
    }

    int state = gpio_get_level(gpio);

    char response[4];
    snprintf(response, sizeof(response), "%d", state);

    httpd_resp_set_type(req, "text/plain");
    httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
    httpd_resp_send(req, response, HTTPD_RESP_USE_STRLEN);

    return ESP_OK;
}

// Handle TURN ON / TURN OFF commands to control GPIO
static esp_err_t output_get_handler(httpd_req_t *req)
{
    char buf[100];
    size_t buf_len = httpd_req_get_url_query_len(req) + 1;

    int gpio = LED_PIN;
    int state = 0;

    if (buf_len > 1 && httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
        char param[16];
        if (httpd_query_key_value(buf, "gpio", param, sizeof(param)) == ESP_OK) {
            gpio = atoi(param);
        }
        if (httpd_query_key_value(buf, "state", param, sizeof(param)) == ESP_OK) {
            state = atoi(param);
        }

        if ((state == 0 || state == 1) && gpio >= 0 && gpio < 40) {
            gpio_set_level(gpio, state);
            ESP_LOGI(TAG, "GPIO %d set to %d via web server", gpio, state);
        }
    }

    // Send empty OK response (client will call updateStatus())
    httpd_resp_set_type(req, "text/plain");
    httpd_resp_send(req, "OK", 2);
    return ESP_OK;
}

// Start the HTTP server
static httpd_handle_t start_web_server(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.stack_size = 8192;
    httpd_handle_t server = NULL;
    
    if (httpd_start(&server, &config) == ESP_OK) {
        ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);

        httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler };
        httpd_register_uri_handler(server, &root);

        httpd_uri_t output = { .uri = "/output", .method = HTTP_GET, .handler = output_get_handler };
        httpd_register_uri_handler(server, &output);

        httpd_uri_t status = { .uri = "/status", .method = HTTP_GET, .handler = status_get_handler };
        httpd_register_uri_handler(server, &status);

        return server;
    }
    ESP_LOGE(TAG, "Failed to start server");
    return NULL;
}

// Wi-Fi and IP event handler
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) {
        esp_wifi_connect();
    } 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, "Got IP Address: " IPSTR, IP2STR(&event->ip_info.ip));
        ESP_LOGI(TAG, "Web Server ready! Access at http://" IPSTR "/", IP2STR(&event->ip_info.ip));
    }
}

void app_main(void)
{
    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();
        nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // Initialize TCP/IP stack and event loop
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // Create default Wi-Fi STA interface
    esp_netif_create_default_wifi_sta();

    // Initialize Wi-Fi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // Register event handlers
    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));
    // Configure Wi-Fi STA
    wifi_config_t wifi_config = { 
        .sta = { 
            .ssid = MY_ESP_WIFI_SSID, 
            .password = MY_ESP_WIFI_PASS 
        }
    };
    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());

    // Configure GPIO 5
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_DISABLE,
        .mode = GPIO_MODE_INPUT_OUTPUT,
        .pin_bit_mask = (1ULL << LED_PIN),
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .pull_up_en = GPIO_PULLUP_DISABLE
    };
    ESP_ERROR_CHECK(gpio_config(&io_conf));
    gpio_set_level(LED_PIN, 0);

    // Start the web server (it will be ready once IP is assigned)
    httpd_handle_t server = start_web_server();
    if (server) {
        ESP_LOGI(TAG, "Web Server initialized. Waiting for Wi-Fi connection...");
    }
}

View raw code

How the Code Works

In this section, we’ll take a look at the main code sections to see how it works.

Libraries

We start by including the required libraries:

  • stdio.h – the standard C library will be used for the printf function that prints the debugging information in the serial monitor;
  • string.h – the standard C library used for string manipulation;
  • stdlib.h – used for memory management and number/string conversions;
  • FreeRTOS.h – provides the core FreeRTOS types and functions;
  • task.h – allows to use task management;
  • esp_system.h – system level functions like restart and hardware info;
  • esp_wifi.h – library for Wi-Fi configuration (station and access point modes and connection options);
  • esp_event.h – handles events like Wi-Fi status changes;
  • esp_log.h – offers a framework to format log messages in the serial monitor for debugging;
  • nvs_flash.h – stores key-value data in non-volatile storage (NVS) memory persistently;
  • esp_netif.h – provides the network interface;
  • esp_http_server.h – library to create an HTTP server and handle HTTP requests/responses;
  • driver/gpio.h – includes the functions required to configure and control GPIOs;
  • sdkconfig.h – includes the project’s configuration file.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "driver/gpio.h"
#include "sdkconfig.h"

Wi-Fi Configuration

These variables will be retrieved from the menuconfig options as described in the previous section “Create the Wi-Fi Config Options File – Kconfig.projbuild“.

#define MY_ESP_WIFI_SSID      CONFIG_ESP_WIFI_SSID
#define MY_ESP_WIFI_PASS      CONFIG_ESP_WIFI_PASSWORD

Logging Tag

The “web_server” prefix will be used to log all the debugging messages.

static const char *TAG = "web_server";

Pin Assignment

This line assigns LED_PIN variable to GPIO 5; you can change it to a different number to match your board pinout or to control a different GPIO.

#define LED_PIN GPIO_NUM_5

HTML – Root Web Page

This is simple HTML web page with some CSS and JavaScript that will be sent to the connected client. The web page is created line by line, with each line enclosed is inside quotation marks and concatenated in a single variable.

static const char *html_root_page = 
    "<!DOCTYPE html>"
    "<html>"
    "<head>"
    "<title>ESP-IDF Web Server - Output Control</title>"
    "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
    "<style>"
    "body { font-family: Arial, sans-serif; background-color: #f0f0f0; text-align: center; padding: 40px; margin: 0; }"
    "h1 { color: #333; margin-bottom: 10px; }"
    "p { font-size: 20px; color: #666; }"
    ".status { font-weight: bold; margin: 20px 0; }"
    ".status-on { color: #28a745; }"
    ".status-off { color: #dc3545; }"
    ".button {"
    "    display: inline-block; padding: 15px 32px; margin: 15px;"
    "    font-size: 20px; font-weight: bold; cursor: pointer; border: none;"
    "    border-radius: 8px; color: white; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"
    "}"
    ".on { background-color: #28a745; }"
    ".off { background-color: #dc3545; }"
    ".button:hover { opacity: 0.9; transform: translateY(-2px); }"
    ".button:active { transform: translateY(1px); }"
    "</style>"
    "</head>"
    "<body>"
    "<h1>ESP-IDF Web Server - Output Control</h1>"
    "<p>GPIO 5 Status: <span id=\"status\" class=\"status %s\">%s</span></p>"
    "<button onclick=\"setOutput(1)\" class=\"button on\">TURN ON</button>"
    "<button onclick=\"setOutput(0)\" class=\"button off\">TURN OFF</button>"
    "<script>"
    "function updateStatus() {"
    "    fetch('/status?gpio=5')"
    "        .then(r => r.text())"
    "        .then(text => {"
    "            const state = parseInt(text.trim());"
    "            const el = document.getElementById('status');"
    "            if (state === 1) {"
    "                el.className = 'status status-on';"
    "                el.textContent = 'ON';"
    "            } else {"
    "                el.className = 'status status-off';"
    "                el.textContent = 'OFF';"
    "            }"
    "        })"
    "        .catch(() => {});"
    "}"
    "function setOutput(state) {"
    "    fetch(`/output?gpio=5&state=${state}`)"
    "        .then(() => updateStatus())"
    "        .catch(() => {});"
    "}"
    "setInterval(updateStatus, 10000);"
    "window.onload = updateStatus;"
    "</script>"
    "</body>"
    "</html>";

Let’s describe briefly how the web page works. It displays a centered title with the current status of GPIO 5. Then, it has two buttons to control the ESP32 GPIO. These two buttons call the setOutput() JavaScript function.

"<h1>ESP-IDF: Output Web Server</h1>"
"<p>GPIO 5 Status: <span id=\"status\" class=\"status %s\">%s</span></p>"
"<button onclick=\"setOutput(1)\" class=\"button on\">TURN ON</button>"
"<button onclick=\"setOutput(0)\" class=\"button off\">TURN OFF</button>"

The setOutput() JavaScript function makes an HTTP GET request in the URL /output?gpio=5&state=0 to actually change the state of the GPIO.

"function setOutput(state) {"
"    fetch(`/output?gpio=5&state=${state}`)"
"        .then(() => updateStatus())"
"        .catch(() => {});"
"}"

Finally, the updateStatus() JavaScript function calls the URL /status?gpio=5 to check the current GPIO state every 10 seconds and updates the state text accordingly.

"function updateStatus() {"
"    fetch('/status?gpio=5')"
"        .then(r => r.text())"
"        .then(text => {"
"            const state = parseInt(text.trim());"
"            const el = document.getElementById('status');"
"            if (state === 1) {"
"                el.className = 'status status-on';"
"                el.textContent = 'ON';"
"            } else {"
"                el.className = 'status status-off';"
"                el.textContent = 'OFF';"
"            }"
"        })"
"        .catch(() => {});"
"}"
"setInterval(updateStatus, 10000);"

HTTP GET – Root Handler

The root_get_handler() function reads the current state of the LED GPIO pin, and serves the html_root_page web page created earlier with the update LED status.

static esp_err_t root_get_handler(httpd_req_t *req)
{
    int gpio_state = gpio_get_level(LED_PIN);
    const char *status_class = gpio_state ? "status-on" : "status-off";
    const char *status_text  = gpio_state ? "ON" : "OFF";

    char response[2048];
    snprintf(response, sizeof(response), html_root_page, status_class, status_text);

    httpd_resp_set_hdr(req, "Cache-Control", "no-cache, no-store, must-revalidate");
    httpd_resp_set_hdr(req, "Pragma", "no-cache");
    httpd_resp_set_hdr(req, "Expires", "0");
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, response, HTTPD_RESP_USE_STRLEN);

    return ESP_OK;
}

HTTP GET – /status handler

The status_get_handler() function checks the current state of the GPIO for example /status?gpio=5, and replies with a request in plain text: 0 (off) or 1 (on).

static esp_err_t status_get_handler(httpd_req_t *req)
{
    char buf[100];
    size_t buf_len = httpd_req_get_url_query_len(req) + 1;

    int gpio = LED_PIN;

    if (buf_len > 1 && httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
        char param[16];
        if (httpd_query_key_value(buf, "gpio", param, sizeof(param)) == ESP_OK) {
            gpio = atoi(param);
        }
    }

    if (gpio < 0 || gpio >= 40) {
        gpio = LED_PIN;   // Fallback to default if invalid
    }

    int state = gpio_get_level(gpio);

    char response[4];
    snprintf(response, sizeof(response), "%d", state);

    httpd_resp_set_type(req, "text/plain");
    httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
    httpd_resp_send(req, response, HTTPD_RESP_USE_STRLEN);

    return ESP_OK;
}

HTTP GET – /output Handler

The output_get_handler() function is an HTTP GET handler that reads the <gpio> and <state> parameters from the URL request, for example /output?gpio=5&state=1. Then, it validates the values sent in the request and changes the GPIO state to 0 or 1 according to the request. If it’s a successful request, it replies back with OK.

static esp_err_t output_get_handler(httpd_req_t *req)
{
    char buf[100];
    size_t buf_len = httpd_req_get_url_query_len(req) + 1;

    int gpio = LED_PIN;
    int state = 0;

    if (buf_len > 1 && httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
        char param[16];
        if (httpd_query_key_value(buf, "gpio", param, sizeof(param)) == ESP_OK) {
            gpio = atoi(param);
        }
        if (httpd_query_key_value(buf, "state", param, sizeof(param)) == ESP_OK) {
            state = atoi(param);
        }

        if ((state == 0 || state == 1) && gpio >= 0 && gpio < 40) {
            gpio_set_level(gpio, state);
            ESP_LOGI(TAG, "GPIO %d set to %d via web server", gpio, state);
        }
    }

    // Send empty OK response (client will call updateStatus())
    httpd_resp_set_type(req, "text/plain");
    httpd_resp_send(req, "OK", 2);
    return ESP_OK;
}

Start the HTTP Web Server

The start_web_server() function starts by configuring the server (uses the default port 80):

httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.stack_size = 8192;
httpd_handle_t server = NULL;

The httpd_start(&server, &config) starts the actual HTTP web server and starts listening to connections on port 80, so a client can connect. Then, we register the following routes:

  • / root: serves the main web page
  • /output output: controls the ESP32 GPIO on/off
  • /status status: checks the current status of the GPIO to update the web page text label
if (httpd_start(&server, &config) == ESP_OK) {
    ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);

    httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler };
    httpd_register_uri_handler(server, &root);

    httpd_uri_t output = { .uri = "/output", .method = HTTP_GET, .handler = output_get_handler };
    httpd_register_uri_handler(server, &output);

    httpd_uri_t status = { .uri = "/status", .method = HTTP_GET, .handler = status_get_handler };
    httpd_register_uri_handler(server, &status);

    return server;
}

If it fails to start the HTTP web server, it prints a failed message.

ESP_LOGE(TAG, "Failed to start HTTP server");

Wi-Fi and IP event handler

This callback function runs when an event related to Wi-Fi happens.

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_LOGI(TAG, "Wi-Fi STA started. Connecting to %s...", MY_ESP_WIFI_SSID);
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGW(TAG, "Wi-Fi disconnected. Retrying connection...");
        esp_wifi_connect();
    } 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, "Got IP Address: " IPSTR, IP2STR(&event->ip_info.ip));
        ESP_LOGI(TAG, "Web Server ready! Access at http://" IPSTR "/", IP2STR(&event->ip_info.ip));
    }
}

For example, when the ESP32 gets assigned an IP address, it triggers the IP_EVENT that prints this message in the Serial Monitor:

ESP_LOGI(TAG, "Got IP Address: " IPSTR, IP2STR(&event->ip_info.ip));
ESP_LOGI(TAG, "Web Server ready! Access at http://" IPSTR "/", IP2STR(&event->ip_info.ip));

app_main(void)

When creating an ESP-IDF project, the app_main function will always be called to run. This function is where you need to write your code for any ESP-IDF applications; it is the equivalent of the setup() in Arduino programming. When the ESP32 boots, the ESP-IDF framework calls app_main.

void app_main(void)
{
    // your code goes here
}

In the app_main(void) function, you start by initializing the NVS (storage). The ESP32 stores the Wi-Fi settings in flash.

esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());
    ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

Then, initialize the TCP/IP stack required for network functions usage.

ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());

Start the Wi-Fi interface in station mode, so the ESP32 can connect to the router. It also initializes the Wi-Fi functionalities.

esp_netif_create_default_wifi_sta();

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

Assign the Wi-Fi events so that when the ESP32 establishes a Wi-Fi connection or gets an IP address, it runs the corresponding Wi-Fi event.

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));

These next few lines set the SSID and password so the ESP32 can connect to your network, set the ESP in station mode, and start Wi-Fi.

wifi_config_t wifi_config = {
    .sta = {
        .ssid = MY_ESP_WIFI_SSID,
        .password = MY_ESP_WIFI_PASS,
    },
};
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());

Create the GPIO config variable, with the GPIO, mode and pull-up/pull-down resistors disabled.

gpio_config_t io_conf = {
    .intr_type = GPIO_INTR_DISABLE,
    .mode = GPIO_MODE_INPUT_OUTPUT,
    .pin_bit_mask = (1ULL << LED_PIN),
    .pull_down_en = GPIO_PULLDOWN_DISABLE,
    .pull_up_en = GPIO_PULLUP_DISABLE
};

Then, call the gpio_config(&io_conf) function to configure the GPIO as an output, because we want to control its state with HIGH/LOW. Set the GPIO to 0 = LOW when the board first boots.

ESP_ERROR_CHECK(gpio_config(&io_conf));
gpio_set_level(LED_PIN, 0);

Finally, start the web server:

httpd_handle_t server = start_web_server();
if (server) {
    ESP_LOGI(TAG, "Web Server initialized. Waiting for Wi-Fi connection...");
}

Build and Flash Code to the ESP32 Board

To build and flash ESP-IDF code to the ESP32, you always need to follow this procedure. You need to select the flash method (UART), the COM port number, the target device (ESP32), build the code, and finally, flash it to the board. All these commands are available in the bottom menu bar of VS Code.

Make sure all your options are correct (they may already be properly configured if you used the project wizard).

VS Code ESP-IDF Check All the Configured Settings UART COM Port Target Board

However, if your setup is not correct, follow the next instructions to ensure everything is set up correctly. First, click the “Star” icon and select the flash method as UART.

VS Code ESP-IDF Select Flash UART Option to Program Flash ESP32

While the ESP32 board is connected to your computer, click the COM Port (plug icon) and select the correct port number that refers to your ESP32.

VS Code ESP-IDF Programming ESP32 Board Select Correct COM Port Number

You also need to select the target device. Click on the chip icon at the bottom bar. In my case, I have an ESP32 with the esp32s3 chip.

VS Code Select the ESP32 S3 or Correct Target Device ESP-IDF

For this board, I also need to select the configuration: ESP32-S chip (via builtin USB-JTAG).

VS Code ESP-IDF Select the ESP32 S3 chip via built in USB JTAG Target Device

Finally, your command bar at the bottom of VS Code should have similar options selected.

VS Code ESP-IDF Check All the Configured Settings UART COM Port Target Board

Now, you can build the project by clicking the wrench icon (Build Project) as shown in the image below.

VS Code Build Project Example Code ESP32 ESP-IDF

The first time you build a project, it usually takes a bit more time. Once completed, it should print a similar message in the Terminal menu and show a “Build Successfully” message.

VS Code Build Example Project ESP32 ESP-IDF Success Message

This is the final step. You can now flash the ESP-IDF project to the ESP32 by clicking the “Flash Device” button (thunder icon).

VS Code Flash Hello World Code Project to ESP32 ESP-IDF

Depending on your board, you might need to hold down the on-board BOOT button on your ESP32 to put it into flashing mode. Once the process is completed, it will pop-up a info message saying “Flash Done“.

VS Code Flash Hello World Project to ESP32 ESP-IDF Done Success Message

Schematic Diagram

Here’s a list of the parts you need to build the circuit:

Connect an LED to GPIO 5 as shown in the schematic diagram below to a board with ESP32-S3 chip.

ESP-IDF ESP32 Output Control Web Server LED Wiring Schematic Diagram

Demonstration

If you followed all the steps, the example should be running successfully on your board. Open your Terminal window — click the “Monitor Device” tool that is illustrated with a screen icon.

VS Code Open Terminal Window Monitor Device ESP32 ESP-IDF

The ESP32 connects to Wi-Fi and prints its IP address on the Serial Monitor. Copy that IP address because you need it to access the ESP32 web server.

ESP-IDF ESP32 Simple Web Server Terminal Demonstration

Note: if nothing shows up on the Serial Monitor, press the ESP32 “EN” button (ENABLE/RESET button next to the microUSB port).

To access your web server in your local network, open your browser and type the ESP32 IP address. The following page should load:

ESP-IDF ESP32 Web Server Output Control Turn On Off Demonstration

Press the TURN ON / TURN OFF buttons to control the ESP32 GPIO:

ESP-IDF ESP32 Output Control Web Server LED TURN ON Demonstration

The text label will update to “OFF” or “ON” depending on the current LED state.

ESP-IDF ESP32 Web Server Output Control Status Text Label Demonstration

You can also check the Terminal to view the requests that the ESP32 is handling to control the GPIO on/off.

ESP-IDF ESP32 Web Server Output Control Terminal Monitor

Wrapping Up

In this tutorial, you’ve learned how to build a simple ESP-IDF ESP32 web server to control the ESP32 GPIOs. You can easily expand this guide to control multiple GPIOs.

You might find it helpful to read other ESP-IDF guides:

Meanwhile, you can check our ESP32 resources to learn more about the ESP32 board:

To learn more about creating ESP32 web servers (but using Arduino Core), we recommend exploring our eBook: Build Web Servers with ESP32 and ESP8266 (3rd Edition).

Thanks for reading.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Recommended Resources

Build a Home Automation System from Scratch » With Raspberry Pi, ESP8266, Arduino, and Node-RED.

Home Automation using ESP8266 eBook and video course » Build IoT and home automation projects.

Arduino Step-by-Step Projects » Build 25 Arduino projects with our course, even with no prior experience!

What to Read Next…


Enjoyed this project? Stay updated by subscribing our newsletter!

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.