In this Device Kit, we’ll be attaching a soil moisture sensor to a Raspberry Pi and sending its output to Avimesa.Live for further processing. This Device Kit will also introduce you to Avimesa Gadget and display to you how simple it can make almost any IoT project when using Avimesa as your IoT provider.
To successfully set up this Device Kit, a rudimentary knowledge of electronics, the Linux command line interface (CLI), and the Python (3) programming language is (probably) required. To connect our soil moisture sensor to the Raspberry Pi and make use of its analog output, we’ll be using a Microchip Technology MCP3002 ADC (analog-to-digital converter). We should note that the MCP3002 uses SPI for serial communication to its controller (in our case a Raspberry Pi). On the Raspberry Pi, we’ll be using the Python programming language to drive (or control) Gadget as well as to read our soil moisture sensor’s output. To be more specific, we’ll make extensive use of two Python modules: the standard library subprocess module and the third party gpiozero module to, respectively, drive Gadget and deal with the Raspberry Pi’s peripherals (in our case, GPIO and SPI). The gpiozero module is the recommended Python module for controlling the Raspberry Pi’s peripherals.
Components
- a (2) SPI-enabled Raspberry Pi – (1) I’m using this Raspbian (Stretch) Lite image
- an Avimesa Gadget installation
- a valid set of Avimesa device credentials. Get a free Avimesa.Live account here and learn more about what device credentials are here
- Python 3 – This comes pre-installed on most relevant Raspbian images
- a (3) Python 3 gpiozero installation
- an MCP3002-I/P or MCP3XXX-I/P series chip. NOTE: get the one with breadboard legs (-I/P)
- This soil moisture sensor by SparkFun Electronics
- a breadboard and wiring (female and male)
Initial Setup
(1) Once you download a desirable Raspbian image, you can use Etcher to quickly copy it to your microSD.
(2) From the CLI, run sudo raspi-config
and enable SPI via Interfacing Options > P4 SPI > Yes. Next, reboot: sudo reboot
. Finally, verify /dev/spidev0.0
is present in the filesystem after the reboot. If you run into any issues, please visit this page for help troubleshooting.
(3) To install the Python module gpiozero, you’ll first want to make sure your repository list is up to date by running sudo apt update
. Second, install the module for Python (3) using the following command: sudo apt install python3-gpiozero
. If you run into any issues, please visit here for help troubleshooting.
Hardware Setup
Following is a diagram and a table that, together, should help in wiring up the soil moisture sensor, MCP3002, and Raspberry Pi. Here is the datasheet for the MCP3002 and here is a great representation of the Raspberry Pi 40 pinout header and the associated Broadcom connections.
NOTE: XXX denotes no connection in the table.
Raspberry Pi | MCP3002 | Soil Moisture Sensor |
---|---|---|
P1 – 3V3 | P8 – VDD/VREF | XXX |
P6 – Ground | P4 – VSS | GND |
P11 – GPIO17 | XXX | VCC |
P19 – SPI MOSI | P5 – DIN | XXX |
P21 – SPI MISO | P6 – DOUT | XXX |
P23 – SPI SCLK | P7 – CLK | XXX |
P24 – SPI CE0 | P1 – nCS/SHDN | XXX |
XXX | P1 – CH0 | SIG |
Sensor Calibration
Most analog sensors require calibration and the SparkFun Electronics soil moisture sensor is definitely one of those sensors. Our goal here is to determine what outputs from the soil moisture sensor signal wet (saturated) dirt and dry (unsaturated) dirt.
First, we’ll need something to read our sensor’s output. We can use the following Python script to obtain the values described previously:
from time import sleep
from gpiozero import MCP3002, DigitalOutputDevice
def get_soil_moisture():
adc = MCP3002(channel=0) # SPI wrapper for ADC chip
r_val = []
for i in range(10): # 10 readings 5 milliseconds apart
sleep(.005)
r_val.append(adc.value)
adc.close()
return r_val[4] # median reading
sensor_power = DigitalOutputDevice(17)
while True:
try:
sensor_power.on()
raw_sense_read = get_soil_moisture()
print('Calibration reading: {0}'.format(raw_sense_read),
flush=True)
sensor_power.off()
sleep(5)
except KeyboardInterrupt:
break
sensor_power.close()
You’ll want to copy this into something like calibration.py
on the Pi and run it using python3 calibration.py
. You’ll want to have this script running in a terminal connected to your Pi whenever collecting calibration values and, whenever finished, you can kill it with ctrl + c. NOTE: This step will also test our wiring of the sensor and ADC – if you’re not getting values similar to mine, your setup may be incorrect and will require some investigation.
If your setup is correct, let’s get a maximum output for our sensor by steadily holding the soil moisture sensor in some water to get some readings.
This sensor forms an electrical connection whenever there is damp soil, and definitely water, in between its two leads (meant for insertion into some dirt). You may wonder why I’m not using dirt: heavily saturated (wet) dirt and water give almost, if not exactly, the same output values. Whenever the sensor is in (highly conductive) water, we should see something like Calibration reading: 0.809380
coming from our running Python script. It should be stated here that the gpiozero module’s MCP3002 class, a SPI wrapper tailored for an MCP3002 chip, returns ADC readings via its member value
that range from 0.0
to 1.0
(lowest to highest). We now know what to expect from our soil moisture sensor in terms of maximum output values.
Now for the tedious portion of this sensor’s calibration: determining its minimum output (output whenever inserted into dry dirt – think almost dust). To determine this value, I took some of the soil my plant resides in and let it sit in the sun for most of the sunny hours of a day. After this, I put my dried dirt into a container and inserted the soil moisture sensor for some calibration readings via python3 calibration.py
:
Script output: Calibration reading: 0.533799
.
Now let’s use our two calibration values to determine what we’re truly after: the equation of the line for our linear sensor. From algebra:
y = m * x + b
We’re going to calculate m
and b
; x
is our sensor output and y
is our soil moisture saturation. We have two data points: (0.53, 0.0)
and (0.81, 100.0)
which are respectively (x1, y1)
and (x2, y2)
. Now, let’s calculate m
:
m = (y2 - y1) / (x2 - x1)
And now b
using the data point (x1, y1)
:
b = y1 - m * x1
My values are m = 357.0
(rounding) and b = -189.0
. So, my equation is:
y = 357.0 * x - 189.0
Yours will most likely be a little different; note it as we’ll be using it in a later section.
Avimesa.Live Setup
First, you’ll need an active Avimesa.Live account.
Second, you’ll need to obtain a valid set of Avimesa device credentials:
Software
The script:
#!/usr/bin/python3
import subprocess
from time import sleep
from gpiozero import MCP3002, DigitalOutputDevice
def get_soil_moisture():
# SPI wrapper for ADC chip
adc = MCP3002(channel=0)
# 10 readings 5 milliseconds apart
r_val = []
for i in range(10):
sleep(.005)
r_val.append(adc.value)
adc.close()
# median reading
r_val.sort()
r_val = r_val[4]
# equation of the line for our sensor
r_val = 357.0 * r_val - 189.0 # CHANGE ME
return r_val
# Use your own Avimesa credentials here
gdgt_args = ['/usr/local/bin/avmsagadget',
'-i',
'deadbeefdeadbeefdeadbeefdeadbeef', # DEVICE ID
'deadbeefdeadbeefdeadbeefdeadbeef'] # DEVICE AUTHENTICATION KEY
gdgt_timeout = 5
# GPIO17 for momentary sensor power
sensor_power = DigitalOutputDevice(17)
try:
sensor_power.on()
# Allow sensor to electrically settle
sleep(2)
sense_read = get_soil_moisture()
if sense_read > 100.0:
sense_read = 100.0
elif sense_read < 0.0:
sense_read = 0.0
gdgt_input = ('{{"ch_idx":0,"ch_data":[{{"data_idx":0,'
'"units":1,"val":{0}}}]}}\nrun\nexit\n').format(sense_read)
gdgt_input = gdgt_input.encode()
gdgt_complete = subprocess.run(args=gdgt_args,
input=gdgt_input,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=gdgt_timeout)
print("STDOUT:\n" + gdgt_complete.stdout.decode())
print("STDERR:\n" + gdgt_complete.stderr.decode())
except subprocess.TimeoutExpired:
print('Gadget timed out after {0} seconds!'.format(gdgt_timeout))
except subprocess.CalledProcessError as E:
print('Gadget exited with non-zero return code {0}'.format(E.returncode))
print('STDERR:\n' + E.stderr.decode())
finally:
sensor_power.close()
Copy this bad boy into something like avmsa_soil_moisture_sensing
, give your user executable permissions using chmod u+x avmsa_soil_moisture_sensing
, and then run it a few times with ./avmsa_soil_moisture_sensing
. Make sure you updated your equation of the line for your sensor and inserted your device’s credentials into the script! And for automated messages every minute, you can copy avmsa_soil_moisture_sensing
to /usr/local/bin
using sudo cp avmsa_soil_moisture_sensing /usr/local/bin
and then add it as a cron job to run every minute by adding * * * * * /usr/local/bin/avmsa_soil_moisture_sensing
when editing your user’s crontab using crontab -e
.
Avimesa.Live Integration
All we have to do here is add our sensor in Avimesa.Live:
Watering: