In the last part, we talked about the basic concept of the plant watering project. Now we go into more detail about the implementation.

Before we write the whole program we need to understand how every part works in order to combine them into a useful piece of software. This means we need to know how to control the pump relay, read analog values (both are quite simple), establish and re-establish a connection to a wifi network, connect and reconnect to an MQTT server.

It is also important to understand the basic principle of a state machine to make our lives easier.

The Simple Basics

Controlling the pumps through a relay module

Controlling the pumps through a relay module is in fact quite simple. First, discuss how these things work. A relay is some sort of switch which can be activated by a current. It works by pulling a lever by magnetizing a coil with electrical current. This current is way higher than most of our esp32 io pins could deliver. So in front of every relay is a transistor. We use a voltage from the esp32 to switch the transistor which lets current through the coil to switch the relay to run our pump. Easy right?

Every relay input pin of the relay board connects to a digital io pin of the esp32. We can now activate the specific relay/pump with:

digitalWrite(ioPin, HIGH);
//digitalWrite(ioPin, LOW); // Depending on the relay module the relay may switched by using HIGH or LOW state

Switching it off is the same but instead of pulling the io pin HIGH you would use LOW. Depending on the model of your relay board these to states could also be switched around.

Reading the moisture from the moisture sensor

Our capacitive soil moisture sensors are analog sensors. They output an analog value between 0 and 3,3V according to a vague moisture level (that’s why we need to calibrate our values later). They do so by utilizing a timer chip to generate a defined voltage frequency and let that flow towards a big part of copper inside the board. If water comes near this copper trace it will act as a capacitor (that only works because water molecules are organized in a specific way so it can be attracted by electrical charges). This capacitor then forms a voltage difference to our target voltage given by the timer chip. This difference can then be smoothed out to become a constant voltage that depends on the amount of water near the copper traces.

Because the sensor outputs a simple voltage we can utilize standard Arduino commands to read it:

int sensorValue = analogRead(ioPin);

Note that this value is not the moisture level in percent. It is just a raw 12bit analog value ranging from 0 to 4095. We need to convert this into moisture. For now, we assume that the percentage of moisture is linearly dependent on the voltage/raw value. With that assumption we can convert by using:

float moisture = (raw_value - raw_0) * 100 / (raw_100 - raw_0);

Where raw_0 is the raw analog value when at 0 percent moisture and raw_100 is the raw analog value when at 100 percent moisture.


Using WiFi

Using wifi on the esp32 inside the Arduino IDE is also pretty much straightforward. Because explaining the principles of WiFi and how it works physically inside the chips is out of the scope of this post I won’t get into that. What I can tell is that the underlying “operating system” of the esp32 handles all intermediate stuff that translates our commands in native WiFi packets. These packets are then passed to an internal chip that is specially designed to transmit these packets over the specified frequency.

Because the underlying “operating system” handles most of the WiFi stuff for us, connecting to a WiFi network is also simple.

WiFiClient client;

void setup(){
  WiFi.begin("SSID", "Password");

That is all we need to connect to a network. Unfortunately, there seems to be a bug inside the esp32 underlying operating system which causes WiFi to not reconnect properly when the connection is lost (esp8266 do this with the same commands). That’s why we should regularly check if the WiFi connection is still established and reconnect otherwise:

int counter = 0
if (WiFi.status() != WL_CONNECTED) {
  if( counter > 50 ){
    Serial.println("WiFi not connected. Connecting...");
    counter = 0;

That snippet of code is called regularly inside the loop. Adjust the value 50 to meet your specific timeout. My loop is made to be run every 100ms meaning that a value of 50 means if the wifi connection is lost for 5 seconds, reconnect.

Using MQTT

MQTT is a protocol for (mainly) embedded devices to communicate their status and to receive commands. They do so by connecting to a server that acts as key-value storage. MQTT clients can push values to keys or subscribe to them in order to get notified when another client pushes new values to that key.

Using MQTT is a bit more complicated than using wifi. To make our lives easier we use a library called PubSubClient to handle our connection to the MQTT server. Connecting to a server is also straightforward:

// WiFi Stuff
// ....

PubSubClient mqttClient(client); // Client is our WiFi client object

void setup(){
  // WiFi Stuff
  // ....

  client.setServer("IP", port);

void callback(char* raw_topic, byte* raw_message, unsigned int length) {
  // Code to handle changes to keys we have subscribed to

void loop(){
  // WiFi Stuff
  // ....

  if (!client.connected() && WiFi.status() == WL_CONNECTED) {
    Serial.println("MQTT not connected. Connecting...");
    if( client.connect("Client Name", "Username", "Password") ) {

Using mqtt we can make the esp32 to publish our moisture values with:

client.publish("key", "value");

That way we can monitor the moisture of the plants with for example Home Assistant or a standalone client that also connects to the MQTT server.

We are also able to change our calibration values by having the esp32 subscribed to keys that we can write to modify our calibration. Think of the subscribed MQTT values as local variables that other people can write to.

State Machines

State machines consist of a set of states and terms that define which action must happen to get from one state to another. By storing a list of states (one for each plant) we can run each plant independently from each other. A good analogy would be the Arduino Blink with Delay and Arduino Blink with Timer examples. The delay example pauses the program flow to achieve the desired effect. The timer example indirectly uses to states and a timer that selects when to switch states. That why the program flow is not paused and you can do other stuff in the meantime. Or handle multiple LEDs at once. If you are not convinced yet then try the following: Let 5 LEDs blink with different frequencies. One with a frequency of 2 seconds, one with 3 seconds, one with 5 seconds, one with 7 seconds and the last one with 11 seconds.

Using delays is, in fact, possible but would require you to think about the next 2 * 3 * 5 * 7 * 11 = 2310 seconds because only then the pattern would repeat (Now you see why I choose prime numbers right?). And that is only using predefined timers.

For our plant state machine, I thought about 5 states: STATE_DEACTIVATED, STATE_IDLE, STATE_PUMPING, STATE_DRYOUT, and STATE_ERROR. Take the names with a grain of salt. Note that the pump will be switched off in every state except for STATE_PUMPING where it will be switched on. They do not quite resemble their meaning very well.

State Machine of a single Plant

This is the complete state machine used to simulate the behavior of every plant. Let’s go through this a bit. We will start in STATE_DEACTIVATED when we receive a mqtt_start signal the plant will switch to STATE_IDLE (state idle is a state where the plant needs to get water but we wait a little bit to let the moisture settle). When a timer variable reaches 60 seconds we will switch to STATE_PUMPING (when the moisture is lower than a given threshold moist soil) or STATE_DRYOUT (when it is moist). In STATE_PUMPING we wait 5 seconds and switch back to STATE_IDLE when we did not pump 10 times successively (when we did turn the pump on 10 times in a row we switch to STATE_ERROR). In STATE_DRYOUT we wait 60 seconds and either switch to itself (when moisture is above a given threshold for dry soil) or to STATE_PUMPING when the given threshold is undercut. When reached STATE_ERROR then it will switch to itself again in 60 seconds.

Why the state switch from STATE_ERROR to itself every 60 seconds you may ask. That is because I decided to let the esp32 post it’s values for a plant to the MQTT server each time a state change occurs.

The thing with the pumpCounter is made to counter possible errors caused by a sensor that broke (can’t reach threshold then and will pump out the reservoir; not good) or a broken tube (can’t reach threshold then and will pump out the reservoir; not good either).

The final program

By combining all these things into one program you will get a decent plant watering system. At least software-wise.

Going through the complete source code does not make sense. If you like you can look at the source code at GitHub in a few days. I need to strip credentials and put in some comments for you.

Feel free to copy and modify it to your needs. If you made serious improvements or other ideas you can reach out to me so I can make these changes to my project as well.

Comments are closed