📜 ⬆️ ⬇️

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).

Introduction


There are many different nightmares about the ruthless black mold that kills people, and mold likes wet rooms. Fighting mold is difficult, but the very first thing to watch out for is air humidity. It is worth remembering more about humidity as winter sets in, since by heating cold air we thereby lower its humidity, and too dry air adversely affects mucous membranes, drastically reducing immunity.

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

The Internet also found such a table of permissible concentrations of CO2 in the air:
')
Summary table of permissible values ​​of CO2 concentration in the air

So, we will need:


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

Components for assembly

Carbon dioxide sensor

The carbon dioxide sensor should be stopped separately. While the pressure and humidity sensor gives readings already in digital, there are no stabilization circuits for power, the cost of a fully digital CO2 sensor starts from $ 120, a bit too much like "to play." The Chinese offer the cheapest in the form of a module to Arduine based on the MG811 for $ 36. I looked at its characteristics, and there was 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 was the TGS4161, I quote its characteristics:

MG811

Reassured by the stability of the readings, I made a purchase and the sensor arrived in two weeks.

Device assembly

When the module came to me, I already had everything assembled on the breadboard, only it was missing. Armed with a soldering iron, I turned my gaze to the advertising picture (see above), saying 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, the module is made by the Chinese piece. What to do? I had to poke the module with a tester at my feet, blow the sensor, involve such and such a mother, write a rabbit ... At random, it was found that the module contains two operational amplifiers. The first of them has an input impedance of 1.5 TΩ (you were right tera-ohm, i.e. 1500 gig-ohm, while a conventional digital multimeter has an input impedance of no more than 20 mega-ohm), the second dual op amp works as a comparator and repeater. Aout is a certain analogue signal from the repeater, Dout - a signal from the comparator (active if CO2 is off scale). The destination of the TCM output could not be established.

How does the sensor work?

The sensor is an electrochemical cell for which you need a high temperature. The temperature is provided by a built-in heater of about 0.2W. The voltage on the cell at a CO2 concentration of 350 ppm and below has a certain stable value, and when the CO2 concentration rises, the voltage on the cell also changes (decreases). For matching high output impedance of the cell and for the purpose of voltage amplification, OA are applied.

To use the sensor you need to convert volts to CO2 concentration. The documentation on the sensor states that the initial voltage on the sensor can be any, but the absolute change in this voltage is strictly datasheet. This means that at a minimum it is necessary to measure the zero starting point in fresh air, then, build a sensor model based on its sensitivity graph, setting the measured value as a reference point. In general, one way or another, the model was built, the sensor was calibrated and it was possible to put everything together in some building:

This is the first Hi, strongly not scold

Inside the device - noodles

The sensitivity of the freshly calibrated sensor was such that it could reliably distinguish more fresh air in the park from a gassed avenue for 300 meters.

For the rest of the sensors: the humidity measures perfectly, by picking up the device you can immediately notice an increase in humidity, straight jumps (still, when the apartment is now only 29% humidity); 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 did not end there, but everything was just beginning, because when the device switched to autonomous power it abruptly poplohelo and the history of its treatment deserves a separate topic, here is an oscillogram from the medical history:

And this is just the beginning.

UPD:
The wiring diagram is typical for these sensors, in more detail about pinout in the sketch.
Work sketch for Arduino UNO
Sketch was going to (copy-paste) from different pieces, so do not scold much, I hope you can understand it
#include <SFE_BMP180.h> #include <Wire.h> #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 DHT dht(DHTPIN, DHTTYPE); #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_RESET, 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) { LcdCharacter(*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; } temp[0]='\0'; outstr[0]='\0'; if(val < 0.0){ strcpy(outstr,"-\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; while(precision--) mult *=10; if(val >= 0) frac = (val - int(val)) * mult; else frac = (int(val)- val ) * mult; unsigned long frac1 = frac; while(frac1 /= 10) padding--; while(padding--) strcat(outstr,"0\0"); strcat(outstr,itoa(frac,temp,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'; strcat(temp,outstr); strcpy(outstr,temp); } 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) { //analogReference(INTERNAL); LcdInitialise(); LcdClear(); gotoXY(0,0); if (pressure.begin()) LcdString("BMP180 init success"); else { // 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. } dht.begin(); } void loop(void) { sensorValue = analogRead(sensorPin); delay(50); sensorValue = analogRead(sensorPin); delay(50); char buf[20]; float value=sensorValue*adc_step; gotoXY(0,0); LcdString("CO2:"); LcdString(floatToString(buf, VoltageToPPM(value),1,5)); LcdString("ppm"); gotoXY(0,1); LcdString("CO2 V:"); LcdString(floatToString(buf, value,4,5)); gotoXY(0,2); delay(50); sensorValue = analogRead(vBattPin); delay(50); sensorValue = analogRead(vBattPin); value=sensorValue*adc_step; 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: delay(status); // 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: //gotoXY(0,3); // 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: delay(status); // 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: gotoXY(0,5); //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("-"); LcdString(floatToString(buf, p0*0.7501,1,4)); //Serial.print(" mb, "); //Serial.print(p0*0.0295333727,2); //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(); gotoXY(0,4); if (isnan(t) || isnan(h)) { LcdString("Failed to read from DHT"); } else{ LcdString(floatToString(buf, h,1,4)); LcdString("%"); } gotoXY(0,3); LcdString("temp: "); LcdString(floatToString(buf, t,1,4)); LcdString(" C"); delay(1000); } 

Source: https://habr.com/ru/post/242521/


All Articles