ESP32 Web Server with ESP-IDF

In this tutorial, we will build an ESP32 web server using ESP-IDF. The web server will be able to control the onboard LED of ESP32 connected with GPIO2. ESP board will host the HTML and CSS file in the form of strings and responds to a web client request with these HTML and CSS. Similarly, it will receive HTTP requests from a web client and control the onboard LED.

We will program our ESP32 development board in VS Code with ESP-IDF extension. Before we move ahead, make sure you have the latest version of VS Code installed on your system with the ESP-IDF extension configured.

Components Required:

ESP32 Development board: You can buy from here.

ESP32 ESP-IDF Web Server Overview

A web server respond with a simple HTML and CSS files that represents LED control buttons, LED status, title ‘ESP32 WEB SERVER’ and a lamp symbol. The user will click these buttons to control the ESP32 output which will be GPIO2 through which the onboard LED is connected. User can access the web server through an IP address and any web client application such as a web browser on your laptop or mobile phone.

ESP32 web server esp idf demo

How Does Web Server Work?

As mentioned earlier, a ESP32 acts a web server and served the HTML and CSS files to a web client. It also responds to HTML get requests to take action on GPIO2. Any web browser will act as the client.

We will create the web server with the help of the HTTP web server library available in esp-idf. Moreover, we will create HTML and CSS files and store them in ESP32 as an array of characters. When a user will send a HTTP get request by entering the IP address on the web browser, an ESP32 will respond with a buffer which contains HTML and CSS files.

There are two buttons on the GUI that is “ON” button and “OFF” button. User will be able to control ESP32 GPIO through these buttons. With “ON” button URL redirects to an IP address followed by /led2on and similarly with “OFF” button, URL redirects to an IP address followed by /led2off. By using these sub paths of URL, we trigger a respective callback function inside our code to control an LED and send response to a web client.

ESP32 ESP IDF Web server control LED

ESP32 Web Server with ESP-IDF

In this section, let us create the ESP32 Output Control Web Server using ESP-IDF. The ESP32 board will be in station mode, whereby it will connect to a Wi-Fi network through a router. After a successful connection with the AP, an IP address will be assigned to the board which will be used to access the web server.

Create Project

Open your VS Code and head over to View > Command Palette. Type ESP-IDF: New Project in the search bar and press enter.

Specify the project name and directory. We have named our project ‘ESP_IDF_WEB_SERVER.’ For the ESP-IDF board, we have chosen the custom board option. For ESP-IDF target, we have chosen ESP32 module. Click ‘Choose Template’ button to proceed forward.

ESP32 Web Server Control Output GPIO Project 1

In the Extension, select ESP-IDF option:

ESP-IDF in VS Code New Project 2

We will click the ‘sample_project’ under the get-started tab. Now click ‘Create project using template sample_project.’

ESP-IDF in VS Code New Project 3

You will get a notification that the project has been created. To open the project in a new window, click ‘Yes.’

This opens our ESP_IDF_WEB_SERVER project that we created inside the EXPLORER tab. There are several folders inside our project folder. This is the same for every project which you will create through ESP-IDF Explorer.

Project Configuration

Create a new file named ‘Kconfig.projbuild‘ under the main folder. Copy the code given below in this newly created file.

menu "Example Configuration"

    config ESP_WIFI_SSID
        string "WiFi SSID"
        default "myssid"
        help
            SSID (network name) for the example to connect to.

    config ESP_WIFI_PASSWORD
        string "WiFi Password"
        default "mypassword"
        help
            WiFi password (WPA or WPA2) for the example to use.

    config ESP_MAXIMUM_RETRY
        int "Maximum retry"
        default 5
        help
            Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent.
endmenu
ESP32 Web Server Control Output GPIO Project Configuration

Now head over to the menuconfig. Click the icon shown below. It opens the ESP-IDF SDK Configuration Editor.

ESP32 Web Server Control Output GPIO Project Configuration 2

Scroll down and open the Example Configuration. Here we can set the configuration parameters for this example according to our needs. This includes the Wi-Fi SSID, Wi-Fi password and Maximum retry number. Specify the Wi-Fi SSID and password of your router that the ESP32 board will connect to. By default, the maximum reties are set to 5. You can change it according to your preference. After specifying the configuration parameters, click the Save button on the top.

ESP32 Web Server Control Output GPIO Project 2

ESP32 Control Output GPIO Web Server Code

The main folder contains the source code meaning the main.c file will be found here.

Go to main > main.c and open it. Copy the code given below in your main.c file. This code will create a web server hosted by ESP32, whereby the user will be able to toggle the on-board LED through buttons.

#include <stdio.h>
#include <stdlib.h>
#include <string.h> //Requires by memset
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include <esp_http_server.h>

#include "esp_wifi.h"
#include "esp_event.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "driver/gpio.h"
#include <lwip/sockets.h>
#include <lwip/sys.h>
#include <lwip/api.h>
#include <lwip/netdb.h>

#define LED_PIN 2

char on_resp[] = "<!DOCTYPE html><html><head><style type=\"text/css\">html {  font-family: Arial;  display: inline-block;  margin: 0px auto;  text-align: center;}h1{  color: #070812;  padding: 2vh;}.button {  display: inline-block;  background-color: #b30000; //red color  border: none;  border-radius: 4px;  color: white;  padding: 16px 40px;  text-decoration: none;  font-size: 30px;  margin: 2px;  cursor: pointer;}.button2 {  background-color: #364cf4; //blue color}.content {   padding: 50px;}.card-grid {  max-width: 800px;  margin: 0 auto;  display: grid;  grid-gap: 2rem;  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));}.card {  background-color: white;  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);}.card-title {  font-size: 1.2rem;  font-weight: bold;  color: #034078}</style>  <title>ESP32 WEB SERVER</title>  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">  <link rel=\"icon\" href=\"data:,\">  <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\"    integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">  <link rel=\"stylesheet\" type=\"text/css\" ></head><body>  <h2>ESP32 WEB SERVER</h2>  <div class=\"content\">    <div class=\"card-grid\">      <div class=\"card\">        <p><i class=\"fas fa-lightbulb fa-2x\" style=\"color:#c81919;\"></i>     <strong>GPIO2</strong></p>        <p>GPIO state: <strong> ON</strong></p>        <p>          <a href=\"/led2on\"><button class=\"button\">ON</button></a>          <a href=\"/led2off\"><button class=\"button button2\">OFF</button></a>        </p>      </div>    </div>  </div></body></html>";

char off_resp[] = "<!DOCTYPE html><html><head><style type=\"text/css\">html {  font-family: Arial;  display: inline-block;  margin: 0px auto;  text-align: center;}h1{  color: #070812;  padding: 2vh;}.button {  display: inline-block;  background-color: #b30000; //red color  border: none;  border-radius: 4px;  color: white;  padding: 16px 40px;  text-decoration: none;  font-size: 30px;  margin: 2px;  cursor: pointer;}.button2 {  background-color: #364cf4; //blue color}.content {   padding: 50px;}.card-grid {  max-width: 800px;  margin: 0 auto;  display: grid;  grid-gap: 2rem;  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));}.card {  background-color: white;  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);}.card-title {  font-size: 1.2rem;  font-weight: bold;  color: #034078}</style>  <title>ESP32 WEB SERVER</title>  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">  <link rel=\"icon\" href=\"data:,\">  <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\"    integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">  <link rel=\"stylesheet\" type=\"text/css\"></head><body>  <h2>ESP32 WEB SERVER</h2>  <div class=\"content\">    <div class=\"card-grid\">      <div class=\"card\">        <p><i class=\"fas fa-lightbulb fa-2x\" style=\"color:#c81919;\"></i>     <strong>GPIO2</strong></p>        <p>GPIO state: <strong> OFF</strong></p>        <p>          <a href=\"/led2on\"><button class=\"button\">ON</button></a>          <a href=\"/led2off\"><button class=\"button button2\">OFF</button></a>        </p>      </div>    </div>  </div></body></html>";

static const char *TAG = "espressif"; // TAG for debug
int led_state = 0;

#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
#define EXAMPLE_ESP_MAXIMUM_RETRY CONFIG_ESP_MAXIMUM_RETRY

/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;

/* The event group allows multiple bits for each event, but we only care about two events:
 * - we are connected to the AP with an IP
 * - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1

static int s_retry_num = 0;

static void 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 < EXAMPLE_ESP_MAXIMUM_RETRY)
        {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "retry to connect to the AP");
        }
        else
        {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
        ESP_LOGI(TAG, "connect to the AP fail");
    }
    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:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void connect_wifi(void)
{
    s_wifi_event_group = xEventGroupCreate();

    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_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .password = EXAMPLE_ESP_WIFI_PASS,
            /* Setting a password implies station will connect to all security modes including WEP/WPA.
             * However these modes are deprecated and not advisable to be used. Incase your Access point
             * doesn't support WPA2, these mode can be enabled by commenting below line */
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    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_init_sta finished.");

    /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
     * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
                                           WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
                                           pdFALSE,
                                           pdFALSE,
                                           portMAX_DELAY);

    /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
     * happened. */
    if (bits & WIFI_CONNECTED_BIT)
    {
        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    }
    else if (bits & WIFI_FAIL_BIT)
    {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    }
    else
    {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
    vEventGroupDelete(s_wifi_event_group);
}

esp_err_t send_web_page(httpd_req_t *req)
{
    int response;
    if (led_state == 0)
        response = httpd_resp_send(req, off_resp, HTTPD_RESP_USE_STRLEN);
    else
        response = httpd_resp_send(req, on_resp, HTTPD_RESP_USE_STRLEN);
    return response;
}
esp_err_t get_req_handler(httpd_req_t *req)
{
    return send_web_page(req);
}

esp_err_t led_on_handler(httpd_req_t *req)
{
    gpio_set_level(LED_PIN, 1);
    led_state = 1;
    return send_web_page(req);
}

esp_err_t led_off_handler(httpd_req_t *req)
{
    gpio_set_level(LED_PIN, 0);
    led_state = 0;
    return send_web_page(req);
}

httpd_uri_t uri_get = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = get_req_handler,
    .user_ctx = NULL};

httpd_uri_t uri_on = {
    .uri = "/led2on",
    .method = HTTP_GET,
    .handler = led_on_handler,
    .user_ctx = NULL};

httpd_uri_t uri_off = {
    .uri = "/led2off",
    .method = HTTP_GET,
    .handler = led_off_handler,
    .user_ctx = NULL};

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

    if (httpd_start(&server, &config) == ESP_OK)
    {
        httpd_register_uri_handler(server, &uri_get);
        httpd_register_uri_handler(server, &uri_on);
        httpd_register_uri_handler(server, &uri_off);
    }

    return server;
}

void app_main()
{
    // Initialize NVS
    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);

    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
    connect_wifi();

    // GPIO initialization
    gpio_pad_select_gpio(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);

    led_state = 0;
    ESP_LOGI(TAG, "LED Control Web Server is running ... ...\n");
    setup_server();
}

How the Code Works?

Firstly, we will start by including the necessary libraries that includes the FreeRTOS libraries to generate delays, create tasks and event groups. The esp_http_server.h to setup an HTTP web server, esp_wifi.h to enable Wi-Fi connectivity, esp_event.h to monitor the Wi-Fi events, esp_log.h as the logging library, driver/gpio.h library for the digital output pins of ESP32 etc.

#include <stdio.h>
#include <stdlib.h>
#include <string.h> //Requires by memset
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include <esp_http_server.h>

#include "esp_wifi.h"
#include "esp_event.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "driver/gpio.h"
#include <lwip/sockets.h>
#include <lwip/sys.h>
#include <lwip/api.h>
#include <lwip/netdb.h>

The on-board LED of ESP32 is connected at GPIO2. Therefore, we will define a variable called ‘LED_PIN’ that will hold the GPIO2. This will be used later on in the code to control the LED.

#define LED_PIN 2

HTML and CSS to build the Web Server

Then we have the array containing the HTML and CSS script for the ON response. This includes the title of the web server, an icon of a bulb along with the GPIO which is to be controlled, a paragraph for the GPIO state, and two buttons (ON and OFF).

char on_resp[] = "<!DOCTYPE html><html><head><style type=\"text/css\">html {  font-family: Arial;  display: inline-block;  margin: 0px auto;  text-align: center;}h1{  color: #070812;  padding: 2vh;}.button {  display: inline-block;  background-color: #b30000; //red color  border: none;  border-radius: 4px;  color: white;  padding: 16px 40px;  text-decoration: none;  font-size: 30px;  margin: 2px;  cursor: pointer;}.button2 {  background-color: #364cf4; //blue color}.content {   padding: 50px;}.card-grid {  max-width: 800px;  margin: 0 auto;  display: grid;  grid-gap: 2rem;  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));}.card {  background-color: white;  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);}.card-title {  font-size: 1.2rem;  font-weight: bold;  color: #034078}</style>  <title>ESP32 WEB SERVER</title>  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">  <link rel=\"icon\" href=\"data:,\">  <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\"    integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">  <link rel=\"stylesheet\" type=\"text/css\" ></head><body>  <h2>ESP32 WEB SERVER</h2>  <div class=\"content\">    <div class=\"card-grid\">      <div class=\"card\">        <p><i class=\"fas fa-lightbulb fa-2x\" style=\"color:#c81919;\"></i>     <strong>GPIO2</strong></p>        <p>GPIO state: <strong> ON</strong></p>        <p>          <a href=\"/led2on\"><button class=\"button\">ON</button></a>          <a href=\"/led2off\"><button class=\"button button2\">OFF</button></a>        </p>      </div>    </div>  </div></body></html>";

The HTML code is specified with in the <html> </html> tags. Inside the <style> </style> tags, we specify the styling of the web page. This includes the font family, font size, positioning of the text, size and color for both the buttons etc.

As you may note, the font family of all the text is set as Arial and it is aligned in the center of the web page. The color and padding of h1 is also set. We have two buttons on our web page. One is the ON button and the second is the OFF button. Both buttons have different colors but rest of the styling is the same that includes the border radius, color of text, padding, cursor etc. Moreover, card holds the GPIO state, buttons and the bulb icon. We set size and color of the title of the card, along with the general styling of the card that includes its background color, box shadow etc.

<style type=\"text/css\">html {  font-family: Arial;  display: inline-block;  margin: 0px auto;  text-align: center;}h1{  color: #070812;  padding: 2vh;}.button {  display: inline-block;  background-color: #b30000; //red color  border: none;  border-radius: 4px;  color: white;  padding: 16px 40px;  text-decoration: none;  font-size: 30px;  margin: 2px;  cursor: pointer;}.button2 {  background-color: #364cf4; //blue color}.content {   padding: 50px;}.card-grid {  max-width: 800px;  margin: 0 auto;  display: grid;  grid-gap: 2rem;  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));}.card {  background-color: white;  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);}.card-title {  font-size: 1.2rem;  font-weight: bold;  color: #034078}</style>

After that we mention the title of the web page. It is placed inside the <title> </title> tags. In our case, we have set the title of our web page as “ESP32 WEB SERVER.” This will be displayed in the title bar of the browser.

<title>ESP32 WEB SERVER</title>  

The following meta tag will enable our web server to be viewed on different devices.

<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> 

As we mentioned before, we want to add an icon of a bulb in our web page. Hence, the following link tag will acquire the icon from fontawesome.

 <link rel=\"icon\" href=\"data:,\">  <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\"    integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">

Inside the <body> </body> tags, the heading, paragraphs and the buttons will be included.

<body>  <h2>ESP32 WEB SERVER</h2>  <div class=\"content\">    <div class=\"card-grid\">      <div class=\"card\">        <p><i class=\"fas fa-lightbulb fa-2x\" style=\"color:#c81919;\"></i>     <strong>GPIO2</strong></p>        <p>GPIO state: <strong> ON</strong></p>        <p>          <a href=\"/led2on\"><button class=\"button\">ON</button></a>          <a href=\"/led2off\"><button class=\"button button2\">OFF</button></a>        </p>      </div>    </div>  </div></body>

The heading of our web server is “ESP32 WEB SERVER” which is placed inside the <h2> </h2> tags.

<h2>ESP32 WEB SERVER</h2>

After the heading, we have a paragraph that displays the lamp icon and the GPIO pin that we are controlling which is GPIO2.

<p><i class=\"fas fa-lightbulb fa-2x\" style=\"color:#c81919;\"></i>     <strong>GPIO2</strong></p> 

We will use Fontawesome website to create the font. The lamp icon is acquired from this website. Let us show you how to do that. First, open fontawesome website. Now type ‘light’ as shown below and press enter.

FontAwesome Create Font 1

Various different icons will be shown according to your search result. Click the one you want to add. The link tag will appear in a window.

FontAwesome Create Font 2

This will be used inside our HTML script as shown below:

<p><i class=\"fas fa-lightbulb fa-2x\" style=\"color:#c81919;\"></i>     <strong>GPIO2</strong></p> 

The second paragraph will display the GPIO state which is “ON” in this case.

<p>GPIO state: <strong> ON</strong></p>

Finally, we have a paragraph that depicts the two buttons. The first button displays the text ‘ON’ and redirects the webpage to the URL /led2on when this button is clicked. The second button displays the text ‘OFF’ and redirects the webpage to the URL /led2off.

    <p> <a href=\"/led2on\"><button class=\"button\">ON</button></a><a href=\"/led2off\"><button class=\"button button2\">OFF</button></a> </p>

Then we have the array containing the HTML and CSS script for the OFF response. It is similar to the ON response array defined previously, however, this time the paragraph displaying the GPIO state will now show “OFF.”

char off_resp[] = "<!DOCTYPE html><html><head><style type=\"text/css\">html {  font-family: Arial;  display: inline-block;  margin: 0px auto;  text-align: center;}h1{  color: #070812;  padding: 2vh;}.button {  display: inline-block;  background-color: #b30000; //red color  border: none;  border-radius: 4px;  color: white;  padding: 16px 40px;  text-decoration: none;  font-size: 30px;  margin: 2px;  cursor: pointer;}.button2 {  background-color: #364cf4; //blue color}.content {   padding: 50px;}.card-grid {  max-width: 800px;  margin: 0 auto;  display: grid;  grid-gap: 2rem;  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));}.card {  background-color: white;  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);}.card-title {  font-size: 1.2rem;  font-weight: bold;  color: #034078}</style>  <title>ESP32 WEB SERVER</title>  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">  <link rel=\"icon\" href=\"data:,\">  <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\"    integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">  <link rel=\"stylesheet\" type=\"text/css\"></head><body>  <h2>ESP32 WEB SERVER</h2>  <div class=\"content\">    <div class=\"card-grid\">      <div class=\"card\">        <p><i class=\"fas fa-lightbulb fa-2x\" style=\"color:#c81919;\"></i>     <strong>GPIO2</strong></p>        <p>GPIO state: <strong> OFF</strong></p>        <p>          <a href=\"/led2on\"><button class=\"button\">ON</button></a>          <a href=\"/led2off\"><button class=\"button button2\">OFF</button></a>        </p>      </div>    </div>  </div></body></html>";

This code uses Informational Logging. The log function takes in two arguments. The first argument is the tag and the second argument is a formatted string. Therefore this global variable will be useful while calling the ESP_LOGI() functions. Here, “espressif” is the tag that will be used while logging.

static const char *TAG = "espressif"; // TAG for debug
int led_state = 0;

Moreover, we the integer variable ‘led_state’ holds the current state of the LED. Initially, it holds the value 0.

int led_state = 0;

Next, we have defined the configuration parameters that we set in the menuconfig. If you have already defined these parameters in project configuration, then there is no need to hard code values here. However, if you want to define these parameters in the main.c file, then define them inside double quotation marks. For example: #define EXAMPLE_ESP_WIFI_SSID “YOUR_SSID”

#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
#define EXAMPLE_ESP_MAXIMUM_RETRY CONFIG_ESP_MAXIMUM_RETRY

Connect with Wi-Fi

Next, we declare a variable ‘s_wifi-event_group’ to hold the created FreeRTOS event group. This FreeRTOS event group is used to signal when we are connected to Wi-Fi.

static EventGroupHandle_t s_wifi_event_group;

Then we define two bits for the Wi-Fi events that are of importance to us. These include the bit for a successful Wi-Fi connection with an AP and another bit for a failed Wi-Fi connection with an AP after the maximum number of retries are over.

#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
event_handler()

Then we have the event_handler() function which handles the Wi-Fi events. This acts as a callback function when either Wi-Fi event or IP event occurs.

The IP event occurs when the ESP32 board connects with our access point and the router assigns an IP address to it. IP events can be one of the following types. But here we are using only IP_EVENT_STA_GOT_IP.

/** IP event declarations */
typedef enum {
    IP_EVENT_STA_GOT_IP,               /*!< station got IP from connected AP */
    IP_EVENT_STA_LOST_IP,              /*!< station lost IP and the IP is reset to 0 */
    IP_EVENT_AP_STAIPASSIGNED,         /*!< soft-AP assign an IP to a connected station */
    IP_EVENT_GOT_IP6,                  /*!< station or ap or ethernet interface v6IP addr is preferred */
    IP_EVENT_ETH_GOT_IP,               /*!< ethernet got IP from connected AP */
    IP_EVENT_ETH_LOST_IP,              /*!< ethernet lost IP and the IP is reset to 0 */
    IP_EVENT_PPP_GOT_IP,               /*!< PPP interface got IP */
    IP_EVENT_PPP_LOST_IP,              /*!< PPP interface lost IP */
} ip_event_t;

Firstly, when the Wi-Fi events (WIFI_EVENT and WIFI_EVENT_STA_START) occur, we connect the ESP32 with the AP. We use the function esp_wifi_connect() to connect the board with the AP.

If the ESP32 board is unable to connect to an AP within the maximum number of retries set, then it again tries to connect with the AP and logs “retry to connect to the AP”. However, if the ESP32 is unable to establish connection with the AP after the maximum number of retries are over, then it logs “connect to the AP fail” and sets the WIFI_FAIL_BIT. Now, if the ESP32 board is able to successfully connect with an AP, then it suggests that both the IP events (IP_EVENT and IP_EVENT_STA_GOT_IP) occurred. In this case, it prints the ip address as an output. Moreover, it also sets the WIFI_CONNECTED_BIT.

static void 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 < EXAMPLE_ESP_MAXIMUM_RETRY)
        {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "retry to connect to the AP");
        }
        else
        {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
        ESP_LOGI(TAG, "connect to the AP fail");
    }
    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:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

connect_wifi()

The connect_wifi() function will be called inside the app_main() function to initialize Wi-Fi in station mode for ESP32.

void connect_wifi(void)
{
    s_wifi_event_group = xEventGroupCreate();

    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_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .password = EXAMPLE_ESP_WIFI_PASS,
            /* Setting a password implies station will connect to all security modes including WEP/WPA.
             * However these modes are deprecated and not advisable to be used. Incase your Access point
             * doesn't support WPA2, these mode can be enabled by commenting below line */
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    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_init_sta finished.");

    /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
     * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
                                           WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
                                           pdFALSE,
                                           pdFALSE,
                                           portMAX_DELAY);

    /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
     * happened. */
    if (bits & WIFI_CONNECTED_BIT)
    {
        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    }
    else if (bits & WIFI_FAIL_BIT)
    {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    }
    else
    {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
    vEventGroupDelete(s_wifi_event_group);
}

We start off by creating a FreeRTOS event group to monitor the connection. This returns a handle to the event group.

s_wifi_event_group = xEventGroupCreate();

The ESP-IDF provides a useful macro called ESP_ERROR_CHECK() that will be used to check whether an error occurred or not. If the function that we pass inside it does not return ESP_OK, then we assert and log the line.

Here we are initializing lwIP through the function esp_netif_init() and create an IwIP task which is also known as a TCP/IP task. lwIP is a TCP/IP library stack provided by ESP-IDF that is used to perform various protocols such as TCP, UDP, DHCP etc.

ESP_ERROR_CHECK(esp_netif_init());

Next, we create a default event loop which enables the system events to be sent to the event task.

ESP_ERROR_CHECK(esp_event_loop_create_default());

Moreover, the following line is used for Wi-Fi default initialization for station mode. This API is used to initialize as well as register event handlers for the default interface which is station in our case. It creates a network interface instance binding Wi-Fi driver and TCP/IP stack. When a station is in the process of connecting to an AP, various processes automatically get handled through this function.

esp_netif_create_default_wifi_sta();

Then we initialize the Wi-Fi allocate resource for the Wi-Fi driver. It is responsible for initiating the Wi-Fi task. It takes in a single parameter cfg which is a pointer to the already initialized Wi-Fi configuration structure which is set to WIFI_INIT_CONFIG_DEFAULT() so that the initialization of the configuration is at default values. Here wifi_init_config_t is a structure that denotes the Wi-Fi stack configuration parameters that are passed inside the esp_wifi_init() function.

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

Next, we create two event handlers called ‘instance_any_id’ for all Wi-Fi events and ‘instance_any_ip’ for when the station obtains the IP.

esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;

Next, we have two registers for the event handler instances that we set up. This way the application task can register for a callback that can listen for events for Wi-Fi and TCP/IP. The first event is ‘ESP_EVENT_ANY_ID‘ and the second event is ‘IP_EVENT_STA_GOT_IP.’ Therefore it will listen to all Wi-Fi events (ESP_EVENT_ANY_ID) and an IP event when the station got its IP address (IP_EVENT_STA_GOT_IP). Hence, the callback event_handler function will be called if any of these two events occur.

ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));

Other wifi events which we can use in our code are:

typedef enum {
    WIFI_EVENT_STA_CONNECTED,            /**< ESP32 station connected to AP */
    WIFI_EVENT_STA_DISCONNECTED,         /**< ESP32 station disconnected from AP */
    WIFI_EVENT_AP_START,                 /**< ESP32 soft-AP start */
    WIFI_EVENT_AP_STOP,                  /**< ESP32 soft-AP stop */
} mdns_used_event_t;

In this part, we set up the SSID and password of the network we want our ESP32 to connect with, as well as assign the security setting.

wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .password = EXAMPLE_ESP_WIFI_PASS,
	    .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };

Moreover, we set Wi-Fi mode as station. To set the Wi-Fi mode as station, we use the function esp_wifi_set_mode() and pass the WiFI_MODE_STA as a parameter inside it.

ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );

Then we assign our network’s SSID and password. This way the Wi-Fi driver gets configured with the AP network credentials and Wi-Fi mode. Then, we start the Wi-Fi connection at our assigned configuration.

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

At this point the information log prints “wifi_init_sta finished.” We are done with the process of initializing the Wi-Fi in station mode.

ESP_LOGI(TAG, "wifi_init_sta finished.");

Next the application moves to a blocked state whereby it waits for either of the bits in the event group to be set. This happens after the Wi-Fi is initialized and started. The application remains in the blocked state until either the Wi-Fi gets connected which is monitored by WIFI_CONNECTED_BIT or the W-Fi fails to connect within the maximum number of retries which is monitored by WIFI_FAIL_BIT.

Now if the event_handler() function sets the WIFI_CONNECTED_BIT which suggests that the ESP32 was able to successfully connect with an AP, then the informational log will print that it got connected to the SSID and password of the specified AP. If on the other hand, the event_handler() function sets the WIFI_FAIL_BIT which suggests that the ESP32 was not able to connected to the AP even after the maximum number of retries have been reached, then the informational log will print that it failed to connect to the SSID and password of the specified AP.

 EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY);


    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
                 EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }

Moreover, we delete the event group ‘s_wifi_event_group’ using the vEventGroupDelete() function. It was created before by using a call to xEventGroupCreate().

vEventGroupDelete(s_wifi_event_group);

esp_http_server.h APIs to host a web server on ESP32

The send_web_page() function is responsible for sending a complete HTTP response by calling httpd_resp_send() function. The httpd_resp_send() API takes in three parameters. The first parameter is the request which is being responded to. The second parameter is the buffer from where the context has to be obtained. The third parameter is the length of the buffer. In the send_web_page() function, the HTTP response ‘off_resp’ is returned if the state of the led is 0. Otherwise, the HTTP response ‘on_resp’ is returned.

esp_err_t send_web_page(httpd_req_t *req)
{
    int response;
    if (led_state == 0)
        response = httpd_resp_send(req, off_resp, HTTPD_RESP_USE_STRLEN);
    else
        response = httpd_resp_send(req, on_resp, HTTPD_RESP_USE_STRLEN);
    return response;
}

We have the uri-get handler structure shown below. Its chosen method is HTTP_GET, URL is “/” and the handler for this is get_req_handler which returns the send_web_page() response.

httpd_uri_t uri_get = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = get_req_handler,
    .user_ctx = NULL};

esp_err_t get_req_handler(httpd_req_t *req)
{
    return send_web_page(req);
}

We have the uri_on structure shown below. Its chosen method is HTTP_GET, URL is “/led2on” and the handler for this is led_on_handler which returns the send_web_page() response and sets the level of the LED_PIN and ‘led_state’ to 1.

httpd_uri_t uri_on = {
    .uri = "/led2on",
    .method = HTTP_GET,
    .handler = led_on_handler,
    .user_ctx = NULL};

esp_err_t led_on_handler(httpd_req_t *req)
{
    gpio_set_level(LED_PIN, 1);
    led_state = 1;
    return send_web_page(req);
}

We have the uri_off structure shown below. Its chosen method is HTTP_GET, URL is “/led2off” and the handler for this is led_off_handler which returns the send_web_page() response and sets the level of the LED_PIN and ‘led_state’ to 0.

httpd_uri_t uri_off = {
    .uri = "/led2off",
    .method = HTTP_GET,
    .handler = led_off_handler,
    .user_ctx = NULL};

esp_err_t led_off_handler(httpd_req_t *req)
{
    gpio_set_level(LED_PIN, 0);
    led_state = 0;
    return send_web_page(req);
}

The following function will start the HTTP web server and return server which is the http handle.

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

    if (httpd_start(&server, &config) == ESP_OK)
    {
        httpd_register_uri_handler(server, &uri_get);
        httpd_register_uri_handler(server, &uri_on);
        httpd_register_uri_handler(server, &uri_off);
    }

    return server;
}

It starts by defining the default configuration and sets an empty handle to http server handle. It is set as NULL.

The httpd_start() function starts the httpd server. It takes in two parameters. Configuration for new instance of the server and handle to the instance of the server. If the instance was successfully created, which means this function returned ESP_OK then we will register the URI handlers. It takes in two parameters. The first parameter is the handle to HTTPD server instance which is ‘server’ in this case. The second parameter is the pointer to the handler that we what to register. We are registering three URI handlers: uri_get, uri_on and uri_off.

app_main()

Inside the app_main() function, first NVS is initialized by calling the nvs_flash_init() function. Then we will check if the NVS partition does not contain any empty pages or if it contains data in a format which is different from the current version of code. If any of these condition is true then we will erase the NVS flash using nvs_flash_erase() and initialize it again.

    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 we will print “ESP_WIFI_MODE_STA” in the terminal and call the connect_wifi() function to connect the Wi-Fi for station mode.

    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
    connect_wifi();

The next step is to configure the LED_PIN as a GPIO. To do that, we will use the gpio_pad_select_gpio() function. Specify the LED_PIN as a parameter inside this function. Then, we will set the direction of the pin as an output using the gpio_set_direction() function. This function takes in two arguments. The first argument is the GPIO pin and the second argument is the mode (input or output) we want to set the pin in. In our case, we want to set the LED_PIN as an output pin.

    gpio_pad_select_gpio(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);

The led_state variable is set to 0. At this point, the terminal prints the message “LED Control Web Server is running… …” The setup_server() function is called to start the HTTP web server.

    led_state = 0;
    ESP_LOGI(TAG, "LED Control Web Server is running ... ...\n");
    setup_server();

Compiling the Sketch

To flash your chip, type the following command in the serial terminal. Remember to replace the COM port with the one through which your board is connected.

idf.py -p COMX flash monitor
ESP32 Web Server Control Output GPIO Project Flash Chip

After the code flashes successfully, you can view all the informational logs. First, the station Wi-Fi is initialized. Then we can view the esp_netif_handlers which includes the IP, mask, and gw addresses. Then we get the log “got IP” followed by the ip address. We will use this IP address to access the web server.

ESP32 Web Server Control Output GPIO Project Terminal

Open a new web browser and type the IP address that you obtained in the terminal, then press enter. The ESP32 web server will open up showing two buttons to control GPIO2. Initially, the LED is OFF which is displayed on the web sever as well.

Now press the ON and OFF buttons and the LED will turn ON/OFF accordingly. Likewise, the state of GPIO2 will also continuously update on the web server.

ESP32 Web Server Control Output GPIO Project Demo 1
ESP32 Web Server Control Output GPIO Project Demo 2

Video demo:

You may also like to read:

If you are using Arduino IDE instead of ESP-IDF, you can refer to these articles:

1 thought on “ESP32 Web Server with ESP-IDF”

Leave a Comment