Advertisement

Build a Raspberry Pi Moisture Sensor to Monitor Your Plants

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

In this tutorial, I’m going to harness the awesomeness of Raspberry Pi to build a moisture sensor for a plant pot. You will be able to monitor the sensor locally on the LCD or remotely, via ControlMyPi.com, and receive daily emails if the moisture drops below a specified level.

Along the way I will:

  • wire up and read a value from an analog sensor over SPI using a breadboard
  • format the sensor reading nicely in the console
  • display the sensor reading on an RGB LCD display
  • have the Raspberry Pi send an email with the sensor reading
  • easily monitor the sensor and some historic readings on the web

Raspberry Pi Moisture Sensor
This is what we are creating in this tutorial.

Hardware Supplies

In order to get the most out of this tutorial, it is recommended that you acquire the following:

The Components
The Components

Some Alternative Components

  • Instead of the soil moisture sensor, you can use any type of varying voltage analog sensor or even just a variable resistor for testing.
  • You can use a smaller MCP3004 (4-Channel ADC SPI) if you wish, but the wiring will be different.
  • You can skip the RGB LCD or replace it with an alternative screen. You’ll have to remove or change a few lines in the final script if you do.

The Cost

After adding on a few dollars for the jumper wires the total project cost works out roughly to $55 without the LCD and $82 with. Remember, though, that you’re stocking your electronics kit – all these parts can be used again for other projects.


1. Wiring

If you are using the LCD with the stacking header then connect it to the Raspberry Pi now. The first two stages of the software don’t use the LCD but it will save you some re-wiring time later if you attach it now.

Warning: Incorrect wiring could cause damage to your Raspberry Pi. Make sure to double check all your connections carefully before powering up.

Step 1: Power and Ground Rails

Power and Ground Rails
Power and Ground Rails

On the left in the photo you can see the red and black jumpers going to the + and – rails on the breadboard. To help describe the connections from the header please refer to this wire colour table. Each cell in the table refers to a pin on the Raspberry Pi header. The colour of the cell corresponds to the colour of the jumper wire as seen in the photo:

Wiring Chart
Wiring Chart

Connect pin 1 to the positive rail and pin 6 to the ground rail.

Step 2: MCP3008

Important: The chip must be located over the valley in the breadboard with the pin 1 indicator, the indentation, top right as in the photo.

MCP3008
MCP3008

Refer to this wire colour table and the datasheet when wiring up this chip:

Wire colour table
Wire colour table

All the connections from the rails and the header to the MCP3008 chip go neatly along the bottom row of the chip in this orientation. First connect the power and ground as show in the photo above:

  • Ground rail to DGND
  • Ground rail to AGND
  • Power rail to VREF
  • Power rail to VDD

Next, connect the Raspberry Pi SPI header pins to the chip:

  • Header 26 to CS
  • Header 19 to DIN
  • Header 21 to DOUT
  • Header 23 to CLK
MCP3008 Wired up
MCP3008 Wired up

Step 3: Sensor

Wiring up the moisture sensor
Wiring up the moisture sensor

The sensor wiring is simple; there are three connections to make:

  • Sensor yellow to CH5
  • Sensor red to power rail
  • Sensor black to ground rail
Tip: If you have some spare header pins in your toolbox, you can insert these into the female plug on the Octopus sensor to form a three pin plug. This makes it easy to insert into the breadboard. Alternatively, jumper wire can be used directly into the plug.
Sensor wiring
Sensor wiring

Finally, if you have your plant pot to hand you can insert the probe into the soil now. Make sure not to push it too deep, just cover the prongs:

The moisture sensor
The moisture sensor

2. Preparing The Software Environment

Step 1: Operating System

This project was built using Occidentalis v0.2 from Adafruit, which comes with the hardware SPI driver ready to go. Follow the instructions on the Adafruit site to install it on your Raspberry Pi.

Tip: If you use the Wheezy download from the Raspberry Pi site, you will then need to find and follow some steps to enable the SPI driver yourself..

Step 2: Required Utilities

git – You should already have this (try typing git) if not then install it with: sudo apt-get install git
pip – install this with: sudo apt-get install python-pip

Step 3: SPI Python Wrapper

