Tommy Bianco
Published © CC BY-NC-ND

My pocket watch tells CO2

A story of a spare SCD30 inspiring a highly portable weather station to fight Sick Building Syndrome and (maybe) COVID-19 spread.

IntermediateFull instructions provided7 hours425
My pocket watch tells CO2

Things used in this project

Hardware components

SparkFun CO₂ Humidity and Temperature Sensor - SCD30
×1
Elegoo Real Time Clock (DS1307)
×1
Wemos D1 clone (ESP-12F)
×1
0.96" OLED 64x128 Display Module
ElectroPeak 0.96" OLED 64x128 Display Module
×1
Photo resistor
Photo resistor
×1
Pushbutton switch 12mm
SparkFun Pushbutton switch 12mm
×3
Resistor 220 ohm
Resistor 220 ohm
×2

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Schematics

Breadboard layout

Code

Final Script

Arduino
/*  |||||||||||||||||||||||||||||||||||||||||||||||||||||
 *  ||||||                WiFi Setup               ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

/*  Unfortunately, just initiating the WiFi connection overloads my D1 clone, leading
 *  to inconsistent OLED output (temporary freezes) and other bugs -> idea shelved 
*/
 
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <WiFiUdp.h>

char ssid[] = "SSID";         //  your network SSID (name)
char pass[] = "PasSWoRd";     // your network password 

/*  |||||||||||||||||||||||||||||||||||||||||||||||||||||
 *  ||||||            0.96 OLED Setup              ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for SSD1306 display connected using software SPI (default case):
#define OLED_MOSI  D4 // SDA
#define OLED_CLK   D3 // SCK
#define OLED_DC    D7 // DC
#define OLED_CS    D8 // CS
#define OLED_RESET D6 // RES
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT,
  OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

void display_YesNO(){
    display.fillTriangle (120, 3, 127, 1, 124, 7, 1);
    display.fillTriangle (120, 61, 127, 63, 124, 57, 1);
    display.setCursor(103, 8);
    display.println (F("Yes"));
    display.setCursor(105, 51);
    display.println (F("No"));
}
void display_startup(){
  display.begin();
  display.clearDisplay();               // Clear the OLED buffer
  display.setTextSize(2);               // Enlarged text for the startup screen
  display.setTextColor(SSD1306_WHITE);  // Draw white text
  display.drawLine(17, 17, 25, 25,1);
  display.drawLine(18, 17, 26, 25,1);
  display.drawLine(25, 17, 17, 25,1);
  display.drawLine(26, 17, 18, 25,1);
  display.drawLine(37, 17, 45, 25,1);
  display.drawLine(38, 17, 46, 25,1);
  display.drawLine(45, 17, 37, 25,1);
  display.drawLine(46, 17, 38, 25,1);
  display.drawCircle(31,31,30,1);
  display.drawCircle(31,31,28,1);
  display.drawCircle(31,45,3,1);
  display.drawCircle(33,40,3,1);
  display.drawCircle(29,43,3,1);
  display.drawCircle(34,47,3,1);
  display.drawCircle(28,47,3,1);
  display.drawCircle(37,43,3,1);
  display.drawCircle(23,43,3,1);
  display.setCursor(75, 5);
  display.println (F("Time"));
  display.setCursor(75, 25);
  display.println (F("to"));
  display.setCursor(75, 45);
  display.println (F("air"));
  display.display();
  display.setTextSize(1);                 // Normal 1:1 pixel scale
  delay(2000);
}

/*  |||||||||||||||||||||||||||||||||||||||||||||||||||||
 *  ||||||      Buttons & Photoresistor setup      ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

const int button [2] = {D4, D3};
bool button_state [2] = {HIGH};

void buttonCheck (int b){if (digitalRead(button[b])==LOW){button_state[b]=LOW;}}
void buttonReset (int b){button_state [b] = HIGH;}

#define photoresistor A0
int photoValue = 0;

void photoCheck () {            // Moving average filter
  photoValue = (photoValue+analogRead (photoresistor))/2;
}

/*  |||||||||||||||||||||||||||||||||||||||||||||||||||||
 *  ||||||          Display brightness             ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

void setContrast_subfunciton (int brightn = 255, int precharge = 34) {
  display.ssd1306_command(SSD1306_SETCONTRAST);         
  display.ssd1306_command(brightn);
  display.ssd1306_command(SSD1306_SETPRECHARGE);      
  display.ssd1306_command(precharge);
}

void setContrast(){
  photoCheck();
  if (photoValue >= 1000){setContrast_subfunciton();}
  // Gaps in the range below to reduce display flickering at border values
  if ((photoValue <= 900)&&(photoValue >= 600)){setContrast_subfunciton(255, 16);}
  if (photoValue < 500){setContrast_subfunciton(1, 16);}
}

/*  |||||||||||||||||||||||||||||||||||||||||||||||||||||
 *  ||||||         SCD30 air sensor setup          ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

//Click here to get the library: http://librarymanager/All#SparkFun_SCD30
#include <Wire.h>
#include "SparkFun_SCD30_Arduino_Library.h"
SCD30 airSensor;

int CO2=0;
float T=0;
float T_offset=0;
float T_start=0;
int H=0;
int basal_level=417; // Basal CO2 level (ppm) corresponding to fresh air (in 2021)
bool skipOnStartUp = true;

/*  |||||||||||||||||||||||||||||||||||||||||||||||||||||
 *  ||||||          Air quality to text            ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

void displayText_AirQuality (){
  // Displaying air quality data from SCD30
  if (airSensor.dataAvailable()){
    CO2=airSensor.getCO2();
    T=airSensor.getTemperature();
    H=airSensor.getHumidity();}
  display.print(F("CO2:  ")); 
  display.print(CO2);   display.println(F(" ppm"));
  display.print(F("Temp: "));
  display.print(T+T_offset);     display.println(F(" C"));
  display.drawCircle(69, display.getCursorY() - 6, 1, WHITE);      // https://stackoverflow.com/questions/47955775/display-degree-symbol-in-arduino-oled
  Serial.println (display.getCursorY());
  display.print(F("Hum.: "));
  display.print(H);     display.println(F(" %"));}

/*  |||||||||||||||||||||||||||||||||||||||||||||||||||||
 *  ||||||         Calibration functions           ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

/* Routing function choosing between calibrate_T () and calibrate_CO2 () */

unsigned long run_timer = millis();

void calibrate () {
  if (millis()-run_timer<120000){
    display.fillTriangle (120, 61, 127, 63, 124, 57, 1);
    display.setCursor(105, 51);
    display.println (F(" T"));
    display.drawCircle(118, display.getCursorY()-7, 1, WHITE);      // https://stackoverflow.com/questions/47955775/display-degree-symbol-in-arduino-oled
    calibrate_T ();
  }
  else {
    display.fillTriangle (120, 61, 127, 63, 124, 57, 1);
    calibrate_CO2 ();
    display.setCursor(102, 51);
    display.println (F("CO2"));
  }
}

/*  The function below offers the user to record temperature offset due to device heating 
 *  by subtractibg the current temperature from the startup value. Only makes sense if the 
 *  device was idle before startup. This version does not use the SCD30 non-volatile memory,
 *  so the value is lost when the device is turned off.
*/
 
void calibrate_T () {
  if (millis()-run_timer<5000) {
    T_start = T;
  }
  buttonCheck (1);
  if (button_state [1] == LOW){
    buttonReset (1);
    if (skipOnStartUp == true){skipOnStartUp=false; return;}
    display.clearDisplay();
    display.setCursor(0, 24);
    display.println (F("Do you want to offset"));
    display.println (F("temperature?"));
    display_YesNO();
    display.display();
    delay(250);
    while ((button_state [0] == HIGH)&&(button_state [1] == HIGH)){
      buttonCheck(0);
      buttonCheck(1);
      delay (100);}
    if (button_state [1] == LOW){
      buttonReset(0);
      buttonReset(1);
      return;}
    // Record temperature offset
    T_offset = T_start - T; 
    display.clearDisplay();
    display.setCursor(0, 24);
    display.println (F("Temperatire offset"));
    display.print (F("is: "));
    display.print (T_offset); display.print (F(" ppm"));
    display.display();
    buttonReset(0);
    buttonReset(1);
    delay(1000);}}

/*   The function below offers the user to calibrate CO2 sensor. According to the module
 *   documentation, SCD30 has to run for at least 2 minutes before  calibration -> function 
 *   can only be called after two minutes runtime. This function uses the SCD30 non-volatile 
 *   memory, allowing to preserve the value when the device is not powered.
*/

void calibrate_CO2 () {
  buttonCheck (1);
  if (button_state [1] == LOW){
    buttonReset (1);
    if (skipOnStartUp == true){skipOnStartUp=false; return;}
    display.clearDisplay();
    display.setCursor(0, 24);
    display.println (F("Do you want to"));
    display.println (F("calibrate CO2?"));
    display_YesNO();
    display.display();
    delay(250);
    while ((button_state [0] == HIGH)&&(button_state [1] == HIGH)){
      buttonCheck(0);
      buttonCheck(1);
      delay (100);}
    if (button_state [1] == LOW){
      buttonReset(0);
      buttonReset(1);
      return;}
    // Start forced recalibration
    airSensor.setForcedRecalibrationFactor(basal_level);
    display.clearDisplay();
    display.setCursor(0, 24);
    display.println (F("Forced recalibration"));
    display.print (F("factor is: "));
    display.print (basal_level); display.print (F(" ppm"));
    display.display();
    buttonReset(0);
    buttonReset(1);
    delay(1000);}}

/*  |||||||||||||||||||||||||||||||||||||||||||||||||||||
 *  ||||||          Time / Date to text            ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

#include <DS3231.h>

RTClib myRTC;
DateTime dt = myRTC.now();

void displayText_Time(){
  dt = myRTC.now();
  display.print (F("Time: "));
  if (dt.hour()<10){display.print(0);}
  display.print(dt.hour());   display.print(F(":"));
  if (dt.minute()<10){display.print(0);}
  display.print(dt.minute()); display.print(F(":"));
  if (dt.second()<10){display.print(0);}
  display.println (dt.second());
  display.print (F("Date: "));
  switch (dt.month()){
    case 1:
      display.print (F("January "));
      break;
    case 2:
      display.print (F("February "));
      break;
    case 3:
      display.print (F("March "));
      break;
    case 4:
      display.print (F("April "));
      break;
    case 5:
      display.print (F("May "));
      break;
    case 6:
      display.print (F("June "));
      break;
    case 7:
      display.print (F("July "));
      break;
    case 8:
      display.print (F("August "));
      break;
    case 9:
      display.print (F("September "));
      break;
    case 10:
      display.print (F("October "));
      break;
    case 11:
      display.print (F("November "));
      break;
    case 12:
      display.print (F("December "));
      break;}
  display.print(dt.day());
  display.println(F("th"));
}

/*  |||||||||||||||||||||||||||||||`||||||||||||||||||||||
 *  ||||||           Main program body             ||||||
 *  ||||||||||||||||||||||||||||||||||||||||||||||||||||| */

void setup() {
  //Serial.begin(9600);
  //WiFi.persistent(false);
  //WiFi.begin(ssid, pass);
  pinMode(button [0], INPUT_PULLUP);
  pinMode(button [1], INPUT_PULLUP);
  Wire.begin();
  // SCD30 air sensor setup
  airSensor.begin();
  airSensor.setAutoSelfCalibration(false); 
  //airSensor.setMeasurementInterval (5);  // https://www.mouser.com/pdfdocs/CD_AN_SCD30_Low_Power_Mode_D2.pdf
  // OLED setup
  display_startup();
}

/* Don't forget to set cursor position, clear the buffer and call display.display(); */
unsigned long screen_timer = millis();    // display refresh rate

void loop() {
  buttonCheck (1);
  if (millis() - screen_timer <= 100){return;} // Screen refresh rate 10 FPS
  else {screen_timer = millis();}
  setContrast();
  display.clearDisplay();
  display.setCursor(0, 0);        // Start at top-left corner
  displayText_Time();         display.println();
  displayText_AirQuality();   display.println();
  calibrate ();
  if ((dt.hour()==165)||(dt.day()==165)){            // Getting rid of a bug: sometimes all readouts equal 165 for some obscure reason....
    display.clearDisplay(); 
    return;
  }
  display.display();
  }

Credits

Tommy Bianco
5 projects • 2 followers
A biologist procrastinating
Contact

Comments

Please log in or sign up to comment.