Pictures
The goal of this project was to familiarize myself with the world of hardware and build a full-stack IoT (Internet-of-Things) solution. I've also never been much of a gardener, but it's something I've wanted to get into. This project allows you to remotely monitor and water your plants over the internet. It consists of a NodeMCU chip connected to a moisture sensor and a water pump. The moisture sensor is inserted into the soil of your plant, and a moisture reading is periodically taken. This data is sent to a web server, where it can be displayed to the user. The user can also activate the pump from the server, so that the plant gets watered. This demonstrates two-way communication between a device and web server from anywhere with an internet connection.
We'll begin with a more technical overview of how everything works. A moisture sensor is stuck into the soil of a house plant and is connected to the NodeMCU microcontroller. The NodeMCU will read these values periodically based on a timer, and they will be sent to the web server using a technology called MQTT. MQTT is just a way for two programs/devices/endpoints/nodes to talk to each other (like HTTP), but it is very amenable to IoT projects since it is fast, lightweight, and can easily handle multi-way communication. Once the data reaches the web server, it is logged to the user. There are many possibilities to extend this functionality into something like a web or mobile app.
The user will see the moisture values, and can water the plant accordingly by sending a request to the web server. This is accomplished using the same mechanism we used to read the moisture: MQTT. This time, however, it's going in the other direction. This time, the web server is sending data to the NodeMCU, instead of the other way around. The user posts a watering time period (in seconds) to the server, which uses the same MQTT channel as the moisture sensor, and sends the message to the NodeMCU. The NodeMCU receives this message, reads the number of seconds, and activates the motorized pump for the requested amount of time. One end of the pump draws from a water source, and the other end feeds the water to the plant.
That's the essence of the project. The moisture value is sent from the NodeMCU to the web server, and the watering command is sent from the web server to the NodeMCU. This communication happens over an MQTT broker, which exists on a completely separate server. So now that we have a clearer understanding of what's going on, let's jump into the hardware.
HardwareI chose a NodeMCU as the controller for this project because it's cheap, easy to use, and has all the required connections. If you're unfamiliar with it, it's similar to an Arduino, but it is wifi-capable. You can use the Arduino IDE to write and upload code onto it (which we'll get to later on). Start by sticking the controller onto your breadboard. At this point we will also add some wire connections to the rails of the breadboard to make the next connections easier.
Next we'll add in the moisture sensor. This project uses a capacitive moisture sensor. Another common type of moisture sensor is a resistive moisture sensor, but the metal on the prongs that get inserted into the soil can rust easily, so they are not ideal. The capacitive sensor avoids this problem since the metal plates of the capacitor are not actually exposed to the water in the soil. It gets a moisture reading by measuring the dissolved ions in the soil. Use the 4-pin connector cable to connect it to power and ground on the breadboard. Then connect the other two pins to the GPIO ports on the NodeMCU labeled D1 and D2. Make sure to follow the picture closely - the green cable should connect to D1 and the white to D2.
Now we will add the motor. The key challenge for the motor is that we only want it on for a given period of time once it is activated. I accomplished this by hooking up a transistor to an output pin on the NodeMCU. When the user sends the pump command from the web server, the NodeMCU will receive the command, and activate the output pin, triggering the transistor. With power applied to gate pin of the transistor, current is now able to flow through the source and drain, supplying power to the motor. Start by connecting the SD3 pin of the NodeMCU to the Gate pin of the transistor
I experimented with a few different transistors, and the IRFZ44N was the most reliable. I started with a smaller BJT, but found that the current being drawn was way too high and it heated up very fast once the motor was on and caused some unpredictable behaviour. I've had the IRFZ44N in use for a few months and I haven't had any issues. Next we are going to connect the motor to the other transistor pins, which will allow current to flow when the gate is triggered.
Finally, add a diode in parallel with the motor to prevent fly-back voltage when the motor is turned off.
Now let's talk about the power. I started with 6V (4 x 1.5V batteries), because I am using a 6V motor. The motor spun quite slowly and therefore did not draw a lot of water. I verified with a multimeter and got a lower voltage reading across the motor of about 4.3. I tried a 9V battery and this was too much power, and I didn't want to overload the motor, so I settled on 7-8V. I also wanted to plug into the wall, since batteries tend not to last very long with the NodeMCU. It was very difficult to find a wall wart with an output of 7-8VDC. I eventually found one that allowed you to adjust the output voltage between 5V and 12V. Once we've finished writing the code and uploading it to the NodeMCU we will plug this in, but for now, we'll just be plugging the NodeMCU into the computer.
That's it for the hardware! Review the diagram below, and then lets move on to the software.
We'll take the following approach for building up the code to get this project running:
- Set up the environment
- Read in the moisture on a timer
- Trigger the pump
- MQTT Discussion
- Connect WiFi and MQTT
- Set up the web server
- Send moisture from the device to the server
- Trigger the pump from the server
- Wrap up
Let's open up the Arduino IDE and create a new file, which comes with a setup() and a loop() function. In order to work with the NodeMCU, we need to use the proper board configuration. Open up Tools > Board > Board Manager, and search for 'esp8266', and install it. Once that's finished, make sure you select NodeMCU as your board in the Tools > Board list. To start just try a simple Serial.println("hello world") in the setup function, upload it to your board, and make sure it runs. You may see some gibberish before the first line in your serial monitor like I have, but we don't need to worry about it for now.
void setup() {
Serial.begin(115200);
Serial.println("");
Serial.println("Hello world!");
}
We will just be reading in the moisture at a timed interval, let's say every 10 seconds. To accomplish this we will need two libraries. The SimpleTimer library will give us the timer functionality, and the Adafruit Seesaw library will give us the functionality to interact with the moisture sensor. Download them from the links below and import them at the top of your file.
Simple Timer: https://playground.arduino.cc/Code/SimpleTimer/
Adafruit Seesaw: https://github.com/adafruit/Adafruit_Seesaw
Imports:
#include <SimpleTimer.h>
#include <Adafruit_seesaw.h>
To set up the timer, declare the following above your setup() function:
int timerId;
SimpleTimer timer;
And start the timer on setup by adding this line to your setup() function:
timerId = timer.setInterval(5000, timesUp);
This will set a timer for 10000ms (10 seconds), and once that time elapses, it will call a function called timesUp, so let's define this function and just have it print a line to the console:
void timesUp() {
Serial.println("testing");
}
Then, in the loop function, ensure that the timer is running by adding the following:
timer.run();
Now, try uploading that to your board. Verify that the word 'testing' prints to the serial console every 10 seconds. Here is the entire code:
#include <SimpleTimer.h>
#include <Adafruit_seesaw.h>
int timerId;
SimpleTimer timer;
void setup() {
Serial.begin(115200);
timerId = timer.setInterval(5000, timesUp);
}
void loop() {
timer.run();
}
void timesUp() {
Serial.println("testing");
}
To add the moisture reading, we need to declare the sensor variable at the beginning of our code, with our other declarations, initiate it in the setup, and then read from it on the timesUp function.
Declaration (before setup):
Adafruit_seesaw ss;
Initialization (inside setup):
ss.begin(0x36); // 0x36 is the I2C address of the moisture sensor
Read Moisture (in timesUp):
uint16_t value = ss.touchRead(0);
Change the print statement in timesUp() to print the moisture value as well. Ensure the moisture sensor is plugged in to the breadboard and try running the code again. In the serial console you should see values between 200 and 2000. A higher value means a higher moisture. Try lightly touching the sensor with your finger, or dipping it in some water or soil, and watch the value change. Be sure not to get any water on the wiring at the top of the sensor.
Now that we have the sensor working, let's just change the name of the timesUp function to be something more descriptive, like readMoisture. Make sure to change it in both places. Let's also pull the timer period out into a variable defined outside of the setup function. The entire code should look like this:
#include <SimpleTimer.h>
#include <Adafruit_seesaw.h>
int timerId;
SimpleTimer timer;
Adafruit_seesaw ss;
int MOISTURE_POLL_MS = 10000;
void setup() {
Serial.begin(115200);
timerId = timer.setInterval(MOISTURE_POLL_MS, readMoisture);
ss.begin(0x36);
}
void loop() {
timer.run();
}
void readMoisture() {
uint16_t value = ss.touchRead(0);
Serial.println(value);
}
Now let's move on to the motorized pump. Eventually we will be activating the motor by listening to the MQTT broker on the web server. We'll go into more detail on this later, but for now, let's just set up the motor to be activated by another timer. So add in another timer just like we did with the first one (we will remove this later on). Declare another timer id, another timer instance, start it in the setup, and configure it to call a function called 'triggerPump' every 10 seconds. Run the timer in the loop, and define an empty function called triggerPump().
No new libraries are required for this step, and it is actually quite simple now that the hardware is set up. Recall that for the motor, we are sending a signal to switch on a transistor that will allow power to flow through the other pins of the transistor, which is where the motor leads will be. We want to provide this signal for a given amount of time - let's start with 3 seconds.
1. Define the pin on the NodeMCU to which we'll be outputting the signal. We hooked up the transistor to SD3 on the NodeMCU, which is Pin 10:
int PUMP_PIN = 10;
2. In the setup function, set the pinMode of that pin to be OUTPUT:
pinMode(PUMP_PIN, OUTPUT);
3. In the triggerPump function, add the following code:
digitalWrite(PUMP_PIN, HIGH);
delay(3000);
digitalWrite(PUMP_PIN, LOW);
This will send the signal to the desired pin, turning on the motor. Then we will wait for 3 seconds, during which time the motor will be running, and then we will turn it off. Make sure the motor is connected and try it out. Note that since you are likely just connected to your laptop, the voltage supplied will be low, causing the motor to spin slowly. This will improve when we hook it up to a higher voltage. You may also note that once the motor starts being used, some of the moisture sensor values will be off - you may see it outputting a value of 65535 to the serial monitor. This is another power-related issue that will be corrected once we switch to a higher voltage. Here is the full code:
#include <SimpleTimer.h>
#include <Adafruit_seesaw.h>
int timerId;
int timerId2;
SimpleTimer timer;
SimpleTimer timer2;
Adafruit_seesaw ss;
int MOISTURE_POLL_MS = 5000;
int PUMP_PIN = 10;
void setup() {
Serial.begin(115200);
timerId = timer.setInterval(MOISTURE_POLL_MS, readMoisture);
timerId2 = timer2.setInterval(10000, triggerPump);
ss.begin(0x36);
pinMode(PUMP_PIN, OUTPUT);
}
void loop() {
timer.run();
timer2.run();
}
void readMoisture() {
uint16_t value = ss.touchRead(0);
Serial.println(value);
}
void triggerPump() {
digitalWrite(PUMP_PIN, HIGH);
delay(3000);
digitalWrite(PUMP_PIN, LOW);
}
MQTT discussionWe now have the two main pieces of hardware functionality: reading the moisture, and triggering the motor. This is all basic arduino stuff, and at this point, if you want to just create an arduino project without any WiFi or IoT functionality, you can use this code as a starting point and adapt it however you want. But for this project, we are now going to get into the more interesting and complex side of things. Before we write any more code, it is worth discussing MQTT in more detail.
MQTT is the communication protocol we will be using to communicate between the web server and the NodeMCU. In this project, MQTT operates in what is known as a PubSub (publisher-subscriber) pattern. We will have a separate server (not the same as our web server) on which an MQTT "broker" is running. The web server and the NodeMCU do not actually communicate *directly* to each other. Rather, they will both connect to this broker, which will handle the communication. Any device that is connected to the broker can publish (i.e. send messages) or subscribe (listen to messages). This is accomplished using 'topics'. Any device connected to the broker can send it a message on any topic, and any device that is listening to that topic will receive the message. So we will have a topic called 'moisture'. The NodeMCU device will publish the moisture readings to this topic, and the web server will subscribe to it. We will have another topic called 'pump'. The web server will publish to pump when it is triggered by the user, and the NodeMCU will subscribe to 'pump', so that it can activate the motor.
It is important to note that the NodeMCU and the web server don't even need to know that the other one exists. The web server just cares about any messages that come in on the topic 'moisture', and any time it is given the command to water the plants by the user, it will publish on the topic 'pump'. Any other device that is connected to the same MQTT broker could publish to the topic moisture, and the broker will let the web server know.
Here is what we will need to do:
1. We will set up an MQTT broker running on a server in the cloud. We will receive some credentials that allow our device and our server to connect to it.
2. We will configure our NodeMCU to connect to the internet using its WiFi capabilities. This will be accomplished by importing a special library and providing the NodeMCU with the WiFi credentials (Network name and WiFi password).
3. Once the NodeMCU is connected to the internet, we will then configure it to connect to our MQTT broker from step 1. This will be accomplished with another library and the set of credentials mentioned in step 1.
4. Once those connections are made, the NodeMCU is able to either publish or subscribe to any topic it wants. The publish topics don't need to be defined in advance. You can just tell NodeMCU to publish to a some topic, say 'topic12345', and it will publish your message to the broker. In order for another device to receive that message, it must be:
a) connected to the internet
b) connected to the same MQTT broker
c) subscribed to that exact same topic (topic12345)
5. The other 'device' in our case is the simple web server, which we will download from github, configure for the same MQTT broker using the same credentials as we used for the NodeMCU, and run locally. Note that this could be any device that can meet those 3 requirements, including another NodeMCU, or an Arduino hooked up to the internet. We could also use MQTT to just communicate between two web servers if we wanted.
Connect MQTT and WiFiFor the MQTT broker, you could set up one to run locally or on a VM, but an easier option is to use a managed MQTT broker service running in the cloud - that way we don't have to worry about any installation. For this project we will use CloudMQTT, which has a free tier for small projects like this.
Head over to their site, sign up, and create a new instance on the 'Cute Cat' free tier. Once created, go into your instance to verify it was created. You'll see some credentials and other info which we will need later on. We now have our own small MQTT broker running in the cloud.
With that set up, we can now turn back to our NodeMCU code. Add the following two libraries and import them at the top of your file:
- ESP8266Wifi: https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/ESP8266WiFi.h
- PubSubClient: https://github.com/knolleary/pubsubclient
Below your includes, add the variables that will be required for connecting to WiFi and MQTT:
// Imports
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
/// WiFi Network credentials
const char* WIFI_SSID = "[INSERT DATA]";
const char* WIFI_PASSWORD = "[INSERT DATA]";
// MQTT Info: Obtained from cloudMQTT
const char *MQTT_SERVER = "[INSERT DATA]";
const int MQTT_PORT = [INSERT NUMBER]; // Use the regular 'Port', not 'SSL Port'
const char *MQTT_USER = "[INSERT DATA]";
const char *MQTT_PASSWORD = "[INSERT DATA]";
// MQTT Topics
const char *PUB_TOPIC = "moisture";
const char *SUB_TOPIC = "pump";
Add in declarations of these two clients before the setup, alongside your other declarations:
WiFiClient espClient;
PubSubClient client(MQTT_SERVER, MQTT_PORT, espClient);
We could set up the WiFi and MQTT in the setup function, but there is a chance the NodeMCU could disconnect during operation, so we are actually going to ensure that it is connected inside the loop function. We are going to call two separate functions from the loop to ensure this. The loop function will look like this:
void loop() {
connectWifi();
connectPubSub();
if (WiFi.status() == WL_CONNECTED && client.connected()) {
client.loop();
timer.run();
}
}
Note that the secondary timer for the motor has been removed, but the original timer for the moisture reading is still there. So every loop, we want to ensure we are connected to both WiFi and MQTT. If we are, then we run the client.loop() function and the timer.run(). We know the timer run function is for reading the moisture. The client in the client.loop() statement refers to the PubSub client we just declared. On each loop, the PubSub client will be polling for any new messages that it is subscribed to on the MQTT broker. So let's implement the connectWifi and connectPubSub functions.
Here is the connectWifi function:
void connectWifi() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Connecting to wifi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
return;
}
Serial.println("WiFi connected");
}
}
So we're only going to attempt a connection if we aren't already connected to WiFi. If we didn't, we'd be wasting a lot of time every single loop making sure we are connected.
Here is the connectPubSub function:
void connectPubSub() {
if (!client.connected()) {
Serial.println("Connecting to MQTT server...");
if (client.connect("iot_garden_demo", MQTT_USER, MQTT_PASSWORD)) {
Serial.println("Connected to MQTT server");
client.setCallback(callback);
client.subscribe(SUB_TOPIC);
} else {
Serial.println("Could not connect to MQTT server");
}
}
}
Again, we only attempt the connection if the connection is not already made. If it's not connected, we attempt the connection using the different variables we declared earlier. If the connection is successful we set a callback (defined next) and we subscribe to the topic we defined as well. The callback is the function that is going to execute whenever a message comes in on a topic to which we're subscribed (i.e. the SUB_TOPIC). We will now define that function, and for now we will just print out the message that comes in. The callback function takes in a topic, a byte payload, and the message length. We will just parse the message from the payload and print it out.
void callback(char* topic, byte* payload, unsigned int len) {
char value[5] = "";
for (int i = 0; i < len; i++) {
value[i] = (char)payload[i];
}
Serial.println(value);
}
Double-check all of your WiFi and MQTT variables, remove the remaining code for the second timer, and add the new functions. Leave in the triggerPump function though, as we will be re-using it once we've hooked up a server. At this point, there isn't much we can do to test it. But we can run the code on the NodeMCU and flip back to our CloudMQTT dashboard to confirm that we're connected to the broker. Navigate to your MQTT instance and find the 'Connections' on the sidebar. Click it and you should see that a device is now connected. We will now switch gears and spin up the web server which will be on the other side of the MQTT broker.
For this portion of the project you will need to have Node.js installed on your computer. Clone the repository, and fill in the values in the config.js file with what is in your MQTT dashboard. Note that there is also a finished.ino file if you need it as a reference.
git clone https://github.com/reganmeloche/iot-garden-demo.git
Let's briefly walk through the server code. Open the index.js file in any code editor. We begin by importing the required libraries. Note the mqtt library, which is what we use to connect to the MQTT broker. We then create an MQTT_URL, which will contain all of the MQTT credentials that we've defined in config.js. This is what the web server uses to connect to the MQTT broker. We also define the SUB_TOPIC and the PUB_TOPIC. Note that these are the reverse values on the NodeMCU. The NodeMCU publishes to 'moisture' and subscribes to 'pump', but the web server publishes to 'pump' and subscribes to 'moisture'. We immediately attempt a connection to the MQTT broker using mqtt.connect().
We then have some MQTT client functions defined. the client.on('connect') will be called once that connection is made, and then we will subscribe to the SUB_TOPIC (moisture). The client.on('message') is called whenever a message comes in that the web server is subscribed to. This is akin to the callback function we defined on the NodeMCU. When we receive a message, all we will be doing is logging it to the console.
Then we expose the endpoint '/water' for the user to POST to. We parse out the number of seconds and publish that message to the PUB_TOPIC (pump), which will be sent by the broker to the NodeMCU.
To run the server, run `npm install` to install your packages and then `npm start`. The server should spin up, and you should see a message that is listening to port 5000 in your console. Now head back to the CloudMQTT dashboard and verify that you have TWO connections. This means we've established a full connection, so it's time to start sending messages!
So we need to get the moisture to show up in our running server. We are currently taking a reading of our moisture every 10 seconds, and simply printing it out into the serial console, from our readMoisture function. We are going to rewire that function to publish to a topic called 'moisture' on the MQTT broker. Then we will make sure the web server is listening to that topic. We read the value in as a float, but we want to send it as a string. All we need to do in our readMoisture function is to do this conversion and use the client.publish function with our topic and message as arguments. The readMoisture function should look like this:
void readMoisture() {
uint16_t capread = ss.touchRead(0);
char msg[4] = "";
sprintf(msg, "%d", capread);
client.publish(PUB_TOPIC, msg);
}
Now try running your code on the NodeMCU, and monitor the console of your running server. You should see the moisture values being logged. Note that the moisture values may still show up as 65535, but remember this will be fixed once we increase the voltage.
Trigger the pump from the serverNow let's communicate in the other direction. We want to be able to control the pump from the server. We will accomplish this with the http endpoint '/water' on our server to which we can send a simple POST request. This will tell the web server that we want to trigger the pump for a certain amount of time. We will then send a message to the MQTT broker on the 'pump' topic. The message that we send will include the amount of time in seconds that we want the pump to run for.
In order to post to our web server, we will need a way to send a simple POST request. For this I recommend Postman. Make sure you're making a POST request, and use the following as a body for your request:
{
"seconds": 5
}
If we send this request, it will go to the server, which will publish to the MQTT broker. We previously set up our NodeMCU to subscribe to the 'pump' topic, so the message should be getting through and firing the callback function we defined. We also have a triggerPump function which we can re-use after some slight modification. So all we have to to is call the triggerPump function from the callback. We also need to read the message to get the number of seconds to trigger the pump for. Change the callback function to the following:
void callback(char* topic, byte* payload, unsigned int len) {
char value[5] = "";
for (int i = 0; i < len; i++) {
value[i] = (char)payload[i];
}
int delayMs = atoi(value);
triggerPump(delayMs);ccc
}
This converts the message to an integer value and calls triggerPump with that integer as an argument. Add the parameter to the triggerPump function:
void triggerPump(int delayMs) {
digitalWrite(PUMP_PIN, HIGH);
delay(delayMs);
digitalWrite(PUMP_PIN, LOW);
}
Run the code and test it out. Verify that you can make a POST to your server with a specified number of seconds, and that this triggers the pump to spin for that amount of time.
Wrap-upAnd there we go! We've established two-way communication between a web server and a device. You can unplug your NodeMCU from the computer and plug it into your higher voltage power supply (7 - 8V) to get the moisture values reading correctly and get more power from your motor. Now put one end of the pump into a water source and affix the other to your favourite house plant. I just stood up a chopstick in the soil and fastened the pump tube using a twist tie. I also found a nice little garden-themed wooden box from a craft store as a little home.
Now you can communicate from anywhere in the world where you have an internet connection. If you have some web dev skills, you could create a web application running on top the web server, where you could have a button on a web page that triggers the POST to the web server. You could also direct the moisture calls into a database, and use the data to create a chart of the moisture over a period of time.
Comments