Перевод статьи ESP-IDF: ESP32 Web Server – Output Control — Rui Santos & Sara Santos, Random Nerd Tutorials.

Это руководство показывает, как собрать на ESP32 локальный веб-сервер, запрограммировав плату через ESP-IDF: сервер отдаёт простую HTML-страницу с двумя кнопками, которыми включается и выключается один GPIO. Страница открывается с любого устройства с браузером — телефона, планшета, ноутбука — пока вы в одной сети с платой.

Это продолжение линейки про ESP-IDF. Раньше мы уже поднимали на ESP32 веб-сервер в режиме точки доступа, когда плата сама раздаёт Wi-Fi и роутер не нужен. Теперь подключим ESP32 к домашней сети как обычного клиента — и управляем выходом платы с любого устройства в сети.

Что понадобится#

Прежде чем браться за руководство, установите расширение ESP-IDF в VS Code. Если ещё не сделали этого, начните с вводного руководства: Программирование ESP32 на ESP-IDF в VS Code: вводное руководство (или оригинальный гайд Random Nerd Tutorials).

Кроме того, понадобится любая отладочная плата ESP32.

Обзор проекта#

Быстро посмотрим, что именно мы будем собирать; более широкое введение в тему — в руководстве о постройке веб-сервера на ESP-IDF.

Схема проекта: браузер через роутер обращается к веб-серверу на ESP32

Браузер и ESP32 в одной сети: плата отдаёт страницу и управляет GPIO 5.

  • В этом руководстве вы научитесь собирать веб-сервер, который отдаёт HTML-страницу.
  • Когда вы открываете по IP-адресу ESP32 страницу сервера, загружается страница с двумя кнопками.
  • Кнопками TURN ON / TURN OFF можно включать и выключать GPIO платы ESP32.

Создаём проект ESP-IDF из шаблона#

Расширение ESP-IDF позволяет легко создать проект с нуля: все нужные файлы и конфигурации генерируются автоматически. Чтобы создать новый проект ESP-IDF в VS Code:

  • Откройте расширение ESP-IDF Espressif и разверните меню Advanced
  • Нажмите New Project Wizard
  • Выберите Use ESP-IDF v6.X, чтобы задать версию фреймворка ESP-IDF

Откроется новое окно, в котором нужно заполнить поля:

  • Project Name — желаемое имя проекта;
  • Enter Project Directory — нажмите на иконку папки и выберите целевую папку для файлов проекта. Подойдёт любая директория. Важно: не используйте папки Google Drive, One Drive или Dropbox — в процессе сборки создаётся масса файлов, и в облачной папке всё может работать мучительно медленно;
  • ESP-IDF Target — целевой чип; у автора это ESP32 с чипом esp32s3;
  • ESP-IDF Board — для чипа esp32s3 нужно дополнительно выбрать конфигурацию ESP32-S chip (via builtin USB-JTAG);
  • Serial Port — при подключённой к компьютеру плате выберите правильный номер COM-порта, который соответствует вашей ESP32;
  • Choose Template — нажмите синюю кнопку, чтобы создать проект по шаблону.

В меню выберите шаблонный проект ESP-IDF Templates и нажмите кнопку Create project using template sample project.

Открываем проект в VS Code#

Через несколько секунд в VS Code появится уведомление — нажмите Open Project, чтобы открыть только что созданный проект. Если уведомление не появилось, откройте проект вручную: File → Open Folder…, найдите на компьютере папку проекта (имя вы задавали при создании) и нажмите Select Folder.

Готово: шаблонный проект ESP-IDF создан и открыт. ESP-IDF генерирует для проекта множество файлов, папок и подпапок. Для этого руководства рекомендуется оставить все файлы по умолчанию без изменений — мы будем менять только файл main.c, в котором и пишется код примера. Чтобы его открыть: разверните проводник проекта, щёлкните по папке проекта (в примере автора она называется ESP-IDF-PROJECT), разверните папку main и откройте файл main.c.

Файл параметров Wi-Fi — Kconfig.projbuild#

Файл Kconfig.projbuild в ESP-IDF — это конфигурационный файл, который кладут в папку main проекта, чтобы определить собственные пункты в редакторе конфигурации SDK (инструмент menuconfig). Так можно задавать конкретные настройки — например, строки и значения, которые будут использоваться при сборке.

Чтобы создать файл, откройте проводник слева, щёлкните правой кнопкой по папке main и выберите New File…. Назовите новый файл ровно так: Kconfig.projbuild — и скопируйте в него следующее содержимое.

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

Сохраните файл. В редакторе конфигурации SDK появится новое меню Wi-Fi Configuration, куда можно вписать SSID и пароль, чтобы плата подключалась к вашей сети.

Теперь, когда Kconfig.projbuild готов, нажмите иконку шестерёнки, чтобы открыть редактор конфигурации SDK (menuconfig). Вместо того чтобы вписывать реквизиты сети прямо в main.c, задайте их в menuconfig:

  • в поиске наберите «wifi»;
  • впишите свой SSID и пароль в соответствующие поля;
  • нажмите Save, чтобы применить изменения.
Редактор конфигурации SDK: раздел Wi-Fi Configuration с полями SSID и пароля

SSID и пароль задаются в menuconfig, а не в коде.

Код: веб-сервер для управления выходом на ESP-IDF#

Скопируйте следующий код в файл main.c. Он отдаёт базовую HTML-страницу, размещённую на 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...");
    }
}

Исходник кода на GitHub.

Как работает код#

Пробежимся по основным участкам кода и посмотрим, как он устроен.

Библиотеки#

Начинаем с подключения нужных библиотек:

  • stdio.h — стандартная библиотека C, нужна для printf, выводящей отладочную информацию в монитор порта;
  • string.h — стандартная библиотека C для работы со строками;
  • stdlib.h — управление памятью и преобразования чисел и строк;
  • FreeRTOS.h — базовые типы и функции FreeRTOS;
  • task.h — управление задачами;
  • esp_system.h — системные функции вроде перезапуска и сведений о железе;
  • esp_wifi.h — конфигурация Wi-Fi (режимы станции и точки доступа, параметры подключения);
  • esp_event.h — обработка событий вроде изменения статуса Wi-Fi;
  • esp_log.h — фреймворк форматирования отладочных сообщений в мониторе порта;
  • nvs_flash.h — постоянное хранение пар «ключ-значение» в энергонезависимой памяти (NVS);
  • esp_netif.h — сетевой интерфейс;
  • esp_http_server.h — создание HTTP-сервера и обработка HTTP-запросов и ответов;
  • driver/gpio.h — функции для настройки и управления GPIO;
  • sdkconfig.h — заголовочный файл с конфигурацией проекта.
#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#

Эти значения подтягиваются из пунктов menuconfig, которые мы описали выше, в разделе про Kconfig.projbuild.

#define MY_ESP_WIFI_SSID      CONFIG_ESP_WIFI_SSID
#define MY_ESP_WIFI_PASS      CONFIG_ESP_WIFI_PASSWORD

Тег для логов#

Префикс web_server будет использоваться во всех отладочных сообщениях.

static const char *TAG = "web_server";

Назначение вывода#

Эта строка назначает переменной LED_PIN GPIO 5. Замените число, чтобы подстроиться под распиновку вашей платы или управлять другим GPIO.

#define LED_PIN GPIO_NUM_5

HTML — корневая страница#

Это простая HTML-страница с CSS и JavaScript, которая отправляется подключившемуся клиенту. Страница собирается построчно: каждая строка заключена в кавычки, и все они склеиваются в одну переменную.

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

Коротко о том, как страница работает. Она показывает заголовок с текущим статусом GPIO 5, а под ним — две кнопки управления GPIO ESP32. Обе кнопки вызывают JavaScript-функцию setOutput().

"<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>"

Функция setOutput() делает HTTP GET-запрос к URL /output?gpio=5&state=0 — именно он и меняет состояние GPIO.

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

Наконец, функция updateStatus() каждые 10 секунд обращается к /status?gpio=5, проверяет текущее состояние GPIO и обновляет текст статуса на странице.

"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_get_handler() читает текущее состояние вывода светодиода и отдаёт созданную ранее страницу html_root_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;
}

HTTP GET — обработчик /status#

Функция status_get_handler() проверяет текущее состояние GPIO — например, для запроса /status?gpio=5 — и отвечает простым текстом: 0 (выключен) или 1 (включён).

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#

Функция output_get_handler() — HTTP GET-обработчик, который читает из URL-запроса параметры gpio и state (например, /output?gpio=5&state=1), проверяет присланные значения и переключает GPIO в 0 или 1 согласно запросу. При успешном запросе она отвечает 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;
}

Запуск HTTP-сервера#

Функция start_web_server() начинает с конфигурации сервера (используется порт 80 по умолчанию):

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

Вызов httpd_start(&server, &config) поднимает сам HTTP-сервер и начинает слушать подключения на порту 80, так что клиент может подключиться. Затем мы регистрируем такие маршруты:

  • / — корень: отдаёт основную веб-страницу;
  • /output — управление GPIO ESP32 (вкл/выкл);
  • /status — проверка текущего статуса GPIO для обновления текстовой метки на странице.
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;
}

Если поднять HTTP-сервер не удалось, выводится сообщение об ошибке.

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

Обработчик событий Wi-Fi и IP#

Этот колбэк выполняется, когда происходит событие, связанное с Wi-Fi.

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

Например, когда ESP32 получает IP-адрес, срабатывает IP_EVENT и в мониторе порта печатаются такие сообщения:

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#

При создании проекта ESP-IDF для запуска всегда вызывается функция app_main — именно в ней пишется код любого приложения ESP-IDF; это аналог setup() в программировании на Arduino. Когда ESP32 загружается, фреймворк ESP-IDF вызывает app_main. Подробнее о таких отличиях ESP-IDF от Arduino — в нашем обзоре: ESP-IDF и ESP32: ключевые концепции для тех, кто пришёл из Arduino.

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

В app_main мы начинаем с инициализации NVS — ESP32 хранит настройки Wi-Fi во флеш-памяти.

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

Затем инициализируется стек TCP/IP, необходимый для работы сетевых функций.

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

Запускается интерфейс Wi-Fi в режиме станции, чтобы ESP32 мог подключиться к роутеру; здесь же инициализируются функции Wi-Fi.

esp_netif_create_default_wifi_sta();

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

Назначаются обработчики событий Wi-Fi, чтобы при установке соединения или получении IP-адреса выполнялся соответствующий обработчик.

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

Следующие строки задают SSID и пароль для подключения к вашей сети, переводят ESP32 в режим станции и запускают 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());

Создаётся переменная конфигурации GPIO: сам вывод, режим и отключённые подтягивающие резисторы.

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

Затем вызывается функция gpio_config(&io_conf), которая настраивает GPIO как выход, — мы ведь хотим управлять его уровнем HIGH/LOW. При загрузке платы GPIO сразу устанавливается в 0 = LOW.

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

И наконец запускается веб-сервер:

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

Сборка и прошивка платы#

Чтобы собрать и залить код ESP-IDF в ESP32, всегда нужно пройти одну и ту же процедуру: выбрать метод прошивки (UART), номер COM-порта и целевое устройство (ESP32), собрать проект и залить его на плату. Все эти команды доступны в нижней панели VS Code.

Убедитесь, что параметры выставлены верно — если вы пользовались мастером создания проекта, они, скорее всего, уже настроены. Если нет: нажмите иконку звезды и выберите метод прошивки UART; при подключённой к компьютеру плате щёлкните по иконке COM-порта (вилка) и выберите правильный номер порта; затем выберите целевое устройство, кликнув по иконке чипа в нижней панели (у автора — ESP32 с чипом esp32s3, для которого дополнительно выбирается конфигурация ESP32-S chip (via builtin USB-JTAG)).

Теперь можно собрать проект, нажав иконку гаечного ключа (Build Project). Первая сборка обычно занимает заметно больше времени; по завершении в терминале появится сообщение Build Successfully.

Последний шаг — залить проект на ESP32 кнопкой Flash Device (иконка молнии). В зависимости от платы может понадобиться удерживать кнопку BOOT, чтобы перевести ESP32 в режим прошивки. По окончании появится сообщение Flash Done.

Схема подключения#

Для сборки схемы понадобятся следующие детали:

Подключите светодиод к GPIO 5, как показано на схеме ниже, — для платы с чипом ESP32-S3.

Схема подключения светодиода к GPIO 5 платы ESP32

Светодиод с резистором 220 Ом на GPIO 5.

Демонстрация#

Если вы выполнили все шаги, пример должен успешно работать на плате. Откройте окно терминала — инструмент Monitor Device с иконкой экрана.

ESP32 подключается к Wi-Fi и печатает в мониторе порта свой IP-адрес. Скопируйте его: он понадобится для доступа к веб-серверу ESP32.

Монитор порта: ESP32 получил IP-адрес

IP-адрес платы в мониторе порта.

Примечание: если в мониторе порта ничего не появилось, нажмите на плате кнопку EN (ENABLE/RESET рядом с разъёмом microUSB).

Чтобы открыть веб-сервер в локальной сети, запустите браузер и введите IP-адрес ESP32. Должна загрузиться такая страница:

Веб-страница с кнопками TURN ON и TURN OFF

Страница управления: две кнопки и статус GPIO 5.

Нажимайте TURN ON / TURN OFF, чтобы управлять GPIO платы:

Светодиод на ESP32 зажёгся после нажатия TURN ON

Нажали TURN ON — светодиод загорелся.

Текстовая метка будет переключаться между ON и OFF в зависимости от текущего состояния светодиода.

Текстовая метка статуса на странице показывает ON

Метка статуса обновляется сама.

По терминалу можно проследить и запросы, которые обрабатывает ESP32, переключая GPIO вкл/выкл:

Монитор порта: лог запросов к /output и /status

В мониторе порта видно каждый запрос к серверу.

Подводя итоги#

В этом руководстве вы научились собирать простой веб-сервер на ESP-IDF, который управляет GPIO платы ESP32. Руководство легко расширить на управление несколькими выходами.

Может быть полезно почитать и другие руководства по ESP-IDF (на английском):

А если хочется глубже разобраться в веб-серверах на ESP32, но уже на Arduino Core, — у Random Nerd Tutorials есть электронная книга Build Web Servers with ESP32 and ESP8266 (3-е издание).