I made a CO2 sensor with a blackjack and more

About 3 years ago, still started to do, but every time something happened and I lost the source code. Finally I got my hands on it. To replicate this device we will need:

  • NodeMcu (aka esp8266).
  • MHZ-19B
  • BME280

I used arduino ide to write the code for the board because I don’t like to tinker with the boards too much. I ended up with a data repository, roughly speaking. We collect statistics from the sensors, store and return them in json format.
The code was not the best and will have to be optimized in the future, but overall it works 100%.

#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include "FS.h"
#include <LittleFS.h>
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <SoftwareSerial.h>
#include "Forecaster.h"

Forecaster cond;

SoftwareSerial co2Serial(D3, D4); // define MH-Z19 RX TX D3 (GPIO0) and D4 (GPIO2)
/* Put your SSID & Password */
const char* ssid = "CO2 NUHACH";  // Enter SSID here
const char* password = "lublu_CO2_nuhat";  //Enter Password here

/* Put IP Address details */
IPAddress local_ip(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);

ESP8266WebServer server(80);


StaticJsonDocument<200> conf;
StaticJsonDocument<200> ppms;
Adafruit_BME280 bme;

const unsigned long graphInterval = 10 * 60 * 1000ul;
unsigned long previousGraphTime = 0;

const unsigned long weatherInterval = 30 * 60 * 1000ul;
unsigned long previousWeatherTime = 0;
int alt = -1;

const unsigned long eventInterval = 5000;
unsigned long previousTime = 0;

bool connectedToWifi = false;
bool mhzConnected = false;
bool bmeConnected = false;


int PPM = 0;
float temperature = 0;
float hum = 0;
float pressure = 0;

bool loadConfig() {
  File configFile = LittleFS.open("/config.json", "r");
  StaticJsonDocument<200> doc;
  if (!configFile) {
    Serial.println("Failed to open config file");
    return false;
  }

  auto error = deserializeJson(doc, configFile);
  if (error) {
    Serial.println("Failed to parse config file");
    return false;
  }

  const char* _ssid = doc["ssid"];
  const char* _password = doc["password"];
  alt = String(doc["alt"]).toInt();

  // Real world application would store these values in some variables for
  // later use.
  serializeJson(doc, Serial);
  Serial.print("Loaded ssid: ");
  Serial.println(_ssid);
  Serial.print("Loaded password: ");
  Serial.println(_password);
  conf = doc;
  return true;
}

bool saveConfig(String _ssid, String _password) {
  StaticJsonDocument<200> doc;
  doc["ssid"] = _ssid;
  doc["password"] = _password;
  if (alt > -1) {
    doc["alt"] = alt;
  }
  Serial.println("ssid saved: " + _ssid);
  Serial.println("password saved: " + _password);
  Serial.println("alt saved: " + alt);
  File configFile = LittleFS.open("/config.json", "w");
  if (!configFile) {
    Serial.println("Failed to open config file for writing");
    return false;
  }
  serializeJson(doc, configFile);
  return true;
}

bool savePPM() {
  File ppmFile = LittleFS.open("/ppm3.json", "r");
  StaticJsonDocument<200> doc;
  JsonArray arr = doc.createNestedArray("ppm");
  if (!ppmFile) {
    Serial.println("Failed to open ppm file");

    for (int i = 0; i < 10; i++) {
      arr.add(0);
    }
    File saveFile = LittleFS.open("/ppm3.json", "w");
    serializeJson(doc, saveFile);

  }

  auto error = deserializeJson(doc, ppmFile);
  if (error) {
    Serial.println("Failed to parse ppm file");
    doc.createNestedArray("ppm");
    for (int i = 0; i < 10; i++) {
      arr.add(0);
    }
    File saveFile = LittleFS.open("/ppm3.json", "w");
    serializeJson(doc, saveFile);
  }

  int newArr[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
  newArr[0] = PPM;
  for (int i = 1; i < 10; i++) {
    newArr[i] = doc["ppm"][i - 1];
  }
  for (int i = 0; i < 10; i++) {
    doc["ppm"][i] = newArr[i];
  }
  ppms = doc;
  serializeJson(doc, Serial);
  File saveFile = LittleFS.open("/ppm3.json", "w");
  serializeJson(doc, saveFile);
  return false;
}

bool loadPPM() {
  File ppmFile = LittleFS.open("/ppm3.json", "r");
  StaticJsonDocument<200> doc;
  JsonArray arr = doc.createNestedArray("ppm");
  if (!ppmFile) {
    Serial.println("Failed to open ppm file");

    for (int i = 0; i < 10; i++) {
      arr.add(0);
    }
    File saveFile = LittleFS.open("/ppm3.json", "w");
    serializeJson(doc, saveFile);

  }

  auto error = deserializeJson(doc, ppmFile);
  if (error) {
    Serial.println("Failed to parse ppm file");
    doc.createNestedArray("ppm");
    for (int i = 0; i < 10; i++) {
      arr.add(0);
    }
    File saveFile = LittleFS.open("/ppm3.json", "w");
    serializeJson(doc, saveFile);
  }
  ppms = doc;
  return false;
}







void ini_locations() {
  server.on("/", handle_OnConnect);
  server.on("/auth", handle_auth);
  server.on("/getData", handle_CO);
  server.on("/stats", handle_stat);
  server.on("/setAlt", handle_alt);
  server.on("/getWeather", handle_weather);
  server.on("/ppms", handle_graph);
  server.begin();
}

void ini_ap() {
  WiFi.softAP(ssid, password);
  WiFi.softAPConfig(local_ip, gateway, subnet);
  delay(100);
  connectedToWifi = false;
  ini_locations();
}

void connect_to_network(String _ssid, String _password) {
  WiFi.begin(_ssid, _password);
  Serial.println("Connect to: " + _ssid);
  int i = 0;
  while (WiFi.status() != WL_CONNECTED && i < 20) { // Wait for the Wi-Fi to connect
    delay(1000);
    Serial.print(i); Serial.print(' ');
    i++;
  }
  if (i >= 20) {
    WiFi.mode(WIFI_AP);
    ini_ap();
  } else {
    saveConfig(_ssid, _password);
    connectedToWifi = true;
    Serial.println('\n');
    Serial.println("Connection established!");
    Serial.print("IP address:\t");
    Serial.println(WiFi.localIP());
  }
}








void setup() {
  Serial.begin(115200);
  co2Serial.begin(9600);

  if (!bme.begin()) {
    Serial.println(F("Could not find a valid BME280 sensor, check wiring!"));
  } else {
    bmeConnected = true;
  }
  loadPPM();
  if (!LittleFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }

  bool error = !loadConfig();
  // Test if parsing succeeds.
  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println("HTTP server started");
    ini_ap();
  } else {
    ini_locations();
    connect_to_network(conf["ssid"], conf["password"]);

  }
}
void loop() {
  server.handleClient();

  unsigned long currentTime = millis();
  if (currentTime - previousTime >= eventInterval) {
    readCO2UART();
    temperature = bme.readTemperature();
    pressure = bme.readPressure();
    hum = bme.readHumidity();
    Serial.println("temperature: " + String(temperature) + " *C");
    Serial.println("pressure: " + String(pressure) + " Pa");
    Serial.println("humidity: " + String(hum) + " %");
    previousTime = currentTime;
  }
  if (currentTime - previousGraphTime >= graphInterval) {
    savePPM();
    previousGraphTime = currentTime;
  }
  if (alt > -1) {
    if (currentTime - previousWeatherTime >= weatherInterval) {
      cond.addP(pressure, temperature);
      previousWeatherTime = currentTime;
    }
  }
}

void handle_stat() {
  server.send(200, "application/json", "{\"wifi\":" + String(connectedToWifi) + ",\"bme\":" + String(bmeConnected) + ",\"mhz\":" + String(mhzConnected) + ",\"alt\":" + String(alt) + "}");
}

void handle_auth() {
  server.send(200, "text/plain", "Try to connect. To edit ssid/password plese go to root of 192.168.1.1");
  delay(100);
  WiFi.softAPdisconnect (true);
  int argsLen = server.args();
  Serial.println(argsLen);
  String _ssid = "";
  String _password = "";
  for (int i = 0; i < argsLen; i++) {
    Serial.println(server.argName(i));
    if (server.argName(i) == "ssid") {
      _ssid = server.arg(i);
    } else {
      _password = server.arg(i);
    }
    Serial.println(server.arg(i));
  }

  connect_to_network(_ssid, _password);

}


void handle_graph() {
  String out;
  serializeJson(ppms, out);
  server.send(200, "application/json", out);
}

void handle_alt() {
  server.send(200, "text/plain", "alt setted");
  Serial.println(server.argName(0));
  if (server.argName(0) == "alt") {
    alt = server.arg(0).toInt();
  }
  cond.setH(alt);
  saveConfig(conf["ssid"], conf["password"]);
}


void handle_weather() {
  server.send(200, "application/json", String(cond.getCast()));
}

void handle_CO() {
  server.send(200, "application/json", "{\"ppm\":" + String(PPM) + ", \"temp\":" + String(temperature) + ",\"presure\":" + String(pressure) + ",\"hum\":" + String(hum) + ",\"wi\":" + String(cond.getCast()) + "}");
}


void handle_OnConnect() {
  server.send(200, "text/html", SendHTML());
}

void handle_NotFound() {
  server.send(404, "text/plain", "Not found");
}



String SendHTML() {
  return "<html lang=\"en\"> <head> <meta charset=\"UTF-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /> <link rel=\"stylesheet\" href=\"style.css\" /> <title>Configuration</title> </head> <body style='text-align: center;'> <div style=\"width:300px;width:-webkit-fill-available;\"> <div style='text-align: center;padding: 10px' > <div>Network SSID</div> <input id = \"ssid\"style=\"width:-webkit-fill-available;\"></input> </div> <div style='text-align: center;padding: 10px' > <div>Network Password</div> <input id = \"password\" style=\"width:-webkit-fill-available;\"></input> </div> <div style='text-align: center;padding: 10px' > <button id = \"save\" onclick=\"if(document.querySelector('#ssid').value != '' && document.querySelector('#password')!=''){ console.log(window.location.href+'/'+document.querySelector('#ssid').value); window.open(window.location.href+'auth?ssid='+document.querySelector('#ssid').value+'&pass='+document.querySelector('#password').value, '_blank').focus(); }\">Check</button> </div> </div> </body> </html>";
}






int readCO2UART() {
  byte cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
  char response[9];
  Serial.println("Sending CO2 request...");
  co2Serial.write(cmd, 9); //request PPM CO2

  // clear the buffer
  memset(response, 0, 9);
  if (co2Serial.available() == 0) {
    Serial.println("MHZ-19B Error");
    mhzConnected = false;
    return 0;
  } else {
    mhzConnected = true;
  }
  if (co2Serial.available() > 0) {
    co2Serial.readBytes(response, 9);

  }
  // print out the response in hexa
  for (int i = 0; i < 9; i++) {
    Serial.print(String(response[i], HEX));
    Serial.print("   ");
  }
  Serial.println("");
  // checksum
  byte check = getCheckSum(response);
  if (response[8] != check) {
    Serial.println("Checksum not OK!");
    Serial.print("Received: ");
    Serial.println(response[8]);
    Serial.print("Should be: ");
    Serial.println(check);
  }
  // ppm
  int ppm_uart = 256 * (int)response[2] + response[3];
  Serial.print("UART CO2 PPM: ");
  Serial.println(ppm_uart);
  // status
  byte status = response[5];
  Serial.print("Status: ");
  Serial.println(status);
  if (status == 0x40) {
    Serial.println("Status OK");
  }
  PPM = ppm_uart;
  return ppm_uart;
}

byte getCheckSum(char *packet) {
  byte i;
  unsigned char checksum = 0;
  for (i = 1; i < 8; i++) {
    checksum += packet[i];
  }
  checksum = 0xff - checksum;
  checksum += 1;
  return checksum;
}

Pins:

  • MHZ-19B TX - D3
  • MHZ-19B RX - D4
  • MHZ-19B VIN - VV
  • MHZ-19B GND - G
  • BME280 3.3 - 3V
  • BME280 GND - G
  • BME280 SCL - D1
  • BME280 SDA - D2

Next, I realized that I needed some kind of interface. To do this I wrote a simple flutter application. I can upload the source code to the git later, if needed. All in all it wasn’t too bad.
For the weather prediction to work you need to go to the settings and click on the button in the upper right corner to determine the altitude.
I’m also too lazy to sign applications, sorry…



app-release.apk (20.3 MB)

In the end we have a weather station and an application. At the first start or if we could not connect to the access point, we create our own. If we connect to it and go to the address of the gateway - we get to the menu of connection to the router. In fact, it is not necessary to connect the board to the router, you can sit on the access point of the board itself.

Also, if you want to repeat the project, remember to calibrate the sensor by shorting the HD pin to GND

4 Likes