- PVSM.RU - https://www.pvsm.ru -

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы

В свое время мне понравился монитор качества воздуха из публикации Сергея Сильнова «Компактный монитор домашнего воздуха (CO2, температура, влажность, давление) с Wi-Fi и мобильным интерфейсом» [1].

В мониторе качества воздуха (далее – монитор) из проекта Сергея информация с датчиков температуры, влажности, давления, содержания СО2 в воздухе обрабатывается контроллером ESP8266 и отображается на монохромном экране несколькими кадрами. Кроме того, в мониторе через форму в браузере сохраняется в памяти ESP8266 ключ идентификации сервиса Blynk и автоматически отправляются данные на Blynk.

Монитор имел одну серьезную проблему: он зависал на стартовом кадре при выключении-включении или даже «промигивании» напряжения питания монитора.

Я повторил проект с несущественными изменениями, а для устранения зависаний монитора добавил в схему альтернативное питание. Простое, как грабли: обмотка реле находилась под напряжением адаптера AC/DC, а его контакты переключали питание с адаптера на батарейки, когда исчезало напряжение в сети 220В.

Мой мнимый успех продержался до первого длительного отключения электроэнергии в доме (у нас такое бывает). Дешевые батарейки разрядились раньше, чем появилось напряжение в розетках, а я вернулся к отправной точке.

После фальстарта, я решил не искать простых решений.

Что изменено или добавлено в проект С. Сильнова:

  • Автоматический переход монитора в автономный режим через 90 сек после выключения и повторного включения напряжения питания монитора, если в это время отсутствует Wi-Fi.
  • Граничные значения температуры, содержание СО2, влажность воздуха, а также часовой пояс и время зима/лето вводятся в память ESP8266, как и ключ Blynk, с формы в браузере.
  • Кардинально упрощено изменение граничных значений. Если раньше эта процедура выполнялась через компилятор кода, то теперь достаточно изменить запись в одном из полей формы с «0» на «1». Эта работа стала посильной даже не продвинутому юзеру.
  • Информация о работе монитора выводится на цветной экран 1.44”,128х128 одним кадром. Выход параметров воздуха за граничные значения отображается в кадре цветом.
  • В мониторе рассчитывается и отображается на экране индекс жары (heat index, humindex).
  • С монитора отправляются уведомления на е-мейл, если температура, содержание СО2 или влажность воздуха находятся за пределами заданных пользователем пороговых значений.
  • Монитор может работать без подключения к сервису Blynk.
  • В монитор добавлены стрелочные часы реального времени, которые через Интернет синхронизируются с сервером точного времени.

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 1

Заранее приношу извинения за «непричесанный» вид макета — это всего лишь одна из задач моего проекта «Беспроводной программируемый по Wi-Fi комнатный термостат с монитором качества воздуха и другими полезными функциями» [2]. Я решил оформить эту задачу отдельной статьей, поскольку в наше время качеством воздуха в жилье интересуются многие.

Сборка

Для сборки устройства понадобятся компоненты, перечень которых и их ориентировочная стоимость по ценам сайта AliExpress [3] приведена в таблице.

Компонент Цена, $
Wi-Fi плата NodeMCU CP2102 ESP8266 2,53
Датчик температуры и влажности DHT22 2,34
Датчик содержания СО2 MH Z-19 18,50
Экран TFTLCD 1.44" SPI 128x128 2,69
Часы RTC DS3231 1,00
Транзистор 2N2222A, конденсатор 0,22 мкФ, резистор 22 кОм, резистор 10 кОм, др. мелочи 3,00
Всего: 30,06

Мозг [4] монитора – контроллер ESP8266 на плате модуля NodeMCU CP2102. Он принимает сигналы с датчиков, часов и формирует сигнал управления экраном, синхронизирует часы, а также отправляет информацию на Blynk и е-мейл.

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 2 [5]

К сожалению, я не нашел библиотеку Fritzing’a для цветного экрана 1.44”, 128x128 с цоколевкой на 8 выводов, поэтому на схеме экран с 11 выводами. При монтаже обращайте внимание не на расположение вывода экрана относительно других, а на его функциональную нагрузку.

Многие не любят собирать макет по монтажной схеме. Для них – таблица соединений монитора:

NodeMCU (GPIO) Sensors, pin
D0 (GPIO 16) displ_1.44, CS
D1 (GPIO 5) DS3231, SCL
D2 (GPIO 4) DS3231, SDA
D3 (GPIO 0)  
D4 (GPIO 2) displ_1.44, AO
D5 (GPIO 14) displ_1.44, SCK
D6 (GPIO 12) displ_1.44, RESET
D7 (GPIO 13) displ_1.44, SDA
D8 (GPIO 15) DHT22, DATA
Tx MH-Z19, Rx
Rx MH-Z19, Tx
Vin (5V) displ_1.44, Vcc; DHT22; MH-Z19
3.3V displ_1.44, LED; DS3231, Vcc
GND Sensors, GND

Как видно из таблицы соединений, все цифровые выводы кроме GPIO0 ESP8266 заняты, выводы Tx, Rx – тоже. Это и понятно, ведь только для управления цветным экраном используется 5 цифровых выводов контроллера.

Для решения проблемы дефицита цифровых пинов предназначен ключ на транзисторе VT1. Вначале при подаче питания вывод GPIO15 ESP8266 ключом подтягивается к GND. Через несколько секунд после подачи питания ключ размыкается и на GPIO15 поступает сигнал с DHT22. Нестандартное решение позволяет вначале запустить ESP8266, а затем использовать это же вывод как цифровой для приема информации с датчика DHT22.

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 3

Если в модуле часов вы используете батарейку вместо аккумулятора, то не забудьте разорвать цепь заряда аккумулятора, иначе батарейка вздуется через несколько недель работы под напряжением. С автономным питанием часов точность хода 2 сек/год вам обеспечена.

Напряжение питания 5В на модуль NodeMCU CP2102 можно подать с USB–порта компьютера стандартным кабелем USB – microUSB.

Скетч монитора для загрузки в ESP8266 находится под спойлером.

Перед загрузкой скетча в модуль не забудьте подправить встроенный драйвер I2C для ядра Arduino ESP8266. Инструкции – тут [6].

скетч монитора

/*
 * Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы
 */
#include <FS.h>
#include <Arduino.h>
#include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino

// Wifi Manager
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager

//OLED
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <TFT_ILI9163C.h>

//clock
#include <pgmspace.h>
#include <TimeLib.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <RtcDS3231.h>
RtcDS3231<TwoWire> Rtc(Wire);
#define countof(a) (sizeof(a) / sizeof(a[0]))
//e-mail
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#define USE_SERIAL Serial
ESP8266WiFiMulti WiFiMulti;
//e-mail, address
char address[64] {"e-mail"};

// HTTP requests
#include <ESP8266HTTPClient.h>

// OTA updates
#include <ESP8266httpUpdate.h>
// Blynk
#include <BlynkSimpleEsp8266.h>

// Debounce
#include <Bounce2.h> //https://github.com/thomasfredericks/Bounce2

// JSON
#include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson

// Debounce interval in ms
#define DEBOUNCE_INTERVAL 10

Bounce hwReset {Bounce()};

// Humidity/Temperature
#include <DHT.h>
#define DHTPIN 15    //D8 gpio15, DHT22 DATA
#define DHTTYPE DHT22     // DHT 22
DHT dht(DHTPIN, DHTTYPE);

// Blynk token
char blynk_token[33] {"Blynk token"};

// Setup Wifi connection
WiFiManager wifiManager;

// Network credentials
String ssid { "am180206" };
String pass { "vb654321" };

//flag for saving data
bool shouldSaveConfig = false;

// Sensors data
float  t {-100}, t_old{-100};
float  hic {-1}, hic_old{-1};
int h {-1}, h_old{-1};
int co2 {-1}, co2_old{-1};
char Tmn[5]{}, Tmx[5]{}, Hmn[5]{}, Cmx[7]{}, tZ[5]{}, timeSW[4]{}, formFS[]{"0"}; //пороговые значения t, h и co2, час. пояс
float  Tmin, Tmax, Hmin, Cmax, tZone, timeSummerWinter, formatingFS;
float trp = 0;
int crbn, bl, ml=18000;
int md; //режим работы: 1 - онлайн, 2 - автономный
int blnk;

// Color definitions
#define  BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0  
#define WHITE   0xFFFF
#define GRAY    0x9999

#define __CS 16   //D0 gpio16, 1.44 CS
#define __DC 2    //D4 gpio2, 1.44 AO
#define __RST 12   // D6  gpio12, 1.44 RESET 

 //char datestring[20];
 char time_r[9];
 char date_r[12];

//analog clock
uint16_t ccenterx = 64,ccentery = 70;//center x,y of the clock clock
const uint16_t cradius = 40;//radius of the clock
const float scosConst = 0.0174532925;
float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;
float sdeg=0, mdeg=0, hdeg=0;
uint16_t osx,osy,omx,omy,ohx,ohy;
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
//uint32_t targetTime = 0;// for next 1 second timeout
uint8_t hh,mm,ss;  //containers for current time

TFT_ILI9163C display = TFT_ILI9163C(__CS, __DC, __RST);

String utf8(String source)
{
  int i,k;
  String target;
  unsigned char n;
  char m[2] = { '0', '' };

  k = source.length(); i = 0;

  while (i < k) {
    n = source[i]; i++;

    if (n >= 0xC0) {
      switch (n) {
        case 0xD0: {
          n = source[i]; i++;
          if (n == 0x81) { n = 0xA8; break; }
          if (n >= 0x90 && n <= 0xBF) n = n + 0x30;
          break;
        }
        case 0xD1: {
          n = source[i]; i++;
          if (n == 0x91) { n = 0xB8; break; }
          if (n >= 0x80 && n <= 0x8F) n = n + 0x70;
          break;
        }
      }
    }
    m[0] = n; target = target + String(m);
  }
return target;
}

// NTP Servers:
//static const char ntpServerName[] = "us.pool.ntp.org";
static const char ntpServerName[] = "time.nist.gov";

//const int timeZone = 2;     // Вильнюс, Киев, Рига, София, Таллин, Хельсинки
//const int timeSummer = 1;

WiFiUDP Udp;
unsigned int localPort = 2390;  // local port to listen for UDP packets

time_t getNtpTime();
void digitalClockDisplay();
void printDigits(int digits);
void sendNTPpacket(IPAddress &address);

void readCO2(){
#define mySerial Serial
static byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79}; //команда чтения
byte response[9];
byte crc = 0;
  while (mySerial.available())mySerial.read();//очистка буфера UART перед запросом 
  memset(response, 0, 9);// очистка ответа
  mySerial.write(cmd,9);// запрос на содержание CO2
  mySerial.readBytes(response, 9);//читаем 9 байт ответа сенсора
  //расчет контрольной суммы
  crc = 0;
  for (int i = 1; i <= 7; i++)
  {
    crc += response[i];
  }
  crc = ((~crc)+1);
  {
  //проверка CRC
  if ( !(response[0] == 0xFF && response[1] == 0x86 && response[8] == crc) ) 
  {
    Serial.println("CRC error");
  } else 
      {
       //расчет значеия CO2
       co2 = (((unsigned int) response[2])<<8) + response[3];
       Serial.println("CO2: " + String(co2) + "ppm");
        }
  }
}

void sendMeasurements() {
float  t1 {-100}, hic1 {-1};
float h1 {-1};
        // Temperature
        t1 = dht.readTemperature();   //раскомментировать!
       // t1 = 25.3;     //закомментировать!!!
       if ((t1 > -1) and (t1 < 100)) t = t1;
       Serial.println("T: " + String(t) + "*C");

        // Humidity
        h1 = dht.readHumidity();    //раскомментировать!
        if ((h1 > -1) and (h1 < 100))  h = h1;
        Serial.println("H: " + String(h) + "%");

        // Humindex
        hic1 = dht.computeHeatIndex(t, h, false);        
        hic = t;
        if (t >= 21.0) hic = hic1;
        Serial.println("Ti: "+String(hic)+"*C"); 

        // CO2
        crbn++;
        if (crbn > 110)
             {readCO2();  
              crbn = 0;
              Serial.println("CO2: " + String(co2) + "ppm");
             }
}

void drawConnectionDetails() {
        display.clearScreen();
        display.setTextSize(1);
        display.setCursor(12,24);
        display.setTextColor(WHITE);
        display.println(utf8("Connect to WiFi:"));
        display.setCursor(12,36);
        display.println(utf8("net: " + String(ssid)));
        display.setCursor(12,48);
        display.println(utf8("pass: " + String(pass)));          
        display.setCursor(12,60);
        display.println(utf8("Open browser:"));          
        display.setCursor(12,72);
        display.println(utf8("http://192.168.4.1"));
        display.setCursor(2,84);
        display.setTextColor(RED);
        display.println(utf8(" Enter your personal     information!"));
}        

void digitalClockDisplay()
{
  // digital clock display of the time
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.print(" ");
  Serial.print(day());
  Serial.print(".");
  Serial.print(month());
  Serial.print(".");
  Serial.print(year());
  Serial.println(); 
}

void printDigits(int digits)
{
  // utility for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

// NTP code
const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets

  time_t getNtpTime() {
  int tZoneI, timeSummerWinterI;    
  tZoneI = (int)tZone;  
  timeSummerWinterI = (int)timeSummerWinterI;    
  IPAddress ntpServerIP; // NTP server's ip address

  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  // get a random server from the pool
  WiFi.hostByName(ntpServerName, ntpServerIP);
  Serial.print(ntpServerName);
  Serial.print(": ");
  Serial.println(ntpServerIP);
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + tZoneI * SECS_PER_HOUR + timeSummerWinterI * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

void draw(){
//temperature  
  display.setTextSize(1);
  display.setCursor(1,6);
  display.setTextColor(CYAN);
  display.println(utf8("T:             CO2:"));

  String t_p;
         t_p = String(t); 
         char t_p_m [12];
        t_p.toCharArray(t_p_m, 5);

 if (t != t_old) { 
  display.fillRect(1,15,48,18,BLACK);
  display.setTextSize(2);
  display.setCursor(1,15);
  display.setTextColor(GREEN);
  if(t < Tmin) display.setTextColor(RED);
  if(t > Tmax) display.setTextColor(RED);
 if ((t > -100) and (t < 100)) display.println(utf8(String(t_p_m)));
  else display.println(utf8("----"));}

//heat index 
  display.setTextSize(1);
  display.setCursor(2,98);
  display.setTextColor(CYAN);
  display.println(utf8("H:               Ti:"));

  String hic_p;
         hic_p = String(hic); 
         char hic_p_m [12];
        hic_p.toCharArray(t_p_m, 5);

 if (hic != hic_old) { 
  display.fillRect(80,108,48,18,BLACK);
  display.setTextSize(2);
  display.setCursor(80,108);
  display.setTextColor(GREEN);
 // if(t < Tmin) display.setTextColor(RED);
  if(hic > 27.0) display.setTextColor(YELLOW);
  if(hic > 31.0) display.setTextColor(RED);
 if ((hic > 0) and (hic < 100)) display.println(utf8(String(t_p_m)));
  else display.println(utf8("----"));}

//CO2
 if (co2 != co2_old) {
  display.fillRect(80,15,48,18,BLACK);
  display.setTextSize(2);
  display.setCursor(80,15);
  display.setTextColor(GREEN);
  if (co2 > Cmax) display.setTextColor(RED);
  if (co2 > 600) display.setTextColor(CYAN);  
  if ((co2 > -1)  and (co2 <= 2000)) display.println(utf8(String(co2))); else display.println(utf8("---"));
 }

//humidity
if (h != h_old) {
  display.fillRect(1,108,49,18,BLACK);
  display.setTextSize(2);
  display.setCursor(1,108);
  display.setTextColor(GREEN);
  if (h < Hmin) display.setTextColor(RED);
  if (h > 60) display.setTextColor(RED);
  if ((h > -1) and (h < 100)) display.println(utf8(String(h))); else display.println(utf8("--"));
 }

//date    
 if (hh==0) display.fillRect(28,1,60,10,BLACK);
  display.setCursor(28,1);
  display.setTextSize(1);
  display.setTextColor(CYAN);
  display.println(utf8(date_r)); 

//OFFLINE
if (md == 2)
{  
  display.fillRect(106,44,18,8,RED);
  display.setCursor(106,44);
  display.setTextSize(1);
  display.setTextColor(CYAN);
  display.println(" A");  
  }

//OFF BLYNK
if (blnk == 1)
{ display.fillRect(106,44,18,8,RED);
  display.setCursor(106,44);
  display.setTextSize(1);
  display.setTextColor(CYAN);
  display.println(" B");  
  }
}

void synchronClockA() {
  Rtc.Begin();
  Serial.print("IP number assigned by DHCP is ");
  Serial.println(WiFi.localIP());
  Serial.println("Starting UDP");
  Udp.begin(localPort);
  Serial.print("Local port: ");
  Serial.println(Udp.localPort());
  Serial.println("waiting for sync");
  setSyncProvider(getNtpTime);
  //setSyncInterval(300);
  if(timeStatus() != timeNotSet){
    digitalClockDisplay();
    Serial.println("here is another way to set rtc");
      time_t t = now();
      char date_0[12];
      snprintf_P(date_0, countof(date_0), PSTR("%s %02u %04u"), monthShortStr(month(t)), day(t), year(t));
      Serial.println(date_0);
      char time_0[9];
      snprintf_P(time_0, countof(time_0), PSTR("%02u:%02u:%02u"), hour(t), minute(t), second(t));
      Serial.println(time_0);
      Serial.println("Now its time to set up rtc");
      RtcDateTime compiled = RtcDateTime(date_0, time_0);
 //      printDateTime(compiled);
       Serial.println("");

        if (!Rtc.IsDateTimeValid()) 
    {
        // Common Cuases:
        //    1) first time you ran and the device wasn't running yet
        //    2) the battery on the device is low or even missing

        Serial.println("RTC lost confidence in the DateTime!");

        // following line sets the RTC to the date & time this sketch was compiled
        // it will also reset the valid flag internally unless the Rtc device is
        // having an issue

    }
     Rtc.SetDateTime(compiled);
     RtcDateTime now = Rtc.GetDateTime();
    if (now < compiled) 
    {
        Serial.println("RTC is older than compile time!  (Updating DateTime)");
        Rtc.SetDateTime(compiled);
    }
    else if (now > compiled) 
    {
        Serial.println("RTC is newer than compile time. (this is expected)");
    }
    else if (now == compiled) 
    {
        Serial.println("RTC is the same as compile time! (not expected but all is fine)");
    }

    // never assume the Rtc was last configured by you, so
    // just clear them to your needed state
    Rtc.Enable32kHzPin(false);
    Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); 
  }
}

void synchronClock() {
  Rtc.Begin();
 // WiFi.begin(lnet, key);
   wifiManager.autoConnect(ssid.c_str(), pass.c_str());
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" ");
  Serial.print("IP number assigned by DHCP is ");
  Serial.println(WiFi.localIP());
  Serial.println("Starting UDP");
  Udp.begin(localPort);
  Serial.print("Local port: ");
  Serial.println(Udp.localPort());
  Serial.println("waiting for sync");
  setSyncProvider(getNtpTime);

  if(timeStatus() != timeNotSet){
    digitalClockDisplay();
    Serial.println("here is another way to set rtc");
      time_t t = now();
      char date_0[12];
      snprintf_P(date_0, countof(date_0), PSTR("%s %02u %04u"), monthShortStr(month(t)), day(t), year(t));
      Serial.println(date_0);
      char time_0[9];
      snprintf_P(time_0, countof(time_0), PSTR("%02u:%02u:%02u"), hour(t), minute(t), second(t));
      Serial.println(time_0);
      Serial.println("Now its time to set up rtc");
      RtcDateTime compiled = RtcDateTime(date_0, time_0);

       Serial.println("");

        if (!Rtc.IsDateTimeValid()) 
    {
        // Common Cuases:
        //    1) first time you ran and the device wasn't running yet
        //    2) the battery on the device is low or even missing

        Serial.println("RTC lost confidence in the DateTime!");

        // following line sets the RTC to the date & time this sketch was compiled
        // it will also reset the valid flag internally unless the Rtc device is
        // having an issue

    }
     Rtc.SetDateTime(compiled);
     RtcDateTime now = Rtc.GetDateTime();
    if (now < compiled) 
    {
        Serial.println("RTC is older than compile time!  (Updating DateTime)");
        Rtc.SetDateTime(compiled);
    }
    else if (now > compiled) 
    {
        Serial.println("RTC is newer than compile time. (this is expected)");
    }
    else if (now == compiled) 
    {
        Serial.println("RTC is the same as compile time! (not expected but all is fine)");
    }

    // never assume the Rtc was last configured by you, so
    // just clear them to your needed state
    Rtc.Enable32kHzPin(false);
    Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); 
}
}

