Володимир Бойчук

Два в одному: програмований по Wi-Fi монітор якості повітря та стрілочний годинник

Свого часу мені сподобався монітор якості повітря з публікації Сергія Сильнова «Компактний монітор домашнього повітря (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
Інші дрібниці 2,00
Всього: 29,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) DHT22, DATA;
D4 (GPIO 2) displ_1.44, A0
D5 (GPIO 14) displ_1.44, SCK
D6 (GPIO 12) displ_1.44, RST
D7 (GPIO 13) displ_1.44, SDA
D8 (GPIO 15) GND
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

Якщо в модулі годинника ви використовуєте батарейку замість акумулятора, то не забудьте розірвати ланцюг заряду акумулятора, інакше батарейка роздується через кілька тижнів роботи під напругою. Навіть з автономним живленням годинника без синхронізації по Інтернету точність ходу 2 сек/рік вам забезпечена.

Напругу живлення монітора 5В можна подати з USB-порту комп'ютера стандартним кабелем USB - microUSB на модуль NodeMCU esp12.

Скетч монітора для завантаження в ESP8266 знаходиться під спойлером. Щоб розгорнути спойлер - натисніть лівою кнопкою мишки на напис "скетч монітора" нижче.

Про всяк випадок, не полінуйтеся підправити вбудований драйвер I2C для ядра Arduino ESP8266. Інструкції – тут.

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

  1. /*
  2. Два в одному: програмований по Wi-Fi монітор якості повітря та стрілочний годинник
  3. http://skorovoda.in.ua/monitor/
  4.  */
  5.  
  6. #include <FS.h>
  7. #include <Arduino.h>
  8. #include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino
  9.  
  10. // Wifi Manager
  11. #include <DNSServer.h>
  12. #include <ESP8266WebServer.h>
  13. #include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager
  14.  
  15. //OLED
  16. #include <SPI.h>
  17. #include <Adafruit_GFX.h>
  18. #include <TFT_ILI9163C.h>
  19.  
  20. //clock
  21. #include <pgmspace.h>
  22. #include <TimeLib.h>
  23. #include <ESP8266WiFi.h>
  24. #include <WiFiUdp.h>
  25. #include <Wire.h>
  26. #include <RtcDS3231.h>
  27. RtcDS3231<TwoWire> Rtc(Wire);
  28. #define countof(a) (sizeof(a) / sizeof(a[0]))
  29. //e-mail
  30. #include <ESP8266WiFiMulti.h>
  31. #include <ESP8266HTTPClient.h>
  32. #define USE_SERIAL Serial
  33. ESP8266WiFiMulti WiFiMulti;
  34. //e-mail, address
  35. char address[64] {"e-mail"};
  36.  
  37. // HTTP requests
  38. #include <ESP8266HTTPClient.h>
  39.  
  40. // OTA updates
  41. #include <ESP8266httpUpdate.h>
  42. // Blynk
  43. #include <BlynkSimpleEsp8266.h>
  44.  
  45. // Debounce
  46. #include <Bounce2.h> //https://github.com/thomasfredericks/Bounce2
  47.  
  48. // JSON
  49. #include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson
  50.  
  51. // Debounce interval in ms
  52. #define DEBOUNCE_INTERVAL 10
  53.  
  54. Bounce hwReset {Bounce()};
  55.  
  56. // Humidity/Temperature
  57. #include <DHT.h>
  58. #define DHTPIN 0    //D3 gpio0, DHT22 DATA
  59. #define DHTTYPE DHT22     // DHT 22
  60. DHT dht(DHTPIN, DHTTYPE);
  61.  
  62. // Blynk token
  63. char blynk_token[33] {"Blynk token"};
  64.  
  65. // Setup Wifi connection
  66. WiFiManager wifiManager;
  67.  
  68. // Network credentials
  69. String ssid { "am180206" };
  70. String pass { "vb654321" };
  71.  
  72. //flag for saving data
  73. bool shouldSaveConfig = false;
  74.  
  75. // Sensors data
  76. float  t {-100}, t_old{-100};
  77. float  hic {-1}, hic_old{-1};
  78. int h {-1}, h_old{-1};
  79. int co2 {-1}, co2_old{-1};
  80. char Tmn[5]{}, Tmx[5]{}, Hmn[5]{}, Cmx[7]{}, tZ[5]{}, timeSW[4]{}, formFS[]{"0"}; //пороговые значения t, h и co2, час. пояс
  81. float  Tmin, Tmax, Hmin, Cmax, tZone, timeSummerWinter, formatingFS;
  82. float trp = 0;
  83. int crbn, bl, ml=18000;
  84. int md; //режим работы: 1 - онлайн, 2 - автономный
  85. int blnk;
  86.  
  87. // Color definitions
  88. #define  BLACK   0x0000
  89. #define BLUE    0x001F
  90. #define RED     0xF800
  91. #define GREEN   0x07E0
  92. #define CYAN    0x07FF
  93. #define MAGENTA 0xF81F
  94. #define YELLOW  0xFFE0  
  95. #define WHITE   0xFFFF
  96. #define GRAY    0x9999
  97.  
  98. #define __CS 16   //D0(gpio16)- CS(display)
  99. #define __DC 2    //D4 gpio2 - AO(display)
  100. #define __RST 12   // D6  gpio12 - RESET(display) 
  101.  
  102.  //char datestring[20];
  103.  char time_r[9];
  104.  char date_r[12];
  105.  
  106. //analog clock
  107. uint16_t ccenterx = 64,ccentery = 70;//center x,y of the clock clock
  108. const uint16_t cradius = 40;//radius of the clock
  109. const float scosConst = 0.0174532925;
  110. float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;
  111. float sdeg=0, mdeg=0, hdeg=0;
  112. uint16_t osx,osy,omx,omy,ohx,ohy;
  113. uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
  114. //uint32_t targetTime = 0;// for next 1 second timeout
  115. uint8_t hh,mm,ss;  //containers for current time
  116.  
  117. TFT_ILI9163C display = TFT_ILI9163C(__CS, __DC, __RST);
  118.  
  119. String utf8(String source)
  120. {
  121.   int i,k;
  122.   String target;
  123.   unsigned char n;
  124.   char m[2] = { '0''\0' };
  125.  
  126.   k = source.length(); i = 0;
  127.  
  128.   while (< k) {
  129.     n = source[i]; i++;
  130.  
  131.     if (>= 0xC0) {
  132.       switch (n) {
  133.         case 0xD0: {
  134.           n = source[i]; i++;
  135.           if (== 0x81) { n = 0xA8; break; }
  136.           if (>= 0x90 && n <= 0xBF) n = n + 0x30;
  137.           break;
  138.         }
  139.         case 0xD1: {
  140.           n = source[i]; i++;
  141.           if (== 0x91) { n = 0xB8; break; }
  142.           if (>= 0x80 && n <= 0x8F) n = n + 0x70;
  143.           break;
  144.         }
  145.       }
  146.     }
  147.     m[0] = n; target = target + String(m);
  148.   }
  149. return target;
  150. }
  151.  
  152. // NTP Servers:
  153. //static const char ntpServerName[] = "us.pool.ntp.org";
  154. static const char ntpServerName[] = "time.nist.gov";
  155.  
  156. //const int timeZone = 2;     // Вильнюс, Киев, Рига, София, Таллин, Хельсинки
  157. //const int timeSummer = 1;
  158.  
  159. WiFiUDP Udp;
  160. unsigned int localPort = 2390;  // local port to listen for UDP packets
  161.  
  162. time_t getNtpTime();
  163. void digitalClockDisplay();
  164. void printDigits(int digits);
  165. void sendNTPpacket(IPAddress &address);
  166.  
  167. void readCO2(){
  168. #define mySerial Serial
  169. static byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79}; //команда чтения
  170. byte response[9];
  171. byte crc = 0;
  172.   while (mySerial.available())mySerial.read();//очистка буфера UART перед запросом 
  173.   memset(response, 09);// очистка ответа
  174.   mySerial.write(cmd,9);// запрос на содержание CO2
  175.   mySerial.readBytes(response, 9);//читаем 9 байт ответа сенсора
  176.   //расчет контрольной суммы
  177.   crc = 0;
  178.   for (int i = 1; i <= 7; i++)
  179.   {
  180.     crc += response[i];
  181.   }
  182.   crc = ((~crc)+1);
  183.   {
  184.   //проверка CRC
  185.   if ( !(response[0] == 0xFF && response[1] == 0x86 && response[8] == crc) ) 
  186.   {
  187.     Serial.println("CRC error");
  188.   } else 
  189.       {
  190.        //расчет значеия CO2
  191.        co2 = (((unsigned int) response[2])<<8) + response[3];
  192.        Serial.println("CO2: " + String(co2) + "ppm");
  193.         }
  194.   }
  195. }
  196.  
  197. void sendMeasurements() {
  198. float  t1 {-100}, hic1 {-1};
  199. float h1 {-1};
  200.         // Temperature
  201.         t1 = dht.readTemperature(); 
  202.        if ((t1 > -1) and (t1 < 100)) t = t1;
  203.        Serial.println("T: " + String(t) + "*C");
  204.  
  205.         // Humidity
  206.         h1 = dht.readHumidity(); 
  207.         if ((h1 > -1) and (h1 < 100))  h = h1;
  208.         Serial.println("H: " + String(h) + "%");
  209.  
  210.         // Humindex
  211.         hic1 = dht.computeHeatIndex(t, h, false);        
  212.         if (>= 21.0) hic = hic1;
  213.         else  hic = t;
  214.         Serial.println("Ti: "+String(hic)+"*C"); 
  215.  
  216.         // CO2
  217.         crbn++;
  218.         if (crbn > 110)
  219.              {readCO2();  
  220.               crbn = 0;
  221.               Serial.println("CO2: " + String(co2) + "ppm");
  222.              }
  223. }
  224.  
  225. void drawConnectionDetails() {
  226.         display.clearScreen();
  227.         display.setTextSize(1);
  228.         display.setCursor(12,24);
  229.         display.setTextColor(WHITE);
  230.         display.println(utf8("Connect to WiFi:"));
  231.         display.setCursor(12,36);
  232.         display.println(utf8("net: " + String(ssid)));
  233.         display.setCursor(12,48);
  234.         display.println(utf8("pass: " + String(pass)));          
  235.         display.setCursor(12,60);
  236.         display.println(utf8("Open browser:"));          
  237.         display.setCursor(12,72);
  238.         display.println(utf8("http://192.168.4.1"));
  239.         display.setCursor(2,84);
  240.         display.setTextColor(RED);
  241.         display.println(utf8(" Enter your personal     information!"));
  242. }        
  243.  
  244. void digitalClockDisplay()
  245. {
  246.   // digital clock display of the time
  247.   Serial.print(hour());
  248.   printDigits(minute());
  249.   printDigits(second());
  250.   Serial.print(" ");
  251.   Serial.print(day());
  252.   Serial.print(".");
  253.   Serial.print(month());
  254.   Serial.print(".");
  255.   Serial.print(year());
  256.   Serial.println(); 
  257. }
  258.  
  259. void printDigits(int digits)
  260. {
  261.   // utility for digital clock display: prints preceding colon and leading 0
  262.   Serial.print(":");
  263.   if (digits < 10)
  264.     Serial.print('0');
  265.   Serial.print(digits);
  266. }
  267.  
  268. // NTP code
  269. const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
  270. byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets
  271.  
  272.   time_t getNtpTime() {
  273.   int tZoneI, timeSummerWinterI;    
  274.   tZoneI = (int)tZone;  
  275.   timeSummerWinterI = (int)timeSummerWinterI;    
  276.   IPAddress ntpServerIP; // NTP server's ip address
  277.  
  278.   while (Udp.parsePacket() > 0) ; // discard any previously received packets
  279.   Serial.println("Transmit NTP Request");
  280.   // get a random server from the pool
  281.   WiFi.hostByName(ntpServerName, ntpServerIP);
  282.   Serial.print(ntpServerName);
  283.   Serial.print(": ");
  284.   Serial.println(ntpServerIP);
  285.   sendNTPpacket(ntpServerIP);
  286.   uint32_t beginWait = millis();
  287.   while (millis() - beginWait < 1500) {
  288.     int size = Udp.parsePacket();
  289.     if (size >= NTP_PACKET_SIZE) {
  290.       Serial.println("Receive NTP Response");
  291.       Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
  292.       unsigned long secsSince1900;
  293.       // convert four bytes starting at location 40 to a long integer
  294.       secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
  295.       secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
  296.       secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
  297.       secsSince1900 |= (unsigned long)packetBuffer[43];
  298.       return secsSince1900 - 2208988800UL + tZoneI * SECS_PER_HOUR + timeSummerWinterI * SECS_PER_HOUR;
  299.     }
  300.   }
  301.   Serial.println("No NTP Response :-(");
  302.   return 0; // return 0 if unable to get the time
  303. }
  304.  
  305. // send an NTP request to the time server at the given address
  306. void sendNTPpacket(IPAddress &address)
  307. {
  308.   // set all bytes in the buffer to 0
  309.   memset(packetBuffer, 0, NTP_PACKET_SIZE);
  310.   // Initialize values needed to form NTP request
  311.   // (see URL above for details on the packets)
  312.   packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  313.   packetBuffer[1] = 0;     // Stratum, or type of clock
  314.   packetBuffer[2] = 6;     // Polling Interval
  315.   packetBuffer[3] = 0xEC;  // Peer Clock Precision
  316.   // 8 bytes of zero for Root Delay & Root Dispersion
  317.   packetBuffer[12] = 49;
  318.   packetBuffer[13] = 0x4E;
  319.   packetBuffer[14] = 49;
  320.   packetBuffer[15] = 52;
  321.   // all NTP fields have been given values, now
  322.   // you can send a packet requesting a timestamp:
  323.   Udp.beginPacket(address, 123); //NTP requests are to port 123
  324.   Udp.write(packetBuffer, NTP_PACKET_SIZE);
  325.   Udp.endPacket();
  326. }
  327.  
  328. void draw(){
  329. //temperature  
  330.   display.setTextSize(1);
  331.   display.setCursor(1,6);
  332.   display.setTextColor(CYAN);
  333.   display.println(utf8("T:             CO2:"));
  334.  
  335.   String t_p;
  336.          t_p = String(t); 
  337.          char t_p_m [12];
  338.         t_p.toCharArray(t_p_m, 5);
  339.  
  340.  if (!= t_old) { 
  341.   display.fillRect(1,15,48,18,BLACK);
  342.   display.setTextSize(2);
  343.   display.setCursor(1,15);
  344.   display.setTextColor(GREEN);
  345.   if(< Tmin) display.setTextColor(RED);
  346.   if(> Tmax) display.setTextColor(RED);
  347.  if ((> -100) and (< 100)) display.println(utf8(String(t_p_m)));
  348.   else display.println(utf8("----"));}
  349.  
  350. //heat index 
  351.   display.setTextSize(1);
  352.   display.setCursor(2,98);
  353.   display.setTextColor(CYAN);
  354.   display.println(utf8("H:               Ti:"));
  355.  
  356.   String hic_p;
  357.          hic_p = String(hic); 
  358.          char hic_p_m [12];
  359.         hic_p.toCharArray(t_p_m, 5);
  360.  
  361.  if (hic != hic_old) { 
  362.   display.fillRect(80,108,48,18,BLACK);
  363.   display.setTextSize(2);
  364.   display.setCursor(80,108);
  365.   display.setTextColor(GREEN);
  366.  // if(t < Tmin) display.setTextColor(RED);
  367.   if(hic > 27.0) display.setTextColor(YELLOW);
  368.   if(hic > 31.0) display.setTextColor(RED);
  369.  if ((hic > 0) and (hic < 100)) display.println(utf8(String(t_p_m)));
  370.   else display.println(utf8("----"));}
  371.  
  372. //CO2
  373.  if (co2 != co2_old) {
  374.   display.fillRect(80,15,48,18,BLACK);
  375.   display.setTextSize(2);
  376.   display.setCursor(80,15);
  377.   display.setTextColor(GREEN);
  378.   if (co2 > Cmax) display.setTextColor(RED);
  379.   if (co2 > 600) display.setTextColor(CYAN);  
  380.   if ((co2 > -1)  and (co2 <= 2000)) display.println(utf8(String(co2))); else display.println(utf8("---"));
  381.  }
  382.  
  383. //humidity
  384. if (!= h_old) {
  385.   display.fillRect(1,108,49,18,BLACK);
  386.   display.setTextSize(2);
  387.   display.setCursor(1,108);
  388.   display.setTextColor(GREEN);
  389.   if (< Hmin) display.setTextColor(RED);
  390.   if (> 60) display.setTextColor(RED);
  391.   if ((> -1) and (< 100)) display.println(utf8(String(h))); else display.println(utf8("--"));
  392.  }
  393.  
  394. //date    
  395.  if (hh==0) display.fillRect(28,1,60,10,BLACK);
  396.   display.setCursor(28,1);
  397.   display.setTextSize(1);
  398.   display.setTextColor(CYAN);
  399.   display.println(utf8(date_r)); 
  400.  
  401. //OFFLINE
  402. if (md == 2)
  403. {  
  404.   display.fillRect(106,44,18,8,RED);
  405.   display.setCursor(106,44);
  406.   display.setTextSize(1);
  407.   display.setTextColor(CYAN);
  408.   display.println(" A");  
  409.   }
  410.  
  411. //OFF BLYNK
  412. if (blnk == 1)
  413. { display.fillRect(106,44,18,8,RED);
  414.   display.setCursor(106,44);
  415.   display.setTextSize(1);
  416.   display.setTextColor(CYAN);
  417.   display.println(" B");  
  418.   }
  419. }
  420.  
  421. void synchronClockA() {
  422.   Rtc.Begin();
  423.   Serial.print("IP number assigned by DHCP is ");
  424.   Serial.println(WiFi.localIP());
  425.   Serial.println("Starting UDP");
  426.   Udp.begin(localPort);
  427.   Serial.print("Local port: ");
  428.   Serial.println(Udp.localPort());
  429.   Serial.println("waiting for sync");
  430.   setSyncProvider(getNtpTime);
  431.   //setSyncInterval(300);
  432.   if(timeStatus() != timeNotSet){
  433.     digitalClockDisplay();
  434.     Serial.println("here is another way to set rtc");
  435.       time_t t = now();
  436.       char date_0[12];
  437.       snprintf_P(date_0, countof(date_0), PSTR("%s %02u %04u"), monthShortStr(month(t)), day(t), year(t));
  438.       Serial.println(date_0);
  439.       char time_0[9];
  440.       snprintf_P(time_0, countof(time_0), PSTR("%02u:%02u:%02u"), hour(t), minute(t), second(t));
  441.       Serial.println(time_0);
  442.       Serial.println("Now its time to set up rtc");
  443.       RtcDateTime compiled = RtcDateTime(date_0, time_0);
  444.  //      printDateTime(compiled);
  445.        Serial.println("");
  446.  
  447.         if (!Rtc.IsDateTimeValid()) 
  448.     {
  449.         // Common Cuases:
  450.         //    1) first time you ran and the device wasn't running yet
  451.         //    2) the battery on the device is low or even missing
  452.  
  453.         Serial.println("RTC lost confidence in the DateTime!");
  454.  
  455.         // following line sets the RTC to the date & time this sketch was compiled
  456.         // it will also reset the valid flag internally unless the Rtc device is
  457.         // having an issue
  458.  
  459.     }
  460.      Rtc.SetDateTime(compiled);
  461.      RtcDateTime now = Rtc.GetDateTime();
  462.     if (now < compiled) 
  463.     {
  464.         Serial.println("RTC is older than compile time!  (Updating DateTime)");
  465.         Rtc.SetDateTime(compiled);
  466.     }
  467.     else if (now > compiled) 
  468.     {
  469.         Serial.println("RTC is newer than compile time. (this is expected)");
  470.     }
  471.     else if (now == compiled) 
  472.     {
  473.         Serial.println("RTC is the same as compile time! (not expected but all is fine)");
  474.     }
  475.  
  476.     // never assume the Rtc was last configured by you, so
  477.     // just clear them to your needed state
  478.     Rtc.Enable32kHzPin(false);
  479.     Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); 
  480.   }
  481. }
  482.  
  483. void synchronClock() {
  484.   Rtc.Begin();
  485.  // WiFi.begin(lnet, key);
  486.    wifiManager.autoConnect(ssid.c_str(), pass.c_str());
  487.   while (WiFi.status() != WL_CONNECTED) {
  488.     delay(500);
  489.     Serial.print(".");
  490.   }
  491.   Serial.println(" ");
  492.   Serial.print("IP number assigned by DHCP is ");
  493.   Serial.println(WiFi.localIP());
  494.   Serial.println("Starting UDP");
  495.   Udp.begin(localPort);
  496.   Serial.print("Local port: ");
  497.   Serial.println(Udp.localPort());
  498.   Serial.println("waiting for sync");
  499.   setSyncProvider(getNtpTime);
  500.  
  501.   if(timeStatus() != timeNotSet){
  502.     digitalClockDisplay();
  503.     Serial.println("here is another way to set rtc");
  504.       time_t t = now();
  505.       char date_0[12];
  506.       snprintf_P(date_0, countof(date_0), PSTR("%s %02u %04u"), monthShortStr(month(t)), day(t), year(t));
  507.       Serial.println(date_0);
  508.       char time_0[9];
  509.       snprintf_P(time_0, countof(time_0), PSTR("%02u:%02u:%02u"), hour(t), minute(t), second(t));
  510.       Serial.println(time_0);
  511.       Serial.println("Now its time to set up rtc");
  512.       RtcDateTime compiled = RtcDateTime(date_0, time_0);
  513.  
  514.        Serial.println("");
  515.  
  516.         if (!Rtc.IsDateTimeValid()) 
  517.     {
  518.         // Common Cuases:
  519.         //    1) first time you ran and the device wasn't running yet
  520.         //    2) the battery on the device is low or even missing
  521.  
  522.         Serial.println("RTC lost confidence in the DateTime!");
  523.  
  524.         // following line sets the RTC to the date & time this sketch was compiled
  525.         // it will also reset the valid flag internally unless the Rtc device is
  526.         // having an issue
  527.  
  528.     }
  529.      Rtc.SetDateTime(compiled);
  530.      RtcDateTime now = Rtc.GetDateTime();
  531.     if (now < compiled) 
  532.     {
  533.         Serial.println("RTC is older than compile time!  (Updating DateTime)");
  534.         Rtc.SetDateTime(compiled);
  535.     }
  536.     else if (now > compiled) 
  537.     {
  538.         Serial.println("RTC is newer than compile time. (this is expected)");
  539.     }
  540.     else if (now == compiled) 
  541.     {
  542.         Serial.println("RTC is the same as compile time! (not expected but all is fine)");
  543.     }
  544.  
  545.     // never assume the Rtc was last configured by you, so
  546.     // just clear them to your needed state
  547.     Rtc.Enable32kHzPin(false);
  548.     Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); 
  549. }
  550. }
  551.  
  552. void Clock(){
  553.     RtcDateTime now = Rtc.GetDateTime();
  554.     //Print RTC time to Serial Monitor
  555.    hh = now.Hour();
  556.    mm = now.Minute(); 
  557.    ss = now.Second();
  558.  
  559.   sprintf(date_r, "%d.%d.%d", now.Day(), now.Month(), now.Year());
  560.   if  (mm < 10) sprintf(time_r, "%d:0%d", hh, mm);
  561.   else sprintf(time_r, "%d:%d", hh, mm);
  562.  
  563.    Serial.println(date_r);
  564.    Serial.println(time_r);
  565.    }
  566.  
  567. //analog
  568. void drawClockFace(){
  569.   display.fillCircle(ccenterx, ccentery, cradius, BLUE);
  570.   display.fillCircle(ccenterx, ccentery, cradius-4, BLACK);
  571.   // Draw 12 lines
  572.   for(int i = 0; i<360; i+= 30) {
  573.     sx = cos((i-90)*scosConst);
  574.     sy = sin((i-90)*scosConst);
  575.     x0 = sx*(cradius)+ccenterx;
  576.     yy0 = sy*(cradius)+ccentery;
  577.     x1 = sx*(cradius-8)+ccenterx;
  578.     yy1 = sy*(cradius-8)+ccentery;
  579.     display.drawLine(x0, yy0, x1, yy1, 0x0377);
  580.   }
  581.   // Draw 4 lines
  582.   for(int i = 0; i<360; i+= 90) {
  583.     sx = cos((i-90)*scosConst);
  584.     sy = sin((i-90)*scosConst);
  585.     x0 = sx*(cradius+6)+ccenterx;
  586.     yy0 = sy*(cradius+6)+ccentery;
  587.     x1 = sx*(cradius-11)+ccenterx;
  588.     yy1 = sy*(cradius-11)+ccentery;
  589.     display.drawLine(x0, yy0, x1, yy1, 0x0377);
  590.   }  
  591. }
  592. //analog
  593. static uint8_t conv2d(const char* p) {
  594.   uint8_t v = 0;
  595.   if ('0' <= *&& *<= '9') v = *- '0';
  596.   return 10 * v + *++- '0';
  597. }
  598.  
  599. //analog  
  600. void drawClockHands(uint8_t h,uint8_t m,uint8_t s){
  601.   // Pre-compute hand degrees, x & y coords for a fast screen update
  602.   sdeg = s * 6;                  // 0-59 -> 0-354
  603.   mdeg = m * 6 + sdeg * 0.01666667;  // 0-59 -> 0-360 - includes seconds
  604.   hdeg = h * 30 + mdeg * 0.0833333;  // 0-11 -> 0-360 - includes minutes and seconds
  605.   hx = cos((hdeg-90)*scosConst);    
  606.   hy = sin((hdeg-90)*scosConst);
  607.   mx = cos((mdeg-90)*scosConst);    
  608.   my = sin((mdeg-90)*scosConst);
  609.   sx = cos((sdeg-90)*scosConst);    
  610.   sy = sin((sdeg-90)*scosConst);
  611.  
  612.   // Erase just old hand positions
  613.   display.drawLine(ohx, ohy, ccenterx+1, ccentery+1, BLACK);  
  614.   display.drawLine(omx, omy, ccenterx+1, ccentery+1, BLACK);  
  615.   display.drawLine(osx, osy, ccenterx+1, ccentery+1, BLACK);
  616.   // Draw new hand positions  
  617.   display.drawLine(hx*(cradius-20)+ccenterx+1, hy*(cradius-20)+ccentery+1, ccenterx+1, ccentery+1, WHITE);
  618.   display.drawLine(mx*(cradius-8)+ccenterx+1, my*(cradius-8)+ccentery+1, ccenterx+1, ccentery+1, WHITE);
  619.   display.drawLine(sx*(cradius-8)+ccenterx+1, sy*(cradius-8)+ccentery+1, ccenterx+1, ccentery+1, RED);
  620.   display.fillCircle(ccenterx+1, ccentery+13, RED);
  621.  
  622.   // Update old x&y coords
  623.   osx = sx*(cradius-8)+ccenterx+1;
  624.   osy = sy*(cradius-8)+ccentery+1;
  625.   omx = mx*(cradius-8)+ccenterx+1;
  626.   omy = my*(cradius-8)+ccentery+1;
  627.   ohx = hx*(cradius-20)+ccenterx+1;
  628.   ohy = hy*(cradius-20)+ccentery+1;
  629. }
  630. void  FaceClock(){
  631.  display.clearScreen();
  632.  display.setTextColor(WHITE, BLACK);
  633.   osx = ccenterx;
  634.   osy = ccentery;
  635.   omx = ccenterx;
  636.   omy = ccentery;
  637.   ohx = ccenterx;
  638.   ohy = ccentery;
  639.   drawClockFace();// Draw clock face  
  640. }
  641.  
  642. void drawSynchron() {
  643.         display.clearScreen();
  644.         display.setTextSize(2);
  645.         display.setCursor(2,48);
  646.         display.setTextColor(WHITE);
  647.         display.println(utf8("      Clock"));
  648.         display.setTextSize(1);
  649.         display.setCursor(2,68);
  650.         display.setTextColor(WHITE);
  651.         display.println(utf8("synchronization..."));
  652.       } 
  653.  
  654. void drawWiFi() {
  655.         display.clearScreen();
  656.         display.setTextSize(2);
  657.         display.setCursor(2,48);
  658.         display.setTextColor(RED);
  659.         display.println(utf8("Connection to Wi-Fi"));
  660.       }
  661.  
  662. void drawBlynk() {
  663.         display.clearScreen();
  664.         display.setTextSize(2);
  665.         display.setCursor(2,48);
  666.         display.setTextColor(RED);
  667.         display.println(utf8("Connection to Blynk"));
  668. }
  669.  
  670. void mailer() {
  671.      // wait for WiFi connection
  672.     if((WiFiMulti.run() == WL_CONNECTED)) {
  673.  
  674.         HTTPClient http;
  675.  
  676.         Serial.print("[HTTP] begin...\n");
  677.  
  678.         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()));
  679.  
  680.         Serial.print("[HTTP] GET...\n");
  681.         // start connection and send HTTP header
  682.         int httpCode = http.GET();
  683.  
  684.         // httpCode will be negative on error
  685.         if(httpCode > 0) {
  686.             // HTTP header has been send and Server response header has been handled
  687.             Serial.printf("[HTTP] GET... code: %d\n", httpCode);
  688.  
  689.             // file found at server
  690.             if(httpCode == HTTP_CODE_OK) {
  691.                 String payload = http.getString();
  692.                 Serial.println(payload);
  693.             }
  694.         } else {
  695.             Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
  696.         }
  697.         http.end();
  698.     }   
  699. }
  700.  
  701. //callback notifying the need to save config
  702. void saveConfigCallback() {
  703.         Serial.println("Should save config");
  704.         shouldSaveConfig = true;
  705. }
  706. void factoryReset() {
  707.         Serial.println("Resetting to factory settings");
  708.         wifiManager.resetSettings();
  709.         SPIFFS.format();
  710.         ESP.reset();
  711. }
  712. void printString(String str) {
  713.         Serial.println(str);
  714. }
  715.  
  716. bool loadConfigS() {
  717.         Blynk.config(address);  
  718.         Serial.print("e-mail: ");  
  719.         Serial.println(  address );   
  720.  
  721.         Blynk.config(tZ);  
  722.         Serial.print("T_Zone: ");  
  723.         Serial.println(  tZ );
  724.  
  725.         Blynk.config(Tmx);  
  726.         Serial.print("T max: ");  
  727.         Serial.println(  Tmx );   
  728.  
  729.         Blynk.config(Cmx);  
  730.         Serial.print("CO2 max: ");  
  731.         Serial.println(  Cmx );   
  732.  
  733.         Blynk.config(Tmn);  
  734.         Serial.print("T min: ");  
  735.         Serial.println(  Tmn );   
  736.  
  737.         Blynk.config(Hmn);  
  738.         Serial.print("H min: ");  
  739.         Serial.println(  Hmn ); 
  740.  
  741.         Blynk.config(timeSW);  
  742.         Serial.print("Time Summer/Winter: ");  
  743.         Serial.println( timeSW ); 
  744.  
  745.         Blynk.config(formFS);  
  746.         Serial.print("format FS: ");  
  747.         Serial.println( formFS );               
  748.  
  749.         Blynk.config(blynk_token, "blynk-cloud.com"8442);   
  750.         Serial.print("token: " );
  751.         Serial.println(  blynk_token  );     
  752. }
  753.  
  754. bool loadConfig() {
  755.         Serial.println("Load config...");
  756.         File configFile = SPIFFS.open("/config.json""r");
  757.         if (!configFile) {
  758.                 Serial.println("Failed to open config file");
  759.                 return false;
  760.         }
  761.  
  762.         size_t size = configFile.size();
  763.         if (size > 1024) {
  764.                 Serial.println("Config file size is too large");
  765.                 return false;
  766.         }
  767.  
  768.         // Allocate a buffer to store contents of the file.
  769.         std::unique_ptr<char[]> buf(new char[size]);
  770.  
  771.         // We don't use String here because ArduinoJson library requires the input
  772.         // buffer to be mutable. If you don't use ArduinoJson, you may as well
  773.         // use configFile.readString instead.
  774.         configFile.readBytes(buf.get(), size);
  775.  
  776.         StaticJsonBuffer<200> jsonBuffer;
  777.         JsonObject &json = jsonBuffer.parseObject(buf.get());
  778.  
  779.         if (!json.success()) {
  780.                 Serial.println("Failed to parse config file");
  781.                 return false;
  782.         }
  783.  
  784.         // Save parameters
  785.         strcpy(blynk_token, json["blynk_token"]);
  786.         strcpy(address, json["address"]);
  787.         strcpy(tZ, json["tZ"]);
  788.         strcpy(Tmx, json["Tmx"]);
  789.         strcpy(Cmx, json["Cmx"]);
  790.         strcpy(Tmn, json["Tmn"]);
  791.         strcpy(Hmn, json["Hmn"]);
  792.         strcpy(timeSW, json["timeSW"]);
  793.         strcpy(formFS, json["formFS"]);      
  794. }
  795.  
  796. void configModeCallback (WiFiManager *wifiManager) {
  797.         String url {"http://192.168.4.1"};
  798.         printString("Connect to WiFi:");
  799.         printString("net: " + ssid);
  800.         printString("pw: "+ pass);
  801.         printString("Open browser:");
  802.         printString(url);
  803.         printString("to setup device");
  804.  
  805.         drawConnectionDetails();
  806. }
  807.  
  808. void setupWiFi() {
  809.         //set config save notify callback
  810.         wifiManager.setSaveConfigCallback(saveConfigCallback);
  811.  
  812.         // Custom parameters
  813.          WiFiManagerParameter custom_blynk_token("blynk_token""Blynk token", blynk_token, 34);
  814.          wifiManager.addParameter(&custom_blynk_token);
  815.          WiFiManagerParameter custom_address("address""E-mail", address, 64);
  816.          wifiManager.addParameter(&custom_address);
  817.          WiFiManagerParameter custom_tZ("tZ""Time Zone", tZ, 5); 
  818.          wifiManager.addParameter(&custom_tZ);
  819.          WiFiManagerParameter custom_Tmn("Tmn""T min", Tmn, 5);  
  820.          wifiManager.addParameter(&custom_Tmn);                  
  821.          WiFiManagerParameter custom_Tmx("Tmx""T max", Tmx, 5); 
  822.          wifiManager.addParameter(&custom_Tmx);         
  823.          WiFiManagerParameter custom_Cmx("Cmx""C max", Cmx, 7);  
  824.          wifiManager.addParameter(&custom_Cmx);
  825.          WiFiManagerParameter custom_Hmn("Hmn""H min", Hmn, 5);  
  826.          wifiManager.addParameter(&custom_Hmn); 
  827.          WiFiManagerParameter custom_timeSW("timeSW""Time Summer(1)/Winter(0)", timeSW, 4);  
  828.          wifiManager.addParameter(&custom_timeSW);     
  829.          WiFiManagerParameter custom_formFS("formFS""formating FS", formFS, 4);  
  830.          wifiManager.addParameter(&custom_formFS);     
  831.  
  832.         wifiManager.setAPCallback(configModeCallback);
  833.  
  834.         wifiManager.setTimeout(60);
  835.  
  836.         if (!wifiManager.autoConnect(ssid.c_str(), pass.c_str())) {
  837.         md = 2;
  838.         Serial.println("mode OffLINE :(");
  839.         loadConfigS();         
  840.         }
  841.  
  842.         //save the custom parameters to FS
  843.         if (shouldSaveConfig) {
  844.                 Serial.println("saving config");
  845.                 DynamicJsonBuffer jsonBuffer;
  846.                 JsonObject &json = jsonBuffer.createObject();
  847.  
  848.                 json["blynk_token"] = custom_blynk_token.getValue();                
  849.                 json["address"] = custom_address.getValue();
  850.                 json["tZ"] = custom_tZ.getValue(); 
  851.                 json["Tmx"] = custom_Tmx.getValue(); 
  852.                 json["Cmx"] = custom_Cmx.getValue(); 
  853.                 json["Tmn"] = custom_Tmn.getValue(); 
  854.                 json["Hmn"] = custom_Hmn.getValue(); 
  855.                 json["timeSW"] = custom_timeSW.getValue();
  856.                 json["formFS"] = custom_formFS.getValue();
  857.  
  858.                 File configFile = SPIFFS.open("/config.json""w");
  859.                 if (!configFile) {
  860.                         Serial.println("failed to open config file for writing");
  861.                 }
  862.  
  863.                 json.printTo(Serial);
  864.                 json.printTo(configFile);
  865.                 configFile.close();
  866.                 //end save
  867.         }
  868.  
  869.         //if you get here you have connected to the WiFi
  870.         Serial.println("WiFi connected");
  871.         Serial.print("IP address: ");
  872.         Serial.println(WiFi.localIP());
  873. }
  874.  
  875. void connectBlynk(){ 
  876.        if(String(blynk_token)== "Blynk token"){
  877.         blnk = 0;
  878.         Serial.println("! Off Blynk!");
  879.         } else {
  880.         Serial.println("Connecting to blynk...");
  881.         while (Blynk.connect() == false) {
  882.         delay(500);
  883.         Serial.println("Connecting to blynk...");
  884.         }    
  885.     } 
  886. }
  887.  
  888. void sendToBlynk(){
  889.         Blynk.virtualWrite(V1, t);
  890.         Blynk.virtualWrite(V2, h);
  891.         Blynk.virtualWrite(V3, co2);
  892.         Blynk.virtualWrite(V5, hic);
  893. }
  894.  
  895. void formatFS(){
  896.           SPIFFS.format();  
  897.           SPIFFS.begin();  
  898. }
  899.  
  900. void setup() {  
  901.         Serial.begin(115200);
  902.  
  903.         display.begin();    
  904.  
  905.         // Init filesystem
  906.         if (!SPIFFS.begin()) {
  907.                 Serial.println("Failed to mount file system");
  908.                 ESP.reset();
  909.         }
  910.         md = 1;
  911.         // Setup WiFi
  912.         drawWiFi();   //"Connecting to Wi-Fi..."
  913.         setupWiFi();
  914.        if(md == 1){  
  915.  
  916.         // Load configuration           
  917.         if (!loadConfig()) {
  918.                 Serial.println("Failed to load config");
  919.              //   factoryReset();
  920.         } else {
  921.                 Serial.println("Config loaded");
  922.         }   
  923.  
  924.         Blynk.config(address);  
  925.         Serial.print("e-mail: ");  
  926.         Serial.println(  address );   
  927.  
  928.         Blynk.config(tZ);  
  929.         Serial.print("T_Zone: ");  
  930.         Serial.println(  tZ );
  931.  
  932.         Blynk.config(Tmx);  
  933.         Serial.print("T max: ");  
  934.         Serial.println(  Tmx );   
  935.  
  936.         Blynk.config(Cmx);  
  937.         Serial.print("CO2 max: ");  
  938.         Serial.println(  Cmx );   
  939.  
  940.         Blynk.config(Tmn);  
  941.         Serial.print("T min: ");  
  942.         Serial.println(  Tmn );   
  943.  
  944.         Blynk.config(Hmn);  
  945.         Serial.print("H min: ");  
  946.         Serial.println(  Hmn ); 
  947.  
  948.         Blynk.config(timeSW);  
  949.         Serial.print("Time Summer/Winter: ");  
  950.         Serial.println( timeSW ); 
  951.  
  952.         Blynk.config(formFS);  
  953.         Serial.print("format FS: ");  
  954.         Serial.println( formFS );                               
  955.  
  956.         Blynk.config(blynk_token, "blynk-cloud.com"8442);   
  957.         Serial.print("token: " );
  958.         Serial.println(  blynk_token  );
  959.  
  960.         Tmax = atof (Tmx);   
  961.         Cmax = atof (Cmx); 
  962.         Tmin = atof (Tmn);  
  963.         Hmin = atof (Hmn);
  964.         tZone = atof (tZ);
  965.         timeSummerWinter = atof (timeSW);  
  966.         formatingFS = atof (formFS);
  967.  
  968.         drawSynchron();
  969.         synchronClock();
  970.         connectBlynk(); 
  971.         FaceClock(); 
  972.  
  973.      if (formatingFS == 1) {
  974.         formatFS();    
  975.        }                      
  976.        }
  977.      else if(md == 2)
  978.      {
  979.  
  980.         Tmax = atof (Tmx);   
  981.         Cmax = atof (Cmx); 
  982.         Tmin = atof (Tmn);  
  983.         Hmin = atof (Hmn);
  984.         tZone = atof (tZ);
  985.         timeSummerWinter = atof (timeSW);  
  986.         formatingFS = atof (formFS);
  987.  
  988.         synchronClockA();
  989.         FaceClock();
  990.  
  991.      if (formatingFS == 1) {
  992.         formatFS();    
  993.        }
  994.      }   
  995. }
  996.  
  997. void loop() {
  998.  if (md == 2) Serial.println(":( OffLINE");  
  999.  else if (md == 1) Serial.println(":) OnLINE");       
  1000.   sendMeasurements();
  1001.   draw();
  1002.   Clock();
  1003.   drawClockHands(hh,mm,ss); 
  1004.  
  1005.    if (ml >= 480000) ml = 0;   //обнуление счетчика
  1006.    if ((ml >= 20000) and ((> Tmax) or (co2 > Cmax) or (< Tmin) or (< Hmin)))
  1007.         {
  1008.         mailer();
  1009.          ml = 0;
  1010.          }
  1011.  
  1012.        Blynk.run();       
  1013.        if  (bl > 210){ // 30 sec
  1014.             sendToBlynk();
  1015.             Serial.println("Отправка данных на Blynk");
  1016.             bl = 0;
  1017.               }
  1018.  
  1019.           bl++;
  1020.           ml++;
  1021.  
  1022.           delay(100);         
  1023.           t_old = t;
  1024.           hic_old = hic;
  1025.           h_old = h;
  1026.           co2_old = co2;
  1027.           Serial.println(" ");
  1028. }

