The hardware for a Tasmota node with a SCD41 CO2 sensor node with SH1106 OLED display

Goal of this project

This project is about building a CO2 sensor from standard modules.

  • It displays the result on a cheap OLED display.
  • It communicates via WIFI.
  • It sends the measurements via my MQTT server to my domotica system (Domoticz).
  • It has a battery inside, so it runs (for a while) without power source.
  • It has a battery charging circuit and can be charged by a USB-C connector.

The hardware

The parts

SCD41 module

3 parts

SH1106 OLED display

WS2812B

74HCT86

  • A box

The box

  • Tasmota software, making use of the universal display driver, HaspMota and the Berry language.

Wiring

At first, I have been building a proto-type with Dupont wires.

Module pin pin Module
SCD41 GND GND ESP32-C3-SuperMini
SCD41 VDD 3V3 ESP32-C3-SuperMini
SCD41 SCL 9 ESP32-C3-SuperMini
SCD41 SDA 8 ESP32-C3-SuperMini
Module pin pin Module
SH1106 OLED GND GND ESP32-C3-SuperMini
SH1106 OLED VDD 3V3 ESP32-C3-SuperMini
SH1106 OLED SCL 9 ESP32-C3-SuperMini
SH1106 OLED SDA 8 ESP32-C3-SuperMini
Module pin pin Module
74HCT86 7 GND GND ESP32-C3-SuperMini
74HCT86 14 VDD 5V ESP32-C3-SuperMini
74HCT86 pin 1 input 1 of gate 1 5 ESP32-C3-SuperMini
74HCT86 pin 2 input 2 of gate 1 GND ESP32-C3-SuperMini
74HCT86 pin 3 output of gate 1 Din WS2812 LED
WS2812 LED 5V 5 ESP32-C3-SuperMini
WS2812 LED GND GND ESP32-C3-SuperMini

Wired by wire-wrapping and mounted in a box

I used the proto-type to develop the software (see below).

When that was done, I mounted everything in a box with hot glue, and wire-wrapped all connections.

The box top and bottom

Detail

The box

Detail

Flashing Tasmota software

The module is a esp32-c3 SuperMini. We need to flash Tasmota software on this module. For flashing, we use esptool from Espressif.

Install esptool

See Espressif esptool.

After install, verify:

michiel@Delphinus:~/development/github/esptool> which esptool.py
/home/michiel/.local/bin/esptool.py
michiel@Delphinus:~/development/github/esptool>

Connect the esp32-c3 SuperMini

Connect a data USB-C cable from your PC to the ESP32-C3.

As described at Tasmota - Getting started, we erase the flash first. No need to specify the serial port to use - we are using Linux. No need to put the device in programming mode - modern devices do that automatically.

michiel@Delphinus:~/development/github/esptool> esptool.py erase_flash
esptool.py v3.3.3
Found 2 serial ports
Serial port /dev/ttyS4
Connecting......................................
/dev/ttyS4 failed to connect: Failed to connect to Espressif device: No serial data received.
For troubleshooting steps visit: https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html
Serial port /dev/ttyACM0
Connecting...
Detecting chip type... ESP32-C3
Chip is ESP32-C3 (revision v0.4)
Features: Wi-Fi
Crystal is 40MHz
MAC: dc:da:0c:a1:77:fc
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 14.9s
Hard resetting via RTS pin...
michiel@Delphinus:~/development/github/esptool> 

Looking for the Tasmota bin file and flashing

Look at the list of Tasmota releases. We look for a heading: “Factory binaries to be used for inital flashing using esptool”.

They even describe how to flash: esptool.py write_flash 0x0 tasmota32.factory.bin. We need the tasmota32c3.factory.bin file, so download it and flash it:

michiel@Delphinus:~/temp> ls
tasmota32c3.factory.bin
michiel@Delphinus:~/temp> esptool.py write_flash 0x0 tasmota32c3.factory.bin 
esptool.py v3.3.3
Found 2 serial ports
Serial port /dev/ttyS4
Connecting......................................
/dev/ttyS4 failed to connect: Failed to connect to Espressif device: No serial data received.
For troubleshooting steps visit: https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html
Serial port /dev/ttyACM0
Connecting...
Detecting chip type... ESP32-C3
Chip is ESP32-C3 (revision v0.4)
Features: Wi-Fi
Crystal is 40MHz
MAC: dc:da:0c:a1:77:fc
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00000000 to 0x002d0fff...
Compressed 2950432 bytes to 1751355...
Wrote 2950432 bytes (1751355 compressed) at 0x00000000 in 37.8 seconds (effective 624.0 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
michiel@Delphinus:~/temp> 

This took only a minute…

Use Platformio as serial monitor

michiel@Delphinus:~/temp> get_pio
(penv) michiel@Delphinus:~/temp>
(penv) michiel@Delphinus:~/temp> pio device monitor
--- Terminal on /dev/ttyACM0 | 9600 8-N-1
--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H

Does not work. Next, I created a platformio.ini file with the correct (?) baud rate. Also this does not work:

(penv) michiel@Delphinus:~/temp> pio run -t monitor
Processing esp32c3_supermini (platform: espressif32; board: esp32-c3-devkitm-1; framework: arduino)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Terminal on /dev/ttyACM0 | 460800 8-N-1
--- Available filters and text transformations: colorize, debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H

Configuration

See the Tasmota documentation at initial-configuration.

Set the Wifi SSID and password

Follow the description on the Tasmota website to set the Wifi SSID and password.

I renamed the module to CO2SensorSCD41 in my router, and gave it a fixed lease for the IP number.

Now we have a web-based UI:

Screenshot of the first UI

Flashing the complete software

We have to replace the “factory” sftware with the complete one.

Go to the web-UI, main menu, then Firmware Upgrade, and the OTA URL is already filled in as: http://ota.tasmota.com/tasmota32/release/tasmota32c3.bin.

However, we need the Tasmota software including LVGL and HaspMota, so we look elsewhere - this is the one: https://github.com/tasmota/install/blob/firmware/firmware/unofficial/tasmota32c3-lvgl.bin.

Replace the URL, and press Start Upgrade.

Templates

See the entry for the SuperMini-ESP32-C3 in the Tasmota supported hardware list.

Use this as template:

{"NAME":"SuperMini ESP32-C3","GPIO":[1,1,1,1,1,1,1,1,640,608,1,0,0,0,0,0,0,0,1,1,1,1],"FLAG":0,"BASE":1}

Set the name to CO2SensorSCD41. Set the friendly name to CO2 Sensor SCD41. Save and restart the module.

Configure the I2C bus

This configuration is already included in the template above!

We connected the I2C devices to GPIO8 as SDA and GPIO9 and SCL.

To check, go to the web-UI menu Configuration, then Configure Template. The GPIO8 is I2C SDA, and GPIO9 is I2C SCL, both number 1.

In the main page of the web-ui, we now see:

SCD40 eCO₂	596 ppm
SCD40 Carbon dioxide	565 ppm
SCD40 Temperature	25.0 °C
SCD40 Humidity	72.1 %
SCD40 Dew point	19.6 °C

The Tasmota software does not make the distinction between the SCD40 and the SCD41 in the labels in the UI: it always shows SCD40. The same applies to the SCD40/41 specific commands: even though the single shot command is SCD41 only, it is still named SCD40Sing.

Configure the internal blue LED

According my previous investigations, the internal blue LED is at GPIO8. That pin is used for SDA of the I2C bus, so it flashes with any communication with the sensor.

Set the SCD41 Altitude

See description at the Tasmota SCD41 documentation.

I live at around 12m altitude, so these commands will configure the sensor:

SCD40Stop
SCD40Alt 12
SCD40Toff 400
SCD40pers

Restart the module to see the periodic measurements again!

Set the SCD41 to low power mode

Since I would like to be able to use this module with a battery, I would like to use the low power mode of the SCD41, with periodic measurements every 30s.

See the Tasmota documentation for SCD41 commands.

SCD40Stop
SCD40StLp

Restart the module to see the periodic measurements again! The web-UI of the Tasmota module now refreshes the values every 30s.

Configure the display

Install LVGL firmware for the display

We connected a SH1106 I2C OLED display, 1.3”, 128x64 pixels, blue.

Use the web-UI to install the tasmota32c3-lvgl.bin firmware.

On the console, scan for I2C devices:

11:43:08.016 CMD: I2Cscan
11:43:08.035 RSL: RESULT = {"I2CScan":"Device(s) found on bus1 at 0x3c 0x62"}

The 0x62 is the SCD41, and the 0x3c is the display.

Setting up the display

See the Tasmota documentation for displays and display commands.

Initially, giving the command display on the console gives:

12:20:16.858 CMD: display
12:20:16.862 RSL: RESULT = {"Display":{"Model":0,"Type":0,"Width":0,"Height":0,"Mode":0,"Dimmer":50,"Size":1,"Font":1,"Rotate":0,"Invert":0,"Refresh":2,"Cols":[16,8],"Rows":2}}

Our displaymodel is the SH1106 OLED display, which is 0x07.

But, we prefer to use the universal display driver. The Universal Display Driver or uDisplay is a way to define your display settings using a simple text file and easily add it to Tasmota. uDisplay is DisplayModel 17. The driver must be enabled by OPTION A3 on any GPIO pin.

The display itself is defined by a “descriptor” file. Many display descriptor files are included in Tasmota GitHub in tasmota/displaydesc folder. There is the file SH1106_display.ini. We download this file, rename it to display.ini and upload it to the Tasmota file system: in the Tasmota Web-UI, goto the tools menu, then Manage file system.

Managing the file system

Now, we enable the driver by setting OPTION A3 on any of the available GPIO pins.

Configuration of OptionA 3

Now, after a restart, the display command returns:

18:50:43.763 CMD: display
18:50:43.767 RSL: RESULT = {"Display":{"Model":17,"Type":0,"Width":128,"Height":64,"Mode":0,"Dimmer":50,"Size":1,"Font":1,"Rotate":0,"Invert":1,"Refresh":2,"Cols":[16,8],"Rows":8}}

Using DisplayMode 3

In DisplayMode 3 for an OLED, the display shows Local sensors and Time/Date. So, on the console, we give:

13:01:01.654 CMD: displaymode 3
13:01:01.658 RSL: RESULT = {"DisplayMode":3}
13:03:49.593 CMD: displayrows 8
13:03:49.597 RSL: RESULT = {"DisplayRows":8}

Using HASPmota

Stop using the displaymode to display things, and invert the display colors:

14:58:16.085 CMD: displaymode 0
14:58:16.208 RSL: RESULT = {"DisplayMode":0}
15:07:28.669 CMD: displayinvert 1
15:07:28.673 RSL: RESULT = {"DisplayInvert":1}

Create a new file pages.jsonl with as content:

{"page":0,"comment":"----------------"}
{"id":10,"obj":"label","x":2,"y":2,"w":124,"text":"5000","align":1,"text_rule":"SCD40#CarbonDioxide","bg_color":"#000000","line_color":"#FFFFFF","text_rule_format":"%d","text_rule_formula":"val","text_font":"montserrat-28"}
{"id":20,"obj":"label","x":2,"y":42,"w":124,"text":"CO2","align":0,"bg_color":"#000000","line_color":"#FFFFFF"}
{"id":30,"obj":"label","x":2,"y":42,"w":124,"text":"ppm","align":2,"bg_color":"#000000","line_color":"#FFFFFF"}

Then, create an autoexec.be file containing the following:

## simple 'autoexec.be' to run HASPmota using the default 'pages.jsonl'
import haspmota
haspmota.start()

Prevent burn-in

To prevent burn-in of the text, we move the text around between the above and below:

{"id":10, ... "y":32, ...}
{"id":20, ... "y":2,  ...}
{"id":30, ... "y":2,  ...}

We do that by adding the following to the autoexec.be. So, every 15 seconds, we toggle the display contents from top to bottom and vice versa.

var burn = false
def toggle_burn() 
    if burn
        global.p0b10.y = 2
        global.p0b20.y = 42
        global.p0b30.y = 42
        burn = false
    else
        global.p0b10.y = 32
        global.p0b20.y = 2
        global.p0b30.y = 2
        burn = true
    end
end
tasmota.add_cron('*/15 * * * * *', toggle_burn, "toggle_burn")

Adding a color LED WS2812

See the Tasmota documentation for WS2812B-and-WS2813 and Berry_Addressable-LED.

I used a 74HCT86 as signal buffer, and connected a single WS2812B to GPIO05.

Configuration

Rename the Web-UI button names

The buttons in the web-UI are named “Toggle 1” and “Toggle 2”.

Main menu before

We can change the names with the following console commands:

WebButton1 Display  
WebButton2 LED  

And this is what it looks like afterwards:

Main menu after

Testing reveals that this they are swapped, so, correction:

17:40:21.572 CMD: backlog WebButton2 Display ; WebButton1 LED  
17:40:21.595 RSL: RESULT = {"WebButton2":"Display"}  
17:40:21.804 RSL: RESULT = {"WebButton1":"LED"}  

Main menu corrected

Set the LED color according the CO2 level

The LED should depend on the measured CO2 value.

CO2 value LED Color Value
<400 off 000000
400 .. 800 Green 00FF00
800 .. 1200 Yellow/Orange BC4000
>1200 Red FF0000

To test the events, we add this to the autoexec.be:

def co2value(value, trigger, msg)
    print (str(value) .. " - " .. trigger .. " - " .. msg)
end
tasmota.add_rule("SCD40#CarbonDioxide", co2value)

The returned values look like this:

18:30:13.473 465 - SCD40#CarbonDioxide - {'SCD40': {'CarbonDioxide': 465, 'Temperature': 25.7, 'Humidity': 57, 'eCO2': 489, 'DewPoint': 16.5}}

So, we can use the first value as an integer:

def co2value(value)
    if (value < 400)
        tasmota.cmd("led1 000000")
    elif (value < 800)
        tasmota.cmd("led1 00FF00")
    elif (value < 1200)
        tasmota.cmd("led1 BC4000")
    else
        tasmota.cmd("led1 FF0000")
    end
end
tasmota.add_rule("SCD40#CarbonDioxide", co2value)

This last code generates much traffic on the MQTT bus for LED updates.

Communication with the outside world

Set up MQTT

See the Tasmota documentation for MQTT.

Setting the MQTT server address, enables the MQTT messages.

Setting the teleperiod

The default teleperiod is 300 seconds. That is a bit long, so we change to 30s:

20:08:19.732 CMD: teleperiod 30
20:08:19.737 MQT: stat/tasmota_A177FC/RESULT = {"TelePeriod":30}

Auto-created Tasmoticz devices

Setting the MQTT server address, causes my Tasmoticz to create 3 new devices in Domoticz:

3 new devices

Adding a Domoticz device for CO2

So, I created a new “Air Quality” dummy device in Domoticz. It got idx 528.

We can update the Domoticz value with MQTT. It works with this example - sending a numeric value 400 for the device with idx 528:

mosquitto_pub -h mqtt -t "domoticz/in" -m '{"idx":528,"nvalue":400}'

Type the above in the command line of your PC. You will see the dummy device in Domoticz updated to the value 400.

So, now we want to have our Tasmota module do this automatically whenever the measurement changes. Hence, we extend the autoexec.be with:

import mqtt
def hitdom(value)
    mqtt.publish ("domoticz/in", '{ "command": "udevice", "idx" : 528, "nvalue" : ' .. str(value) .. ', "parse": false }')
end
tasmota.add_rule("Tele#SCD40#CarbonDioxide", hitdom)

Measurements got stuck after several months

I had the module measuring the CO2 in my living room for several months, until a problem appeared: the value did not update any more.

Is this caused by a millis overflow? Probably not.

The cause is unknown - so, let’s just reset the module every week by extending the autoexec.be with:

## Reboot every week to prevent long term unstability (do we need this?):
def f() 
    tasmota.cmd("restart 1")
end
tasmota.add_cron("0 0 * * 0 *", f, "every_week")
Comments: