Хабрахабр

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

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

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

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

Простое, как грабли: обмотка реле находилась под напряжением адаптера 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 кОм, др. мелочи

3,00

Всего:

30,06

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

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

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

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

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

Вначале при подаче питания вывод GPIO15 ESP8266 ключом подтягивается к GND. Для решения проблемы дефицита цифровых пинов предназначен ключ на транзисторе VT1. Нестандартное решение позволяет вначале запустить ESP8266, а затем использовать это же вывод как цифровой для приема информации с датчика DHT22. Через несколько секунд после подачи питания ключ размыкается и на GPIO15 поступает сигнал с 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] ; // 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. Об автономном режиме – чуть позже. Постарайтесь подключиться к этой точке за полторы минуты, иначе монитор автоматически перейдет в режим автономной работы. 168. Затем откроем в браузере страницу http://192. 1. 4.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Выводы

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

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

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

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

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

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

Успехов!

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

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

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть