Асинхронный веб-сервер на ESP32 с WebSocket: панель управления в реальном времени
Асинхронный веб-сервер на ESP32 с WebSocket: переключай реле и следи за данными датчика без перезагрузки страницы. Установка, PROGMEM или LittleFS, esp32.local, пароль и решение проблем.
Асинхронный веб-сервер на ESP32, построенный на библиотеке ESPAsyncWebServer, превращает ESP32 в быстрый веб-сервер, который одновременно отвечает нескольким телефонам и компьютерам. Благодаря встроенной в библиотеку поддержке WebSocket между браузером и платой держится постоянно открытая линия: нажимаешь кнопку на странице — и реле щёлкает мгновенно, а когда меняется температура, число на странице обновляется само, без перезагрузки. Любое другое устройство, на котором открыта та же панель, видит изменение в ту же секунду. Короче говоря, эта пара — самый практичный способ сделать собственную панель умного дома внутри домашней сети без всяких облачных сервисов.
В этом руководстве сначала разберём, чем асинхронный сервер отличается от классической библиотеки WebServer и почему WebSocket быстрее обычных HTTP-запросов. Затем установим нужные библиотеки, запустим самый простой сервер и соберём панель управления в реальном времени с реле и датчиком DHT22. После этого ты узнаешь, как хранить HTML в PROGMEM или LittleFS, заходить по адресу esp32.local вместо IP, работать с несколькими клиентами, добавить на панель простой пароль и исправить самые частые ошибки компиляции и подключения. Код написан так, чтобы работать с ядром ESP32 версий 2.x и 3.x в Arduino IDE.

Что такое асинхронный веб-сервер на ESP32 и как он работает?
Библиотека WebServer.h, которая идёт вместе с ядром ESP32, синхронная: входящие запросы обрабатываются по одному и только тогда, когда внутри loop() вызывается server.handleClient(). Если в loop() есть delay() на 2 секунды, страница тоже ждёт 2 секунды; если подключаются двое сразу, второму приходится ждать, пока обслужат первого. ESPAsyncWebServer, напротив, работает в отдельной задаче FreeRTOS (async_tcp), которую создаёт библиотека AsyncTCP. Когда приходит запрос, его заголовки разбираются в фоне, написанная тобой функция (обратный вызов, callback) вызывается, когда запрос готов, и ответ тоже отправляется в фоне. Тем временем loop() продолжает свою работу. Благодаря этому несколько соединений могут быть открыты одновременно, и становятся возможны постоянно открытые соединения вроде WebSocket и Server-Sent Events (SSE). Если интересно, как устроены два ядра ESP32 и задачи, загляни в статью ESP32 и FreeRTOS.
| Характеристика | WebServer.h (синхронный) | ESPAsyncWebServer (асинхронный) |
|---|---|---|
| Установка | Идёт вместе с ядром ESP32 | 2 библиотеки из «Менеджера библиотек» |
| Обработка запросов | По одному через handleClient() в loop() | В фоновой задаче async_tcp, несколько сразу |
| delay() внутри loop() | Задерживает ответ страницы | На сервер не влияет |
| WebSocket | Нет; нужна отдельная библиотека (например, WebSockets от Markus Sattler) | Встроен (AsyncWebSocket) |
| Server-Sent Events (события от сервера) | Нет | Встроены (AsyncEventSource) |
| Раздача статических файлов | Через serveStatic | serveStatic, автоматическая поддержка .gz и управление кэшем |
| На что обратить внимание | Проще всего в освоении | Никаких delay() и долгой работы внутри обратных вызовов |
Что такое WebSocket и чем он отличается от HTTP-запроса?
При обычном HTTP-запросе браузер открывает соединение, говорит «дай мне эту страницу», получает ответ, и на этом разговор заканчивается. Сервер не может ничего сообщить браузеру по своей инициативе; чтобы увидеть новое значение, браузер должен спросить снова. WebSocket (RFC 6455) — это рукопожатие, которое начинается как HTTP: браузер добавляет к запросу заголовок Upgrade: websocket, ESP32 соглашается ответом 101 Switching Protocols, и то же TCP-соединение остаётся открытым. С этого момента обе стороны могут отправлять сообщения когда угодно (полный дуплекс, full-duplex). Каждое сообщение несёт всего от 2 до 14 байт служебных данных, тогда как каждый HTTP-запрос тащит за собой сотни байт заголовков. Поэтому сообщения WebSocket доходят за миллисекунды, и это самый эффективный способ для панелей, которые отправляют несколько обновлений в секунду. Вот как сравниваются четыре способа держать страницу в курсе показаний датчиков:
| Способ | Как работает | Задержка и накладные расходы | Когда использовать |
|---|---|---|---|
| Обновление страницы (meta refresh) | Браузер каждые несколько секунд заново скачивает всю страницу | Высокие; страница каждый раз мигает | Лучше не использовать |
| Опрос через fetch | JavaScript каждые 1–2 секунды запрашивает небольшой JSON | Средние; каждый запрос несёт новые заголовки | Одно редко меняющееся значение |
| Server-Sent Events (SSE) | Сервер отправляет односторонние события по открытому соединению | Низкие; только от сервера к браузеру | Панели только для наблюдения |
| WebSocket | Двусторонний обмен сообщениями по открытому соединению | Самые низкие; кадры в несколько байт | Кнопки, ползунки и живые данные вместе |
Нужные библиотеки: установка ESPAsyncWebServer и AsyncTCP
- Убедись, что в Arduino IDE установлен пакет плат ESP32 (Espressif Systems); шаги установки есть в статье «Что такое ESP32?»
- Открой «Менеджер библиотек» (Library Manager, Ctrl+Shift+I) и найди ESPAsyncWebServer. Установи библиотеку ESP Async WebServer, автор которой — ESP32Async.
- Эта библиотека не сообщает «Менеджеру библиотек» о своей зависимости, поэтому IDE не предложит доустановить другие библиотеки: отдельно установи библиотеку Async TCP, которую тоже публикует ESP32Async. Для ESP8266 вместо неё нужна библиотека ESP Async TCP от ESP32Async (ESPAsyncTCP.h).
- Для примера с панелью установи DHT sensor library от Adafruit и зависимость Adafruit Unified Sensor, которую она запросит. Если библиотеки ставятся впервые, загляни в руководство по добавлению библиотек в Arduino.
- Если в папке Documents/Arduino/libraries есть папки ESPAsyncWebServer-master, AsyncTCP-master или ESPAsyncWebSrv, когда-то добавленные из ZIP, удали их. Из двух копий одной библиотеки компилятор выбирает не ту.
Первый шаг: самый простой сервер на AsyncWebServer
Прежде чем переходить к панели, посмотрим на небольшом примере, как работает библиотека. Код ниже подключает ESP32 к домашней сети, показывает приветствие на главной странице и включает и выключает встроенный светодиод по адресам /led?durum=1 и /led?durum=0. Загрузи его, открой монитор порта на скорости 115200 бод и введи показанный адрес в браузере телефона, подключённого к той же сети Wi-Fi. Если плата не подключается к сети, воспользуйся диагностическим кодом из руководства «ESP32 не подключается к Wi-Fi».
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4
5const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
6const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
7const int LED_PIN = 2; // çoğu DevKit'te dahili LED / onboard LED on most DevKits
8
9AsyncWebServer server(80); // 80 = standart HTTP portu / standard HTTP port
10bool ledAcik = false;
11
12void setup() {
13 Serial.begin(115200);
14 pinMode(LED_PIN, OUTPUT);
15
16 WiFi.mode(WIFI_STA);
17 WiFi.begin(ssid, password);
18 while (WiFi.status() != WL_CONNECTED) {
19 delay(500);
20 Serial.print(".");
21 }
22 Serial.println();
23 Serial.print("Adres / address: http://");
24 Serial.println(WiFi.localIP());
25
26 // Ana sayfa / home page
27 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
28 request->send(200, "text/plain", "Merhaba, ben ESP32 / Hello from ESP32");
29 });
30
31 // /led?durum=1 yakar, /led?durum=0 söndürür / turns the LED on or off
32 server.on("/led", HTTP_GET, [](AsyncWebServerRequest *request) {
33 if (request->hasParam("durum")) {
34 ledAcik = request->getParam("durum")->value() == "1";
35 digitalWrite(LED_PIN, ledAcik ? HIGH : LOW);
36 }
37 request->send(200, "text/plain", ledAcik ? "LED: ON" : "LED: OFF");
38 });
39
40 // Tanımsız adresler / unknown addresses
41 server.onNotFound([](AsyncWebServerRequest *request) {
42 request->send(404, "text/plain", "Sayfa yok / Not found");
43 });
44
45 server.begin();
46}
47
48void loop() {
49 // Sunucu arka planda çalışır, loop() boş kalabilir / the server runs in the background
50}| Команда | Что делает |
|---|---|
| AsyncWebServer server(80) (создание сервера) | Создаёт сервер, который слушает HTTP-порт 80 |
| server.on("/led", HTTP_GET, ...) (регистрация адреса) | Регистрирует функцию (обратный вызов), которая выполняется для заданного адреса и типа запроса |
| request->hasParam("durum") (проверка параметра) | Проверяет, есть ли в адресе параметр ?durum=... |
| request->getParam("durum")->value() (значение параметра) | Возвращает значение параметра в виде String |
| request->send(код, тип, содержимое) | Отправляет ответ: 200 OK, 404 — не найдено; тип text/plain, text/html или application/json |
| server.onNotFound(...) (неизвестный адрес) | Выполняется для каждого адреса, который не определён |
| server.begin() (запуск) | Запускает сервер; вызывай после всех строк server.on() |
Детали и подключение для панели управления
В примере с панелью модуль реле переключает лампу или вентилятор, а DHT22 измеряет температуру и влажность. Понадобятся ESP32 DevKit, одноканальный модуль реле на 5 V, модуль датчика DHT22 (или DHT11), макетная плата и провода-перемычки. При выборе реле отдавай предпочтение модулям с оптопарой. DHT22 измеряет от −40 до 80 °C с точностью ±0,5 °C, и читать его можно не чаще раза в 2 секунды; DHT11 дешевле, но даёт точность ±2 °C в диапазоне 0–50 °C. Подробности о датчике — в статье «Датчик температуры и влажности DHT11», а карта пинов твоей платы — в утилите распиновки.
| Пин модуля | ESP32 DevKit | ESP8266 NodeMCU | Примечание |
|---|---|---|---|
| VCC реле | VIN (5 V) | VIN или VU (5 V) | Катушке реле нужно 5 V; питание от USB |
| GND реле | GND | GND | Общая земля |
| IN реле | GPIO 26 | D1 (GPIO 5) | Большинство модулей включается сигналом LOW |
| DHT22 VCC (+) | 3V3 | 3V3 | Питание 3,3 V, линия данных тоже 3,3 V |
| DHT22 DATA (OUT) | GPIO 4 | D2 (GPIO 4) | Голый датчик: резистор 10 kΩ между DATA и VCC |
| DHT22 GND (−) | GND | GND | Общая земля |
Панель управления в реальном времени: реле, DHT22 и WebSocket
Теперь главный проект. Код ниже — один файл: HTML, CSS и JavaScript лежат внутри него в виде массива PROGMEM. Когда страница открывается, браузер устанавливает WebSocket-соединение с /ws. Каждые 2 секунды ESP32 читает DHT22 и одним JSON-сообщением отправляет всем браузерам температуру, влажность, состояние реле и число подключённых устройств. При нажатии кнопки браузер отправляет сообщение toggle, ESP32 переключает реле и снова сообщает всем новое состояние. Измени имя сети и пароль, загрузи код и открой в браузере http://esp32.local или IP-адрес из монитора порта.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4#include <ESPmDNS.h>
5#include <DHT.h>
6
7const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
8const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
9
10const int ROLE_PIN = 26; // röle modülü IN / relay module IN
11const bool ROLE_AKTIF_LOW = true; // çoğu modül LOW ile çeker / most modules switch on LOW
12const int DHT_PIN = 4; // DHT22 veri pini / DHT22 data pin
13
14DHT dht(DHT_PIN, DHT22); // DHT11 için DHT11 yaz / write DHT11 for a DHT11
15AsyncWebServer server(80);
16AsyncWebSocket ws("/ws"); // WebSocket adresi / WebSocket path
17
18bool roleAcik = false;
19float sicaklik = NAN;
20float nem = NAN;
21volatile bool gonder = false; // loop() yayın yapsın mı? / should loop() broadcast?
22unsigned long sonOlcum = 0;
23unsigned long sonTemizlik = 0;
24
25// Tarayıcıya giden sayfa / the page sent to the browser
26const char index_html[] PROGMEM = R"rawliteral(
27<!DOCTYPE html>
28<html lang="tr">
29<head>
30<meta charset="utf-8">
31<meta name="viewport" content="width=device-width, initial-scale=1">
32<title>ESP32 Panel</title>
33<style>
34body{font-family:sans-serif;background:#0f172a;color:#e2e8f0;text-align:center;margin:0;padding:16px}
35.kart{background:#1e293b;border-radius:12px;padding:16px;margin:12px auto;max-width:320px}
36.deger{font-size:2.4em;font-weight:bold}
37button{font-size:1.2em;padding:14px 32px;border:0;border-radius:8px;color:#fff;width:100%}
38.acik{background:#16a34a}.kapali{background:#475569}
39</style>
40</head>
41<body>
42<h2>ESP32 Panel</h2>
43<p id="baglanti">Bağlanıyor / Connecting...</p>
44<div class="kart">Sıcaklık / Temperature<div class="deger"><span id="sicaklik">--</span> °C</div></div>
45<div class="kart">Nem / Humidity<div class="deger"><span id="nem">--</span> %</div></div>
46<div class="kart">Röle / Relay<p><button id="dugme" class="kapali" onclick="degistir()">KAPALI / OFF</button></p></div>
47<p>Bağlı cihaz / Clients: <span id="istemci">0</span></p>
48<script>
49let ws;
50const $ = (id) => document.getElementById(id);
51function baglan() {
52 ws = new WebSocket('ws://' + location.host + '/ws');
53 ws.onopen = () => { $('baglanti').textContent = 'Bağlı / Connected'; };
54 ws.onclose = () => {
55 $('baglanti').textContent = 'Koptu, yeniden deneniyor / Reconnecting...';
56 setTimeout(baglan, 2000);
57 };
58 ws.onmessage = (e) => {
59 const d = JSON.parse(e.data);
60 $('sicaklik').textContent = d.sicaklik === null ? '--' : d.sicaklik.toFixed(1);
61 $('nem').textContent = d.nem === null ? '--' : d.nem.toFixed(0);
62 $('istemci').textContent = d.istemci;
63 $('dugme').textContent = d.role ? 'AÇIK / ON' : 'KAPALI / OFF';
64 $('dugme').className = d.role ? 'acik' : 'kapali';
65 };
66}
67function degistir() {
68 if (ws.readyState === WebSocket.OPEN) ws.send('toggle');
69}
70baglan();
71</script>
72</body>
73</html>
74)rawliteral";
75
76void roleAyarla(bool acik) {
77 roleAcik = acik;
78 digitalWrite(ROLE_PIN, ROLE_AKTIF_LOW ? !acik : acik);
79}
80
81// Durumu JSON metnine çevir / build the state as JSON text
82String durumJson() {
83 String s = "{\"role\":";
84 s += roleAcik ? "true" : "false";
85 s += ",\"sicaklik\":";
86 s += isnan(sicaklik) ? String("null") : String(sicaklik, 1);
87 s += ",\"nem\":";
88 s += isnan(nem) ? String("null") : String(nem, 0);
89 s += ",\"istemci\":";
90 s += (unsigned long)ws.count();
91 s += "}";
92 return s;
93}
94
95// WebSocket olayları (async_tcp görevinde çalışır) / WebSocket events (run in the async_tcp task)
96void wsOlay(AsyncWebSocket *sunucu, AsyncWebSocketClient *client, AwsEventType type,
97 void *arg, uint8_t *data, size_t len) {
98 switch (type) {
99 case WS_EVT_CONNECT:
100 Serial.printf("Istemci #%u baglandi / connected: %s\n",
101 (unsigned)client->id(), client->remoteIP().toString().c_str());
102 gonder = true; // yeni gelen güncel durumu alsın / newcomer gets the state
103 break;
104 case WS_EVT_DISCONNECT:
105 Serial.printf("Istemci #%u ayrildi / disconnected\n", (unsigned)client->id());
106 gonder = true;
107 break;
108 case WS_EVT_DATA: {
109 AwsFrameInfo *info = (AwsFrameInfo *)arg;
110 // Tek parça, tam bir metin mesajı mı? / a complete single-frame text message?
111 if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
112 if (len == 6 && memcmp(data, "toggle", 6) == 0) {
113 roleAyarla(!roleAcik);
114 gonder = true; // tüm ekranlar eşitlensin / sync every screen
115 }
116 }
117 break;
118 }
119 default: // PONG, ERROR vb. / PONG, ERROR etc.
120 break;
121 }
122}
123
124void setup() {
125 Serial.begin(115200);
126 pinMode(ROLE_PIN, OUTPUT);
127 roleAyarla(false); // açılışta röle kapalı / relay off at start
128 dht.begin();
129
130 WiFi.mode(WIFI_STA);
131 WiFi.begin(ssid, password);
132 Serial.print("Baglaniyor / connecting");
133 while (WiFi.status() != WL_CONNECTED) {
134 delay(500);
135 Serial.print(".");
136 }
137 WiFi.setSleep(false); // daha hızlı tepki / lower latency
138 Serial.println();
139 Serial.print("http://");
140 Serial.println(WiFi.localIP());
141
142 if (MDNS.begin("esp32")) { // http://esp32.local
143 MDNS.addService("http", "tcp", 80);
144 Serial.println("http://esp32.local");
145 }
146
147 ws.onEvent(wsOlay);
148 server.addHandler(&ws);
149
150 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
151 request->send(200, "text/html", index_html);
152 });
153 server.onNotFound([](AsyncWebServerRequest *request) {
154 request->send(404, "text/plain", "Sayfa yok / Not found");
155 });
156 server.begin();
157}
158
159void loop() {
160 if (millis() - sonOlcum >= 2000) { // DHT22 en fazla 2 sn'de bir / DHT22: once per 2 s
161 sonOlcum = millis();
162 float t = dht.readTemperature();
163 float h = dht.readHumidity();
164 if (!isnan(t) && !isnan(h)) { // hatalı okumayı atla / skip failed reads
165 sicaklik = t;
166 nem = h;
167 }
168 gonder = true;
169 }
170
171 if (gonder) {
172 gonder = false;
173 ws.textAll(durumJson()); // tüm tarayıcılara yolla / send to every browser
174 }
175
176 if (millis() - sonTemizlik >= 1000) {
177 sonTemizlik = millis();
178 ws.cleanupClients(); // kopuk istemcileri temizle / drop stale clients
179 }
180}Как работает код?
- AsyncWebSocket ws("/ws") создаёт конечную точку WebSocket, а server.addHandler(&ws) подключает её к серверу. Адрес в JavaScript должен в точности совпадать с этим путём.
- Функция wsOlay() вызывается при каждом подключении, отключении и входящем сообщении. WS_EVT_CONNECT означает нового клиента, WS_EVT_DISCONNECT — уход клиента, а WS_EVT_DATA — входящее сообщение.
- AwsFrameInfo содержит сведения о входящем кадре. Проверки final, index == 0 и len подтверждают, что сообщение пришло целиком в одном кадре, а WS_TEXT — что это текст. Короткие команды всегда помещаются в один кадр.
- Входящие данные сравниваются вместе с длиной через memcmp(), поэтому не важно, есть ли в конце данных нулевой символ.
- Поскольку обратные вызовы выполняются в задаче async_tcp, они только переключают реле и поднимают флаг gonder. Сама отправка происходит в loop() через ws.textAll(), так что все сообщения уходят из одного места и по порядку.
- ws.cleanupClients() вызывается раз в секунду: освобождает память оборванных соединений и закрывает самое старое соединение, когда число клиентов превышает лимит.
- На стороне JavaScript событие onclose переподключается через 2 секунды. Даже если ESP32 перезагрузится или Wi-Fi на мгновение пропадёт, страница восстановится сама. Поскольку адрес собирается из location.host, страница работает и по IP, и через esp32.local.
- WiFi.setSleep(false) отключает энергосбережение Wi-Fi. Режим modem sleep по умолчанию может добавлять к ответам задержки до нескольких сотен миллисекунд; если отключить его на панели с питанием от сети, кнопки срабатывают мгновенно.
Формат сообщений JSON сильно упрощает жизнь: браузер одной строкой превращает пришедший текст в объект через JSON.parse(). Пример сообщения выглядит так: {"role":true,"sicaklik":23.4,"nem":48,"istemci":2}. Если датчик не удаётся прочитать, значение отправляется как null, и страница показывает «--»; если бы неудачное чтение ушло как NaN, некорректный JSON вообще остановил бы обновление страницы. Каждое сообщение занимает всего около 60 байт, поэтому даже несколько обновлений в секунду не нагружают сеть. Для более сложных сообщений можно перейти на библиотеку ArduinoJson, но для панели такого размера склеивания строк String более чем достаточно. Тот же код без изменений можно попробовать и со светодиодом или зуммером вместо реле.
Где хранить HTML: в PROGMEM или в LittleFS?
В коде панели страница лежит в сыром строковом литерале вида R"rawliteral( ... )rawliteral". Такой синтаксис позволяет использовать кавычки и переносы строк без экранирования. На Arduino с AVR директива PROGMEM нужна, чтобы хранить текст во флеш-памяти, а не в ОЗУ; на ESP32 константные массивы и так читаются из флеша, поэтому PROGMEM пишется только ради совместимости и ничему не мешает. Когда страница разрастается, править HTML внутри кода становится неудобно; тогда аккуратнее перенести файлы в файловую систему LittleFS во флеш-памяти.
| Критерий | PROGMEM (внутри кода) | LittleFS (файловая система) |
|---|---|---|
| Настройка | Без лишних шагов, один файл | Нужны папка data и плагин для загрузки |
| Редактирование | HTML внутри массива C++ | Отдельные файлы index.html, style.css и app.js |
| Обновление | После каждого изменения скетч загружается заново | Заново загружается только файловая система |
| Размер | Идеально для страниц в несколько КБ | Около 1,5 МБ места при разметке по умолчанию |
| Изображения и шрифты | Неудобно | Можно раздавать иконки, изображения и файлы .gz |
| Когда? | Первые тесты и небольшие панели | Многостраничные или растущие интерфейсы |
Раздача файлов HTML, CSS и JavaScript из LittleFS
- В папке скетча (там, где лежит файл .ino) создай папку data и сохрани HTML, который в коде панели стоит между R"rawliteral( и )rawliteral", как data/index.html. При желании CSS и JavaScript можно вынести в отдельные файлы.
- Arduino IDE 2 не поддерживает старое меню ESP32 Sketch Data Upload. Вместо него скачай файл .vsix плагина arduino-littlefs-upload и положи его в C:\Users\(твоё имя пользователя)\.arduinoIDE\plugins на Windows или в ~/.arduinoIDE/plugins на macOS и Linux (если папки нет, создай её), затем перезапусти IDE. Плагину нужна IDE 2.2.1 или новее.
- Закрой монитор порта, если он открыт, затем открой палитру команд сочетанием Ctrl+Shift+P (Cmd+Shift+P на macOS) и выбери Upload LittleFS to Pico/ESP8266/ESP32. Файлы записываются в раздел файловой системы во флеш-памяти.
- Проверь, что в «Инструменты › Partition Scheme» выбрана схема разделов с разделом под файловую систему (например, Default 4MB with spiffs). LittleFS использует этот раздел, который называется spiffs.
- Загрузи код ниже как обычно. Пока в меню «Инструменты» отключена опция Erase All Flash, загрузка скетча не стирает файловую систему; при изменении файлов достаточно повторить шаг 3.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4#include <LittleFS.h>
5
6const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
7const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
8
9AsyncWebServer server(80);
10
11void setup() {
12 Serial.begin(115200);
13
14 if (!LittleFS.begin(true)) { // true: bağlanamazsa biçimlendir / format if mount fails
15 Serial.println("LittleFS hatasi / LittleFS error");
16 return;
17 }
18
19 WiFi.mode(WIFI_STA);
20 WiFi.begin(ssid, password);
21 while (WiFi.status() != WL_CONNECTED) {
22 delay(500);
23 }
24 Serial.println(WiFi.localIP());
25
26 // data/ klasörünün içeriğini sun / serve the contents of the data/ folder
27 server.serveStatic("/", LittleFS, "/")
28 .setDefaultFile("index.html") // varsayılan index.htm'dir / default is index.htm
29 .setCacheControl("max-age=600"); // tarayıcı 10 dk önbelleğe alsın / cache for 10 min
30
31 server.onNotFound([](AsyncWebServerRequest *request) {
32 request->send(404, "text/plain", "Sayfa yok / Not found");
33 });
34 server.begin();
35}
36
37void loop() {
38}mDNS: esp32.local вместо IP-адреса
Роутер может выдавать ESP32 новый IP-адрес при каждом запуске, а числа вроде 192.168.1.47 трудно запомнить. mDNS (multicast DNS) позволяет устройству объявить своё имя в локальной сети. Благодаря строке MDNS.begin("esp32") в коде панели устройства в той же сети могут открыть страницу по адресу http://esp32.local, а MDNS.addService("http", "tcp", 80) сообщает приложениям для обзора сети, что здесь есть веб-сервер. Не добавляй к имени «.local» и используй только строчные латинские буквы, цифры и дефисы. Если ESP32 несколько, дай каждой своё имя (kitchen, living-room и так далее), потому что два устройства с одинаковым именем мешают друг другу. mDNS поддерживается не везде одинаково:
| Система | Поддержка .local | Если не работает |
|---|---|---|
| iPhone, iPad, macOS | Встроена (Bonjour) | Проверь, что устройство в той же сети Wi-Fi |
| Windows 10 и 11 | Обычно работает в обновлённых версиях | Установи службу Bonjour или используй IP |
| Linux | Работает, если установлен avahi-daemon | Установи пакеты avahi-daemon и nss-mdns |
| Android | Зависит от версии и браузера | Используй IP-адрес или настрой резервирование DHCP на роутере |
Несколько клиентов: телефон, планшет и компьютер одновременно
Больше всего в WebSocket ценят то, что все, кто открыл панель, видят одно и то же состояние. Когда реле включается с телефона, ESP32 отправляет новое состояние во все соединения через textAll(), и вкладка на компьютере в тот же момент становится зелёной. Каждая вкладка — отдельный клиент: открой три вкладки в одном браузере, и счётчик покажет 3. По умолчанию библиотека допускает 8 одновременных клиентов WebSocket на ESP32 и 4 на ESP8266 (DEFAULT_MAX_WS_CLIENTS). Этот лимит не соблюдается автоматически: при вызове cleanupClients() закрывается самое старое соединение сверх лимита. Вот что ещё стоит учитывать:
- Когда телефон засыпает: телефон с погасшим экраном может не закрыть соединение как положено. Такие полуоткрытые соединения со временем вычищаются благодаря cleanupClients() и регулярной отправке данных.
- Частота отправки: в очереди каждого клиента может ждать до 32 сообщений (8 на ESP8266). Если вызывать textAll() на каждом проходе loop(), очередь заполняется, и новые сообщения молча отбрасываются; выставь «Инструменты › Core Debug Level» на Warn или более подробный уровень, и в мониторе порта появится предупреждение Too many messages queued (слишком много сообщений в очереди). Отправляй значение только при изменении или несколько раз в секунду.
- Новый подключившийся клиент: сразу отправляй текущее состояние каждой подключившейся странице, иначе до первого измерения на экране будет пустая панель. В коде панели это делает флаг, поднимаемый в WS_EVT_CONNECT.
- Ответ одному клиенту: чтобы ответить не всем, а только спросившему браузеру, используй client->text("..."); client->id() возвращает номер клиента.
- Память: каждое открытое соединение занимает TCP-сокет и память буферов. 520 КБ ОЗУ у ESP32 с запасом хватает на несколько клиентов, но если время от времени выводить свободную память через ESP.getFreeHeap(), утечки удаётся поймать рано.
Простая защита паролем
Любой в домашней сети, кто знает адрес панели, может переключить реле. С функциями библиотеки request->authenticate() и request->requestAuthentication() можно закрыть страницу именем пользователя и паролем; браузер откроет своё окно входа и запомнит данные на время сеанса. По умолчанию используется аутентификация Digest, то есть пароль не передаётся по сети открытым текстом. Но защитить только страницу недостаточно: адрес WebSocket — это отдельная дверь, и к ws://ip/ws можно подключиться напрямую. Пример ниже закрывает и эту дверь: при каждой загрузке ESP32 генерирует случайный ключ, выдаёт его по адресу /anahtar только браузеру, прошедшему вход, и с помощью setFilter() отклоняет WebSocket-соединения без правильного ключа.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4
5const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
6const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
7const char* KULLANICI = "admin"; // panel kullanıcı adı / panel username
8const char* PAROLA = "Gizli-2026"; // panel parolası / panel password
9const int LED_PIN = 2;
10
11AsyncWebServer server(80);
12AsyncWebSocket ws("/ws");
13String wsAnahtar; // her açılışta yeni anahtar / a new key on every boot
14bool ledAcik = false;
15
16const char index_html[] PROGMEM = R"rawliteral(
17<!DOCTYPE html>
18<html><head><meta charset="utf-8">
19<meta name="viewport" content="width=device-width, initial-scale=1"></head>
20<body style="font-family:sans-serif;text-align:center">
21<h2>Korumali panel / Protected panel</h2>
22<button style="font-size:1.4em;padding:12px 32px" onclick="ws && ws.send('toggle')">LED</button>
23<p id="durum">...</p>
24<script>
25let ws;
26fetch('/anahtar').then(r => r.text()).then(k => {
27 ws = new WebSocket('ws://' + location.host + '/ws?k=' + k);
28 ws.onmessage = (e) => { document.getElementById('durum').textContent = e.data; };
29});
30</script>
31</body></html>
32)rawliteral";
33
34bool girisVar(AsyncWebServerRequest *request) {
35 return request->authenticate(KULLANICI, PAROLA);
36}
37
38void setup() {
39 Serial.begin(115200);
40 pinMode(LED_PIN, OUTPUT);
41
42 WiFi.mode(WIFI_STA);
43 WiFi.begin(ssid, password);
44 while (WiFi.status() != WL_CONNECTED) {
45 delay(500);
46 }
47 Serial.println(WiFi.localIP());
48
49 // Wi-Fi açıkken random() donanım üretecini kullanır / random() uses the hardware RNG
50 wsAnahtar = String(random(0x7FFFFFFF), HEX) + String(random(0x7FFFFFFF), HEX);
51
52 // Sayfa: tarayıcı kullanıcı adı ve parola sorar / page: the browser asks for login
53 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
54 if (!girisVar(request)) return request->requestAuthentication();
55 request->send(200, "text/html", index_html);
56 });
57
58 // Anahtar yalnız giriş yapana verilir / the key is given only after login
59 server.on("/anahtar", HTTP_GET, [](AsyncWebServerRequest *request) {
60 if (!girisVar(request)) return request->requestAuthentication();
61 request->send(200, "text/plain", wsAnahtar);
62 });
63
64 // Doğru anahtarı getirmeyen bağlantıyı reddet / reject sockets without the right key
65 ws.setFilter([](AsyncWebServerRequest *request) {
66 return request->hasParam("k") && request->getParam("k")->value() == wsAnahtar;
67 });
68
69 ws.onEvent([](AsyncWebSocket *sunucu, AsyncWebSocketClient *client, AwsEventType type,
70 void *arg, uint8_t *data, size_t len) {
71 if (type == WS_EVT_DATA && len == 6 && memcmp(data, "toggle", 6) == 0) {
72 ledAcik = !ledAcik;
73 digitalWrite(LED_PIN, ledAcik ? HIGH : LOW);
74 sunucu->textAll(ledAcik ? "LED: ON" : "LED: OFF");
75 }
76 });
77
78 server.addHandler(&ws);
79 server.begin();
80}
81
82void loop() {
83 ws.cleanupClients(); // saniyede bir yeterli / once per second is enough
84 delay(1000);
85}Заметки для ESP8266 и Raspberry Pi Pico W
Та же библиотека работает и на ESP8266 (NodeMCU, Wemos D1 mini). Что нужно изменить: вместо Async TCP установи библиотеку ESP Async TCP от ESP32Async; подключай ESP8266WiFi.h вместо WiFi.h, ESPAsyncTCP.h вместо AsyncTCP.h и ESP8266mDNS.h вместо ESPmDNS.h, а в loop() добавь строку MDNS.update(). Замени WiFi.setSleep(false) на WiFi.setSleepMode(WIFI_NONE_SLEEP) и назначь пины по столбцу NodeMCU из таблицы подключения. У ESP8266 всего около 80 КБ доступного ОЗУ, поэтому делай страницу компактной и помни, что лимит клиентов — 4. Если под рукой Raspberry Pi Pico W, о том, как сделать веб-сервер на MicroPython, рассказано в руководстве «Raspberry Pi Pico W и Wi-Fi»; новые версии ESPAsyncWebServer на стороне Arduino поддерживают и микросхемы RP2040 и RP2350.
Идеи проектов с веб-сервером на ESP32
- Многоканальная умная розетка: с 4-канальной платой реле сделай отдельные кнопки для лампы, вентилятора и розетки; команды различай по коротким текстам вроде «r1» и «r2». О выборе реле — в статье о модуле реле.
- Панель полива растений: показывай значение датчика влажности почвы вживую, задавай порог полива ползунком на странице и включай насос вручную, когда нужно.
- Метеостанция: вместо DHT22 возьми BME280, чтобы добавить атмосферное давление, и выводи последние 60 измерений небольшим линейным графиком на странице.
- Лампа с выбором цвета: отправляй значение палитры цветов со страницы по WebSocket и мгновенно окрашивай ленту WS2812B NeoPixel.
- Сервопривод на ползунке: отправляй значение ползунка при каждом движении и поворачивай сервопривод; идеально для наведения камеры или дверной защёлки.
- Журнал с отметками времени: добавляй к каждому измерению время, полученное по NTP, а код обновляй по воздуху через OTA, а не каждый раз по USB.
Частые ошибки и как их исправить
| Симптом | Причина | Решение |
|---|---|---|
| 'mbedtls_md5_starts_ret' was not declared in this scope (функция не объявлена) | Старая библиотека me-no-dev с ядром ESP32 3.x | Установи ESP Async WebServer и Async TCP от ESP32Async, удали старые папки |
| Multiple libraries were found for "AsyncTCP.h" (найдено несколько библиотек) | Две копии одной библиотеки | Удали старые копии -master в папке libraries |
| Предупреждение Deprecated для send_P | send_P в новой версии удаляется | Используй request->send(200, "text/html", index_html) |
| Страница вообще не открывается | Телефон в гостевой сети или на мобильном интернете, либо сменился IP-адрес | Подключись к той же сети Wi-Fi и возьми актуальный IP из монитора порта |
| Страница открывается, но значения остаются «--» | WebSocket не подключился: другой путь или забыт addHandler | Открой консоль по F12 и сверь ws("/ws") с адресом в JavaScript |
| Task watchdog got triggered при async_tcp | delay(), длинный цикл или HTTP-запрос внутри обратного вызова | Делай обратный вызов коротким, а работу переноси в loop() через флаг |
| Too many messages queued (переполнена очередь) | textAll() вызывается слишком часто | Отправляй только при изменении или несколько раз в секунду |
| Страница из LittleFS возвращает 404 | Папка data не загружена или нет setDefaultFile | Загрузи файлы плагином и добавь setDefaultFile("index.html") |
| does not exist, no permits for creation (файл не найден) в мониторе порта | Запрошенного файла нет в LittleFS | Проверь имя файла и регистр букв; строки с упоминанием .gz безвредны |
| Реле никогда не выключается | Модуль на 5 V не воспринимает HIGH 3,3 V | Используй модуль, совместимый с 3,3 V, JD-VCC или транзистор |
| ESP32 перезагружается, когда щёлкает реле | Ток катушки просаживает питание (brownout) | Используй надёжный адаптер 5 V и конденсатор 470 µF |
| esp32.local не открывается | Устройство не поддерживает mDNS или имя записано с .local | Пиши MDNS.begin("esp32") и при необходимости используй IP-адрес |
| Кнопка реагирует с задержкой | Включён режим modem sleep Wi-Fi | Вызови WiFi.setSleep(false) после подключения |
Связка асинхронного веб-сервера на ESP32 и WebSocket — самый короткий путь к быстрой панели управления, которая работает без облачного сервиса и без написания приложения для телефона и которой могут пользоваться несколько устройств. Начни с однофайловой панели в PROGMEM, по мере роста интерфейса переходи на LittleFS и давай каждому новому устройству в сети собственное имя mDNS. Если ESP32 для тебя в новинку, основы найдёшь в статье «Что такое ESP32?» В приложении BlueGrays можно также пошагово разобрать проекты на ESP32 — асинхронный веб-сервер, счётчик на WebSocket, доступ через mDNS, часы с NTP и обновление по OTA — со схемами подключения и готовым кодом.
Частые вопросы
Чем ESPAsyncWebServer отличается от WebServer на ESP32?
Библиотека WebServer обрабатывает запросы по одному через вызов handleClient() в loop(); если loop() замедляется, замедляется и страница. ESPAsyncWebServer обрабатывает запросы в отдельной фоновой задаче, справляется с несколькими соединениями сразу и имеет встроенную поддержку WebSocket и SSE. Для панелей реального времени асинхронная версия подходит лучше.
Сколько клиентов может одновременно подключиться к веб-серверу на ESP32?
По умолчанию ESPAsyncWebServer допускает 8 клиентов WebSocket на ESP32 и закрывает лишние при вызове cleanupClients(); на ESP8266 лимит — 4. Обычные запросы страниц короткие, поэтому открыть страницу по очереди может больше устройств. На практике домашняя панель спокойно работает с 3–5 устройствами одновременно.
Можно ли получить доступ к веб-серверу на ESP32 из интернета?
Технически это возможно через проброс портов на роутере, но так делать не рекомендуется: на ESP32 библиотека не поддерживает HTTPS, и устройство остаётся открытым для атак из интернета. Для удалённого управления гораздо безопаснее Telegram-бот, система на основе MQTT или функция VPN на роутере.
Почему не работает esp32.local?
Чаще всего причина в том, что телефон или компьютер не умеет разрешать имена mDNS; особенно это бывает на некоторых телефонах с Android и в старых версиях Windows. Ошибкой будет и запись MDNS.begin("esp32.local") в коде, потому что имя нужно задавать без .local. Если имя всё равно не разрешается, используй IP-адрес из монитора порта или закрепи за ESP32 постоянный IP на роутере.
Может ли веб-сервер на ESP32 работать без роутера?
Да. С WiFi.softAP("ESP32-Panel", "12345678") ESP32 создаёт собственную сеть Wi-Fi; подключи к ней телефон и открой в браузере 192.168.4.1. В режиме точки доступа интернета нет, но панель и WebSocket работают точно так же. Пароль сети должен быть не короче 8 символов.
Какую библиотеку ESPAsyncWebServer устанавливать?
В «Менеджере библиотек» Arduino установи библиотеки ESP Async WebServer и Async TCP, автор которых — ESP32Async. Оригинальная версия в аккаунте me-no-dev больше не поддерживается и не компилируется с ядром ESP32 3.x. Также удали из папки libraries все старые копии, добавленные из ZIP.
Что собрать после прочтения
Все проекты
Асинхронный веб-сервер на ESP32 (с выводом IP на LCD)
В этом проекте ESP32 превращается в асинхронный веб-сервер, которым можно управлять с телефона или компьютера в той же сети Wi-Fi. Благодаря библиотеке ESPAsyncWebServer сервер может обрабатывать несколько запросов одновременно (в отличие от классического WebServer.h, который блокируется на одном запросе). Открыв IP-адрес ESP32 в браузере, ты нажимаешь кнопки ON/OFF (маршруты /on и /off) и включаешь и выключаешь светодиод на GPIO 5.

Счётчик на WebSocket с ESP32 (2 кнопки + LCD + веб в реальном времени)
В этом проекте две кнопки, подключённые к ESP32 (GPIO 5 увеличивает, GPIO 4 уменьшает), меняют счётчик. Значение сразу обновляется и на I2C LCD 16x2, и — через поддержку WebSocket в ESPAsyncWebServer — во всех подключённых браузерах без перезагрузки страницы. Вместо опроса по HTTP используется одно постоянное соединение, и ESP32 отправляет новое значение всем через ws.textAll().

mDNS на ESP32: доступ к устройству через bluegrays-esp.local (с экраном LCD)
В этом проекте к ESP32 добавляется служба mDNS (Multicast DNS), и к устройству можно обращаться по понятному имени вроде bluegrays-esp.local вместо IP-адреса. Даже если IP, выданный DHCP, меняется, устройство всегда доступно по одному имени. На устройстве также работает маленький веб-сервер, через который по адресу http://bluegrays-esp.local/ из браузера отправляются команды светодиоду. I2C LCD показывает имя хоста, IP-адрес и состояние светодиода.

Подключение ESP32 к Wi-Fi в режиме STA (с экраном LCD)
В этом проекте ESP32 надёжно и устойчиво к сбоям подключается к домашней сети Wi-Fi в режиме станции (STA). SSID и пароль хранятся в отдельном файле secrets.h, чтобы не утекли в Git; настроено автоматическое переподключение, а уровень сигнала RSSI и IP-адрес выводятся на I2C LCD-экран. Встроенный светодиод служит индикатором состояния. Это менеджер подключения уровня реальных устройств.

Температура и влажность с DHT22 на ESP32 (с LCD-дисплеем)
Этот проект измеряет температуру и влажность воздуха датчиком DHT22 и в реальном времени показывает данные на I2C LCD 16x2. Данные читаются с датчика по однопроводному протоколу через библиотеку Adafruit DHT. Проект служит основой для IoT-метеостанций, умного дома или систем мониторинга среды в теплицах. Связь с DHT22 идёт через пин GPIO 4 у ESP32.

Умное освещение на ESP32 (PIR + LDR + реле + OLED)
Сделай умное освещение для коридора, ванной или кладовки на ESP32! С PIR-датчиком движения, датчиком освещённости LDR и модулем реле собирается классический сценарий «свет включается при движении в темноте»: если вокруг уже светло, движение свет НЕ включает, и днём энергия экономится. Свет горит 30 секунд; если движение продолжается, таймер сбрасывается, и свет горит дольше. OLED SSD1306 крупно показывает состояние (ON / DAYTIME / WAITING) и обратный отсчёт.

Обновление ESP32 по воздуху (OTA): беспроводная установка прошивки (с экраном LCD)
В этом проекте ты узнаешь, как загружать новую прошивку в ESP32 по Wi-Fi без USB-кабеля. С библиотекой ArduinoOTA ESP32 становится видна в сети и автоматически появляется как bluegrays-ota на вкладке «Сетевой порт» в Arduino IDE. Первая загрузка выполняется по USB, а все следующие обновления — без проводов: это важно для устройств в эксплуатации. I2C LCD показывает состояние Wi-Fi, IP-адрес, имя хоста, процент загрузки OTA и сообщения об ошибках.

Точное время из интернета через NTP на ESP32
ESP32 подключается к Wi-Fi и получает точное время с точностью до миллисекунд от серверов NTP (Network Time Protocol). Благодаря функции configTzTime() автоматически применяется часовой пояс Турции (TRT, UTC+3), а в странах с летним временем переходы обрабатываются автоматически по строке POSIX TZ. Время выводится в монитор порта и на необязательный I2C LCD. configTzTime() запускает службу SNTP, которая периодически пересинхронизирует часы.
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

