How to automate the measurement and collecting data process

So far when we wanted to measure temperature, humidity, air pressure, or C02 concentration in the air, we had to connect to the microcontroller and run some commands in the terminal. We couldn’t save the data obtained from the sensors, could only see them in the terminal. As soon as we disconnected from the microcontroller, and connected again, we had to repeat everything.

I will show you how I automate this whole process, by writing a few scripts and uploading them to the microcontroller.

Detailed code explanation

The first script I wrote is microcontroller.py, where I define the class with the methods on how to connect to Wi-Fi, set the time using NTP (Network Time Protocol), and set a reset pin.

Begin with importing the time module, a module that provides various time-related functions. We will use it together with the ntptime, module used for time synchronization, providing accurate time, International Standard Time (UTC).
Continue with importing the network module, which will assist us in configuring the WiFi connection. From the machine module, we need only Pin class to set a reset pin, so the OLED display can work.

1import time
2import network
3import ntptime
4from machine import Pin

Next thing is to define the ‘Microcontroller’ class. Classes are blueprints for creating objects with certain properties and behaviors in Python.

1class Microcontroller:
2    def __init__(self, SSID, KEY):
3        print(f"Initialising the Microcontroller.")
4        self.SSID = SSID
5        self.KEY = KEY
6        self.sta_if = network.WLAN(network.STA_IF)
7        self.connectToWifi()
8        self.setTime()
9        self.setResetPin()

This method is a special method called the constructor. It's automatically called when you create a new instance of the ‘Microcontroller’ class.
It takes two parameters: SSID (the name of the WiFi network) and KEY (the WiFi password).

1def __init__(self, SSID, KEY):

These lines store the values of SSID and KEY passed to the constructor as attributes of the Microcontroller object.

1self.SSID = SSID
2self.KEY = KEY

This line sets up a way for our microcontroller to connect to the internet through a WiFi network.

1self.sta_if = network.WLAN(network.STA_IF)

At the end we are calling three methods:

1self.connectToWifi()
2self.setTime()
3self.setResetPin()

The connectToWifi() method serves to connect the microcontroller to the WiFi network.
The setTime() method sets the current time on the microcontroller using Network Time Protocol (NTP), and
the setResetPin() method sets a pin (Pin 4) to a high voltage level, which is necessary for an OLED display to work properly.

1def connectedToWifi(self):
2    return self.sta_if.isconnected()

The connectedToWifi() cheks if the microcontroller is connected to the WiFi. I will use this function inside the connectToWifi() method:

1def connectToWifi(self):
2    if not self.connectedToWifi():
3        try:
4            print(f"Enabling WiFi on the Microcontroller.")
5            self.sta_if.active(True)
6            print(f"Connecting to {self.SSID}.")
7            self.sta_if.connect('self.SSID, self.KEY')
8            while not self.sta_if.isconnected():
9                pass
10            print(f"WiFi connected successfully: {self.sta_if.ifconfig()}")
11        except Exception as e:
12            print(f"Could not connect to the WiFi. Error: {e}")
13    else:
14        print(f"WiFi already connected.")

Here is the code for the setTime():

1def setTime(self):
2    if not self.connectedToWifi():
3        print(
4            f"First connect to the internet to set the time using NTP.")
5        print(f"Trying to connect automatically for you.")
6        self.connectToWifi()
7    else:
8        print(f"Automatically setting the time using NTP.")
9        ntptime.settime()
10        print(f"The current local time is: {time.localtime()}")

and the setResetPin():

1def setResetPin(self):
2    print(f"Setting the reset pin to HIGH so the OLED can work.")
3    try:
4        reset_pin = Pin(4)
5        reset_pin.value(1)
6        print(f"Reset pin set to HIGH successfully.")
7    except Exception as e:
8        print(f"Error while setting reset pin to HIGH: {e}")    

The next script is boot.py, a script that will run first when the microcontroller starts up. Before the main code starts to run, we need to configure our microcontroller, and that’s the role of the boot.py script. This can be a lot of things, but in our case, it means that before anything else, we want our microcontroller to connect to the WiFi, and then set the current time and reset the pin to HIGH so the OLED can work.

1from microcontroller import Microcontroller
2SSID = "YourNetworkName"
3PASSWORD = "YourPassword"
4microcontroller = Microcontroller(SSID, PASSWORD)

So import the ‘Microcontroller’ class from the microcontroller.py, then define your WiFi network name and the password, and create an instance of the ‘Microcontroller’ class with the SSID and password of your WiFi network.
Finally, when the boot.py and other scripts are uploaded to the microcontroller, the microcontroller connects to the WiFi, sets the current time, and resets the pin.

Sending data to the Google Sheets

There are many tools that can be used to save the data obtained from the sensors. I decided to use Google Sheets, as a place where I would save and collect the data obtained from the weather station.
IFTTT is another tool that helped me to connect my microcontroller with the Google Sheets, which enabled me to send the data from the microcontroller to the Google Sheets.

Tip

This is the reason why you want to connect your microcontroller to the WiFI, as IFTTT can be used only if your microcontroller has the internet connection.

Before writing the code, we have to set up the Google Sheets and IFTTT. Log in to your Google account and navigate to Google Sheets. Create the new spreadsheet where you want to store your data (I named my spreadsheet “esp32”).

To set up the IFTTT, first create an IFTTT account and open the app in the web. Now do the following:

  • Create a new applet. An applet in IFTTT is a small application that connects two or more services, in our case the microcontroller and Google Sheets, to enable specific functionality.
    • On the homepage of the app, select "Create" in the top right corner.
    • Click the “Add” button inside the “If this” bubble to choose a trigger for your applet.
    • Search and select the trigger service, in our case “Webhooks”.
    • Choose “Receive a web request with a JSON payload” as the specific trigger event.
    • Name your event as you want it, my event is “esp32_measurement”. Then click on the “Create trigger”.
  • Set up Google Sheets as the action.
    • Click the “Add” button inside the “Then That” bubble.
    • Search and select "Google Sheets."
    • Choose "Add row to spreadsheet" as the specific action.
    • When you chose "Add row to spreadsheet", the new page would open. In “Google Sheets account”, type your Google Sheets account and authorize IFTTT to access it. Then click “Create action” button.
    • Then go to the Webhooks page, and click on the “Documentation” button. A new page appears, with the webhook key at the top of the page. Note down your unique key, as you would need it later when writing the code for the post request.

When the Google Sheets and IFTTT are set up, it’s time to write an API.py script, a script that contains a code for sending data from the microcontroller to the Google Sheets.

The first thing to do is to import two modules: urequests and ujson as json.

Urequests module helps us to write HTTTP requests in Python, and ujson module alows us to convert between Python objects and the JSON data format.

1import urequests
2import ujson as json

The only thing to remain is to define the function ‘sendData’, which will be responsible for sending the data to the Google Sheets.

First I define the function ‘sendData’ that takes one parameter, ‘payload’, which is the data to be sent to IFTTT.

1def sendData(payload):

Then create a dictionary named ‘headers’ that specifies the content type of the data being sent as JSON.

1headers = ("Content-Type": "application/json")

This snippet of code checks if the ‘payload’ is empty or None. If it is, the function prints a message indicating that the payload is empty and returns, indicating that no data should be sent.

1if not payload:
2print("Payload is empty or None. Not sending.")
3return

The post request that we have defined contains:

  • url of the IFTTT Webhooks, with the specified event name (esp32_measurement) and Webhooks key. Instead of the ‘esp32_measurement’, you should type your event name.
  • the ‘data’ parameter contains the payload converted to a JSON string using json.dumps() method.
  • headers, which I already explained.
1response = urequests.post(
2    url="https://maker.ifttt.com/trigger/esp32_measurement/json/with/key/'yourwebhookkey'",
3    data=json.dumps(payload),
4    headers=headers)

This checks the HTTP response status code returned by IFTTT. If the status code is 200 (indicating success), it prints a success message. Otherwise, it prints a failure message along with the status code.

1if response.status_code == 200:
2    print("Successfully sent data to IFTTT.")
3else:
4    print(f"Failed to send data to IFTTT. Status code: {response.status_code}")

At the end of of the script, we will close the HTTP response object to free up resources.

1response.close()

Collecting data from the sensors

Before we send the data from the microcontroller, we have to tell the sensors how and when to take the measurements, and where to save the data on the microcontroller, before being sent to the Google Sheets.
All of this will be defined in the sensors.py script, so take a look at the snippets of code and its explanation.

1import dht
2import bmp085
3import mq135
4import machine
5import time
6from API import sendData


As always, we will start with importing things. As you already know, in order to write the code for the sensors, we have to use the sensor’s libraries, in our case dht, bmp085, and mq135.
The machine module contains functions that allow us to access the microcontroller’s hardware. I already explained the time module, from which we will use two functions.
As you may recall, ‘sendData’ is a function responsible for sending the data from the microcontroller to the Google Sheets, via IFTTT.
Now we will define the three classes, each representing one sensor. The first one is the class “DHT11”.

1class DHT11:
2    def __init__(self, pin_number):
3        self.pin = machine.Pin(pin_number)
4        self.sensor = dht.DHT11(self.pin)
5
6    def getTemperature(self):
7        self.sensor.measure()
8        return self.sensor.temperature()
9
10    def getHumidity(self):
11        self.sensor.measure()
12        return self.sensor.humidity()
13
14    def getTemperatureHumidity(self):
15        return self.getTemperature(), self.getHumidity()

The instance of the DHT11 class will be initialized with the pin_number, which represents the microcontroller’s GPI pin that the sensor’s DATA pin is connected to, as an argument.

This class contains three methods:

  • ‘getTemperature’:
    • It first calls the measure() method of the sensor to perform a measurement.
    • It then retrieves the temperature reading using the temperature() method of the sensor and returns the result.
  • ‘getHumidity’:
    • Similar to getTemperature(), but it retrieves the humidity reading using the humidity() method of the sensor.
  • ‘getTemperatureHumidity’:
    • It's a convenience method that calls getTemperature() and getHumidity() to retrieve both temperature and humidity readings at once, returning them as a tuple.
1class BMP180:
2    def __init__(self, scl_number, sda_number):
3        self.i2c = machine.SoftI2C(scl=machine.Pin(scl_number), sda=machine.Pin(sda_number))
4        self.bmp = bmp085.BMP180(self.i2c)
5
6    def getPressure(self):
7        self.bmp.oversample = 2
8        self.bmp.sealevel = 101325
9        self.bmp.blocking_read()
10        return self.bmp.pressure

To initialize the “BMP180” class, we need two arguments: scl and sda pins of the microcontroller.

This class contains only one method called ‘getPressure’. It is used to retrieve the atmospheric pressure reading from the BMP180 sensor. It sets the oversampling and sea level pressure values for the sensor to enhance accuracy, then performs a blocking read from the sensor to measure the pressure. Finally, it returns the measured pressure value.

The last class represents the MQ135 sensor, used to measure the concentration of CO2, expressed in PPM.

1class MQ135:
2    def __init__(self, pin_adc_number):
3        self.mq135 = mq135.MQ135(machine.Pin(pin_adc_number))
4
5    def getPPM(self):
6        return self.mq135.get_ppm()

To initialize this class, you would need to provide the microcontroller’s ADC pin, to which is the sensor’s AO pin connected.

The getPPM() method of the ‘MQ135’ class is used to retrieve the PPM (parts per million) value of CO2 from the MQ135 sensor.

We’re done with the classes representing each of our sensors. Now it’s time to write a class, which will link all of the functionalities we have written so far:

  • Do the measurements.
  • Save the data obtained from the measurements.
  • Send those data to the Google Sheets file.
1class SensorManager(DHT11, BMP180, MQ135):
2    def __init__(self, dht_pin, bmp_scl, bmp_sda, mq135_pin_adc):
3        DHT11.__init__(self, dht_pin)
4        BMP180.__init__(self, bmp_scl, bmp_sda)
5        MQ135.__init__(self, mq135_pin_adc)
6        self.history = {"'temperature': [], 'humidity': [], 'pressure': [], 'co2_ppm': [], 'time': []"}
7        self.interval = 600

The ‘SensorManager’ class inherits from the ‘DHT11’, ‘BMP180’, and ‘MQ135’ classes. This means that ‘SensorManager’ will have access to all the attributes and methods of these classes.

The __init__ method takes four parameters: dht_pin, bmp_scl, bmp_sda, and mq135_pin_adc, which represent the GPIO pin numbers connected to the respective sensors. First thing is to call the constructors of the parent classes (’DHT11’, ‘BMP180’, and ‘MQ135’) using DHT11.__init__(), BMP180.__init__(), and MQ135.__init__(), passing the pin numbers as arguments to initialize instances of these sensor classes.

The self.history dictionary will be used to save the data obtained from the sensors. Remember that in the sendData() method, which takes this object as an argument, he will be converted to the JSON.

Finally, It sets the self.interval attribute to 600 seconds, representing the time interval between sensor measurements.

This class contains two methods: measure() and getDataPeriodically().

The measure() attempts to read temperature and humidity from the DHT11 sensor, pressure from the BMP180 sensor, and CO2 ppm from the MQ135 sensor. It stores the sensor readings along with the current date and time in the self.history dictionary. If an exception occurs during sensor reading, it prints an error message.

The getDataPeriodically() method is used to continuously measure sensor data at regular intervals and send it to the Google Sheets, as we already explained.
Here is the breakdown of this method:

  • It enters an infinite loop to repeatedly measure sensor data.
  • Calls the measure() method to obtain sensor readings and store them in self.history.
  • Prints the current time when the data is measured.
  • Sends the collected sensor data using the sendData() function.
  • Clears the self.history dictionary to prepare for the next round of measurements.
  • Sleeps for the specified self.interval duration before repeating the process.
  • If an exception occurs during data measurement or transmission, it prints an error message.

With the defined “SensorManager” class, our sensors.py script is done. The last script we need to write, before uploading them to the microcontroller, is the main.py script.

Main.py serves as the entry point for the program execution when the microcontroller starts up. This means, it will be automatically executed after the boot.py script, which is the way how to automate the weather station.

1from sensors import SensorManager
2sensors = SensorManager(dht_pin=16, bmp_scl=22, bmp_sda=21, mq135_pin_adc=36)
3sensors.getDataPeriodically()

We start with importing the ‘SensorManager’ class from the ‘sensors’ script. Then create an instance of the ‘SensorManager’ class named ‘sensors’, passing GPIO pin numbers for the DHT11, BMP180, and MQ135 sensors as arguments to the constructor. Finally, call the getDataPeriodically() method of the ‘sensors’ instance to continuously measure sensor data at regular intervals and send it to the Google Sheets file.

Info

I made a github repo, containing all of the scripts I wrote for the weather station. If you want to see it, please check this link

Upload the scripts

That’s it! All of the code is writen and ready to deploy. To make it happen, you have to upload the next scripts to the microcontroller:

  • the sensor’s libraries:
    • bmp085.py and mq135.py, remember that dht11.py is already on your microcontroller.
  • API.py
  • urequests.py
  • boot.py
  • microcontroller.py
  • sensors.py
  • main.py

After you upload the scripts via ampy in the terminal, close the terminal. Open the MobaXTerm and connect to the microcontroller using the saved session. When you successfully connect to the microcontroller, you will see in the terminal that your microcontroller automatically connects to the WiFi network, sets the time, resets the RST pin of the microcontroller, takes the measurements using sensors, and sends the data obtained from the measurements to the Google Sheets file via IFTTT.

Now your weather station works automatically and you can leave it plugged into your laptop/PC, to do its job!

Weather station in work