Веб-сервер на ESP32 в ESP-IDF: управляем GPIO из браузера

Перевод статьи 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 в одной сети: плата отдаёт страницу и управляет 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, чтобы применить изменения.

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...");
}
}
Как работает код#
Пробежимся по основным участкам кода и посмотрим, как он устроен.
Библиотеки#
Начинаем с подключения нужных библиотек:
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.

Светодиод с резистором 220 Ом на GPIO 5.
Демонстрация#
Если вы выполнили все шаги, пример должен успешно работать на плате. Откройте окно терминала — инструмент Monitor Device с иконкой экрана.
ESP32 подключается к Wi-Fi и печатает в мониторе порта свой IP-адрес. Скопируйте его: он понадобится для доступа к веб-серверу ESP32.

IP-адрес платы в мониторе порта.
Примечание: если в мониторе порта ничего не появилось, нажмите на плате кнопку EN (ENABLE/RESET рядом с разъёмом microUSB).
Чтобы открыть веб-сервер в локальной сети, запустите браузер и введите IP-адрес ESP32. Должна загрузиться такая страница:

Страница управления: две кнопки и статус GPIO 5.
Нажимайте TURN ON / TURN OFF, чтобы управлять GPIO платы:

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

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

В мониторе порта видно каждый запрос к серверу.
Подводя итоги#
В этом руководстве вы научились собирать простой веб-сервер на ESP-IDF, который управляет GPIO платы ESP32. Руководство легко расширить на управление несколькими выходами.
Может быть полезно почитать и другие руководства по ESP-IDF (на английском):
- ESP-IDF: ESP32 Simple Web Server (Serve HTML Page)
- ESP-IDF ESP32 Web Server – Basic HTTP Authentication (Login/Logout)
- ESP-IDF: ESP32 GPIO PWM with LEDC (Control LED Brightness)
- ESP-IDF: ESP32 GPIO – Read Analog Input (ADC – Analog to Digital Converter)
А если хочется глубже разобраться в веб-серверах на ESP32, но уже на Arduino Core, — у Random Nerd Tutorials есть электронная книга Build Web Servers with ESP32 and ESP8266 (3-е издание).