owned this note
owned this note
Published
Linked with GitHub
# Humidity and temperature monitoring
<p style="font-size:10px; color:green;"> Last edit: 2020-08-23 </p>
Credentials: Annica Alienus (aa225mx)
* A system to monotoring humidity and temperature for one plant.
* Develompment time: 20 hours
# Objective
I've always wanted to grow vegetables and such for domestic purposes, but I'm terrible at keeping any plants alive. So if I was to succeed growing anything, I had to change tactics.
So my thought with this project was that if I could monitor the soil, say perhaps online, and over time, I could easier remember to water it. And I could also call the kids and tell **them** to water it.
Using wifi and adafruit.io I can monitore it anywhere, and if needed, tell my offspring to water it!
# Material
* Hardware used in the project:
* LoPy4, antenna, expansion board and micro usb cable.
* Wires, a breadboard (used during development and testing)
* Sensor DHT11
All the components, exept the bread board, were bought at electrokit.com for around 800 SEK.
LoPy4 is rather easy to get going and using microPython there's no need to compile the code which makes the testing quite easy (you can test the different components alone and adhook).
I recommend to use the bread board during development due to the simplicity, and connect the wires from the ports on the expansion board to your breadboard as shown in image below. (Ohwell, maybe not *exacly* as such but you get the point)
The LoPy4 with the kids master piece
<img src="https://i.imgur.com/mAGdfqW.jpg" alt="drawing" style="width:250px;"/>
and the breadboard whith the sensor
<img src="https://i.imgur.com/ilYwWsY.jpg" alt="drawing" style="width:250px;"/>
<p style="font-size:10px;">(yes it's a storm trooper holding the wire to the antenna)<p>
# Computer setup
* The firmware on the expansion board I recieved was already up to date from electrokit.com so there was no need for me to update the firmware.
* Connect the LoPy on the expansion board
* Since I'm using Windows 10 I had to install drivers from Pycom to be able to connect to the LoPy4.
* Update the firmware on the LoPy4 using the tools and guide from [Pycome](https://docs.pycom.io/gettingstarted/installation/firmwaretool/)
* Dev. envirorment:
* IDE: VS code
* Extension PyMakr
* You'll also need to install Node.js
The dashboard I used are located on adafruit.io, but as usual, there's a variety of options to choose between.
# Putting everything together
During development I used the breadboard to get the data from the sensor. I should point out that, even if I didn't,it's a good idea to use resistance between the sensor and wires when working on the breadboard.
Connect a wire to a port on the expansion board (this is the port you'll refer to in your code later, so makke sure you always uses the same port).
Then connect ground (GND) and power (3.3 v) as such:
<img src="https://i.imgur.com/uydpFZy.png" alt="drawing" style="width:200px;"/>
# Platform
To convert the data to information I used adafruit.io, this is a platform that is free for usage up to 3 dashboards, which was more then enough for this project.
To connect to the platform, using WIFI, I used MQTT protocol.
I used a seperate file to store the credentials for the wifi, and imported these into `boot.py`, responsible for the WIFI.
<details style="margin-bottom:20px">
<summary>
<a class="btnfire small stroke" style="font-size:15px; font-weight: bold;"> Reveal code... </a>
</summary>
```python=
from network import WLAN # For operation of WiFi network
import ubinascii # Needed to run any MicroPython code
import pycom # Base library for Pycom devices
import machine
from umqtt import MQTTClient # For use of MQTT protocol to talk to Adafruit IO
from credentials import creds # wifi name and password
wifi_ssid = creds.WIFI_SSID
wifi_pass = creds.WIFI_PASS
# wifi config
# Code source: https://docs.pycom.io/chapter/tutorials/all/wlan.html
wlan = WLAN(mode=WLAN.STA)
wlan.connect(wifi_ssid, auth=(WLAN.WPA2, wifi_pass), timeout=5000)
while not wlan.isconnected(): # Code waits here until WiFi connects
machine.idle()
print("Connected to Wifi")
```
</details>
To connect and send data to adafruit.io using the MQQT-client copy paste following code into a new file, call it `umqptt.py`
<details style="margin-bottom:20px">
<summary>
<a class="btnfire small stroke" style="font-size:15px; font-weight: bold;"> Reveal code...</a>
</summary>
```python=
# https://github.com/micropython/micropython-lib/blob/master/umqtt.simple/umqtt/simple.py
import usocket as socket
import ustruct as struct
from ubinascii import hexlify
class MQTTException(Exception):
pass
class MQTTClient:
def __init__(self, client_id, server, port=0, user=None, password=None, keepalive=0,
ssl=False, ssl_params={}):
if port == 0:
port = 8883 if ssl else 1883
self.client_id = client_id
self.sock = None
self.server = server
self.port = port
self.ssl = ssl
self.ssl_params = ssl_params
self.pid = 0
self.cb = None
self.user = user
self.pswd = password
self.keepalive = keepalive
self.lw_topic = None
self.lw_msg = None
self.lw_qos = 0
self.lw_retain = False
def _send_str(self, s):
self.sock.write(struct.pack("!H", len(s)))
self.sock.write(s)
def _recv_len(self):
n = 0
sh = 0
while 1:
b = self.sock.read(1)[0]
n |= (b & 0x7f) << sh
if not b & 0x80:
return n
sh += 7
def set_callback(self, f):
self.cb = f
def set_last_will(self, topic, msg, retain=False, qos=0):
assert 0 <= qos <= 2
assert topic
self.lw_topic = topic
self.lw_msg = msg
self.lw_qos = qos
self.lw_retain = retain
def connect(self, clean_session=True):
self.sock = socket.socket()
addr = socket.getaddrinfo(self.server, self.port)[0][-1]
self.sock.connect(addr)
if self.ssl:
import ussl
self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
premsg = bytearray(b"\x10\0\0\0\0\0")
msg = bytearray(b"\x04MQTT\x04\x02\0\0")
sz = 10 + 2 + len(self.client_id)
msg[6] = clean_session << 1
if self.user is not None:
sz += 2 + len(self.user) + 2 + len(self.pswd)
msg[6] |= 0xC0
if self.keepalive:
assert self.keepalive < 65536
msg[7] |= self.keepalive >> 8
msg[8] |= self.keepalive & 0x00FF
if self.lw_topic:
sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
msg[6] |= self.lw_retain << 5
i = 1
while sz > 0x7f:
premsg[i] = (sz & 0x7f) | 0x80
sz >>= 7
i += 1
premsg[i] = sz
self.sock.write(premsg, i + 2)
self.sock.write(msg)
#print(hex(len(msg)), hexlify(msg, ":"))
self._send_str(self.client_id)
if self.lw_topic:
self._send_str(self.lw_topic)
self._send_str(self.lw_msg)
if self.user is not None:
self._send_str(self.user)
self._send_str(self.pswd)
resp = self.sock.read(4)
assert resp[0] == 0x20 and resp[1] == 0x02
if resp[3] != 0:
raise MQTTException(resp[3])
return resp[2] & 1
def disconnect(self):
self.sock.write(b"\xe0\0")
self.sock.close()
def ping(self):
self.sock.write(b"\xc0\0")
def publish(self, topic, msg, retain=False, qos=0):
pkt = bytearray(b"\x30\0\0\0")
pkt[0] |= qos << 1 | retain
sz = 2 + len(topic) + len(msg)
if qos > 0:
sz += 2
assert sz < 2097152
i = 1
while sz > 0x7f:
pkt[i] = (sz & 0x7f) | 0x80
sz >>= 7
i += 1
pkt[i] = sz
#print(hex(len(pkt)), hexlify(pkt, ":"))
self.sock.write(pkt, i + 1)
self._send_str(topic)
if qos > 0:
self.pid += 1
pid = self.pid
struct.pack_into("!H", pkt, 0, pid)
self.sock.write(pkt, 2)
self.sock.write(msg)
if qos == 1:
while 1:
op = self.wait_msg()
if op == 0x40:
sz = self.sock.read(1)
assert sz == b"\x02"
rcv_pid = self.sock.read(2)
rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
if pid == rcv_pid:
return
elif qos == 2:
assert 0
def subscribe(self, topic, qos=0):
assert self.cb is not None, "Subscribe callback is not set"
pkt = bytearray(b"\x82\0\0\0")
self.pid += 1
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
#print(hex(len(pkt)), hexlify(pkt, ":"))
self.sock.write(pkt)
self._send_str(topic)
self.sock.write(qos.to_bytes(1, "little"))
while 1:
op = self.wait_msg()
if op == 0x90:
resp = self.sock.read(4)
assert resp[1] == pkt[2] and resp[2] == pkt[3]
if resp[3] == 0x80:
raise MQTTException(resp[3])
return
# Wait for a single incoming MQTT message and process it.
# Subscribed messages are delivered to a callback previously
# set by .set_callback() method. Other (internal) MQTT
# messages processed internally.
def wait_msg(self):
res = self.sock.read(1)
self.sock.setblocking(True)
if res is None:
return None
if res == b"":
raise OSError(-1)
if res == b"\xd0": # PINGRESP
sz = self.sock.read(1)[0]
assert sz == 0
return None
op = res[0]
if op & 0xf0 != 0x30:
return op
sz = self._recv_len()
topic_len = self.sock.read(2)
topic_len = (topic_len[0] << 8) | topic_len[1]
topic = self.sock.read(topic_len)
sz -= topic_len + 2
if op & 6:
pid = self.sock.read(2)
pid = pid[0] << 8 | pid[1]
sz -= 2
msg = self.sock.read(sz)
self.cb(topic, msg)
if op & 6 == 2:
pkt = bytearray(b"\x40\x02\0\0")
struct.pack_into("!H", pkt, 2, pid)
self.sock.write(pkt)
elif op & 6 == 4:
assert 0
# Checks whether a pending message from server is available.
# If not, returns immediately with None. Otherwise, does
# the same processing as wait_msg.
def check_msg(self):
self.sock.setblocking(False)
return self.wait_msg()
```
</details>
Now the code to connect to WIFI and send data is inplace and now we need to get the data from the LoPy4 and into our code.
Create a new file and name it `dth.py`. Paste following code into the file.
<details style="margin-bottom:20px">
<summary>
<a class="btnfire small stroke" style="font-size:15px; font-weight: bold;"> Reveal code...</a>
</summary>
```python=
import time
from machine import enable_irq, disable_irq
class DTHResult:
# 'DHT sensor result returned by DHT.read() method'
ERR_NO_ERROR = 0
ERR_MISSING_DATA = 1
ERR_CRC = 2
error_code = ERR_NO_ERROR
temperature = -1
humidity = -1
def __init__(self, error_code, temperature, humidity):
self.error_code = error_code
self.temperature = temperature
self.humidity = humidity
def is_valid(self):
return self.error_code == DTHResult.ERR_NO_ERROR
class DTH:
# 'DHT sensor (dht11, dht21,dht22) reader class for Pycom'
# __pin = Pin('P3', mode=Pin.OPEN_DRAIN)
# __dhttype = 0 # Is this one really needed?
def __init__(self, pin, sensor=0): # sensor default value dht11
self.__pin = pin
self.__dhttype = sensor
self.__pin(1)
time.sleep(1.0)
def read(self):
# pull down to low
self.__send_and_sleep(0, 0.019)
# collect data into an array
data = self.__collect_input()
# parse lengths of all data pull up periods
pull_up_lengths = self.__parse_data_pull_up_lengths(data)
# if bit count mismatch, return error (4 byte data + 1 byte checksum)
if len(pull_up_lengths) != 40:
return DTHResult(DTHResult.ERR_MISSING_DATA, 0, 0)
# calculate bits from lengths of the pull up periods
bits = self.__calculate_bits(pull_up_lengths)
# we have the bits, calculate bytes
the_bytes = self.__bits_to_bytes(bits)
# calculate checksum and check
checksum = self.__calculate_checksum(the_bytes)
if the_bytes[4] != checksum:
print('Invalid data') # For debug purposes
return DTHResult(DTHResult.ERR_CRC, 0, 0)
# ok, we have valid data, return it
print('Valid data, return it') # For debug purposes
[int_rh, dec_rh, int_t, dec_t, csum] = the_bytes
if self.__dhttype == 0: # dht11
rh = int_rh # dht11 20% ~ 90%
t = int_t # dht11 0..50°C
else: # dht21,dht22
rh = ((int_rh * 256) + dec_rh)/10
t = (((int_t & 0x7F) * 256) + dec_t)/10
if (int_t & 0x80) > 0:
t *= -1
return DTHResult(DTHResult.ERR_NO_ERROR, t, rh)
def __send_and_sleep(self, output, mysleep):
self.__pin(output)
time.sleep(mysleep)
def __collect_input(self):
# collect the data while unchanged found
unchanged_count = 0
# this is used to determine where is the end of the data
max_unchanged_count = 100
last = -1
data = []
m = bytearray(800) # needs long sample size to grab all the bits from the DHT # noqa
irqf = disable_irq()
self.__pin(1)
for i in range(len(m)):
m[i] = self.__pin() # sample input and store value
enable_irq(irqf)
for i in range(len(m)):
current = m[i]
data.append(current)
if last != current:
unchanged_count = 0
last = current
else:
unchanged_count += 1
if unchanged_count > max_unchanged_count:
break
return data
def __parse_data_pull_up_lengths(self, data):
STATE_INIT_PULL_DOWN = 1
STATE_INIT_PULL_UP = 2
STATE_DATA_FIRST_PULL_DOWN = 3
STATE_DATA_PULL_UP = 4
STATE_DATA_PULL_DOWN = 5
state = STATE_INIT_PULL_UP
lengths = [] # will contain the lengths of data pull up periods
current_length = 0 # will contain the length of the previous period
for i in range(len(data)):
current = data[i]
current_length += 1
if state == STATE_INIT_PULL_DOWN:
if current == 0:
# ok, we got the initial pull down
state = STATE_INIT_PULL_UP
continue
else:
continue
if state == STATE_INIT_PULL_UP:
if current == 1:
# ok, we got the initial pull up
state = STATE_DATA_FIRST_PULL_DOWN
continue
else:
continue
if state == STATE_DATA_FIRST_PULL_DOWN:
if current == 0:
# we have the initial pull down,
# the next will be the data pull up
state = STATE_DATA_PULL_UP
continue
else:
continue
if state == STATE_DATA_PULL_UP:
if current == 1:
# data pulled up, the length of this pull up
# will determine whether it is 0 or 1
current_length = 0
state = STATE_DATA_PULL_DOWN
continue
else:
continue
if state == STATE_DATA_PULL_DOWN:
if current == 0:
# pulled down, we store the length of
# the previous pull up period
lengths.append(current_length)
state = STATE_DATA_PULL_UP
continue
else:
continue
return lengths
def __calculate_bits(self, pull_up_lengths):
# find shortest and longest period
shortest_pull_up = 1000
longest_pull_up = 0
for i in range(0, len(pull_up_lengths)):
length = pull_up_lengths[i]
if length < shortest_pull_up:
shortest_pull_up = length
if length > longest_pull_up:
longest_pull_up = length
# use the halfway to determine whether the period it is long or short
halfway = shortest_pull_up + (longest_pull_up - shortest_pull_up) / 2
bits = []
for i in range(0, len(pull_up_lengths)):
bit = False
if pull_up_lengths[i] > halfway:
bit = True
bits.append(bit)
return bits
def __bits_to_bytes(self, bits):
the_bytes = []
byte = 0
for i in range(0, len(bits)):
byte = byte << 1
if (bits[i]):
byte = byte | 1
else:
byte = byte | 0
if ((i + 1) % 8 == 0):
the_bytes.append(byte)
byte = 0
return the_bytes
def __calculate_checksum(self, the_bytes):
return the_bytes[0] + the_bytes[1] + the_bytes[2] + the_bytes[3] & 255
```
</details>
and finally the the `main.py`
<details style="margin-bottom:20px">
<summary>
<a class="btnfire small stroke" style="font-size:15px; font-weight: bold;"> Reveal code...</a>
</summary>
```python=
import time
from machine import Pin
from dth import DTH
import _thread
from umqtt import MQTTClient # For use of MQTT protocol to talk to Adafruit IO
from credentials import creds
# Type 0 = dht11
# Type 1 = dht22
th = DTH(Pin('P23', mode=Pin.OPEN_DRAIN), 0) # Use the pin number to open port
time.sleep(2)
# # Use the MQTT protocol to connect to Adafruit IO
client = MQTTClient(creds.AIO_CLIENT_ID, creds.AIO_SERVER, creds.AIO_PORT,
creds.ADAFRUIT_IO_USERNAME, creds.ADAFRUIT_IO_KEY)
def send_env_data():
while True:
result = th.read()
while not result.is_valid():
# if the recieved data isn't valid, the code waits 0.5 seconds until it tries again.
time.sleep(.5)
result = th.read()
client.publish(topic=creds.aio_temperature_feed,
msg=str(result.temperature))
client.publish(topic=creds.aio_humidity_feed, msg=str(result.humidity))
time.sleep(30)
_thread.start_new_thread(send_env_data, ())
```
</details>
# Explain your code!
The data is transfer in intervals of 30 seconds using wifi and MQTT as transfer protocol.
Adafruit.io are using "feeds" which are basically a ```string feedName``` in which micropython tells adafruit where to output that specific data.
This piece of code is responsible to publish the data (you don't need to copy this one, it's already in the code above on row 558)
```python=
client.publish(topic=creds.aio_temperature_feed,
msg=str(result.temperature))
````
## Presenting the data
I used two seperated graphs to present the data.

To create a graph with adafruit.io you need at least a free account.
Create a dashboard and click on it. Up to the left there's a plus sign

To create a feed click on the gear beside the plus sign used to create the graph and choose you component. Then something like this should pop up (but empty of course). Just give your feed a name and press create.

Later you'll call this feed like:
```python=
client.publish(topic="the_name_on_the_feed_you_just_created",
msg=str(result.temperature))
```
To be able to connect to adafruit you'll need usename and generated keys. Press the key up to the left and this should show up

copy the "scripting" info and paste it in your code as such
```python=
ADAFRUIT_IO_USERNAME = "your_adafruit_username"
ADAFRUIT_IO_KEY = "generated_key_you_just_copied"
AIO_SERVER = "io.adafruit.com"
AIO_PORT = 1883
AIO_CLIENT_ID = ubinascii.hexlify(machine.unique_id())
```
# Finalizing the design
The project was fun working with, although I wished I had more time aIt is a fun project and I had a lot of fun working with it.
So even if I had a good time working with it I wished I had more time and possibilities to scale it from what it is today.
Functions such as giving a reminder to water when the soil gets under a sertain percentage, connect more plants and set individual reminders, and maybe even make it send a message to the offsprings phone so that one of them can do it for me.