ESP32 веб-сервер с BME680 – метеостанция (Arduino IDE)

В этом руководстве показано, как создать веб-сервер метеостанции на ESP32 для отображения показаний датчика окружающей среды BME680: газ (качество воздуха), температура, влажность и давление. Показания автоматически обновляются на веб-сервере с помощью Server-Sent Events (SSE). ESP32 будет программироваться с использованием Arduino IDE.

ESP32 BME680 датчик газа, влажности, барометрического давления, температуры окружающей среды, качества воздуха - веб-сервер Arduino IDE

Для создания веб-сервера мы будем использовать библиотеку ESP Async Web Server, которая предоставляет простой способ создания асинхронного веб-сервера.

Датчик окружающей среды BME680

BME680 – это датчик окружающей среды, который объединяет датчики газа, температуры, влажности и давления. Датчик газа может обнаруживать широкий спектр газов, таких как летучие органические соединения (ЛОС). По этой причине BME680 может использоваться для контроля качества воздуха в помещениях.

BME680 датчик газа, влажности, барометрического давления, температуры окружающей среды, качества воздуха - вид спереди

BME680 содержит MOX (металлооксидный) сенсор, который обнаруживает ЛОС в воздухе. Этот сенсор дает вам качественное представление о сумме ЛОС/загрязнителей в окружающем воздухе. В качестве необработанного сигнала BME680 выдает значения сопротивления. Эти значения изменяются в зависимости от концентрации ЛОС:

BME680 датчик газа - сопротивление и принцип работы
  • Более высокая концентрация ЛОС >> Более низкое сопротивление

  • Более низкая концентрация ЛОС >> Более высокое сопротивление

Для получения дополнительной информации о BME680 прочитайте наше руководство по началу работы: ESP32: BME680 Environmental Sensor using Arduino IDE (Gas, Pressure, Humidity, Temperature).

Необходимые компоненты

ESP32 плата и BME680 датчик газа - схема подключения

Для выполнения этого руководства вам понадобятся следующие компоненты:

Вы можете использовать приведенные выше ссылки или перейти непосредственно на MakerAdvisor.com/tools, чтобы найти все компоненты для ваших проектов по лучшей цене!

Схема подключения – ESP32 с BME680

BME680 может взаимодействовать по протоколам связи I2C или SPI. В этом руководстве мы будем использовать протокол связи I2C.

Следуйте приведенной ниже схеме подключения, чтобы соединить BME680 с ESP32, используя стандартные выводы I2C.

Схема подключения ESP32 BME680 I2C

Рекомендуемое чтение: ESP32 Pinout Reference – Which GPIO pins should you use?

Подготовка Arduino IDE

Мы будем программировать плату ESP32 с помощью Arduino IDE. Поэтому убедитесь, что у вас установлено дополнение ESP32. Следуйте следующему руководству:

Вам также необходимо установить следующие библиотеки:

Вы можете установить библиотеки через менеджер библиотек Arduino. Перейдите в Sketch > Include Library > Manage Libraries и найдите библиотеки по имени для установки.

Код веб-сервера ESP32 BME680

Откройте Arduino IDE и скопируйте следующий код. Для работы вам нужно указать учетные данные вашей сети: SSID и пароль.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-bme680-sensor-arduino/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
#include <WiFi.h>
#include "ESPAsyncWebServer.h"

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

//Uncomment if using SPI
/*#define BME_SCK 18
#define BME_MISO 19
#define BME_MOSI 23
#define BME_CS 5*/

Adafruit_BME680 bme; // I2C
//Adafruit_BME680 bme(BME_CS); // hardware SPI
//Adafruit_BME680 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK);

float temperature;
float humidity;
float pressure;
float gasResistance;

AsyncWebServer server(80);
AsyncEventSource events("/events");

unsigned long lastTime = 0;
unsigned long timerDelay = 30000;  // send readings timer

void getBME680Readings(){
  // Tell BME680 to begin measurement.
  unsigned long endTime = bme.beginReading();
  if (endTime == 0) {
    Serial.println(F("Failed to begin reading :("));
    return;
  }
  if (!bme.endReading()) {
    Serial.println(F("Failed to complete reading :("));
    return;
  }
  temperature = bme.temperature;
  pressure = bme.pressure / 100.0;
  humidity = bme.humidity;
  gasResistance = bme.gas_resistance / 1000.0;
}

