Погодный чёрный ящик: пишем лог давления и температуры на MicroSD с ESP32-C3

от автора

Введение

Привет Хабр! Большинство современных метеостанций для умного дома бесполезны без интернета или локального MQTT-брокера. Пропал Wi-Fi — данные о микроклимате за последние сутки испарились вместе с перезагрузкой контроллера. Готовые логгеры стоят дорого, а их проприетарное ПО не дает доступа к сырым данным. Мне требовалось максимально дешевое и автономное решение — классический «черный ящик» для квартиры и балкона. Устройство должно пережить внезапное отключение света, не потерять ни одной точки замера и при этом отдавать накопленную историю в виде простого txt файла. Никаких облаков, подписок и зависимостей от сторонних сервисов. В этой статье я расскажу, как собрать такой регистратор на базе ESP32-C3 Super Mini, барометра bme280, датчика температуры и влажности и обычного карт-ридера!

Выбор компонентов

Все компоненты

Все компоненты

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

Компоненты

Почему

Цена

Esp32c3 super mini

Очень компактная и довольно мощная.

Около 250 руб.

Совмещённая плата bme280 + aht20

Компактная, у bme280 нет возможности читать влажность, но это компенсируется aht20.

Около 100 руб.

Карт-ридер

Самый простой способ подключить sd-карту.

Около 50 руб

Макетная плата + конекторы

Идеально для сборки прототипа.

Около 30 руб

Сборка

Я расположил всё компоненты на макетной плате 3×7 см. Вот фото:

Готовое устройство

Готовое устройство

Вот схема сборки:

Модуль

Пин на нём

Пин на esp32

датчик с bme280 и aht20

vcc

3v3

датчик с bme280 и aht20

gnd

gnd

датчик с bme280 и aht20

scl

gpio 8

датчик с bme280 и aht20

sda

gpio 9

карт-ридер

3v3

3v3

карт-ридер

gnd

gnd

карт-ридер

cs

gpio 7

карт-ридер

mosi

gpio 6

карт-ридер

miso

gpio 5

карт-ридер

clk

gpio 4

Распиновка платы
esp32c3 super mini

esp32c3 super mini

Код

Вот код схемы с комментариями:

// Пины SPI для ESP32-C3 Super Mini (GPIO)#define SCK  4   // Serial Clock - тактовый сигнал шины SPI#define MISO 5   // Master In Slave Out - линия данных от карты к контроллеру#define MOSI 6   // Master Out Slave In - линия данных от контроллера к карте#define CS   7   // Chip Select - выбор ведомого устройства (SD-карты)// Период опроса датчиков и записи в мс (1 секунда)#define PRD_WRITE 1000#include <WiFi.h>#include <WebServer.h>const char *ssid = "ESPstation"; // Имя создаваемой точки доступа Wi-FiWebServer server(80); // Создание веб-сервера на стандартном порту HTTP (80)#include "SPI.h"      // Библиотека аппаратного SPI#include "SD.h"       // Библиотека для работы с SD-картами через FS API#include "FS.h"       // Абстракция файловой системы Arduino#include <GyverBME280.h> // Оптимизированная библиотека для датчика BME280 (температура, давление, влажность)GyverBME280 bme;#include <Adafruit_AHTX0.h> // Официальная библиотека Adafruit для AHT10/AHT20 (температура, влажность)Adafruit_AHTX0 aht;float dataBus[] = {0.0, 0.0, 0.0}; /* Массив для хранения усредненных показаний:dataBus[0] — Температура (°C)dataBus[1] — Давление (мм рт. ст.)dataBus[2] — Влажность (%)*//* Вспомогательная функция: перевод давления из Паскалей (возвращает BME280) в миллиметры ртутного столба по формуле: 1 мм рт. ст. ≈ 133.322 Па */float pressureToMmHg(float pa) {  return pa / 133.322f;}void get_data() {  sensors_event_t humidity, temp; // Структуры для получения сырых данных от I2C-датчика  aht.getEvent(&humidity, &temp);  /* Сборка финальных значений в массиве dataBus.     Важно: температура берется как среднее арифметическое между двумя датчиками,     чтобы сгладить погрешности измерений. */  dataBus[0] = (temp.temperature + bme.readTemperature()) / 2.0;  dataBus[1] = pressureToMmHg(bme.readPressure());  dataBus[2] = humidity.relative_humidity;  // Открытие файла на SD-карте в режиме дозаписи (если файла нет — он создастся)  File file = SD.open("/ESPstation_logs.txt", FILE_APPEND);  if (file) {    /* Запись данных БЕЗ использования класса String (экономия оперативной памяти!).       Метод print() автоматически преобразует числа в текстовое представление.       Использование F("...") поместит строковые литералы во флеш-память вместо RAM. */    file.print(F("time after launch:"));    file.print(millis() / 60000); // Время работы в минутах    file.print(F(", temp: "));    file.print(dataBus[0], 1);    // 1 знак после запятой    file.print(F(", MmHg: "));    file.print(dataBus[1], 1);    file.print(F(", humd: "));    file.print(dataBus[2], 1);    file.println();               // Добавляем перенос строки (\n)    file.close(); // Обязательно закрываем файл, иначе данные могут остаться в буфере и не записаться физически  } else {    Serial.println(F("Failed to open file for writing"));  }}void handleRoot() {  // Формирование HTML-страницы "на лету"  String html = "<!DOCTYPE html>";  html += "<html><head>";  // Автоматическая перезагрузка страницы каждые 3 секунды для обновления данных без JS  html += "<meta http-equiv='refresh' content='3'>";  html += "<meta charset='UTF-8'>";    // Инлайн-стили для оформления интерфейса метеостанции  html += "<style>";  html += "body { font-family: Montserrat, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #f0f4f8; }";  html += "h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }";  html += "h2 { color: #34495e; margin-top: 30px; }";  html += ".data-box { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin: 15px 0; }";  html += "</style>";  html += "</head><body>";  html += "<h1>ESPstation: локальная метеостанция</h1>";  // Блок вывода комнатной температуры и влажности  html += "<div class='data-box'>";  html += "<h2>Комната</h2>";  html += "<p>Температура: " + String(dataBus[0], 1) + " °C</p>";  html += "<p>Влажность: " + String(dataBus[2], 1) + " %</p>";  html += "</div>";  // Блок вывода атмосферного давления  html += "<div class='data-box'>";  html += "<h2>Давление</h2>";  html += "<p>Давление: " + String(dataBus[1], 1) + " мм рт. ст.</p>";  html += "</div>";  html += "</body></html>";  server.send(200, "text/html", html); // Отправка сформированного кода браузеру}void setup() {  Serial.begin(115200); // Скорость монитора порта  delay(2000);          // Задержка для корректного отображения сообщений при подаче питания  Serial.println("\n=== Start ESPstation ===");  // Инициализация SPI с явным указанием пинов (актуально для кастомных плат или переназначения GPIO)  SPI.begin(SCK, MISO, MOSI, CS);  // Небольшая задержка для стабилизации питания карты после подачи напряжения  delay(500);  // Инициализация SD карты.   // Указываем пин CS и максимальную частоту SPI (4 МГц для стабильности на макетке/длинных проводах)  if (!SD.begin(CS, SPI, 4000000)) {    Serial.println(F("Card Mount Failed! Check wiring and power."));    return; // Выходим из setup, дальнейшая работа невозможна без накопителя  }  uint8_t cardType = SD.cardType();  if (cardType == CARD_NONE) {    Serial.println(F("No SD card attached"));    return;  }  // Диагностика типа и объема установленной карты  Serial.print(F("SD Card Type: "));  if (cardType == CARD_MMC) Serial.println(F("MMC"));  else if (cardType == CARD_SD) Serial.println(F("SDSC"));  else if (cardType == CARD_SDHC) Serial.println(F("SDHC"));  else Serial.println(F("UNKNOWN"));  uint64_t cardSize = SD.cardSize() / (1024 * 1024);  Serial.printf(F("SD Card Size: %lluMB\n"), cardSize);  // Явная инициализация I2C ПЕРЕД использованием библиотек сенсоров  Wire.begin();  delay(100); // Даем время шине стабилизироваться  // Инициализация BME280 по адресу 0x77 (адрес зависит от подтяжки пина SDO на плате)  if (!bme.begin(0x77)) {    Serial.println(F("BME280 not found!"));  }  // Инициализация AHT (автоматически ищет адрес на шине)  if (!aht.begin()) {    Serial.println(F("AHT not found!"));  }  // Поднятие собственной точки доступа (SoftAP). Станция подключается напрямую к модулю.  WiFi.softAP(ssid);  Serial.print(F("http://"));  Serial.println(WiFi.softAPIP()); // Выводим IP-адрес точки доступа в монитор  // Привязка корневого пути ("/") к функции генерации HTML  server.on("/", handleRoot);  server.begin(); // Запуск веб-сервера  Serial.println(F("\nInitialization and test complete!"));}void loop() {  static uint32_t tmr = 0; // Таймер периодических задач (статик сохраняет значение между итерациями)    // Неблокирующий таймер: проверка раз в секунду (PRD_WRITE)  if (millis() - tmr >= PRD_WRITE) {    tmr = millis(); // Сброс опорной метки времени    get_data();     // Чтение сенсоров и запись на SD  }    // Обработка входящих запросов от браузера (обязательно вызывать в цикле)  server.handleClient();}

Тесты

Итак, подключаем плату к питанию, ждем пару секунд и вот:

Веб-страница

Веб-страница
Данные с карты

Данные с карты

Всё исправно работает!

Итоги

Итак, я сделал оффлайн погодный логгер.
Идеи по улучшению:

  • Сделать приложение для чтения, вывода и прогнозирования данных

  • Добавить радиатор на плату esp

  • Сделать корпус

Пишите ваши идеи по доработке в комментариях, спасибо за внимание!

ссылка на оригинал статьи https://habr.com/ru/articles/1060342/