ESP8266:Примеры/Измерение внутренней и наружной температуры с помощью WeatherStation

Материал из Онлайн справочника
Версия от 20:11, 21 июня 2017; Myagkij (обсуждение | вклад)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Черновик


Измерение внутренней и наружной температуры с помощью WeatherStation[1][2]

Предназначение программы, идущей вместе с WeatherStation – просто отображение информации. Это как бы исходная точка для новичков, которую можно дополнять и изменять под собственные нужды. К примеру, к программе можно пристроить специальный модуль, считывающий из интернета правильное время или информацию о текущей и будущей погоде. Но что если вам нужно измерить и показать информацию из разных мест дома или квартиры? Эта статья рассказывает, как считывать данные с датчика температуры/влажности DHT22 – и когда он подключен к WeatherStation напрямую, и когда он находится в другой комнате, а данные передает через WiFi.

Эта статья написана с прицелом на то, что вы уже работали с WorkingStation и датчиком DHT22.

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

  • Одна погодная станция WeatherStation на базе ESP8266
  • Один датчик температуры/влажности DHT22
  • Одна адаптерная плата NodeMCU V1.0

Прямое подключение DHT22 к погодной станции WeatherStation

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

Погодная станция WeatherStation, к которой напрямую подключен датчик DHT22, измеряющий температуру и влажность

Теперь нужно импортировать библиотеку «DHT», и проще всего сделать это при помощи менеджера библиотек. Чтобы открыть его, кликните в IDE Arduino по Скетч > Подключить библиотеку > Управлять библиотеками... (Sketch > Include Library > Manage Libraries...).

Теперь давайте найдем библиотеку «DHT»:

Теперь, когда библиотека установлена в IDE Arduino, можно приступить к модификации скетча. Но перед тем, как продолжить, у вас должна быть полностью настроена WeatherStation. Отредактировав скетч, сохраните его под новым названием. Вот так скетч выглядит в отредактированном виде:

/** Лицензия MIT (MIT)
Копирайт (c) 2016, Дэниэл Эйкорн (Daniel Eichhorn)

Данная лицензия разрешает лицам, получившим копию данного программного
обеспечения и сопутствующей документации (в дальнейшем именуемыми
«Программное Обеспечение»), безвозмездно использовать Программное
Обеспечение без ограничений, включая неограниченное право на
использование, копирование, изменение, слияние, публикацию,
распространение, сублицензирование и/или продажу копий Программного
Обеспечения, а также лицам, которым предоставляется данное Программное
Обеспечение, при соблюдении следующих условий:

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

ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ 
КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ 
ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ 
НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ. НИ В КАКОМ 
СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО 
КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ, 
ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, ВОЗНИКШИМ ИЗ-ЗА 
ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ 
ОБЕСПЕЧЕНИЕМ.

Более подробно читайте на http://blog.squix.ch
*/

#include <ESP8266WiFi.h>
#include <Ticker.h>
#include <JsonListener.h>
#include "SSD1306Wire.h"
#include "OLEDDisplayUi.h"
#include "Wire.h"
#include "WundergroundClient.h"
#include "WeatherStationFonts.h"
#include "WeatherStationImages.h"
#include "TimeClient.h"
#include "ThingspeakClient.h"
#include "DHT.h"

/***************************
 * Начальные настройки
 **************************/
// о начальных настройках для WeatherStation читайте тут:
// http://blog.squix.org/weatherstation-getting-code-adapting-it

// константы для WiFi:
const char* WIFI_SSID = "X";
const char* WIFI_PWD = "X";

// интервал обновления – 10 минут (ограничение Wunderground):
const int UPDATE_INTERVAL_SECS = 10 * 60;

// константы для дисплея:
const int I2C_DISPLAY_ADDRESS = 0x3c;
const int SDA_PIN = D3;
const int SDC_PIN = D4;

// константа для библиотеки TimeClient:
const float UTC_OFFSET = 1;

// константы для библиотеки WundergroundClient:
const boolean IS_METRIC = true;
const String WUNDERGRROUND_API_KEY = "X";
const String WUNDERGRROUND_LANGUAGE = "DL";
const String WUNDERGROUND_COUNTRY = "CH";
const String WUNDERGROUND_CITY = "Zurich";

