IoT Weight Logging Scale

Pages
Contributors: SFUptownMaker
Favorited Favorite 8

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.