One of the most common forms of electronics projects is those that monitor events using sensors providing the data either to another machine, cloud service, or local server (like a web server). One way to do that is to wire your Pico up to a set of sensors and then log the data. You can find several examples of general data loggers on the Internet, but few combine the logging of data with a visualization component. Indeed, making sense of the data is the key to making a successful project.
In this chapter, we won’t jump directly into making our project run on the Internet. Rather, we will start with the basics and explore combining data logging with data visualization. We will use a different OLED made specifically for the Pico using a third-party host board. We will also see how to use an analog sensor that produces analog data that we will then have to interpret. In fact, we will rely on the analog-to-digital conversion (ADC) capabilities of our Pico to change the voltage reading to a value we can use. Finally, we will be reusing the RTC module from Chapter 6.
However, this chapter includes a few hardware challenges that are great examples of incompatibilities among components that you may encounter in your own projects. We will explore these issues in detail along with solutions and ways to mitigate the issues. With that comes added complexity that makes this project the most complex so far in the book.
As you will see, the code used in this chapter is more modular and uses more functions than previous projects, but not much more than the previous examples. However, the use of a third-party host board makes the project quite different. As you will see, the code isn’t difficult to learn and uses concepts we have seen in previous chapters. It is the hardware that is the most challenging.
Overview
In this chapter, we will implement a plant soil moisture monitoring solution (plant monitor for brevity). This will involve using one or more soil moisture sensors connected to our Pico. We will set up a timer alarm (an interrupt) to run periodically to read the data from the sensors and store it in a comma-separated value (CSV) file as well as display the last value read and average over time.
The project also supports a rudimentary user interface that includes four buttons whose functions include turning the display off, turning it back on, and clearing the data log (the extra button is used to confirm the delete). We will also use an LED to indicate when a sensor is being read.
We will be separating the code for reading the sensor from the display. This means we can reuse or modify either without confusing ourselves as we dig into the code. For example, so long as the visualization component reads the sensor data from the file, it doesn’t matter to the sensor reading code how it is used. The only interface or connection between these two parts is the format of the file, and since we’re using a CSV file, it is very easy to read and use in our code.
To make things more interesting and to make it easier to code, we will place all the sensor code in a separate code module. Recall, this is a technique used to help reduce the amount of code in any one module, thereby making it easier to write and maintain.
Now let’s see what components are needed for this project, and then we will see how to wire everything together.
Since we are well into our third project and have seen many of the techniques employed in this project, some topics such as wiring and setup of the hardware shall be brief in favor of discussing the hardware details.
Required Components
The components for this project include a new host board that you can plug your Pico into that supports two additions (copies) of the GPIO headers, allowing you to use up to two modules made for the Pico.
One of those modules is called the Pico Display, which has a nice RGB OLED screen that is about 75% of the size of the Pico. Onboard that module are four buttons and an RGB LED, making this module very handy in creating simple user interfaces like the one for this project.
Required Components
Component | Qty | Description | Cost | Links |
---|---|---|---|---|
Soil moisture | 1+ | Sensor | $6.95 | |
RTC breakout board | 1 | RTC module with battery backup | $15.95 | |
$7.50 | ||||
Coin cell battery | 1 | CR1225 (SparkFun RTC) | $1.95 | |
CR1220 (Adafruit RTC) | $0.95 | |||
Host board | 1 | Omnibus | $7.75 | https://thepihut.com/collections/pico/products/pico-omnibus-dual-expander |
OLED | 1 | Pico Display | $14.00 | https://thepihut.com/collections/pico/products/pico-display-pack |
Jumper wires | 3 | M/M jumper wires, 7” (set of 30) | $2.25 | |
M/M jumper wires, 6” (set of 20) | $1.95 | |||
Jumper wires | 4 | F/F jumper wires, 6” (set of 20) | $1.95 | |
F/F jumper wires, 6” (set of 40) | $3.95 |
You can purchase the components separately from Adafruit (adafruit.com), SparkFun (sparkfun.com), or any electronics store that carries electronic components. Costs shown are estimates and do not include any shipping costs.
Notice we are using two different forms of jumper cables. The usual we’ve seen before with a male connector on each end as well as a female/female cable, which we will use to connect to the RTC from the host board. However, the number of M/M jumper wires needed will vary depending on how many sensors you plan to use.
Now, let’s discuss the new components we will be using.
Pico Omnibus
Pimoroni sells a variety of modules you can connect to the Pico Omnibus. See https://shop.pimoroni.com/collections/pico for more details and the latest offerings. You can purchase Pimoroni components at adafruit.com, sparkfun.com, and thepihut.com or directly from pimoroni.com.
Pico Display
Soil Moisture Sensor
Soil moisture sensors come in a variety of formats, but most have two prongs that are inserted into the soil and, using a small electrical charge, measure the resistance between the prongs. The higher the value read, the more moisture is in the soil. However, there is a bit of configuration needed to obtain reliable or realistic thresholds. While the manufacturer will have threshold recommendations, some experimentation may be needed to find the right values.
These sensors can also be affected by environmental factors including the type of pot the plant is in, the soil composition, and other factors. Thus, experimenting with a known overwatered soil, dry soil, and properly tended soil will help you narrow down the thresholds for your environment.
Of special note is how these soil moisture sensors work. If you were to leave the sensors powered on, they can degrade over time. The metal on the prongs can become degraded due to electrolysis, thereby dramatically reducing its lifespan. You can use a technique of a GPIO pin to power the sensor by turning the pin on when you want to read a value. Keep in mind there will be a small delay while the sensor settles, but we can use a simple delay to wait and then read the value and turn the sensor off. In this way, we can extend the life of the sensor greatly.
The soil moisture sensors come using a variety of connectors from a terminal block to one of several connectors with pins. Be sure to check your soil moisture sensors to ensure you use the correct jumper wires. For example, you can use a male-to-female jumper wire for the terminal block version or a female-to-female connector for those using standard pins.
Potential Hardware Conflicts
Now, let’s talk about a subject that occurs more often than you think – conflicts between hardware components. Most times, conflicts can be resolved by changing the software libraries we use like using SoftI2C or SoftSPI or even a different driver, but other times it’s simply because of how the hardware is wired internally.
In this case, we have a potential conflict between the Pico Display and the pins needed for the soil moisture sensors as well as the RTC module. Yes, all three have a potential to make your project miserable! Since this can happen in other projects, we need to examine the issue in more detail to prepare you to diagnose and overcome the situation.
Let’s begin by looking at the interface pins that the Pico Display uses. The nice folks at Pimoroni have provided us with an excellent color-coded chart that has on the left a view from the top of the module (looking at the OLED), and on the right is a view from the underside.
Looking at the left side, notice the blocks that have a text box next to them. These are the pins the Pico Display uses. To be safe, we should avoid using these pins for other hardware. That is normally an easy thing to do, but in this case, we will need three pins for each soil moisture sensor (although you can combine the ground pins) and four for the I2C interface for the RTC. Since we must avoid using the same pins as the Pico Display, we must make our choices for the power and signal pins for the soil moisture carefully.
For example, if you were to use pins numbered 9 and 10 for the power pins on the soil moisture sensors, these are wired to the RGB LED on the Pico Display. So, each time you power the soil moisture on, you will see the RGB LED turn on. That might be fine if you want to turn the LED on when reading, but it is a good example of hardware conflicts.
Another thing to consider is the signal pins for the soil moisture sensors require analog-to-digital (ADC) pins for the soil moisture sensors. However, the Pico has only three pins that can do ADC conversions, GPIO26, GPIO27, and GPIO28, which limits us to at most three soil moisture sensors.
Now, let’s see how to wire the components together.
Set Up the Hardware
Since we are using the Pico Omnibus, we need to take a slight detour and load a custom image provided by Pimoroni. It is much easier to load the custom image than to try and install all of the libraries needed to use the Omnibus and Pico Display. We will need the same library we used in Chapter 6 for the RTC, but the custom image has all of the other libraries we will need.
Now that we’re aware of the limitation of the pins we need to use and the layout change for the Omnibus, let’s first discuss how to install the custom image before we discuss how to connect the hardware.
Load the Pimoroni Image on the Pico
Pimoroni has prepared a special, custom image that includes all of the libraries we will need to use the host board and the display. Recall from Chapter 1, to install an image, we first download the .uf2 file and then copy the file to our Pico in boot select mode.
For the Pimoroni image, begin by visiting https://github.com/pimoroni/pimoroni-pico/releases/ and click the link to download the MicroPython .uf2 image for the latest version. For example, the latest version at the time of this writing was version 0.2.5, and the link to the .uf2 file is https://github.com/pimoroni/pimoroni-pico/releases/download/v0.2.5/pimoroni-pico-v0.2.5-micropython-v1.16.uf2.
Next, unplug your Pico from your PC and hold down the BOOTSEL button and reconnect to your PC. Release the BOOTSEL button and then drag and drop the .uf2 file to the RPI-RP2 drive. Once the copy is finished, you can then disconnect and reconnect the Pico.
Now that we have our custom image installed, let’s see how to connect the hardware.
Connecting the Hardware
Connections for the Plant Monitor
Omnibus | Pin Number | Component | Pin |
---|---|---|---|
VBUS | 40 | RTC | 5V |
GND | 38 | RTC | GND |
GP11 | 15 | RTC | SCL |
GP10 | 14 | RTC | SDA |
GND | 8 | Soil #1 | GND |
GP21 | 27 | Soil #1 | VCC |
GP27 | 32 | Soil #1 | SIG |
GND | 3 | Soil #2 | GND |
GP22 | 29 | Soil #2 | VCC |
GP28 | 34 | Soil #2 | SIG |
Of course, you must insert the soil moisture sensors into the soil of your plants. If your plants are located further away from your power source, you may need to use longer wires to connect the sensors. You should start with a single, small plant and one sensor (or for testing, two sensors in one plant) that you can place close to your PC (or power source).
You will need soil moisture sensors that can operate at 3.3–5V. Some MicroPython boards may limit output on the pins to 3.3V. The sensors from SparkFun are compatible.
Once again, always make sure to double-check your connections before powering the board on. Now, let’s talk about the code we need to write. Don’t power on your board just yet – there is a fair amount of discussion needed before we’re ready to test the project.
Write the Code
Now it’s time to write the code for our project. The code is longer than what we’ve seen thus far, and due to all the bits and bobs we’re working with, it is best to divide the project into parts. So, we are going to write the code in stages. We won’t have a working project until the end, so most of the discussion will be about the individual parts. We will put it all together before testing the project.
ReadTimer: A class to control how often the code reads the sensors. Recall, the soil moisture sensors need some time to power on, stabilize, and read.
SoilMoisture: A class to read one or more soil moisture sensors and return the data. The class will also save the data collected to a comma-separated value file (CSV).
PlantDisplay: A class to display the data to the Pico Display.
However, before we examine the class modules, we need to work on calibrating our sensors.
Calibrating the Sensor
Calibration of sensors is very important. This is especially true for soil moisture sensors because there are so many different versions available. These sensors are also very sensitive to the soil composition, temperature, and even the type of pot in which the plant lives. Thus, we should experiment with known soil moisture, so we know what ranges to use in our code.
More specifically, we want to classify the observation from the sensor so that we can determine if the plant needs watering. We will use the values “dry,” “Ok,” and “wet” to classify the value read from the sensor. Seeing these labels is much easier for us to determine – at a glance – whether the plant needs watering. In this case, the raw data such as a value of 1756 may not mean much, but if we see “dry,” we know it needs water.
Since the sensors are analog sensors, we will use the analog-to-digital conversion on the board. When we read the data from the pin, we will get a value in a range starting at zero. This value is related to the resistance the sensor reads in the soil. Low values indicate dry soil, and high values indicate wet soil.
However, the sensors from different vendors can vary widely in the values read. For example, sensors from SparkFun tend to read values in the range 0–32768, but sensors from other vendors can read as high as 65535. Fortunately, they all seem to be consistent in that the lower the value, the drier the soil.
So, we must determine thresholds for the three classifications. Again, there are several factors that can influence the values read from the sensor. Thus, you should select several pots of soil including one that you feel is dry, another that is correctly watered, and a third that is overwatered. The best thing to do is select one that is dry, take measurements, then water it until the soil moisture is correct, measure that, then water it again until there is too much water.
To determine the threshold, we must first write a short bit of code to set up our board for reading values from the sensor. This includes choosing a GPIO pin that supports ADC. We also need to choose a pin to use to power the board. This is also an analog output pin. We will use GP27 for the sensor signal pin to read data and GP21 for the power pin. The ground for the sensor can be connected to any of the ground pins on the Pico.
Finally, we will write a loop to read several values every five seconds and then average them. Five seconds is an arbitrary value, and it was derived from reading the data sheet for the sensor. Check your sensors to see how much time is needed for the read to settle (maybe under the heading of frequency of reads).
Calibrating the Soil Moisture Threshold
Running the Calibration Code
Here, we see an average value of 770 (always round the number – you need integers). Further tests running the code on dry soil resulted in a value of 425 and for a wet plant, 3100. Thus, the thresholds for this example are 500 for dry and 2500 for wet. However, your results may vary greatly, so make sure to run this code with your sensors, board, and plant of choice.
To make things easier for calibrating the thresholds, use sensors from the same vendor. Otherwise, you may have to use a different set of thresholds for each sensor supported.
Notice the values read. As you can see, the values can vary from one moment to another. This is normal for these sensors. They are known for producing some jumpy values. Thus, you should consider sampling the sensor more than once to get an average over a short period rather than a single value. Even taking an average can be skewed slightly if one or more of the samples is off by a large margin. However, sampling even ten values and averaging will help reduce the possibility of getting an anomalous reading. We will do this in our project code.
Now that we have our threshold values for our sensors, we can begin with the code modules for the classes.
Class Modules
The first part of the project will be to create the code modules to contain the new classes that contain all the functionality to read data from the sensors, save the data to a file, and display the information on a display. In this section, we will see how to write the code for the class modules starting with the timer.
ReadTimer
We will use a new class named ReadTimer to create a hardware timer that we can use to read the values from the sensor. Since we will use a loop to read the sensor waiting 5 seconds for each read, we will need a minimum of 50–55 seconds to read ten values. Thus, we cannot set the update frequency to anything less than about one minute. While you may want to set this to a low value for testing, you certainly do not want to check the soil moisture of your plants every minute. That is, how often do you check your plants normally? Once every few days or once a day? Why check it sooner than normal?
How often you sample data from a sensor (also called sampling rate) is often overlooked when designing sensor networks. The tendency is to store as many values as you can, thinking more data is better. But that is not applicable in the general case. Consider the plant monitoring project. If you normally check your plants once per day, how can sampling the sensors once every five minutes benefit you? It won’t!
Sampling rate must be calculated carefully to deliver the data you need to draw conclusions without creating too much data. While more data is always better than too little data, saving data too often at unrealistic frequencies can generate so much data that it could exceed the storage capacity of your device.
You should carefully consider the sampling rate when designing projects that sample sensors. Choose a sampling rate that is based on realistic expectations. Generally, if you are sampling data that can change very slowly, the sampling rate should be long. Sampling data that can change more quickly should have a higher (shorter time between samples) sampling rate.
For this project, we will set the frequency at two minutes (120,000 microseconds).
The design of this class is a bit new and may seem a bit unorthodox at first. Rather than use a timer as a callback function and assign it to a hardware timer, we will use the hardware timer to set a variable named data_read_event to True when the timer fires. We can then create a function to get the value of that variable as well as reset it (set it to False). This way, we can use the hardware timer to periodically set the data_read_event to True, and once we’ve read the data, set it to False. This allows the code for our soil moisture class to run independently from the timer.
read_data_event(): The callback function as described earlier to set the read data event variable.
time_to_read(): A function callers can use to get the read data event variable.
reset(): A function callers can use to reset the read data event variable.
The ReadTimer Class
SoilMoisture
This class is where we will read the soil moisture sensors and record the data in a CSV file. We will write the class so that most of the work in writing data to the CSV file will be functions used only within the class, but we will expose one function to clear the CSV file. Recall, the user interface has a button that clears the log file. The code is designed to create the file even if it doesn’t exist.
Constructor
The class is designed to read any number of sensors via a list of dictionaries passed when the class is instantiated. Thus, we will write the constructor to accept the list and set up the sensors. To do so, we will use a new list of dictionaries that contain the Pin class instantiations for controlling the power (turning on or off) and the ADC class instantiations for reading data (signal pin).
Public Functions
clear_log(): Clears the log file (erases all data in the file)
get_values(): Returns the values read
read_sensors(): Reads the data from the sensor if the read timer has fired (data_read_event is True)
Notice we use the same code from the threshold.py example to turn on the power pin, wait five seconds, then read the value using the ADC class. We do this ten times and then average the values.
Private Functions
_format_time(): Format the time (epoch) for a better view
_get_value(): Read the sensor ten times and average the values read
_convert_value(): Convert the raw sensor value to an enumeration
If you want to start with some sample data, you can do so, but just make sure it is comma separated with no spaces and one line of data per row.
The SoilMoisture Class
PlantDisplay
The last class we will create is a class to display data to the Pico Display. We place this code in a separate class to keep the display portion of the code separate from the data. There are no surprises in this code other than how to initialize and communicate with the Pico Display.
Recall, the user interface allows the user to turn the screen off and back on, so this class will need to take care of those operations. Also, there are cases where we want to display a message to the user, so the class will provide that feature as well.
clear_screen(): Clear the screen
_write_text(): Write data to the screen
screen_on(): Turn the screen on
screen_off(): Turn the screen off
show_data(): Show the data on the OLED
show_message(): Clear the screen and write a message
is_screen_on(): Return True if the display is turned on
button_pressed(): Return the button pressed or None if no buttons are pressed
The PlantDisplay Class
Now, let’s see the main code for this project.
Main Code
The main code for this project is stored in a file named main.py. It is a continuation of the pattern we saw previously where we create a function named main() and call it from the conditional at the bottom of the file. So, there’s nothing new there, but it is best to take a slower walk through this code as it defines how the project works.
Plant Monitor Complete Code (main.py)
Now, let’s run this project!
Execute
Now is the fun part! We’ve got the code all set up to read soil moisture from our plants and display the data. But first, we have to copy all of our files to the Pico. Go ahead and create a folder named project3 on the Pico and then copy the soil_moisture.py, plant_display.py, read_event.py, and urtc.py (from Chapter 6) to the project3 folder on the Pico. Finally, copy the main.py file to the root folder of the Pico.
Next, we need to insert our soil moisture sensors into our plants. If you need to relocate the plants to your work area, go ahead and do so while you test the project. You may find you will need longer jumper wires if you plan to mount your Pico near the normal location for your plants. Both Adafruit and SparkFun sell longer jumper wires (or you can make your own).
If you do not see the output or the Pico Display does not show any data, be sure to double-check all of your wiring and make sure you’ve copied all of the files to the proper locations on the Pico.
Once everything is working, you can disconnect your Pico and connect it to a 5V power supply to run the project on boot. Cool!
Taking It Further
Add more sensors to expand your project to more plants.
Add LEDs to your board to illuminate when the plants need watering.
Change the color of the text where OK is green, dry is red, and wet is blue.
Make RGB light each time a sensor is read. Use a different color for each sensor.
Change the frequency of the sensor read.
Make the B button force a new sensor read.
Save the sensor configuration to a file and read it from the main application instead of hard-coding the data.
Move the log write/read to a new class and control it from the main.py module.
Of course, if you want to press on to the next project, you’re welcome to do so, but take some time to explore these potential embellishments – it will be a good practice.
Summary
One of the more common forms of electronics or IoT projects is those that generate data (sometimes called data collectors). The implementation of data collectors can vary greatly, but they generally store the data in some location and provide a way to view the data. The simplest forms are those that log the data locally (sometimes called data loggers), as opposed to these that transmitted to a remote server, where the data is stored in a database or a cloud service.
In this chapter, we saw a MicroPython project that logs data read from a series of soil moisture sensors. We created a plant monitoring solution that saved the data to the local SD card. The project also displayed the data on a Pico Display so that we can see the data at any time. This project can be used as a template for a host of data collection projects. You can simply follow the pattern established in this chapter and build your own data logging project.
In the next chapter, we will take a look at a technology that makes creating electronics projects easier using a component system called Grove.