ESP32 Метеостанция на печатной плате (Температура, Влажность, Давление, Дата и Время)

В этом проекте вы узнаете, как собрать PCB Shield (печатную плату-шилд) для ESP32, работающий как интерфейс метеостанции. На плате установлен датчик температуры, влажности и давления BME280, фоторезистор (LDR), кнопка, OLED-дисплей и множество адресных RGB-светодиодов WS2812B. OLED-дисплей показывает показания датчиков, а светодиоды создают различные световые эффекты, соответствующие отображаемым данным. Также отображаются дата и время.

ESP32 Weather Station Interface PCB Shield Temperature Humidity Pressure Date Time

Ресурсы

Все необходимые файлы для сборки проекта доступны по ссылкам ниже (или вы можете посетить страницу проекта на GitHub):

Обзор проекта

ESP32 Weather Station Interface Project Overview

Аппаратные особенности PCB метеостанции

Шилд спроектирован с штыревыми разъёмами для установки платы ESP32 сверху. Поэтому, если вы хотите собрать и использовать нашу PCB, вам необходимо приобрести такую же плату ESP32. Мы используем ESP32 DEVKIT DOIT V1 (модель с 36 GPIO).

Stacked board PCB Weather Station Interface Shield

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

Шилд включает в себя:

  • Датчик температуры, влажности и давления BME280;

  • LDR (фоторезистор — датчик освещённости);

  • OLED-дисплей 0.96 дюйма I2C;

  • Кнопка;

  • 12 адресных RGB-светодиодов WS2812B;

Если вы собираете проект на макетной плате, вместо отдельных WS2812B можно использовать адресную RGB-ленту или адресное RGB-кольцо с тем же количеством светодиодов (12).

Назначение пинов PCB метеостанции

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

Компонент

Назначение пинов ESP32

BME280

GPIO 21 (SDA), GPIO 22 (SCL)

OLED-дисплей

GPIO 21 (SDA), GPIO 22 (SCL)

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

GPIO 33

Кнопка

GPIO 18

Адресные RGB-светодиоды

GPIO 27

Программные возможности PCB метеостанции

Существует бесчисленное множество способов запрограммировать одну и ту же схему для получения разных результатов с различными функциями. В данном проекте мы запрограммируем PCB следующим образом:

Weather Station PCB ESP32 OLED Screens
  • OLED-дисплей отображает пять различных экранов:

    1. Текущая дата и время;

    2. Температура;

    3. Влажность;

    4. Давление;

    5. Освещённость.

  • Каждый экран отображается 15 секунд перед переключением на следующий.

  • Также можно нажать кнопку для переключения экранов.

  • На каждом экране адресные RGB-светодиоды WS2812B показывают разный эффект:

    • На экране даты и времени RGB-светодиоды отображают эффект радуги;

    • На остальных экранах RGB-светодиоды работают как индикатор-шкала. Например, 100% влажности зажигает все светодиоды, 50% влажности — половину.

    • Цвет светодиодов отличается для каждого экрана: зелёный для температуры, синий для влажности, фиолетовый для давления и жёлтый для освещённости.

Weather Station PCB ESP32 Design

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

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

ESP32 Breadboard Weather Station Interface Shield Components Parts Required

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

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

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

Собрав все компоненты, соберите схему по следующей принципиальной схеме:

ESP32 Weather Station Interface Schematic Diagram Fritzing

Проектирование PCB

Для проектирования схемы и PCB мы использовали EasyEDA — браузерное программное обеспечение для проектирования печатных плат. Если вы хотите настроить свою PCB, просто загрузите следующие файлы:

Проектирование схемы работает как в любом другом инструменте для схемотехники — вы размещаете компоненты и соединяете их проводниками. Затем вы назначаете каждому компоненту посадочное место (footprint).

ESP32 Weather Station Interface PCB Schematic Diagram Wiring Circuit

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

ESP32 PCB Weather Station Interface PCB Layout

Сохраните проект и экспортируйте Gerber-файлы.

Примечание: вы можете скачать файлы проекта и отредактировать их для настройки шилда под свои нужды.

Заказ PCB на PCBWay

Этот проект спонсируется PCBWay. PCBWay — это полнофункциональный сервис по производству печатных плат.

Ordering the PCBs at PCBWay

Превратите свои макетные схемы в профессиональные PCB — получите 10 плат примерно за $5 + доставка (которая зависит от вашей страны).

Когда у вас есть Gerber-файлы, вы можете заказать PCB. Следуйте следующим шагам.

  1. Скачайте Gerber-файлы — `нажмите здесь для скачивания .zip файла <https://github.com/RuiSantosdotme/ESP32-Weather-Station-PCB/raw/main/Gerber_PCB_ESP32%20Weather%20Station%20Interface_2020-12-06_17-30-24.zip>`_

  2. Перейдите на сайт PCBWay и откройте страницу PCB Instant Quote.

PCBWay Order PCB open instant quote page
  1. PCBWay может извлечь все параметры PCB и автоматически заполнить их. Используйте «Quick-order PCB (Autofill parameters)».

PCBWay Order PCB autofill parameters
  1. Нажмите кнопку «+ Add Gerber file», чтобы загрузить предоставленные Gerber-файлы.

PCBWay Order PCB add gerber file button

Вот и всё. Вы также можете использовать OnlineGerberViewer, чтобы проверить, что ваша PCB выглядит правильно.

ESP32 PCB Weather Station Interface Gerber Viewer Online

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

PCBWay Order PCB China post shipping method

Вы можете увеличить количество заказываемых PCB и изменить цвет паяльной маски. Мы заказали синий цвет.

ESP32 PCB Weather Station Interface Final PCB PCBWay Order PCB

Когда всё готово, вы можете заказать PCB, нажав «Save to Cart» и завершив заказ.

Распаковка

Примерно через неделю при использовании доставки DHL, мы получили печатные платы в офис.

ESP32 PCB Weather Station Interface Shield

Как обычно, всё хорошо упаковано, а качество PCB действительно высокое. Надписи на шелкографии отлично напечатаны и легко читаются. Кроме того, припой легко ложится на контактные площадки.

Мы очень довольны сервисом PCBWay. Вот некоторые другие проекты, которые мы собрали с помощью сервиса PCBWay:

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

Следующий шаг — пайка компонентов на PCB. Мы использовали SMD-светодиоды, SMD-резисторы и SMD-конденсаторы. Их может быть немного сложно паять, но PCB выглядит намного лучше.

Если вы никогда не паяли SMD, рекомендуем посмотреть несколько видео, чтобы научиться. Также можно приобрести SMD DIY набор для пайки для практики.

Вот список всех компонентов, необходимых для сборки PCB:

ESP32 PCB Weather Station Interface Shield Components Parts Required

Вот инструменты для пайки, которые мы использовали:

TS80 Soldering Iron Review Best Portable Soldering Iron

Читайте наш обзор паяльника TS80: TS80 Soldering Iron Review – Best Portable Soldering Iron.

Начните с пайки SMD-компонентов. Затем припаяйте штыревые разъёмы. И наконец, припаяйте остальные компоненты или используйте разъёмы, если не хотите подключать компоненты на постоянной основе.

Вот как выглядит ESP32 Shield после сборки всех компонентов.

ESP32 PCB Weather Station Interface Final PCB

Плата ESP32 должна идеально встать на штыревые разъёмы с другой стороны PCB.

Stacked board PCB Weather Station Interface Shield

Программирование PCB интерфейса метеостанции

Код для этого проекта отображает показания датчиков на разных экранах OLED-дисплея, а также дату и время. Адресные RGB-светодиоды показывают различные цвета и анимации в соответствии с тем, что отображается на экране.

Вы можете запрограммировать PCB любым удобным для вас способом.

Мы будем программировать ESP32 с помощью Arduino IDE. Убедитесь, что у вас установлено дополнение для платы ESP32.

Если вы хотите программировать ESP32/ESP8266 с помощью VS Code + PlatformIO, следуйте руководству:

Установка библиотек (Arduino IDE)

Для этого проекта необходимо установить все эти библиотеки в Arduino IDE.

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

Установка библиотек (VS Code + PlatformIO)

Если вы программируете ESP32 с помощью PlatformIO, включите библиотеки в файл platformio.ini следующим образом:

[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps = adafruit/Adafruit NeoPixel @ ^1.7.0
           adafruit/Adafruit SSD1306 @ ^2.4.1
           adafruit/Adafruit Unified Sensor @ ^1.1.4
           adafruit/Adafruit BME280 Library @ ^2.1.2
           adafruit/Adafruit GFX Library @ ^1.10.3
           adafruit/Adafruit BusIO @ ^1.6.0

Код

Скопируйте следующий код в Arduino IDE или в файл main.cpp, если используете PlatformIO.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-weather-station-pcb/

  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 <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <WiFi.h>
#include <time.h>

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

// NTP Server Details
const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 0;
const int   daylightOffset_sec = 3600;

// OLED Display
#define SCREEN_WIDTH 128  // OLED display width, in pixels
#define SCREEN_HEIGHT 64  // OLED display height, in pixels

#define I2Cdisplay_SDA 21
#define I2Cdisplay_SCL 22
TwoWire I2Cdisplay = TwoWire(1);

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2Cdisplay, -1);

// WS2812B Addressable RGB LEDs
#define LED_PIN    27  // GPIO the LEDs are connected to
#define LED_COUNT  12  // Number of LEDs
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

// BME280
#define I2C_SDA 21
#define I2C_SCL 22
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;

// LDR (Light Dependent Resistor)
#define ldr  33

// Pushbutton
#define buttonPin  18

int buttonState;              // current reading from the input pin
int lastButtonState = LOW;    // previous reading from the input pin

unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers

// Screens
int displayScreenNum = 0;
int displayScreenNumMax = 4;

unsigned long lastTimer = 0;
unsigned long timerDelay = 15000;

unsigned char temperature_icon[] ={
  0b00000001, 0b11000000, //        ###
  0b00000011, 0b11100000, //       #####
  0b00000111, 0b00100000, //      ###  #
  0b00000111, 0b11100000, //      ######
  0b00000111, 0b00100000, //      ###  #
  0b00000111, 0b11100000, //      ######
  0b00000111, 0b00100000, //      ###  #
  0b00000111, 0b11100000, //      ######
  0b00000111, 0b00100000, //      ###  #
  0b00001111, 0b11110000, //     ########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11111000, //    ##########
  0b00001111, 0b11110000, //     ########
  0b00000111, 0b11100000, //      ######
};

unsigned char humidity_icon[] ={
  0b00000000, 0b00000000, //
  0b00000001, 0b10000000, //        ##
  0b00000011, 0b11000000, //       ####
  0b00000111, 0b11100000, //      ######
  0b00001111, 0b11110000, //     ########
  0b00001111, 0b11110000, //     ########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11011000, //    ####### ##
  0b00111111, 0b10011100, //   #######  ###
  0b00111111, 0b10011100, //   #######  ###
  0b00111111, 0b00011100, //   ######   ###
  0b00011110, 0b00111000, //    ####   ###
  0b00011111, 0b11111000, //    ##########
  0b00001111, 0b11110000, //     ########
  0b00000011, 0b11000000, //       ####
  0b00000000, 0b00000000, //
};

unsigned char arrow_down_icon[] ={
  0b00001111, 0b11110000, //     ########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11111000, //    ##########
  0b00011100, 0b00111000, //    ###    ###
  0b00011100, 0b00111000, //    ###    ###
  0b00011100, 0b00111000, //    ###    ###
  0b01111100, 0b00111110, //  #####    #####
  0b11111100, 0b00111111, // ######    ######
  0b11111100, 0b00111111, // ######    ######
  0b01111000, 0b00011110, //  ####      ####
  0b00111100, 0b00111100, //   ####    ####
  0b00011110, 0b01111000, //    ####  ####
  0b00001111, 0b11110000, //     ########
  0b00000111, 0b11100000, //      ######
  0b00000011, 0b11000000, //       ####
  0b00000001, 0b10000000, //        ##
};

unsigned char sun_icon[] ={
  0b00000000, 0b00000000, //
  0b00100000, 0b10000010, //   #     #     #
  0b00010000, 0b10000100, //    #    #    #
  0b00001000, 0b00001000, //     #       #
  0b00000001, 0b11000000, //        ###
  0b00000111, 0b11110000, //      #######
  0b00000111, 0b11110000, //      #######
  0b00001111, 0b11111000, //     #########
  0b01101111, 0b11111011, //  ## ######### ##
  0b00001111, 0b11111000, //     #########
  0b00000111, 0b11110000, //      #######
  0b00000111, 0b11110000, //      #######
  0b00010001, 0b11000100, //    #   ###   #
  0b00100000, 0b00000010, //   #           #
  0b01000000, 0b10000001, //  #      #      #
  0b00000000, 0b10000000, //         #
};

// Clear the LEDs
void colorWipe(uint32_t color, int wait, int numNeoPixels) {
  for(int i=0; i<numNeoPixels; i++) { // For each pixel in strip...
    strip.setPixelColor(i, color);         //  Set pixel's color (in RAM)
    strip.show();                          //  Update strip to match
    delay(wait);                           //  Pause for a moment
  }
}

// Rainbow cycle along all LEDs. Pass delay time (in ms) between frames.
void rainbow(int wait) {
  long firstPixelHue = 256;
    for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
      int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
      strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
    }
    strip.show(); // Update strip with new contents
    delay(wait);  // Pause for a moment
}

// Create display marker for each screen
void displayIndicator(int displayNumber) {
  int xCoordinates[5] = {44, 54, 64, 74, 84};
  for (int i =0; i<5; i++) {
    if (i == displayNumber) {
      display.fillCircle(xCoordinates[i], 60, 2, WHITE);
    }
    else {
      display.drawCircle(xCoordinates[i], 60, 2, WHITE);
    }
  }
}

//SCREEN NUMBER 0: DATE AND TIME
void displayLocalTime(){
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");

  //GET DATE
  //Get full weekday name
  char weekDay[10];
  strftime(weekDay, sizeof(weekDay), "%a", &timeinfo);
  //Get day of month
  char dayMonth[4];
  strftime(dayMonth, sizeof(dayMonth), "%d", &timeinfo);
  //Get abbreviated month name
  char monthName[5];
  strftime(monthName, sizeof(monthName), "%b", &timeinfo);
  //Get year
  char year[6];
  strftime(year, sizeof(year), "%Y", &timeinfo);

  //GET TIME
  //Get hour (12 hour format)
  /*char hour[4];
  strftime(hour, sizeof(hour), "%I", &timeinfo);*/

  //Get hour (24 hour format)
  char hour[4];
  strftime(hour, sizeof(hour), "%H", &timeinfo);
  //Get minute
  char minute[4];
  strftime(minute, sizeof(minute), "%M", &timeinfo);

  //Display Date and Time on OLED display
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(3);
  display.setCursor(19,5);
  display.print(hour);
  display.print(":");
  display.print(minute);
  display.setTextSize(1);
  display.setCursor(16,40);
  display.print(weekDay);
  display.print(", ");
  display.print(dayMonth);
  display.print(" ");
  display.print(monthName);
  display.print(" ");
  display.print(year);
  displayIndicator(displayScreenNum);
  display.display();
  rainbow(10);
}

// SCREEN NUMBER 1: TEMPERATURE
void displayTemperature(){
  display.clearDisplay();
  display.setTextSize(2);
  display.drawBitmap(15, 5, temperature_icon, 16, 16 ,1);
  display.setCursor(35, 5);
  float temperature = bme.readTemperature();
  display.print(temperature);
  display.cp437(true);
  display.setTextSize(1);
  display.print(" ");
  display.write(167);
  display.print("C");
  display.setCursor(0, 34);
  display.setTextSize(1);
  display.print("Humidity: ");
  display.print(bme.readHumidity());
  display.print(" %");
  display.setCursor(0, 44);
  display.setTextSize(1);
  display.print("Pressure: ");
  display.print(bme.readPressure()/100.0F);
  display.print(" hpa");
  displayIndicator(displayScreenNum);
  display.display();
  int temperaturePer = map(temperature, -5, 36, 0, LED_COUNT-1);
  colorWipe(strip.Color(0,   255,   0), 50, temperaturePer);
}

// SCREEN NUMBER 2: HUMIDITY
void displayHumidity(){
  display.clearDisplay();
  display.setTextSize(2);
  display.drawBitmap(15, 5, humidity_icon, 16, 16 ,1);
  display.setCursor(35, 5);
  float humidity = bme.readHumidity();
  display.print(humidity);
  display.print(" %");
  display.setCursor(0, 34);
  display.setTextSize(1);
  display.print("Temperature: ");
  display.print(bme.readTemperature());
  display.cp437(true);
  display.print(" ");
  display.write(167);
  display.print("C");
  display.setCursor(0, 44);
  display.setTextSize(1);
  display.print("Pressure: ");
  display.print(bme.readPressure()/100.0F);
  display.print(" hpa");
  displayIndicator(displayScreenNum);
  display.display();
  int humidityPer = map(humidity, 0, 100, 0, LED_COUNT-1);
  colorWipe(strip.Color(0,   0,   255), 50, humidityPer);
}

// SCREEN NUMBER 3: PRESSURE
void displayPressure(){
  display.clearDisplay();
  display.setTextSize(2);
  display.drawBitmap(0, 5, arrow_down_icon, 16, 16 ,1);
  display.setCursor(20, 5);
  display.print(bme.readPressure()/100.0F);
  display.setTextSize(1);
  display.print(" hpa");
  display.setCursor(0, 34);
  display.setTextSize(1);
  display.print("Temperature: ");
  display.print(bme.readTemperature());
  display.cp437(true);
  display.print(" ");
  display.write(167);
  display.print("C");
  display.setCursor(0, 44);
  display.setTextSize(1);
  display.print("Humidity: ");
  display.print(bme.readHumidity());
  display.print(" hpa");
  displayIndicator(displayScreenNum);
  display.display();
  colorWipe(strip.Color(255,   0,   255), 50, 12);
}

// SCREEN NUMBER 4: LUMINOSITY
void displayLDR(){
  display.clearDisplay();
  display.setTextSize(2);
  display.drawBitmap(33, 5, sun_icon, 16, 16 ,1);
  display.setCursor(53, 5);
  int ldrReading = map(analogRead(ldr), 0, 4095, 100, 0);
  display.print(ldrReading);
  display.print(" %");
  display.setTextSize(1);
  display.setCursor(0, 34);
  display.print("Temperature: ");
  display.print(bme.readTemperature());
  display.print(" ");
  display.cp437(true);
  display.write(167);
  display.print("C");
  display.setCursor(0, 44);
  display.setTextSize(1);
  display.print("Humidity: ");
  display.print(bme.readHumidity());
  display.print(" %");
  display.setCursor(0, 44);
  displayIndicator(displayScreenNum);
  display.display();
  int ldrReadingPer = map(ldrReading, 0, 100, 0, LED_COUNT-1);
  colorWipe(strip.Color(255,   255,   0), 50, ldrReadingPer);
}

// Display the right screen accordingly to the displayScreenNum
void updateScreen() {
  colorWipe(strip.Color(0, 0, 0), 1, LED_COUNT);
  if (displayScreenNum == 0){
    displayLocalTime();
  }
  else if (displayScreenNum == 1) {
    displayTemperature();
  }
  else if (displayScreenNum ==2){
    displayHumidity();
  }
  else if (displayScreenNum==3){
    displayPressure();
  }
  else {
    displayLDR();
  }
}

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

  // Initialize the pushbutton pin as an input
  pinMode(buttonPin, INPUT);

  I2CBME.begin(I2C_SDA, I2C_SCL, 100000);
  I2Cdisplay.begin(I2Cdisplay_SDA, I2Cdisplay_SCL, 100000);

  // Initialize OLED Display
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }
  display.clearDisplay();
  display.setTextColor(WHITE);

  // Initialize BME280
  bool status = bme.begin(0x76, &I2CBME);
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }

  // Initialize WS2812B LEDs
  strip.begin();           // INITIALIZE NeoPixel strip object (REQUIRED)
  strip.show();            // Turn OFF all pixels ASAP
  strip.setBrightness(50); // Set BRIGHTNESS to about 1/5 (max = 255)

  // Connect to Wi-Fi
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected.");

  // Init and get the time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}

void loop() {
  // read the state of the switch into a local variable
  int reading = digitalRead(buttonPin);

  // Change screen when the pushbutton is pressed
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != buttonState) {
      buttonState = reading;
      if (buttonState == HIGH) {
        updateScreen();
        Serial.println(displayScreenNum);
        if(displayScreenNum < displayScreenNumMax) {
          displayScreenNum++;
        }
        else {
          displayScreenNum = 0;
        }
        lastTimer = millis();
      }
    }
  }
  lastButtonState = reading;

  // Change screen every 15 seconds (timerDelay variable)
  if ((millis() - lastTimer) > timerDelay) {
    updateScreen();
    Serial.println(displayScreenNum);
    if(displayScreenNum < displayScreenNumMax) {
      displayScreenNum++;
    }
    else {
      displayScreenNum = 0;
    }
    lastTimer = millis();
  }
}

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

Мы получаем дату и время с NTP-сервера. Поэтому ESP32 должен подключиться к интернету. Вставьте данные вашей сети в следующие переменные, и код сразу заработает.

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

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

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

Этот код довольно длинный, но простой. Если вы хотите полностью понять, как он работает, вам может понадобиться ознакомиться со следующими руководствами:

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

Сначала необходимо подключить нужные библиотеки.

#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <WiFi.h>
#include <time.h>

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

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

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

NTP-сервер

Затем необходимо определить следующие переменные для настройки и получения времени с NTP-сервера: ntpServer, gmtOffset_sec и daylightOffset_sec.

Мы будем запрашивать время у pool.ntp.org — кластера серверов времени, которыми может пользоваться любой.

const char* ntpServer = "pool.ntp.org";

Смещение GMT

Переменная gmtOffset_sec определяет смещение в секундах между вашим часовым поясом и GMT. Мы живём в Португалии, поэтому смещение равно 0. Измените переменную gmtOffset_sec в соответствии с вашим часовым поясом.

const long gmtOffset_sec = 0;

Смещение летнего времени

Переменная daylightOffset_sec определяет смещение в секундах для перехода на летнее время. Обычно это один час, что соответствует 3600 секундам.

const int daylightOffset_sec = 3600;

Подробнее о получении времени с NTP-сервера: ESP32 NTP Client-Server: получение даты и времени (Arduino IDE)

OLED-дисплей

Переменные SCREEN_WIDTH и SCREEN_HEIGHT определяют размеры OLED-дисплея в пикселях. Мы используем OLED-дисплей 0.96 дюйма: 128 x 64 пикселя.

#define SCREEN_WIDTH 128  // OLED display width, in pixels
#define SCREEN_HEIGHT 64  // OLED display height, in pixels

OLED-дисплей подключён к GPIO 22 (SCL) и GPIO 21 (SDA).

#define I2Cdisplay_SDA 21
#define I2Cdisplay_SCL 22
TwoWire I2Cdisplay = TwoWire(1);

Инициализируйте объект дисплея с определёнными ранее шириной и высотой с протоколом I2C (&I2Cdisplay).

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2Cdisplay, -1);

Адресные RGB-светодиоды WS2812B

Необходимо определить GPIO, к которому подключены RGB-светодиоды. Адресные RGB-светодиоды взаимодействуют с ESP32 по протоколу One Wire. Поэтому все светодиоды могут управляться одним GPIO. В данном случае они подключены к GPIO 27.

#define LED_PIN 27

Переменная LED_COUNT сохраняет количество адресных RGB-светодиодов, которыми мы хотим управлять. В данном случае их 12.

#define LED_COUNT 12

Создайте объект Adafruit_NeoPixel с именем strip для управления адресными RGB-светодиодами.

Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

Датчик BME280

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

#define I2C_SDA 21
#define I2C_SCL 22
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;

LDR

Определите GPIO, к которому подключён LDR.

const int ldr = 33; // LDR (Light Dependent Resistor)

Кнопка

Определите GPIO, к которому подключена кнопка.

#define buttonPin 18

Следующие переменные используются для обработки нажатия кнопки.

int buttonState;              // current reading from the input pin
int lastButtonState = LOW;    // previous reading from the input pin

unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers

Экраны OLED

Как упоминалось ранее, OLED будет отображать пять различных экранов. Каждый экран пронумерован от 0 до 4. Переменная displayScreenNum хранит номер экрана, который должен отображаться на OLED — начинается с нуля. displayNumMax хранит максимальное количество экранов.

int displayScreenNum = 0;
int displayScreenNumMax = 4;

Следующие переменные используются для управления таймером отображения каждого экрана в течение 15 секунд. Вы можете изменить период отображения экрана в переменной timerDelay.

unsigned long lastTimer = 0;
unsigned long timerDelay = 15000;

Иконки

На каждом экране OLED отображает иконку, связанную с показываемым значением. На экране температуры — термометр (temperature_icon), на экране влажности — капля (humidity_icon), на экране давления — стрелка (arrow_down_icon), а на экране освещённости — солнце (sun_icon). Нам нужно включить эти иконки в код. Эти иконки имеют размер 16x16 пикселей.

unsigned char temperature_icon[] ={
  0b00000001, 0b11000000, //        ###
  0b00000011, 0b11100000, //       #####
  0b00000111, 0b00100000, //      ###  #
  0b00000111, 0b11100000, //      ######
  0b00000111, 0b00100000, //      ###  #
  0b00000111, 0b11100000, //      ######
  0b00000111, 0b00100000, //      ###  #
  0b00000111, 0b11100000, //      ######
  0b00000111, 0b00100000, //      ###  #
  0b00001111, 0b11110000, //     ########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11111000, //    ##########
  0b00001111, 0b11110000, //     ########
  0b00000111, 0b11100000, //      ######
};

unsigned char humidity_icon[] ={
  0b00000000, 0b00000000, //
  0b00000001, 0b10000000, //        ##
  0b00000011, 0b11000000, //       ####
  0b00000111, 0b11100000, //      ######
  0b00001111, 0b11110000, //     ########
  0b00001111, 0b11110000, //     ########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11011000, //    ####### ##
  0b00111111, 0b10011100, //   #######  ###
  0b00111111, 0b10011100, //   #######  ###
  0b00111111, 0b00011100, //   ######   ###
  0b00011110, 0b00111000, //    ####   ###
  0b00011111, 0b11111000, //    ##########
  0b00001111, 0b11110000, //     ########
  0b00000011, 0b11000000, //       ####
  0b00000000, 0b00000000, //
};

 unsigned char arrow_down_icon[] ={
  0b00001111, 0b11110000, //     ########
  0b00011111, 0b11111000, //    ##########
  0b00011111, 0b11111000, //    ##########
  0b00011100, 0b00111000, //    ###    ###
  0b00011100, 0b00111000, //    ###    ###
  0b00011100, 0b00111000, //    ###    ###
  0b01111100, 0b00111110, //  #####    #####
  0b11111100, 0b00111111, // ######    ######
  0b11111100, 0b00111111, // ######    ######
  0b01111000, 0b00011110, //  ####      ####
  0b00111100, 0b00111100, //   ####    ####
  0b00011110, 0b01111000, //    ####  ####
  0b00001111, 0b11110000, //     ########
  0b00000111, 0b11100000, //      ######
  0b00000011, 0b11000000, //       ####
  0b00000001, 0b10000000, //        ##
};

 unsigned char sun_icon[] ={
  0b00000000, 0b00000000, //
  0b00100000, 0b10000010, //   #     #     #
  0b00010000, 0b10000100, //    #    #    #
  0b00001000, 0b00001000, //     #       #
  0b00000001, 0b11000000, //        ###
  0b00000111, 0b11110000, //      #######
  0b00000111, 0b11110000, //      #######
  0b00001111, 0b11111000, //     #########
  0b01101111, 0b11111011, //  ## ######### ##
  0b00001111, 0b11111000, //     #########
  0b00000111, 0b11110000, //      #######
  0b00000111, 0b11110000, //      #######
  0b00010001, 0b11000100, //    #   ###   #
  0b00100000, 0b00000010, //   #           #
  0b01000000, 0b10000001, //  #      #      #
  0b00000000, 0b10000000, //         #
};

Подробнее об отображении иконок читайте в нашем руководстве по OLED: ESP32 OLED-дисплей с Arduino IDE.

Функции colorWipe() и rainbow()

Функции colorWipe() и rainbow() используются для управления адресными RGB-светодиодами.

Функция colorWipe() используется для включения или очистки определённых светодиодов.

void colorWipe(uint32_t color, int wait, int numNeoPixels) {
  for(int i=0; i<numNeoPixels; i++) { // For each pixel in strip...
    strip.setPixelColor(i, color);         //  Set pixel's color (in RAM)
    strip.show();                          //  Update strip to match
    delay(wait);                           //  Pause for a moment
  }
}

Функция rainbow(), как следует из названия, отображает эффект радуги.

// Rainbow cycle along all LEDs. Pass delay time (in ms) between frames.
void rainbow(int wait) {
  long firstPixelHue = 256;
    for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
      int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
      strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
    }
    strip.show(); // Update strip with new contents
    delay(wait);  // Pause for a moment
}

Вы можете узнать больше об этих и других функциях управления лентой, посмотрев примеры библиотеки Neopixel.

Функция displayIndicator()

Функция displayIndicator() создаёт пять маленьких кружков в нижней части дисплея в соответствии с текущим отображаемым экраном.

// Create display marker for each screen
void displayIndicator(int displayNumber) {
  int xCoordinates[5] = {44, 54, 64, 74, 84};
  for (int i =0; i<5; i++) {
    if (i == displayNumber) {
      display.fillCircle(xCoordinates[i], 60, 2, WHITE);
    }
    else {
      display.drawCircle(xCoordinates[i], 60, 2, WHITE);
    }
  }
}

Функция drawCircle(x, y, radius, color) создаёт кружок. Функция fillCircle(x, y, radius, color) создаёт закрашенный кружок.

Мы размещаем центры кружков по следующим координатам x: 44, 54, 64, 74 и 84.

int xCoordinates[5] = {44, 54, 64, 74, 84};

Мы рисуем закрашенный кружок для текущего экрана и «пустой» кружок для остальных:

if (i == displayNumber) {
  display.fillCircle(xCoordinates[i], 60, 2, WHITE);
}
else {
  display.drawCircle(xCoordinates[i], 60, 2, WHITE);
}

Экран 0: Дата и время

Первый экран на OLED отображает дату и время. Это делает функция displayLocalTime(). Она также отображает эффект радуги на адресных RGB-светодиодах.

//SCREEN NUMBER 0: DATE AND TIME
void displayLocalTime(){
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");

  //GET DATE
  //Get full weekday name
  char weekDay[10];
  strftime(weekDay, sizeof(weekDay), "%a", &timeinfo);
  //Get day of month
  char dayMonth[4];
  strftime(dayMonth, sizeof(dayMonth), "%d", &timeinfo);
  //Get abbreviated month name
  char monthName[5];
  strftime(monthName, sizeof(monthName), "%b", &timeinfo);
  //Get year
  char year[6];
  strftime(year, sizeof(year), "%Y", &timeinfo);

  //GET TIME
  //Get hour (12 hour format)
  /*char hour[4];
  strftime(hour, sizeof(hour), "%I", &timeinfo);*/

  //Get hour (24 hour format)
  char hour[4];
  strftime(hour, sizeof(hour), "%H", &timeinfo);
  //Get minute
  char minute[4];
  strftime(minute, sizeof(minute), "%M", &timeinfo);

  //Display Date and Time on OLED display
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(3);
  display.setCursor(19,5);
  display.print(hour);
  display.print(":");
  display.print(minute);
  display.setTextSize(1);
  display.setCursor(16,40);
  display.print(weekDay);
  display.print(", ");
  display.print(dayMonth);
  display.print(" ");
  display.print(monthName);
  display.print(" ");
  display.print(year);
  displayIndicator(displayScreenNum);
  display.display();
  rainbow(10);
}

Подробнее о получении даты и времени с NTP-сервера с ESP32: ESP32 NTP Client-Server: получение даты и времени (Arduino IDE)

Экран 1: Температура

Второй экран отображает температуру. Это делается вызовом функции displayTemperature(). Эта функция также зажигает светодиоды зелёным цветом в соответствии с значением температуры.

// SCREEN NUMBER 1: TEMPERATURE
void displayTemperature(){
  display.clearDisplay();
  display.setTextSize(2);
  display.drawBitmap(15, 5, temperature_icon, 16, 16 ,1);
  display.setCursor(35, 5);
  float temperature = bme.readTemperature();
  display.print(temperature);
  display.cp437(true);
  display.setTextSize(1);
  display.print(" ");
  display.write(167);
  display.print("C");
  display.setCursor(0, 34);
  display.setTextSize(1);
  display.print("Humidity: ");
  display.print(bme.readHumidity());
  display.print(" %");
  display.setCursor(0, 44);
  display.setTextSize(1);
  display.print("Pressure: ");
  display.print(bme.readPressure()/100.0F);
  display.print(" hpa");
  displayIndicator(displayScreenNum);
  display.display();
  int temperaturePer = map(temperature, -5, 36, 0, LED_COUNT-1);
  colorWipe(strip.Color(0,   255,   0), 50, temperaturePer);
}

Экран 2: Влажность

Функция displayHumidity() аналогична функции displayTemperature(), но отображает значение влажности.

// SCREEN NUMBER 2: HUMIDITY
void displayHumidity(){
  display.clearDisplay();
  display.setTextSize(2);
  display.drawBitmap(15, 5, humidity_icon, 16, 16 ,1);
  display.setCursor(35, 5);
  float humidity = bme.readHumidity();
  display.print(humidity);
  display.print(" %");
  display.setCursor(0, 34);
  display.setTextSize(1);
  display.print("Temperature: ");
  display.print(bme.readTemperature());
  display.cp437(true);
  display.print(" ");
  display.write(167);
  display.print("C");
  display.setCursor(0, 44);
  display.setTextSize(1);
  display.print("Pressure: ");
  display.print(bme.readPressure()/100.0F);
  display.print(" hpa");
  displayIndicator(displayScreenNum);
  display.display();
  int humidityPer = map(humidity, 0, 100, 0, LED_COUNT-1);
  colorWipe(strip.Color(0,   0,   255), 50, humidityPer);
}

Экран 3: Давление

Функция displayPressure() аналогична двум предыдущим функциям, но отображает значение давления.

// SCREEN NUMBER 3: PRESSURE
void displayPressure(){
  display.clearDisplay();
  display.setTextSize(2);
  display.drawBitmap(0, 5, arrow_down_icon, 16, 16 ,1);
  display.setCursor(20, 5);
  display.print(bme.readPressure()/100.0F);
  display.setTextSize(1);
  display.print(" hpa");
  display.setCursor(0, 34);
  display.setTextSize(1);
  display.print("Temperature: ");
  display.print(bme.readTemperature());
  display.cp437(true);
  display.print(" ");
  display.write(167);
  display.print("C");
  display.setCursor(0, 44);
  display.setTextSize(1);
  display.print("Humidity: ");
  display.print(bme.readHumidity());
  display.print(" hpa");
  displayIndicator(displayScreenNum);
  display.display();
  colorWipe(strip.Color(255,   0,   255), 50, 12);
}

Экран 4: Освещённость

Наконец, последний экран отображает освещённость (функция displayLDR()).

void displayLDR(){
  display.clearDisplay();
  display.setTextSize(2);
  display.drawBitmap(33, 5, sun_icon, 16, 16 ,1);
  display.setCursor(53, 5);
  int ldrReading = map(analogRead(ldr), 0, 4095, 100, 0);
  display.print(ldrReading);
  display.print(" %");
  display.setTextSize(1);
  display.setCursor(0, 34);
  display.print("Temperature: ");
  display.print(bme.readTemperature());
  display.print(" ");
  display.cp437(true);
  display.write(167);
  display.print("C");
  display.setCursor(0, 44);
  display.setTextSize(1);
  display.print("Humidity: ");
  display.print(bme.readHumidity());
  display.print(" %");
  display.setCursor(0, 44);
  displayIndicator(displayScreenNum);
  display.display();
  int ldrReadingPer = map(ldrReading, 0, 100, 0, LED_COUNT-1);
  colorWipe(strip.Color(255,   255,   0), 50, ldrReadingPer);
}

Функция updateScreen()

Функция updateScreen() вызывает нужные функции в соответствии с экраном, который мы хотим отобразить:

void updateScreen() {
  colorWipe(strip.Color(0, 0, 0), 1, LED_COUNT);
  if (displayScreenNum == 0){
    displayLocalTime();
  }
  else if (displayScreenNum == 1) {
    displayTemperature();
  }
  else if (displayScreenNum ==2){
    displayHumidity();
  }
  else if (displayScreenNum==3){
    displayPressure();
  }
  else {
    displayLDR();
  }
}

setup()

Установите кнопку как вход.

pinMode(buttonPin, INPUT);

Инициализация OLED-дисплея.

if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
  Serial.println(F("SSD1306 allocation failed"));
  for(;;);
}

Инициализация датчика BME280:

if (!bme.begin(0x76)) {
  Serial.println("Could not find a valid BME280 sensor, check wiring!");
  while (1);
}

При первом запуске ESP32 мы хотим очистить OLED-дисплей. Также устанавливаем белый цвет текста.

display.clearDisplay();
display.setTextColor(WHITE);

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

strip.begin();           // INITIALIZE NeoPixel strip object (REQUIRED)
strip.show();            // Turn OFF all pixels ASAP
strip.setBrightness(50); // Set BRIGHTNESS to about 1/5 (max = 255)

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

// Connect to Wi-Fi
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected.");

Настройка NTP-сервера с ранее определёнными параметрами.

configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
printLocalTime();

loop()

В цикле loop() следующие строки переключают экран (displayScreenNum) каждый раз при нажатии кнопки.

// read the state of the switch into a local variable
int reading = digitalRead(buttonPin);

// Change screen when the pushbutton is pressed
if (reading != lastButtonState) {
  lastDebounceTime = millis();
}

if ((millis() - lastDebounceTime) > debounceDelay) {
  if (reading != buttonState) {
    buttonState = reading;
    if (buttonState == HIGH) {
      updateScreen();
      Serial.println(displayScreenNum);
      if(displayScreenNum < displayScreenNumMax) {
        displayScreenNum++;
      }
      else {
        displayScreenNum = 0;
      }
      lastTimer = millis();
    }
  }
}
lastButtonState = reading;

Следующие строки переключают экраны каждые 15 секунд (timerDelay).

if ((millis() - lastTimer) > timerDelay) {
  updateScreen();
  Serial.println(displayScreenNum);
  if(displayScreenNum < displayScreenNumMax) {
    displayScreenNum++;
  }
  else {
    displayScreenNum = 0;
  }
  lastTimer = millis();
}

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

После загрузки кода на плату нажмите встроенную кнопку RESET, чтобы ESP32 начал выполнять код.

ESP32 Weather Station Interface Project Overview

Для полной демонстрации рекомендуем посмотреть видео выше.

Заключение

Мы надеемся, что этот проект показался вам интересным и вы сможете собрать его самостоятельно. Вы можете воспользоваться сервисом PCBWay и получить высококачественную PCB для своих проектов.

Вы можете запрограммировать ESP32 другим кодом, подходящим для ваших нужд. Также можно отредактировать Gerber-файлы и добавить другие функции на PCB или другие датчики.

У нас есть другие похожие проекты, включающие создание и проектирование PCB, которые могут вам понравиться:

Узнайте больше об ESP32 с нашими ресурсами:


Источник: Random Nerd Tutorials — :doc:`ESP32 Weather Station Interface PCB Shield <../esp32-weather-station-pcb/index>`