Как стать автором
Обновить
536.18
OTUS
Цифровые навыки от ведущих экспертов

Крошечный веб-сервер на ESP32

Время на прочтение12 мин
Количество просмотров12K

Доступ по сети к вашему 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 +="<p>LED1 Status: ON</p><a class=\"button button-off\" href=\"/led1off\">OFF</a>\n";}
else
 {ptr +="<p>LED1 Status: OFF</p><a class=\"button button-on\" href=\"/led1on\">ON</a>\n";}

if(led2stat)
 {ptr +="<p>LED2 Status: ON</p><a class=\"button button-off\" href=\"/led2off\">OFF</a>\n";}
else
 {ptr +="<p>LED2 Status: OFF</p><a class=\"button button-on\" href=\"/led2on\">ON</a>\n";}

Веб‑интерфейс будет иметь следующий вид:

При нажатии:

Полный код скетча.

Скрытый текст
#include <WiFi.h>
#include <WebServer.h>

/*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 = "<!DOCTYPE html> <html>\n";
  ptr +="<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr +="<title>LED Control</title>\n";
  ptr +="<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr +=".button {display: block;width: 80px;background-color: #3498db;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr +=".button-on {background-color: #3498db;}\n";
  ptr +=".button-on:active {background-color: #2980b9;}\n";
  ptr +=".button-off {background-color: #34495e;}\n";
  ptr +=".button-off:active {background-color: #2c3e50;}\n";
  ptr +="p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr +="</style>\n";
  ptr +="</head>\n";
  ptr +="<body>\n";
  ptr +="<h1>ESP32 Web Server</h1>\n";
    ptr +="<h3>Using Station(STA) Mode</h3>\n";
  
   if(led1stat)
  {ptr +="<p>LED1 Status: ON</p><a class=\"button button-off\" href=\"/led1off\">OFF</a>\n";}
  else
  {ptr +="<p>LED1 Status: OFF</p><a class=\"button button-on\" href=\"/led1on\">ON</a>\n";}

  if(led2stat)
  {ptr +="<p>LED2 Status: ON</p><a class=\"button button-off\" href=\"/led2off\">OFF</a>\n";}
  else
  {ptr +="<p>LED2 Status: OFF</p><a class=\"button button-on\" href=\"/led2on\">ON</a>\n";}

  ptr +="</body>\n";
  ptr +="</html>\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 <WiFi.h>
#include <WebServer.h>

/* 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 = "<!DOCTYPE html> <html>\n";
  ptr +="<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  ptr +="<title>LED Control</title>\n";
  ptr +="<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
  ptr +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
  ptr +=".button {display: block;width: 80px;background-color: #3498db;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}\n";
  ptr +=".button-on {background-color: #3498db;}\n";
  ptr +=".button-on:active {background-color: #2980b9;}\n";
  ptr +=".button-off {background-color: #34495e;}\n";
  ptr +=".button-off:active {background-color: #2c3e50;}\n";
  ptr +="p {font-size: 14px;color: #888;margin-bottom: 10px;}\n";
  ptr +="</style>\n";
  ptr +="</head>\n";
  ptr +="<body>\n";
  ptr +="<h1>ESP32 Web Server</h1>\n";
  ptr +="<h3>Using Access Point(AP) Mode</h3>\n";
  
   if(led1stat)
  {ptr +="<p>LED1 Status: ON</p><a class=\"button button-off\" href=\"/led1off\">OFF</a>\n";}
  else
  {ptr +="<p>LED1 Status: OFF</p><a class=\"button button-on\" href=\"/led1on\">ON</a>\n";}

  if(led2stat)
  {ptr +="<p>LED2 Status: ON</p><a class=\"button button-off\" href=\"/led2off\">OFF</a>\n";}
  else
  {ptr +="<p>LED2 Status: OFF</p><a class=\"button button-on\" href=\"/led2on\">ON</a>\n";}

  ptr +="</body>\n";
  ptr +="</html>\n";
  return ptr;
}

Заключение

В этой статье мы рассмотрели использование веб интерфейса для управления устройством на базе платы ESP32. Конечно, при желании представленную концепцию можно усовершенствовать, добавив к примеру, аутентификацию при доступе к серверу или что‑то еще.

Однако, предложенное решение тоже можно использовать для управления различными устройствами как в режиме клиента беспроводной сети, так и в качестве самостоятельной точки доступа.

В завершение рад пригласить всех желающих на открытые уроки, которые пройдут в рамках курса Otus «Embedded Developer»:

  • 2 апреля: Эхо технологий: Звуковая магия микроконтроллеров. Подробнее

  • 10 апреля: АЦП ESP32. Оцифровать сигнал, а не «погоду на Марсе». Нюансы, особенности, повышение точности. Подробнее

  • 21 апреля: Атмосфера в кармане: Управляйте климатом с легкостью. Подробнее

Теги:
Хабы:
Всего голосов 15: ↑11 и ↓4+10
Комментарии15

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS