Atmo - Home-made Weatherstation - Part 2 (Coding)

Now that I have the base of “Atmo”, my weather station ready, I continue programming the ESP32 to read the sensor values and transmit them centrally.

In the 1st part of “Atmo” I described how I assembled the indoor base station, on which this article is based.

What should the ESP do?

The goal is for ESP to do the following:

  • Read the temperature, pressure, humidity and air quality from the BME680 sensor
  • Read the Lux value from the TSL2591 sensor
  • Calculate the Indoor Air Quality Index
  • Values corrected/calibrated if necessary
  • Data transmitted to the MQTT broker

Set up Arduino IDE

I use the Arduino IDE for programming the ESP32, which still needs some settings.

Settings
First of all, the ESP32 board Arduino should be introduced:

  • File -> Preferences -> Additional Board Manager URLs: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • Tools -> Board -> Board Manager: Search for “esp32” and install it.

The Arduino IDE then needs the following settings:

Libraries
Afterwards the following libraries must be installed:

Coding

A preliminary note on the code: The Arduino IDE is a bit inflexible, because a “sketch” is a file. I want to avoid that a file has too many lines, because this makes the code confusing. The entry point here is always setup() and after that loop(), which are methods/functions, but I would rather split the whole thing into classes. Since the whole thing is written in C++ you have to work with header files for many different files so that the compiler can find everything. I tried to split the whole thing up a bit and work with functions and some global variables - a case for a later refactoring in classes, should this grow even more…

So, now let's let's code. 🤓

To find the sensors, you need to specify the connected pins and the mode:

#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
#include "Adafruit_TSL2591.h"

#define BME_CS 16 // Pin 16 where the CS is connected

Adafruit_BME680 bme(BME_CS); // hardware SPI
Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591); // hardware I2C

Environment sensor

First I would like to read the values from the BME680. For all values I can specify how many times they should be read out (oversampling). The higher/often the sampling is, the more inaccurate the values are, the more noise in the values can be removed.
With the IIR filter you can catch the peaks of temperature and air pressure, which are generated by a slamming door or a draft.

// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms

The values can now be read out:

bme.performReading();
bme.temperature // C°
bme.pressure / 100.0F; // hPa
bme.humidity; // %h
bme.gas_resistance / 1000.0; // kOhm

#define SEALEVELPRESSURE_HPA (1013.25) // Air pressure at sea level
bme.readAltitude(SEALEVELPRESSURE_HPA); // m

The value for the air quality is obtained by heating the air and measuring the resistance at the measuring layer, which gives me a value in ohms - from which the IAQ index (Indoor Air Quality Index) can be calculated.

More examples can be found in the Git-Repo of the library “Adafruit_BME680”.

Light Sensor

Top, now to the light sensor which is a bit special, because the Adafruit Library is based on the “Unified Sensors” Library of Adafruit. So some things are abstracted, but for some (other) sensors the same:

sensors_event_t event;
tsl.getEvent(&event);
event.light // lux

Further examples can be found in the Git-Repo of the “Adafruit_TSL2591_Library”.

Atmo

To have a better overview and easier access to the values, I store them in a structure:

struct AtmoValues_t {
  float temperature;
  float humidity;
  float pressure;
  float gas;
  int iaq;
  float altitude;
  int light;
};

I store further values in it to transmit them at the end via MQTT. In a loop, all values are now read out every few seconds and sent every minute.

  • Why don't I put the ESP32 into deep sleep and wake it up only every minute to read and transmit the values?

The problem with the gas sensor is that it needs a “burn-in”, i.e. a run time and several cycles until it stabilizes. This can take a few minutes, which does not work in sleep mode.
In addition, I have found that the best way is to use a “baseline” (mean of measurements over a period of time), that individual peaks can be intercepted because the measurements fluctuate.
As you can see, this would be a problem to solve for a battery powered mode.

Calibration

For some of the values a calibration is necessary to ensure that they correspond to real values. However, this is not very easy as I do not have a calibrated “lux” sensor or barometer, so I simply accept these values of the sensors.

For temperature, humidity and IAQ there is a need for optimization. The measured temperature at my place is higher than the actual value (have dug out a thermometer). This is understandable, since the ESP32 and the heater of the gas sensor emits waste heat. In addition, the gas sensor dries the ambient air, which has a negative influence on the humidity. This is the disadvantage of the “all-in-one” solution here. An advantage here would certainly have been an additional, more distant temperature and humidity sensor. In this case, however, the BME680 cannot include the temperature and humidity in the measurements.

Another possibility, which I might implement in the future, is that the gas sensor is used less frequently and is measured/activated offset to the temperature and humidity, so that the heat can evaporate and the air can “regenerate”.

Correcting the temperature
Here I have two options:

  • read the raw (ADC) value from the sensor itself and add an offset.
    But this is not possible with the library. This would have the advantage that according to the documentation the calibrated value for the measurement of the humidity is included.
  • Subtract the determined C° value directly from the result.

Currently I simply add a difference in temperature, which is -2.0°C after the running-in phase.

values.temperature = bme.temperature + atmo_temperature_offset; // -2.0 °C

Indoor Air Quality Index

Another more complex issue. There are various discussions about the IAQ value, and how it should be calculated for the BME680. Bosch keeps the algorithms for this for itself and can only be used with their software. Not quite open source like :(. Additionally, there is no definitive ISO standard/method for calculating the index.

I use the following model, which is discussed in multiple forums:

  • The index is a combination of humidity and gas.
  • 21°C and humidity of 40% and a gas value of 50kOhm would be the index at 0. This would be a “perfect” air.
  • The calculation uses 25% humidity and 75% gas.

The two scores (humidity and gas) are calculated using the baselines and the proportions:

int humidity_score = ATMO_get_humidity_score(current_humidity);
int gas_score = ATMO_get_gas_score(current_gas);

float air_quality_score = humidity_score + gas_score; // score
int air_quality_index = (100 - air_quality_score) * 5; // resize to IAQ index of 0-500

For even more accuracy, the temperature could also be taken into account, but I am content with this approach.

MQTT

After these calculations and corrections, the data is now stored in ‘AtmoValues_t’ and ready for dispatch with MQTT.

I do not explain here how MQTT works, but I recommend another reading.

Each measurement type (humidity, temperature etc.) is a separate topic with the corresponding value. In order to distinguish different devices/stations, the name is encoded in the topic path. So this is also expandable with one outstation or additional sensors per room.

The following results from the current data:

/sensors/atmo/temperature
/sensors/atmo/humidity
/sensors/atmo/gas
/sensors/atmo/pressure
/sensors/atmo/iaq
/sensors/atmo/altitude
/sensors/atmo/light

Instead of encoding the name in the path, a special format could also be used for data transmission values, e.g. JSON and in it the device name as well as the value is present.
To keep it simple, I do without this.

There is also a library from Adafruit for the transmission. This is initialized with a WiFiClient or WiFiClientSecure. “WiFiClientSecure” uses transport encryption (TLS) for the connection to the MQTT.

#include <WiFiClient.h>

WiFiClient wlan_client;
Adafruit_MQTT_Client mqtt(&wlan_client, mqtt_config.server, mqtt_config.port, mqtt_config.user, mqtt_config.password);

So only an Adafruit_MQTT_Publish topic needs to be initialized with the correct path to publish the value.

/**
 * Function to send sensors readings to MQTT Broker in specified topics
 */
bool MQTT_send_values(AtmoValues_t values) {

  MQTT_connect();

  Serial.print(F("\nSending to MQTT...\n"));

  // TEMPERATURE
  topic = Adafruit_MQTT_Publish(&mqtt, String("/sensors/" + hostname + "/temperature").c_str())
  if (! topic.publish(values.temperature)) {
    Serial.println(F("Failed to send temperature"));
  }

  // ...
}

Compile & Run

Just connect the ESP32, enter the configurations for WLAN and MQTT and upload:

17:42:04.147 -> -=# Atmo Weather Station #=-
17:42:04,180 -> I'm awake.
17:42:04.180 -> BME680 detected!
17:42:07.833 -> TSL2591 detected!
17:42:07.867 -> ------------------------------------
17:42:07.900 -> Sensor: TSL2591
17:42:07.933 -> Driver Ver: 1
17:42:07.933 -> Unique ID: 2591
17:42:07.966 -> Max Value: 88000.00 lux
17:42:07.999 -> Min Value: 0.00 lux
17:42:08.033 -> Resolution: 0.0010 lux
17:42:08.033 -> Gain: 25x (Medium)
17:42:08.066 -> Timing: 100 ms
17:42:08.099 -> ------------------------------------
17:42:08.132 ->
17:42:09,095 -> Connecting to WiFi..
17:42:09.128 -> Connected to the WiFi network
17:42:09.162 -> IP address: 192.168.1.200
17:42:09.460 ->
17:42:09,460 -> => Getting a new gas baseline
17:42:13.116 -> Gas Baseline = 409802.438
17:42:13.714 ->
17:42:13.733 -> Sensor Readings:
17:42:13.736 -> Temperature = 23.51°C
17:42:13.770 -> Pressure = 982.12 hPa
17:42:13,770 -> Humidity = 35.6%
17:42:13.803 -> Gas = 337.82 kOhms
17:42:13.836 -> Air Quality Index = 15
17:42:13.869 ->
17:42:13,869 -> Sending to MQTT...
17:42:13,879 -> waiting.

And the reception of the values in the MQTT broker:

Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/temperature', ... (5 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/humidity', ... (5 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/pressure', ... (5 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/gas', ... (6 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/iaq', ... (3 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/altitude', ... (5 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/light', ... (2 bytes))

Hooray. 🎉

Result

I only became aware of the complexity of the topic of correcting and calibrating values when I tried it out. Not only the chip itself, but also the mathematics/physics behind it, like calculating the IAQ or how to calculate the height from pressure and temperature.
In order to stabilize the values further, I am thinking about trying a DH22 sensor to complement the temperature and humidity values of the BME680.

I also want to give something back to the Open Source community, so to find the full code on GitHub:

The other components, like the Atmo-Bridge or dashboards, I will add and describe them down the road of this series.