# Tutorial on measuring Soil and Air temperature with LoPy4
## Oliver Olsson oo222ew
### Short overview of project
In this project, you are going to learn how to read the temperature in both the soil of a flower pot and in the air nearby. We are also going to take into account the amount of sunlight that is shining on the pot to see if there are any differences between the soil and air temperature.
The data that we are collecting are then going to be send to the pybytes-platform and displayed in nice charts.
#### Estimated time for the project
- Computer and pybytes setup: 1 hour
- Wiring and coding: 2 hours
- 3D-prints: 15-20 hours
### Objective
This project was an eximination of the course "Tillämpad (Applied) Internet of Things, Introduktion (Introduction)" given at Linnéuniversitetet. We had free hands to do what ever we wanted, as long as it had to do with "Internet of Things".
Therefore I chose to do something with my plants at home, and I found it interesting to examine how the soil temperature is affacted by the surrounding air temperature, and the intensity of the sun light on the plants and soil.
### Materials
| Unit | Picture |Specification|Price (euros)| Link |
| ---------- |:------|--------|---|-----|
|LoPy4||MicroPython board with LoRa, Sigfox, Wifi and bluetooth|34.95|https://pycom.io/product/lopy4/|
|Expansion board 3.0||LoPy4 compatabile expansion board|16.00|https://pycom.io/product/expansion-board-3-0/|
|USB-kabel A-male – micro B 5p male 1m||USB connection between LoPy4 and computer|~3.7|https://www.electrokit.com/produkt/usb-kabel-a-hane-micro-b-5p-hane-1m/|
|DS18B20||Waterproof 1-wire temperature sensor|~9.5|https://www.electrokit.com/produkt/temperatursensor-vattentat-metallholje-ds18b20/|
|MCP9700 TO-92||IC temperature sensor with linear output voltage depending on temperature|~0.76|https://www.electrokit.com/produkt/mcp9700-e-to-to-92-temperaturgivare/|
|Breadbord| ||~5.6|https://www.electrokit.com/produkt/kopplingsdack-400-anslutningar/|
|Resistor 330 ohm|||~0.1|https://www.electrokit.com/produkt/motstand-kolfilm-0-25w-330ohm-330r/|
|Photoresistor 2-5 kohm||Lightsensitiv resistor that changes resistans depending on light|~0.8|https://www.electrokit.com/produkt/fotomotstand-cds-2-5-kohm/|
### Computer setup
To start, you have to download an IDE and I recommend Atom as it works well with the LoPy4 and is quite an easy IDE to learn.
You can download Atom here:
https://atom.io/
When you have downloaded Atom, you will have to download the package "pymakr".
You can find a complete guide on how to this here:
https://docs.pycom.io/pymakr/installation/atom/
The next step will be to flash the firmware and connect your unit to Pybytes.
To do this you first need to download the Pycom Firmware Update which can be found here:
https://docs.pycom.io/gettingstarted/installation/firmwaretool/
Next, go to https://pybytes.pycom.io/ and create an account, and then add a Wifi network by pressing:
- Configure networks :arrow_right: ADD WIFI
Lastly you are going to need to add your device to Pybytes and then provision your unit.
Guides on how to do this can be found here:
1. https://docs.pycom.io/pybytes/connect/
2. https://docs.pycom.io/pybytes/connect/quick/
When all this is done, you can download the files from the device into your project folder in Atom. Do this by pressing "Download from device" at the left side in pymakr window:

This will give you three different files:
main.py
boot.py
pybytes_config.json
The main.py is empty now but should be filled with the code that is furter down below.
When you are finnished with all the files, which we will come to later, and want to upload the project to the device, just press "Upload project to device" at the left in pymakr window.
### Putting everything together
#### 3D files
I have designed a small box to hold the LoPy4 with the expansionbord and the breadbord.
Just a neat way of storing part of your project.
Pictures of the box with the components can be found at the end of this tutorial.
The files can be found here:
https://www.thingiverse.com/thing:4514952/files
#### Circute

Waterproof temperature sensor:
- Red to 3.3 V
- Black to GND
- Yellow to pin 10
Photoresistor:
- Black to 330 ohm resistor -> resistor to GND
- Red to 3.3 V
- Yellow between black on photoresistor and 330 ohm resistor to pin 14
IC temperature sensor:
- Red to 3.3 V
- Black to GND
- Yellow to pin 15
IMPORTANT: The legs of the sensor are not colored, so see the picture above to see the orientation of the sensor in correlation to the drawn wires!
##### Fun fact: Why the 330 ohm resistor?
We know from **Kirchhoff voltage** law that:
*“In any closed loop network, the total voltage around the loop is equal to the sum of all the voltage drops within the same loop”*
This means that if the only resistans that we have in the closed loop with the photoresistor, is the photoresistor it self. All voltage that the LoPy4 is delivering (3.3 V), is going to end up over the photoresistor, no matter the resistans due to the light shining on the photoresistor. The current is going to change, but as the ADC (Analog to Digital converter) in the LoPy4, which we are using in the setup, is measuring voltage, the ADC value will be constant despite the amount of sunlight.
So, what the 330 ohm resistor is doing in the circute, is making the voltage divide between the photoresistor and the 330 ohm resistor.
Without getting too tecnical, this means that now the voltage across each resistor will change in conjunction to the amount of sunlight that is reaching the photoresistor as more light on the photoresistor means lower resistans in the photoresistor which means lower voltage ends up over it, and less light means higher resistans, which means that more voltage ends up over it.
All we now have to do is measure the voltage across one of the resistors (in our case the 330 ohm resistor), and we will get different values depending on the amount of sunlight!
#### Form circuit to plant pot
Now all you have to do is to put the waterproof temperature sensor in the soil of the pot and point the photoresistor out the window to sense the sunlight hitting the plant/pot.
The IC tempsensor can be put on the breadbord as it only going to read the air temperature around the pot.
### Platform
The platform that I used was Pybytes with Wifi connection.
Pybytes gives you a simple way of transmitting and displaying your data on pybytes.pycom.io.
How to setup pybytes with charts you can check out this guide:
https://docs.pycom.io/pybytes/dashboard/
### The code
You will need the following two files:
#### onewire.py
Lirbrary for the waterproof 1-wire sensor.
This library can be found here:
https://docs.pycom.io/tutorials/all/owd/#app
```python=1
"""
OneWire library for MicroPython
"""
import time
import machine
class OneWire:
CMD_SEARCHROM = const(0xf0)
CMD_READROM = const(0x33)
CMD_MATCHROM = const(0x55)
CMD_SKIPROM = const(0xcc)
def __init__(self, pin):
self.pin = pin
self.pin.init(pin.OPEN_DRAIN, pin.PULL_UP)
def reset(self):
"""
Perform the onewire reset function.
Returns True if a device asserted a presence pulse, False otherwise.
"""
sleep_us = time.sleep_us
disable_irq = machine.disable_irq
enable_irq = machine.enable_irq
pin = self.pin
pin(0)
sleep_us(480)
i = disable_irq()
pin(1)
sleep_us(60)
status = not pin()
enable_irq(i)
sleep_us(420)
return status
def read_bit(self):
sleep_us = time.sleep_us
enable_irq = machine.enable_irq
pin = self.pin
pin(1) # half of the devices don't match CRC without this line
i = machine.disable_irq()
pin(0)
sleep_us(1)
pin(1)
sleep_us(1)
value = pin()
enable_irq(i)
sleep_us(40)
return value
def read_byte(self):
value = 0
for i in range(8):
value |= self.read_bit() << i
return value
def read_bytes(self, count):
buf = bytearray(count)
for i in range(count):
buf[i] = self.read_byte()
return buf
def write_bit(self, value):
sleep_us = time.sleep_us
pin = self.pin
i = machine.disable_irq()
pin(0)
sleep_us(1)
pin(value)
sleep_us(60)
pin(1)
sleep_us(1)
machine.enable_irq(i)
def write_byte(self, value):
for i in range(8):
self.write_bit(value & 1)
value >>= 1
def write_bytes(self, buf):
for b in buf:
self.write_byte(b)
def select_rom(self, rom):
"""
Select a specific device to talk to. Pass in rom as a bytearray (8 bytes).
"""
self.reset()
self.write_byte(CMD_MATCHROM)
self.write_bytes(rom)
def crc8(self, data):
"""
Compute CRC
"""
crc = 0
for i in range(len(data)):
byte = data[i]
for b in range(8):
fb_bit = (crc ^ byte) & 0x01
if fb_bit == 0x01:
crc = crc ^ 0x18
crc = (crc >> 1) & 0x7f
if fb_bit == 0x01:
crc = crc | 0x80
byte = byte >> 1
return crc
def scan(self):
"""
Return a list of ROMs for all attached devices.
Each ROM is returned as a bytes object of 8 bytes.
"""
devices = []
diff = 65
rom = False
for i in range(0xff):
rom, diff = self._search_rom(rom, diff)
if rom:
devices += [rom]
if diff == 0:
break
return devices
def _search_rom(self, l_rom, diff):
if not self.reset():
return None, 0
self.write_byte(CMD_SEARCHROM)
if not l_rom:
l_rom = bytearray(8)
rom = bytearray(8)
next_diff = 0
i = 64
for byte in range(8):
r_b = 0
for bit in range(8):
b = self.read_bit()
if self.read_bit():
if b: # there are no devices or there is an error on the bus
return None, 0
else:
if not b: # collision, two devices with different bit meaning
if diff > i or ((l_rom[byte] & (1 << bit)) and diff != i):
b = 1
next_diff = i
self.write_bit(b)
if b:
r_b |= 1 << bit
i -= 1
rom[byte] = r_b
return rom, next_diff
class DS18X20(object):
def __init__(self, onewire):
self.ow = onewire
self.roms = [rom for rom in self.ow.scan() if rom[0] == 0x10 or rom[0] == 0x28]
self.fp = True
try:
1/1
except TypeError:
self.fp = False # floatingpoint not supported
def isbusy(self):
"""
Checks wether one of the DS18x20 devices on the bus is busy
performing a temperature convertion
"""
return not self.ow.read_bit()
def start_conversion(self, rom=None):
"""
Start the temp conversion on one DS18x20 device.
Pass the 8-byte bytes object with the ROM of the specific device you want to read.
If only one DS18x20 device is attached to the bus you may omit the rom parameter.
"""
if (rom==None) and (len(self.roms)>0):
rom=self.roms[0]
if rom!=None:
rom = rom or self.roms[0]
ow = self.ow
ow.reset()
ow.select_rom(rom)
ow.write_byte(0x44) # Convert Temp
def read_temp_async(self, rom=None):
"""
Read the temperature of one DS18x20 device if the convertion is complete,
otherwise return None.
"""
if self.isbusy():
return None
if (rom==None) and (len(self.roms)>0):
rom=self.roms[0]
if rom==None:
return None
else:
ow = self.ow
ow.reset()
ow.select_rom(rom)
ow.write_byte(0xbe) # Read scratch
data = ow.read_bytes(9)
return self.convert_temp(rom[0], data)
def convert_temp(self, rom0, data):
"""
Convert the raw temperature data into degrees celsius and return as a fixed point with 2 decimal places.
"""
temp_lsb = data[0]
temp_msb = data[1]
if rom0 == 0x10:
if temp_msb != 0:
# convert negative number
temp_read = temp_lsb >> 1 | 0x80 # truncate bit 0 by shifting, fill high bit with 1.
temp_read = -((~temp_read + 1) & 0xff) # now convert from two's complement
else:
temp_read = temp_lsb >> 1 # truncate bit 0 by shifting
count_remain = data[6]
count_per_c = data[7]
if self.fp:
return temp_read - 25 + (count_per_c - count_remain) / count_per_c
else:
return 100 * temp_read - 25 + (count_per_c - count_remain) // count_per_c
elif rom0 == 0x28:
temp = None
if self.fp:
temp = (temp_msb << 8 | temp_lsb) / 16
else:
temp = (temp_msb << 8 | temp_lsb) * 100 // 16
if (temp_msb & 0xf8) == 0xf8: # for negative temperature
temp -= 0x1000
return temp
else:
assert False
```
#### main.py
```python=1
import time
import machine
from machine import Pin
from onewire import DS18X20
from onewire import OneWire
import pycom
#Variables
airTemp = 0.0
soilTemp = 0.0
moist = 0.0
light = 0.0
#Enable DS18B20 TEMPSENSOR on pin 10
ow = OneWire(Pin('P10'))
temp = DS18X20(ow)
#Enable LIGHTSENSOR on pin 14
adc = machine.ADC()
bpin = adc.channel(pin='P14', attn = machine.ADC.ATTN_11DB)
#Enable ROOMTEMP SENSOR on pin 15
adc = machine.ADC()
cpin = adc.channel(pin='P15')
while True:
#===========================================================================
#Take the average of 10 readings
for i in range(10):
temp.start_conversion()
time.sleep(0.5)
airTemp += (cpin.voltage()-500.0)/10.0
soilTemp += (temp.read_temp_async()+0.0)
light += ((bpin.voltage()-110.0)/2400.0)*100 #Gives light in percent
airTemp /= 10.0
soilTemp /= 10.0
light /= 10.0
#===========================================================================
#Send data to pybytes
pybytes.send_signal(1, airTemp)
pybytes.send_signal(2, soilTemp)
pybytes.send_signal(3, light)
#Sleep for 15 minutes (900 seconds) before next reading
time.sleep(900)
```
Just copy and paste them into two files, main.py and onewire.py, and save into your project folder.
### Transmitting the data / connectivity
Data is sent, over Wifi, once every 15 minutes as the temperature and amount of sunlight changes quite slow. So sending data every second isn't going to give better information. But sending data four times an hour seemed to be a good interval.
The data is sent to pybytes platform using MQTT, which is a lightweight messaging protocol that works great when you want to send small package of data.
### Presenting the data
In Pybytes you can create some nice charts displaying your data. The data is then saved for one month. The data is saved every 15 minutes.
This is how it can look at the Pybytes platform when displaying data:

### Finalizing the design
Now you are all set and done if you want to recreate the project!
This project has great potential if you want to keep building and expanding the project.
The next step could be to add an soil moisture sensor and maybe connect a water pump that will water the plants automatically when soil moisture drops below a certain percentage.
Another potential for development could be to send the data via LoRa or Sigfox if you want to monitor your plants in your summer cabin that has no Wifi.
Here are some picture of the final project (note: the antenna isn't needed for wifi connection):



Thanx for checking out my project and I hope you found it intresting enough to try it out your self!