ESP8266-01, AHT20, BMP280 и MQTT мини метеорологична станция

ESP8266-01, AHT20, BMP280 и MQTT мини метеорологична станция

Въведение

В тази статия ще изградим компактна мини метеорологична станция с ESP8266-01, която измерва температура, относителна влажност, атмосферно налягане и надморска височина. За измерването се използват два популярни I2C сензора – AHT20 и BMP280.

Получените данни се публикуват чрез MQTT, което позволява лесното им използване в системи за домашна автоматизация, MQTT Panel, Node-RED, Home Assistant или собствена IoT система.

Проектът използва ESP8266-01, който е малък и евтин WiFi модул, но въпреки ограничените си размери може да изпълнява доста сложни задачи. В конкретния проект са реализирани:

  • измерване на температура и влажност чрез AHT20;
  • измерване на атмосферно налягане чрез BMP280;
  • изчисляване на надморска височина;
  • изчисляване на точка на оросяване;
  • публикуване на данните чрез MQTT;
  • автоматично конфигуриране на WiFi чрез WiFiManager;
  • изтриване на WiFi настройките чрез хардуерен бутон;
  • OTA обновяване на програмата;
  • MQTT статус online/offline чрез Last Will and Testament (LWT);
  • възможност за използване на няколко станции чрез уникален DEVICE_ID.

Това превръща ESP8266-01 в напълно функционална IoT метеорологична станция, която може да бъде поставена в различни помещения или на различни места и да изпраща данните към един общ MQTT брокер.


Схема на свързване на: ESP8266-01, AHT20, BMP280 и бутон за WiFi мениджър


Основен код на проекта: Мини метеорологична станция с ESP8266-01, AHT20, BMP280 и MQTT

Преди да продължим нпред трябва да инсталираме необходимите библиотеки за проекта. Може да ги инсталирате от “Manage Libraries…” от Arduino IDE или да ги свалите от тук, и да ги добавите ръчно. А за да разберете как да прибавите контролера към вашето Arduino IDE прочетете статията ни: Добавяне на ESP8266 в Arduino IDE: Стъпка по стъпка.

По-долу е основният код на проекта:

/*
 * ==========================================================
 *  Метеорологична станция - ESP8266-01 + AHT20 + BMP280
 * ==========================================================
 *
 *  Функции:
 *   - Температура и влажност от AHT20 (адрес 0x38)
 *   - Налягане и надм. височина от BMP280 (адрес 0x77)
 *   - Точка на оросяване, изчислена от температура + влажност
 *   - Публикуване по MQTT на всеки 5 секунди
 *   - WiFiManager (captive portal) за конфигурация на WiFi
 *   - Хардуерен бутон на GPIO3 (RX), INPUT_PULLUP -> GND,
 *     задръжка 5 сек -> изтрива WiFi настройките и рестартира
 *   - OTA ъпдейт (ArduinoOTA)
 *   - Статус online/offline чрез LWT
 *
 *  Нужни библиотеки (Library Manager):
 *   - WiFiManager        by tzapu
 *   - PubSubClient        by Nick O'Leary (knolleary)
 *   - Adafruit AHTX0
 *   - Adafruit BMP280 Library
 *   - Adafruit Unified Sensor
 *   - Adafruit BusIO
 *
 *  Свързване (I2C, споделена шина за двата сензора):
 *   VCC -> 3.3V (модул с 3.3V логика!)
 *   GND -> GND
 *   SDA -> GPIO0
 *   SCL -> GPIO2
 *
 *  Бутон за WiFi reset:
 *   GPIO3 (RX) -> бутон -> GND
 * ==========================================================
 */

#include <ESP8266WiFi.h>
#include <WiFiManager.h>          // tzapu/WiFiManager
#include <PubSubClient.h>
#include <ArduinoOTA.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_AHTX0.h>
#include <Adafruit_BMP280.h>

// ---------------------------------------------------------
//  ВАЖНО: смени номера на всяко устройство преди да го качиш!
//  Устройство 1 -> "1", устройство 2 -> "2" и т.н.
// ---------------------------------------------------------
#define DEVICE_ID "1"

// ---------------------------------------------------------
//  Настройки на MQTT сървъра
// ---------------------------------------------------------
const char* mqtt_server = "xxx.xxx.xxx.xxx";
const int   mqtt_port   = 1883;
const char* mqtt_user   = "xxxxxxxxx";
const char* mqtt_pass   = "xxxxxxxxx";

// ---------------------------------------------------------
//  Пинова конфигурация
// ---------------------------------------------------------
#define I2C_SDA_PIN     0     // GPIO0
#define I2C_SCL_PIN     2     // GPIO2
#define WIFI_RESET_PIN  3     // GPIO3 (RX), INPUT_PULLUP, бутон към GND
#define RESET_HOLD_MS   5000  // 5 секунди задържане за reset

// ---------------------------------------------------------
//  I2C адреси на сензорите (открити чрез I2C scanner)
// ---------------------------------------------------------
#define AHT20_ADDR   0x38
#define BMP280_ADDR  0x77

// ---------------------------------------------------------
//  Интервал на публикуване - статичен, 5 секунди
// ---------------------------------------------------------
const unsigned long publishInterval = 5000;
unsigned long lastPublish = 0;

// ---------------------------------------------------------
//  Глобални обекти
// ---------------------------------------------------------
WiFiClient   espClient;
PubSubClient mqttClient(espClient);
Adafruit_AHTX0 aht;
Adafruit_BMP280 bmp;
bool ahtAvailable = false;
bool bmpAvailable = false;

// Топици - weatherStation/<DEVICE_ID>/...
String topicTemp;
String topicHum;
String topicPressure;
String topicAltitude;
String topicDewpoint;
String topicStatus;

// Стойност на морското ниво за изчисление на надм. височина (hPa)
#define SEALEVELPRESSURE_HPA (1013.25)

// ---------------------------------------------------------
//  Изчисление на точка на оросяване (Magnus formula, апроксимация)
// ---------------------------------------------------------
float calculateDewPoint(float temperature, float humidity) {
  float a = 17.27;
  float b = 237.7;
  float alpha = ((a * temperature) / (b + temperature)) + log(humidity / 100.0);
  return (b * alpha) / (a - alpha);
}

// ---------------------------------------------------------
//  WiFi reset бутон - неблокираща проверка в loop()
// ---------------------------------------------------------
void checkWifiResetButton() {
  static unsigned long pressStart = 0;
  static bool pressing = false;

  bool pressed = (digitalRead(WIFI_RESET_PIN) == LOW);

  if (pressed && !pressing) {
    pressing = true;
    pressStart = millis();
  } else if (pressed && pressing) {
    if (millis() - pressStart >= RESET_HOLD_MS) {
      Serial.println(F("[WiFiReset] Бутонът е задържан 5 сек -> изтривам настройки и рестартирам..."));
      WiFiManager wm;
      wm.resetSettings();
      delay(500);
      ESP.restart();
    }
  } else if (!pressed && pressing) {
    pressing = false; // пуснат преди 5 сек - нищо не се случва
  }
}

// ---------------------------------------------------------
//  Свързване към MQTT (с LWT за статус)
// ---------------------------------------------------------
void reconnectMqtt() {
  static unsigned long lastAttempt = 0;
  if (mqttClient.connected()) return;
  if (millis() - lastAttempt < 5000) return; // 5 сек между опитите
  lastAttempt = millis();

  Serial.print(F("[MQTT] Свързване към брокера..."));
  String clientId = "ESP8266_weatherStation_" + String(DEVICE_ID);

  bool connected = mqttClient.connect(
    clientId.c_str(),
    mqtt_user, mqtt_pass,
    topicStatus.c_str(),   // will topic
    1,                       // will QoS
    true,                    // will retain
    "offline"                // will message
  );

  if (connected) {
    Serial.println(F(" свързан!"));
    mqttClient.publish(topicStatus.c_str(), "online", true);
  } else {
    Serial.printf(" неуспешно, rc=%d. Ще опитам пак...\n", mqttClient.state());
  }
}

// ---------------------------------------------------------
//  Публикуване на сензорните данни
// ---------------------------------------------------------
void publishSensorData() {
  float temperature = NAN;
  float humidity = NAN;
  float pressure = NAN;
  float altitude = NAN;

  if (ahtAvailable) {
    sensors_event_t humEvent, tempEvent;
    aht.getEvent(&humEvent, &tempEvent);
    temperature = tempEvent.temperature;
    humidity = humEvent.relative_humidity;
  } else {
    Serial.println(F("[AHT20] Сензорът не е наличен, пропускам четенето."));
  }

  if (bmpAvailable) {
    pressure = bmp.readPressure() / 100.0F; // Pa -> hPa
    altitude = bmp.readAltitude(SEALEVELPRESSURE_HPA);
  } else {
    Serial.println(F("[BMP280] Сензорът не е наличен, пропускам четенето."));
  }

  char buf[16];

  if (!isnan(temperature)) {
    dtostrf(temperature, 4, 2, buf);
    mqttClient.publish(topicTemp.c_str(), buf, true);
  }

  if (!isnan(humidity)) {
    dtostrf(humidity, 4, 2, buf);
    mqttClient.publish(topicHum.c_str(), buf, true);
  }

  if (!isnan(pressure)) {
    dtostrf(pressure, 4, 2, buf);
    mqttClient.publish(topicPressure.c_str(), buf, true);
  }

  if (!isnan(altitude)) {
    dtostrf(altitude, 4, 2, buf);
    mqttClient.publish(topicAltitude.c_str(), buf, true);
  }

  if (!isnan(temperature) && !isnan(humidity)) {
    float dewpoint = calculateDewPoint(temperature, humidity);
    dtostrf(dewpoint, 4, 2, buf);
    mqttClient.publish(topicDewpoint.c_str(), buf, true);
  }

  Serial.printf("[Data] T=%.2fC H=%.2f%% P=%.2fhPa Alt=%.2fm\n",
                temperature, humidity, pressure, altitude);
}

// ---------------------------------------------------------
//  OTA настройка
// ---------------------------------------------------------
void setupOTA() {
  ArduinoOTA.setHostname(("weatherStation-" + String(DEVICE_ID)).c_str());
  // ArduinoOTA.setPassword("xxxxxxxxxxx"); // препоръчително - разкоментирай и сложи парола

  ArduinoOTA.onStart([]() {
    Serial.println(F("[OTA] Начало на ъпдейт..."));
  });
  ArduinoOTA.onEnd([]() {
    Serial.println(F("\n[OTA] Край."));
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("[OTA] Прогрес: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("[OTA] Грешка [%u]\n", error);
  });
  ArduinoOTA.begin();
}

// ---------------------------------------------------------
//  setup()
// ---------------------------------------------------------
void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println(F("\n\n=== Метеорологична станция ESP8266-01 (AHT20+BMP280) ==="));

  // Бутон за WiFi reset - GPIO3 (RX)
  pinMode(WIFI_RESET_PIN, INPUT_PULLUP);

  // Топици
  String base = "weatherStation/" + String(DEVICE_ID) + "/";
  topicTemp     = base + "temp";
  topicHum      = base + "hum";
  topicPressure = base + "pressure";
  topicAltitude = base + "altitude";
  topicDewpoint = base + "dewpoint";
  topicStatus   = base + "status";

  Serial.println("[Info] Device ID: " + String(DEVICE_ID));

  // I2C инициализация на кастомни пинове
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);

  // AHT20 (температура + влажност)
  if (aht.begin(&Wire, 0, AHT20_ADDR)) {
    ahtAvailable = true;
    Serial.println(F("[AHT20] Сензорът е открит успешно."));
  } else {
    Serial.println(F("[AHT20] ГРЕШКА: сензорът не е открит! Провери свързването."));
  }

  // BMP280 (налягане + височина)
  if (bmp.begin(BMP280_ADDR)) {
    bmpAvailable = true;
    bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
                     Adafruit_BMP280::SAMPLING_X2,
                     Adafruit_BMP280::SAMPLING_X16,
                     Adafruit_BMP280::FILTER_X16,
                     Adafruit_BMP280::STANDBY_MS_500);
    Serial.println(F("[BMP280] Сензорът е открит успешно."));
  } else {
    Serial.println(F("[BMP280] ГРЕШКА: сензорът не е открит! Провери свързването."));
  }

  // WiFiManager - captive portal при първо пускане / липса на запазена мрежа
  WiFiManager wm;
  String apName = "WeatherStation-" + String(DEVICE_ID);
  Serial.println(F("[WiFi] Стартирам WiFiManager..."));
  if (!wm.autoConnect(apName.c_str())) {
    Serial.println(F("[WiFi] Неуспешно свързване, рестартирам..."));
    ESP.restart();
  }
  Serial.print(F("[WiFi] Свързан, IP: "));
  Serial.println(WiFi.localIP());

  // MQTT
  mqttClient.setServer(mqtt_server, mqtt_port);

  // OTA
  setupOTA();

  Serial.println(F("[Setup] Готово.\n"));
}

// ---------------------------------------------------------
//  loop()
// ---------------------------------------------------------
void loop() {
  ArduinoOTA.handle();

  checkWifiResetButton();

  if (!mqttClient.connected()) {
    reconnectMqtt();
  } else {
    mqttClient.loop();
  }

  if (mqttClient.connected() && (millis() - lastPublish >= publishInterval)) {
    lastPublish = millis();
    publishSensorData();
  }
}

Обяснение на кода

Използвани библиотеки

Проектът използва няколко библиотеки, всяка от които изпълнява конкретна функция:

#include <ESP8266WiFi.h>

Това е основната WiFi библиотека за ESP8266.

#include <WiFiManager.h>

WiFiManager позволява на устройството да се свърже с WiFi мрежа чрез captive portal. Това означава, че не е необходимо WiFi името и паролата да бъдат записани директно в кода.

#include <PubSubClient.h>

Тази библиотека осигурява MQTT комуникацията между ESP8266-01 и MQTT брокера.

#include <ArduinoOTA.h>

Позволява обновяване на firmware-а през WiFi, без да е необходимо ESP8266-01 да бъде свързан физически към USB-to-Serial адаптер.

#include <Wire.h>

Използва се за I2C комуникацията с AHT20 и BMP280.

#include <Adafruit_AHTX0.h>
#include <Adafruit_BMP280.h>

Това са библиотеките за двата сензора.


Уникален идентификатор на метеорологичната станция

#define DEVICE_ID "1"

Това е една от важните настройки в проекта.

Ако използваме няколко метеорологични станции, всяка трябва да има различен идентификатор:

#define DEVICE_ID "1"
#define DEVICE_ID "2"
#define DEVICE_ID "3"

Така всяко устройство ще използва собствен MQTT namespace:

weatherStation/1/
weatherStation/2/
weatherStation/3/

Това позволява няколко ESP8266 модула да работят едновременно с един MQTT брокер, без да си пречат.


Свързване на AHT20 и BMP280

Двата сензора използват обща I2C шина.

В проекта са използвани:

#define I2C_SDA_PIN 0
#define I2C_SCL_PIN 2

Следователно свързването е:

ESP8266-01AHT20BMP280
GPIO0SDASDA
GPIO2SCLSCL
3.3VVCCVCC
GNDGNDGND

И двата сензора работят чрез I2C, което позволява да бъдат свързани паралелно към едни и същи SDA и SCL линии.

Разликата е, че използват различни I2C адреси:

#define AHT20_ADDR  0x38
#define BMP280_ADDR 0x77

Това позволява на ESP8266 да комуникира с двата сензора по една и съща шина.


Измерване на температура и влажност с AHT20

AHT20 предоставя:

  • температура;
  • относителна влажност.

Четенето се извършва в:

aht.getEvent(
  &humEvent,
  &tempEvent
);

След това стойностите се записват:

temperature =
  tempEvent.temperature;

humidity =
  humEvent.relative_humidity;

Температурата е в градуси Celsius, а влажността е в проценти.


Измерване на налягане и височина с BMP280

BMP280 измерва атмосферното налягане:

pressure =
  bmp.readPressure() / 100.0F;

Функцията readPressure() връща стойността в Pascal.

Тъй като стандартно атмосферното налягане се представя в hPa, стойността се разделя на 100:

1 hPa = 100 Pa

Така например:

101325 Pa = 1013.25 hPa

Изчисляване на надморската височина

Височината се изчислява чрез:

altitude =
  bmp.readAltitude(
    SEALEVELPRESSURE_HPA
  );

В проекта е зададено:

#define SEALEVELPRESSURE_HPA 1013.25

Това е стандартното атмосферно налягане на морското равнище.

Важно е да се знае, че изчислената надморска височина зависи от правилната референтна стойност на атмосферното налягане. За по-точни резултати стойността на морското ниво трябва да бъде коригирана според текущото локално налягане.


Изчисляване на точка на оросяване

Една от интересните функции на метеорологичната станция е изчисляването на точката на оросяване.

Това е температурата, при която въздухът достига 100% относителна влажност и започва кондензация на водната пара.

Изчислението се извършва чрез функцията:

float calculateDewPoint(
  float temperature,
  float humidity
)

В нея е използвана апроксимация на формулата на Magnus.

mqttClient.publish(
  topicDewpoint.c_str(),
  buf,
  true
);

Точката на оросяване може да бъде полезна при:

  • следене на риска от кондензация;
  • контрол на отопление и вентилация;
  • наблюдение на влажността в помещения;
  • метеорологични приложения;
  • автоматизация на оранжерии.

След изчисляването стойността се публикува чрез MQTT:


Работа с WiFiManager

Едно от големите предимства на проекта е, че WiFi настройките не са записани директно в програмата.

При първо стартиране:

WiFiManager wm;

се създава временна WiFi мрежа:

WeatherStation-1

Потребителят се свързва към тази мрежа и отваря captive portal, от който избира домашната WiFi мрежа и въвежда паролата.

След това WiFiManager запазва настройките във flash паметта на ESP8266.

При следващо стартиране устройството автоматично се свързва към запазената WiFi мрежа.


Хардуерен бутон за изтриване на WiFi настройките

В проекта е използван GPIO3, който е RX пинът на ESP8266-01:

#define WIFI_RESET_PIN 3

Бутонът е свързан между: GPIO3 -> Бутон -> GND

Пинът е конфигуриран като:

pinMode(
  WIFI_RESET_PIN,
  INPUT_PULLUP
);

Това означава, че в нормално състояние пинът е HIGH.

При натискане на бутона той става LOW.

За да не се изтрият настройките при случайно натискане, бутонът трябва да бъде задържан 5 секунди:

#define RESET_HOLD_MS 5000

След изтичане на 5 секунди:

wm.resetSettings();

изтрива запазените WiFi настройки.

След това:

ESP.restart();

рестартира ESP8266.

При следващото стартиране устройството отново ще създаде WiFiManager captive portal.


MQTT връзка

MQTT връзката се управлява от функцията:

void reconnectMqtt()

Преди свързване се проверява дали клиентът вече е свързан:

if (
  mqttClient.connected()
)
  return;

Между отделните опити за свързване има интервал от 5 секунди:

if (
  millis() -
  lastAttempt < 5000
)
  return;

Това е важно, защото предотвратява постоянното блокиращо свързване към MQTT брокера.


MQTT Last Will and Testament

При свързването се задава MQTT статус:

topicStatus.c_str()

Ако устройството загуби връзка неочаквано, брокерът автоматично публикува:

offline

При успешно свързване ESP8266 публикува:

online

Така други устройства или системи могат да разберат дали метеорологичната станция е активна.

Тази функция е особено полезна при:

  • Home Assistant;
  • Node-RED;
  • MQTT Panel;
  • собствени системи за мониторинг.

Публикуване на данните

Данните се публикуват на всеки 5 секунди:

const unsigned long publishInterval =
  5000;

В loop() се проверява:

if (
  mqttClient.connected() &&
  (
    millis() -
    lastPublish >=
    publishInterval
  )
)

Това е неблокиращ подход, защото се използва millis() вместо delay().

Така ESP8266 може едновременно да:

  • обслужва MQTT;
  • обработва OTA заявки;
  • проверява бутона;
  • измерва сензорите;
  • публикува данните.

MQTT топици и тяхното предназначение

Базовият MQTT топик за всяка метеорологична станция е:

weatherStation/<DEVICE_ID>/

Например, ако DEVICE_ID е 1, всички топици ще започват с:

weatherStation/1/

MQTT топици

MQTT топикОписание
weatherStation/1/tempПубликува измерената температура от сензора AHT20 в градуси Целзий (°C).
weatherStation/1/humПубликува относителната влажност на въздуха, измерена от AHT20, в проценти (%).
weatherStation/1/pressureПубликува атмосферното налягане, измерено от BMP280, в хектопаскали (hPa).
weatherStation/1/altitudeПубликува изчислената надморска височина в метри (m), базирана на измереното атмосферно налягане.
weatherStation/1/dewpointПубликува изчислената точка на оросяване, получена на базата на температурата и влажността.
weatherStation/1/statusПоказва текущото състояние на устройството. Стойността е online при успешно свързване към MQTT брокера и offline, ако връзката бъде прекъсната неочаквано (LWT).
MQTT-Panel-ESP8266-01-AHT20-BMP280-мини-метеорологична-станция

Пример за публикувани стойности

MQTT топикПримерна стойност
weatherStation/1/temp24.56
weatherStation/1/hum48.32
weatherStation/1/pressure1012.45
weatherStation/1/altitude125.34
weatherStation/1/dewpoint13.20
weatherStation/1/statusonline / offline

Благодарение на използването на DEVICE_ID, към един и същ MQTT брокер могат да бъдат свързани множество метеорологични станции, като всяка публикува данните си в собствен набор от топици, например:

weatherStation/1/temp
weatherStation/2/temp
weatherStation/3/temp

Това позволява лесно разграничаване на данните от различните устройства и тяхната последваща обработка в Home Assistant, Node-RED, MQTT Explorer, MQTT Panel или други IoT платформи.


OTA обновяване на ESP8266-01

Проектът включва и OTA обновяване:

ArduinoOTA.begin();

След като ESP8266-01 се свърже към WiFi, програмата може да бъде обновявана без физическо свързване чрез USB-to-Serial адаптер.

Името на устройството се създава динамично:

weatherStation-1

Ако имаме второ устройство:

weatherStation-2

Това улеснява разпознаването на различните устройства в Arduino IDE.

При необходимост е препоръчително да се активира OTA парола:

ArduinoOTA.setPassword(
  "YOUR_OTA_PASSWORD"
);

Това предотвратява неоторизирано обновяване на firmware-а в локалната мрежа.


Обобщение

В този проект създадохме компактна IoT метеорологична станция с ESP8266-01, която използва два I2C сензора:

  • AHT20 за температура и влажност;
  • BMP280 за атмосферно налягане и изчисляване на надморска височина.

Станцията изпраща данните към MQTT брокер на всеки 5 секунди.

Чрез уникалния DEVICE_ID могат да бъдат изградени няколко станции, например:

weatherStation/1/
weatherStation/2/
weatherStation/3/

Допълнителните функции правят проекта много по-практичен:

  • WiFiManager премахва необходимостта от записване на WiFi парола в кода;
  • хардуерният бутон позволява лесно изтриване на WiFi настройките;
  • OTA позволява безжично обновяване;
  • MQTT LWT показва дали устройството е онлайн;
  • точката на оросяване предоставя допълнителна метеорологична информация.

Подробен пример за публикуване на данни в MQTT брокер е наличен в това ръководство за ESP8266 и MQTT от Random Nerd Tutorials.


Заключение

ESP8266-01 е малък WiFi модул, но с помощта на AHT20, BMP280 и MQTT може да се превърне в напълно функционална мини метеорологична станция.

Използването на MQTT прави проекта много гъвкав. Данните могат да бъдат визуализирани в MQTT Panel, обработвани чрез Node-RED или интегрирани в по-голяма система за домашна автоматизация.

Комбинацията от ESP8266-01 + AHT20 + BMP280 е особено подходяща за проекти, при които са важни малките размери, ниската цена и възможността за безжично предаване на данните.

Добавянето на WiFiManager, OTA обновяване и MQTT статус превръща проекта от обикновен сензорен модул в завършена IoT метеорологична станция, която може да работи автономно и да бъде разширявана с допълнителни сензори и функции.

Translate »