En un momento, me gustó el monitor de calidad del aire de la publicación de Sergei Silnov, "Monitor de aire doméstico compacto (CO2, temperatura, humedad, presión) con Wi-Fi y una interfaz móvil" .
En el monitor de calidad del aire (en adelante, el monitor) del proyecto de Sergey, el controlador ESP8266 procesa la información de los sensores de temperatura, humedad, presión y contenido de 2 en el aire y se muestra en una pantalla monocromática en varios cuadros. Además, en el monitor, a través del formulario en el navegador, la clave de identificación del servicio Blynk se almacena en la memoria ESP8266 y los datos se envían automáticamente a Blynk.
El monitor tenía un problema grave: se bloqueó al apagar / encender o incluso "parpadear" la tensión de alimentación del monitor.
Repetí el proyecto con cambios menores, y para eliminar las congelaciones del monitor, agregué energía alternativa al circuito. Simple como un rastrillo: el devanado del relé estaba bajo el voltaje del adaptador de CA / CC, y los contactos del relé cambiaron la alimentación del adaptador a las baterías cuando desapareció el voltaje en la red de 220V.
Mi éxito imaginario duró hasta la primera interrupción en la casa (esto sucede con nosotros). Las baterías baratas se agotaron antes de que apareciera el voltaje en los enchufes, y volví al punto de partida.
Después de pisar su propio rastrillo, decidí no buscar soluciones simples.
Cambios / adiciones hechas al bosquejo por S. Silnov:
- El monitor se desconecta automáticamente 90 segundos después de apagar y encender nuevamente el voltaje de la fuente de alimentación del monitor, si no hay Wi-Fi disponible en este momento.
- Los valores de temperatura límite, el contenido de CO2, la humedad del aire, así como la zona horaria y el horario de invierno / verano se ingresan en la memoria ESP8266, como la tecla Blynk, desde el formulario en el navegador.
- El cambio en los valores límite se simplifica radicalmente. Si antes este procedimiento se realizaba a través del compilador de código, ahora es suficiente cambiar el registro en uno de los campos del formulario de "0" a "1". Este trabajo se ha convertido en factible, ni siquiera para usuarios avanzados.
- La información sobre el funcionamiento del monitor se muestra en la pantalla a color de 1,44 ”, 128x128 en un cuadro. La salida de los parámetros del aire más allá de los valores límite se muestra en el cuadro en color.
- En el monitor, el índice de calor (humindex) se calcula y se muestra en la pantalla.
- Las notificaciones se envían desde el monitor por correo electrónico si la temperatura, el contenido de CO2 o la humedad del aire están fuera de los umbrales establecidos por el usuario.
- El monitor puede funcionar sin conectarse al servicio Blynk y la dirección de correo electrónico.
- Se ha agregado un reloj analógico en tiempo real al monitor, que se sincroniza a través de Internet con el servidor de hora exacto.

Esta es una de las tareas no resueltas de mi proyecto "Termostato ambiental inalámbrico programable por Wi-Fi con monitor de calidad del aire y otras funciones útiles" . Decidí formalizar esta tarea como un artículo separado, porque hoy en día muchos están interesados en la calidad del aire en la vivienda.
Asamblea
Para ensamblar el dispositivo, necesitará componentes, una lista de los cuales y su costo estimado a los precios del sitio web AliExpress se muestran en la tabla.
Componente | Precio ($) |
Tarjeta wifi NodeMCU CP2102 ESP8266 | 2,53 |
Sensor de temperatura y humedad DHT22 | 2,34 |
Sensor de contenido de CO2 MH Z-19 | 18.50 |
Pantalla TFTLCD 1.44 "SPI 128x128 | 2,69 |
Reloj RTC DS3231 | 1.00 |
Cables de montaje, otras bagatelas | 2,00 |
Total (aproximadamente): | 30 |
El cerebro del monitor es el controlador ESP8266 en la placa del módulo NodeMCU CP2102. Recibe señales de sensores, relojes y genera una señal de control de pantalla, sincroniza el reloj y también envía información a Blynk y correo electrónico.

Desafortunadamente, no encontré la biblioteca Fritzing para la pantalla a color de 1.44 ", 128x128 con un pinout de 8 pines, por lo que la pantalla tiene una pantalla de 11 pines. Al montar, preste atención no a la ubicación de la salida de la pantalla en relación con otros, sino a su carga funcional.
Para aquellos a quienes no les gusta armar un prototipo para el diagrama de cableado, la tabla de conexión:
NodeMCU (GPIO) | Pin de sensores |
D0 (GPIO 16) | displ_1.44, CS |
D1 (GPIO 5) | DS3231, SCL |
D2 (GPIO 4) | DS3231, SDA |
D3 (GPIO 0) | DHT22, DATOS; |
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 | Sensores, GND |
Si usa una batería en lugar de una batería en el módulo del reloj, no olvide romper el circuito de carga de la batería, de lo contrario, la batería se hinchará después de unas pocas semanas de trabajar bajo voltaje. Con una precisión de reloj autoalimentado de 2 segundos / año, está garantizado.
El voltaje del monitor de 5V se puede suministrar desde el puerto USB de la computadora con un cable USB estándar - microUSB al módulo NodeMCU esp12 .
El boceto del monitor para cargar en el ESP8266 está debajo del spoiler.
Por si acaso, no seas demasiado vago para modificar el controlador I2C incorporado para el núcleo Arduino ESP8266. Las instrucciones están aquí .
monitor boceto #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 0 //D3 gpio0, 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)- CS(display) #define __DC 2 //D4 gpio2 - AO(display) #define __RST 12 // D6 gpio12 - RESET(display) //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(); 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); if (t >= 21.0) hic = hic1; else hic = t; 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); 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() { 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(100); t_old = t; hic_old = hic; h_old = h; co2_old = co2; Serial.println(" "); }
Si al menos uno de los parámetros de aire está fuera de los umbrales establecidos, el dispositivo envía un mensaje de correo electrónico sobre los siguientes contenidos aproximadamente una vez por hora:

Los mensajes de correo electrónico se envían mediante script PHP. El script se carga en mi servidor de correo. Será necesario si planea enviar mensajes desde otro recurso.
Enciende el monitor.
El dispositivo elevó el punto de acceso am180206. Encontramos este punto en la lista de redes disponibles y nos conectamos, la contraseña está en la pantalla. Intente conectarse a este punto en un minuto y medio, de lo contrario, el monitor pasará automáticamente al modo de batería. Acerca del modo sin conexión: un poco más tarde. Luego abra la página del navegador http://192.168.4.1.
Haga clic en el botón Configurar WiFi (sin escaneo) . Se abrirá una página con el formulario de configuración del termostato:
Indique en el formulario el nombre y la contraseña de su red doméstica, la clave de identificación BLynk, su correo electrónico, zona horaria, horario de verano / invierno, así como los valores umbral de temperatura, humedad y contenido de CO2.
Al elegir los valores de umbral para los parámetros del aire, guíese por los indicadores que encontré en Internet:
1. Temperatura confortable en la noche durante el sueño 19 ... 21 ° C, durante el día - 22 ... 23 ° C.
2. La humedad relativa óptima en la estación fría se considera humedad del 30 ... 45%, y en la cálida - 30 ... 60%. Indicadores máximos de humedad máxima: en invierno no debe exceder el 60%, y en verano - 65%.
3. El nivel máximo de dióxido de carbono en las instalaciones no debe exceder 1000 ppm. El nivel recomendado para dormitorios, habitaciones infantiles: no más de 600 ppm. La marca de 1400 ppm durante mucho tiempo es el límite del contenido de CO2 permitido en la habitación. Si hay más, la calidad del aire se considera baja.
El campo e-mai se puede dejar en blanco. Luego, se perderá la oportunidad de recibir cartas por correo electrónico sobre la salida de parámetros aéreos más allá de los valores límite. Sin la clave Blynk ingresada, perderá la capacidad de recibir información sobre los parámetros del aire a distancia. Sin embargo, el monitor no se "perderá" si los campos con los valores límite de los parámetros del aire permanecen vacíos.
Rellene sin dudar todos los campos del formulario con números en formato de coma flotante, por ejemplo, la zona horaria es 2.0 . El boceto proporciona la conversión posterior de números al formato deseado.
Después de guardar la configuración en la memoria ESP8266 (botón Guardar ), el monitor se conectará a la red y comenzará a funcionar.
Considere la imagen en la pantalla.
La fecha, la hora, la temperatura medida, el contenido de CO2 y la humedad del aire se explican por sí mismas. Aclararé si los parámetros del aire van más allá de los límites establecidos, entonces su visualización en la pantalla cambiará de color verde a rojo.
La letra "B" sobre un fondo rojo indica que el monitor está funcionando sin conectarse a Blynk, y si aparece la letra "A", la energía desapareció y no había Wi-Fi en el momento en que apareció (el dispositivo se desconectó).
En general, la aparición de color rojo en la pantalla debería alertar: hay desviaciones del funcionamiento normal del dispositivo.
En la esquina inferior derecha de la pantalla vemos el índice de calor (índice de calor, humindex).
“Humidex es una cantidad adimensional basada en el punto de rocío. Este índice es ampliamente utilizado en informes meteorológicos canadienses en el verano.
Según el Servicio Meteorológico de Canadá, los valores de Humidex por encima de 30 causan algunas molestias, por encima de 40 causan grandes molestias y un valor por encima de 45 es peligroso. Si humidex alcanza 54, el golpe de calor es inevitable. El viento no tiene en cuenta este índice ". (Wikipedia, humidex ).
Debe aclararse que el índice de calor se calcula solo para temperaturas relativamente altas, cuando la temperatura del aire medida es superior a 21 ° C. Figurativamente, el índice de calor es la temperatura percibida en un día caluroso de verano sin viento.
Según la tabla del mismo artículo en Wikipedia, la temperatura medida de 25 ° C a una humedad del 30% se siente como 24 ° C, a 50% - 28 ° C, y a 90% - 35 ° C (10 ° C más que el termómetro). En el interior, este indicador puede reducirse organizando una corriente de aire o encendiendo el ventilador. El aire acondicionado se encenderá automáticamente si configura la temperatura del aire acondicionado a no más de 25 ° C. El índice de calor, en mi opinión, es un parámetro más relevante de la calidad del aire que, por ejemplo, la presión, en la que no podemos influir de ninguna manera.
El sensor DHT22, como el DHT11, junto con el "menos" - baja precisión de la medición de humedad tiene un "plus" innegable: el bus de datos de este sensor tiene información sobre el índice de calor. Aproveché este "plus" y mostré el índice de calor en la pantalla del dispositivo.
Hay muchas quejas en Internet sobre la baja precisión del DHT22. Para aquellos que están decididos a negarse a usar este sensor en sus proyectos, les pido que busquen sensores de temperatura y humedad más modernos HTU21D, Si7021 o SHT21.
Es hora de lanzar la aplicación Blynk en su teléfono inteligente.
Configura tu aplicación. Variables para Blynk (no buscarlas en el boceto): temperatura - V1 , humedad - V2 , contenido de CO2 - V3 , índice de calor - V5 .
En mi teléfono inteligente, la interfaz de Blynk'a se ve así:
El gráfico muestra la temperatura medida (amarillo), la humedad (azul), el índice de calor (púrpura). El primer pico en el gráfico es el calentamiento del sensor de presión-humedad en la palma de la mano: la curva del índice de calor se encuentra por encima de la curva de temperatura. El segundo pico es el calentamiento del sensor con un secador de pelo. La humedad del aire disminuye cuando el sensor se calienta con un secador de pelo, y la curva del índice de calor en algunas áreas se repite o por debajo de la línea de temperatura. (ver llamada).
No se preocupe si la línea de temperatura ha desaparecido en el gráfico: a temperaturas inferiores a 21 ° C, la curva de índice de calor repite la curva de temperatura medida.
Ahora estamos probando el funcionamiento del sistema de notificación por correo electrónico. Ingrese la dirección http del código php-script en la barra de direcciones del navegador (en el script, la línea con la dirección http está comentada). Si no se ha olvidado de especificar su correo electrónico en la configuración y en la ventana del navegador (información, como en la imagen a continuación), lo más probable es que no haya problemas al recibir notificaciones. La prueba es especialmente útil cuando transfiero un script php de mi servidor a otro.

Conclusiones
No repetiré lo que se ha hecho, pero me detendré en puntos ambiguos.
Lógico, por ejemplo, es la pregunta "¿Por qué está aquí el reloj?" Se necesita un reloj de flecha para llenar la pantalla. Aunque en la actualidad los relojes digitales están integrados en casi todos los electrodomésticos, los analógicos tienen una ventaja: la escala de tiempo, gracias al dial, facilita la estimación del tiempo de un evento. No discutiré, puedes hacerlo sin horas.
En qué necesita trabajar:
• Organice una fuente de alimentación autónoma para el monitor a partir de dos baterías AA durante mucho tiempo, al menos un año.
• No vale la pena discutir la apariencia: aquí cada uno tiene su propia opinión y posibilidades. Por ejemplo, me gusta la opción avs24rus , utilizaba una pantalla de 7 "en un marco con impresión 3D como pantalla . Puede reemplazar un marco costoso por uno barato de una baguette. Y en el modo de espera, visualice un paisaje, un retrato de su amada o una foto de niños, también tendrá Marco de fotos original además.
Si no es muy exigente en el aspecto estético del problema, entonces quizás algo similar le convenga:
Nota Inicialmente, el monitor fue concebido como un dispositivo universal listo para usar en un termostato . Su versatilidad ha llevado a una complicación injustificada del esquema. Por lo tanto, luego rechacé la posibilidad de usar la solución de monitor en un termostato y edité la publicación. Las huellas de la versión anterior más compleja del dispositivo permanecieron en los comentarios.
Buena suerte
Mis marcadores sobre un tema de Habr
1. Termómetro Wi-Fi en ESP8266 + DS18B20 por solo $ 4
2. Monitor de aire doméstico compacto (CO2, temperatura, humedad, presión) con Wi-Fi y una interfaz móvil
3. Experiencia práctica con Blynk para un sensor de CO2. Parte 1
4. Uso de la memoria de visualización SPI Flash para almacenar recursos gráficos o mostrar una estación meteorológica doméstica
5. Medimos la concentración de CO2 en el apartamento usando el MH-Z19
6. El lado oscuro de la MH-Z19
7. Medimos la concentración de CO2 en el apartamento usando el MH-Z19
Finalmente, gracias a @kumekay por los valiosos consejos.
La publicación "La experiencia práctica de usar Blynk para un sensor de CO2 me ayudó a resolver el problema de volver a ingresar variables en la memoria ESP8622 " Parte 1 " , @ a3x . Artículo muy versátil!