IoT Weight Logging Scale
Web Code
The web application is developed under Flask, which is a light web framework allowing Python applications to be deployed on the web. Here, we'll get into the directory structure and file structure of the Flask app. We're going to assume that you're going into this with a naive installation of Linux: perhaps a new Linode server, or a new Raspberry Pi. We're also going to assume that you have a console window open to whatever server you're going to use and that the server is connected to the Internet.
Install Flask
The first step is to install Flask. Simply type the following:
language:bash
sudo pip install flask
That should install flask automatically.
Install Tkinter and Matplotlib
You'll now need to install Matplotlib, so you can generate plots of your collected data. First, you need to make sure that Tkinter is installed. It probably is if you're working on any kind of server that is expected to have a desktop option (like a Raspberry Pi). In the case of something like Linode, where your primary interface is expected to be a command line, Tkinter may not be installed. Use the following command:
language:bash
sudo apt-get install python-tk
That will install Tkinter. Next we can install Matplotlib.
language:bash
sudo pip install matplotlib
That should install Matplotlib and all its dependencies.
Create the Directory Structure
The Flask app we're going to create has a directory structure that allows all the various pieces to be segregated into functional groups. Here's the structure you want to keep it neat and tidy:
website
└── app
├── images
└── templates
As we go through the rest of the tutorial, we'll populate these directories with the appropriate files. Note that the top-level directory (website
) can be located wherever you please and named, but I like to leave it in my home directory for ease of access. We're also going to assume that you followed this template for the rest of the tutorial, so it's to your benefit to do so.
Add the FLASK_APP Variable to Your Path
By adding the FLASK_APP variable to the path, we allow Flask to be run from a very simple command, which saves us time in the long run. Type:
language:bash
export FLASK_APP=~/website/website.py
This of course assumes that you created the directory named website
in your home directory. If not, change the path accordingly.
You probably want to add this line to your .bashrc file, so you don't have to type this command in every time you log into the server.
Create Your First Files
Unsurprisingly, our first file is going to be called website.py and is going to be located in the top-level website
directory. The contents of the file are very simple:
language:bash
from app import app
We'll create the app
class that we're importing in a minute.
Next, create the config.py file in the same place. This file is used to store site configuration information, but we'll only be storing one little piece of info in it.
language:python
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
The SECRET_KEY
is used by Flask to verify the signature of cookies to ensure that they haven't been tampered with by some third party. We won't make use of it, but it's good practice to set it up.
Create Your App Files
Now it's time to create the meaty parts of the project, the files that actually do something. These files will go in the app
directory.
Start with the __init__.py file. This is a super important file which allows us to put our actual app in the app
directory and have python treat it as a module that can be imported:
language:python
from flask import Flask
from config import Config
app = Flask(__name__)
app._static_folder = 'images'
app.config.from_object(Config)
from app import routes
We first import Flask
, which is (obvi) our overall Flask support object. From the config file, we created earlier, we import the Config
class, which stores configuration data for flask, although ours is tragically underutilized. We then create an object of the Flask
class named app
, which will be imported throughout the rest of the program. app._static_folder_
tells Flask where to expect static files (such as images) to be served from. "Static" is here somewhat of a misnomer, as the files in that directory can change, they just are expected to not be templates to be rendered. Lastly, we configure our Flask object with the Config
object we imported earlier.
Let's look at our routes.py file. It goes in the app
directory, and looks like this:
language:python
from flask import render_template, send_from_directory
from app import app
from weight import add_weight_point
from plot_weight import plot_weight
@app.route('/')
@app.route('/index')
def index():
return render_template('index.html', title='Home')
@app.route('/images/<path:path>')
def send_image(path):
return send_from_directory('images', path)
@app.route('/post_weight/<string:weight>')
def post_weight(weight):
add_weight_point(weight)
plot_weight()
return "weight posted"
The routes.py file is the secret sauce that allows our app to know what to do when a request comes in from a remote client. At the top, we import all the various bits and bobs that make our project work. We import render_template
and send_from_directory
from flask to support routes that we make for rendering templates and displaying images. We import the app
object that we created in __init__.py, and then the add_weight_point
and plot_weight
functions that we'll create in a little while.
The decorators @app.route('/')
and @app.route('/index')
tell the Flask app what to do when a request for either of those paths comes in. In this case, we would choose to render a template called index.html. We'll cover templates a little later, but for now, just know that this is the document that gives a webpage its appearance.
The @app.route('/images/<path:path>')
decorator is of particular interest because for the first time, we see a path with a variable in it. This route serves up any file requested from the images
directory. This is important because Flask isn't a full-featured web server, so it doesn't know what to do with requests that it has no route for.
The third and final route, @app.route('/post_weight/<string:weight>')
, is interesting because it's a purely server-side function. It does return a response (the string "weight posted"), but its primary function is to take the string passed to it and concatenate it to the weight file, then plot the data in the weight file.
Next, we create the supporting files, to which we outsource the recording of data points and the plotting of data on a chart. We'll look at weight.py first, which does the recording portion:
language:python
from datetime import datetime
import pytz
def add_weight_point(weight):
file_name = "weight"
try:
with open(file_name, 'a') as f:
f.write(weight + ", ")
tz = pytz.timezone('America/Denver')
local_now = datetime.now(tz)
dt_string = str(local_now.date()) + ' ' + str(local_now.time())
f.write(dt_string + "\n")
except:
print "Weight sad :-("
pass
if __name__ == "__main__":
add_weight_point("0.0")
We need datetime
and pytz
to create timezone-aware date and time values for when we post our data. The meat of the function is enclosed in a try/except
clause, mostly to make debugging easier during development, but it could prove helpful in deployment too. We open our file in append mode, which automatically places the cursor at the end of the file, then plop in the weight value (which was received as a string from the calling function, our route handler in the prior file) with a comma to provide separation for readability. We then create a timezone
object for my timezone ('America/Denver'), and fetch the local date and time (as a datetime
object). The local time and date are converted to strings and appended to the file. Finally, we provide a directive to how to handle the case where this file is called as the main file (i.e., run by itself, for test purposes): call the add_weight_point()
with a string of "0.0".
Finally, before you can run the application, you need to create the file that stores the weight data. This is easily done with this command:
touch weight
The final code file we create is plot_weight.py. This file uses Matplotlib to create a .png file of a graph of all the data in our weight
data file.
language:python
from datetime import datetime
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
def plot_weight():
file_name = "weight"
weight_data = []
date_data = []
with open(file_name, 'r') as f:
file_data = f.readlines()
for line in file_data:
line = line.strip().split(', ')
line = [line[0], datetime.strptime(line[1],
"%Y-%m-%d %H:%M:%S.%f")]
weight_data.append(float(line[0]))
date_data.append(line[1])
fig = plt.figure()
plt.plot(date_data, weight_data)
plt.xticks(rotation = 75)
fig.savefig('images/plot.png', bbox_inches='tight')
if __name__ == "__main__":
plot_weight()
The import section is fairly self explanatory, except for the line matplotlib.use("Agg")
. This line forces Matplotlib to ignore the fact that no X server is available for plotting. Without it, the Tkinter GUI library will throw an error when you try to plot, even if you're plotting to an output file. The rest of the file is pretty easy to understand: import the data file into a one-line-per-element list, then create two separate lists for weight data, one of float values and one of datetime
objects. It creates a pyplot figure, plots the data, rotates the labels on the x-axis so they don't overlap, and then saves the figure to an image.
Create the HTML Templates for Displaying Data
As mentioned previously, one of the beautiful things about Flask is that it can render templates for displaying pages. This means that you can have one template (say, base.html) that handles the page header and footer material while other pages render into that template.
We're going to start by placing a base.html file in our templates
directory. Here's the content of that file.
<html>
<head>
{% if title %}
<title>{{ title }} - A Website</title>
{% else %}
<title>A Website</title>
{% endif %}
{% block head_content %}{% endblock %}
</head>
<body style="background-color:#42dff4">
{% block body_content %}{% endblock %}
</body>
</html>
I'm not going to explain the html portions of this document, just the templating that makes it different. There are three templating concepts in play here: variables, if/else statements, and blocks. You can see that we create an if/else statement which checks the variable title
to see if it is defined. If it is defined, we set the title of the page accordingly. If not, we use a default title. If you scroll back up and look at our routes.py file, you can see that when we rendered that template, we passed in the title
variable, set to "Home". The other templating concept is that of a text "block". To illustrate how that works, I'm going to need to pull in the contents of our other html template, also created in the templates folder: index.html.
{% extends "base.html" %}
{% block head_content %}
{% endblock %}
{% block body_content %}
<img src="images/plot.png" alt="Plotted data" />
{% endblock %}
First, you can see the "extends" statement. That tells Flask that this file should be viewed through the lens of base.html, and that the two together create a single document. Now check out the block statements. The "head_content
" block is empty--nothing will be created there. However, in the "body_content
" block, there is a link to the image created by plot_weight.py. When Flask goes to render the index.html template, it will "draw" first base.html, then it will pull all the block content from index.html into the appropriate places in base.html
. You can see how this can be extremely powerful, allowing you to render websites in which 10% of the content is generated by a base template and the other 90% comes from specific subpages that you wish to display.
Deploy!
If you've been following along, your Linux server should be prepped and ready to have your website deployed! To launch your app all you have to do is run this command:
language:bash
flask run -h 0.0.0.0
That uses that FLASK_APP variable that we set up earlier to determine where the application to be launched is and launches it. The -h 0.0.0.0
switch tells it that you want the app to be visible to the outside world. If you're running this on a Raspberry Pi, it should be visible to any computer on the same network as the Raspberry Pi, but probably not the whole internet. If you're running on a Linode (or other cloud service) server, then you should be able to access the web page from anywhere.
There's a problem, though: once you disconnect your console from the server, the Flask app process gets killed and the page will no longer be accessible. To get around that, we use the "no hangup" command, telling the server to run the process in the background and not kill it when the console disconnects. To do this, enter the command thus:
language:bash
nohup flask run -h 0.0.0.0 &
Now the task will run in the background and you can disconnect from the server without affecting it. To terminate the process, you have to do a little more work. First, you must find the process id for your app. To do this, use the following command:
language:bash
pgrep flask
That will return the PID for the current running flask instance. To then stop that process, type:
language:bash
kill <PID>
Replace <PID>
with the process ID that you found in the previous step. That will stop the process. You'll need to stop and restart the process any time you make any changes to any of the files other than the image file.