---
title: 'Automatic Watering Plant System'
disqus: hackmd
---
###### tags: `ESP32C3` `Node-Red` `Mosquitto` `MQTT` `MicroPython` `Tutorial` `ESP32` `Dashboard` `Thonny IDE` `Wi-Fi` `Flashing`
Automatic Watering Plant System
===
> Author: Nu11ifier
> Summer 2022
This project tutorial explains how to set up an automatic watering plant system using a microcontroller ESP32-C3 and Wi-Fi as the connectivity. The microcontroller will be publishing sensor data to a specific topic to the MQTT-broker and Node-RED will be subscribing to a specific topic from the MQTT-broker and then displaying the sensor information in a dashboard in the form of graphs/charts.
The time to implement the whole project takes approximately 1-3 hours, not considering any problems in the process using this tutorial.
Initially the project took around 20-30 hours to implement.
# Table of Contents
[TOC]
# Objective
The automatic watering plant system project build was choosen so that watering plants becomes automatic and also to keep better track of plants health and enviroment.
The purpose of this project is to make your plants love you more without doing any hard work.
This tutorial will aid individuals in setting up an automatic watering plant system and learn how the code, MQTT-broker and NodeRed works to be able to publish and subscribe to sensor data and then display the data visually through graphs/charts.
# Material
---
* **ESP32-C3-DevKitM-1U**: Main microcontroller for collecting and publishing sensor data

* **HC-SR04 Ultrasonic sensor**: Measure water levels based on the sound reflection from the sensor to the water and back

* **DHT11 Temperature/Humidity sensor**: Measure the temperature and humidity

* **Soil Moisture Sensor**: Measure how moist the soil is

* **3-5V Water Pump**: Pump inside the tank

* **5V Relay Module**: Trigger the water pump when relay on

* **Clear Vinyl Tube**: For the 5V water pump

* **Jumper Wires**: To connect the circuit

* **Breadboard**: Makes wiring circuit easier

| Part | Price(€/kr) | Purchased Link |
| -------- | -------- | -------- |
| **ESP32-C3-DevKitM-1U** | 9.9€/106kr | [ESP32-C3-DevKitM-1U](https://www.digikey.com/en/products/detail/espressif-systems/ESP32-C3-DEVKITM-1U/15198974 ) |
| **HC-SR04** | 5.3€/57kr | [HC-SR04](https://www.digikey.se/sv/products/detail/sparkfun-electronics/SEN-15569/10384560?s=N4IgTCBcDaIBYGMC0BnATgBgCwgLoF8g ) |
| **DHT11** | 2.6€/28kr | [DHT11](https://sizable.se/P.WGXR4/DHT11-Temperatur-och-luftfuktighetssensor) |
| **Soil Moisture Sensor** | 3.2€/34kr | [Soil Moisture Sensor](https://sizable.se/P.8AS66/Capacitive-Soil-Moisture-Sensor ) |
| **3-5V Water Pump (Package (1))** | 10.9€/117kr | [3-5V Water Pump (Package*)](https://www.amazon.com/Submersible-Flexible-Aquariums-Fountain-Hydroponics/dp/B09SF1C9DQ/ref=sr_1_17?keywords=5v+water+pump&qid=1656611592&sr=8-17 ) |
| **Clear Vinyl Tube (Package (1))** | 10.9€/117kr | [Clear Vinyl Tube (Package*)](https://www.amazon.com/Submersible-Flexible-Aquariums-Fountain-Hydroponics/dp/B09SF1C9DQ/ref=sr_1_17?keywords=5v+water+pump&qid=1656611592&sr=8-17 ) |
| **5V Relay Module** | 7.7€/82kr | [5V Relay Module](https://www.amazon.com/WINGONEER-KY-019-Channel-Module-arduino/dp/B06XHJ2PBJ/ref=pd_di_sccai_cn_sccl_2_11/134-0898683-4936248?pd_rd_w=ZtWq1&content-id=amzn1.sym.c32bd24a-4ef0-4e42-bb31-8027e5f6a185&pf_rd_p=c32bd24a-4ef0-4e42-bb31-8027e5f6a185&pf_rd_r=T339Z4VXTQVW2VAGR130&pd_rd_wg=Esjx7&pd_rd_r=ae3012f2-8666-4b36-97b0-dae39202cf8d&pd_rd_i=B06XHJ2PBJ&psc=1 ) |
| **Jumper Wires Male-Male (20CM)** | 2.2€/24kr | [Jumper Wires Male-Male (20CM)](https://sizable.se/P.K2HUW/Dupont-kopplingskablar-Hane-Hane-40-pin ) |
| **Jumper Wires Male-Female (20CM)**| 2.2€/24kr | [Jumper Wires Male-Female (20CM)](https://sizable.se/P.3V1BE/Dupont-kopplingskablar-Hane-Hona-40-pin ) |
| **Jumper Wires Female-Female (20CM)**| 2.2€/24kr | [Jumper Wires Female-Female (20CM)](https://sizable.se/P.4LTWU/Dupont-kopplingskablar-Hona-Hona-40-pin ) |
| **Breadboard** | 3.2€/34kr | [Breadboard](https://sizable.se/P.SFGYT/Kopplingsdack-med-400-punkter)|
## Computer and Device Setup
> Operative System: Windows 10
### ESP32-C3 WiFi Security
ESP32-C3 supports different Wi-Fi security protocols called Protected Access (WPA). The newest Wi-Fi security protocol that the ESP32-C3 supports is WPA3 which prevents e.g dictionary attacks, which WPA2 do not.
### Prerequisites (Download and Install Required Software)
The setup will only be ran locally. To allow remote setup, please refer to the documentation of each service below.
Download and install the prerequisities before continuing with other steps:
:::info
- **MicroPython Firmware (ESP32-C3)** - [Reference Page](http://www.micropython.org/download/esp32c3/)
- **NodeJS (Latest Current Version)** - [Reference Page](https://nodejs.org/en/)
- **Python (Latest)** - [Reference Page]((https://www.python.org/downloads/))
:::
### Flashing the ESP32-C3 with MicroPython
ESP32-C3 does not come with MicroPython out of the box, therefore flashing the ESP32-C3 with MicroPython firmware is required to be able to program it with python.
The steps for flashing MicroPython are shown below:
Download and install the latest MicroPython firmware for the ESP32-C3 found on the official page (*Check Prerequisites - MicroPython Firmware*).
Go to [ESP WEB FLASHER](https://nabucasa.github.io/esp-web-flasher/).
Connect a USB cable between the ESP32-C3 and the PC, a pop up will appear with the port which the device is connected to and connect.

Set the baud rate to 921600 and and press connect. 
After connecting the ESP32-C3 press 'Erase' and wait until the operation is complete in the terminal.
Thereafter on the first row press 'Choose a file...' and choose the previously downloaded ESP32-C3 MicroPython firmware. Set the offset to 0 and press 'Program'.

When the terminal displays that the flashing is complete, unplug the ESP32-C3 usb cable and close the flashing website and then replug the ESP32-C3 usb cable.
Flashing MicroPython on the ESP32-C3 microcontroller is now done!
Here is a MicroPython quick reference for ESP32 [MicroPython Quick Reference](https://docs.micropython.org/en/latest/esp32/quickref.html#networking).
### Setting up Thonny IDE
:::info
**Thonny IDE (Python)** - [Reference Page](https://thonny.org/)
:::
After Thonny IDE is downloaded, make sure the USB cable is connected and press 'Run' in Thonny IDE then 'Select interpreter...' then navigate to 'Interpreter' tab and choose MicroPython (ESP32) as the interpreter and the connected ESP32-C3 port then press 'OK'. Thonny IDE is now setup.

Code can then be written and uploaded to the ESP32-C3 by pressing 'File' then 'Save as...' which will display two options of where you want to save the python file, the same window prompt will be displayed if you want to open a file with 'Open...'.

### Setting up Node-RED
:::info
**Node-RED** - [Reference Page](https://nodered.org/docs/getting-started/local)
:::
Open a new CMD/terminal in Windows and type the command:
> npm install -g --unsafe-perm node-red
After the download has finished run the command below to start Node-RED:
> node-red
This will start a local instance of Node-RED which can be opened by typing the local IP and the port number (127.0.0.1:1880) in a browser.
### Setting up Moquitto MQTT broker
:::info
**Mosquitto MQTT** - [Reference Page](https://mosquitto.org/download/)
:::
Download the appropriate Mosquitto MQTT binary from the reference page. Follow the steps of the installer.
Before running mosquitto, locate the mosquitto configuration file 'moquitto.conf' and create a copy of the default configuration file and add these lines:
> listener 1883 0.0.0.0
> allow_anonymous true
> max_keepalive 0
Then open a new CMD/terminal in Windows and navigate to the mosquitto folder and type the command:
> mosquitto -v -c "(path to the configuration file)"
This will start the mosquitto MQTT broker.
## Putting everything together
### Circuit Diagram
ESP32-C3-DevKitM-1 [Hardware Reference](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html#user-guide-c3-devkitm-1-v1-board-front).
ESP32-C3-DevKitM-1 Pinout diagram:

:::info
The image below displays the wiring/circuit diagram used for this project. (Note: place the ESP32-C3 at the end of the breadboard with the usb port directed towards the outer side and follow the pinout diagram found here [Hardware Reference](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html#user-guide-c3-devkitm-1-v1-board-front) or use the pinout image above)
- **Black Wire - GND**
- **Red Wire - VCC**
- **Yellow Wire - Signal**
- **Green Wire - Echo**
- **Brown Wire - Trigger**
**Sensor/Module Pins to ESP32-C3-DevKitM-1U Pins:**
* **HC-SR04 Ultrasonic sensor**:
GND <-> GND
Echo <-> PIN 7
Trigger <-> PIN 6
VCC <-> 5V
* **DHT11 Temperature/Humidity sensor**:
GND <-> GND
VCC <-> 5V
Signal <-> Pin 3
* **Soil Moisture Sensor**:
GND <-> GND
VCC <-> 3.3V
Signal <-> ADC Pin 2
* **3-5V Water Pump**:
GND <-> GND
VCC <-> Relay Module (NO - Normally Open)
* **5V Relay Module**:
COM <-> GND
VCC <-> 5V
Signal <-> Pin 0

:::
## The code
The main.py file contains functions that handle each sensor and the neccessary logic of the program. The functions are commented with some explanation of what they do.
:::info
The program first connects to an Wi-Fi access point.
Five different topics are created for each data piece that are going to be published to the MQTT-broker.
The program then tries to connect to the MQTT-broker that was previously setup.
Thereafter each sensor is initialized with the correct pins.
The while loop takes measurments with each sensor and stores it in variables which are then published to the MQTT-broker. The relay module will be triggered which activates the water pump only when the soil moisture percentage is lower than 20% (can be changed) and if the water levels are not 0%. Each time the relay module is triggered it also adds to a pump counter.
The code that is running excluding functions and imports is displayed below:
:::
```python=
#Main
#Wifi Connect
do_connect()
#MQTT
# The MQTT topics that we publish data to
topic_pub_temperature = 'esp32c3/sensors/temperature'
topic_pub_soil_moisture = 'esp32c3/sensors/soil_moisture'
topic_pub_water_level = 'esp32c3/sensors/water_level'
topic_pub_humidity = 'esp32c3/sensors/humidity'
topic_pub_pump_counter = 'esp32c3/sensors/pump_counter'
try:
client = connect_mqtt(config.CLIENT_ID, config.MQTT_BROKER)
except OSError as e:
restart_and_reconnect()
#Sensor initialize
try:
print('-- Initializing sensors... --')
ultra_sonic_sensor = HCSR04(trigger_pin=6, echo_pin=7, echo_timeout_us=10000)
print('> Ultra Sonic sensor connected!')
soil_moisture_sensor = ADC(Pin(2)) # Analog-Digital Converter
soil_moisture_sensor.atten(ADC.ATTN_11DB) # This means we want to read voltage from 0 to 3.3V. This corresponds to setting the attenuation ratio of 11db. For that, we use the atten() method and pass as argument: ADC.ATTN_11DB.
print('> Soil Moisture sensor connected!')
humidity_temperature_sensor = dht.DHT11(Pin(3))
print('> Humidity/Temperature sensor connected!')
relay_module = Pin(0, Pin.OUT)
print('> Relay Module connected!')
print("-- All sensors connected! --")
except OSError as e:
print('Failed to initialize sensors! Error:', e)
sleep(3) # wait 3 secodns before starting
#cycles = 0 # cycles of the loop
pump_counter = 0
while True:
humidity_temperature_sensor.measure()
humidity = measure_humidity(humidity_temperature_sensor)
client.publish(topic=topic_pub_humidity, msg=str(humidity))
client.check_msg()
sleep(0.2)
temperature = measure_temperature(humidity_temperature_sensor,2)
client.publish(topic=topic_pub_temperature, msg=str(temperature))
client.check_msg()
sleep(0.2)
soil_moisture = measure_soil_moisture(soil_moisture_sensor) #Before
sleep(0.2)
water_level = measure_water_level(ultra_sonic_sensor,2, 16) #Before
sleep(0.2)
if(pump_water(relay_module, 3, water_level, soil_moisture)): #turn on for 3 seconds
pump_counter += 1
soil_moisture = measure_soil_moisture(soil_moisture_sensor) #After
client.publish(topic=topic_pub_soil_moisture, msg=str(int(soil_moisture)))
client.check_msg()
sleep(0.2)
water_level = measure_water_level(ultra_sonic_sensor,2, 16) #After
client.publish(topic=topic_pub_water_level, msg=str(int(water_level)))
client.check_msg()
sleep(0.2)
client.publish(topic=topic_pub_pump_counter, msg=str(pump_counter))
client.check_msg()
#cycles += 1
sleep(600) #sleep for 10 minutes
#deepsleep(10000) #Can be set to deepsleep instead of sleep to reduce power consumption (It will restart the micro-controller after the time has expired)
```
#### Thonny IDE main.py Console Output

#### Configuration file: config.py
:::info
Use 2.4 GHz frequency band Wi-Fi since micro controllers often only support 2.4 GHz and not 5 GHz.
:::
```python=
# WiFi credentials
WIFI_SSID = 'Your WiFi Name'
WIFI_PASS = 'Your WiFi Password'
# MQTT MOSQUITTO
CLIENT_ID = 'Name of the client that will connect to the MQTT-broker'
MQTT_BROKER = 'IP of the local MQTT-broker Example: 192.168.0.144'
```
#### Main file: main.py
```python=
import os
from machine import Pin, ADC, reset, deepsleep
from time import sleep
import dht
from hcsr04 import HCSR04
from mqtt import MQTTClient
import boot
import config
import gc
gc.collect()
#The remap function takes a value which is in between a range of two values and turns it into percentage, based on the two value range and the initial given value.
def remap(value, leftMin, leftMax, rightMin, rightMax):
leftSpan = leftMax - leftMin
rightSpan = rightMax - rightMin
valueScaled = float(value - leftMin) / float(leftSpan)
return rightMin + (valueScaled * rightSpan)
#sensor - sensor used
#sensor_offset - the distance between a full water tank and the sensor
#water_contrainer_height - height of the water tank container
def measure_water_level(sensor,sensor_offset, water_container_height):
distance_value = sensor.distance_cm()
#print('Distance:', distance_value, 'cm')
water_level_value = remap(distance_value - sensor_offset,1, water_container_height, 100, 1)
if(water_level_value >= 100):
water_level_value = 100
print('Water Level:', water_level_value, '%')
elif(water_level_value <= 0):
water_level_value = 0
print('Water Level:', water_level_value, '%')
else:
print('Water Level:', water_level_value, '%')
return water_level_value
#sensor - sensor used
def measure_soil_moisture(sensor):
soil_moisture_value = remap(sensor.read(),2075, 4095, 100, 0)
#soil_moisture_value = soil_moisture_sensor.read()
if(soil_moisture_value >= 100):
soil_moisture_value = 100
print('Soil Moisture:',soil_moisture_value,'%')
elif(soil_moisture_value <= 0):
soil_moisture_value = 0
print('Soil Moisture:',soil_moisture_value,'%')
else:
print('Soil Moisture:',soil_moisture_value,'%')
return soil_moisture_value
#sensor - sensor used
def measure_humidity(sensor):
humidity_value = sensor.humidity()
print('Humidity: %3.1f %%' %humidity_value)
return humidity_value
#sensor - sensor used
#sensor_offset - margin differnce
def measure_temperature(sensor, sensor_offset):
temperature_value = sensor.temperature() - sensor_offset
print('Temperature: %3.1f C' %temperature_value)
return temperature_value
#module - the module used
#state - On(1), Off(0)
#timer - for how long the module should be active in seconds
#water_level - the amount of water left
def pump_water(module, timer, water_level_value, soil_moisture_value):
if(water_level <= 0):
print("Not enough water to pump!")
elif(soil_moisture_value <= 20):
module.value(1)
print("Relay: ON! - Pumping Water...")
sleep(timer)
module.value(0)
print("Relay: OFF! - Water pump stopped.")
return True
def sub_cb(topic, msg):
print(msg)
def connect_mqtt(client_id, mqtt_broker):
print("Attempting to connect to MQTT broker...")
client = MQTTClient(client_id, mqtt_broker, port=1883)
client.set_callback(sub_cb)
client.connect()
print('Connected to %s MQTT broker' % (mqtt_broker))
return client
def restart_and_reconnect():
print('Failed to connect to MQTT broker. Reconnecting...')
sleep(10)
reset()
#Main
#Wifi Connect
do_connect()
#MQTT
# The MQTT topics that we publish data to
topic_pub_temperature = 'esp32c3/sensors/temperature'
topic_pub_soil_moisture = 'esp32c3/sensors/soil_moisture'
topic_pub_water_level = 'esp32c3/sensors/water_level'
topic_pub_humidity = 'esp32c3/sensors/humidity'
topic_pub_pump_counter = 'esp32c3/sensors/pump_counter'
try:
client = connect_mqtt(config.CLIENT_ID, config.MQTT_BROKER)
except OSError as e:
restart_and_reconnect()
#Sensor initialize
try:
print('-- Initializing sensors... --')
ultra_sonic_sensor = HCSR04(trigger_pin=6, echo_pin=7, echo_timeout_us=10000)
print('> Ultra Sonic sensor connected!')
soil_moisture_sensor = ADC(Pin(2))
soil_moisture_sensor.atten(ADC.ATTN_11DB)
print('> Soil Moisture sensor connected!')
humidity_temperature_sensor = dht.DHT11(Pin(3))
print('> Humidity/Temperature sensor connected!')
relay_module = Pin(0, Pin.OUT)
print('> Relay Module connected!')
print("-- All sensors connected! --")
except OSError as e:
print('Failed to initialize sensors! Error:', e)
sleep(3) # wait 3 secodns before starting
#cycles = 0 # cycles of the loop
pump_counter = 0
while True:
humidity_temperature_sensor.measure()
humidity = measure_humidity(humidity_temperature_sensor)
client.publish(topic=topic_pub_humidity, msg=str(humidity))
client.check_msg()
sleep(0.2)
temperature = measure_temperature(humidity_temperature_sensor,2)
client.publish(topic=topic_pub_temperature, msg=str(temperature))
client.check_msg()
sleep(0.2)
soil_moisture = measure_soil_moisture(soil_moisture_sensor) #Before
sleep(0.2)
water_level = measure_water_level(ultra_sonic_sensor,2, 16) #Before
sleep(0.2)
if(pump_water(relay_module, 3, water_level, soil_moisture)): #turn on for 3 seconds
pump_counter += 1
soil_moisture = measure_soil_moisture(soil_moisture_sensor) #After
client.publish(topic=topic_pub_soil_moisture, msg=str(int(soil_moisture)))
client.check_msg()
sleep(0.2)
water_level = measure_water_level(ultra_sonic_sensor,2, 16) #After
client.publish(topic=topic_pub_water_level, msg=str(int(water_level)))
client.check_msg()
sleep(0.2)
client.publish(topic=topic_pub_pump_counter, msg=str(pump_counter))
client.check_msg()
#cycles += 1
sleep(600) #sleep for 10 minutes
#deepsleep(10000)
```
#### Boot file (Network Connection): boot.py
```python=
def do_connect():
import network
import config
sta_if = network.WLAN(network.STA_IF) # Put modem on Station mode
if not sta_if.isconnected(): # Check if already connected
print('connecting to network...')
sta_if.active(True) # Activate network interface
sta_if.connect(config.WIFI_SSID, config.WIFI_PASS) # Your WiFi Credential
# Check if it is connected otherwise wait
while not sta_if.isconnected():
pass
# Print the IP assigned by router
print('network config:', sta_if.ifconfig())
```
#### MQTT file: mqtt.py
```python=
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.addr = socket.getaddrinfo(server, port)[0][-1]
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()
self.sock.connect(self.addr)
if self.ssl:
import ussl
self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
msg = bytearray(b"\x10\0\0\x04MQTT\x04\x02\0\0")
msg[1] = 10 + 2 + len(self.client_id)
msg[9] = clean_session << 1
if self.user is not None:
msg[1] += 2 + len(self.user) + 2 + len(self.pswd)
msg[9] |= 0xC0
if self.keepalive:
assert self.keepalive < 65536
msg[10] |= self.keepalive >> 8
msg[11] |= self.keepalive & 0x00FF
if self.lw_topic:
msg[1] += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
msg[9] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
msg[9] |= self.lw_retain << 5
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)
#print(resp)
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()
```
#### HCSR04 Ultrasonic sound sensor file: hcsr04.py
```python=
import machine, time
from machine import Pin
__version__ = '0.2.0'
__author__ = 'Roberto Sánchez'
__license__ = "Apache License 2.0. https://www.apache.org/licenses/LICENSE-2.0"
class HCSR04:
"""
Driver to use the untrasonic sensor HC-SR04.
The sensor range is between 2cm and 4m.
The timeouts received listening to echo pin are converted to OSError('Out of range')
"""
# echo_timeout_us is based in chip range limit (400cm)
def __init__(self, trigger_pin, echo_pin, echo_timeout_us=500*2*30):
"""
trigger_pin: Output pin to send pulses
echo_pin: Readonly pin to measure the distance. The pin should be protected with 1k resistor
echo_timeout_us: Timeout in microseconds to listen to echo pin.
By default is based in sensor limit range (4m)
"""
self.echo_timeout_us = echo_timeout_us
# Init trigger pin (out)
self.trigger = Pin(trigger_pin, mode=Pin.OUT, pull=None)
self.trigger.value(0)
# Init echo pin (in)
self.echo = Pin(echo_pin, mode=Pin.IN, pull=None)
def _send_pulse_and_wait(self):
"""
Send the pulse to trigger and listen on echo pin.
We use the method `machine.time_pulse_us()` to get the microseconds until the echo is received.
"""
self.trigger.value(0) # Stabilize the sensor
time.sleep_us(5)
self.trigger.value(1)
# Send a 10us pulse.
time.sleep_us(10)
self.trigger.value(0)
try:
pulse_time = machine.time_pulse_us(self.echo, 1, self.echo_timeout_us)
return pulse_time
except OSError as ex:
if ex.args[0] == 110: # 110 = ETIMEDOUT
raise OSError('Out of range')
raise ex
def distance_mm(self):
"""
Get the distance in milimeters without floating point operations.
"""
pulse_time = self._send_pulse_and_wait()
# To calculate the distance we get the pulse_time and divide it by 2
# (the pulse walk the distance twice) and by 29.1 becasue
# the sound speed on air (343.2 m/s), that It's equivalent to
# 0.34320 mm/us that is 1mm each 2.91us
# pulse_time // 2 // 2.91 -> pulse_time // 5.82 -> pulse_time * 100 // 582
mm = pulse_time * 100 // 582
return mm
def distance_cm(self):
"""
Get the distance in centimeters with floating point operations.
It returns a float
"""
pulse_time = self._send_pulse_and_wait()
# To calculate the distance we get the pulse_time and divide it by 2
# (the pulse walk the distance twice) and by 29.1 becasue
# the sound speed on air (343.2 m/s), that It's equivalent to
# 0.034320 cm/us that is 1cm each 29.1us
cms = (pulse_time / 2) / 29.1
return cms
```
## Data transmission, connectivity and platform
Everything was setup and hosted locally both the MQTT-broker and Node-RED. Local setup was chosen because it is a controlled enviroment, data is not required to be sent outside the network which makes it more secure and data is sent faster since it is in a WLAN (Wireless Local Area Network) and does not require any hops through the internet. The data in the dashboard can still be displayed outside the WLAN by using either e.g VPN/routing or even tools like ngrok to serve a web page.
The data is transmitted from the ESP32-C3 to the Mosquitto MQTT-broker every 10 minutes and then fetched by a subscribing Node-RED MQTT-node. (The data publishing time can be changed to a higher value to have less updates of values but also lowers the energy consumption)
There could be better options for the platform but Node-RED seems very simple with drag and drop which makes the project more managable, scalable and visually appealing.
Wi-Fi was used as the wireless communication protocol, WPA2 as the security protocol and MQTT as the transport protocol.
MQTT as a transport protocol was choosen because it is optimized and resource efficient for low power devices. It has low data overhead and is simple when transmitting/fetching data.
Wi-Fi (802.11ac) as connectivity protocol was choosen because it is a more controlled enviroment and data can mostly be manipulated by users/devices that are connected to the network. The drawback with using Wi-Fi is the energy consumption, other connectivty protocols could be use to further decrease the energy consumption.
2.4 GHz Frequency Band was used since it is supported by the ESP32-C3 and most of the micro-controllers out there.
The choosen security protocol was WPA2 but WPA3 could also be used to improve the security of the micro-controller since IoT devices are primarly built with the focus on performance and energy consumption and not security which makes them vulnerable to alot of attacks. (Keep in mind that WPA3 have a larger data overhead compared to WPA2 because of the encryption algorithms that require larger key-size e.g 192-bit)
## Presenting the data
To present the data visually, a dashboard has to be created through Node-RED.
### Creating a Node-RED flow and dashboard
After starting Node-RED in the terminal go to a browser of your choosing and type 127.0.0.1:1880 in the browser address field.
After the page opens up, press the three lines on the top right corner and choose 'Manage Palette' then choose the 'Install' tab and type '*node-red-dashboard*' then install.
Pick the MQTT subscriber node  and drag it to the grid.
Then pick  and drag them to the grid.
Then click on the 'MQTT in' node and change the settings.
*Server* should be the IP that the Mosquitto MQTT-broker is running on and the default unencrypted port 1883 for MQTT. Port 8883 for encrypted.

Press the Pencil Button in the image above, this will provide additional settings which can be changed. The additonal settings are displayed in the image below.

Then change the settings for each chart/gauge nodes as displayed in the image below.

The final flow should look like the image below.

(The connection with Node-RED broker node will also be shown in the terminal)

Then press  which will open a new page with a dashboard of all the charts/gauges created. Image below displays the dashboard that was created during this project.

The transmited 'publish' data and the associated devices can be confirmed throught Moquitto MQTT-broker.

## Finalizing the design
I think the project went better than expected. Everything worked without any issues.
The project could be improved by mounting better sensors with better accuracy and precision. For example, instead of DHT11 temperature/humidity sensor a better version could be used instead becuase of the the accuracy margin +/- 2 °C and humidity 5%, while DHT22 has a accuracy margin of +/- 0.5 °C and humidity 2%. The sensors could also be calibrated better.
The project is not limited to a single soil moisture or a single-channel relay module. Multi-channel relay module and multiple soil moisture sensors could be used to monitor multiple plants.
Here are some images and videos of the automatic water plant system in action:
---


---
<div style="width:100%;height:0px;position:relative;padding-bottom:56.250%;"><iframe src="https://streamable.com/e/i7c36r" frameborder="0" width="100%" height="100%" allowfullscreen style="width:100%;height:100%;position:absolute;left:0px;top:0px;overflow:hidden;"></iframe></div>
---
<div style="width:100%;height:0px;position:relative;padding-bottom:56.250%;"><iframe src="https://streamable.com/e/06e1t5" frameborder="0" width="100%" height="100%" allowfullscreen style="width:100%;height:100%;position:absolute;left:0px;top:0px;overflow:hidden;"></iframe></div>