With IoT (Internet of Things) on the rise and hardware getting cheaper and cheaper, it’s a great time to explore the possibilities this new technology provides. In this tutorial, I will show you how to create your own thermometer app using a NodeMcu microcontroller, a DHT22 temparature and humidity sensor and the Flask framework.
We will use the NodeMcu to gather sensor data from our DHT22 sensor and send it to a REST-API implemented in Flask. To read and display this data, we will create a simple HTML page which will download the data from our API:
You can check out the entire source code on my github page.
Requirements
- basic knowledge of Python and C
- NodeMcu
- Breadboard + Jumper wires + power adapter + 9V battery
- Arduino IDE
- Docker
You can buy all the hardware for a very cheap price on amazon or alibaba.
I went with the NodeMcu and Breadboard starter kit on amazon.
Designing the Microcontroller
Before we can write any code, we have to combine all the components required for the thermometer. We will connect our NodeMcu with the DHT22 sensor and power both components using a simple power supply.
Let’s start by (literally) wiring up our new hardware. First, grab an empty breadboard and put the power adapter on it:
Now, connect it to a battery using the power adapter. Before you connect the NodeMcu, make sure both switches that control the voltage are set to 3.3V, otherwise, you might fry the poor NodeMcu! Make sure everything works by pressing the power button:
Now it’s time to put the NodeMcu on the board. Since my boards are very small, I had to add a second one to fit the NodeMcu comfortably:
Time to connect our NodeMcu with the power supply. Again, make sure the power supply is set to 3.3V. We will use the negative pole of our power supply as GND (Ground) pin, so let’s use our jumper cables to connect one of the G pins of the NodeMcu to the negative pole. The 3V pin should then go to the positive pole of the power supply:
To make sure the power supply is connected correctly, you can flash the NodeMcu with a program to blink the LED. I will explain how you can overwrite the NodeMcu code later.
Finally, we can connect the DHT22 temperature sensor. As you can see in the DHT22 pinout, we have to connect the first pin to the positive terminal and the last pin to GND. The second pin is the data pin which must be connected to the NodeMcu for it to read the sensor data. You can use any Data pin on the NodeMcu for this, just make sure to remember which one you chose so you can easily program it later. I`ve chosen D2 as data pin.
This concludes our trip into the hardware tinkering world for now. Put aside the board(s) and let’s start writing some C code to give life to our newly created Microcontroller.
Programming the NodeMcu
There are many ways to write software for microcontrollers, but to keep things simple, I decided to stick with C. If you haven’t written code for microcontrollers like a NodeMcu before, this is the most straightforward way. We will use the Arduino IDE to write the code for our NodeMcu and then flash it.
Let’s start by importing all the libraries we will need:
#include <SimpleDHT.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
To use these libraries, go to Tools -> Library manager and search for and install the SimplDHT and Adafruit libraries. If you haven`t worked with Arduino before, I recommend you check out some tutorials to get started.
Once you feel comfortable with the IDE, let’s continue by setting up our Wifi connection:
const char* ssid = "***";
const char* password = "***";
void setup_wifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.println("Connecting...");
}
}
void setup() {
Serial.begin(115200);
setup_wifi();
}
void loop() {
}
The setup
method will be called by the NodeMcu only once upon startup, so it’s the perfect place to set up our WiFi connection initially. I put the WiFi code into its own method so we can re-use it later in case of connection problems.
To see what our NodeMcu is doing during development time, I also set up a Serial connection. This way we can print errors to Arduino Serial Monitor. The WiFi code tries to establish a connection every 1000ms. Once it succeeds, the loop will end and our setup code will be done.
Flash the NodeMcu and open the Serial Monitor, you should be seeing something like this:
Now, let’s implement the loop
method, which will be called continuously by our NodeMCU while it’s activated:
void loop() {
int pinDHT22 = 4;
SimpleDHT22 dht22(pinDHT22);
float temperature = 0;
float humidity = 0;
int err = SimpleDHTErrSuccess;
if ((err = dht22.read2(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) {
Serial.print("Read DHT22 failed, err=");
Serial.println(err);
delay(2500);
return;
}
Serial.print("Sample OK: ");
Serial.print((float)temperature); Serial.print(" *C, ");
Serial.print((float)humidity); Serial.println(" RH%");
delay(2500);
Here, after declaring all required variables, we use the DHT22 library to read both the temperature and humidity from our sensor. If an error occurred, we wait for 2.5s and end the current iteration of the loop. Otherwise, we print the acquired data to the Serial port. Again, I added a 2.5s delay here. This is important because the DHT22 sensor only allows us to read data every 2s, so we have to give it some time after each request.
Flash the NodeMcu again and open the Serial monitor. The output should look similar to this:
This looks very nice already, but our goal is not to print the temperature data to the Serial monitor. Instead, we are going to send it to a Restful API which can store this data and then make it available for display by some client:
if (WiFi.status() != WL_CONNECTED) {
setup_wifi();
} else {
HTTPClient http;
http.begin("http://192.168.0.35:5000/therm/push");
http.addHeader("Content-Type", "text/plain");
int httpCode = http.POST(String(temperature));
http.end();
}
First, we have to check if we are still connected to our WiFi network. If not, we will simply call the setup_wifi()
method again. Once we have established a connection, we can use the ESP8266HTTPClient library to send the sensor data to our API, which will be hosted on http://192.168.0.35:5000
in my case.
As you can see, I’m only sending the temperature data for now, but you can easily extend this code and also send the humidity data if you want.
For now, nothing will change if you run this code. This is because we haven’t created our API yet. So let’s go ahead and do that.
If you got lost during any of the above steps, here is the full source code of our NodeMcu:
#include <SimpleDHT.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
const char* ssid = "***";
const char* password = "***";
void setup_wifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.println("Connecting...");
}
Serial.println("Connected successfully.");
}
void setup() {
pinMode(2, OUTPUT);
Serial.begin(115200);
setup_wifi();
}
void loop() {
int pinDHT22 = 4;
SimpleDHT22 dht22(pinDHT22);
float temperature = 0;
float humidity = 0;
int err = SimpleDHTErrSuccess;
if ((err = dht22.read2(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) {
Serial.print("Read DHT22 failed, err=");
Serial.println(err);
delay(2500);
return;
}
Serial.print("Sample OK: ");
Serial.print((float)temperature); Serial.print(" *C, ");
Serial.print((float)humidity); Serial.println(" RH%");
if (WiFi.status() != WL_CONNECTED) {
setup_wifi();
} else {
HTTPClient http;
http.begin("http://192.168.0.35:5000/therm/push");
http.addHeader("Content-Type", "text/plain");
int httpCode = http.POST(String(temperature));
http.end();
}
delay(2500);
}
Creating the Flask API
Now that our NodeMcu is running and happily sending data, let’s create an API to consume that data. Using Python and the Flask framework, we can set up a simple API with less than 10 lines of code:
#!flask/bin/python
from flask import Flask
from flask_cors import CORS
app = Flask(__name__, static_url_path='')
CORS(app)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
As you can see, I provided the static_url_path
parameter. We will need this later so Flask can find and serve our HTML file.
To give our API some power, we have to create a new controller for our thermometer API. For this, I created a new therm.py
file inside a new /controllers
directory:
from flask import Flask, Blueprint, request, jsonify
import flask
import redis
red = redis.StrictRedis.from_url('redis://redis:6379/0')
therm_controller = Blueprint('therm', 'therm', url_prefix='/therm')
def therm_stream():
pub_sub = red.pubsub()
pub_sub.subscribe('therm')
for message in pub_sub.listen():
if isinstance(message['data'], (bytes, bytearray)):
result = message['data'].decode('utf-8')
yield 'data: %s\n\n' % result
@therm_controller.route('/push', methods=['POST'])
def post():
message = flask.request.data
red.publish('therm', message)
return flask.Response(status=204)
@therm_controller.route('/stream')
def stream():
return flask.Response(therm_stream(), mimetype="text/event-stream")
As you can see, we are using Flask blueprints here to make it easier to expose this new controller to our app.py
file.
Our NodeMcu will use the post
method with the '/push'
route. This method reads the request data which contains the sensor data from our NodeMcu and stores it in a Redis database defined above.
The stream
method will then subscribe to a push stream defined in the stream
method which is populated everytime the NodeMcu pushes data to the API. This way we avoid polling and can always display the latest temperature on our client.
therm_stream
is a simple helper function that listens to changes in our Redis database and decodes this data for display.
Since we have to listen to this stream continuously, this function is implemented as a generator.
To connect our new controller with our API, let’s include it in our app.py
file:
#!flask/bin/python
from flask import Flask
from flask_cors import CORS
from controllers.therm import therm_controller
app = Flask(__name__, static_url_path='')
app.register_blueprint(therm_controller)
CORS(app)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
Finally, we can implement a client to consume this API. Inside a new /static
directory, create a new index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>mcu Therm</title>
</head>
<body>
<h2 id="temp" />
</body>
</html>
<script>
function subscribe() {
var source = new EventSource('/therm/stream');
var tempDiv = document.getElementById('temp');
source.onmessage = function(e) {
tempDiv.innerHTML = "Temparature: " + e.data + "°C";
};
};
subscribe();
</script>
I placed the content inside a simple h2
element without any CSS to keep things simple. We can subscribe to our API with some simple Javascript calls using the EventSource
class. It’s onmessage
method is triggered whenever the stream is populated by the API. All we have to do then is update the innerHTML
of our content. As you can see I chose °C as the display unit (which is also what the NodeMcu sends). Feel free to add conversions to °F or Kelvin if you want.
We can download this new HTML page directly from our Flask API. To do so, we can define a new route in our app.py
file:
@app.route('/')
def root():
return app.send_static_file('index.html')
Since this file is also the entry point to our program, I named the route simply /
. The send_static_file
method will then take care of returning this HTML page to our client.
Running the App using Docker Compose
To easily spin up, run and deploy our new App, we are going to initialize all our services using Docker Compose. We only need two services: Redis and the Flask API. We can spin both of them up using this simple docker-compose.yml
file, placed in the root directory of the project:
version: '3'
services:
redis:
image: redis
command: redis-server /usr/local/etc/redis/redis.conf
container_name: redis
volumes:
- /redis.conf:/usr/local/etc/redis/redis.conf
ports:
- "6379:6379"
web:
build: ./src/webapp
working_dir: /var/www/app
ports:
- "5000:5000"
volumes:
- ./src/webapp:/var/www/app:rw
depends_on:
- redis
Make sure that the web
service runs only after redis
is initialized and check that all the ports used here match those used in the Python code. Now we can start our App using the docker-compose up
command. Once everything is spun up, open a new browser tab and go to http://localhost:5000/. Until we send some data to our Flask API, we will only see an empty page. So, let’s go ahead and turn on our microcontroller. After a few seconds, you should see something like this:
You can also see how this value changes from time to time. You can see what`s happening under the hood by opening a browser console and navigating to the Network tab:
Conclusion
That’s it, our homemade thermometer app is ready! Feel free to play around with the source code and deploy it wherever you want, for example on a Raspberry Pi with an external display. Let me know if you have any questions, hints or comments.