Универсальная метеостанция на ESP32 с печатной платой

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

Видеоурок и демонстрация проекта

Это руководство доступно в видеоформате (смотрите ниже) и в текстовом формате (продолжайте чтение).

Ресурсы

Все необходимые ресурсы для сборки этого проекта перечислены ниже:

Возможности платы метеостанции ESP32

Для сборки этого проекта я разработал печатную плату для платы ESP32 DEVKIT V1 DOIT. Разработанная мной печатная плата работает только с версией, имеющей 30 GPIO.

ESP32 DEVKIT V1 DOIT

Я разработал плату расширения (shield) в качестве компактной метеостанции. Печатная плата имеет множество функций, что позволяет использовать её в различных проектах для разных применений. На самом деле, в этом проекте я использовал не все возможности платы.

Кроме того, эта плата расширения может также использоваться как учебная плата, поскольку она содержит некоторые из наиболее часто используемых компонентов при изучении программирования ESP32.

Возможности печатной платы

Плата расширения позволяет управлять:

  • 2x SMD-светодиода

  • 1x Кнопка

  • 1x Подстроечный потенциометр (тримпот)

  • 1x Датчик температуры и влажности DHT22

  • 1x Барометрический датчик BMP180

  • 1x Фоторезистор (LDR)

  • 1x Модуль карты MicroSD

  • 2x Клеммные колодки – предоставляющие доступ к 3 GPIO для подключения других компонентов

Модуль карты MicroSD является очень интересным дополнением к плате: он может использоваться для хранения показаний, если вы хотите создать регистратор данных, или для хранения HTML-файла для обслуживания веб-страницы – как мы сделаем в этом проекте. Я считаю, что это лучший и более простой способ создания веб-сервера, требующего более сложных веб-страниц.

Назначение выводов платы расширения ESP32

В следующей таблице описано назначение выводов для каждого компонента на плате:

Компонент

Назначение выводов ESP32

Кнопка

GPIO 33

Подстроечный потенциометр

GPIO 32

Фоторезистор (LDR)

GPIO 4

DHT22 (вывод данных)

GPIO 15

LED1

GPIO 27

LED2

GPIO 26

BMP180

SDA (GPIO 21); SCL (GPIO 22)

Модуль SD-карты

MOSI (GPIO 23); MISO (GPIO 19); CLK (GPIO 18); CS (GPIO 5)

Свободные GPIO (клеммные колодки)

GPIO14, GPIO13, GPIO12

Примечание: есть небольшая проблема с назначением выводов. В настоящее время библиотека Arduino WiFi использует GPIO 4, который подключён к LDR. Поэтому у вас, вероятно, возникнут проблемы с получением показаний от LDR при использовании библиотеки WiFi. Чтобы это исправить, вы можете припаять провод от LDR к другому свободному GPIO (который поддерживает АЦП).

Тестирование схемы на макетной плате

Перед разработкой платы расширения я собрал схему на макетной плате. Если вы не хотите делать печатную плату, вы всё равно можете следовать этому проекту, собрав схему на макетной плате.

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

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

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

Схема

Собрав все необходимые компоненты, вы можете собрать схему, следуя приведённой ниже схеме:

Схема сборки на макетной плате

Важно: если вы используете другую плату, вам необходимо перепроверить распиновку.

Вот принципиальная схема:

Принципиальная схема в KiCad

Разработка печатной платы

Убедившись, что схема работает правильно, я разработал печатную плату в KiCad. KiCad – это программное обеспечение с открытым исходным кодом для проектирования печатных плат.

Я не буду подробно описывать процесс разработки печатной платы, но предоставляю все файлы, если вы хотите модифицировать плату для себя. Нажмите здесь, чтобы скачать файлы проекта KiCad.

Вид печатной платы в KiCad

Заказ печатных плат

Вам не нужно знать, как проектировать печатную плату, чтобы заказать её. Вам просто нужно:

  1. Скачать Gerber-файлы, нажмите здесь, чтобы скачать .zip файл.

  2. Перейдите на JLCPCB.com, нажмите кнопку «QUOTE NOW» и загрузите только что скачанный .zip файл.

JLCPCB Quote Now
  1. Вы увидите сообщение об успешной загрузке внизу. Затем вы можете использовать ссылку «Gerber Viewer» в правом нижнем углу, чтобы проверить, всё ли прошло как ожидалось. Вы можете просматривать верхнюю и нижнюю стороны печатной платы. Вы можете показывать или скрывать маску пайки, шелкографию, медные слои и т.д.

Успешная загрузка JLCPCB

С настройками по умолчанию вы можете заказать 10 печатных плат всего за $2. Однако, если вы хотите выбрать другие настройки, например другой цвет печатной платы, это будет стоить на несколько долларов дороже.

Когда вы довольны своим заказом, нажмите кнопку «SAVE TO CART», чтобы завершить заказ.

Сохранить в корзину JLCPCB

Мои печатные платы были изготовлены за 1 день и прибыли через 5 рабочих дней при использовании доставки DHL.

Распаковка

Через неделю я получил свои печатные платы в офис. Всё было хорошо упаковано, и я также получил ручку от JLCPCB.

Посылка от JLCPCB

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

Качество печатных плат

Пайка компонентов

Следующим шагом была пайка компонентов на печатную плату. Я использовал SMD-светодиоды и SMD-резисторы. Я знаю, что паять SMD-компоненты немного сложно, но они позволяют значительно сэкономить место на печатной плате. Я припаял штыревые разъёмы для подключения ESP32 и датчиков. Таким образом, я могу легко заменить датчики при необходимости.

Компоненты для пайки

Вот список всех компонентов, которые нужно припаять на печатную плату:

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

Печатная плата после пайки

Подготовка платы ESP32 в Arduino IDE

Чтобы загрузить код на ESP32 с помощью Arduino IDE, вам необходимо установить дополнение для Arduino IDE, которое позволяет программировать ESP32 с использованием Arduino IDE и его языка программирования. Следуйте следующему руководству для подготовки вашей Arduino IDE:

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

Код

Следующий шаг – написание кода для считывания показаний датчиков и создания веб-сервера. Код для этого проекта разделён на две части:

  • Код в Arduino IDE для считывания показаний датчиков и размещения веб-сервера

  • HTML-файл для создания веб-страницы. Этот HTML-файл должен быть сохранён на карте MicroSD.

Скопируйте приведённый код в Arduino IDE. Код для этого проекта довольно длинный, но его достаточно легко понять. Я также добавил различные комментарии по всему коду. Пока не загружайте код.

/*
 * Rui Santos
 * Complete Project Details https://randomnerdtutorials.com
 */

// Load required libraries
#include <WiFi.h>
#include "SD.h"
#include "DHT.h"
#include <Wire.h>
#include <Adafruit_BMP085.h>

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

// uncomment one of the lines below for whatever DHT sensor type you're using
//#define DHTTYPE DHT11   // DHT 11
//#define DHTTYPE DHT21   // DHT 21 (AM2301)
#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321

// GPIO the DHT is connected to
const int DHTPin = 15;
//intialize DHT sensor
DHT dht(DHTPin, DHTTYPE);

// create a bmp object
Adafruit_BMP085 bmp;

// Web page file stored on the SD card
File webFile;

// Set potentiometer GPIO
const int potPin = 32;

// IMPORTANT: At the moment, GPIO 4 doesn't work as an ADC when using the Wi-Fi library
// This is a limitation of this shield, but you can use another GPIO to get the LDR readings
const int LDRPin = 4;

// variables to store temperature and humidity
float tempC;
float tempF;
float humi;

// Variable to store the HTTP request
String header;

// Set web server port number to 80
WiFiServer server(80);

void setup(){
  // initialize serial port
  Serial.begin(115200);

  // initialize DHT sensor
  dht.begin();

  // initialize BMP180 sensor
  if (!bmp.begin()){
    Serial.println("Could not find BMP180 or BMP085 sensor");
    while (1) {}
  }

  // initialize SD card
  if(!SD.begin()){
      Serial.println("Card Mount Failed");
      return;
  }
  uint8_t cardType = SD.cardType();
  if(cardType == CARD_NONE){
      Serial.println("No SD card attached");
      return;
  }
  // initialize SD card
  Serial.println("Initializing SD card...");
  if (!SD.begin()) {
      Serial.println("ERROR - SD card initialization failed!");
      return;    // init failed
  }

  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  server.begin();
}

void loop(){
  WiFiClient client = server.available();   // Listen for incoming clients

  if (client) {  // if new client connects
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {   // client data available to read
        char c = client.read(); // read 1 byte (character) from client
        header += c;
        // if the current line is blank, you got two newline characters in a row.
        // that's the end of the client HTTP request, so send a response:
        if (c == '\n' && currentLineIsBlank) {
          // send a standard http response header
          client.println("HTTP/1.1 200 OK");
          // Send XML file or Web page
          // If client already on the web page, browser requests with AJAX the latest
          // sensor readings (ESP32 sends the XML file)
          if (header.indexOf("update_readings") >= 0) {
            // send rest of HTTP header
            client.println("Content-Type: text/xml");
            client.println("Connection: keep-alive");
            client.println();
            // Send XML file with sensor readings
            sendXMLFile(client);
          }
          // When the client connects for the first time, send it the index.html file
          // stored in the microSD card
          else {
            // send rest of HTTP header
            client.println("Content-Type: text/html");
            client.println("Connection: keep-alive");
            client.println();
            // send web page stored in microSD card
            webFile = SD.open("/index.html");
            if (webFile) {
              while(webFile.available()) {
                // send web page to client
                client.write(webFile.read());
              }
              webFile.close();
            }
          }
          break;
        }
        // every line of text received from the client ends with \r\n
        if (c == '\n') {
          // last character on line of received text
          // starting new line with next character read
          currentLineIsBlank = true;
        }
        else if (c != '\r') {
          // a text character was received from client
          currentLineIsBlank = false;
        }
        } // end if (client.available())
    } // end while (client.connected())
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected.");
  } // end if (client)
}

// Send XML file with the latest sensor readings
void sendXMLFile(WiFiClient cl){
  // Read DHT sensor and update variables
  readDHT();

  // Prepare XML file
  cl.print("<?xml version = \"1.0\" ?>");
  cl.print("<inputs>");

  cl.print("<reading>");
  cl.print(tempC);
  cl.println("</reading>");

  cl.print("<reading>");
  cl.print(tempF);
  cl.println("</reading>");

  cl.print("<reading>");
  cl.print(humi);
  cl.println("</reading>");

  float currentTemperatureC = bmp.readTemperature();
  cl.print("<reading>");
  cl.print(currentTemperatureC);
  cl.println("</reading>");
  float currentTemperatureF = (9.0/5.0)*currentTemperatureC+32.0;
  cl.print("<reading>");
  cl.print(currentTemperatureF);
  cl.println("</reading>");

  cl.print("<reading>");
  cl.print(bmp.readPressure());
  cl.println("</reading>");

  cl.print("<reading>");
  cl.print(analogRead(potPin));
  cl.println("</reading>");

  // IMPORTANT: Read the note about GPIO 4 at the pin assignment
  cl.print("<reading>");
  cl.print(analogRead(LDRPin));
  cl.println("</reading>");

  cl.print("</inputs>");
}

void readDHT(){
  // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
  humi = dht.readHumidity();
  // Read temperature as Celsius (the default)
  tempC = dht.readTemperature();
  // Read temperature as Fahrenheit (isFahrenheit = true)
  tempF = dht.readTemperature(true);

  // Check if any reads failed and exit early (to try again).
  if (isnan(humi) || isnan(tempC) || isnan(tempF)) {
    Serial.println("Failed to read from DHT sensor!");
    return;
  }

  /*Serial.print("Humidity: ");
  Serial.print(humi);
  Serial.print(" %\t Temperature: ");
  Serial.print(tempC);
  Serial.print(" *C ");
  Serial.print(tempF);
  Serial.println(" *F");*/
}

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

Перед загрузкой кода вам необходимо изменить следующие строки, добавив ваш SSID и пароль.

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

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

Создайте новый файл в текстовом редакторе и скопируйте следующий код. Также вы можете нажать здесь, чтобы скачать файл index.html.

<!DOCTYPE html>
<html>
  <head>
    <title>ESP32 Weather Station</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" href="data:,">
    <script>
      function DisplayCurrentTime() {
          var date = new Date();
          var hours = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
          var minutes = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
          var seconds = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
          time = hours + ":" + minutes + ":" + seconds;
          var currentTime = document.getElementById("currentTime");
          currentTime.innerHTML = time;
      };
      function GetReadings() {
             nocache = "&nocache";
             var request = new XMLHttpRequest();
             request.onreadystatechange = function() {
                     if (this.status == 200) {
                             if (this.responseXML != null) {
                                     // XML file received - contains sensor readings
                                     var count;
                                     var num_an = this.responseXML.getElementsByTagName('reading').length;
                                     for (count = 0; count < num_an; count++) {
                                             document.getElementsByClassName("reading")[count].innerHTML =
                                       this.responseXML.getElementsByTagName('reading')[count].childNodes[0].nodeValue;
                                     }
                             }
                     }
             }
             // Send HTTP GET request to get the latest sensor readings
             request.open("GET", "?update_readings" + nocache, true);
             request.send(null);
        DisplayCurrentTime();
        setTimeout('GetReadings()', 10000);
      }
      document.addEventListener('DOMContentLoaded', function() {
        DisplayCurrentTime();
        GetReadings();
      }, false);
    </script>
    <style>
      body {
        text-align: center;
        font-family: "Trebuchet MS", Arial;
      }
      table {
        border-collapse: collapse;
        width:60%;
        margin-left:auto;
        margin-right:auto;
      }
      th {
        padding: 16px;
        background-color: #0043af;
        color: white;
      }
      tr {
        border: 1px solid #ddd;
        padding: 16px;
      }
      tr:hover {
        background-color: #bcbcbc;
      }
      td {
        border: none;
        padding: 16px;
      }
      .sensor {
        color:white;
        font-weight: bold;
        background-color: #bcbcbc;
        padding: 8px;
      }
    </style>
  </head>
  <body>
    <h1>ESP32 Weather Station</h1>
    <h3>Last update: <span id="currentTime"></span></h3>
    <table>
      <tr>
        <th>SENSOR</th>
        <th>MEASUREMENT</th>
        <th>VALUE</th>
      </tr>
      <tr>
        <td><span class="sensor">DHT</span></td>
        <td>Temp. Celsius</td>
        <td><span class="reading">...</span> *C</td>
      </tr>
      <tr>
        <td><span class="sensor">DHT</span></td>
        <td>Temp. Fahrenheit</td>
        <td><span class="reading">...</span> *F</td>
      </tr>
      <tr>
        <td><span class="sensor">DHT</span></td>
        <td>Humidity</td>
        <td><span class="reading">...</span> %</td>
      </tr>
      <tr>
        <td><span class="sensor">BMP180</span></td>
        <td>Temp. Celsius</td>
        <td><span class="reading">...</span> *C</td>
      </tr>
      <tr>
        <td><span class="sensor">BMP180</span></td>
        <td>Temp. Fahrenheit</td>
        <td><span class="reading">...</span> *F</td>
      </tr>
      <tr>
        <td><span class="sensor">BMP180</span></td>
        <td>Pressure</td>
        <td><span class="reading">...</span> Pa</td>
      </tr>
      <tr>
        <td><span class="sensor">POT</span></td>
        <td>Position</td>
        <td><span class="reading">...</span>/4095</td>
      </tr>
      <tr>
        <td><span class="sensor">LDR</span></td>
        <td>Luminosity</td>
        <td><span class="reading">...</span>/4095</td>
      </tr>
    </table>
  </body>
</html>

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

Это HTML-код, и он создаст вашу веб-страницу. В этом файле вы можете изменить внешний вид вашей веб-страницы, заголовки, таблицу и т.д. ESP32 отправит этот HTML-текст в ваш браузер, когда вы сделаете HTTP-запрос по IP-адресу ESP32.

Сохраните файл как index.html. Скопируйте HTML-файл на вашу карту MicroSD и вставьте карту MicroSD в модуль SD-карты.

Вставка SD-карты

Теперь всё должно быть готово.

Тестирование веб-сервера метеостанции ESP32

Откройте монитор порта со скоростью 115200 бод и проверьте IP-адрес ESP32.

IP-адрес в мониторе порта

По завершении проекта у вас будет собственный веб-сервер метеостанции на ESP32, и всё оборудование будет компактно размещено на печатной плате.

Откройте браузер, введите IP-адрес, и вы увидите таблицу с последними показаниями датчиков. Веб-сервер отображает показания DHT22, BMP180, потенциометра и LDR. Показания обновляются каждые 10 секунд без необходимости перезагрузки веб-страницы.

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

Для обновления показаний без перезагрузки веб-страницы мы используем AJAX. Как вы можете прочитать здесь, AJAX – это мечта разработчика, потому что он может обновлять веб-страницу без её перезагрузки, запрашивать и получать данные с сервера после загрузки страницы и отправлять данные на сервер в фоновом режиме.

AJAX для разработчиков

Дальнейшее развитие

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

Если вы хотите получить свою собственную универсальную плату метеостанции ESP32, вам нужно просто загрузить .zip файл с Gerber-файлами на сайт JLCPCB. Вы получите высококачественные печатные платы по очень разумной цене.

Заключение

Мы надеемся, что этот проект был для вас полезен. Если вам понравился этот проект, вам также могут понравиться другие связанные проекты:

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


Источник: Build an All-in-One ESP32 Weather Station Shield