Monday, November 15, 2021

Make an ESP8266 WiFi Temperature Sensor & Python Flask Backend - Part 2

In the previous article, I used an example sketch to read the temperature sensor, and another example sketch to connect to WiFi and POST data to an API that I still have to write. 

Once I build out this basic example capability, I'll incorporate it into the temperature example and refactor the backend to receive temperature data.

As you can probably guess, next is to write a prototype Flask app, then I'll add the code to receive example data from our ESP8266. Please continue reading...

Flask Backend

Whenever I develop Flask applications I like to follow best practices and compartmentalize the app's dependencies using virtualenv and direnv. The former creates a Python installation specific to your project. The latter is a convenient way to automatically switch to that installation when working in the directory.

Set Up The Environment

I'm assuming you already created a directory for your project (mine is temp8266). Create a new virtual python environment in the env subdirectory via direnv -p python3 env

You can now install python dependencies with pip and capture those dependencies into a requirements.txt file using pip --freeze. Since I already did all that for you, just create a requirements.txt file containing:

click==8.0.3
dataclasses==0.8
Flask==2.0.2
importlib-metadata==4.8.1
itsdangerous==2.0.1
Jinja2==3.0.2
MarkupSafe==2.0.1
typing-extensions==3.10.0.2
Werkzeug==2.0.2
zipp==3.6.0

And then run the command pip install -r requirements.txt which will install the packages specified in that text file.

Now create a .envrc file containing the following lines:

source env/bin/activate
export FLASK_APP="app"
export FLASK_ENV="development"
export FLASK_DEBUG=1
export SERVER_NAME="0.0.0.0:5000"

The first line sources a virtualenv shell script that switches environment variables around to use the virtual python environment. The remaining lines will be used later to run our Flask app.

Once the .envrc is created, type direnv allow to confirm that the changes made to .envrc were made by you. Anytime you cd into the directory, direnv will run the .envrc script and set up your virtual python environment. When you cd out of the directory, it will restore your previous environment.

Now that the environment is set up, it's time to create our simple Flask app.

Simple Flask App

All we need to display a simple message is a file, app.py, containing:

import os
from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def index():
return "Hello world"
if __name__ == '__main__':
print("\nStarting...")
app.run(host='0.0.0.0')

You program a Flask app by defining functions that handle requests for different URLs, called routes. The root URL is handled by index(). It simply responds with "Hello World".

To start the webserver, type python app.py at the shell prompt and you should see:

 * Serving Flask app 'app' (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://192.168.1.15:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 995-911-114


Now point your browser to http://127.0.0.1:5000/ (or
http://localhost:5000/) and you should see "Hello World" displayed. Flask will output the following (ignore the favicon.ico request):

127.0.0.1 - - [12/Nov/2021 10:58:57] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [12/Nov/2021 10:58:57] "GET /favicon.ico HTTP/1.1" 404 -

Here's my source code so far

Stop the flask process (CTRL/C). Now it's time to add an API endpoint.

Add an API Endpoint

Before we add another endpoint, we need to know what data our Huzzah is posting, exactly.

Let's say your SERVER_IP is set to "192.168.0.1". The example sketch we installed will post the following JSON to http://192.168.0.1/postplain:

{ "hello": "world" } 

The Flask route we need to handle is /postplain

So let's add a route to receive the data and print it out. Add this route function after index():

@app.route('/postplain', methods=["POST"])
def postplain():
data = request.json
print(data)
return "Success"

We'll need to import request from flask.

from flask import Flask, request

Type python app.py

at the prompt and your app will run. Make sure your ESP8266 is still connected to your PC and running and open the Serial Monitor. You may be disappointed to see 404 errors still showing up. What gives?

Well, our Flask app is listening to connections on port 5000/tcp, but our ESP8266 HttpPostClient example didn't specify a port, so it is still trying to talk to port 80.

Pointing ESP8266 to Port 5000

We'll edit the HttpPostClient code to communicate with our host on port 5000. Change this line:

#define SERVER_IP "192.168.1.15"

to this:

#define SERVER_IP "192.168.1.15:5000"

Upload your sketch and restart, open your Serial Monitor. Use EspTouch to configure the ESP8266 again, and ...wait, what?!

.....................................................................
Connected! IP address: 192.168.1.76
[HTTP] begin...
[HTTP] POST...
[HTTP] POST... code: 404

Still getting a 404? Over in the Flask app we see:

192.168.1.76 - - [12/Nov/2021 11:19:24] "POST /postplain/ HTTP/1.1" 404 -

Interesting. Why is there a trailing slash (/) in the URL? Let's take a look at the ESP8266 code again. 

There it is:

http.begin(client, "http://" SERVER_IP "/postplain/"); //HTTP

Remove the trailing slash (red), upload the sketch, restart, reconfigure with EspTouch and then... et voila! Serial Monitor shows:

[HTTP] POST... code: 200
received payload:
<<
Success
>>

Flask app shows:

192.168.1.76 - - [12/Nov/2021 11:26:22] "POST /postplain HTTP/1.1" 200 -
{'hello': 'world'}

It works! Our ESP8266 is connecting to our newly written Flask backend app and sending the example JSON data.

This is all the code so far

Hopefully you can see that this iterative, baby-steps, bite-at-a-time approach works really well. When something goes wrong it is easy to isolate the cause. Also, the changes I commit to the repo are relatively small and easy to review, or easy to undo.

Next time I'll incorporate the HttpPostClient example into the temperature example and refactor the backend API to receive temperature data.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.