--- title: 'Tutorial IoT 2020' disqus: hackmd --- <sub>Note to peer reviewers, it's a long text because I included all the code in the text instead of linking to github. 26 of the 28 checkboxes from the template are included in the tutorial, see sections marked with '&check;'</sub> ## Tutorial on how to build a plant monitoring device &check; #### by Esther Castro Severiche (kc222fy) &check;</sup> ### Overview &check; The tutorial describes how to assemble and program a connected plant monitoring device measuring temperature, humidity and soil moisture. By using a Pycom LoPy4 module and a couple of sensors, data is sent over WiFi to the Ubidots cloud and presented on a dashboard. You can monitor the environment of your favorite plant online, and get an email or text message when it's time to water it. ### How much time is required &check; The project can be completed in approximately 3 - 7 hours depending on your previous knowledge of hardware and Python programming. ### Purpose &check; The purpose of the project (&check;) is to learn about some key aspects of IoT. How to collect data using sensors and how they connect to a module like the LoPy4. What are the possibilities and challenges? How is data sent over wireless to a cloud for analyzing and viewing? How can data be presented in a simple and useful format with dashboards in the cloud? ### Why this project? &check; A plant monitoring device project is fun because it solves a real-world problem. The tutorial device is of course a rough prototype, but it could be refined into a product which has an actual use. When on vacation I always worry about how my loved tomato plant is doing at home. With the online plant monitor dashboard and the text message notifications, I know when it's time to ask someone to water the plant. ### Insights &check; The insights gained from the project is an understanding of what it takes to build and program an IoT product. Which are the challenges when it comes to for example energy consumption, costs, wireless protocol limitations and sensor data quality? ### List of material &check; The components needed for this project are listed below with links to online stores (&check;). The cost is about €75 (&check;) plus the cost of a USB power bank if needed. * [Pycom LoPy4 (€35)](https://pycom.io/product/lopy4/?gclid=EAIaIQobChMIpdHf-de56gIVFtKyCh3Nkwk6EAAYAiAAEgLQBfD_BwE), a MicroPython programmable bearer board supporting WiFi, Bluetooth, LoRa and Sigfox. A capable and easy-to-use IoT platform ([data sheet](https://docs.pycom.io/datasheets/development/lopy4/)). ![](https://i.imgur.com/sqgABWk.png =250x) * [Pycom expansion board 3.x (€16)](https://pycom.io/product/expansion-board-3-0/), USB and LiPo battery powered board mounting several of the Pycom units including the LoPy4 ([data sheet](https://docs.pycom.io/datasheets/boards/expansion3/)). ![](https://i.imgur.com/qdJpSfs.png =250x) * [DHT11 (€5)](https://www.electrokit.com/en/product/digital-temperature-and-humidity-sensor-dht11/) temperature and relative humidity sensor ([data sheet](https://www.electrokit.com/uploads/productfile/41015/DHT11.pdf)). ![](https://i.imgur.com/wPZzVFn.jpg =250x) * [Soil hygrometer module (€3)](https://www.electrokit.com/en/product/soil-hygrometer-module/) for measuring moisture level in soil ([data sheet](https://www.electrokit.com/uploads/productfile/41015/41015738_-_Soil_Moisture_Sensor.pdf)). ![](https://i.imgur.com/CPa1nC7.jpg =250x) * [Breadboard (€6)](https://www.electrokit.com/produkt/kopplingsdack-400-anslutningar/), for connecting components without soldering. ![](https://i.imgur.com/BdHR6kw.jpg =250x) * [Jumper wires (€5-10)](https://www.electrokit.com/produkt/labbsladd-20-pin-15cm-hona-hane/) for connecting components together. ### Expansion board firmware I didn't have to update the firmware of the expansion board (v3.1). If you run into problems while upgrading the firmware of the LoPy4 as described next, a guide to flashing the expansion board is available [here](https://docs.pycom.io/pytrackpysense/installation/firmware/). ### LoPy4 firmware To flash the LoPy4 with the latest firmware, [download](https://docs.pycom.io/gettingstarted/installation/firmwaretool/) and install the Pycom Upgrade tool. - Mount the LoPy4 on the board with the reset button towards the USB connection ([guide](https://docs.pycom.io/gettingstarted/connection/lopy4/)). - Connect the board to the computer via USB. - Close any programs using the device COM port. - Run the upgrade tool, follow the instructions. - I selected the pybytes-legacy firmware even if I don't use the Pybytes platform. I tried with the plain legacy firmware first, but I couldn't get the WiFi to work. It might have been fixed since then (it's a known problem). - Disconnect the device and reconnect it. Done! ### Computer setup The LoPy4 is programmed using MicroPython. For writing the code, I recommend to use one of two popular editors (IDE:s), Visual Studio (VS) Code or Atom. Both have similar functions and good support for connecting to the LoPy4. The IDE of choice for this tutorial is VS Code. #### Installing and setting up VS Code &check; VS Code runs on Windows, MacOS and Linux. If you are using a PC, Windows 10 is recommended for a seamless experience without the need to install additional drivers. Download VS Code [here](https://code.visualstudio.com/download) and install it. #### The Pymakr extension &check; The next step is to install the Pymakr extension by clicking on the extensions icon in the side bar of VS Code and search for Pymakr. Pymakr requires [Node.js](https://nodejs.org/en/download/) and VS Code will show a notification about this. [Download](https://nodejs.org/en/download/) and install Node.js if necessary. For a detailed Pymakr installation guide, see the [Pycom website](https://docs.pycom.io/pymakr/installation/vscode/). #### The Pymakr REPL With Pymakr installed it's easy to communicate with, and control the LoPy4. Just connect the expansion board to your computer through USB and Pymakr will automagically detect and establish a connection to the device. The [REPL](https://docs.pycom.io/pymakr/toolsfeatures/) is an interactive code tool with which you can type and run MicroPython code directly on the device. In the 'Terminal' tab of the Pymakr Console, just type some code and it will execute on the device. #### Uploading code &check; While it's convenient to be able to run code in the REPL, for real applications you must upload code to the device. In VS Code, open a folder containing your MicroPython code. You will see the folder structure and the .py code files in the VS Code explorer. To upload the folder contents to the device, click the 'Upload' button in the VS Code task bar at the bottom of the editor window. The contents of the currently opened project folder is uploaded to the device and executed. ### Putting everything together For this project I'm using two sensors, the combined temperature and humidity sensor DHT11 and a soil moisture sensor. As this is a development setup, a [breadboard](https://en.wikipedia.org/wiki/Breadboard) and jumper wires are used to connect the sensors to the expansion board. One of the red bus strips labeled '+' on the breadboard is connected to the 3V3 pin. A blue bus strip labeled '-' is connected to the GND pin. The DHT11 has three pins; power, ground and sensor signal. The sensor is attached on the breadboard and the power and ground pins are connected with wires to the corresponding bus strips. The signal pin is connected to the P23 pin on the expansion board. The soil moisture sensor has four pins; power, ground, analog and digital signal output. Only the analog output pin is used for this project. The sensor ground pin is connected to ground on the breadboard and the analog output pin is connected to pin P15 on the expansion board. #### Circuit diagram &check; ![](https://i.imgur.com/oXOqYWz.png) Instead of connecting the power pin to 3V3, we want to power it up when taking a measurement and shut the power down afterwards. The sensor measures humidity by passing current through the soil. Having the current on continuously will corrode the surface layer of the probe. The sensor power pin is connected to the digital pin P9 on the expansion board. Before doing a mesaurent, a high signal is sent on the pin to power on the sensor followed by a low signal after the measurement has been made. For this development setup that uses WiFi, a power bank connected to the USB port on the expansion board is used as power supply. ### Cloud vs local platform A cloud based platform has the advantage over a local installation as it requires no hardware and software setup. It's easy to get started sending data and create online accessible dashboards. Cloud platforms are also very reliable and perform well. ### The Ubidots platform &check; Two platforms were evaluated, [Pybytes](https://docs.pycom.io/pybytes/) and [Ubidots](https://ubidots.com/). Pybytes is integrated with the LoPy4 through firmware and is easy to use. You are up and running and sending data to the Pybytes cloud in no time. It's a good choice to start with and it's free for this kind of development projects. In the end, Ubidots was selected as the project platform because of better dashboard and event trigger support (&check;). Ubidots is easy to use, data can be sent using a REST API. Ubidots also has good integration with [The Things Network](https://www.thethingsnetwork.org/). Creating nice looking dashboards take only minutes and you can define event triggers to send email and text messages (additional cost). For educational and DIY projects, they have a free STEM account so it doesn't cost anything to get started with a development project. There are plenty of cloud services available, and for a future real product project, it's worth taking the time to make a broader evaluation of the market to find a reasonably priced platform with a suitable level of functionality. ### Create a Ubidots STEM account You need a Ubidots account to run the code in the tutorial. Signup for a free [STEM](https://ubidots.com/stem/) account which is suitable for development projects. You can register up to three devices for free on your account. To send data from your device to the Ubidots cloud, you need a token. You find the token in your Ubidots account under the [API Credentials section](https://help.ubidots.com/en/articles/590078-find-your-token-from-your-ubidots-account). ### Sending data to Ubidots There are several ways to send sensor data to Ubidots, either through a direct connection from the device or through different gateways. The plant monitor device communicates directly with Ubidots using a REST API. See [this step-by-step guide](https://help.ubidots.com/en/articles/961994-connect-any-pycom-board-to-ubidots-using-wi-fi-over-http) for connecting a Pycom device to Ubidots over HTTP. ### The code - libraries The folder structure below is how the tutorial code is structured. It's common for MicroPython programs to also have a boot.py file but it's not needed in this case. ``` > tutorial - main.py - config.json > lib - config.py - urequests.py - dht.py ``` Create the tutorial folder with a lib subfolder. The urequests library is used to send data to Ubidots. [Download](https://github.com/jotathebest/micropython-lib/blob/master/urequests/urequests.py) and add the file to the lib folder. The dht library is used for communicating with the DHT11 sensor, [download](https://github.com/iot-lnu/applied-iot-20/blob/master/sensor-examples/DHT11-22/lib/dht.py) and add the file. To read values from the config.json file, add the following code in the config.py file. ```python= import json class Config: def __init__(self, filename = "config.json"): self.filename = filename self.data = self.__read_file() def __read_file(self): with open(self.filename) as data: return json.load(data) def get(self, key): return self.data[key] ``` It's good practice to not have passwords and tokens in the code in case you want to share it. Create a config.json file to store secret information. ```jsonld= { "ssid": "YourWiFiNetwork", "password": "YourWifiPassword", "ubidots_token": "XXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" } ``` Now to the application code which shall be put in the main.py file. I've added a lot of comments explaining every part of the code. ### The code - application It's recommended that you do some calibration measurements with the soil moisture sensor to find out the min (soaked soil) and max (dry soil) values returned by the sensor. These values are set as constants at the start of the code and are used to convert the sensor value to a percentage value where 0% is dry soil and 100% is soaked soil. At the end of the code it's described how to run the program for doing calibration measurements. The measurements should be run from the REPL so you can see the values printed in the console. When you are done with calibration and have updated the MIN/MAX_MOISTURE_SENSOR_VALUE constants, comment out the call to moisture_calibration_measurements() and uncomment the call to main(). Upload the code to the device and attach the moisture sensor probe to the plant soil. The sensor values will be sent to Ubidots every 15th minute. You don't need to register your device at Ubidots. It's done automatically when the sensor data is received by the service and the measurement variables are created in Ubidots. This code block is long and have a lot of explaining comments. I chose to include the complete code here for readability instead of linking to an external repository. ```python= from network import WLAN import urequests as requests # Ubidots connectivity library from dht import DHT # DHT sensor library from machine import Pin, ADC, idle from time import sleep import _thread import pycom # Comment out if you are not using the pybytes firmware from config import Config # Simple configuration library # Disable pybytes, comment out if you are not using the pybytes firmware pycom.pybytes_on_boot(False) # Get the configuration with the secret credentials and tokens config = Config("config.json") # Global constants # Most of these can be defined in the config file instead, but I define them here # to make the tutorial code easier to read. DELAY_BETWEEN_MEASUREMENTS = 900 # Delay between measurements in seconds MIN_MOISTURE_SENSOR_VALUE = 1000 # From calibration measurements in soaked soil MAX_MOISTURE_SENSOR_VALUE = 4095 # From calibration measurements in dry soil MOISTURE_SENSOR_PIN_IN = 'P15' # Pin used for reading data from the moisture sensor MOISTURE_SENSOR_PIN_OUT = 'P9' # Pin used to power the moisture sensor TH_SENSOR_PIN_IN = 'P23' # Pin for reading temp/humidity data (dht11 sensor) UBIDOTS_DEVICE_NAME = "moki" # Name of the device defined in your Ubidots account UBIDOTS_TOKEN = config.get("ubidots_token") # Ubidots token, see API Credentials UBIDOTS_API_URL = "https://things.ubidots.com/api/v1.6/devices/" # STEM accounts #UBIDOTS_API_URL = "https://industrial.api.ubidots.com/api/v1.6/devices/" # Industrial accounts UBIDOTS_REQUEST_HEADERS = { "X-Auth-Token": UBIDOTS_TOKEN, "Content-Type": "application/json" } WLAN_AUTH_MODE = WLAN.WPA2 # Change if not using WPA2 (https://docs.pycom.io/firmwareapi/pycom/network/wlan/) # Global variables quit_measurement_loop = False # Set to True in the REPL to stop measurements wlan = WLAN(mode = WLAN.STA) # Access to the WiFi wlan.antenna(WLAN.INT_ANT) # Setup the moisture sensor adc = ADC() # Analog to Digital conversion class moisture_sensor_pin_in = adc.channel(pin = MOISTURE_SENSOR_PIN_IN, attn = ADC.ATTN_11DB) moisture_sensor_pin_out = Pin(MOISTURE_SENSOR_PIN_OUT, mode = Pin.OUT, pull = Pin.PULL_DOWN) # Setup the DHT11 temperature and humidity sensor th_sensor_pin_in = DHT(Pin(TH_SENSOR_PIN_IN, mode = Pin.OPEN_DRAIN), 0) # Type 0 = dht11, Type 1 = dht22 sleep(2) # Functions # Connects to the WiFi network def connect_wifi(): wlan.connect(config.get("ssid"), auth = (WLAN_AUTH_MODE, config.get("password")), timeout = 5000) while not wlan.isconnected(): idle() print("Connected to WiFi {0}\n".format(config.get("ssid"))) # Converts a moisture sensor value to a percentage value (0% dry, 100% soaking wet) def convert_moisture_sensor_value(value): converted_value = max(value, MIN_MOISTURE_SENSOR_VALUE) converted_value = min(converted_value, MAX_MOISTURE_SENSOR_VALUE) return round((1 - ((converted_value - MIN_MOISTURE_SENSOR_VALUE) / (MAX_MOISTURE_SENSOR_VALUE - MIN_MOISTURE_SENSOR_VALUE))) * 100) # Reads a value from the soil moisture sensor. Before making the reading, # the sensor is powered on by sending a high signal on the out pin. # This is to keep the sensor from corroding, which happens faster when the # power is on. # # The value returned by the sensor is an analog value, where # a high value represents dry soil and a low value represents wet soil. # The range of these extreme values should be found through calibration # measurements. # # For easier understanding and presentation of the moisture level, the # sensor value is converted to a percentage value where 0% is dry and # 100% is soaking wet. def measure_moisture_value(): moisture_sensor_pin_out.value(1) # Turn on the power sleep(1) # Without the sleep, inconsistent values are returned sensor_value = moisture_sensor_pin_in.value() moisture_sensor_pin_out.value(0) # Turn off the power return int(sensor_value) def measure_moisture(): return convert_moisture_sensor_value(measure_moisture_value()) # Reads a value from the combined temperature and humidity sensor (DHT11) def measure_temperature_and_humidity(): sensor_values = th_sensor_pin_in.read() while not sensor_values.is_valid(): sleep(.5) sensor_values = th_sensor_pin_in.read() return sensor_values # Builds the json to send in the request to Ubidots def build_json(temperature, humidity, moisture): data = { "temperature": {"value": temperature }, "humidity": {"value": humidity }, "moisture": {"value": moisture } } return data # Sends the request to Ubidots, see the REST API reference at # https://ubidots.com/docs/api/ def send_sensor_data(temperature, humidity, moisture): try: url = UBIDOTS_API_URL + UBIDOTS_DEVICE_NAME data = build_json(temperature, humidity, moisture) print("Sending data to Ubidots:", data) response = requests.post(url = url, headers = UBIDOTS_REQUEST_HEADERS, json = data) return response.json() except Exception as error: print("Failed to send data to Ubidots: {0}".format(error)) pass # Makes repeated measurements and sends data to Ubidots def measure_and_send_data(): while quit_measurement_loop == False: # Measure moisture moisture_sensor_value = measure_moisture() print("Moisture: {0}%".format(moisture_sensor_value)) # Measure temperature and humidity th_sensor_values = measure_temperature_and_humidity() print("Temperature: {0}°".format(th_sensor_values.temperature)) print("Humidity: {0}%".format(th_sensor_values.humidity)) # Send the sensor data to Ubidots send_sensor_data(th_sensor_values.temperature, th_sensor_values.humidity, moisture_sensor_value) # Sleep until the next measurement if quit_measurement_loop == False: sleep(DELAY_BETWEEN_MEASUREMENTS) # Call this function to make calibration measurements for the moisture sensor. # Send in the number of measurements to make and the time to sleep between each. def moisture_calibration_measurements(number_of_measurements, sleep_time): for i in range(number_of_measurements): print("Moisture, analog reading:", measure_moisture_value()) if i < number_of_measurements - 1: sleep(sleep_time) # Runs the measurements in a separate thread def main(): connect_wifi() _thread.start_new_thread(measure_and_send_data, ()) # Main code # Before attaching the device to the plant to be monitored, # you will likely need to do some calibration measurements to find # the extreme values of the moisture sensor. # # Make measurements in totally dry soil (or air) and totally # soaked soil (or water). # Use these values to set the MIN_MOISTURE_SENSOR_VALUE and # MAX_MOISTURE_SENSOR_VALUE constants at the top of the code. # # For my moisture sensor, I got the values min = 1000 and max = 4095 # using the calibration function. When you are done with # calibration, comment out the moisture_calibration_measurements() call and # uncomment the call to main(). # # You can use the lowest/highest values or averages, approximating min/max # is good enough. Any measurements values outside the range # will be set to the min/max values respectively during main execution. #moisture_calibration_measurements(3, 10) # When you have completed the moisture sensor calibration, uncomment the # call to main() and upload the code to the device again. # Power the board usingUSB, attach the sensor to the plant soil and monitor # the sensor data at the dashboard in your Ubidots account. #main() ``` ### Transmitting the data / connectivity A goal of the project was to use the low-power wide-area [LoRa](https://en.wikipedia.org/wiki/LoRa) network to send data from the device to Ubidots through the integration available via [The Things Network](https://www.thethingsnetwork.org/) (TTN). Unfortunately this was not possible as I don't have a TTN gateway close enough. The alternative of using a commercial solution like Sigfox was not an option for this project. I ended up using WiFi as the wireless protocol (&check;) and HTTP as the transport protocol (&check;) to send data to Ubidots using their [REST API](https://ubidots.com/docs/hw/#introduction). Ubidots supports other protocols like MQTT and TCP/UDP, but HTTP/REST is simple and adequate for this application. #### Power consumption and range &check; A consequence of not using LoRa is that I didn't put effort into optimizing the power consumption. There is a lot to be done on that part, like minimizing the use of WiFi and using deepsleep or similar to lower the power consumption of the device. With WiFi, the range is very short compared to LoRa and that is also something that has to be considered when selecting the wireless protocol to use for a real product. A factor with LoRa is that one has to choose between higher bandwith or longer range. With WiFi, this is not a problem. ### Package format WiFi is used for wireless and the data points are only sent every 15th minute (&check;), so it's not necessary use a compact format to save bandwidth as would have been the case with LoRa/Sigfox. The data package sent every 15th minute (configurable) consists of three measurement values and is represented with the following json structure. ```jsonld= { "temperature": {"value": 23 }, "humidity": { "value": 67 }, "moisture": { "value": 74 } } ``` The values are collected as variables in Ubidots under the device name defined by setting the UBIDOTS_DEVICE_NAME constant in the code. The call to Ubidots is a standard REST call to the API endpoint. The device name is specified in the URL so the variables are attached to the correct device. ### Presenting the data &check; It's super easy to make good looking dashboards in Ubidots. It's not really necessary to read the [documentation](https://help.ubidots.com/en/articles/2400308-create-dashboards-and-widgets), just create a dashboard and start adding different kinds of widgets connected to the device variables. The widgets are customizable so you can get the look and feel you like by tweaking colors and other parameters. ![](https://i.imgur.com/iMUfPQM.jpg) The plant monitor dashboard has five widgets. The gauge widget displays the current soil moisture as a percentage value (variable: moisture). The colors red/green/blue indicates dry/good/wet moisture status. The same color scheme is used for the plant moisture alert (indicator widget). The thermometer widget is used to display the current temperature at the plant location (variable: temperature). Relative humidity is displayed using a metric widget (variable: humidity). Finally the soil moisture history is displayed as a line chart. #### How often is the data saved in the database? &check; Data sent to Ubidots is persisted in a database when it arrives to the service. There is a limit of how many dots (variable data point) you can send and store. For STEM accounts it's 30 dots/minute and 4000 dots/day. This is more than enough for the plant monitor application that sends 4x3=12 dots/hour with a total of 288 dots/day. #### The choice of database? &check; As Ubidots takes care of persistence there is no option of selecting a database. If using a locally hosted platform, I would use an open source database like POSTGRES. #### How long is the data stored? The data retention for a free Ubidots STEM account is one month. For an industrial account the data is stored one year. #### Event triggers &check; One of the coolest things with Ubidots is the support for [event triggers](https://help.ubidots.com/en/articles/1445537-events-creating-conditional-events-and-alerts) to send for example email or SMS notifications when a sensor variable passes a threshold. For the plant monitor it can be used to send an SMS when the plant is getting too dry and it needs water. It's only the first 10 SMS messages that are free, so if using this notification type in real-world usage, there is a cost for it. ### Finalizing the design I'm happy with the overall result of the project. Even if the time didn't allow to go deep in all different areas of IoT, it has been rewarding and I learned a lot. The result is a working plant monitor device with a dashboard accessible from wherever I happen to be at the moment. Photo of the development build &check; ![](https://i.imgur.com/X00igm3.jpg) One insight gained is that it's a challenge to make a consumer product using wireless networks like LoRa where gateway coverage is dependent on a community. For real consumer products a commercial solution like Sigfox must be used. Or 3/4G/WiFi but then cost and power consumption become factors to consider. Dashboard detail ![](https://i.imgur.com/FuggAFO.jpg) #### Possible improvements There are several improvements that can be made to the project to explore even more aspects of IoT. Using LoRa and TTN possibly with MQTT would be interesting. Also to optimize the code and hardware usage to minimize the power consumption. It would also be fun to 3D-print a case for the device and mount the components using soldering instead of wiring and a breadboard. That would also mean the plant sensor could be attached to an outdoor plant, now I'm limited to using it on indoor plants or when it's not raining outside.