Якщо хоча б один з параметрів повітря знаходиться за межами запрограмованих порогових значень, то пристрій приблизно один раз на годину відправляє повідомлення на е-мейл такого змісту:

Повідомлення на е-мейл відправляються php-скриптом. Скрипт завантажений на мій поштовий сервер. Він знадобиться, якщо планується відправка повідомлень з іншого ресурсу.

php-скрипт
  1. <?php
  2.  
  3. // тест -  http://skorovoda.in.ua/php/aqm42.php?mymail=my_login@my.site.net&t=22.2&h=55&co2=666
  4. $EMAIL=0;
  5. $TEMPER=0;
  6. $vlaga=0;
  7. $carbon=0;
  8. $device=0;
  9. $EMAIL=$_GET["mymail"];
  10. $device=$_GET["ID"];
  11. echo $EMAIL;
  12. $TEMPER=$_GET["t"];
  13. $vlaga=$_GET["h"];
  14. $carbon=$_GET["co2"];
  15. $mdate = date("H:i d.m.y");
  16. echo <<<END
  17. <p>Температура:  $TEMPER °С<p>
  18. <p>Влажность: $vlaga %<p>
  19. <p>Содержание углекислого газа:  $carbon ppm<p>
  20. <p>--------------------<p>
  21. <p>Метеостанция №:  $device<p>
  22. END;
  23. echo <<<END
  24. <p>$mdate</p>
  25. END;
  26. mail($EMAIL, "Air Quality Monitor " .$device. " v.051018","   Данное сообщение сформировано монитором качества воздуха №" .$device. " автоматически.  Один или несколько параметров воздуха в помещении (температура, влажность или содержание углекислого газа) находятся за пределами заданных граничных значений. === Температура: ".$TEMPER."°C === "."Влажность: ".$vlaga."% === "."Содержание углекислого газа: ".$carbon." ppm === "."Проанализируйте информацию! === Время, дата: ".$mdate,"From: my_sensors@air-monitor.info \n")
  27.  
  28. ?>

 

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

Пристрій підняв точку доступу 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 - тепловий удар неминучий. Вплив вітру цей індекс не враховує - інфа з Вікіпедії

Слід уточнити, що індекс спеки розраховується тільки для відносно високих температур, коли температура повітря вище 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-скрипта з мого сервера на інший.

Висновки

Про зроблене в проекті - не буду повторюватись, а зупинюсь на неоднозначному моменті.

Логічна, наприклад, постановка запитання «А навіщо тут годинник?». Відповідь: Механічний годинник потрібен для заповнення екрану. Хоча в даний час цифровий годинник вбудований практично в кожен побутовий прилад, стрілочні володіють однією перевагою: масштаб часу, завдяки циферблату, що дозволяє легко оцінити час до якої-небудь події. Втім, не буду сперечатись - можна обійтись і без годинника.

Тут - повна версія статті.