Compact home air monitor (CO2, temperature, humidity, pressure) with Wi-Fi and mobile interface

    image


    About measurement of CO2 and its importance on Geektimes there were already quite a few publications (links at the end of the article). Here I want to describe the project of a compact monitor for CO2 level, as well as temperature, humidity and pressure with Wi-Fi, firmware update over the air and interface in a mobile application. Hearts of the system module based on esp8266, CO2 sensor MH-Z19 and framework esp8266-arduino . And so, turn on the device in a USB-outlet:


    image


    First, it will try to connect to the previously saved network and, having successfully completed the connection, it starts working. If the wifi network is not detected, the monitor will load in the standby mode settings and raise the access point. By connecting to it and opening in the browser, http://192.168.4.1you can specify the data of the real network, as well as the key to connect to the server and the name of the device.


    image


    On the server and for the mobile client used open project Blynk . He was already mentioned a couple of times on Geektimes. This opensource server, which has libraries for different embedded systems and allows you to connect communication with a third-party service in a few lines. You can install it on your server, for example, by running a docker container , or you can use a public developer server.


    The interface for blynk is built from ready-made bricks in a mobile application available for iOS and Android . Unfortunately, there is no web interface. Through it, you can view the measurement history of all parameters, set up notifications or a web hook for a condition (here, exceed the CO2 level), update the firmware and reset to default settings.


    Blynk allows you to distribute applications through an impressively sized QR code, in which the configuration of all modules is stored.


    QR code of the application, scan the application Blynk

    image


    image


    How to assemble a similar device?


    First you need to buy all the necessary modules, for example, on aliexpress. Since specific offers and sellers are constantly changing, therefore, the table lists only search queries for which you can find what you need.


    ComponentSearch phraseEstimated price
    Esp-12 NodeMCU Module *esp8266 nodemcu cp2102$ 3.50
    CO2 sensor mh-z19mh-z19$ 22
    Pressure meter**,***bmp280$ 1
    Temperature and humidity sensor **, ***gy-21 SI7021 i2c$ 2.2
    Screen0.96 "128x64 i2c OLED blue$ 2.7
    Wiresdupont female-female 10cm$ 1
    3d print case-$ 8
    Total:$ 40.4

    * - There are two versions of the NodeMCU module in the Chinese market. They differ primarily in the USB-UART converter cp2102 and ch340g, but also in size - the version with the ch340g is wider by 3 mm and no longer fits into this case. Also, the version with cp2102 may be more interesting, because this converter has no problems with drivers for any OSes.
    ** - The current version used bmp-085, which is now no longer buy. But modules with bmp-280 have the same dimensions and all that needs to be done is to replace the library used by https://github.com/adafruit/Adafruit_BMP280_Library - all methods are the same.
    *** - Instead of a pair of sensors bmp280 + si7021, you can use one bme280, which measures all three indicators: temperature, humidity and pressure. It can be found on aliexpress for $ 3.5.


    Housing


    On the one hand, I wanted to make a device from ready-made modules; on the other hand, I did not want to make it the size of a loaf of bread. To do this, in Tinkercad was drawn a body suitable for 3D printing. The big problem with tinkercad is that it is very difficult to make changes to already completed projects. If I started now, I would use openSCAD


    image


    The case turned out really compact, but the wires inside still take up most of the space and they had to be made quite short so that the box closed.


    image


    The disadvantage of such a dense arrangement is that the heat from the microcontroller and the dc-dc converter heat the air inside ~ 3 degrees higher than the real temperature, which also damages the accuracy of the humidity readings. On the other hand, it creates convection, which updates the air in the CO2 sensor faster.


    In part, this problem can be solved by working on the consumption of the microcontroller, for example, to transmit data over the network less frequently.


    Connection


    The electrical circuit is very simple - a pressure sensor, a temperature and humidity sensor and a display are powered from 3.3V and connected via a two-wire I2C interface. It allows you to connect all devices in parallel: SDA of each device to the D1 leg (GPIO5), and SCL - to the D2 leg (GPIO4)


    The CO2 sensor is powered by 5V (in our case, Vin is powered by the USB input) and is connected via the UART. To do this, use the legs of D7 (GPIO13) - RX (which connects to the TX sensor) and D8 (GPIO15) - TX (which connects to the RX sensor)


    image


    Software part


    All project files were developed in the PlatformIO development environment and are on Github-e .


    Firmware code to not go to Github
    #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// HTTP requests#include<ESP8266HTTPClient.h>// OTA updates#include<ESP8266httpUpdate.h>// Blynk#include<BlynkSimpleEsp8266.h>// JSON#include<ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson// GPIO Defines#define I2C_SDA 5 // D1 Orange#define I2C_SCL 4 // D2 Yellow// Humidity/Temperature SI7021#include<SI7021.h>
    SI7021 si7021;
    #include<Wire.h>// Pressure and Temperature#include<Adafruit_BMP085.h>// Use U8g2 for i2c OLED Lib#include<SPI.h>#include<U8g2lib.h>U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, I2C_SCL, I2C_SDA, U8X8_PIN_NONE);
    byte x {0};
    byte y {0};
    // Handy timers#include<SimpleTimer.h>// CO2 SERIAL#define DEBUG_SERIAL Serial1#define SENSOR_SERIAL Serial
    byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
    unsignedchar response[7];
    // Pressure and temperature
    Adafruit_BMP085 bme;
    // Blynk tokenchar blynk_token[33] {"Blynk token"};
    char blynk_server[64] {"blynk-cloud.com"};
    constuint16_t blynk_port {8442};
    // Device Idchar device_id[17] = "Device ID";
    constchar fw_ver[17] = "0.1.0";
    // Handy timer
    SimpleTimer timer;
    // Setup Wifi connection
    WiFiManager wifiManager;
    // Network credentials
    String ssid { "ku_" +  String(ESP.getChipId())};
    String pass {"ku_" + String(ESP.getFlashChipId()) };
    //flag for saving databool shouldSaveConfig = false;
    // Sensors dataint t {-100};
    int p {-1};
    int h {-1};
    int co2 {-1};
    char loader[4] {'.'};
    //callback notifying the need to save configvoidsaveConfigCallback(){
            DEBUG_SERIAL.println("Should save config");
            shouldSaveConfig = true;
    }
    voidfactoryReset(){
            wifiManager.resetSettings();
            SPIFFS.format();
            ESP.reset();
    }
    voidprintString(String str){
            DEBUG_SERIAL.println(str);
    }
    voidreadCO2(){
            // CO2bool header_found {false};
            char tries {0};
            SENSOR_SERIAL.write(cmd, 9);
            memset(response, 0, 7);
            // Looking for packet startwhile(SENSOR_SERIAL.available() && (!header_found)) {
                    if(SENSOR_SERIAL.read() == 0xff ) {
                            if(SENSOR_SERIAL.read() == 0x86 ) header_found = true;
                    }
            }
            if (header_found) {
                    SENSOR_SERIAL.readBytes(response, 7);
                    byte crc = 0x86;
                    for (char i = 0; i < 6; i++) {
                            crc+=response[i];
                    }
                    crc = 0xff - crc;
                    crc++;
                    if ( !(response[6] == crc) ) {
                            DEBUG_SERIAL.println("CO2: CRC error: " + String(crc) + " / "+ String(response[6]));
                    } else {
                            unsignedint responseHigh = (unsignedint) response[0];
                            unsignedint responseLow = (unsignedint) response[1];
                            unsignedint ppm = (256*responseHigh) + responseLow;
                            co2 = ppm;
                            DEBUG_SERIAL.println("CO2:" + String(co2));
                    }
            } else {
                    DEBUG_SERIAL.println("CO2: Header not found");
            }
    }
    voidsendMeasurements(){
            // Read data// Temperaturefloat tf = si7021.getCelsiusHundredths() / 100.0;
            float t2f =bme.readTemperature();
            t = static_cast<int>((tf + t2f) / 2);
            // Humidity
            h = si7021.getHumidityPercent();
            // Pressure (in mmHg)
            p = static_cast<int>(bme.readPressure() * 760.0 / 101325);
            // CO2
            readCO2();
            // Send to server
            Blynk.virtualWrite(V1, t);
            Blynk.virtualWrite(V2, h);
            Blynk.virtualWrite(V4, p);
            Blynk.virtualWrite(V5, co2);
            // Write to debug console
            printString("H: " + String(h) + "%");
            printString("T: " + String(t) + "C");
            printString("P: " + String(p) + "mmHg");
            printString("CO2: " + String(co2) + "ppm");
    }
    voidloading(){
            longunsignedint count {(millis() / 500) % 4};
            memset(loader, '.', count);
            memset(&loader[count], 0, 1);
    }
    voiddraw(){
            u8g2.clearBuffer();
            // CO2if (co2 > -1) {
                    char co2a [5];
                    sprintf (co2a, "%i", co2);
                    u8g2.setFont(u8g2_font_inb19_mf);
                    x = (128 - u8g2.getStrWidth(co2a))/2;
                    y = u8g2.getAscent() - u8g2.getDescent();
                    u8g2.drawStr(x, y, co2a);
                    constchar ppm[] {"ppm CO2"};
                    u8g2.setFont(u8g2_font_6x12_mf);
                    x = (128 - u8g2.getStrWidth(ppm)) / 2;
                    y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
                    u8g2.drawStr(x, y, ppm);
            } else {
                    loading();
                    u8g2.setFont(u8g2_font_inb19_mf);
                    x = (128 - u8g2.getStrWidth(loader)) / 2;
                    y = u8g2.getAscent() - u8g2.getDescent();
                    u8g2.drawStr(x, y, loader);
            }
            // Cycle other meauserments
            String measurement {"..."};
            constchar degree {176};
            // Switch every 3 secondsswitch((millis() / 3000) % 3) {
            case0:
                    if (t > -100) { measurement = "T: " + String(t) + degree + "C"; }
                    break;
            case1:
                    if (h > -1) { measurement = "H: " + String(h) + "%"; }
                    break;
            default:
                    if (p > -1) { measurement =  "P: " + String(p) + " mmHg"; }
            }
            char measurementa [12];
            measurement.toCharArray(measurementa, 12);
            u8g2.setFont(u8g2_font_9x18_mf);
            x = (128 - u8g2.getStrWidth(measurementa))/2;
            y = 64 + u8g2.getDescent();
            u8g2.drawStr(x, y, measurementa);
            u8g2.sendBuffer();
    }
    voiddrawBoot(String msg = "Loading..."){
            u8g2.clearBuffer();
            u8g2.setFont(u8g2_font_9x18_mf);
            x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
            y = 32 + u8g2.getAscent() / 2;
            u8g2.drawStr(x, y, msg.c_str());
            u8g2.sendBuffer();
    }
    voiddrawConnectionDetails(String ssid, String pass, String url){
            String msg {""};
            u8g2.clearBuffer();
            msg = "Connect to WiFi:";
            u8g2.setFont(u8g2_font_7x13_mf);
            x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
            y = u8g2.getAscent() - u8g2.getDescent();
            u8g2.drawStr(x, y, msg.c_str());
            msg = "net: " + ssid;
            x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
            y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
            u8g2.drawStr(x, y, msg.c_str());
            msg = "pw: "+ pass;
            x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
            y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
            u8g2.drawStr(x, y, msg.c_str());
            msg = "Open browser:";
            x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
            y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
            u8g2.drawStr(x, y, msg.c_str());
            // URL// u8g2.setFont(u8g2_font_6x12_mf);
            x = (128 - u8g2.getStrWidth(url.c_str())) / 2;
            y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
            u8g2.drawStr(x, y, url.c_str());
            u8g2.sendBuffer();
    }
    boolloadConfig(){
            File configFile = SPIFFS.open("/config.json", "r");
            if (!configFile) {
                    DEBUG_SERIAL.println("Failed to open config file");
                    returnfalse;
            }
            size_t size = configFile.size();
            if (size > 1024) {
                    DEBUG_SERIAL.println("Config file size is too large");
                    returnfalse;
            }
            // Allocate a buffer to store contents of the file.std::unique_ptr<char[]> buf(newchar[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()) {
                    DEBUG_SERIAL.println("Failed to parse config file");
                    returnfalse;
            }
            // Save parametersstrcpy(device_id, json["device_id"]);
            strcpy(blynk_token, json["blynk_token"]);
    }
    voidconfigModeCallback(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(ssid, pass, url);
    }
    voidsetupWiFi(){
            //set config save notify callback
            wifiManager.setSaveConfigCallback(saveConfigCallback);
            // Custom parametersWiFiManagerParameter custom_device_id("device_id", "Device name", device_id, 16);
            WiFiManagerParameter custom_blynk_server("blynk_server", "Blynk server", blynk_server, 64);
            WiFiManagerParameter custom_blynk_token("blynk_token", "Blynk token", blynk_token, 34);
            wifiManager.addParameter(&custom_blynk_server);
            wifiManager.addParameter(&custom_blynk_token);
            wifiManager.addParameter(&custom_device_id);
            // wifiManager.setTimeout(180);
            wifiManager.setAPCallback(configModeCallback);
            if (!wifiManager.autoConnect(ssid.c_str(), pass.c_str())) {
                    DEBUG_SERIAL.println("failed to connect and hit timeout");
            }
            //save the custom parameters to FSif (shouldSaveConfig) {
                    DEBUG_SERIAL.println("saving config");
                    DynamicJsonBuffer jsonBuffer;
                    JsonObject &json = jsonBuffer.createObject();
                    json["device_id"] = custom_device_id.getValue();
                    json["blynk_server"] = custom_blynk_server.getValue();
                    json["blynk_token"] = custom_blynk_token.getValue();
                    File configFile = SPIFFS.open("/config.json", "w");
                    if (!configFile) {
                            DEBUG_SERIAL.println("failed to open config file for writing");
                    }
                    json.printTo(DEBUG_SERIAL);
                    json.printTo(configFile);
                    configFile.close();
                    //end save
            }
            //if you get here you have connected to the WiFi
            DEBUG_SERIAL.println("WiFi connected");
            DEBUG_SERIAL.print("IP address: ");
            DEBUG_SERIAL.println(WiFi.localIP());
    }
    // Virtual pin update FW
    BLYNK_WRITE(V22) {
            if (param.asInt() == 1) {
                    DEBUG_SERIAL.println("Got a FW update request");
                    char full_version[34] {""};
                    strcat(full_version, device_id);
                    strcat(full_version, "::");
                    strcat(full_version, fw_ver);
                    t_httpUpdate_return ret = ESPhttpUpdate.update("http://romfrom.space/get", full_version);
                    switch (ret) {
                    case HTTP_UPDATE_FAILED:
                            DEBUG_SERIAL.println("[update] Update failed.");
                            break;
                    case HTTP_UPDATE_NO_UPDATES:
                            DEBUG_SERIAL.println("[update] Update no Update.");
                            break;
                    case HTTP_UPDATE_OK:
                            DEBUG_SERIAL.println("[update] Update ok.");
                            break;
                    }
            }
    }
    // Virtual pin reset settings
    BLYNK_WRITE(V23) {
            factoryReset();
    }
    voidsetup(){
            // Init serial ports
            DEBUG_SERIAL.begin(115200);
            SENSOR_SERIAL.begin(9600);
            SENSOR_SERIAL.swap();  // GPIO15 (TX) and GPIO13 (RX)// Init I2C interface
            Wire.begin(I2C_SDA, I2C_SCL);
            // Init display
            u8g2.begin();
            drawBoot();
            // Init Humidity/Temperature sensor
            si7021.begin(I2C_SDA, I2C_SCL);
            // Init Pressure/Temperature sensorif (!bme.begin()) {
                    DEBUG_SERIAL.println("Could not find a valid BMP085 sensor, check wiring!");
            }
            // Init filesystemif (!SPIFFS.begin()) {
                    DEBUG_SERIAL.println("Failed to mount file system");
                    ESP.reset();
            }
            // Setup WiFi
            setupWiFi();
            // Load config
            drawBoot();
            if (!loadConfig()) {
                    DEBUG_SERIAL.println("Failed to load config");
                    factoryReset();
            } else {
                    DEBUG_SERIAL.println("Config loaded");
            }
            // Start blynk
            Blynk.config(blynk_token, blynk_server, blynk_port);
            // Setup a function to be called every 10 second
            timer.setInterval(10000L, sendMeasurements);
            sendMeasurements();
    }
    voidloop(){
            Blynk.run();
            timer.run();
            draw();
    }

    The development has been greatly simplified due to the presence of good examples and the availability of functional libraries:



    For working with a CO2 sensor, a slightly improved example from the datasheet was used .


    What can be done better?


    There is more work to do:
    1) Solve the problem of excess heat in the corus
    2) Make it even smaller by spreading the board and getting rid of the wires
    3) Make work in offline mode


    What else can you do? What are your ideas? If someone is going to do something like that I will be happy to help and answer questions.


    Existing articles on the topic of Geektimes:


    1. We measure the concentration of CO2 in the apartment with the help of MH-Z19
    2. Overview of the infrared CO2 sensor MH-Z19
    3. Wi-Fi CO2 meter on ESP8266 + K-30

    Also popular now: