Click here to Skip to main content
13,864,281 members
Click here to Skip to main content
Add your own
alternative version

Stats

3.2K views
7 bookmarked
Posted 13 Feb 2019
Licenced CPOL

WiFi thermometer using NodeMCU, DHT22 and Flask

, 13 Feb 2019
Rate this:
Please Sign up or sign in to vote.
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.

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> // read temperature data from our DHT22 sensor
#include <ESP8266WiFi.h> // control the NodeMcu
#include <ESP8266HTTPClient.h> // send http requests from the NodeMcu

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Philipp_Engelmann
Software Developer (Senior)
Germany Germany
Hi there 🙂
My name is Philipp Engelmann, I work as a web developer at FIO SYSTEMS AG in Leipzig. I am interested in C#, Python, (REST-)API-Design, software architecture, algorithms and AI. Check out my blog at https://cheesyprogrammer.com/

You may also be interested in...

Pro

Comments and Discussions

 
GeneralMy vote of 5 Pin
tbayart3hrs 45mins ago
membertbayart3hrs 45mins ago 
GeneralRe: My vote of 5 Pin
Philipp_Engelmann10mins ago
memberPhilipp_Engelmann10mins ago 
GeneralMy vote of 5 Pin
sanus14-Feb-19 21:08
membersanus14-Feb-19 21:08 
GeneralRe: My vote of 5 Pin
Philipp_Engelmann15-Feb-19 1:49
memberPhilipp_Engelmann15-Feb-19 1:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.190214.1 | Last Updated 13 Feb 2019
Article Copyright 2019 by Philipp_Engelmann
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid