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


В мониторе качества воздуха (далее – монитор) из проекта Сергея информация с датчиков температуры, влажности, давления, содержания СО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 комнатный термостат с монитором качества воздуха и другими полезными функциями». Я решил оформить эту задачу отдельной статьей, поскольку в наше время качеством воздуха в жилье интересуются многие.


Сборка


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


Компонент Цена, $
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 кОм, резистор 390 Ом, др. мелочи 3,00
Всего: 30,06

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



К сожалению, я не нашел библиотеку 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.



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


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


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


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


скетч монитора
/*
 * Два в одном: программируемый по 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', '\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: %d\n", httpCode);

            // file found at server
            if(httpCode == HTTP_CODE_OK) {
                String payload = http.getString();
                Serial.println(payload);
            }
        } else {
            Serial.printf("[HTTP] GET... failed, error: %s\n", 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(" ");
}

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



Сообщения на е-мейл отправляются 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")

?>


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



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



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



Укажем в форме имя и пароль своей домашней сети, ключ идентификации 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), монитор подключится к сети и начнет работу.


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



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


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


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


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


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


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


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


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


В Интернете много нареканий на низкую точность измерения влажности и «неадекватность» датчика DHT22. Для тех, кто настроен отказаться от использования этого датчика в своих проектах, прошу посмотреть в сторону более современных датчиков температуры и влажности HTU21D, Si7021 или SHT21.


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


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


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



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


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


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




Выводы


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


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


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


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


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


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



Успехов!


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


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


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


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


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


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


6. Тёмная сторона MH-Z19


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


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

Комментарии (39)


  1. glebovgin
    06.03.2019 23:56

    А вы не экспериментировали с CCS811? Я как раз сейчас жду все запчасти для сбора своего анализатора воздуха и мне CCS811 нравится своим низким энергопотреблением. Но есть мнение, что он не совсем про измерение углекислого газа. Вдруг у кого опыт был.


    1. batja84
      07.03.2019 00:14

      Вроде измеряет equivalent CO2 и Total Volatile Organic Compound

      CO2e allows other greenhouse gas emissions to be expressed in terms of CO2 based on their relative global warming potential (GWP).

      CO2 has a GWP of 1, methane has a GWP of approximately 25 (on a 100 year time horizon). In other words, for every 1 tonne of methane (CH4) emitted, an equivalent of 25 tonnes of CO2 would be emitted.


  1. u010602
    07.03.2019 01:57
    -1

    Ни когда не понимал стрелочные часы на экранах с низким разрешением. Какой-то кибер-стим-панк получается. Т.е. современная технология эмитирует архаизм но делает это очень плохо, в итоге выходит ерунда в квадрате. Читаемость хуже чем у цифрового вывода, детализация ужасная, цвета ужасные, расчеты сложные. Зачем? Вы-же сами пишете что вам нравится вариант с численным выводом. Тогда даже такой скромный экран можно скрыть за тонированным стеклом и будет красиво.
    Если очень хочется стрелочные часы, может стоит сделать вывод на подложку реальных стрелочных часов?


    1. Cadil_TM Автор
      07.03.2019 08:58

      «Ни когда не понимал стрелочные часы на экранах с низким разрешением.»
      Сейчас вы смотрите на экран через объектив камеры с расстояния 3-10 см. Если смотреть человеческим глазом с расстояния 0,3...0,5м, то угловатость сглаживается, а цифры на таком экране еще можно прочитать.
      Под другой экран — будут другие часы, другая графика символов и остальное. Конечно, выводить информацию в таком виде на дисплей планшета — это дикость!


      1. Cadil_TM Автор
        07.03.2019 09:20

        "… цвета ужасные,… "
        На этом цветном экране 1,44 можно задать столько же цветов, как и на мониторе вашего компа: (0xzуzуzуzу), zу — числа в шестнадцатиричном формате.
        Повторюсь, снимки в статье сделаны с очень маленького расстояния.


        1. u010602
          07.03.2019 14:05

          Задать то конечно можно, но цветопередача не даст их нормально отобразить. Судя по фото подсветка экрана на светодиодах очень холодного света. Черного нет, он выглядит как голубой. Размер угловатости красной стрелки в 2 раза больше чем размер минимального шрифта на экране (дата в верху), значит или не будет видно дату, или будет видно угловатости. У меня были телефоны с такими экранами, я помню как красиво на них выглядели круги и диагональные линии.


          1. Cadil_TM Автор
            07.03.2019 14:19

            Если найду цветной экран размером до 1,5 дюйма и с разрешением хотя бы раз 5 выше — ваши претензии снимутся автоматически. Ставить в устройство экран со старого телефона — сомнительная идея.


            1. u010602
              07.03.2019 15:08

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


              1. Cadil_TM Автор
                07.03.2019 15:29

                «Претензий ни каких нет, что вы. ...»
                Спасибо за понимание!


  1. FGV
    07.03.2019 06:46

    Нестандартное решение позволяет вначале запустить ESP8266, а затем использовать это же вывод как цифровой для приема информации с датчика DHT22.

    а зачем так сложно? не проще ли поменять gpio15 и gpio16 местами и gpio15 притянуть резистором на землю?
    Т.е. cs экрана дергать через gpio15, а с датчиком общаться по gpio16.
    Тогда и транзистор не нужен будет.


    1. Cadil_TM Автор
      07.03.2019 09:32

      «а зачем так сложно? не проще ли поменять gpio15 и gpio16 местами и gpio15 притянуть резистором на землю?»
      В этом случае, возможно, — да, но я смотрю на задачу под углом использования экрана в термостате и возможного увеличения количества датчиков в будущем.
      И не стоит забывать, что только отдельные цифровые выводы ESP8266 работают в режиме прерываний или поддерживают протокол I2C.


      1. FGV
        07.03.2019 10:36

        >И не стоит забывать, что только отдельные цифровые выводы ESP8266 работают в режиме прерываний или поддерживают протокол I2C.

        I2c у 8266 софтварный, так что любой пин.
        Прерывания вроде все кроме gpio16. Так что если нужны прерывания для датчика можно поменять местами 12-15, уж для дрыгания ресетом экрана точно прерываний не надо :)


        1. Cadil_TM Автор
          07.03.2019 13:12

          «I2c у 8266 софтварный, так что любой пин.
          Прерывания вроде все кроме gpio16. Так что если нужны прерывания для датчика можно поменять местами 12-15, уж для дрыгания ресетом экрана точно прерываний не надо :)»
          О том, что в режиме прерываний работают все пины, кроме gpio16 — не знал. Спасибо!
          Когда подключал библиотеку RCSwitch.h (передатчик, режим прерываний) — там речь шла только о двух пинах, работающих в режиме прерываний с этой библиотекой. К сожалению, страница с этой информацией уже не открывается.
          Протокол I2c со всеми датчиками и экраном я с трудом запустил на «родных» выводах, используя «заплатки».
          Впрочем, возможно ваш подход — правильный. Набросайте схему подключений ESP8266+цветной экран SPI+dht22+mh-z19+передатчик 433МГц + датчик угарного газа на ваш выбор. Заранее приношу извинения, если схема заработает.


          1. AndyKorg
            07.03.2019 13:45

            ...ESP8266+цветной экран SPI+dht22+mh-z19+передатчик 433МГц + датчик угарного газа ..

            Есть похожая схема часов, но для передачи использую nrf24l01 и esp используется только как чип работы с Инетом (управление и получение погоды)


          1. FGV
            09.03.2019 13:08

            цепи питания условно не показаны:

            RC-цепь — стандартная цепь сброса при подаче питания (низким или высоким уровнем в зависимости от того какой дисплей);
            Аналоговая часть для MQ-7 — возможно просто резисторный делитель;
            Ключ — тоже вроде в некоторые MQ-7 уже встроен.


  1. zodiak
    07.03.2019 09:00

    Еще бы коробочку симпатичную…


    1. Cadil_TM Автор
      07.03.2019 09:41

      «Еще бы коробочку симпатичную…»
      До коробочки еще далеко.
      Еще надо решить проблему энергозатратности. Уменьшение частоты опроса датчиков, сна тут, похоже, не спасет.


  1. r00tGER
    07.03.2019 09:10

    не забудьте подправить встроенный драйвер I2C

    Подскажите, а зачем эта «заплатка»?


    1. Cadil_TM Автор
      07.03.2019 09:45

      «Подскажите, а зачем эта «заплатка»?»
      Кликните на слово тут в «Инструкции — тут.», спросите у автора доработки. Мне «заплатка» помогла.


      1. r00tGER
        07.03.2019 10:49

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

        Использую «коробочную» реализацию I2C (на шине: DS3231, BME280, PCF8574, ADS1115), и пока не сталкивался с проблемами. Аптайм более года, и было несколько апдейтов с пересборкой на разных версиях SDK.


        1. Cadil_TM Автор
          07.03.2019 13:35

          «Использую «коробочную» реализацию I2C (на шине: DS3231, BME280, PCF8574, ADS1115), и пока не сталкивался с проблемами. Аптайм более года, и было несколько апдейтов с пересборкой на разных версиях SDK.»
          Спасибо! Я вернусь к своей проблеме дефицита цифровых пинов, учитывая ваш опыт.


  1. avs24rus
    07.03.2019 11:00

    он в качестве экрана использовал планшет в рамке с 3D печатью

    Да нет же, нету никакого планшета.
    Обычный 7" дисплей, подключен к Arduino Mega, через адаптер. Для связи с внешним миром использован nRF24, вставленный в туже Мегу через самопальный адаптер. Все это долго время пребывало в голом виде, и только в ноябре прошлого года я заказал изготовить корпус на 3Д принтере. Получилось вполне пристойно. Если интересно, могу выложить фото снаружи и внутри.


    1. Cadil_TM Автор
      07.03.2019 13:29

      «Да нет же, нету никакого планшета. ...»
      Это ваш комментарий — habr.com/ru/post/440978/#comment_19780298?!
      Удивил поход — дисплей в качестве экрана устройства, детали не важны.


  1. vladkorotnev
    07.03.2019 11:10

    Эх, а я по названию уж думал, что вы монитор воздуха и настройку часов запихнули в настенные стрелочные, а не софтварные на экране и без корпуса :-)


  1. geisha
    07.03.2019 14:33

    Мой традиционный вопрос автору: зачем колхозил готовые модули?


    1. Cadil_TM Автор
      07.03.2019 15:08

      «Мой традиционный вопрос автору: зачем колхозил готовые модули?»
      Я теряюсь с ответом на ваш вопрос. Предлагаете, сидя на кухне, разработать самому все то, что можно купить на AliExpress'e?
      Утонченная у вас реклама. Успехов в продвижении продукта.


      1. VJean
        07.03.2019 16:42

        Вы ссылку проверяли? Тот же али, только варианты esp с корпусом и экраном.


        1. Cadil_TM Автор
          07.03.2019 19:15

    1. Cadil_TM Автор
      07.03.2019 19:22

      Прошу прощения за иронию в предыдущем моем комментарии. Бегло взглянул на картинки, не читая описаний. Фантастика!
      Еще раз — успехов!


  1. Andronas
    07.03.2019 15:34

    Скажите, за передачу данных наружу отвечает void sendToBlynk()? Хочется например передавать по http данные куда то на свой сервис.


    1. Cadil_TM Автор
      07.03.2019 16:23

      «Скажите, за передачу данных наружу отвечает void sendToBlynk()? Хочется например передавать по http данные куда то на свой сервис.»
      Функция sendToBlynk() отравляет данные на сервер Blynk'а. Как отправить данные на другой сервис? — у меня нет ответа. Извините.


      1. Andronas
        07.03.2019 20:08

        Вы не поняли, я просто хотел узнать — данные отправляются куда либо только из этой функции? Т.е. нет других участков кода, откуда отправляются данные наружу?
        Реализовать отправку куда то в другое место это дело несложное.


  1. Cadil_TM Автор
    07.03.2019 20:52

    «Вы не поняли, я просто хотел узнать — данные отправляются куда либо только из этой функции? Т.е. нет других участков кода, откуда отправляются данные наружу?»
    Посмотрите mailer() — тут тоже отравляются данные наружу.


  1. GREGOR_812
    08.03.2019 18:41
    +1

    На куске схемы с транзистором 2N2222A нужно поставить ограничивающий резистор в базу, иначе он будет помирать с завидной регулярностью.

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


    Соре за негатив, но разработки подобного уровня не дотягивают до звания «профессиональные». Их нельзя тиражировать, т.к. они не до конца продуманы и не имеют сборочного комплекта, т.е. платы, на которую можно монтировать компоненты (или готовая смонтированная, которую нужно собрать в корпус).

    Удачи


    1. Cadil_TM Автор
      08.03.2019 20:58

      «На куске схемы с транзистором 2N2222A нужно поставить ограничивающий резистор в базу, иначе он будет помирать с завидной регулярностью.»
      Спасибо! Сделал.

      «Соре за негатив, но разработки подобного уровня не дотягивают до звания «профессиональные». Их нельзя тиражировать, т.к. они не до конца продуманы и не имеют сборочного комплекта, т.е. платы, на которую можно монтировать компоненты (или готовая смонтированная, которую нужно собрать в корпус).»
      Не отрицаю. Читаем выше: "… немного приблизился к статусу профессиональной разработки".


  1. Bobnecat
    08.03.2019 20:37

    На вашем месте я бы сменил DHT-22 на BME, или хотя-бы на более дешевые BMP от Bosch. По моему опыту DHT датчики совершенно не адекватные в плане измерение влажности и не очень долговечные.


    1. Cadil_TM Автор
      08.03.2019 21:33

      «На вашем месте я бы сменил DHT-22 на BME, или хотя-бы на более дешевые BMP от Bosch. По моему опыту DHT датчики совершенно не адекватные в плане измерение влажности и не очень долговечные.»
      Я вожусь с датчиком DHT22 около двух лет (после того, как благополучно сжег дорогой BME280). У меня свой опыт:
      Разница в показаниях влажности между DHT22 и метеостанцией **CROSS* не более 10%. Сейчас, когда набираю эти строки 52% и 49%.
      Неадекватность проявилась только один раз: датчик на двое суток зашкалил на отметке 99%, скорее всего, после моих издевательств.
      У датчика есть «плюс» — он рассчитывает индекс жары. Насколько этот «плюс» актуальный — посмотрим летом…


      1. Bobnecat
        08.03.2019 23:35

        Для домашнего применения сойдет, но для улицы я бы держался от них подальше. Мой опыт использования DHT22 был как в суровом морозном климате (норвегия) так и в удушающи влажном климате. В обоих влажность не показывала реальные данные, либо сильно завышенные, либо сильно заниженные. При морозе влажность частенько зашкаливала, а в удушливом климате хьюстона наоборот показывал 40-60% летом в то время как реальная влажность была далеко за 90% при температуре за 30. В обоих условиях DHT не жил больше 2х месяцев при эксплуатации на улице.
        Я честно говоря не понимаю зачем вам измерять индекс жары в помещении (heat index — как я полагаю) параметр, имеющий значение при температурах намного выше тех что бывают в обитаемых помещениях. Например стандартный расчет который имеет какое либо значение для человека по Heat Index'у начинается от 27ми градусов. Я склонен думать что BME умеет отображать этот параметр. Как минимум его можно вывести математически зная температуру и влажность.
        Глянул на алиэкспресс, на данный момент BME280 стоит дешевле DHT22. Так же не совсем уверен как вы могли его спалить на логике 3.3в на которой работает ESP8266, особенно если подключали по I2C. Впрочем самоделка ваша, вам и решать что на нее ставить. Я всего лишь написал что-бы поделиться опытом.


        1. Cadil_TM Автор
          09.03.2019 01:30

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

          «В обоих условиях DHT не жил больше 2х месяцев при эксплуатации на улице.»
          Жаль, что в то время с интервалом два месяца вы только набирали статистику, меняя приборы с датчиком или сам датчик DHT22. Возможно задумайся раньше, у вас на сегодня была положительная (или другая) статистика по другому датчику, например, тому же ВМЕ280.

          «Я честно говоря не понимаю зачем вам измерять индекс жары в помещении»
          В северной стране Канада считают по-другому — там метеорологи публикуют этот индекс, в США тоже есть его аналог — индекс тепла. Что касается меня, то планы есть, но пока не буду их обнародовать, не проверив на практике.

          «Я склонен думать что BME умеет отображать этот параметр.»
          У меня тоже много догадок.

          «Я всего лишь написал что-бы поделиться опытом.»
          Спасибо!