void Clock(){
    RtcDateTime now = Rtc.GetDateTime();
    //Print RTC time to Serial Monitor
   hh = now.Hour();
   mm = now.Minute(); 
   ss = now.Second();

  sprintf(date_r, "%d.%d.%d", now.Day(), now.Month(), now.Year());
  if  (mm < 10) sprintf(time_r, "%d:0%d", hh, mm);
  else sprintf(time_r, "%d:%d", hh, mm);

   Serial.println(date_r);
   Serial.println(time_r);
   }

//analog
void drawClockFace(){
  display.fillCircle(ccenterx, ccentery, cradius, BLUE);
  display.fillCircle(ccenterx, ccentery, cradius-4, BLACK);
  // Draw 12 lines
  for(int i = 0; i<360; i+= 30) {
    sx = cos((i-90)*scosConst);
    sy = sin((i-90)*scosConst);
    x0 = sx*(cradius)+ccenterx;
    yy0 = sy*(cradius)+ccentery;
    x1 = sx*(cradius-8)+ccenterx;
    yy1 = sy*(cradius-8)+ccentery;
    display.drawLine(x0, yy0, x1, yy1, 0x0377);
  }
  // Draw 4 lines
  for(int i = 0; i<360; i+= 90) {
    sx = cos((i-90)*scosConst);
    sy = sin((i-90)*scosConst);
    x0 = sx*(cradius+6)+ccenterx;
    yy0 = sy*(cradius+6)+ccentery;
    x1 = sx*(cradius-11)+ccenterx;
    yy1 = sy*(cradius-11)+ccentery;
    display.drawLine(x0, yy0, x1, yy1, 0x0377);
  }  
}
//analog
static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9') v = *p - '0';
  return 10 * v + *++p - '0';
}

//analog  
void drawClockHands(uint8_t h,uint8_t m,uint8_t s){
  // Pre-compute hand degrees, x & y coords for a fast screen update
  sdeg = s * 6;                  // 0-59 -> 0-354
  mdeg = m * 6 + sdeg * 0.01666667;  // 0-59 -> 0-360 - includes seconds
  hdeg = h * 30 + mdeg * 0.0833333;  // 0-11 -> 0-360 - includes minutes and seconds
  hx = cos((hdeg-90)*scosConst);    
  hy = sin((hdeg-90)*scosConst);
  mx = cos((mdeg-90)*scosConst);    
  my = sin((mdeg-90)*scosConst);
  sx = cos((sdeg-90)*scosConst);    
  sy = sin((sdeg-90)*scosConst);

  // Erase just old hand positions
  display.drawLine(ohx, ohy, ccenterx+1, ccentery+1, BLACK);  
  display.drawLine(omx, omy, ccenterx+1, ccentery+1, BLACK);  
  display.drawLine(osx, osy, ccenterx+1, ccentery+1, BLACK);
  // Draw new hand positions  
  display.drawLine(hx*(cradius-20)+ccenterx+1, hy*(cradius-20)+ccentery+1, ccenterx+1, ccentery+1, WHITE);
  display.drawLine(mx*(cradius-8)+ccenterx+1, my*(cradius-8)+ccentery+1, ccenterx+1, ccentery+1, WHITE);
  display.drawLine(sx*(cradius-8)+ccenterx+1, sy*(cradius-8)+ccentery+1, ccenterx+1, ccentery+1, RED);
  display.fillCircle(ccenterx+1, ccentery+1, 3, RED);

  // Update old x&y coords
  osx = sx*(cradius-8)+ccenterx+1;
  osy = sy*(cradius-8)+ccentery+1;
  omx = mx*(cradius-8)+ccenterx+1;
  omy = my*(cradius-8)+ccentery+1;
  ohx = hx*(cradius-20)+ccenterx+1;
  ohy = hy*(cradius-20)+ccentery+1;
}
void  FaceClock(){
 display.clearScreen();
 display.setTextColor(WHITE, BLACK);
//  ccenterx = display.width()/2;
//  ccentery = display.height()/2;
  osx = ccenterx;
  osy = ccentery;
  omx = ccenterx;
  omy = ccentery;
  ohx = ccenterx;
  ohy = ccentery;
  drawClockFace();// Draw clock face  
}

void drawSynchron() {
        display.clearScreen();
        display.setTextSize(2);
        display.setCursor(2,48);
        display.setTextColor(WHITE);
        display.println(utf8("  Clock"));
        display.setTextSize(1);
        display.setCursor(2,68);
        display.setTextColor(WHITE);
        display.println(utf8("synchronization..."));
      } 

void drawWiFi() {
        display.clearScreen();
        display.setTextSize(2);
        display.setCursor(2,48);
        display.setTextColor(RED);
        display.println(utf8("Connection to Wi-Fi"));
      }

void drawBlynk() {
        display.clearScreen();
        display.setTextSize(2);
        display.setCursor(2,48);
        display.setTextColor(RED);
        display.println(utf8("Connection to Blynk"));
}

void mailer() {
     // wait for WiFi connection
    if((WiFiMulti.run() == WL_CONNECTED)) {

        HTTPClient http;

        Serial.print("[HTTP] begin...n");

        http.begin("http://skorovoda.in.ua/php/wst41.php?mymail="+String(address)+"&t="+String(t) +"&h="+String(h)+"&co2="+String(co2)+"&ID="+String(ESP.getChipId()));

        Serial.print("[HTTP] GET...n");
        // start connection and send HTTP header
        int httpCode = http.GET();

        // httpCode will be negative on error
        if(httpCode > 0) {
            // HTTP header has been send and Server response header has been handled
            Serial.printf("[HTTP] GET... code: %dn", httpCode);

            // file found at server
            if(httpCode == HTTP_CODE_OK) {
                String payload = http.getString();
                Serial.println(payload);
            }
        } else {
            Serial.printf("[HTTP] GET... failed, error: %sn", http.errorToString(httpCode).c_str());
        }
        http.end();
    }   
}

//callback notifying the need to save config
void saveConfigCallback() {
        Serial.println("Should save config");
        shouldSaveConfig = true;
}
void factoryReset() {
        Serial.println("Resetting to factory settings");
        wifiManager.resetSettings();
        SPIFFS.format();
        ESP.reset();
}
void printString(String str) {
        Serial.println(str);
}

bool loadConfigS() {
        Blynk.config(address);  
        Serial.print("e-mail: ");  
        Serial.println(  address );   

        Blynk.config(tZ);  
        Serial.print("T_Zone: ");  
        Serial.println(  tZ );

        Blynk.config(Tmx);  
        Serial.print("T max: ");  
        Serial.println(  Tmx );   

        Blynk.config(Cmx);  
        Serial.print("CO2 max: ");  
        Serial.println(  Cmx );   

        Blynk.config(Tmn);  
        Serial.print("T min: ");  
        Serial.println(  Tmn );   

        Blynk.config(Hmn);  
        Serial.print("H min: ");  
        Serial.println(  Hmn ); 

        Blynk.config(timeSW);  
        Serial.print("Time Summer/Winter: ");  
        Serial.println( timeSW ); 

        Blynk.config(formFS);  
        Serial.print("format FS: ");  
        Serial.println( formFS );               

        Blynk.config(blynk_token, "blynk-cloud.com", 8442);   
        Serial.print("token: " );
        Serial.println(  blynk_token  );     
}

bool loadConfig() {
        Serial.println("Load config...");
        File configFile = SPIFFS.open("/config.json", "r");
        if (!configFile) {
                Serial.println("Failed to open config file");
                return false;
        }

        size_t size = configFile.size();
        if (size > 1024) {
                Serial.println("Config file size is too large");
                return false;
        }

        // Allocate a buffer to store contents of the file.
        std::unique_ptr<char[]> buf(new char[size]);

        // We don't use String here because ArduinoJson library requires the input
        // buffer to be mutable. If you don't use ArduinoJson, you may as well
        // use configFile.readString instead.
        configFile.readBytes(buf.get(), size);

        StaticJsonBuffer<200> jsonBuffer;
        JsonObject &json = jsonBuffer.parseObject(buf.get());

        if (!json.success()) {
                Serial.println("Failed to parse config file");
                return false;
        }

        // Save parameters
        strcpy(blynk_token, json["blynk_token"]);
        strcpy(address, json["address"]);
        strcpy(tZ, json["tZ"]);
        strcpy(Tmx, json["Tmx"]);
        strcpy(Cmx, json["Cmx"]);
        strcpy(Tmn, json["Tmn"]);
        strcpy(Hmn, json["Hmn"]);
        strcpy(timeSW, json["timeSW"]);
        strcpy(formFS, json["formFS"]);      
}

void configModeCallback (WiFiManager *wifiManager) {
        String url {"http://192.168.4.1"};
        printString("Connect to WiFi:");
        printString("net: " + ssid);
        printString("pw: "+ pass);
        printString("Open browser:");
        printString(url);
        printString("to setup device");

        drawConnectionDetails();
}

void setupWiFi() {
        //set config save notify callback
        wifiManager.setSaveConfigCallback(saveConfigCallback);

        // Custom parameters
         WiFiManagerParameter custom_blynk_token("blynk_token", "Blynk token", blynk_token, 34);
         wifiManager.addParameter(&custom_blynk_token);
         WiFiManagerParameter custom_address("address", "E-mail", address, 64);
         wifiManager.addParameter(&custom_address);
         WiFiManagerParameter custom_tZ("tZ", "Time Zone", tZ, 5); 
         wifiManager.addParameter(&custom_tZ);
         WiFiManagerParameter custom_Tmn("Tmn", "T min", Tmn, 5);  
         wifiManager.addParameter(&custom_Tmn);                  
         WiFiManagerParameter custom_Tmx("Tmx", "T max", Tmx, 5); 
         wifiManager.addParameter(&custom_Tmx);         
         WiFiManagerParameter custom_Cmx("Cmx", "C max", Cmx, 7);  
         wifiManager.addParameter(&custom_Cmx);
         WiFiManagerParameter custom_Hmn("Hmn", "H min", Hmn, 5);  
         wifiManager.addParameter(&custom_Hmn); 
         WiFiManagerParameter custom_timeSW("timeSW", "Time Summer(1)/Winter(0)", timeSW, 4);  
         wifiManager.addParameter(&custom_timeSW);     
         WiFiManagerParameter custom_formFS("formFS", "formating FS", formFS, 4);  
         wifiManager.addParameter(&custom_formFS);     

        wifiManager.setAPCallback(configModeCallback);

        wifiManager.setTimeout(60);

        if (!wifiManager.autoConnect(ssid.c_str(), pass.c_str())) {
        md = 2;
        Serial.println("mode OffLINE :(");
        loadConfigS();         
        }

        //save the custom parameters to FS
        if (shouldSaveConfig) {
                Serial.println("saving config");
                DynamicJsonBuffer jsonBuffer;
                JsonObject &json = jsonBuffer.createObject();

                json["blynk_token"] = custom_blynk_token.getValue();                
                json["address"] = custom_address.getValue();
                json["tZ"] = custom_tZ.getValue(); 
                json["Tmx"] = custom_Tmx.getValue(); 
                json["Cmx"] = custom_Cmx.getValue(); 
                json["Tmn"] = custom_Tmn.getValue(); 
                json["Hmn"] = custom_Hmn.getValue(); 
                json["timeSW"] = custom_timeSW.getValue();
                json["formFS"] = custom_formFS.getValue();

                File configFile = SPIFFS.open("/config.json", "w");
                if (!configFile) {
                        Serial.println("failed to open config file for writing");
                }

                json.printTo(Serial);
                json.printTo(configFile);
                configFile.close();
                //end save
        }

        //if you get here you have connected to the WiFi
        Serial.println("WiFi connected");
        Serial.print("IP address: ");
        Serial.println(WiFi.localIP());
}

void connectBlynk(){ 
       if(String(blynk_token)== "Blynk token"){
        blnk = 0;
        Serial.println("! Off Blynk!");
        } else {
        Serial.println("Connecting to blynk...");
        while (Blynk.connect() == false) {
        delay(500);
        Serial.println("Connecting to blynk...");
        }    
    } 
}

void sendToBlynk(){
        Blynk.virtualWrite(V1, t);
        Blynk.virtualWrite(V2, h);
        Blynk.virtualWrite(V3, co2);
        Blynk.virtualWrite(V5, hic);
}

void formatFS(){
          SPIFFS.format();  
          SPIFFS.begin();  
}

void setup() {
      // factoryReset();   //форматирование RAM

        Serial.begin(115200);

        display.begin();    

        // Init filesystem
        if (!SPIFFS.begin()) {
                Serial.println("Failed to mount file system");
                ESP.reset();
        }
        md = 1;
        // Setup WiFi
        drawWiFi();   //"Connecting to Wi-Fi..."
        setupWiFi();
       if(md == 1){  

        // Load configuration           
        if (!loadConfig()) {
                Serial.println("Failed to load config");
             //   factoryReset();
        } else {
                Serial.println("Config loaded");
        }   

        Blynk.config(address);  
        Serial.print("e-mail: ");  
        Serial.println(  address );   

        Blynk.config(tZ);  
        Serial.print("T_Zone: ");  
        Serial.println(  tZ );

        Blynk.config(Tmx);  
        Serial.print("T max: ");  
        Serial.println(  Tmx );   

        Blynk.config(Cmx);  
        Serial.print("CO2 max: ");  
        Serial.println(  Cmx );   

        Blynk.config(Tmn);  
        Serial.print("T min: ");  
        Serial.println(  Tmn );   

        Blynk.config(Hmn);  
        Serial.print("H min: ");  
        Serial.println(  Hmn ); 

        Blynk.config(timeSW);  
        Serial.print("Time Summer/Winter: ");  
        Serial.println( timeSW ); 

        Blynk.config(formFS);  
        Serial.print("format FS: ");  
        Serial.println( formFS );                               

        Blynk.config(blynk_token, "blynk-cloud.com", 8442);   
        Serial.print("token: " );
        Serial.println(  blynk_token  );

        Tmax = atof (Tmx);   
        Cmax = atof (Cmx); 
        Tmin = atof (Tmn);  
        Hmin = atof (Hmn);
        tZone = atof (tZ);
        timeSummerWinter = atof (timeSW);  
        formatingFS = atof (formFS);

        drawSynchron();
        synchronClock();
        connectBlynk(); 
        FaceClock(); 

     if (formatingFS == 1) {
        formatFS();    
       }                      
       }
     else if(md == 2)
     {

        Tmax = atof (Tmx);   
        Cmax = atof (Cmx); 
        Tmin = atof (Tmn);  
        Hmin = atof (Hmn);
        tZone = atof (tZ);
        timeSummerWinter = atof (timeSW);  
        formatingFS = atof (formFS);

        synchronClockA();
        FaceClock();

     if (formatingFS == 1) {
        formatFS();    
       }
     }   
}

void loop() {
 if (md == 2) Serial.println(":( OffLINE");  
 else if (md == 1) Serial.println(":) OnLINE");       
  sendMeasurements();
  draw();
  Clock();
  drawClockHands(hh,mm,ss); 

   if (ml >= 480000) ml = 0;   //обнуление счетчика
   if ((ml >= 20000) and ((t > Tmax) or (co2 > Cmax) or (t < Tmin) or (h < Hmin)))
        {
        mailer();
         ml = 0;
         }

       Blynk.run();       
       if  (bl > 210){ // 30 sec
            sendToBlynk();
            Serial.println("Отправка данных на Blynk");
            bl = 0;
              }

          bl++;
          ml++;

        // delay(1000);   //расскоментировать при тестировании
          delay(100);         
          t_old = t;
          hic_old = hic;
          h_old = h;
          co2_old = co2;
          Serial.println(" ");
}

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

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 4 [7]

Сообщения на е-мейл отправляются php-скриптом. Скрипт загружен на мой почтовый сервер. Он понадобится, если планируется отправка сообщений с другого ресурса.

php-скрипт

<?php
// тест -  http://skorovoda.in.ua/php/aqm42.php?mymail=my_login@my.site.net&t=22.2&h=55&co2=666
$EMAIL=0;
$TEMPER=0;
$vlaga=0;
$carbon=0;
$device=0;
$EMAIL=$_GET["mymail"];
$device=$_GET["ID"];
echo $EMAIL;
$TEMPER=$_GET["t"];
$vlaga=$_GET["h"];
$carbon=$_GET["co2"];
$mdate = date("H:i d.m.y");
echo <<<END
<p>Температура:  $TEMPER °С<p>
<p>Влажность: $vlaga %<p>
<p>Содержание углекислого газа:  $carbon ppm<p>
<p>--------------------<p>
<p>Метеостанция №:  $device<p>
END;
echo <<<END
<p>$mdate</p>
END;
mail($EMAIL, "Air Quality Monitor " .$device. " v.051018","   Данное сообщение сформировано монитором качества воздуха №" .$device. " автоматически.  Один или несколько параметров воздуха в помещении (температура, влажность или содержание углекислого газа) находятся за пределами заданных граничных значений. === Температура: ".$TEMPER."°C === "."Влажность: ".$vlaga."% === "."Содержание углекислого газа: ".$carbon." ppm === "."Проанализируйте информацию! === Время, дата: ".$mdate,"From: my_sensors@air-monitor.info n")

?>

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 5

Включим монитор.

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 6

Устройство подняло точку доступа am180206. Найдем эту точку в списке доступных сетей и подключимся к ней, пароль – на экране. Постарайтесь подключиться к этой точке за полторы минуты, иначе монитор автоматически перейдет в режим автономной работы. Об автономном режиме – чуть позже. Затем откроем в браузере страницу http://192.168.4.1.

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 7

Нажмем кнопку Configure WiFi (No Scan). Откроется страница с формой настроек термостата:

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 8

Укажем в форме имя и пароль своей домашней сети, ключ идентификации BLynk, свой е-мейл, часовой пояс, летнее/зимнее время, а также пороговые значения температуры, влажности и содержания СО2.

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

1. Комфортная температура ночью во время сна 19…21°С, днем — 22…23°С.

2. Оптимальной относительной влажностью в холодное время года считается влажность 30…45%, а в теплое – 30…60%. Предельные максимальные показатели влажности: зимой она не должна превышать 60%, а летом – 65%.

3. Максимальный уровень содержания углекислого газа в помещениях не должен превышать 1000 ppm. Рекомендованный уровень для спален, детских комнат – не более 600 ppm. Отметка 1400 ppm – предел допустимого содержания СО2 в помещении. Если его больше, то качество воздуха считается низким.

Поле e-mai можно не заполнять. Тогда предоставленная возможность получать письма на электронную почту о выходе параметров воздуха за граничные значения будет утрачена. Без введенного ключа Blynk’а вы потеряете возможность получать информацию о параметрах воздуха на удалении. Впрочем, монитор не «растеряется», если останутся незаполненными поля с предельными значениями параметров воздуха.

Заполняйте, не задумываясь, все поля формы с числами в формате с плавающей запятой, например, часовой пояс – 2.0. В скетче предусмотрены последующие преобразования чисел в нужный формат.

После сохранения настроек в памяти ESP8266 (кнопка Save), монитор подключится к сети и начнет работу.

Рассмотрим картинку на экране.

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 9

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

Буква «В» на красном фоне говорит о том, что монитор работает без подключения к Blynk’у, а если появится буква «А» — исчезало питание и на момент его появления отсутствовал Wi-Fi (прибор перешел в автономный режим).

В общем, появление красного цвета на экране должно насторожить – есть отклонения от нормального функционирования устройства.

В нижнем правом углу на экране мы видим индекс жары (heat index, humindex).

«Humidex — безразмерная величина, основанная на точке росы. Данный индекс широко используется в канадских метеосводках летом.
Согласно канадской метеорологической службе, значения Humidex выше 30 причиняют некоторый дискомфорт, выше 40 — большой дискомфорт, а значение выше 45 является опасным. Если humidex достигает 54, тепловой удар неизбежен. Влияние ветра этот индекс не учитывает.»
(Википедия, humidex [8] ).

Следует уточнить, что индекс жары рассчитывается только для относительно высоких температур, когда измеренная температура воздуха выше 21°С. Образно, индекс жары – это ощущаемая температура в жаркий безветренный день.

В соответствии с таблицей этой же статьи в Википедии измеренная температура 25°С при влажности 30% ощущается как 24°С, при 50% — 28°С, а при 90% — 35°С (на 10°С выше показаний термометра). В помещении этот показатель можно уменьшить, устроив «сквозняк» или включив вентилятор. Кондиционер включится автоматически, если вы задали температуру кондиционирования не выше 25°С. Индекс жары, на мой взгляд, более актуальный параметр качества воздуха, чем, допустим, давление, на которое мы никак не можем влиять.

Датчик DHT22, как и DHT11, наряду с «минусом» — низкой точностью измерения влажности обладает неоспоримым «плюсом»: на шине данных этого датчика есть информация о индексе жары. Я воспользовался этим «плюсом» и отобразил индекс жары на экране устройства.

Пришло время запустить на смартфоне приложение Blynk.

Настройте у себя приложение. Переменные для Blynk (чтобы не искать их в скетче): температура — V1, влажность – V2, содержание СО2 – V3, индекс жары – V5.

На моем смартфоне интерфейс Blynk’a имеет вид:

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 10

На графике – измеренная температура (желтый), влажность (голубой), индекс жары (фиолетовый). Первый пик на графике – это нагрев датчика давления-влажности в ладони руки: кривая индекса жары при этом располагается выше кривой температуры. Второй пик – нагрев датчика с помощью фена. Влажность воздуха при нагреве датчика феном падает, а кривая индекса жары на некоторых участках повторяет или ниже линии температуры. (см. выноску).

Не беспокойтесь, если на графике исчезла линия температуры: при температуре ниже отметки 21°С кривая индекса жары повторяет кривую измеренной температуры.

Теперь протестируем работу системы оповещений на е-мейл. Введем в адресную строку браузера закомментированную строку с http-адресом из кода php-скрипта. Если вы не забыли в настройках указать свой е-мейл, а в окне браузера — информация, как на картинке ниже, то проблем с приемом оповещений скорее всего не будет. Тест особенно полезен при переносе php-скрипта с моего сервера на другой.

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 11

Выводы

Не буду повторять, что сделано. Остановлюсь лишь на неоднозначных моментах.

Логична, например, постановка вопроса «А зачем здесь часы?». Ответ: Стрелочные часы нужны для наполнения экрана. Хотя в настоящее время цифровые часы встроены практически в каждый бытовой прибор, стрелочные обладают одним преимуществом: масштаб времени, благодаря циферблату, позволяет легко оценить время до какого-либо события. Не буду спорить — можно обойтись без часов.

Субъективно монитор на пути от непростой игрушки немного приблизился к статусу профессиональной разработки. Тут под профессиональной разработкой подразумевается прибор, который можно тиражировать.

Над чем нужно поработать:

• Организовать питание монитора от двух батареек типа АА на протяжении длительного времени — не меньше года.

• Внешний вид обсуждать не стоит – здесь у каждого свой взгляд и возможности. Мне, например, симпатичен вариант avs24rus [9], он в качестве экрана использовал планшет в рамке с 3D печатью [10]. Дорогую рамку можно заменить дешевой из багета. А на дисплей планшета в режиме ожидания вывести мыльные пузыри, рыбок в аквариуме, портрет любимой или фото детей – у вас будет еще и оригинальная фоторамка в придачу. Если вы не очень требовательны к эстетичной стороне вопроса, то, возможно, вас устроит нечто похожее:

Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы - 12

Успехов!

Мои закладки по теме с Хабра

1. Wi-Fi термометр на ESP8266 + DS18B20 всего за 4$ [11]

2. Компактный монитор домашнего воздуха (CO2, температура, влажность, давление) с Wi-Fi и мобильным интерфейсом [12]

3. Практический опыт использования Blynk для датчика СО2. Часть 1 [13]

4. Ипользование SPI Flash памяти дисплея для хранения графических ресурсов или дисплей домашней метеостанции [10]

5. Измеряем концентрацию CO2 в квартире с помощью MH-Z19 [14]

И наконец, благодарю @kumekay [15] за ценные советы.

Решить проблему повторного ввода переменных в память монитора мне помогла публикация «Практический опыт использования Blynk для датчика СО2. Часть 1» [13], @a3x [16]. Глубокая разноплановая статья!

Автор: Владимир Бойчук

Источник [17]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/diy-ili-sdelaj-sam/310864

Ссылки в тексте:

[1] «Компактный монитор домашнего воздуха (CO2, температура, влажность, давление) с Wi-Fi и мобильным интерфейсом»: https://habr.com/ru/post/400975/

[2] «Беспроводной программируемый по Wi-Fi комнатный термостат с монитором качества воздуха и другими полезными функциями»: https://habr.com/ru/post/440978/

[3] AliExpress: https://ru.aliexpress.com/

[4] Мозг: http://www.braintools.ru

[5] Image: https://habrastorage.org/webt/dn/io/sw/dnioswmr26e_idbcejacqd7zd4c.png

[6] тут: https://github.com/enjoyneering/ESP8266-I2C-Driver

[7] Image: https://habrastorage.org/webt/sb/s7/e5/sbs7e5g0boyq3gqvnyos8vhgpic.png

[8] humidex: https://ru.wikipedia.org/wiki/Humidex

[9] avs24rus: https://habr.com/ru/users/avs24rus/

[10] он в качестве экрана использовал планшет в рамке с 3D печатью: https://habr.com/ru/post/388449/

[11] Wi-Fi термометр на ESP8266 + DS18B20 всего за 4$: https://habr.com/ru/post/252481/

[12] Компактный монитор домашнего воздуха (CO2, температура, влажность, давление) с Wi-Fi и мобильным интерфейсом: https://habr.com/post/400975/

[13] Практический опыт использования Blynk для датчика СО2. Часть 1: https://habr.com/ru/post/122601/

[14] Измеряем концентрацию CO2 в квартире с помощью MH-Z19: https://habr.com/ru/post/395755/

[15] @kumekay: https://habr.com/ru/users/kumekay/

[16] @a3x: https://habr.com/ru/users/a3x/

[17] Источник: https://habr.com/ru/post/442856/?utm_source=habrahabr&utm_medium=rss&utm_campaign=442856