Introduction
Last year, I bought the Birdie CO² monitor 1 to measure my CO² values at home. This air quality meter resembles a canary bird, referencing the days where canaries were used in coal mines to detect deadly carbon monoxide. When these stopped singing (or even collapsed 😥), this was a sign that the air quality was poor, and the workers needed to leave the mines 2.
This is how the Birdie looks when the air quality changes from good to bad and again from bad to good:
But what does this mean? How bad is “bad”? According to the specification, the bird drops when CO² level is above 1'000 ppm and returns when above 800 ppm. These are also recommended values on various resources on the Internet 3.
Often in the morning, even if the window was partially open during the night, the bird was fallen of it’s perch. This was the point where I was interested in knowing the actual CO² values. I wanted a CO² sensor that could record measurements over a longer period, allowing me to access the data on a computer and mobile phone without sending it to a cloud service.
Inspired by a talk of a friend at the Winterchaos in 2022 4, I wanted to build my own solution. After some research, I decided to implement this using an ESP32 5 and a CO² sensor. The ESP32 will be used to export Prometheus metrics which can then be visualized in a Grafana dashboard.
This blog post explains this implementation. Since I don’t have much experience in such electronics/hardware projects, I think this post is suitable for beginners and I try to explain it as simple as possible.
Hardware Setup
Microcontroller
Because I want to have the CO² values accessible in the network, I went for the ESP32 microcontroller which has an integrated WiFi module and also because I already had one laying around at home. This board can e.g. be bought in the Adafruit shop 6.
CO² Sensor
To measure the CO² values, I went for the Adafruit SCD-41 sensor 7, which is a “true” CO² sensor.
Unlike others, the SCD-41 isn’t approximating it from the VOC gas concentration, but measures the CO² concentration in the air in PPM (parts per million). This sensor is produces by the Swiss company Sensirion 8 and can be bought soldered on an a custom-made PCB in the STEMMA QT form factor at Adafruit 9. It can be used industrial or scientific CO² measurements as a range of 400 to 5000 ppm with an accuracy of ±(40 ppm + 5% of reading) according to the datasheet 10. In addition, the sensor can also measure the temperature and the relative humidity.
Connecting the Sensor to the ESP32
The sensor values can be read via an I²C interface. The documentation 10 shows how the sensor must be wired to the microcontroller using 4 cables:
Where the SCL
/SDC
PINs on the ESP32 are, can be found in the Espressif ESP32
documentation 11 (note that this can be different on other boards):
This results in the following connections:
3V
of the sensor →3V
/5V
on the microcontrollerGND
of the sensor →GND
on the microcontrollerSCL
of the sensor →SCL
on the microcontroller (GPIO22
GPIO PIN on the ESP32)SDA
of the sensor →SDA
on the microcontroller (GPIO21
GPIO PIN on the ESP32)
Now, the sensor is connected to the board:
The next step is to program the microcontroller, query the sensor values and make them accessible in the network.
Source Code
Basic Code Example
The SCD-41 datasheet 10 on page 15 has some simple example Arduino code that shows how the sensor can be queried via the I²C interface using a library from Sensirion. It’s not that difficult. Some simplified code which explains the usage:
// [...]
#include <SensirionI2CScd4x.h>
// [...]
SensirionI2CScd4x scd4x;
// [...]
void setup() {
// [...]
error = scd4x.startPeriodicMeasurement();
// [...]
}
void loop() {
// [...]
error = scd4x.readMeasurement(co2, temperature, humidity);
// [...]
Serial.print("Co2:");
Serial.print(co2);
Serial.print("\t");
Serial.print("Temperature:");
Serial.print(temperature);
Serial.print("\t");
Serial.print("Humidity:");
Serial.println(humidity);
// [...]
}
Existing Project on GitHub
I also found the GitHub repository sighmon/co2_sensor_scd4x_esp32_http_server 12 where someone already implemented what I needed. This creates a simple HTTP endpoint which exports Prometheus metrics that can be visualized in a Grafana dashboard.
My Modifications
This however did not work out of the box, so i forked it 13 and implemented some changes
to fix some issues with the TaskScheduler
that reads the sensor values,
removed the not needed code for Bluetooth Low Energy (BLE), added support
for static IP addresses and fixed a bug that returned a wrong metric data format. I also created a Grafana dashboard and added this
configuration, because this was missing in the original repo.
Device Configuration
Configure, Compile and Upload
So, let’s configure the ESP and upload the code.
Clone my forked Git repository:
git clone https://github.com/emanuelduss/co2_sensor_scd4x_esp32_http_server.git
Copy the secrets template file secrets.tmpl.h
to secrets.h
:
cp secrets.tmpl.h secrets.h
Add your WiFi credentials to the file secrets.h
. Note, your ESP may only support 2.4 GHz WiFi, so use a 2.4 GHz SSID.
const char* SECRET_SSID = "myssid-legacy";
const char* SECRET_PASSWORD = "**************";
const char IPADDRESS[] = {192, 168, 23, 75};
Download the Arduino IDE from https://www.arduino.cc/.
Install the following libraries including their dependencies via Tools
→ Manage Libraries...
:
- Sensirion I2C SCD4x
- TaskScheduler
In the dropdown at the top, select the ESP32 board:
Open the co2_sensor_scd4x_esp32_http_server.ino
file and upload it to the ESP32 by pressing the upload / arrow button:
Result
In the serial monitor, you can see how the device connects to your WiFi and prints the measured values:
So everything looks good so far!
The device exposes an endpoint on port 80/tcp
that exports the CO², relative
humidity and temperature metrics:
$ ncat 192.168.23.75 80 <<< ""
HTTP/1.1 200 OK
Content-type: text/plain
# HELP ambient_temperature Ambient temperature
# TYPE ambient_temperature gauge
ambient_temperature 22.52
# HELP ambient_humidity Ambient humidity
# TYPE ambient_humidity gauge
ambient_humidity 43.88
# HELP co2 CO2
# TYPE co2 gauge
co2 984
Because it simply sends this response the values whenever something was received, this also works on HTTP requests:
$ curl http://192.168.23.75
# HELP ambient_temperature Ambient temperature
# TYPE ambient_temperature gauge
ambient_temperature 22.86
# HELP ambient_humidity Ambient humidity
# TYPE ambient_humidity gauge
ambient_humidity 42.92
# HELP co2 CO2
# TYPE co2 gauge
co2 1005
This is exactly the Prometheus text-based exposition format 14 we need to collect it using Prometheus and display it in a Grafana dashboard.
Prometheus Configuration & Grafana Dashboard
I used an already existing Prometheus and Grafana infrastructure in combination with the Traefik reverse proxy 15 based on Docker containers using the official images 16 17. If you want to reproduce the Traefik setup which also configures Let’s Encrypt certificates automatically, you can use the template from my repository 18.
A simplified docker-compose.yml
file could look like this:
services:
prometheus:
image: prom/prometheus:latest
restart: unless-stopped
command:
- '--storage.tsdb.retention.time=10y'
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
- '--web.console.templates=/usr/share/prometheus/consoles'
volumes:
- ./prometheus_config:/etc/prometheus/
- ./prometheus_data:/prometheus
grafana:
image: grafana/grafana:latest
restart: unless-stopped
volumes:
- ./grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=changeme
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_DOMAIN=example.net
- GF_SMTP_ENABLED=false
The exact Prometheus & Grafana setup is not part of this post.
A new job has to be added in the Prometheus configuration prometheus.yml
:
# [...]
scrape_configs:
# [...]
- job_name: sensor1
static_configs:
- targets:
- 192.168.23.75:80
This entry should then show up in the target list:
In Grafana, the Prometheus data source has to be configured:
Then, the dashboard configuration can be imported:
After collecting some data, the dashboard shows the current and past CO² level in ppm, temperature and humidity:
Because my phone is connected to my home network via Wireguard (explained in this blogpost here 19), I can also access the dashboard when I’m not at home:
Alternatives
- A very cool alternative is the Datagnome (Datenzwerg) project 20. These Datagnomes can collect more data and have more features.
- There are also several tutorials on how you can 3D-print and build your own Birdie 21.
-
Birdie CO² Meter: https://www.birdie.design/ ↩︎
-
What Happened to the Canary in the Coal Mine? The Story of How the Real-Life Animal Helper Became Just a Metaphor: https://www.smithsonianmag.com/smart-news/what-happened-canary-coal-mine-story-how-real-life-animal-helper-became-just-metaphor-180961570/ ↩︎
-
CO2Meter, Carbon Dioxide Levels Chart: https://www.co2meter.com/blogs/news/carbon-dioxide-indoor-levels-chart ↩︎
-
Winterchaos Talk, Open Source Luftqualitäts-Monitoring für Zuhause: https://media.ccc.de/v/luzern-2818-open-source-luftqualitats-mo ↩︎
-
Adafruit Shop ESP32: https://www.adafruit.com/search?q=esp32 ↩︎
-
Adafruit SCD-40 and SCD-41: https://learn.adafruit.com/adafruit-scd-40-and-scd-41 ↩︎
-
Sensirion SCD41 Sensor: https://sensirion.com/products/catalog/SCD41 ↩︎
-
Adafruit SCD-41 - True CO2 Temperature and Humidity Sensor - STEMMA QT / Qwiic: https://www.adafruit.com/product/5190 ↩︎
-
Adafruit SCD-40 and SCD-41 Datasheet: https://cdn-learn.adafruit.com/downloads/pdf/adafruit-scd-40-and-scd-41.pdf ↩︎ ↩︎ ↩︎
-
Espressif ESP32 Documentation for I²C: https://docs.espressif.com/projects/arduino-esp32/en/latest/api/i2c.html ↩︎
-
GitHub: sighmon/co2_sensor_scd4x_esp32_http_server: https://github.com/sighmon/co2_sensor_scd4x_esp32_http_server ↩︎
-
GitHub Fork: emanuelduss/co2_sensor_scd4x_esp32_http_server: https://github.com/emanuelduss/co2_sensor_scd4x_esp32_http_server ↩︎
-
Prometheus Documentation, Exposition formats: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md ↩︎
-
Traefik Reverse Proxy: https://traefik.io/traefik/ ↩︎
-
Prometheus Docker Image: https://hub.docker.com/r/prom/prometheus ↩︎
-
Grafana Docker Image: https://hub.docker.com/r/grafana/grafana ↩︎
-
Traefik/Prometheus/Grafana Example Setup: https://github.com/emanuelduss/co2_sensor_scd4x_esp32_http_server/tree/main/dashboard ↩︎
-
Wireguard Road Warrior Setup: https://emanuelduss.ch/posts/wireguard-vpn-road-warrior-setup/ ↩︎
-
Datagnome / Datenzwerg Project: https://datagnome.de/ ↩︎
-
Birb - the Canary Shaped Air Quality Sensor: https://www.printables.com/model/450447-birb-the-canary-shaped-air-quality-sensor ↩︎