В этой статье мы расскажем, как использовать SSE (события отправленные сервером) с платой ESP8266 NodeMCU, запрограммированной с помощью Arduino IDE.
SSE позволяет браузеру получать автоматические обновления с сервера через HTTP-соединение. Это полезно, например, для отправки в браузер обновленных показаний датчиков. Всякий раз, когда доступны новые данные, ESP8266 отправляет их клиенту, и веб-страница может быть обновлена автоматически без необходимости делать дополнительные запросы.
В качестве примера мы построим веб-сервер, который будет отображать показания температуры, влажности и давления на основе датчика BME280.
Введение в события, отправленные сервером (SSE)
Server-Sent Events (SSE) позволяет клиенту получать автоматические обновления с сервера через HTTP-соединение.
Клиент инициирует соединение SSE, а сервер использует протокол источника событий для отправки обновлений клиенту. Клиент будет получать обновления от сервера, но он не может отправлять данные на сервер после первоначального рукопожатия.
Это полезно для отправки обновленных показаний датчиков в браузер. Всякий раз, когда доступны новые данные ESP8266 отправляет их клиенту, и веб-страница будет обновляться автоматически без дополнительных запросов.
Вместо показаний датчиков вы можете отправлять любые данные, которые могут быть полезны для вашего проекта, такие как состояния GPIO, уведомления при обнаружении движения и т. д.
Важно: События, отправленные сервером (SSE), не поддерживаются в Internet Explorer.
Обзор проекта
Вот веб-страница, которую мы создадим для этого проекта:
- Веб-сервер ESP8266 организует нам три таблички с показаниями: температуры, влажности и давления датчика BME280;
- ESP8266 будет получать новые показания датчика каждые 30 секунд;
- Всякий раз, когда будут доступны новые данные, плата (сервер) отправит эти данные клиенту, используя SSE;
- Клиент, получив данные, соответствующим образом обновит веб-страницу;
Как это работает?
На следующей схеме показано, как события, отправленные сервером, работают для обновления веб-страницы.
- Клиент инициирует соединение SSE, а сервер использует протокол источника событий в URL-адресе /events для отправки обновлений клиенту;
- ESP8266 получает новые показания датчика;
- Он отправляет клиенту показания как события со следующими именами: temperature, humidity а также pressure;
- У клиента есть прослушиватели событий для SSE, и он получает обновленные показания датчиков для этих событий;
- Он обновляет веб-страницу с новыми показаниями.
Подготовка Arduino IDE
Мы запрограммируем плату ESP8266 с помощью Arduino IDE, поэтому убедитесь, что плата установлена в вашей Arduino IDE.
Установка библиотек — асинхронный веб-сервер
Для создания веб-сервера мы будем использовать библиотеку ESPAsyncWebServer. Для правильной работы этой библиотеке требуется библиотека ESPAsyncTCP. Щелкните ссылки ниже, чтобы загрузить библиотеки:
Эти библиотеки недоступны для установки через менеджер библиотек Arduino, поэтому вам необходимо скопировать файлы библиотеки в папку библиотек установки Arduino. В качестве альтернативы, в вашей Arduino IDE вы можете перейти в Эскиз > Include Library > Add .zip Library и выбрать библиотеки, которые вы только что загрузили.
Установка библиотек — датчик BME280
Чтобы получить показания от датчика BME280, мы будем использовать библиотеку Adafruit_BME280. Вам также необходимо установить библиотеку Adafruit Unified Sensor. Выполните следующие шаги, чтобы установить библиотеки в вашу Arduino IDE:
- Откройте IDE Arduino и выберите Скетч > Include Library > Manage Libraries. Должен открыться менеджер библиотеки.
- Введите adafruit bme280 в поле поиска и установите библиотеку.
Чтобы использовать библиотеку BME280, вам также необходимо установить библиотеку Adafruit Unified Sensor. Выполните следующие шаги, чтобы установить библиотеку в свою Arduino IDE:
- В менеджере библиотеки введите «Adafruit Unified Sensor» в поле поиска. Прокрутите до конца, чтобы найти библиотеку и установите ее.
После установки библиотек перезапустите IDE Arduino.
Построение схемы
Чтобы проиллюстрировать, как использовать события, отправленные сервером с ESP8266, мы отправим показания с датчика BME280 в браузер. Итак, вам нужно подключить датчик BME280 к ESP8266.
Требуемые детали
Для выполнения этого проекта вам понадобятся следующие детали:
- Модуль датчика BME280
- ESP8266
- Макетная плата
- Перемычки
Схема подключения BME280 к ESP8266 с использованием I2C
Для подключения датчика BME280 к ESP8266 мы используем I2C шину. Для этого подключим датчик к контактам SDA и SCL платы ESP8266, как показано ниже:
Скетч для веб-сервера ESP8266 с использованием событий, отправленных сервером (SSE)
Скопируйте следующий код в свою среду разработки Arduino:
#include <ESP8266WiFi.h> #include <ESPAsyncTCP.h> #include <ESPAsyncWebServer.h> #include <Adafruit_BME280.h> #include <Adafruit_Sensor.h> // Замените учетными данными своей сети const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Создайте объект AsyncWebServer на порту 80 AsyncWebServer server(80); // Создайте источник события на /events AsyncEventSource events("/events"); // Переменные таймера unsigned long lastTime = 0; unsigned long timerDelay = 30000; // Создать сенсорный объект Adafruit_BME280 bme; // BME280 подключается к ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL) float temperature; float humidity; float pressure; // инициализация BME280 void initBME(){ if (!bme.begin(0x76)) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } } void getSensorReadings(){ temperature = bme.readTemperature(); // Convert temperature to Fahrenheit //temperature = 1.8 * bme.readTemperature() + 32; humidity = bme.readHumidity(); pressure = bme.readPressure()/ 100.0F; } // Инициализировать WiFi void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); } String processor(const String& var){ getSensorReadings(); //Serial.println(var); if(var == "TEMPERATURE"){ return String(temperature); } else if(var == "HUMIDITY"){ return String(humidity); } else if(var == "PRESSURE"){ return String(pressure); } return String(); } const char index_html[] PROGMEM = R"rawliteral( <!DOCTYPE HTML><html> <head> <title>ESP Web Server</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"> <link rel="icon" href="data:,"> <style> html {font-family: Arial; display: inline-block; text-align: center;} p { font-size: 1.2rem;} body { margin: 0;} .topnav { overflow: hidden; background-color: #50B8B4; color: white; font-size: 1rem; } .content { padding: 20px; } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .cards { max-width: 800px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .reading { font-size: 1.4rem; } </style> </head> <body> <div class="topnav"> <h1>BME280 WEB SERVER (SSE)</h1> </div> <div class="content"> <div class="cards"> <div class="card"> <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p><p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p> </div> <div class="card"> <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p><p><span class="reading"><span id="hum">%HUMIDITY%</span> %</span></p> </div> <div class="card"> <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p> </div> </div> </div> <script> if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('temperature', function(e) { console.log("temperature", e.data); document.getElementById("temp").innerHTML = e.data; }, false); source.addEventListener('humidity', function(e) { console.log("humidity", e.data); document.getElementById("hum").innerHTML = e.data; }, false); source.addEventListener('pressure', function(e) { console.log("pressure", e.data); document.getElementById("pres").innerHTML = e.data; }, false); } </script> </body> </html>)rawliteral"; void setup() { Serial.begin(115200); initWiFi(); initBME(); // Обработка веб-сервера server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html, processor); }); // Обработка событий веб-сервера events.onConnect([](AsyncEventSourceClient *client){ if(client->lastId()){ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); } // отправка события "hello!", id текущие миллисекунды // задержка повторного подключения 1 сек client->send("hello!", NULL, millis(), 10000); }); server.addHandler(&events); server.begin(); } void loop() { if ((millis() - lastTime) > timerDelay) { getSensorReadings(); Serial.printf("Temperature = %.2f ºC \n", temperature); Serial.printf("Humidity = %.2f \n", humidity); Serial.printf("Pressure = %.2f hPa \n", pressure); Serial.println(); // Отправка событий на веб-сервер с показаниями датчика events.send("ping",NULL,millis()); events.send(String(temperature).c_str(),"temperature",millis()); events.send(String(humidity).c_str(),"humidity",millis()); events.send(String(pressure).c_str(),"pressure",millis()); lastTime = millis(); } }
Вставьте свои сетевые учетные данные в следующие переменные:
- const char* ssid = «REPLACE_WITH_YOUR_SSID»;
- const char* password = «REPLACE_WITH_YOUR_PASSWORD»;
Разбор скетча
Подключение библиотек
Подключаем библиотеки Adafruit_Sensor и Adafruit_BME280 которые необходимы для взаимодействия с датчиком BME280:
#include <Adafruit_BME280.h> #include <Adafruit_Sensor.h>
Для создания веб-сервера подключаем библиотеки WiFi, ESPAsyncWebServer и AsyncTCP:
#include <WiFi.h> #include <ESPAsyncTCP.h> #include "ESPAsyncWebServer.h"
Сетевые учетные данные
Вставьте свои сетевые учетные данные в следующие переменные, чтобы ESP8266 мог с помощью Wi-Fi подключиться к вашей локальной сети:
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer и AsyncEventSource
Создаем объект AsyncWebServer на 80 порту:
AsyncWebServer server(80);
Следующая строка создает новый источник событий на /events:
AsyncEventSource events("/events");
Объявление переменных
Переменные lastTime и timerDelay будут использоваться для обновления показаний датчика каждые X секунд. Например, мы будем получать новые показания датчиков каждые 30 секунд (30000 миллисекунд). Вы можете изменить это время задержки в переменной timerDelay:
unsigned long lastTime = 0; unsigned long timerDelay = 30000;
Создаем объект с именем bme (Adafruit_BME280) на выводах I2C ESP8266 по умолчанию:
Adafruit_BME280 bme;
Переменные с плавающей запятой temperature, humidity и pressure будут использоваться для хранения показаний датчика BME280:
float temperature; float humidity; float pressure;
Инициализация BME280
Следующая функция вызывается для инициализации датчика BME280:
void initBME(){ if (!bme.begin(0x76)) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } }
Получение показания BME280
Функция getSensorReading() получает показания температуры, влажности и давления с датчика BME280 и сохраняет их в переменных temperature, humidity и pressure соответственно:
void getSensorReadings(){ temperature = bme.readTemperature(); // Convert temperature to Fahrenheit //temperature = 1.8 * bme.readTemperature() + 32; humidity = bme.readHumidity(); pressure = bme.readPressure()/ 100.0F; }
Инициализация Wi-Fi
Следующая функция устанавливает ESP8266 как Wi-Fi станцию и подключается к вашему маршрутизатору, используя ssid и пароль, определенные ранее:
void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); }
Функция processor()
Функция processor() заменяет любые заполнители HTML-текста, используемого для построения веб-страницы, текущими показаниями датчиков перед отправкой их в браузер:
String processor(const String& var){ getSensorReadings(); //Serial.println(var); if(var == "TEMPERATURE"){ return String(temperature); } else if(var == "HUMIDITY"){ return String(humidity); } else if(var == "PRESSURE"){ return String(pressure); } }
Это позволяет нам вывести на веб-страницу текущие показания датчиков, когда мы обращаетесь к ней в первый раз. В противном случае мы увидим пустое место до тех пор, пока не появятся новые показания (это может занять некоторое время в зависимости от времени задержки, определенного в коде).
Создание веб-страницы
Переменная index_html содержит все HTML, CSS и JavaScript элементы для построения веб-страницы.
Примечание: Для простоты этого урока мы помещаем все необходимое для построения веб-страницы в переменную index_html. Обратите внимание, что более практичным было бы создать отдельные HTML, CSS и JavaScript файлы, которые затем загружались бы в файловую систему ESP8266, и впоследствии происходила бы ссылка на них в коде.
Вот содержание переменной index_html:
<!DOCTYPE HTML> <html> <head> <title>ESP Web Server</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"> <link rel="icon" href="data:,"> <style> html { font-family: Arial; display: inline-block; text-align: center; } p { font-size: 1.2rem; } body { margin: 0; } .topnav { overflow: hidden; background-color: #50B8B4; color: white; font-size: 1rem; } .content { padding: 20px; } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .cards { max-width: 800px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .reading { font-size: 1.4rem; } </style> </head> <body> <div class="topnav"> <h1>BME280 WEB SERVER (SSE)</h1> </div> <div class="content"> <div class="cards"> <div class="card"> <p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p> <p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p> </div> <div class="card"> <p><i class="fas fa-tint" style="color:#00add6;"></i> HUMIDITY</p> <p><span class="reading"><span id="hum">%HUMIDITY%</span> %</span></p> </div> <div class="card"> <p><i class="fas fa-angle-double-down" style="color:#e1e437;"></i> PRESSURE</p><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p> </div> </div> </div> <script> if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('temperature', function(e) { console.log("temperature", e.data); document.getElementById("temp").innerHTML = e.data; }, false); source.addEventListener('humidity', function(e) { console.log("humidity", e.data); document.getElementById("hum").innerHTML = e.data; }, false); source.addEventListener('pressure', function(e) { console.log("pressure", e.data); document.getElementById("pres").innerHTML = e.data; }, false); } </script> </body> </html>
Мы не будем вдаваться в подробности того, как работают HTML и CSS. Мы просто посмотрим, как обрабатывать события, отправленные сервером.
CSS
Между тегами <style></style> мы включаем стили для оформления веб-страницы с помощью CSS. Не бойтесь менять его. Изменяя его, вы можете оформить страницу по своему вкусу. Мы не будем объяснять, как работает CSS для этой веб-страницы, потому что это не имеет отношения к данному уроку.
HTML
Между тегами <body</body> мы добавляем содержимое веб-страницы, которое видно пользователю.
В коде есть заголовок с содержанием » BME280 WEB SERVER (SSE)”. Это текст, который отображается на верхней панели (текст можно менять по желанию):
<h1>BME280 WEB SERVER (SSE)</h1>
Затем мы отображаем показания датчиков в отдельных тегах div. Давайте быстро взглянем на абзацы, в которых отображается температура:
<p><i class="fas fa-thermometer-half" style="color:#059e8a;"></i> TEMPERATURE</p> <p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p>
Первый абзац просто отображает текст «TEMPERATURE». Мы определяем цвет, а также значок.
Во втором абзаце видно, что плейсхолдер %TEMPERATURE% окружен тегом <span id = ”temp”> </span>. Атрибут id используется для указания уникального идентификатора HTML-элемента.
Он используется для указания на определенный стиль или может быть использован JavaScript для доступа и манипулирования элементом с этим конкретным идентификатором.
Именно это мы и собираемся сделать. Например, когда клиент получает новое событие с последним показанием температуры, он обновляет HTML-элемент с идентификатором “temp” с новым показанием.Аналогичный процесс выполняется для обновления других показаний (влажности и давления).
JavaScript – EventSource
JavaScript находится между тегами < script > </script>. Он отвечает за инициализацию соединения EventSource с сервером и обрабатывает события, полученные от сервера.
Давайте посмотрим, как это работает.
Обработка событий
Создается новый объект EventSource с указанием URL страницы, отправляющей обновления. В нашем случае это /events:
if (!!window.EventSource) { var source = new EventSource('/events');
Создав экземпляр источника событий, вы можете начать прослушивание сообщений с сервера с помощью addEventListener().
Это прослушиватели событий по умолчанию, как указано в документации на AsyncWebServer:
source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false);
Затем добавляем прослушиватель событий для temperature:
source.addEventListener('temperature', function(e) {
Когда появляется новое значение температуры, ESP8266 отправляет клиенту событие («temperature»). В следующих строках описывается, что происходит, когда браузер получает это событие:
console.log("temperature", e.data); document.getElementById("temp").innerHTML = e.data;
По сути, выводятся новые показания на веб-странице с указанием полученных данных в элемент с соответствующим идентификатором (“temp“).
Аналогичный процессор используется для измерения влажности и давления:
source.addEventListener('humidity', function(e) { console.log("humidity", e.data); document.getElementById("hum").innerHTML = e.data; }, false); source.addEventListener('pressure', function(e) { console.log("pressure", e.data); document.getElementById("pres").innerHTML = e.data; }, false); source.addEventListener('gas', function(e) { console.log("gas", e.data); document.getElementById("gas").innerHTML = e.data; }, false);
setup()
в setup() инициализируются Serial Monitor, Wi-Fi и датчик BME280:
Serial.begin(115200); initWiFi(); initBME();
Обработка запросов
Когда вы получаете доступ к IP-адресу ESP8266 в корневом каталоге / для создания веб-страницы отправляется текст, который хранится в переменной index_html и передается processor в качестве аргумента, чтобы все плейсхолдеры были заменены последними показаниями датчиков.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html, processor); });
Источник события сервера
Настройка источника событий на сервере:
// Обработка событий веб-сервера events.onConnect([](AsyncEventSourceClient *client){ if(client->lastId()){ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); } // отправка события "hello!", id текщие милисекунды // задержка повторного подключения 1 сек client->send("hello!", NULL, millis(), 10000); }); server.addHandler(&events);
Запуск сервера:
server.begin();
loop()
в loop(), получаем новые показания датчика:
getSensorReadings();
Отправляем новые показания в Serial Monitor:
Serial.printf("Temperature = %.2f ºC \n", temperature); Serial.printf("Humidity = %.2f % \n", humidity); Serial.printf("Pressure = %.2f hPa \n", pressure); Serial.println();
Наконец, отправляем в браузер события с новыми показаниями датчика, чтобы обновить веб-страницу.
// Отправка событий на веб-сервер с показаниями датчика events.send("ping",NULL,millis()); events.send(String(temperature).c_str(),"temperature",millis()); events.send(String(humidity).c_str(),"humidity",millis()); events.send(String(pressure).c_str(),"pressure",millis());
Демонстрация
После ввода ваших сетевых учетных данных в переменные ssid и password, вы можете загрузить код в свою плату. Не забудьте проверить правильность выбора платы и COM-порта.
После загрузки кода откройте Serial Monitor со скоростью 115200 бод и нажмите встроенную кнопку EN/RST. В результате должен быть напечатан IP-адрес ESP:
Откройте браузер в локальной сети и введите IP-адрес ESP8266. Вы должны получить доступ к веб-странице для мониторинга показаний датчиков:
Показания обновляются автоматически каждые 30 секунд.
В то же время вы должны получить новые показания датчика в последовательном мониторе, как показано на предыдущем экране. Кроме того, вы можете проверить, получает ли клиент события. В браузере откройте консоль, нажав Ctrl+Shift+J.