// константы для библиотеки ThingspeakClient:
const String THINGSPEAK_CHANNEL_ID = "X";
const String THINGSPEAK_API_READ_KEY = "X";

// инициализируем OLED-дисплей для адреса 0x3c
// (контакт SDA – это 14, а SDC – это 12):

SSD1306Wire     display(I2C_DISPLAY_ADDRESS, SDA_PIN, SDC_PIN);
OLEDDisplayUi   ui( &display );

// настройки для библиотеки «DHT»:
#define DHTPIN D6  //  это цифровой контакт, к которому 
                   //  мы подключены; если используете не NodeMCU,
                   //  а что-то другое, поменяйте D6
                   //  на другой контакт

// уберите знаки комментария у типа датчика, который используете:
//#define DHTTYPE DHT11   // DHT 11
#define DHTTYPE DHT22     // DHT 22  (AM2302), AM2321
//#define DHTTYPE DHT21   // DHT 21 (AM2301)

/***************************
 * Финальные настройки
 **************************/

TimeClient timeClient(UTC_OFFSET);

// если предпочитаете имперскую/дюймовую систему и Фаренгейты:
WundergroundClient wunderground(IS_METRIC);

ThingspeakClient thingspeak;

// инициализируем датчик температуры/влажности:
DHT dht(DHTPIN, DHTTYPE);

// флаг, который меняется в «маятниковой» функции каждые 10 минут:
bool readyForWeatherUpdate = false;

String lastUpdate = "--";

float humidity = 0.;
float temperature = 0.;

Ticker ticker;

// объявляем прототипы:
void drawProgress(OLEDDisplay *display, int percentage, String label);
void updateData(OLEDDisplay *display);
void drawDateTime(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawCurrentWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawForecast(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawThingspeak(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);
void drawForecastDetails(OLEDDisplay *display, int x, int y, int dayIndex);
void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state);
void setReadyForWeatherUpdate();
void drawIndoor(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y);

// добавляем фреймы;
// этот массив хранит указатели функции на все фреймы;
// фрейм – это блок данных, перемещающийся справа налево:
FrameCallback frames[] = { drawDateTime, drawCurrentWeather, drawForecast, drawThingspeak, drawIndoor };
int numberOfFrames = 5;

OverlayCallback overlays[] = { drawHeaderOverlay };
int numberOfOverlays = 1;

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

  // инициализируем дисплей:
  display.init();
  display.clear();
  display.display();

  //display.flipScreenVertically();
  display.setFont(ArialMT_Plain_10);
  display.setTextAlignment(TEXT_ALIGN_CENTER);
  display.setContrast(255);

  WiFi.begin(WIFI_SSID, WIFI_PWD);

  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    display.clear();
    display.drawString(64, 10, "Connecting to WiFi");
                           //  "Подключение к WiFi"
    display.drawXbm(46, 30, 8, 8, counter % 3 == 0 ? activeSymbole : inactiveSymbole);
    display.drawXbm(60, 30, 8, 8, counter % 3 == 1 ? activeSymbole : inactiveSymbole);
    display.drawXbm(74, 30, 8, 8, counter % 3 == 2 ? activeSymbole : inactiveSymbole);
    display.display();

    counter++;
  }

  ui.setTargetFPS(30);

  ui.setActiveSymbol(activeSymbole);
  ui.setInactiveSymbol(inactiveSymbole);

  // в этом аргументе можно выставить TOP, LEFT, BOTTOM и RIGHT:
  ui.setIndicatorPosition(BOTTOM);

  // здесь задаем, где будет находиться первый фрейм: 
  ui.setIndicatorDirection(LEFT_RIGHT);

  // здесь задаем тип перехода (можно использовать варианты
  // SLIDE_LEFT, SLIDE_RIGHT, SLIDE_TOP, SLIDE_DOWN):
  ui.setFrameAnimation(SLIDE_LEFT);

  ui.setFrames(frames, numberOfFrames);

  ui.setOverlays(overlays, numberOfOverlays);

  // функция ui.init() тоже принимает участие в инициализации дисплея:
  ui.init();

  Serial.println("");

  updateData(&display);

  ticker.attach(UPDATE_INTERVAL_SECS, setReadyForWeatherUpdate);

}

void loop() {

  if (readyForWeatherUpdate && ui.getUiState()->frameState == FIXED) {
    updateData(&display);
  }

  int remainingTimeBudget = ui.update();

  if (remainingTimeBudget > 0) {
    // если у вас еще осталось время, здесь можно 
    // добавить еще кода для других задач:
    delay(remainingTimeBudget);
  }


}

void drawProgress(OLEDDisplay *display, int percentage, String label) {
  display->clear();
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  display->drawString(64, 10, label);
  display->drawProgressBar(2, 28, 124, 10, percentage);
  display->display();
}

void updateData(OLEDDisplay *display) {
  drawProgress(display, 10, "Updating time...");
                        //  "Обновляем время..."
  timeClient.updateTime();
  drawProgress(display, 30, "Updating conditions...");
                        //  "Обновляем условия..."
  wunderground.updateConditions(WUNDERGRROUND_API_KEY, WUNDERGRROUND_LANGUAGE, WUNDERGROUND_COUNTRY, WUNDERGROUND_CITY);
  drawProgress(display, 50, "Updating forecasts...");
                        //  "Обновляем прогнозы..."
  wunderground.updateForecast(WUNDERGRROUND_API_KEY, WUNDERGRROUND_LANGUAGE, WUNDERGROUND_COUNTRY, WUNDERGROUND_CITY);
  drawProgress(display, 80, "Updating thingspeak...");
                        //  "Обновляем ThingSpeak..."
  thingspeak.getLastChannelItem(THINGSPEAK_CHANNEL_ID, THINGSPEAK_API_READ_KEY);
  lastUpdate = timeClient.getFormattedTime();

  drawProgress(display, 90, "Updating local temperature and humidity");
                       //  "Обновляем местную температуру и влажность"
  humidity = dht.readHumidity();
  // считываем температуру в Фаренгейтах (isFahrenheit = true):
  temperature = dht.readTemperature(!IS_METRIC);
  
  readyForWeatherUpdate = false;
  drawProgress(display, 100, "Done...");
                         //  "Готово..."
 
  delay(1000);
}

void drawDateTime(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  String date = wunderground.getDate();
  int textWidth = display->getStringWidth(date);
  display->drawString(64 + x, 5 + y, date);
  display->setFont(ArialMT_Plain_24);
  String time = timeClient.getFormattedTime();
  textWidth = display->getStringWidth(time);
  display->drawString(64 + x, 15 + y, time);
  display->setTextAlignment(TEXT_ALIGN_LEFT);
}

void drawCurrentWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  display->setFont(ArialMT_Plain_10);
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->drawString(60 + x, 5 + y, wunderground.getWeatherText());

  display->setFont(ArialMT_Plain_24);
  String temp = wunderground.getCurrentTemp() + "°C";
  display->drawString(60 + x, 15 + y, temp);
  int tempWidth = display->getStringWidth(temp);

  display->setFont(Meteocons_Plain_42);
  String weatherIcon = wunderground.getTodayIcon();
  int weatherIconWidth = display->getStringWidth(weatherIcon);
  display->drawString(32 + x - weatherIconWidth / 2, 05 + y, weatherIcon);
}

void drawIndoor(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  display->drawString(64 + x, 0 + y, "Indoor");
                                 //  "Внутри"

  display->setFont(ArialMT_Plain_16);
  display->drawString(64 + x, 10 + y, String(temperature, 1) + "°C");
  display->drawString(64 + x, 30 + y, String(humidity, 1) + "%");
}

void drawForecast(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  drawForecastDetails(display, x, y, 0);
  drawForecastDetails(display, x + 44, y, 2);
  drawForecastDetails(display, x + 88, y, 4);
}

void drawThingspeak(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  display->drawString(64 + x, 0 + y, "Outdoor");
                                 //  "Снаружи”

  display->setFont(ArialMT_Plain_16);
  display-> rawstring(64 + x, 10 + y, thingspeak.getFieldValue(0) + “°C);
  display-> rawstring(64 + x, 30 + y, thingspeak.getFieldValue(1) + %);
}

void drawForecastDetails(OLEDDisplay *display, int x, int y, int dayIndex) {
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->setFont(ArialMT_Plain_10);
  String day = wunderground.getForecastTitle(dayIndex).substring(0, 3);
  day.toUpperCase();
  display-> rawstring(x + 20, y, day);

  display->setFont(Meteocons_Plain_21);
  display-> rawstring(x + 20, y + 12, wunderground.getForecastIcon(dayIndex));

  display->setFont(ArialMT_Plain_10);
  display-> rawstring(x + 20, y + 34, wunderground.getForecastLowTemp(dayIndex) + | + wunderground.getForecastHighTemp(dayIndex));
  display->setTextAlignment(TEXT_ALIGN_LEFT);
}

void drawHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) {
  display->setColor(WHITE);
  display->setFont(ArialMT_Plain_10);
  String time = timeClient.getFormattedTime().substring(0, 5);
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display-> rawstring(0, 54, time);
  display->setTextAlignment(TEXT_ALIGN_RIGHT);
  String temp = wunderground.getCurrentTemp() + “°C;
  display-> rawstring(128, 54, temp);
  display->drawHorizontalLine(0, 52, 128);
}

void setReadyForWeatherUpdate() {
  Serial.println(Setting readyForUpdate to true);
             //  “Установление readyForUpdate на «true»”
  readyForWeatherUpdate = true;
}

Теперь подробнее об изменениях:

  • Строчка 37: Я добавил библиотеку «DHT»
  • Строчки 77-83: Настройки для библиотеки «DHT». Если используете DHT11 или DHT21, эти настройки можно поменять. Также можно поменять используемый контакт. Если не использовать контакт D6 на NodeMCU, при компиляции будет ошибка
  • Строчки 104/105: Две новых переменных для влажности и температуры. Если используете новый модуль WeatherStation, эти переменные можно поместить в новые классы, а не в глобальное пространство...
  • Строчки 110-119: Эти прототипы помогают компилятору/линкеру использовать функции, которые будут использоваться только потом
  • Строчки 119/124: Я добавил новую функцию drawIndoor() и увеличил количество фреймов до 6. Благодаря этому фреймворк знает, какой метод вызвать, чтобы показать информацию о внутреннем помещении
  • Строчки 231-233: В данном руководстве данные о температуре и влажности обновляются только один раз в 10 минут. Обновление данных обойдется нам лишь в 250 мс, но для цикла loop() это по-прежнему может быть слишком много. Но это можно делать и в while()
  • Строчки 271-277: Этот фрагмент рисует фрейм с информацией о внутреннем помещении. Как видите, он очень похож на функцию drawThingspeak(). И это не случайно, т.к. они отображают похожие данные.

Вот и все! Теперь, когда код отредактирован, скомпилируйте его и загрузите на ESP8266.

Теперь давайте приступим ко второй части – созданию сенсорного «узла», который будет сначала постить данные в бесплатном облачном сервисе, а лишь затем выгружать их на WeatherStation, которая находится в другой комнате и будет показывать их на дисплее.

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

Для сенсорного «узла» вам потребуются:

  • Одна адаптерная плата NodeMCU ESP8266 (или любой другой модуль с чипом ESP8266)
  • Один датчик DHT22
  • Три провода типа «мама-мама»
  • Один источник питания с USB-разъемом

И я пишу это руководство с учетом того, что вы уже настроили рабочую WeatherStation.

Подключение компонентов

В подключении сенсорного «узла» нет ничего сложного. У платы датчика DHT есть только три контакта: GND («земля»), VCC (3,3-вольтовое напряжение) и DAT (это линия для передачи данных, подключаемая к контакту D6 на NodeMCU):

Датчик DHT, подключенный к WeatherStation напрямую

ThingSpeak

ThingSpeak – это бесплатный облачный сервис, позволяющий без особого труда постить данные, считанные с датчиков, а также визуализировать их и считывать при помощи простых HTTP-методов. Здесь стоит отметить, что для хранения погодных данных можно также использовать Wunderground, и способ этот, вероятно, будет даже проще, чем ThingSpeak. Но я все же считаю, что при использовании в образовательных целей ThingSpeak дает гораздо больше свободы. Ему можно отсылать данные, к примеру, от датчика движения и других подобных датчиков. Кроме того, в ThingSpeak встроена очень полезная функция, называемая «webhook» – она позволяет отправлять push-уведомления на смартфон, планшет и т.д.

Итак, вначале нужно зарегистрировать аккаунт в ThingSpeak (это бесплатно). Сделать это можно по этой ссылке. Зарегистрировавшись, залогиньтесь в новом аккаунте и перейдите к пункту «My Channels».

Затем кликните по кнопке «New Channel». Откроется новое меню. Заполните форму, которая находится в этом меню.

Поле «Name» (т.е. «название») поможет вам распознать свой канал среди множества других, которые вы, возможно, создали ранее. Другие важные элементы – названия в полях «Field1» и «Field2». Эти названия позднее будут отображены в графике, и с их помощью я сообщаю ThingSpeak, что значение, которое я отсылаю вместе с атрибутом «field1», должно быть показано как «Temperature» (т.е. температура).

Теперь перейдите к вкладке «API Keys» и запомните (а лучше – запишите) где-нибудь два ключа, сгенерированных в этом меню:

Первый ключ позволит вам записывать данные на этот ThingSpeak-канал, а второй – считывать данные с этого канала. Это секретные ключи, так что берегите их как зеницу ока. Зная эти ключи, злоумышленники могут заспамить ваш канал или даже «украсть» данные. К слову, сделав скриншот, я сгенерировал новые ключи :D

Итак, запишите ключи, скоро они нам понадобятся. Также запомните ID канала – он изображен в самом верху экрана.

Программирование сенсорного «узла»

Итак, теперь у нас есть все необходимые ингредиенты для постинга данных на ThingSpeak. Осталось лишь нужным образом запрограммировать ESP8266. Перейдите по этой ссылке и загрузите ZIP-файл с программой или воспользуйтесь командой git checkout (знающие поймут).

Теперь адаптируйте настройки под собственные нужды. Особенно это касается настроек WiFi и ключа «Write API», который мы сгенерировали и записали в разделе выше.

Интереса ради можно поиграться с интервалом обновления (в секундах). Имейте в виду, что минимальный интервал обновления в ThingSpeak составляет 15 секунд. Если задать интервал меньше, обновления будут просто игнорироваться. Теперь осталось записать программу на NodeMCU, и сенсорный «узел» должен начать работать. Чтобы посмотреть на результаты, можно вернуться к ThingSpeak и взглянуть на графики:

Показ данных на погодной станции

Теперь самое простое. Словно повар из кулинарной ТВ-передачи, я уже давно все приготовил за вас :) В библиотеке WeatherStation есть скетч WeatherStationDemo, который уже содержит все необходимое для отображения данных нашего сенсорного «узла».

Измените в нем строчки, в которые нужно вписать ключ «Read API» и ID канала, о которых шла речь в разделе «ThingSpeak» выше. Если вы не удалили фрагмент с сенсорным «узлом» из программы для WeatherStation, вам нужно просто загрузить на WeatherStation скетч с обновленным API-ключом и ID канала, и вуаля! Вы только что успешно отправили данные о температуре и влажности из соседней комнаты во внешний мир, а затем на маленький OLED-дисплей! Кстати, знаю пару людей (включая мою жену), которых этот трюк не впечатлит вовсе :D

Этот последний шаг может быть слегка непонятным, т.к. повар приготовил жаркое за несколько часов до начала ТВ-шоу, поэтому вот то же самое, но в замедленной съемке: библиотека WeatherStation теперь оснащена классом ThingSpeak, который берет на себя всю черновую работу. Вы «скармливаете» ему ID канала и API-ключ, а он загружает JSON-объект и выбирает только последний блок данных, т.к. именно он нам, в сущности, и нужен.

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

Почему бы, к примеру, не замутить график, показывающий данные за последние 24 часа?

См.также

Внешние ссылки