All the code for this project is written in Python. We need a Python wrapper for the SPI driver so we can read the values from the sensor over SPI:

cd ~
git clone git://github.com/doceme/py-spidev
cd py-spidev/
sudo python setup.py install

Step 4: Adafruit Python Library

You will already have installed the Adafruit library when assembling and testing your LCD. Make sure you know the location of the library as this will be needed by the project code.

Step 5: ControlMyPi Library

ControlMyPi is a service to quickly and easily make control panels on the Internet from your Raspberry Pi Python scripts. Use pip to install it:

sudo pip install controlmypi

Step 6: Project Code

All the code from this tutorial can be downloaded like so:

cd ~
mkdir py
cd py
git clone git://github.com/jerbly/tutorials

You will need to edit some of these files to supply user names, passwords and file paths. This is explained as we go.


3. Reading a Value From the Sensor

The MCP3008 converts the input voltage to a number from 0 to 1023 (10 bits). You can then read this value over SPI (the green and white connections to the header on the Pi). The py-spidev package allows us to do this from Python.

There is some subtle complication with reading a 10-bit number in an 8-bit system. The SPI driver will return two 8-bit words and we are interested in the last 2 bits of the first word followed by all 8 bits of the next word. To turn this into a number we can work with we need to mask and then shift left by 8 the 2 bits from the first word before adding the second word.

You don’t need to worry about coding this though, as in the download I have included the mcp3008.py module to take care of it. You can use an interactive Python shell to test your SPI setup and your wiring like so:

pi@raspberrypi ~/py/tutorials/moisture $ sudo python
Python 2.7.3 (default, Jan 13 2013, 11:20:46)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import mcp3008
>>> mcp3008.readadc(5)
444

Important: You have to run Python with sudo so you can access the SPI device driver.

If you are having problems, first double check your wiring. Then double check the software installation steps above. If you are using the Octopus moisture sensor, hold the prongs to make a connection with your hand. Otherwise, it will likely just read zero.

You can take the sensor out of the equation here by connecting a jumper from the power rail to, say, CH1 and then using readadc(1). This should return 1023 (or close to it). Likewise, a connection from the ground rail should return 0.


Programs

Program 1: Monitoring In the Console

The first program simply extends what we practiced in the interactive console to include a loop so the value from the sensor is printed out continuously:

from time import sleep
import mcp3008

while True:
    m = mcp3008.readadc(5)
    print "Moisture level: {:>5} ".format(m)
    sleep(.5)

Notice that there is a half-second sleep in the loop. This is so the program yields; it gives way to other processes. Otherwise it would use up a lot of CPU time and other processes you are running would not perform so well. A reading twice per second is probably way too much anyway, certainly for a moisture sensor.

The output should be like so:

pi@raspberrypi ~/py/tutorials/moisture $ sudo python moist_cmd.py
Moisture level:   452
Moisture level:   486
Moisture level:   485
Moisture level:   483
Moisture level:   489
Moisture level:   491
Moisture level:   490
^CTraceback (most recent call last):
    File "moist_cmd.py", line 7, in <module>
            sleep(.5)
KeyboardInterrupt
pi@raspberrypi ~/py/tutorials/moisture $

Just press Control-C when you’re finished and the program will exit as above.

Program 2: Better Console Monitoring

This program improves the display to the console. Rather than print the value out to the screen in a scrolling window, this script will create a dashboard effect. Each sensor reading is written over the top of the last so your window doesn’t scroll.

Also red, yellow and green backgrounds are used as traffic light highlights. When the soil is well watered the background will be green. When it is too dry it’ll show red.

To achieve this colouring, special ANSI escape codes are used to send commands to the console. Each escape code sequence starts with \x1b[ followed by the command codes to produce an effect.

from time import sleep
import mcp3008

# ANSI escape codes
PREVIOUS_LINE="\x1b[1F"
RED_BACK="\x1b[41;37m"
GREEN_BACK="\x1b[42;30m"
YELLOW_BACK="\x1b[43;30m"
RESET="\x1b[0m"

# Clear the screen and put the cursor at the top
print '\x1b[2J\x1b[H'
print 'Moisture sensor'
print '===============\n'

while True:
    m = mcp3008.readadc(5)
    if m < 150:
        background = RED_BACK
     elif m < 500:
        background = YELLOW_BACK
     else:
        background = GREEN_BACK
     print PREVIOUS_LINE + background + "Moisture level: {:>5} ".format(m) + RESET
     sleep(.5)

The program clears the screen and displays a title. It then goes into the perpetual loop again but this time uses thresholds to determine the background colour. Finally, the escape sequences and text are printed out followed by a RESET sequence to switch the colouring off. The PREVIOUS_LINE escape code is used to move the cursor back up one line so that we write over the top of the previous value each time.

Run this example like so:

pi@raspberrypi ~/py/tutorials/moisture $ sudo python moist_ansi.py

The output should be something like this:

Output
Output

Program 3: LCD Monitoring

We’re now going to move away from the console and display the sensor data on the LCD instead. Before continuing with this tutorial, make sure you have built your Adafruit LCD with the stacking header and you’ve tested it by following the Adafruit tutorial.

Our program is going to use the Adafruit library so we need to know the full path to the Adafruit_CharLCDPlate directory. I simply create a py directory under the pi user’s home to keep all the Python code in one place, so on my Raspberry Pi the path is:

/home/pi/py/Adafruit-Raspberry-Pi-Python-Code/Adafruit_CharLCDPlate

You may need to adjust the following script if your path is different.

import sys
sys.path.append('/home/pi/py/Adafruit-Raspberry-Pi-Python-Code/Adafruit_CharLCDPlate')

from time import sleep
from Adafruit_CharLCDPlate import Adafruit_CharLCDPlate
import mcp3008

lcd = Adafruit_CharLCDPlate()

while True:
    m = mcp3008.readadc(5)
    try:
        lcd.home()
        lcd.message("Moisture level:\n%d    " % m)
        if m < 150:
            lcd.backlight(lcd.RED)
        elif m < 500:
            lcd.backlight(lcd.YELLOW)
        else:
            lcd.backlight(lcd.GREEN)
        except IOError as e:
            print e
        sleep(.5)

Run the program like so:

pi@raspberrypi ~/py/tutorials/moisture $ sudo python moist_lcd.py

You should see the LCD version of the colour console program. This time the red, yellow and green threshold indicators make use of the RGB backlight.

Liquid Crystal Display (LCD)
Liquid Crystal Display (LCD)

Program 4: Remote Monitoring With ControlMyPi

In this section we’re going to make use of ControlMyPi to produce a dashboard on the Internet. Here’s how the service is described from the FAQ:

ControlMyPi provides a web based service to allow simple Python scripts to be controlled from a panel over the Internet. Your Python script defines a widget layout of labels, buttons, status indicators and more for ControlMyPi to display. When a button is clicked your script will get a message. If you have some status to report, your script can send that to ControlMyPi at any time and it’ll be pushed out to your web browser.

If you haven’t already installed the Python package, then do so now. You will also need an XMPP account. I would recommend a Google Gmail account since this works well and will also be used later when we want our script to send an email. Follow the instructions on the ControlMyPi website to get started and to test your connection.

The first iteration of the dashboard will simply show a single gauge with the sensor reading. We’ll push a new reading up to ControlMyPi every 30 seconds to produce this:

Moisture monitor
Moisture monitor

The script to produce this is still relatively straight-forward:

from time import sleep
import mcp3008
from controlmypi import ControlMyPi
import logging

def on_msg(conn, key, value):
    pass

logging.basicConfig(level=logging.INFO)

p = [ 
    [ ['G','moist','level',0,0,1023] ], 
    ]

conn = ControlMyPi('you@gmail.com', 'password', 'moisture', 'Moisture monitor', p, on_msg)
if conn.start_control():
        try:
            while True:
                    m = mcp3008.readadc(5)
                    conn.update_status({'moist':m})
                    sleep(30)
    finally:
conn.stop_control()

At the top, we’re now importing the ControlMyPi class and the logging module. If you need to debug any XMPP connection problems change the log level to DEBUG.

The on_msg function is what would be called if we defined any buttons or other inputs in our widget layout. Since we only have a gauge there is no input and so this function does nothing.

The list, p, is where the widget layout is defined. Each entry in the list defines a row of widgets to be displayed on the dashboard. We have a single row with a single widget on it, the gauge. Moist is the name of the widget, level is the label to appear on the gauge, 0 is the initial level, 0 is the minimum level and 1023 is the maximum.

The script then creates a ControlMyPi object and starts up. Here you will have to supply your own Gmail address and password.

Tip: This is a secure connection to Google’s servers; the password is not shared with any other service. If you are at all concerned then simply set up another account. In fact, a separate account is quite handy, especially when you want to have automated emails sent from your Raspberry Pi.

Finally, the main loop takes the sensor reading as usual but instead of printing it to the console or the LCD it pushes the new value to ControlMyPi. Updates are sent in a map of widget name / value.

Run the script using sudo as usual:

pi@raspberrypi ~/py/tutorials/moisture $ sudo python moist_cmp.py

Some connection information will be printed to the console followed by “Registered with controlmypi”. Now go to ControlMyPi.com and enter your Gmail address in the form:

Control my Pi
Control my Pi

Click Find panels to show a list of control panels associated with your Gmail address. There should be one in the list, Moisture monitor. Click this to start your panel and see the live data pushed from your Raspberry Pi.

Every 30 seconds, when a new sensor reading is taken, this value is pushed through the XMPP connection to ControlMyPi. It then pushes this data to all viewers of your panel (just you at the moment). You’ll see the gauge move before your eyes! If you like, you can copy/paste the long url and send it to a friend to show off your moisture monitor. Now updates will be pushed to you and your friend.

Program 5: Adding a Line Chart

ControlMyPi has a number of widgets available for you to use on your dashboard. One of the most useful for visualising data is the line chart. The program in this section adds a simple line chart to plot the changes in moisture level over time. To begin, we’ll just plot a new point on the chart every thirty seconds when we take a reading.

from time import sleep
import mcp3008
from controlmypi import ControlMyPi
import logging
import datetime

def on_msg(conn, key, value):
    pass

def append_chart_point(chart, point):
    if len(chart) >= 10:
        del chart[0]
    chart.append(point)
    return chart

logging.basicConfig(level=logging.INFO)

p = [ 
    [ ['G','moist','% level',0,0,100], ['LC','chart1','Time','Value',0,100] ], 
    ]

c1 = []

conn = ControlMyPi('you@gmail.com', 'password', 'moistcmp2', 'Moisture monitor 2', p, on_msg)
if conn.start_control():
    try:
        while True:
            dt = datetime.datetime.now().strftime('%H:%M:%S')
            m = mcp3008.read_pct(5)                
            c1 = append_chart_point(c1, [dt, m])
            conn.update_status({'moist':m,'chart1':c1})
            sleep(30)
    finally:
        conn.stop_control()

In the widget layout, p, you can see the definition of the line chart widget after the gauge. The widget name is chart1, the x axis is Time, the y axis is Value and the max and min values are set at 0 and 100.

Tip: I’ve changed the scale from the raw sensor range (0 to 1023) to a more meaningful percentage value. The mcp3008 module provides a function read_pct() to return an integer percentage point instead of the raw value.

To update the chart on the dashboard, you simply send a list of data points to ControlMyPi. The data points are held in the list c1 in the program and each point consists of a time and a value. The function append_chart_point() is used to maintain a rolling list of the last ten points, so as each new value arrives the oldest one is deleted. Without this the chart would grow forever.

Run the script using sudo as usual:

pi@raspberrypi ~/py/tutorials/moisture $ sudo python moist_cmp2.py

Moisture monitor with widget
Moisture monitor with widget

Program 6: Improving the Line Chart

This script improves on the previous version by:

  • Re-introducing the LCD so we have both local and online monitoring.
  • Recording data points on the line chart over 24 hours.
  • Smoothing out the fluctuations on the chart by taking an average.
  • Saving the chart data to a file so we don’t lose it if we need to restart the program.

import sys
sys.path.append('/home/pi/py/Adafruit-Raspberry-Pi-Python-Code/Adafruit_CharLCDPlate')

from time import sleep
from Adafruit_CharLCDPlate import Adafruit_CharLCDPlate
import mcp3008
from controlmypi import ControlMyPi
import logging
import datetime
import pickle
from genericpath import exists

lcd = Adafruit_CharLCDPlate()

PICKLE_FILE = '/home/pi/py/moisture/moist.pkl'

def on_msg(conn, key, value):
    pass

def append_chart_point(chart, point):
    if len(chart) >= 48:
        del chart[0]
    chart.append(point)
    return chart

def save(data):
    output = open(PICKLE_FILE, 'wb')
    pickle.dump(data, output)
    output.close()

def load(default):
    if not exists(PICKLE_FILE):
        return default
    pkl_file = open(PICKLE_FILE, 'rb')
    data = pickle.load(pkl_file)
    pkl_file.close()
    return data

def update_lcd(m):
    try:
        lcd.home()
        lcd.message("Moisture level:\n%d%%   " % m)
        if m < 15:
            lcd.backlight(lcd.RED)
        elif m < 50:
            lcd.backlight(lcd.YELLOW)
        else:
            lcd.backlight(lcd.GREEN)
    except IOError as e:
        print e

logging.basicConfig(level=logging.INFO)

p = [ 
    [ ['G','moist','% level',0,0,100], ['LC','chart1','Time','Value',0,100] ], 
    ]

c1 = load([])

readings = []

conn = ControlMyPi('you@gmail.com', 'password', 'moisture3', 'Moisture monitor 3', p, on_msg)

delta = datetime.timedelta(minutes=30)
next_time = datetime.datetime.now()

if conn.start_control():
    try:
        while True:
            dt = datetime.datetime.now()
            m = mcp3008.read_pct(5)
            readings.append(m)
            update_lcd(m)
            to_update = {'moist':m}
            if dt > next_time:       
                # Take the average from the readings list to smooth the graph a little
                avg = int(round(sum(readings)/len(readings)))             
                readings = []   
                c1 = append_chart_point(c1, [dt.strftime('%H:%M'), avg])                         
                save(c1)
                next_time = dt + delta
                to_update['chart1'] = c1
            conn.update_status(to_update)
            sleep(30)
    finally:
        conn.stop_control()

You should recognise the content of the update_lcd() function from the earlier program. This function is now called from the main loop to update the LCD on each iteration.

Tip: If you don’t have the LCD simply delete this function and the line that calls it. Also remove the path and import from the top and the line lcd = Adafruit_CharLCDPlate().

Recording Data

To record 24 hours of data on the line chart, we will take a reading every 30 minutes and show 48 points on the chart. The append_chart_point() function has been updated to keep 48 data points. In the main loop we now hold a time 30 minutes in the future in the variable next_time.

Each loop we check the current time against the next_time. If we’ve passed next_time a data point is appended to the chart and next_time is moved forward by 30 minutes again. Using the clock is a neat way to perform actions at different time granularities without having to have multiple counters counting loops and so on. We’ll use this technique again in the final program to send a daily email.

As you may have noticed, the reading from the sensor fluctuates quite a bit. Other types of sensors won’t necessarily do this, but this one does. So, instead of taking a single reading and plotting that on the graph every half an hour, we’re going to plot the average of all the readings taken in the last half an hour. The readings list is used to hold all the readings, int(round(sum(readings)/len(readings))) calculates the average to the nearest whole number. This is then plotted on the chart.

Python’s pickle module is used to save and load the chart data to a file. This simply stores the c1 list in case we have to stop the program and start it again. A reboot for a battery change for example. The save() function is called every time we update the chart and the load() function is called whenever the program starts.

Run the script using sudo as usual:

pi@raspberrypi ~/py/tutorials/moisture $ sudo python moist_cmp3_lcd.py

The screenshot shows a much smoother chart now:

Moisture monitor chart
Moisture monitor chart

The Final Program

To finish this project off, we’re going add one last step: email. We can monitor the sensor from the LCD and look at the current reading and history on the web, but we might forget to check on it after a while. To cover this, we’re going to get the program to send an email once a day whenever the sensor reading is below a specified value.

import sys
sys.path.append('/home/pi/py/Adafruit-Raspberry-Pi-Python-Code/Adafruit_CharLCDPlate')

from time import sleep
from Adafruit_CharLCDPlate import Adafruit_CharLCDPlate
import mcp3008
from controlmypi import ControlMyPi
import logging
import datetime
import pickle
from genericpath import exists
import smtplib

lcd = Adafruit_CharLCDPlate()

PICKLE_FILE = '/home/pi/py/moisture/moist.pkl'

def on_msg(conn, key, value):
    pass

def append_chart_point(chart, point):
    if len(chart) >= 48:
        del chart[0]
    chart.append(point)
    return chart

def save(data):
    output = open(PICKLE_FILE, 'wb')
    pickle.dump(data, output)
    output.close()

def load(default):
    if not exists(PICKLE_FILE):
        return default
    pkl_file = open(PICKLE_FILE, 'rb')
    data = pickle.load(pkl_file)
    pkl_file.close()
    return data

def update_lcd(m):
    try:
        lcd.home()
        lcd.message("Moisture level:\n%d%%   " % m)
        if m < 15:
            lcd.backlight(lcd.RED)
        elif m < 50:
            lcd.backlight(lcd.YELLOW)
        else:
            lcd.backlight(lcd.GREEN)
    except IOError as e:
        print e

def send_gmail(from_name, sender, password, recipient, subject, body):
    '''Send an email using a GMail account.'''
    senddate=datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d')
    msg="Date: %s\r\nFrom: %s <%s>\r\nTo: %s\r\nSubject: %s\r\nX-Mailer: My-Mail\r\n\r\n" % (senddate, from_name, sender, recipient, subject)
    server = smtplib.SMTP('smtp.gmail.com:587')
    server.starttls()
    server.login(sender, password)
    server.sendmail(sender, recipient, msg+body)
    server.quit()

logging.basicConfig(level=logging.INFO)

p = [ 
    [ ['G','moist','level',0,0,100], ['LC','chart1','Time','Value',0,100] ], 
    ]

c1 = load([])

readings = []

conn = ControlMyPi('you@gmail.com', 'password', 'moisture', 'Moisture monitor', p, on_msg)

delta = datetime.timedelta(minutes=30)
next_time = datetime.datetime.now()

delta_email = datetime.timedelta(days=1)
next_email_time = datetime.datetime.now()

if conn.start_control():
    try:
        while True:
            dt = datetime.datetime.now()
            m = mcp3008.read_pct(5)
            readings.append(m)
            update_lcd(m)
            to_update = {'moist':m}

            # Update the chart?
            if dt > next_time:
                # Take the average from the readings list to smooth the graph a little
                avg = int(round(sum(readings)/len(readings)))             
                readings = []   
                c1 = append_chart_point(c1, [dt.strftime('%H:%M'), avg])
                save(c1)
                next_time = dt + delta
                to_update['chart1'] = c1
            conn.update_status(to_update)

            #Send an email?
            if dt > next_email_time:
                next_email_time = dt + delta_email
                if m < 40:
                    send_gmail('Your Name', 'you@gmail.com', 'password', 'recipient@email.com', 'Moisture sensor level', 'The level is now: %s' % m)

            sleep(30)
    finally:
        conn.stop_control()

The send_gmail() function takes care of sending the email. In the main loop, we’re using the clock checking technique to determine whether a day has passed since the last time we sent out an email. If it has then we bump this time on by a day for the next check. Next, if the moisture value is below 40, we send the email.

That’s the program complete! Run it using sudo as usual:

pi@raspberrypi ~/py/tutorials/moisture $ sudo python moist_final.py

There’s one last thing to do: automatically run the program on start up. This will allow you to run your Raspberry Pi headless. To do this edit the /etc/rc.local/ file like this:

pi@raspberrypi ~ $ sudo pico /etc/rc.local

Add this line to the file and save it:

python /home/pi/py/tutorials/moisture/moist_final.py &amp;

Now you can shut down your Raspberry Pi, move it to the plant you’re monitoring, power it up and the program will start for you.


Conclusion

In this tutorial, you learned how to set up and use SPI on your Raspberry Pi for use with an Analog-to-Digital converter. You then used a sensor to monitor the moisture level of the soil in a plant pot. The software allows us to see this sensor reading on the console, on an LCD, on a gauge and chart over the Internet and through a daily email.

There was a lot to learn, but now you can use these techniques for all sorts of different sensors to measure temperature, humidity, light intensity and so on. You could hook it up to an accelerometer or infrared distance sensor too. Have fun!

The finished project
The finished project
Advertisement