Arduino in the service of health

The article describes the path from the idea to creating a home portable air quality analyzer (CO2, humidity, temperature, pressure).


There are many different nightmares on the net about the ruthless black mold that kills people, and the mold loves wet rooms. It’s hard to deal with mold, but the very first thing you need to monitor is air humidity. Humidity should also be remembered with the onset of winter, since by heating cold air we thereby lower its humidity, and too dry air negatively affects the mucous membranes, drastically reducing immunity.

Arguing in this way, I came to the conclusion that it would not be bad to build such a humidity indicator on the Arduina base. Taking the opportunity and the cheapness of digital sensors, it was a sin not to add the function of a barometer, thermometer and, importantly, the function of measuring the amount of carbon dioxide in the air (in old high-rise buildings there is often poor ventilation in the apartments, and the apartments themselves are tightly sealed for the winter). There are also a lot of horror stories about carbon dioxide, decreased performance, headache, these may be symptoms of a high concentration of CO2 in the air.

The Internet also found such a plate of permissible concentrations of CO2 in the air:

Summary table of acceptable values ​​of CO2 concentration in air

So, we need:
  • 1. Arduino Pro Mini price $ 2.15
  • 2. The humidity and temperature sensor DHT-22 aka AM2302 (improved accuracy compared to DHT-11), price $ 4
  • 3. Pressure sensor BMP180, price 2.98 $
  • 4. Module with carbon dioxide sensor TGS4161, price $ 60
  • 5. Display type Nokia 5110, price $ 2.35

Total : $ 71.48 (industrial devices with a similar set of sensors cost from $ 300)

Build Components

Carbon dioxide sensor

The carbon dioxide sensor should be stopped separately. While the pressure and humidity sensors give readings already in numbers, they have some kind of power supply stabilization circuit, the cost of a fully digital CO2 sensor starts at $ 120, a bit much like “to play around”. The cheapest in the form of a module for Arduin, the Chinese offer on the basis of MG811 for $ 36. I looked at its characteristics, and there a wild dependence on humidity, the presence of extraneous gases in the air and decided that it was necessary to look for something more accurate and this turned out to be TGS4161, I bring its characteristics:


Having calmed down at the expense of stability of readings, I made a purchase and the sensor arrived through two weeks.

Device assembly

When the module came to me, I already had everything assembled on a breadboard, only it was missing. Armed with a soldering iron, I turned my eyes to the advertising picture (see above), they say how to connect it? Here I had to be a little taken aback, the hope that Dout is a digital output did not materialize, there is no documentation for this module at all and nowhere else, the Chinese make the module piece by piece. What to do? I had to poke at the feet of the module with a tester, blow on the sensor, draw such a mother, write to the rabbit ... Using the poke method, it was established that the module contains two operational amplifiers. The first of them has an input impedance of 1.5 TH(You were not mistaken precisely with tera-Ohm, i.e. 1500 gig-Ohm, while a conventional digital multimeter has an input impedance of not more than 20 mega-Ohm), the second dual op-amp operates as a comparator and repeater. Aout is a certain analog signal from the repeater, Dout is a signal from the comparator (active if CO2 goes off scale). TCM pin assignment could not be established.

How does the sensor work?

The sensor is an electrochemical cell which requires high temperature to operate. The temperature is provided by a heater built in the sensor with a power of approximately 0.2W. The voltage on the cell at a CO2 concentration of 350ppm and below has a certain stable value, and when the concentration of CO2 increases, the voltage on the cell also changes (decreases). To match the high output impedance of the cell and to amplify the voltage, opamps are used.

To use the sensor, you need to convert volts to CO2 concentration. The documentation for the sensor claims that the initial voltage on the sensor can be any, but the absolute change in this voltage is strictly according to the datasheet. So you need to at least measure the zero starting point in fresh air, then build a model of the sensor based on a graph of its sensitivity by setting the measured value as a reference point. In general, at the very least, the model was built, the sensor was calibrated and everything could be assembled together in some kind of housing:

This is the first pancake, do not scold much

Inside the appliance is noodles

The sensitivity of the freshly calibrated sensor was such that it could reliably distinguish the fresher air in the park from the gas-filled avenue 300 meters away.

For the rest of the sensors: humidity measures perfectly, picking up the device you can immediately notice the increase in humidity, just the same jump (even if the humidity in the apartment is only 29% now); the pressure also measures perfectly, both absolute and relative, by raising and lowering the device, you can notice the difference in pressure.

Followed by...

The story didn’t end there, but it was just beginning, because when the device switched to autonomous power, it suddenly went bad and the treatment history deserves a separate topic, here is an oscillogram from the medical history:

And this is just the beginning

Connection diagram typical for the indicated sensors, more about pinout in sketch.
Work sketch for Arduino UNO
The sketch was going to (copied) from different pieces, so do not scold much, I hope you can figure it out
#include "DHT.h"
#define PIN_SCE   7  // LCD CS  .... Pin 2
#define PIN_RESET 6  // LCD RST .... Pin 1
#define PIN_DC    5  // LCD Dat/Com. Pin 3
#define PIN_SDIN  4  // LCD SPIDat . Pin 4
#define PIN_SCLK  3  // LCD SPIClk . Pin 5
                     // LCD Gnd .... Pin 8
                     // LCD Vcc .... Pin 6
                     // LCD Vlcd ... Pin 7
#define LCD_C     LOW
#define LCD_D     HIGH
#define LCD_X     84
#define LCD_Y     48
#define LCD_CMD   0
SFE_BMP180 pressure;
#define DHTPIN 10
#define DHTTYPE DHT22 
#define ALTITUDE 113.0 // Altitude of SparkFun's HQ in Boulder, CO. in meters
int a = 0;
int sensorPin = A0;    // вход подключается к Aout модуля с датчиком СО2
int vBattPin = A1;    // вход используется для мониторинга за состояние батареи
int sensorValue = 0;  // variable to store the value coming from the sensor
float adc_step=5/1024.0f; //шаг измерения АЦП 
float a_k=5.0894E-7; //магическая константа модели датчика СО2
float b_k=6.7303E-4;//еще одна магическая константа модели датчика СО2
float v_0=1.09; //напряжение на выходе датчика на свежем воздухе
int ledPin=13;
static const byte ASCII[][5] =
 {0x00, 0x00, 0x00, 0x00, 0x00} // 20  
 ,{0xff, 0xff, 0xff, 0xff, 0xff} // 21 !
//,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ¥
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j 
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e ←
,{0x00, 0x06, 0x09, 0x09, 0x06} // 7f →
void LcdCharacter(char character)
  LcdWrite(LCD_D, 0x00);
  for (int index = 0; index < 5; index++)
    LcdWrite(LCD_D, ASCII[character - 0x20][index]);
  LcdWrite(LCD_D, 0x00);
void LcdClear(void)
  for (int index = 0; index < LCD_X * LCD_Y / 8; index++)
    LcdWrite(LCD_D, 0x00);
void LcdInitialise(void)
  pinMode(PIN_SCE,   OUTPUT);
  pinMode(PIN_DC,    OUTPUT);
  pinMode(PIN_SDIN,  OUTPUT);
  pinMode(PIN_SCLK,  OUTPUT);
  digitalWrite(PIN_RESET, LOW);
 // delay(1);
  digitalWrite(PIN_RESET, HIGH);
  LcdWrite( LCD_CMD, 0x21 );  // LCD Extended Commands.
  LcdWrite( LCD_CMD, 0xBf );  // Set LCD Vop (Contrast). //B1
  LcdWrite( LCD_CMD, 0x04 );  // Set Temp coefficent. //0x04
  LcdWrite( LCD_CMD, 0x14 );  // LCD bias mode 1:48. //0x13
  LcdWrite( LCD_CMD, 0x0C );  // LCD in normal mode. 0x0d for inverse
  LcdWrite(LCD_C, 0x20);
  LcdWrite(LCD_C, 0x0C);
void LcdString(char *characters)
  while (*characters)
void LcdWrite(byte dc, byte data)
  digitalWrite(PIN_DC, dc);
  digitalWrite(PIN_SCE, LOW);
  shiftOut(PIN_SDIN, PIN_SCLK, MSBFIRST, data);
  digitalWrite(PIN_SCE, HIGH);
// gotoXY routine to position cursor 
// x - range: 0 to 84
// y - range: 0 to 5
void gotoXY(int x, int y)
  LcdWrite( 0, 0x80 | x);  // Column.
  LcdWrite( 0, 0x40 | y);  // Row.  
char * floatToString(char * outstr, double val, byte precision, byte widthp){
  char temp[16];
  byte i;
  // compute the rounding factor and fractional multiplier
  double roundingFactor = 0.5;
  unsigned long mult = 1;
  for (i = 0; i < precision; i++)
    roundingFactor /= 10.0;
    mult *= 10;
  if(val < 0.0){
    val = -val;
  val += roundingFactor;
  strcat(outstr, itoa(int(val),temp,10));  //prints the int part
  if( precision > 0) {
    strcat(outstr, ".\0"); // print the decimal point
    unsigned long frac;
    unsigned long mult = 1;
    byte padding = precision -1;
      mult *=10;
    if(val >= 0)
      frac = (val - int(val)) * mult;
      frac = (int(val)- val ) * mult;
    unsigned long frac1 = frac;
    while(frac1 /= 10)
  // generate space padding 
  if ((widthp != 0)&&(widthp >= strlen(outstr))){
    byte J=0;
    J = widthp - strlen(outstr);
    for (i=0; i< J; i++) {
      temp[i] = ' ';
    temp[i++] = '\0';
  return outstr;
void drawLine(void)
  unsigned char  j;  
   for(j=0; j<84; j++) // top
          gotoXY (j,0);
	  LcdWrite (1,0x01);
  for(j=0; j<84; j++) //Bottom
          gotoXY (j,5);
	  LcdWrite (1,0x80);
  for(j=0; j<6; j++) // Right
          gotoXY (83,j);
	  LcdWrite (1,0xff);
	for(j=0; j<6; j++) // Left
          gotoXY (0,j);
	  LcdWrite (1,0xff);
float VoltageToPPM(float voltage)
  return pow(b_k, voltage)/a_k;
void setup(void)
    if (pressure.begin())
    LcdString("BMP180 init success");
    // Oops, something went wrong, this is usually a connection problem,
    // see the comments at the top of this sketch for the proper connections.
   LcdString("BMP180 init fail\n\n");
    while(1); // Pause forever.
void loop(void)
  sensorValue = analogRead(sensorPin);
  sensorValue = analogRead(sensorPin);
  char buf[20];
  float value=sensorValue*adc_step;
  LcdString(floatToString(buf, VoltageToPPM(value),1,5));
   LcdString("CO2 V:");
  LcdString(floatToString(buf, value,4,5));
  sensorValue = analogRead(vBattPin);
  sensorValue = analogRead(vBattPin);
  LcdString("V batt:");
  LcdString(floatToString(buf, value,3,4));
//  static bool led_on_off=true;
//    digitalWrite(ledPin, led_on_off);
//  led_on_off=!led_on_off;
    char status;
  double T,P,p0,a;
  // Loop here getting pressure readings every 10 seconds.
  // If you want sea-level-compensated pressure, as used in weather reports,
  // you will need to know the altitude at which your measurements are taken.
  // We're using a constant called ALTITUDE in this sketch:
  // If you want to measure altitude, and not pressure, you will instead need
  // to provide a known baseline pressure. This is shown at the end of the sketch.
  // You must first get a temperature measurement to perform a pressure reading.
  // Start a temperature measurement:
  // If request is successful, the number of ms to wait is returned.
  // If request is unsuccessful, 0 is returned.
  status = pressure.startTemperature();
  if (status != 0)
    // Wait for the measurement to complete:
    // Retrieve the completed temperature measurement:
    // Note that the measurement is stored in the variable T.
    // Function returns 1 if successful, 0 if failure.
    status = pressure.getTemperature(T);
    if (status != 0)
      // Print out the measurement:
     // LcdString("temp: ");
      //LcdString(floatToString(buf, T,1,4));
     // LcdString(" C");
      // Start a pressure measurement:
      // The parameter is the oversampling setting, from 0 to 3 (highest res, longest wait).
      // If request is successful, the number of ms to wait is returned.
      // If request is unsuccessful, 0 is returned.
      status = pressure.startPressure(2);
      if (status != 0)
        // Wait for the measurement to complete:
        // Retrieve the completed pressure measurement:
        // Note that the measurement is stored in the variable P.
        // Note also that the function requires the previous temperature measurement (T).
        // (If temperature is stable, you can do one temperature measurement for a number of pressure measurements.)
        // Function returns 1 if successful, 0 if failure.
        status = pressure.getPressure(P,T);
        if (status != 0)
          // Print out the measurement:
          //lcd.print("ap ");
          LcdString(floatToString(buf, P*0.7501,1,4));
          //lcd.print(" mb");
          // The pressure sensor returns abolute pressure, which varies with altitude.
          // To remove the effects of altitude, use the sealevel function and your current altitude.
          // This number is commonly used in weather reports.
          // Parameters: P = absolute pressure in mb, ALTITUDE = current altitude in m.
          // Result: p0 = sea-level compensated pressure in mb
          p0 = pressure.sealevel(P,ALTITUDE); // we're at 1655 meters (Boulder, CO)
          LcdString(floatToString(buf, p0*0.7501,1,4));
          //Serial.print(" mb, ");
          //Serial.println(" inHg");
          // On the other hand, if you want to determine your altitude from the pressure reading,
          // use the altitude function along with a baseline pressure (sea-level or other).
          // Parameters: P = absolute pressure in mb, p0 = baseline pressure in mb.
          // Result: a = altitude in m.
          a = pressure.altitude(P,p0);
        else LcdString("error retrieving pressure measurement\n");
      else LcdString("error starting pressure measurement\n");
    else LcdString("error retrieving temperature measurement\n");
  else LcdString("error starting temperature measurement\n");
  float h = dht.readHumidity();
  float t = dht.readTemperature();
  if (isnan(t) || isnan(h)) {
   LcdString("Failed to read from DHT");
    LcdString(floatToString(buf, h,1,4));
     LcdString("temp: ");
     LcdString(floatToString(buf, t,1,4));
     LcdString(" C");

Also popular now: