Weighing Plants

by phidgeteer in Circuits > Raspberry Pi

1746 Views, 6 Favorites, 0 Comments

Weighing Plants

Weighing Plants

It's quite difficult to determine when you should be watering your house plants. In this project, you will learn how to do the following:

  • Monitor the weight and moisture level of a plant
  • Display the data on a website hosted on your raspberry pi

With the moisture data AND weight data all easily viewable from a webpage, determining when to water your plants should be much easier!

Supplies

plantscale_parts.jpg
  • Raspberry Pi
  • VINT Hub Phidget
  • Wheatstone Bridge Phidget
  • Moisture Phidget
  • Red LED
  • Scale Assembly

If you are a student or educator, the components can be found in the Phidgets Plant Kit and the Phidgets Scale Kit ($30 and $40 respectively). Otherwise, all the parts are available at phidgets.com.

Hardware Overview

HUM1100.jpg
scale_kit_product.jpg

Notes about the hardware:

Moisture Phidget

  • Capacitive sensor (will survive in the soil for much longer than a voltage-based sensor, example)
  • Returns a value between 0 and 1 depending on the amount of moisture. Closer to 0 is no moisture and closer to 1 is submerged in water.

Scale Assembly:

Software Setup

Install the Phidget libraries on your Raspberry Pi. This tutorial shows how to do that.

Set up an Apache Web Server on your Raspberry Pi. Documentation.

Software Overview

plantscale_overview.png

As shown above, there are two main parts to this project:

  1. Python script
  2. Webpage

Let's start by taking a look at the Python script.

Python Script

The Python script has to do a few things:

  • Connect to the sensors and configure them
  • Log sensor data at a particular interval

The code below is used to achieve these goals:

# Add Phidgets library
from Phidget22.Phidget import *
from Phidget22.Devices.VoltageRatioInput import *
from Phidget22.Devices.DigitalOutput import *
from Phidget22.Devices.Log import *
# Used to get current time
from datetime import datetime
# Required for sleep statement
import time
# Check file status
import os

#Enable Phidget logging
#The libraries will automatically log any errors/information about the sensors. Check this log periodically to see how things are going.
Log.enable(LogLevel.PHIDGET_LOG_INFO, "plant_log.log")

#Use this to zero out scale when the program starts
def zeroScale():
    statusLED.setState(True) #turn on LED while zeroing
    count = 0
    avg = 0
    while(count < 64):
        avg += scale.getVoltageRatio()
        count += 1
        time.sleep(scale.getDataInterval()/1000.0) #sleep until new data is ready
    scale.offsetVal = avg/count
    print("Offset val: " + str(scale.offsetVal)) #In python you can just add onto an existing class
    statusLED.setState(False)

#Create
soil = VoltageRatioInput()
scale = VoltageRatioInput()
statusLED = DigitalOutput()

#Address
statusLED.setHubPort(0)
statusLED.setIsHubPortDevice(True)
soil.setHubPort(1) #Both soil and scale use the same object, so we must address them
scale.setHubPort(2)

#Open
soil.openWaitForAttachment(1000)
scale.openWaitForAttachment(1000)
statusLED.openWaitForAttachment(1000)

#Zero scale
zeroScale()

# Write Headers if file doesn't exist
if (not os.path.isfile('/var/www/html/data.csv')):
    with open('/var/www/html/data.csv', 'a') as datafile:
        datafile.write("Date,Soil Level,Weight(g)\n")

# Use your Phidgets
while(True): #if an error occurs, just retry for now, if you are getting multiple errors (check your log file), do this in a better way
    try:
        count = 0 #keep track of time
        while (True):
            weightval = 24168000 * (scale.getVoltageRatio() - scale.offsetVal)
            if(weightval < 0):
                weightval = 0
            soilStr = str(soil.getVoltageRatio())
            scaleStr = str(round(weightval, 3))
            now = datetime.now()
            timeStr = now.strftime("%Y-%m-%dT%H:%M:%SZ")
            
            #Format data for csv
            fileStr = timeStr + "," + soilStr + "," + scaleStr + "\n"
            
            #Only update main file every minute, should probably cut back even more
            if(count == 60):
                count = 0
                # Write data to file in CSV format                    
                with open('/var/www/html/data.csv', 'a') as datafile:
                    datafile.write(fileStr)

            # Update incase someone is looking at webpage
            with open('/var/www/html/latest.csv', 'w') as datafile:
                datafile.write(fileStr)
            # Blink LED
            statusLED.setState(not statusLED.getState())
            # Sleep, modify this based on your needs
            time.sleep(1.0)
            #increment count
            count += 1
    except:
        Log.log(LogLevel.PHIDGET_LOG_INFO, "Error in main loop")
            

Python Script Review

Here is a quick review of the code above:

#Add Phidgets library

Import the Phidgets module and any other modules that are needed (e.g. time, datetime, os).


#Enable Phidget logging

The Phidgets library has a prebuilt logging system. If you enable it, it will log all Phidget errors/warnings. This can be useful if you want your program to operate over days/weeks/years without crashing. Usually things will go wrong in the first few minutes/days, so this will help you track down what is happening.


zeroScale

This is used to zero out the scale to start the program. You will want to make sure the scale is empty at this point. For more information, visit the scale project that was mentioned above.


#Create/Address/Open

Configuring the sensors is done here. One thing to note is that both the sensors used in this project (Moisture Phidget and Wheatstone Bridge Phidget) use the VoltageRatioInput API. This means you have to address them by setting the hubport, or else you won't know which software object has opened which sensor.


#Use your Phidget

This section has a main while loop that samples the sensors, converts the scale reading to weight, and writes data to files. The main while loop is embedded in a second while loop that has a try-except block. This is because there may be an error every once in a while if this runs 24/7 (e.g. communication with a sensor may fail, writing to the file may error, etc.) and instead of the program going down, we want to just keep polling. You could limit the number of times the try-except while loop executes, e.g. if there are 100 errors, just end the program, but for now, this is working well enough.


Now that the data is being gathered and logged to a file, the next step is to take the data and display it via a website.

Webpage Review

webpage_folder.png

Previously, we mentioned using the Raspberry Pi documentation to set up an Apache webserver. This information is also covered in this project.

It may sound complicated, but there are only two commands to run:

  1. sudo apt update
  2. sudo apt install apache2 -y

After running the commands in your terminal, you are ready to view/edit your webpage. There will be a folder under /var/www/html that holds all of your web files. In this next step, we will be editing the index.html file to create graphs and labels that display the Phidgets data.


Note: /var/www/html is also where your Python script is now logging data. This is so that it can be served by the webserver and used by the website.

Webpage

plantscale_webpage.jpg

The webpage has a few things to do:

  • Display the primary data file on a graph
  • Display the secondary data file on labels

The code below is used to achieve these goals. Replace the index.html file with the following:

<!doctype html>
<html lang="en">
<head>
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Expires" content="0" />
    <meta charset="utf-8">
	<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
	<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.css" rel="stylesheet" type="text/css">
    <title>Phidgets</title>
</head>
<script>
    var soilgraph;
    var soilwdata = [];
    var soilDataset;

    var weightgraph;
    var weightwdata = [];
    var weightDataset;

    setInterval(function() {
        fetch('latest.csv')
        .then(response => response.text())
        .then(text => {
            let newData = text.split(",");
            let date = new Date(newData[0]);
            document.getElementById("soilLabel").innerHTML = "Current Soil: " + newData[1]
            document.getElementById("weightLabel").innerHTML = "Current Weight: " + newData[2] + " g" 

            //Soil update
            soilwdata.push({ x: date, y: parseFloat(newData[1]), group: 0});
            soilDataset.update(soilwdata);       
            soilgraph.setWindow(null,date);
            soilgraph.redraw()  
            
            //Weight update
            weightwdata.push({ x: date, y: parseFloat(newData[2]), group: 1});
            weightDataset.update(weightwdata);       
            weightgraph.setWindow(null,date);
            weightgraph.redraw()  
        })
    }, 2000);

    function connectPhidgets(){
        fetch('data.csv')
        .then(response => response.text())
        .then(text => {
            
            let data = text.split("\n");

            let options = {
				legend: true,
				width: '75%'
			};

            let groups = new vis.DataSet();
				groups.add({
					id:0,
					content: "Soil Moisture"
				});
                groups.add({
					id:1,
					content: "Weight (g)"
				});

                //Soil
				let soilContainer = document.getElementById('soilgraph');			
				for (let i = 1; i < data.length -1 ; i++) { //first data point is headers, last is new line, so avoid those 
                    realData = data[i].split(",");                    
                    let date = new Date(realData[0]);
					soilwdata.push({ x: date, y: parseFloat(realData[1]), group: 0});
				}
				soilDataset = new vis.DataSet();
				soilDataset.add(soilwdata);
				soilgraph = new vis.Graph2d(soilContainer, soilDataset, groups, options);

                //Weight
                let weightContainer = document.getElementById('weightgraph');			
				for (let i = 1; i < data.length -1 ; i++) { //first data point is headers, last is new line, so avoid those                    
                    realData = data[i].split(",")                    
                    let date = new Date(realData[0]);
					weightwdata.push({ x: date, y: parseFloat(realData[2]), group: 1});
				}
				weightDataset = new vis.DataSet();
				weightDataset.add(weightwdata);
				weightgraph = new vis.Graph2d(weightContainer, weightDataset, groups, options);
        })
    }

</script>
    <body onload="connectPhidgets()">
        <div>
            <h1>Phidgets Data</h1>
            <h2 id="soilLabel"></h2>
            <h2 id="weightLabel"></h2>
            <div id="soilgraph"></div><br><br><br>
            <div id="weightgraph"></div><br><br><br>
        </div>
    </body>
</html>

Webpage Review

Note: the website code can be vastly improved. This is an extremely basic website that displays data on graphs.

We will review the following sections:

  • Main flow
  • Graphing
  • Updating Graphs
  • Caching


Main Flow

The website has two main parts:

  • Webpage load: when the webpage loads the main data file (data.csv) is loaded and the contents of the file are displayed on graphs
  • Every 2 seconds: there is a function that runs every 2 seconds (you can change this frequency) that grabs the small data file (latest.csv) and displays the latest data on the webpage AND updates the graphs.

Graphing

Graphing is accomplished using the vis.js graphing library. When the webpage loads, we fetch the data.csv file, extract the data, and graph it. If you need more information about how this is done, leave a comment below.

Updating Graphs

In order to update the graphs and the labels on the website, we fetch the much smaller latest.csv file, extract the data, update the labels and add the data points to our graphs.

Caching

Caching may need to be looked into more closely on this project. Depending on your browser, it may want to pull old versions of data.csv and latest.csv. There are multiple ways around this, and if you are getting strange results when running the code above, look there first.

Run on Boot

Now that all of your software is running, you will want to make your Python script runs every time your Pi boots. You will want to do this because your Pi will be running "headless" (i.e. without a monitor), so you won't be able to manually run the program.

Follow the instructions on this project for more information.

Install and Run

plantscale_overview.jpg
plantscale_overview2.jpg
plantscale_moisture.jpg

The last step is to install your project and run it. You will likely have to do some debugging to get it running solidly, but if you do, you will have some awesome data to review.

Going Forward Points

plantscale_webpage_comments.jpg

Here are some things you can do to improve on the project:

  • Make your website viewable from the outside world. Right now, your project runs on your local network. Try using PiTunnel to make it accessible from anywhere. This project has some good points.
  • Add some filters to your graphs. Allow users to filter by day, month, year, etc.
  • Think about your data. Right now you have a file that is constantly being written to and is getting larger and larger. Maybe you want to make a backup? Maybe you want to create files based on the month/year instead of just one large file, etc etc.
  • Make your website look better. There are many website templates available online that you can simply import and use.

Some other notes:

  • Load cells can be impacted by temperature which can cause a drift. More data will be collected and this project will be updated with our findings.
  • Load cells can also be permanently deformed if a constant load is applied. More data will be collected to update our findings here as well.
  • The soil moisture sensor shot up from around 0 to around 1 right after watering (the water pooled around the sensor before draining into the soil). The moisture reading then dropped, and slowly crept back up to 1. More watering will give us some insight on this behavior.


If you have any questions, leave a comment below.


Update Oct 11/2021

Part 2 of the project is now available here: https://www.instructables.com/Weighing-Plants-Part-2/