ESP32 based CO2 monitor using a SenseAir S8 sensor. Some additional features like graphs, touch screen, and MQTT support were added to have a fully functional device.
This project is an ESP32 based CO2 monitor using a SenseAir S8 sensor. Some additional features like graphs, touch screen, and MQTT support were added to have a fully functional device. The full feature list is:
Most ideas used here were inspired by the YouTube Channel of Andreas Spiess which I highly recommend if you are interested in microcontrollers and sensors.
I know three different NDIR infrared sensors capable to measure CO2:
Please also see these videos from Andreas Spiess:
So I tried my luck with the SenseAir S8. This sensor uses serial communication. The used Modbus protocol is well documented and available via PDF.
There is even a standalone software called UIP5 available from SenseAir for the S8. You only need an FTDI adapter or a special test board which is available for the SenseAir S8.
The S8 is available in different flavors (see this page on tab "variations"). I took the 004-0-0053. According to the datasheet measurement range is from 400-2000 ppm, but the sensor reports even 3000 ppm and more. I guess only precision is best between 400 and 2000, which perfectly fits to normal use.
As you might now, CO2 concentration is measured in ppm. All the NDIR infrared sensors I know need to be calibrated from time to time. All these sensors have autocalibration, which is ok, but the autocalibration process takes some days or even weeks to work fine.
As far as I understand, autocalibration works quite simple: as soon as values below 400 are measured, the sensor adjusts itself, as values below 400 don't exist in normal life. In the other direction autocalibration works probably similar: if you never get values close to 400, your sensor needs to be adjusted to provide lower values.
An easy way to calibrate your sensor is to go outside to fresh air where CO2 concentration should be 400 ppm, wait one or two minutes and then to tell your sensor to calibrate to 400 ppm.
You can do this by selecting "Settings - Calibrate CO2 sensor" on the display.
In Germany, the Robert Koch Institut provides every day the so called COVID-19 7-day-incidence for every region. Values are provided by an API which can be used by everyone without charge.
If you want the CO2 monitor to get the incidence and show it on the display, you only need to know the ID of your region. This ID can be found here: https://npgeo-corona-npgeo-de.hub.arcgis.com/datasets/917fc37a709542548cc3be077a786c17_0 In the map, click on your region and look for the value "ObjectID" in the table. Put this value in file "incidenceMap.cpp". You can even provide two or more regions.
If you are not in Germany or do not want to use the incidences, simply comment
#define useIncidenceFromRKI
in file "config.h".
Graphs both for CO2 values and incidences are provided. CO2 values will be shown for the last 24 hours. Incidences are shown as much as available.
In order to keep values from the CO2 sensors and incidents even after the ESP32 reboots, the NVS (nonvolatile storage) from the ESP32 is used. Unfortunately, the standard NVS partition has only 20.480 bytes size and can only hold 630 values. The ESP32 (and especially WiFi) already takes approx. 280 of them. So only 350 are left. To less for us.
This is the reason why most software developers create their own NVS partition with which they can do whatever they want. The new partition scheme is in file "partitions_custom.csv". The custom partition I am using has 655.360 bytes size and can hold 20.160 values.
For getting space for the custom NVS partition I reduced the application partitions a little bit and drastically reduced the size of the SPIFFS partition.
If you are using PlatformIO, the custom partition scheme is automatically used when uploading to the ESP32. If you are using the Arduino IDE, please have a look here on how to use a custom partition scheme.
You will not change your ESP32 in a non-reversible way when using this custom partition scheme. The default scheme will automatically be applied if you ever upload a different project to your ESP32.
I used a DHT11 for measuring temperature and humidity. Unfortunately, temperature raises significantly when charging the battery, so I wouldn't use a temperature sensor any more.
Inspired by the video #250 Universal Power Source (UPS) for only 2$. Is this possible? (Raspberry Pi, Arduino, ESP32) from Andreas Spiess, I used a smart and versatile power source with a single 18650 LiIon battery. The board can both deliver 5 V and 3.3 V and can charge the battery. Perfect. (Drawbacks of the board are stated in the video).
With TFT turned off, one 18650 can power the device for about 15h, with TFT turned on about 8h. Both with WiFi enabled. I didn't test it with WiFi disabled.
Knowing the state of the battery is always helpful. Since the ESP32 cannot directly measure up to 4.2 V, a simple voltage divider is used. Additionally, I used a filter capacitor, but probably it works without it.
Reading analogue values with an ESP32 unfortunately isn't very precise. You always must calibrate it, which can easily be done. If you don't worry too much about precision, it is sufficient to only calibrate your reading at one single voltage level.
If you want to have more precision, then you must deal with the ESP32 non-linearity issue. This means you cannot simply do linear calculations to get correct measurements. You can use the approach from here to generate a lookup table specific for your ESP32 to correct non-linear errors.
The lookup table in the code fits to my ESP32 and most likely will not perfectly fit to your ESP32.
For more details on how to calibrate see file "liIonVoltage.cpp".
It is highly recommended to use WiFi. With WiFi enabled you can
Please adjust your WiFi settings in file "config.h"
I have several WiFi Access Points and Repeaters in my house, and I like to know to which AP or Repeater and on which frequency (channel and 2.4/5GHz) the ESP32 is connected. For that the function "void setAccessPointName()" is available. Please adjust this to your needs, if you want to. Sometimes it is hard to know the MAC addresses of your AP or Repeater. The easiest way to find this out is most likely the administration page of your AP/repeater.
There are three state topics available where the CO2 monitor sends its state every 10 seconds.
Topic | Example |
---|---|
esp32_co2/tele/STATE1 | { "co2": 1078, "co2status": 0, "temp": 29.6, "hum": 26, "volt": 4.16, "battstate": 100 } |
esp32_co2/tele/STATE2 | { "wifiRSSI":-62,"wifiChan":1,"wifiSSID":secret,"wifiBSSID":AA:BB:CC:DD:EE:FF } |
esp32_co2/tele/STATE3 | { "up": 902897, "heapSize": 300812, "heapFree": 124012, "heapMin": 70548, "heapMax": 67188, "nvsUsed": 1503, "nvsTotal": 20160 } |
Please adjust your MQTT server settings in file "config.h"
A simple REST API is available, which is probably almost self-explaining. It is intended for maintaining the values in the permanent storage, which you cannot simply delete by rebooting the ESP32.
Function | Example | Remark |
---|---|---|
Get all CO2 values | http://\<IPaddress>/co2values?pageno=00&pretty=1 | 0 <= pageno <= 14, each page has 100 valuesonly available if real time is set |
Delete single CO2 value | curl "http://\<IPaddress>/co2values" -X DELETE -d "{ "dateTime": "2021-05-22 19:10:00"}" -H "Content-Type: application/json" | only available if real time is set |
Get all incidences for a region | http://\<IPaddress>/incidence?pageno=00®ionID=193&pretty=0 | each page has 100 values |
Delete single incidence for a region | curl "http://\<IPaddress>/incidence" -X DELETE -d "{"regionID": 193, "date": "2021-05-23"}" -H "Content-Type: application/json" | |
Put single incidence for a region | curl "http://\<IPaddress>/incidence" -X PUT -d "{"regionID": 194,\n"date": "2021-05-23",\n"value":68.1}" | only use regions which are known to the firmware |
Get screenshot | http://\<IPaddress>/screenshot |
For using endpoints which need body data (DELETE, PUT), either use curl as in the example, or a tool like Advanced REST Client
I'm using the fantastic library ezTime which can retrieve NTP network time and supports timezones. By default, I'm using timezone "Europe/Berlin". Please adjust this to your needs in the file "config.h"
#define myTimezone "Europe/Berlin"
Two translation files are provided
In file "config.h" you can define which one is used.
#include "lang/en.h"
I would be happy if you create more translation files and send me a pull request.
On screen "Settings" you can
If you want to upload new firmware OTA (over the air, which means without serial connection), activate
#define useOTAUpdate
in the file "config.h".
Please note that for uploading the first time you have to use a serial connection!
For more details about what OTA is and how to use, please see this video from Andreas Spiess: #332 ESP32 OTA tutorial with tricks (incl. OTA debugging)
If you want to upload a new firmware, use these two lines in file "platform.ini":
upload_protocol = espota
upload_port = <ip-adress of your ESP32, see serial log>
On how to do it with the Arduino IDE, see the video above.
I recommend to activate OTA only when needed for uploading a new firmware. As soon as you activate OTA it takes about 10K of heap space, which leads to an unstable ESP32 (reboot after some days, hours or even minutes, usually when receiving a WiFi packet). It seems the the CO2 monitor already needs a lot of heap space for normal operation. Activate OTA via sending a MQTT message to "esp32_co2/cmnd/OTA" with payload "ON" and then upload the new firmware.
Same holds for using a seperate thread for checking for OTA updates. Creating a seperate thread for this takes also about 10k of heap space, so I don't recommend it. I had instability issues when doing it that way. It's also fine to check for OTA updates in the main loop.
If you have no serial connection, how can you get debug messages? For this I'm using the library jandrassy/TelnetStream. Instead of using "Serial.print(...)" you simply use "TelnetStream.print(...)". Then you can use a telnet connection to your ESP32 to get debug messages.
But what if you sometimes want to have debug messages via serial connection and sometimes via TelnetStream? Always changing the code from "Serial.print(...)" to "TelnetStream.print(...)" and back is not a good solution. For this I created a class called "LogStreamClass". Now in your code you can simply write "Log.printf(format, ...)". In "config.h" you can define one or both of
#define useSerial
#define useTelnetStream
No need to change the code.
STL and STEP files for a housing are in folder "housing". Of course, it only fits if you use exactly the same components as I did. By adjusting the STEP file, you should be able to do some changes if you need to.
Function | Parts | Remarks | approx. price |
---|---|---|---|
CO2 sensor | SenseAir S8 004-0-0053 | AliExpress | 26 EUR |
TFT display with touch | 2.8 inch 320x240, ILI9341 / XPT2046 | AliExpress | 10 EUR |
Microcontroller | ESP32 | AliExpress | 5 EUR |
Battery charge board | 18650 | AliExpress | 5 EUR |
On/off switch | whatever you like | AliExpress | 3 EUR |
Temperature sensor | DHT11 | AliExpress | 2.50 EUR |
Voltage divider | resistor 1 MOhm, 560 kOhmoptional: ceramic capacitor 100 nF | cents |
If you are only used to the Arduino IDE, I highly recommend having a look at PlatformIO IDE.
While the Arduino IDE is sufficient for flashing, it is not very comfortable for software development. There is no syntax highlighting and no autocompletion. All the needed libraries must be installed manually, and you will sooner or later run into trouble with different versions of the same library.
This cannot happen with PlatformIO. All libraries will automatically be installed into the project folder and cannot influence other projects.
If you absolutely want to use the Arduino IDE, please have look at the file "platformio.ini" for the libraries needed. Also read here on how to use the custom partition scheme "partitions_custom.csv" needed by the CO2 monitor (see "Permanent storage" above).
For installing PlatformIO IDE, follow this guide. It is as simple as: