Сервер из базового урока раздела я запустил за вечер и был доволен, пока не понадобился второй путь. Сначала появился /status, потом /blink, потом к ним прибавилась температура с DHT22 — и loop превратился в три экрана вложенных if с indexOf. Четвёртый путь добавлялся копипастой, а на пятом я сломал третий: одна строчка else не там, и /status начал отдавать страницу /blink. Скетч работал, но трогать его я боялся.
Причина кашы была в архитектуре: WiFiServer отдаёт сырую запрос-строку, и всю маршрутизацию я клеил вручную. В ядре ESP32 давно лежит библиотека WebServer.h, которая делает это за вас: один вызов server.on на путь, тело ответа — отдельно, перебор if не нужен вовсе. Ставить ничего не надо — она приезжает вместе с пакетом плат.
Здесь соберём сервер заново: маршруты вместо перебора, HTML-страница из PROGMEM, которая не ест RAM, ответ тремя аргументами и вежливый 404 для чужих путей. Потом заглянем на внутреннюю сторону сервера в симуляторе — увидим ровно то, что WebServer.h делает за кулисами на живой плате. А чтобы страница открывалась всегда, в скетче ниже сеть поднимается первой и только потом стартует сервер — порядок, который мы разбирали в уроке про подключение к роутеру.
server.on: маршрут вместо перебора if
Идея библиотеки — таблица маршрутов, как оглавление в книге: "/" ведёт к handleRoot, "/on" — к handleOn. Браузер просит путь, библиотека сама находит строку в таблице и вызывает вашу функцию. В скетче не остаётся ни одного indexOf, а новый путь добавляется одной строкой server.on рядом с остальными — не трогая чужой код. Именно так я в тот вечер переписал пять путей в пять строк таблицы: скетч стал короче на полтора экрана, и страх его трогать пропал.
Всю механику тянет server.handleClient(): принимает соединение, разбирает запрос-строку, ищет маршрут, вызывает обработчик и отправляет ответ — на 240 МГц это доли миллисекунды. Ваше дело — вызывать его в каждом проходе loop и не задерживать цикл: delay(3000) внутри loop ставит очередь запросов на паузу, и браузер крутит загрузку. Цифра для чувства масштаба: страница в 400 байт уходит за единицы миллисекунд, и за те же три секунды паузы мимо проходит пара десятков запросов. Про то, как жить без delay в принципе, мы собирали в уроке про millis вместо delay.
Отдельно про параметры. Когда понадобится передать значение — яркость, порог, номер канала, — оно приходит в пути после знака вопроса: /set?value=40. Библиотека разбирает его сама: server.arg("value") вернёт строку "40", и вам останется перевести её в число. Я держу такие маршруты подальше от кнопок: чтение параметров — уже не выключатель, а настройки, и для них я завожу отдельные пути, чтобы случайный тап не менял конфигурацию.
Мнение с границей: на два-три пути хватит и голого WiFiServer из базового урока — там меньше абстракций и весь обмен виден как на ладони. WebServer.h окупается, когда маршрутов больше трёх, появляются GET-параметры (server.arg()) или хочется отдавать JSON рядом с HTML. Мой порог — третий маршрут: после него копипаста if всегда выигрывает у таблицы маршрутов по числу граблей.
HTML из скетча: страница в PROGMEM
#include <WiFi.h>#include <WebServer.h> const char* ssid = "DachaNet";const char* password = "superParol123"; const char INDEX_HTML[] PROGMEM = R"rawliteral(<!DOCTYPE html><html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Дача: сервер</title> </head> <body> <h1>Сервер на плате</h1> <p>Эта страница лежит в flash, а не в облаке.</p> </body></html>)rawliteral"; WebServer server(80); // объект сервера на стандартном 80 порту void handleRoot() { // маршрут "/": отдаём страницу server.send(200, "text/html", INDEX_HTML);} void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Откройте в браузере http://"); Serial.println(WiFi.localIP()); server.on("/", handleRoot); // таблица маршрутов: путь -> функция server.begin(); // старт после подключения к WiFi} void loop() { server.handleClient(); // принять и обработать запрос; без delay!}PROGMEM — главная строчка, которую стоит понять. Страница на 400-600 байт объявлена константой во flash: у ESP32 там 4 МБ, и RAM она не трогает вообще. Без PROGMEM страница попала бы в оперативную память — на ESP32 с 520 КБ это терпимо, но привычка идёт из мира Uno, где 2 КБ RAM и каждая строка на счету. Для прямой отдачи из flash у send есть вариант send_P — он экономит копирование, хотя обычный send с PROGMEM-строкой тоже корректно работает.
Конструкция R"rawliteral(...)rawliteral" — сырая строка C++: всё между маркерами попадает в текст как есть, кавычки и скобки внутри HTML экранировать не нужно. Без неё каждую кавычку разметки пришлось бы гасить слешем, и страница превратилась бы в кашу из слешей и кавычек. Маркеры можно переименовать, важно лишь чтобы открывающий и закрывающий совпадали.
server.send(200, "text/html", INDEX_HTML) — ответ тремя аргументами: код, Content-Type и тело. Странице положен text/html, данным — application/json: тот же сервер легко отдаёт и JSON-эндпойнт, собирая тело через serializeJson из урока про сериализацию. Все пути, которых нет в таблице, падают в server.onNotFound(): я печатаю туда путь в Serial и отвечаю коротким 404 — за неделю лог соберёт десяток чужих сканеров, и вы будете знать, кого ищут в вашей сети.
Отдельно про строчку с viewport. Без неё браузер телефона показывает страницу в масштабе рабочего стола: текст мелкий, кнопка-горошина, и тапать по ней вслепую — мучение. Мета-тег width=device-width говорит браузеру: ширина страницы равна ширине экрана, и элементы наливаются в размер пальца. На эту строчку я однажды потратил вечер: сервер работал, на ноутбуке страница выглядела образцовой, а со смартфона — сломанной. Если на живой плате страница не открывается вовсе, начните с сети по уроку про первый WiFi, а чек-лист из базового урока про сервер — там про гостевые сети и http-префикс — закроет остальное.
| Код | Content-Type | Тело | Когда я это шлю |
|---|---|---|---|
| 200 | text/html | Страница из PROGMEM | Корень "/" и страницы с интерфейсом |
| 200 | application/json | {"led":1,"up":345} | /api/status: данные для другой техники |
| 303 | — | Пусто, заголовок Location: / | После действий: браузер сам вернётся на "/" |
| 404 | text/plain | net takogo putya | onNotFound: всё, чего нет в таблице маршрутов |
Сервер в симуляторе: роутинг из поля ввода
WebServer.h в симуляторе не эмулируется: там нет сетевого стека и браузера, поэтому показываю внутреннюю сторону сервера — то, что библиотека делает за вас молча. Печатаете в поле ввода путь, как в адресной строке: "/", "/on", "/off" или выдуманный — скетч разбирает строку по той же логике таблицы маршрутов и печатает, какой обработчик сработал бы и какой ответ ушёл бы браузеру.
Пульс uptime раз в пять секунд — вторая половина урока в действии: loop не блокируется и продолжает тикать, пока «обслуживает» запросы. На живой плате этот же ритм даёт handleClient(): сервер отвечает, а миллисекундные таймеры платы продолжают работать. В следующих уроках тройки эта внутренняя сторона превратится в полноценные кнопки.
Почему поле ввода, а не кнопки? Потому что здесь мы смотрим на сервер изнутри: браузер и его кнопки появятся в следующем уроке, а важно почувствовать механику — путь пришёл, таблица маршрутов нашла строку, обработчик отработал, ответ ушёл. На живой плате эта цепочка срабатывает молча за доли миллисекунды; в симуляторе она растянута в лог, чтобы вы прочли её глазами и потом узнавали в логах монитора порта.
- Добавьте маршрут /blink в таблицу: обработчик печатает, что светодиод мигнёт три раза, и тоже отвечает 303 на "/". Одна строка в таблице — и путь не сломал соседей.
- Соберите /api/status: печать вида led=1 up=12 — на плате это serializeJson в application/json. Такой эндпойнт читает другая техника, а не человек.
- Заведите счётчик обращений: n= после каждого разбора пути. Минуты не пройдёт, как счётчик обгонит ваши клики — считайте favicon и сканеров.
- Соберите будущий onNotFound: все незнакомые пути печатайте одной строкой no_route= — через неделю в логе будет карта того, кого привлекает ваш сервер.
- WebServer.h входит в ядро ESP32: server.on(путь, функция) — маршрут, server.begin() после WiFi, handleClient() в каждом проходе loop
- server.send(код, тип, тело): 200 + text/html для страниц, application/json для данных, чужие пути — 404 через onNotFound
- HTML в PROGMEM живёт во flash и не тратит RAM; R"rawliteral" избавляет от экранирования кавычек в разметке
- delay в loop ставит очередь запросов на паузу: браузер висит, пока handleClient не получает управления
- В симуляторе роутинг заменён командами из порта: печатаете путь — видите обработчик и ответ; на плате путь присылает браузер
- 1. Что делает строка server.on("/on", handleOn)?
- 2. Где вызывать server.handleClient()?
- 3. Зачем HTML-странице PROGMEM?
- 4. Браузер попросил /data, а такого маршрута в таблице нет. Что ответит правильно собранный сервер?
- 5. Зачем после действия /on отвечать 303 с заголовком Location: /?
Каркас готов: маршруты в таблице, страница во flash, чужие пути уходят в 404 — сервер из этого урока готов нести хоть лампочку, хоть весь дачный щиток. Осталось придать ему смысл: в следующем уроке пути /on и /off начнут дергать пин платы, страница научится показывать состояние, а редирект 303 сделает обновление страницы безобидным.
А если что-то пойдёт не так, откройте урок про отладку через Serial и заведи привычку печатать каждый входящий путь: одна строка с маршрутом в логе стоит часа гаданий, какой обработчик сработал и почему.
Частые вопросы по теме урока
- Что делает строка server.on("/on", handleOn)?
- server.on только записывает маршрут в таблицу: путь и функцию. Сам запрос придёт от браузера позже, и тогда библиотека вызовет ваш обработчик. Регистрирует обработчик: путь /on будет вызывать handleOn
- Где вызывать server.handleClient()?
- handleClient принимает и разбирает подключения, поэтому его место — каждый проход loop. delay внутри loop ставит очередь запросов на паузу. В каждом проходе loop, без долгих delay
- Зачем HTML-странице PROGMEM?
- PROGMEM держит константу во flash: у ESP32 там 4 МБ, и оперативная память остаётся для буферов и библиотек. Страница лежит во flash и не тратит оперативную память
- Браузер попросил /data, а такого маршрута в таблице нет. Что ответит правильно собранный сервер?
- Все пути мимо таблицы попадают в onNotFound. Я печатаю там путь в Serial и отвечаю коротким 404 — и разведка, и вежливость. 404 из обработчика onNotFound
- Зачем после действия /on отвечать 303 с заголовком Location: /?
- 303 говорит браузеру: действие принято, иди по Location. Адресная строка возвращается на "/", и обновление страницы не жмёт /on повторно. Чтобы страница открылась в новой вкладке