Author: **Jeremy Hearfield** (*jh225ic*) # Soil Moisture Sensor Project This tutorial will provide steps on how to build an IoT device that measures the soil moisture and transmits the collected data over the internet for easy monitoring. This guide will showcase how to do the following: * connect a sensor to a microcontroller * write code to process the data collected from the sensor * connect microcontroller to the internet * send collected data over the internet to be visualised **Estimated Build Time:** 1-2 hours Build Result: ![](https://hackmd.io/_uploads/HkVWgBoO2.png) ## Objective I have recently developed an interest in growing own crops/plants at home, so a device that lets me measure the moisture levels in the soil of my plants allows me to gain a better understanding of which plants need more frequent watering. I am also able to see at which times during the day my plants' moisture levels drop the most, meaning that I know when to relocate them (ex. move them into/out of the sun). The purpose of this device is therefore to provide real-time data showing the moisture (in percent) of the soil for each connected plant which is useful in gaining better knowledge of general gardening. My hope is that I will be able to care for my plants better using the insights from this project and hopefully in the future implement more ways of monitoring and perhaps automatically tending to my plants. This being my first IoT project, I will not only gain knowledge from the sensor data, I will also learn much more about the world of IoT and the usefulness of being able to create a simple device such as this. Here are some potential areas that I want this project to branch into in the future: * Automatic watering system connected to moisture data * More sensors connecting to more plants * Adding new sensors for other parameters such as: *temperature, humidity, soil nutrition content, sun brightness, etc*. * Automatic shading mechanism to increase/decrease sun exposure In summary, this device would be useful for anybody who either forgets to water their plants and might need a reminder or simply wants to gain a better understanding of their plants' soil moisture for whatever other purpose. ## Material The table below shows a list of the hardware used in this project, each item's approximate price and specification. | Hardware | Specification | Price (SEK) | Retailer | | -------- | -------- | ------- | -------- | | Raspberry Pi Pico W | A popular microcontroller with WiFi connectivity. RP2040 CPU, ARM Cortex-M0+ 133MHz, 256kB RAM, 30 GPIO pins, 2MB on-board QSPI Flash, CYW43439 wireless chip, IEEE 802.11 b/g/n wireless LAN | 98.00 | [Electro:kit](https://www.electrokit.com/produkt/raspberry-pi-pico-w/?gclid=Cj0KCQjw1_SkBhDwARIsANbGpFvdkdQkZtr43BvzGP3cgM6_aefOt3wP0-Jxq4xvKRszoOwqytsjxm8aApYLEALw_wcB) | | FC-28 Soil Moisture Sensor | Resistive sensor for measuring moisture content in soil between the sensor's two legs. Dry soil means a high resistance => high voltage, wet soil means a low resistance => low voltage.| 29.00 | [Electro:kit](https://www.electrokit.com/produkt/jordfuktighetssensor/) | | Breadboard | Simple board for connecting cables and sensors to microcontroller, with/without soldering. Used in project: Solderless Breadboard 840 tie-points.| 69.00 | [Electro:kit](https://www.electrokit.com/en/product/solderless-breadboard-840-tie-points-2/) | | Jumper Wires | Male - Male wires for connecting sensors to microcontroller (via breadboard).| Very Cheap | [Various](https://www.google.com/search?q=jumper+wires&rlz=1C1CHBD_enSE1025SE1034&oq=jumper+wires&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIMCAEQABgUGIcCGIAEMgcIAhAAGIAEMgcIAxAAGIAEMgcIBBAAGIAEMgcIBRAAGIAEMgcIBhAAGIAEMgcIBxAAGIAEMgcICBAAGIAEMgcICRAAGIAE0gEIMTQxOWowajeoAgCwAgA&sourceid=chrome&ie=UTF-8) | | Micro-USB-cable | USB-A to USB-B Micro (Male - Male) for connecting power and transferring data to/from microcontroller (may be included in microcontroller package).| Free-~100.00 | [Various](https://www.google.com/search?q=usb+a+to+micro+usb&source=lmns&bih=929&biw=1920&rlz=1C1CHBD_enSE1025SE1034&hl=en&sa=X&ved=2ahUKEwjo5Jbopen_AhWNvicCHZ2oDsAQ_AUoAHoECAEQAA) | | **Total:** | | **196.00-296.00** | | Click on retailer for link to website. More information and datasheet can be found there. ## Computer Setup The code for this project is coded in **Micropython**, which is an efficient implementation of the programming language **Python 3** and is optimised to run on a microcontroller. The following steps need to be completed in order to begin writing code onto the microcontroller: (for more in-depth installation instructions for **Steps 1-3** follow [this guide](https://hackmd.io/@lnu-iot/rkiTJj8O9)) ### 1. Install Node js * Download and install Node js [here](https://nodejs.org/en) ### 2. Install Integrated Development Environment (IDE) The IDE used for this project is **Visual Studio Code** (version 1.79.2) running on Windows 11. #### Requirements * Computer (VS Code works on Windows, Mac & Linux) * Internet Access #### Download and install IDE * Click [here](https://code.visualstudio.com/download) to download Visual Studio Code * Run installer and follow the steps ### 3. Install Pymakr extension in VS Code * In VS Code, click on extensions on the left-hand side bar and search "**Pymakr**" and click install on the Pymakr extension * Follow the steps in creating your project on the Pymakr extension page * **Alt**. follow [this guide](https://hackmd.io/@lnu-iot/B1T1_KM83) from applied-iot-lnu and follow **Steps 1-3** under the heading: "*Creating a new project in VS Code*" ### 4. Install Python * Click [here](https://www.python.org/downloads/) and download the latest release of **Python 3** * Run installer and follow the steps Before you can start connecting sensors and cables to pins on the microcontroller, micropython firmware needs to be loaded onto the Raspberry Pi Pico W. ### 5. Flashing firmware onto Raspberry Pi Pico W * Download the latest micropython firmware from [this](https://micropython.org/download/rp2-pico-w/) website (choose the one under **Releases** and not **Nightly Builds**) * Connect the USB-B Micro end of the power cable into the microcontroller first * Hold down the **BOOTSEL** button on the microcontroller while connecting the USB-A end of the cable into your computer's USB port. * A new drive will open in your file system called **RPI-RP2**. Copy and paste the micropython firmware file that you downloaded into the new drive folder. The microcontroller will automatically disconnect from your computer and then reconnect. A more in-depth guide is given [here](https://hackmd.io/@lnu-iot/rkFw7gao_). Once the firmware is on the Pico W you will need to connect the device to the Pymakr project. ### 6. Adding device to Pymakr project * Click on the Pymakr extension icon on the left-hand side in VS Code * Click on "**ADD DEVICES**" under *Empy Project* under *Projects*. The name of the Project may vary if you've named it something else. * Check the box next to the device that you want to connect (mine is called "USB Serial Device (COM3)") * Click OK ### 7. Connect and enter Development Mode Your device should now be added to your project. It is suggested that you put your project in development mode. This means that once you have made changes to your project files and you save it it will automatically upload the latest changes to your device and reset it. * Click on the Pymakr extension icon on the left-hand side bar * Click on the lightning icon next to your device * Click on the </> icon right above, next to your project Your device is now connected and in development mode. * Click on the icon with the symbol > inside a box to create a new terminal Use the shortcut CTRL + S to instantly save your changes to the device and restart it. For **Steps 6-7**, follow [this guide](https://hackmd.io/@lnu-iot/B1T1_KM83) for more detailed instructions. Under the header "**Creating a new project in VS Code**", follow **Steps 4-5**, respectively. ## Putting Everything Together The wiring for this project is very simple. There is only one sensor (FC-28 Soil Moisture Sensor) and one microcontroller (Raspberry Pi Pico W). The connection between the sensor and the Pico W looks like this: ![](https://hackmd.io/_uploads/Hy0WAa3uh.png) GND is connected from the sensor module to pin 38, 3.3V power to pin 36 and analogue data to pin 34 (GPIO pin 28), as seen above. The module part has an additional pin for digital data output which is not used. Image showing how the cables are connected to the Pico W: ![](https://hackmd.io/_uploads/Bk90Np2_2.png) ## Platform The platform of choice for this project is [Adafruit IO](https://io.adafruit.com/). Adafruit is a free cloud service platform used to display and interact with project data in real-time. I will be using this platform because it is free but also seems to be the standard choice for many others doing IoT projects. The interface is very clean and simple to use and connectivity via WiFi is also available. This fits all my requirements, which is why I will be using Adafruit for this project. ## The Code All of the code for this project is contained in the main.py file. There is also a file called mqtt.py which will be mentioned again in the next section. The following code snippet is the section that imports the FC-28 sensor data from GPIO-pin 28 and calculates it into a usable moisture percentage value: ```python= from machine import Pin, ADC from time import sleep def send_moisture(): # Import sensor value sm_sensor = ADC(Pin(28)) sensor_value = sm_sensor.read_u16() # Calculations percentage_step = round(65535 - 12000) / 100 # 0% Wetness is 65535, 100% Wetness is approx. 12000 moisture_val = 65535 - sensor_value moisture_percentage = moisture_val / percentage_step print("Sensor value: " + sensor_value) print("Moisture: " + moisture_percentage) sleep(5) #5 second delay ``` #### Code Explanation This function collects analogue data from GPIO pin 28 and stores it in sensor_value. The highest value attainable (completely dry) is 65535 and the lowest I found was approximately 12000 when the FC-28 moisture sensor was fully submerged in water. I therefore calculate the size of each percentage step, and the moisture value is defined as the difference between maximum sensor value and current sensor value. The higher this value, the more wet the sensor measures the soil to be. This value is then converted into a more readable percentage value which is stored in the variable moisture_percentage and later sent to Adafruit. After all calculations there is a 5-second sleep delay. This means that a new moisture value is sent every 5 seconds. The function send_moisture() is not shown fully here. The rest will be shown in the next section and is used to send the moisture value to the Adafruit IO server. ## Transmitting the data / connectivity The code used for connecting the microcontroller to WiFi as well as connecting to Adafruit was directly imported from the applied-iot-lnu [hackmd.io](https://hackmd.io/@lnu-iot) page found [here](https://hackmd.io/@lnu-iot/r1yEtcs55?utm_source=preview-mode&utm_medium=rec). The setup in this project for the connection to WiFi and to Adafruit IO was identical to everything in the guide mentioned above - the only change made is in what is sent to the Adafruit IO server and the credentials for the Wireless Network and Adafruit IO configuration. The guide also explains exactly how to set up an Adafruit IO account, create feeds which data is sent into and combine all feeds into a neat dashboard. This setup was followed and I created three feeds: **lights**, **moisture_graph** and **moisture_gauge** which will be mentioned again in the next section where I describe how the data is visualised. The transport protocol used for this project is MQTT. The mqtt.py file was also created exactly as specified in the applied-iot-lnu guide. No changes were made so I won't show the code here. The initial imports and settings were set as following, almost exactly the same as in the guide except for my own personal WiFi credentials and Adafruit IO config: ```python= from mqtt import MQTTClient # For use of MQTT protocol to talk to Adafruit IO import ubinascii # Conversions between binary data and various encodings import machine # Interfaces with hardware components import micropython # Needed to run any MicroPython code import random # Random number generator from machine import Pin, ADC from time import sleep # BEGIN SETTINGS led = Pin("LED", Pin.OUT) # led pin initialization for Raspberry Pi Pico W # Wireless network WIFI_SSID = "OWN WIFI_SSID" WIFI_PASS = "OWN WIFI_PASS" # No this is not our regular password. :) # Adafruit IO (AIO) configuration AIO_SERVER = "io.adafruit.com" AIO_PORT = 1883 AIO_USER = "OWN AIO_USER" AIO_KEY = "OWN AIO_KEY" AIO_CLIENT_ID = ubinascii.hexlify(machine.unique_id()) # Can be anything AIO_MOISTURE_GRAPH_FEED = "OWN AIO MOISTURE GRAPH FEED" AIO_MOISTURE_GAUGE_FEED = "OWN AIO MOISTURE GAUGE FEED" AIO_LIGHTS_FEED = "OWN AIO LIGHTS FEED" # END SETTINGS ``` Part of the guide mentions the functions: * **do_connect()** * **sub_cb()** which were kept completely identical as shown below: ```python= # Function to connect Pico to the WiFi def do_connect(): #import keys import network from time import sleep import machine wlan = network.WLAN(network.STA_IF) # Put modem on Station mode if not wlan.isconnected(): # Check if already connected print('connecting to network...') wlan.active(True) # Activate network interface # set power mode to get WiFi power-saving off (if needed) wlan.config(pm = 0xa11140) wlan.connect(WIFI_SSID, WIFI_PASS) # Your WiFi Credential print('Waiting for connection...', end='') # Check if it is connected otherwise wait while not wlan.isconnected() and wlan.status() >= 0: print('.', end='') sleep(1) # Print the IP assigned by router ip = wlan.ifconfig()[0] print('\nConnected on {}'.format(ip)) return ip # Callback Function to respond to messages from Adafruit IO def sub_cb(topic, msg): # sub_cb means "callback subroutine" print((topic, msg)) # Outputs the message that was received. Debugging use. if msg == b"ON": # If message says "ON" ... led.on() # ... then LED on elif msg == b"OFF": # If message says "OFF" ... led.off() # ... then LED off else: # If any other message is received ... print("Unknown message") # ... do nothing but output that it happened. ``` The function **do_connect()** connects the device to the WiFi Network specified in the settings earlier. **sub_cb()** is used to turn on and off the onboard LED using the lights feed (copied exactly from the guide mentioned earlier). I left this function in the code despite it not having to do anything with my project so that I am able to check if my device is connected to Adafruit. More on this in the next section. The following functions from the guide were altered: * **random_integer()** * **send_random()** random_integer() was deleted completely and send_random() was replaced by send_moisture() and edited to what was shown in the previous section. In the send_moisture() function, after the data is imported from the sensor and the moisture values are calcualted, the following code is used to send the data to the Adafruit IO server: ```python= try: client.publish(topic=AIO_MOISTURE_GRAPH_FEED, msg=str(moisture_percentage)) client.publish(topic=AIO_MOISTURE_GAUGE_FEED, msg=str(moisture_percentage)) print("DONE") except Exception as e: print("FAILED") ``` The contents of the format function in the line printing "Publishing: {0} to {1} ... ".format(...) was changed to AIO_MOISTURE_GRAPH_FEED and then copied again using AIO_MOISTURE_GAUGE_FEED instead. The same change was made in what is published to the Adafruit IO server under try: client.publish(...). There, the topic is set to AIO_MOISTURE_GRAPH_FEED and again to AIO_MOISTURE_GAUGE_FEED so as to send the same string "moisture_percentage" to both feeds. The following code section is located furthest down in the main.py file and runs all the functions after they have been defined. The comments describe what each part does. Everything is the same as in the [applied_iot_lnu guide](https://hackmd.io/@lnu-iot/r1yEtcs55?utm_source=preview-mode&utm_medium=rec) except where send_moisture() is called (this used to be send_random() which sent a random number to the Adafruit IO server) in the "while 1:"-loop. This while-loop repeats and calls send_moisture() forever. ```python= # Try WiFi Connection try: ip = do_connect() except KeyboardInterrupt: print("Keyboard interrupt") # Use the MQTT protocol to connect to Adafruit IO client = MQTTClient(AIO_CLIENT_ID, AIO_SERVER, AIO_PORT, AIO_USER, AIO_KEY) # Subscribed messages will be delivered to this callback client.set_callback(sub_cb) client.connect() client.subscribe(AIO_LIGHTS_FEED) print("Connected to %s, subscribed to %s topic" % (AIO_SERVER, AIO_LIGHTS_FEED)) try: # Code between try: and finally: may cause an error # so ensure the client disconnects the server if # that happens. while 1: # Repeat this loop forever client.check_msg()# Action a message if one is received. Non-blocking. send_moisture() # Send moisture value to Adafruit IO. finally: # If an exception is thrown ... client.disconnect() # ... disconnect the client and clean up. client = None print("Disconnected from Adafruit IO.") ``` ## Presenting the data So, the data is collected and calculated in the Pico W microcontroller, and then sent to Adafruit IO over WiFi. Here, the data is stored in each feed. There are three feeds: * **lights** * **moisture_graph** * **moisture_gauge** All feeds are displayed on the dashboard called "**Moisture Sensor Dashboard**" as shown below: ![](https://hackmd.io/_uploads/H1trb0h_3.png) The lights feed is connected to the toggle button (currently set to OFF) which simply turns on or off the onboard LED on the Pico W device. This takes effect every 5 seconds at the same time as the next moisture value is imported, when the next iteration of the "while 1:"-loop is repeated and client.check_msg() is run. The moisture_graph feed is shown in the graph in the top-left of the dashboard. A new data point is created every 5 seconds which then updates the graph. As you can see, the moisture value drops nicely in the first half of the graph, from 12AM to 12AM the next day. At 12AM I watered the plant, right where the sensor is, so the moisture rose to 96%. The moisture then declines steadily for the next 8 hours. The moisture_gauge feed is shown on the right-hand side where the gauge meter is. This shows the current moisture percentage and is also updated every 5 seconds. The value shown here every fifth second is the same as is recorded into the graph as a new data point. The graph displaying the moisture is currently set to show the past 2 days. This can be changed to show either longer or shorter periods of time, so as to either see multiple waterings over multiple days or to see more recent data in more detail. ## Finalizing the design I think this project was a success and I am very happy with how it turned out. The data visualisation in Adafruit is very easy to read and it shows exactly what I expected when I started this project. Below is another image of the dashboard with the LED light toggled on, an image of the moisture sensor implanted into the soil and another image of the full device setup: ![](https://hackmd.io/_uploads/rJ3OER3_n.png) ![](https://hackmd.io/_uploads/rJ454Ah_h.png) ![](https://hackmd.io/_uploads/HJ9i4Ah_2.png) Thank you to you for reading and thank you to Linnaeus University for providing valuable lectures, workshops, guides and support in this IoT venture. I have learnt a lot and this will be a great stepping-off point for me concerning the Internet of Things. I would like to add additional moisture sensors for more plants and also new sensors for measuring other parameters apart from soil moisture. If I move this setup outside I might need to implement a LoRaWan connectivity solution. The ultimate goal is to create a fully automated system for monitoring and watering a full garden of crops and plants.