Крошечный веб-сервер на ESP32
Доступ по сети к вашему DIY‑устройству позволяет сделать его более гибким, ведь для того, чтобы внести какие‑то изменения в настройки к примеру, вашей метеостанции, вы можете просто подключиться к ней удаленно. Еще лучше, если доступ к устройству можно получить с помощью Wi‑Fi. В таком случае мы можем сделать устройство полностью мобильным, подключив к аккумулятору или пауэрбанку.
В этой статье мы рассмотрим использование ESP32 в качестве веб‑сервера для администрирования вашего DIY‑устройства. Пожалуй, веб‑интерфейс сейчас является наиболее распространенным способом удаленного управления различным оборудованием и приложениями, опережая столь любимую инженерами командную строку. Для работы через веб-интерфейс нужен только браузер и не требуется какой‑либо толстый клиент.
В качестве примера наш веб‑сервер будет управлять парой светодиодов, в соответствии с представленной схемой. Соответственно, на плате ESP у нас будет размещен веб‑сервер, с кнопками включения диодов.

Далее мы покажем общие принципы написания скетчей, с помощью которых вы сможете без труда модифицировать прошивку под свои задачи.
Центр всего или один из узлов
Для начала нам необходимо определиться с сетевой топологией, которую мы будем использовать. Здесь возможны два варианта. В первом случае, наше устройство подключено к беспроводной сети и является одним из ее клиентов. Это типичная топология для домашней сети, где у нас есть стационарная точка доступа, к которой все подключаются.

Здесь наше устройство будет иметь фиксированный IP адрес и настройки для подключения к беспроводной сети в качестве клиента.
Во втором случае, наша плата ESP будет сама выступать в качестве точки доступа. Этот вариант предполагает, что у нас нет никаких других точек доступа и наше устройство должно стать такой точкой доступа, к которой смогут подключаться клиенты для управления.

Здесь у нас будет не только фиксированный IP адрес, но и настройки, необходимые для работы в режиме точки доступа. Кстати, в качестве клиента, как показано на схеме, может также выступать плата ESP.
Обычно, второй вариант используется там, где нет никакой инфраструктуры WiFI. То есть наше устройство является полностью мобильным и для управления им необходимо подключаться к нему по WiFi.
Теперь давайте посмотрим реализацию каждого из вариантов.
Клиент, просто клиент
Для начала рассмотрим более простой вариант, когда ESP является просто клиентом. Полный код скетча будет приведен в конце статьи, а здесь мы проясним основные моменты.
Для подключения к беспроводной сети необходимо в блоке Setup () воспользоваться функцией WiFi.begin (), передав ей в качестве параметров SSID и ключ сети.
const char* ssid = "ESP32";
const char* password = "12345678";
WiFi.begin(ssid, password);
Пока ESP32 пытается подключиться к сети, мы можем использовать функцию WiFi.status (), чтобы проверить статус подключения.
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.print(".");
}
Здесь возможны следующие статусы:
WL_CONNECTED: при успешном подключении к сети Wi‑Fi
WL_NO_SHIELD: отсутствует модуль Wi‑Fi
WL_IDLE_STATUS: временное состояние, присваиваемое при вызове WiFi.begin () и остающееся активным до истечения количества попыток (в результате WL_CONNECT_FAILED) или установления соединения (в результате WL_CONNECTED)
WL_NO_SSID_AVAIL: ни один SSID не доступен
WL_SCAN_COMPLETED: сканирование сетей завершено
WL_CONNECT_FAILED: соединение не удалось после всех попыток
WL_CONNECTION_LOST: соединение потеряно
WL_DISCONNECTED: отключение от сети
После подключения к сети функция WiFi.localIP () используется для вывода на серийный порт IP‑адреса ESP32.
Serial.println("");
Serial.println("WiFi connected..!");
Serial.print("Got IP: ");
Serial.println(WiFi.localIP());
Дальше мы рассмотрим код, который будет идентичен для обоих вариантов реализации нашего устройства.
Инициализация
В блоке Setup (), который выполняется только при включении устройства, мы создаем объект библиотеки WebServer, чтобы иметь доступ к ее функциям. Конструктор этого объекта принимает в качестве параметра порт, который будет прослушивать сервер. Поскольку по умолчанию HTTP использует порт 80, мы будем использовать это значение. Это позволит нам подключаться к серверу, не указывая порт в URL.
WebServer server(80);
Далее мы объявляем пины GPIO ESP32, к которым подключены светодиоды, а также их начальное состояние.
uint8_t LED1pin = 4;
bool LED1status = LOW;
uint8_t LED2pin = 5;
bool LED2status = LOW;
Для отладки мы будем использовать серийный порт. Проинициализируем его и оба светодиода.
Serial.begin(115200);
pinMode(LED1pin, OUTPUT);
pinMode(LED2pin, OUTPUT);
Далее в блоке Setup () нам надо указать, какой код должен выполняться при обращении к определенному URL. Для этого мы используем метод.on (). Этот метод принимает два параметра: относительный путь к URL и имя функции, которая будет выполняться при посещении этого URL.
Например, первая строка приведенного ниже фрагмента кода указывает, что когда сервер получает HTTP‑запрос по корневому (/) пути, он вызовет функцию handle_OnConnect (). Важно отметить, что указанный URL является относительным путем.
Аналогично, мы должны указать еще четыре URL‑адреса для обработки двух состояний двух светодиодов.
server.on("/", handle_OnConnect);
server.on("/led1on", handle_led1on);
server.on("/led1off", handle_led1off);
server.on("/led2on", handle_led2on);
server.on("/led2off", handle_led2off);
Мы не указали, что должен выдать сервер, если клиент запрашивает URL, который не указан в server.on (). В качестве ответа он должен выдать ошибку 404 (Page Not Found). Для этого мы используем метод server.onNotFound ().
server.onNotFound(handle_NotFound);
И нам остается только запустить сервер с помощью метода begin ()
server.begin();
Serial.println("HTTP server started");
Бесконечный цикл
В блоке loop () у нас обрабатываются входящие HTTP‑запросы. Для этого мы используем метод handleClient (). Мы также изменяем состояние светодиодов в зависимости от запроса.
void loop() {
server.handleClient();
if(LED1status)
{digitalWrite(LED1pin, HIGH);}
else
{digitalWrite(LED1pin, LOW);}
if(LED2status)
{digitalWrite(LED2pin, HIGH);}
else
{digitalWrite(LED2pin, LOW);}
}
Теперь мы должны написать функцию handle_OnConnect (), которую мы ранее прикрепили к корневому (/) URL с помощью server.on. Установим начальные состояния обоих светодиодов в LOW.
Мы используем метод send для ответа на HTTP‑запрос. Хотя этот метод может быть вызван с различными аргументами, в простейшей форме он требует код ответа HTTP, тип содержимого и содержимое.
Первым параметром, который мы передаем методу send, является код 200 (один из кодов состояния HTTP), который соответствует ответу OK. Затем мы указываем тип содержимого «text/html» и, наконец, передаем пользовательскую функцию SendHTML (), которая генерирует динамическую HTML‑страницу со статусом LED.
void handle_OnConnect() {
LED1status = LOW;
LED2status = LOW;
Serial.println("GPIO4 Status: OFF | GPIO5 Status: OFF");
server.send(200, "text/html", SendHTML(LED1status,LED2status));
}
void handle_led1on() {
LED1status = HIGH;
Serial.println("GPIO4 Status: ON");
server.send(200, "text/html", SendHTML(true,LED2status));
}
void handle_led1off() {
LED1status = LOW;
Serial.println("GPIO4 Status: OFF");
server.send(200, "text/html", SendHTML(false,LED2status));
}
void handle_led2on() {
LED2status = HIGH;
Serial.println("GPIO5 Status: ON");
server.send(200, "text/html", SendHTML(LED1status,true));
}
void handle_led2off() {
LED2status = LOW;
Serial.println("GPIO5 Status: OFF");
server.send(200, "text/html", SendHTML(LED1status,false));
}
void handle_NotFound(){
server.send(404, "text/plain", "Not found");
}
Всякий раз, когда веб‑сервер ESP32 получает запрос от веб‑клиента, функция sendHTML () генерирует веб‑страницу. Она просто конкатенирует HTML‑код в длинную строку и возвращается к функции server.send (), о которой мы говорили ранее. Функция использует состояние светодиодов в качестве параметра для динамической генерации HTML‑контента.
Подробно рассматривать HTML и стили мы не будем, но обратим внимание на следующее. С помощью if мы можем динамически менять отображаемый на странице контент, просто подставляя соответствующую строку при изменении состояния светодиодов.
if(led1stat)
{ptr +="LED1 Status: ON
OFF\n";}
else
{ptr +="LED1 Status: OFF
ON\n";}
if(led2stat)
{ptr +="LED2 Status: ON
OFF\n";}
else
{ptr +="LED2 Status: OFF
ON\n";}
Веб‑интерфейс будет иметь следующий вид:

При нажатии:

Полный код скетча.
#include
#include
/*Put your SSID & Password*/
const char* ssid = " YourNetworkName"; // Enter SSID here
const char* password = " YourPassword"; //Enter Password here
WebServer server(80);
uint8_t LED1pin = 4;
bool LED1status = LOW;
uint8_t LED2pin = 5;
bool LED2status = LOW;
void setup() {
Serial.begin(115200);
delay(100);
pinMode(LED1pin, OUTPUT);
pinMode(LED2pin, OUTPUT);
Serial.println("Connecting to ");
Serial.println(ssid);
//connect to your local wi-fi network
WiFi.begin(ssid, password);
//check wi-fi is connected to wi-fi network
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected..!");
Serial.print("Got IP: "); Serial.println(WiFi.localIP());
server.on("/", handle_OnConnect);
server.on("/led1on", handle_led1on);
server.on("/led1off", handle_led1off);
server.on("/led2on", handle_led2on);
server.on("/led2off", handle_led2off);
server.onNotFound(handle_NotFound);
server.begin();
Serial.println("HTTP server started");
}
void loop() {
server.handleClient();
if(LED1status)
{digitalWrite(LED1pin, HIGH);}
else
{digitalWrite(LED1pin, LOW);}
if(LED2status)
{digitalWrite(LED2pin, HIGH);}
else
{digitalWrite(LED2pin, LOW);}
}
void handle_OnConnect() {
LED1status = LOW;
LED2status = LOW;
Serial.println("GPIO4 Status: OFF | GPIO5 Status: OFF");
server.send(200, "text/html", SendHTML(LED1status,LED2status));
}
void handle_led1on() {
LED1status = HIGH;
Serial.println("GPIO4 Status: ON");
server.send(200, "text/html", SendHTML(true,LED2status));
}
void handle_led1off() {
LED1status = LOW;
Serial.println("GPIO4 Status: OFF");
server.send(200, "text/html", SendHTML(false,LED2status));
}
void handle_led2on() {
LED2status = HIGH;
Serial.println("GPIO5 Status: ON");
server.send(200, "text/html", SendHTML(LED1status,true));
}
void handle_led2off() {
LED2status = LOW;
Serial.println("GPIO5 Status: OFF");
server.send(200, "text/html", SendHTML(LED1status,false));
}
void handle_NotFound(){
server.send(404, "text/plain", "Not found");
}
String SendHTML(uint8_t led1stat,uint8_t led2stat){
String ptr = " \n";
ptr +="\n";
ptr +="LED Control \n";
ptr +="\n";
ptr +="\n";
ptr +="\n";
ptr +="ESP32 Web Server
\n";
ptr +="Using Station(STA) Mode
\n";
if(led1stat)
{ptr +="LED1 Status: ON
OFF\n";}
else
{ptr +="LED1 Status: OFF
ON\n";}
if(led2stat)
{ptr +="LED2 Status: ON
OFF\n";}
else
{ptr +="LED2 Status: OFF
ON\n";}
ptr +="
\n";
ptr +="\n";
return ptr;
}
Точка доступа
Теперь рассмотрим второй вариант, когда наше устройство само является точкой доступа. Как уже упоминалось, весь код в блоке loop () будет такой же. А вот в блоке Setup () нам необходимо будет инициализировать режим точки доступа.
Здесь мы также указываем SSID и ключ.
const char* ssid = "ESP32";
const char* password = "12345678";
Далее укажем адрес, маску и шлюз. Обратите внимание, что здесь у нас нет DHCP так что на клиентах адрес нужно будет указывать вручную.
IPAddress local_ip(192,168,1,1);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);
Запускаем и настраиваем точку доступа.
WiFi.softAP(ssid, password);
WiFi.softAPConfig(local_ip, gateway, subnet);
delay(100);
Ну, а весь остальной код у нас будет таким же, и найти его можно здесь.
#include
#include
/* Put your SSID & Password */
const char* ssid = "ESP32"; // Enter SSID here
const char* password = "12345678"; //Enter Password here
/* Put IP Address details */
IPAddress local_ip(192,168,1,1);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);
WebServer server(80);
uint8_t LED1pin = 4;
bool LED1status = LOW;
uint8_t LED2pin = 5;
bool LED2status = LOW;
void setup() {
Serial.begin(115200);
pinMode(LED1pin, OUTPUT);
pinMode(LED2pin, OUTPUT);
WiFi.softAP(ssid, password);
WiFi.softAPConfig(local_ip, gateway, subnet);
delay(100);
server.on("/", handle_OnConnect);
server.on("/led1on", handle_led1on);
server.on("/led1off", handle_led1off);
server.on("/led2on", handle_led2on);
server.on("/led2off", handle_led2off);
server.onNotFound(handle_NotFound);
server.begin();
Serial.println("HTTP server started");
}
void loop() {
server.handleClient();
if(LED1status)
{digitalWrite(LED1pin, HIGH);}
else
{digitalWrite(LED1pin, LOW);}
if(LED2status)
{digitalWrite(LED2pin, HIGH);}
else
{digitalWrite(LED2pin, LOW);}
}
void handle_OnConnect() {
LED1status = LOW;
LED2status = LOW;
Serial.println("GPIO4 Status: OFF | GPIO5 Status: OFF");
server.send(200, "text/html", SendHTML(LED1status,LED2status));
}
void handle_led1on() {
LED1status = HIGH;
Serial.println("GPIO4 Status: ON");
server.send(200, "text/html", SendHTML(true,LED2status));
}
void handle_led1off() {
LED1status = LOW;
Serial.println("GPIO4 Status: OFF");
server.send(200, "text/html", SendHTML(false,LED2status));
}
void handle_led2on() {
LED2status = HIGH;
Serial.println("GPIO5 Status: ON");
server.send(200, "text/html", SendHTML(LED1status,true));
}
void handle_led2off() {
LED2status = LOW;
Serial.println("GPIO5 Status: OFF");
server.send(200, "text/html", SendHTML(LED1status,false));
}
void handle_NotFound(){
server.send(404, "text/plain", "Not found");
}
String SendHTML(uint8_t led1stat,uint8_t led2stat){
String ptr = " \n";
ptr +="\n";
ptr +="LED Control \n";
ptr +="\n";
ptr +="\n";
ptr +="\n";
ptr +="ESP32 Web Server
\n";
ptr +="Using Access Point(AP) Mode
\n";
if(led1stat)
{ptr +="LED1 Status: ON
OFF\n";}
else
{ptr +="LED1 Status: OFF
ON\n";}
if(led2stat)
{ptr +="LED2 Status: ON
OFF\n";}
else
{ptr +="LED2 Status: OFF
ON\n";}
ptr +="\n";
ptr +="\n";
return ptr;
}
Заключение
В этой статье мы рассмотрели использование веб интерфейса для управления устройством на базе платы ESP32. Конечно, при желании представленную концепцию можно усовершенствовать, добавив к примеру, аутентификацию при доступе к серверу или что‑то еще.
Однако, предложенное решение тоже можно использовать для управления различными устройствами как в режиме клиента беспроводной сети, так и в качестве самостоятельной точки доступа.
В завершение рад пригласить всех желающих на открытые уроки, которые пройдут в рамках курса Otus «Embedded Developer»:
2 апреля: Эхо технологий: Звуковая магия микроконтроллеров. Подробнее
10 апреля: АЦП ESP32. Оцифровать сигнал, а не «погоду на Марсе». Нюансы, особенности, повышение точности. Подробнее
21 апреля: Атмосфера в кармане: Управляйте климатом с легкостью. Подробнее