String processor(const String& var){
  getBME680Readings();
  //Serial.println(var);
  if(var == "TEMPERATURE"){
    return String(temperature);
  }
  else if(var == "HUMIDITY"){
    return String(humidity);
  }
  else if(var == "PRESSURE"){
    return String(pressure);
  }
  else if(var == "GAS"){
    return String(gasResistance);
  }
  return "0";
}

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>BME680 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: #4B1D3F; color: white; font-size: 1.7rem; }
    .content { padding: 20px; }
    .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }
    .cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
    .reading { font-size: 2.8rem; }
    .card.temperature { color: #0e7c7b; }
    .card.humidity { color: #17bebb; }
    .card.pressure { color: #3fca6b; }
    .card.gas { color: #d62246; }
  </style>
</head>
<body>
  <div class="topnav">
    <h3>BME680 WEB SERVER</h3>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card temperature">
        <h4><i class="fas fa-thermometer-half"></i> TEMPERATURE</h4><p><span class="reading"><span id="temp">%TEMPERATURE%</span> &deg;C</span></p>
      </div>
      <div class="card humidity">
        <h4><i class="fas fa-tint"></i> HUMIDITY</h4><p><span class="reading"><span id="hum">%HUMIDITY%</span> &percnt;</span></p>
      </div>
      <div class="card pressure">
        <h4><i class="fas fa-angle-double-down"></i> PRESSURE</h4><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p>
      </div>
      <div class="card gas">
        <h4><i class="fas fa-wind"></i> GAS</h4><p><span class="reading"><span id="gas">%GAS%</span> K&ohm;</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").textContent = e.data;
 }, false);

 source.addEventListener('humidity', function(e) {
  console.log("humidity", e.data);
  document.getElementById("hum").textContent = e.data;
 }, false);

 source.addEventListener('pressure', function(e) {
  console.log("pressure", e.data);
  document.getElementById("pres").textContent = e.data;
 }, false);

 source.addEventListener('gas', function(e) {
  console.log("gas", e.data);
  document.getElementById("gas").textContent = e.data;
 }, false);
}
</script>
</body>
</html>)rawliteral";

void setup() {
  Serial.begin(115200);

  // Set the device as a Station and Soft Access Point simultaneously
  WiFi.mode(WIFI_AP_STA);

  // Set device as a Wi-Fi Station
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Setting as a Wi-Fi Station..");
  }
  Serial.print("Station IP Address: ");
  Serial.println(WiFi.localIP());
  Serial.println();

  // Init BME680 sensor
  if (!bme.begin()) {
    Serial.println(F("Could not find a valid BME680 sensor, check wiring!"));
    while (1);
  }
  // Set up oversampling and filter initialization
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(320, 150); // 320*C for 150 ms

  // Handle Web Server
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/html", index_html, processor);
  });

  // Handle Web Server Events
  events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis
    // and set reconnect delay to 1 second
    client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&events);
  server.begin();
}

void loop() {
  if ((millis() - lastTime) > timerDelay) {
    getBME680Readings();
    Serial.printf("Temperature = %.2f oC \n", temperature);
    Serial.printf("Humidity = %.2f %% \n", humidity);
    Serial.printf("Pressure = %.2f hPa \n", pressure);
    Serial.printf("Gas Resistance = %.2f KOhm \n", gasResistance);
    Serial.println();

    // Send Events to the Web Server with the Sensor Readings
    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());
    events.send(String(gasResistance).c_str(),"gas",millis());

    lastTime = millis();
  }
}

Просмотреть исходный код

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

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Как работает код

Прочитайте этот раздел, чтобы узнать, как работает код, или перейдите к следующему разделу.

Подключение библиотек

Начните с подключения необходимых библиотек. Библиотека Wire необходима для протокола связи I2C. Мы также подключаем библиотеку SPI, если вы хотите использовать связь SPI вместо I2C.

#include <Wire.h>
#include <SPI.h>

Библиотеки Adafruit_Sensor и Adafruit_BME680 необходимы для взаимодействия с датчиком BME680.

#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"

Библиотеки WiFi и ESPAsyncWebServer используются для создания веб-сервера.

#include <WiFi.h>
#include "ESPAsyncWebServer.h"

Сетевые учетные данные

Вставьте учетные данные вашей сети в следующие переменные, чтобы ESP32 мог подключиться к вашей локальной сети через Wi-Fi.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Связь I2C

Создайте объект Adafruit_BME680 с именем bme на стандартных выводах I2C ESP32.

Adafruit_BME680 bme; // I2C

Если вы хотите использовать связь SPI вместо I2C, вам нужно определить выводы SPI ESP32 в следующих строках (для раскомментирования удалите /* и */):

/*#define BME_SCK 18
#define BME_MISO 19
#define BME_MOSI 23
#define BME_CS 15*/

А затем создайте объект Adafruit_BME680, используя эти выводы (для раскомментирования удалите //).

//Adafruit_BME680 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK);

Объявление переменных

Переменные типа float – temperature, humidity, pressure и gasResistance – будут использоваться для хранения показаний датчика BME680.

float temperature;
float humidity;
float pressure;
float gasResistance;

Переменные lastTime и timerDelay будут использоваться для обновления показаний датчика каждые X секунд. В качестве примера мы будем получать новые показания датчика каждые 30 секунд (30000 миллисекунд). Вы можете изменить время задержки в переменной timerDelay.

unsigned long lastTime = 0;
unsigned long timerDelay = 30000;

Создайте асинхронный веб-сервер на порту 80.

AsyncWebServer server(80);

Создание источника событий

Для автоматического отображения информации на веб-сервере при поступлении нового показания мы будем использовать Server-Sent Events (SSE).

Следующая строка создает новый источник событий на /events.

AsyncEventSource events("/events");

Server-Sent Events позволяют веб-странице (клиенту) получать обновления от сервера. Мы будем использовать это для автоматического отображения новых показаний на странице веб-сервера, когда доступны новые показания BME680.

Важно: Server-Sent Events не поддерживаются в Internet Explorer.

Получение показаний BME680

Функция getBME680Reading() получает показания газа, температуры, влажности и давления от датчика BME680 и сохраняет их в переменных gasResistance, temperature, humidity и pressure.

void getBME680Readings(){
  // Tell BME680 to begin measurement.
  unsigned long endTime = bme.beginReading();
  if (endTime == 0) {
    Serial.println(F("Failed to begin reading :("));
    return;
  }
  if (!bme.endReading()) {
    Serial.println(F("Failed to complete reading :("));
    return;
  }
  temperature = bme.temperature;
  pressure = bme.pressure / 100.0;
  humidity = bme.humidity;
  gasResistance = bme.gas_resistance / 1000.0;
}

Процессор

Функция processor() заменяет все заполнители в HTML-тексте, используемом для создания веб-страницы, текущими показаниями датчика.

String processor(const String& var){
  getBME680Readings();
  //Serial.println(var);
  if(var == "TEMPERATURE"){
    return String(temperature);
  }
  else if(var == "HUMIDITY"){
    return String(humidity);
  }
  else if(var == "PRESSURE"){
    return String(pressure);
  }
 else if(var == "GAS"){
    return String(gasResistance);
  }
}

Это позволяет отображать текущие показания датчика на веб-странице при первом обращении к ней. В противном случае вы бы видели пустое место до тех пор, пока не будут доступны новые показания (что может занять некоторое время в зависимости от времени задержки, которое вы определили в коде).

Создание веб-страницы

Переменная index_html содержит весь HTML, CSS и JavaScript для создания веб-страницы. Мы не будем подробно описывать работу HTML и CSS. Мы просто рассмотрим, как обрабатывать события, отправленные сервером.

Давайте быстро взглянем на строку, которая отображает температуру:

<h4><i class="fas fa-thermometer-half"></i> TEMPERATURE</h4><p><span class="reading"><span id="temp">%TEMPERATURE%</span> &deg;C</span></p>

Вы можете видеть, что заполнитель %TEMPERATURE% окружен тегами <span id="temp"></span>. HTML-атрибут id используется для указания уникального идентификатора для HTML-элемента.

Он используется для указания на определенный стиль или может использоваться JavaScript для доступа и манипулирования элементом с этим конкретным идентификатором. Именно это мы и собираемся сделать.

Например, когда веб-сервер получает новое событие с последним показанием температуры, мы обновляем HTML-элемент с идентификатором «temp» новым показанием.

Аналогичный процесс выполняется для обновления остальных показаний.

Обработка событий

Создайте новый объект 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) {

Когда доступно новое показание температуры, ESP32 отправляет событие («temperature») клиенту. Следующие строки обрабатывают то, что происходит, когда браузер получает это событие.

console.log("temperature", e.data);
document.getElementById("temp").textContent = e.data;

По сути, выводим новые показания в консоль браузера и помещаем полученные данные в элемент с соответствующим идентификатором («temp») на веб-странице.

Аналогичный процесс выполняется для влажности, давления и сопротивления газа.

source.addEventListener('humidity', function(e) {
  console.log("humidity", e.data);
  document.getElementById("hum").textContent = e.data;
}, false);

source.addEventListener('pressure', function(e) {
  console.log("pressure", e.data);
  document.getElementById("pres").textContent = e.data;
}, false);

source.addEventListener('gas', function(e) {
  console.log("gas", e.data);
  document.getElementById("gas").textContent = e.data;
}, false);

setup()

В функции setup() инициализируйте монитор последовательного порта.

Serial.begin(115200);

Подключите ESP32 к вашей локальной сети и выведите IP-адрес ESP32.

// Set device as a Wi-Fi Station
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Setting as a Wi-Fi Station..");
}
Serial.print("Station IP Address: ");
Serial.println(WiFi.localIP());
Serial.println();

Инициализируйте датчик BME680.

// Init BME680 sensor
if (!bme.begin()) {
  Serial.println(F("Could not find a valid BME680 sensor, check wiring!"));
  while (1);
}
// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms

Обработка запросов

Когда вы обращаетесь к IP-адресу ESP32 по корневому URL /, отправьте текст, сохраненный в переменной index_html, для создания веб-страницы и передайте процессор в качестве аргумента, чтобы все заполнители были заменены последними показаниями датчика.

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html, processor);
});

Источник событий сервера

Настройте источник событий на сервере.

// Handle Web Server Events
events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
  // send event with message "hello!", id current millis
  // and set reconnect delay to 1 second
  client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);

Наконец, запустите сервер.

server.begin();

loop()

В функции loop() получите новые показания датчика:

getBME680Readings();

Выведите новые показания в монитор последовательного порта.

Serial.printf("Temperature = %.2f oC \n", temperature);
Serial.printf("Humidity = %.2f %% \n", humidity);
Serial.printf("Pressure = %.2f hPa \n", pressure);
Serial.printf("Gas Resistance = %.2f KOhm \n", gasResistance);
Serial.println();

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

// Send Events to the Web Server with the Sensor Readings
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());
events.send(String(gasResistance).c_str(),"gas",millis());

Следующая диаграмма обобщает, как Server-Sent Events работают для обновления веб-страницы.

Обновление показаний веб-сервера BME680 ESP32 с Arduino IDE через Server-Sent Events

Загрузка кода

Теперь загрузите код в ваш ESP32. Убедитесь, что вы выбрали правильную плату и COM-порт.

После загрузки откройте монитор последовательного порта на скорости 115200 бод. Нажмите встроенную кнопку RST/EN на ESP32. IP-адрес ESP32 должен быть напечатан в мониторе последовательного порта.

IP-адрес ESP32 в мониторе последовательного порта Arduino IDE

Демонстрация

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

Демонстрация веб-сервера ESP32 BME680 с датчиком газа

Показания обновляются автоматически с помощью Server-Sent Events.

Демонстрация веб-сервера ESP32 с датчиком газа BME680 на мобильном устройстве

Заключение

В этом руководстве вы узнали, как создать асинхронный веб-сервер метеостанции на ESP32 для отображения показаний датчика BME680 – газ (качество воздуха), температура, влажность и давление – и как автоматически обновлять показания на веб-странице с помощью Server-Sent Events.

У нас есть другие руководства по веб-серверам, которые могут вас заинтересовать:

Надеемся, вы нашли этот проект интересным. Узнайте больше об ESP32 с нашими ресурсами:

Спасибо за чтение.


Источник: ESP32 Web Server with BME680 – Weather Station (Arduino IDE)