Last time, we combined our example sketches into a functioning temperature sensor and refactored the backend to receive temperature data. This time, let's get basic plotting working. To do that, we need to:
- Save temperature data on the backend
- Implement an API for getting that data
Here's our familiar diagram; we'll focus on the items marked with an orange star.
Save Temperature Data
Eventually you'd want to save temperature data to a database. To keep things simple, I'm simply going to log the data to a file in CSV (comma-separated) format. Each entry will include temperature (C), a timestamp, the units of measurement ("C"), and the sender's IP address.
Logging the data is simply a matter of appending the data to a local file each time it's posted to the API. To get the timestamp, we need to import datetime.
We also need a format string for the timestamp.
The timestamp will be in the form: YYYY-MM-DD hh:mm:ss
The IP address is available from Flask's request object via request.remote_addr. The code looks like this:
Once everything is running, our temp.log begins filling up with entries:
The next step is to get the backend API working to retrieve this data.
Backend GET API
We already have a route defined in our Flask app to POST data. We need another route to GET data. I've stubbed it out:
Reading The File
First, we open the file. One of the principles of software engineering is Don't Repeat Yourself (DRY). We could add to our new route the following open statement.
The problem is if we decide to change the log file, we have to do it in two places: here and in our temp_post() route. Instead, following DRY, make a single variable for the logfile name
and use that in both routes like this:
Now we read the lines out of the file into an array using readlines().
Converting to JSON
Our API is supposed to return JSON data. We'll return an array of JSON objects that look like this:
We can either construct the JSON by creating an array of formatted strings, or we can create a data structure and use jsonify(). The former is preferable, because we're not reinventing the wheel with one more opportunity to goof up the format.
We'll loop through the lines of data, converting each to a dict and adding it to an list. Then we'll convert the list of dicts JSON and return the text.
We need to import jsonify.
With that, I came up with the following route function:
Note that python reads in the newline (\n) for each line so we use rstrip() to remove it.
When I visit the endpoint with the browser, I see properly formatted JSON data in the form of an array of objects with the expected keys and values:
Flask allows you to serve static pages for a given route, or to render a page template, server-side. We'll use the approach.
First of all, let's create a subdirectory for our static content called static.
We'll create a boilerplate index.html with an H1 header.
To return this page, instead of "hello world", for the "/" route in our Flask app, have to configure the static_dir for app when our app is instantiated.
In our "/" route, we change the return statement, to call the send_static_file() method on app.
Testing with your browser shows it works!
I created plot.js in the static directory and populated it with the above chart object.
So let's obtain the data from our API that we made earlier. I'm going to use jQuery to call the API asynchronously. If you're not familiar with the concept, basically the query runs in a thread and when it's done, it executes the code you specify.
Next, in plot.js, we add the code to perform an asynchronous get request against our API. The anonymous function simply logs the response data to the console.
When you visit the index page, you can view console output by pressing F12 to open developer tools. You can expand the data to see that, indeed, we're retrieving the JSON data.
It took me awhile to sort it out, but Chart.js is looking for a line chart object that looks like this:
The data consists of a label for each data point, an array of data sets. Each data set consists of data points. We'll only have one data set, temperature from our single device.
First, import the necessary JChart scripts in our index.html, like so:
To begin working on the plotting, we need to add a canvas with id "myChart" to our html body below the H1 tags.
In jQuery we grab the 2d context for the canvas like this.
Except, if you just put this at the top of the script it won't work because it executes before the document is fully loaded. So we wrap our code in a $().ready() handler to ensure it runs after the document has been loaded.
With that working, adding chart.update() within the getJSON response handler makes the chart appear. Next, I populated the Chart object's labels. To keep it simple, I just numbered them 1..n.
Finally, I added each temperature entry to the Chart dataset. All together, the code looks like this:
Rather than plotting all the data, I decided to only plot the most recent 50 entries. I define a variable N, set it to 50, and then use slice(-N) on the array.
Et voila, it works!
Done For Now
Hopefully you've found this journey helpful if you're new to software engineering or some of these technologies. Or, at least, hopefully it was fun to follow along. Now you kind of get a general idea of how I approach these sorts of projects.
So what's next?
My plans to refine the project include accommodating multiple sensors, multiple types of measurements (temp, humidity, soil moisture, etc.). I'd like to use an off-the-shelf plotting app like ThingsBoard. The ESP8266 configuration needs some improvements as well.
Stay tuned for further updates.