Крошечный веб-сервер на 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 +="<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 апреля: Атмосфера в кармане: Управляйте климатом с легкостью. Подробнее


ссылка на оригинал статьи https://habr.com/ru/articles/894174/