Mobiler CO2 Sensor

CO2 Gehalt in der Luft mobil überwachen.

Mobiler CO2 Sensor

In Zeiten des steigenden Kohlendioxids in der Luft wäre es doch mal schön zu sehen, wie schlecht die Luft wirklich ist. Getestet habe ich zwei Messverfahren. Das erste mit einem Heiz-Sensor (MQ-135). Dieser ist nicht Hauptsächlich für CO2 ausgelegt. Man kann ihn aber auf CO2 Kalibrieren. Das funktioniert auch in einem gewissen Maß. Leider misst der auch andere Gase und zeigt deshalb manchmal verfälschte Werte. Der andere aber entscheidende Nachteil: Der Sensor braucht sehr viel Energie. Zu viel um in mit einer Batterie zu betreiben.
Das schöne wäre jedoch gewesen, dass man diesen Sensor für 2-3€ kaufen kann. Als nächstes habe ich einen Infrarot-Sensor ausprobiert. Dieser kostet ~20€. Ich habe mich für den MH-Z19 entschieden. Der kann 0-2000ppm CO2 messen. Man kann ihn auch bis 5000pm bekommen. Mir reichen aber die 2000ppm, da höchstens 1000ppm in Räumen empfohlen wird. Aktuell liegt die Außenluft bei 400ppm. Somit ein guter Messbereich.
mh-z19
mh-z19_pins
Besonders schön ist, dass man den Sensor mit UART (RS232, 9600@8N1) auslesen kann.

Hardware

  • Arduino Pro Mini (5V 16MHz)
  • MH-Z19 CO2 Sensor
  • 0.96 Zoll 128x64 OLED-Display
  • TP4056 Laderegler
  • LiPo
  • BMP180 Temperatur und Luftdruck (optional)

IMG_20180113_135408

Als Box habe ich eine Anschlussbox in der größe 85x85mm genommen.
Für das Display habe ich in den Deckel einen kleinen Schlitz geschnitten. Auch ohne es fest zu kleben hält das Display so sehr gut.

IMG_20180113_135236

Den Laderegler habe ich mit Heißkleber innen fest gemacht. Hier musste ich mehrere Male nachbessern. Die Kraft, die man braucht, um den Stecker anzustecken war teilweise zu groß.

IMG_20180113_135259

Für den Arduino habe ich das Newbie-PCB von Sunberg84 genommen. Auch ohne NRF-Chip ein gutes Board.

Programmierung

Display und BMP180 Sensor werden mit kompatiblen Bibliotheken eingebunden. Für den MH-Z19 habe ich selbst eine Bibliothek geschrieben. Das Interface für diesen Sensor ist sehr einfach gehalten.
Nähere Informationen hat bertrik sehr gut zusammen getragen: https://revspace.nl/MHZ19

Abhängigkeiten

Für die Sensoren und zum Strom sparen sind folgende Bibliotheken eingebunden.

#include <z19.h>                // Bibliothek für den CO2-Sensor [MH-Z19-Lib](/attachments/MH-Z19-Lib.zip)
#include <SFE_BMP180.h>         // Bibliothek für den Drucksensor https://github.com/sparkfun/BMP180_Breakout
#include <Wire.h>               // Bibliothek für die Displaykommunikation
#include <SSD1306AsciiWire.h>   // Bibliothek für das Display https://github.com/greiman/SSD1306Ascii
#include <LowPower.h>           // Bibliothek zum Schlafen: https://github.com/rocketscream/Low-Power

Quellcode

Download: Sensor_MHZ19_Mobil.zip

Sensor_MHZ19_Mobil.ino


#define SERIAL_DEBUG

#define BATTERY_SENSE_PIN PIN_A0

#include <time.h>
boolean timeReceived = false;
unsigned int cycle = 0;
unsigned long lastControllerTime = 0;

//LowPower
#include <LowPower.h>

#include <z19.h>
Z19 CO2Sensor;

//Pressure
#include <SFE_BMP180.h>
SFE_BMP180 pressure;
double T, P, p0, a;
void processBmp();
void getReadings(float &temp, float &press);

float batteryV = 0;
int batteryPcnt = 0;
const unsigned long SLEEP_TIME = 60000UL;

int Co2ppmMin;
int Co2ppmMax;
int lastCo2ppm;

void writeValues(int CO2 = -1, int CO2UC = -1, float RZero = -1.0, float temp = -1, float humid = -1.0, int pressure = -1, float battV = -1.0, int battP=-1);

void setup()
{

	/* add setup code here */
	//requestTime(100);
	setup_display();
	BmpSetup();
	setupBattery();
#ifdef SERIAL_DEBUG
	Serial.begin(9600);
#endif // SERIAL_DEBUG
}

boolean set2000 = true;
int Co2ppm = 0;
void loop()
{
	unsigned long start = millis();
	// CO2 Messen
	int Co2ppmAct = CO2Sensor.getCo2();
	if (Co2ppmAct != -1) // -1 == kein wert empfangen
	{
		if (Co2ppmAct > Co2ppmMax)
			Co2ppmMax = Co2ppmAct;
		if (Co2ppmAct < Co2ppmMin)
			Co2ppmMin = Co2ppmAct;
		//Serial.println(Co2ppmAct);
		if (Co2ppm == 0)
		{
			Co2ppm = Co2ppmAct;
			Co2ppmMin = Co2ppmAct;
			Co2ppmMax = Co2ppmAct;
		}
		else
		{
			Co2ppm += (Co2ppmAct - Co2ppm) / 5; // average value by 5 samples
		}

		// Temperatur messen
		uint8_t temperatur = CO2Sensor.temperature;
		readBattery();
		float temp, press;
		getReadings(temp, press);
		writeValues(Co2ppm, -1, -1.0, temp, -1.0, (int)press , batteryV, batteryPcnt);
	}
#ifdef SERIAL_DEBUG
	else
		Serial.println("Fehler CO2 Sensor");
#endif // SERIAL_DEBUG
	int nSekOff = 30;
	while (nSekOff > 0)
	{
		LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
		nSekOff -= 8;
	}
	
	
	if (!set2000)
	{
		CO2Sensor.setResolution2000();
		set2000 = true;
	}
}

BMP180.ino

#define ALTITUDE 297.0 // Höhe am Ort
#define SERIAL_DEBUG 0

void BmpSetup()
{
	if (pressure.begin())
		Serial.println(F("BMP180 init success"));
}


void getReadings(float &temp, float &press)
{
	processBmp();
	temp = (float)T;
	press = (float)p0;
}

void processBmp()
{
	char status;
	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)
		{
			// Print out the measurement:
			if (SERIAL_DEBUG)
			{
				Serial.print(F("temperature: "));
				Serial.print(T, 2);
				Serial.println(F(" deg 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(3);
			if (status)
			{
				// 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)
				{
					// Print out the measurement:
					if (SERIAL_DEBUG)
					{
						Serial.print(F("absolute pressure: "));
						Serial.print(P, 2);
						Serial.print(F(" mb, "));
						Serial.print(P * 0.0295333727, 2);
						Serial.println(F(" inHg"));
					}

					// 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)
					if (SERIAL_DEBUG)
					{
						Serial.print(F("relative (sea-level) pressure: "));
						Serial.print(p0, 2);
						Serial.print(F(" mb, "));
						Serial.print(p0 * 0.0295333727, 2);
						Serial.println(F(" 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);
					if (SERIAL_DEBUG)
					{
						Serial.print(F("computed altitude: "));
						Serial.print(a, 0);
						Serial.println(F("m"));
					}
				}
				else Serial.println(F("error retrieving pressure measurement\n"));
			}
			else Serial.println(F("error starting pressure measurement\n"));
		}
		else Serial.println(F("error retrieving temperature measurement\n"));
	}
	else Serial.println(F("error starting temperature measurement\n"));
}

display.ino

// Display
#include <Wire.h>
#include <SSD1306AsciiWire.h>

SSD1306AsciiWire display;

void setup_display()
{
	// initialize with the I2C addr 0x78 / mit I2C-Adresse 0x3c initialisieren
	Wire.begin();
	display.begin(&Adafruit128x64, 0x3C);
	display.setFont(System5x7);
	display.clear();
	display.print(F("MH-Z19 CO2 Sensor"));
}

void writeCo2Value(int value)
{
	display.setCursor(0, 0);
	display.print("CO2: ");
	display.print(value);
	display.println("ppm");
}

void writeRzero(int value)
{
	display.setCursor(0, 12);
	display.print("RZ: ");
	display.print(value);
	display.println("Ohm");
}

void writeValues(int CO2 = -1, int CO2UC = -1, float RZero = -1.0, float temp = -1, float humid = -1.0, int pressure = -1, float battV = -1.0, int battP = -1)
{

	display.clear();
	if (CO2 != -1)
	{
		display.print(F("CO2: "));
		display.print(CO2);
		if (CO2UC != -1)
		{
			display.print("/");
			display.print(CO2UC);
		}
		display.println(F("ppm"));
	}
	if (RZero != -1.0)
	{
		display.print(F("Rs: "));
		display.print(RZero, 1);
		display.println(F(" Ohm"));
	}
	if (temp != -1.0)
	{
		display.print(F("T: "));
		display.print(temp, 1);
		display.println(F("C"));
	}
	if (humid != -1.0)
	{
		display.print(F("  L: "));
		display.print(humid, 1);
		display.println("%");
	}

	if (pressure != -1)
	{
		display.print(F("Druck.: "));
		display.print(pressure, 1);
		display.println(F("kPa"));
	}
	if (batteryV != -1.0)
	{
		display.print(F("Batt.: "));
		display.print(batteryV, 1);
		display.println(F("V"));
	}
	if (battP != -1)
	{
		display.print(F("Batt.: "));
		display.print(battP, 1);
		display.println(F("%"));
	}
}

battery.ino

int oldBatteryPcnt = 0;

void setupBattery()
{
	// use the 1.1 V internal reference
#if defined(__AVR_ATmega2560__)
	analogReference(INTERNAL1V1);
#else
	analogReference(INTERNAL);
#endif
}

void readBattery()
{
	// get the battery Voltage
	int sensorValue = analogRead(BATTERY_SENSE_PIN);
	delay(10);
	sensorValue += analogRead(BATTERY_SENSE_PIN);
	delay(10);
	sensorValue += analogRead(BATTERY_SENSE_PIN);
	delay(10);
	sensorValue += analogRead(BATTERY_SENSE_PIN);
	delay(10);
	sensorValue += analogRead(BATTERY_SENSE_PIN);
	sensorValue /= 5;
#ifdef DEBUG
	Serial.println(sensorValue);
#endif

	// 1M, 220k divider across battery and using internal ADC ref of 1.1V
	// Sense point is bypassed with 0.1 uF cap to reduce noise at that point
	// ((1e6+470e3)/470e3)*1.1 = Vmax = 3.44 Volts
	// 3.44/1023 = Volts per bit = 0.003363075
	batteryV = sensorValue * 0.00595703125;
	// 3,5v .. 4,3v
	batteryPcnt = map(sensorValue, 587, 721, 0, 100);

#ifdef DEBUG
	SPrint(Battery Voltage : );
	Serial.print(batteryV);
	SPrintLn(V);

	SPrint(Battery percent : );
	Serial.print(batteryPcnt);
	SPrintLn(%);
#endif
}

Sensor auf 2000ppm umstellen

Hat man nun einen Sensor gekauft, der mit 0-5000ppm ausgeliefert wurde, kann man diesen mit einem Kommando auf 0-2000ppm einstellen.
Das kann in der Sensor_MHZ19_Mobil.ino gemacht werden

boolean set2000 = false; // auf false setzen, damit Befehl ausgeführt wird

Dann wird dieser IF-Zweig durchlaufen und die Auflösung umgestellt.

if (!set2000)
{
	CO2Sensor.setResolution2000();
	set2000 = true;
}

Wenn der Sensor einmal umgestellt ist, würde ich das bit set2000 wieder auf true setzen.