Python Web UI with Tornado

Evan Boldt's picture

Introduction

So, you want a GUI for your robot. Sure, you could make a nice GUI in Python with Tkinter, but there are some really good reasons to try it as a website instead:

  • A web GUI can be accessed from almost any location
  • A well-designed web GUI can be used on almost any device like a tablet or a phone
  • HTML, CSS, and Javascript are well documented, powerful, and very flexible
  • HTML can be easily made to look very nice

There are some drawbacks to implementing your GUI as a website though:

  • It is only able to start communication one-way (browser to server, server responds)
    • Great for buttons and other input
    • Requres continuous polling or a commet to get data to be pushed to the browser
  • It adds another "layer" of complexity to your code
    • Several different languages (C++ on Arduino, Python Server, and HTML+CSS+Javascript in browser)
    • The server has to relay information from the browser to the robot

Installing Python Tornado

I used the Tornado web server for my projects. It is better than just using CGI, since the environment is always loaded while the server is running instead of loading it every time a request is made. CGI would be problematic then, since serial.begin would be run every time a request was made, which simply wouldn't work since the Arduino would also restart all the time.

On Ubuntu, the installation is as easy as:

sudo apt-get install python-tornado python-serial

Using Tornado with the Serial Library

So one of the challenges with this is that reading serial input in python is usually blocking, so the webserver becomes unresponsive to browsers' requests while waiting for serial input.

#! /usr/bin/python2.7
import os
import json
import tornado.ioloop
import tornado.web
from serial import *


tornadoPort = 8888
cwd = os.getcwd() # used by static file server


# Make a Serial object
serialPort = '/dev/ttyACM0'
serialBaud = 9600
ser = Serial( serialPort, serialBaud, timeout=0, writeTimeout=0 )



# gets serial input in a non-blocking way
serialPending = ''
def checkSerial():
    try:
        s = ser.read( ser.inWaiting() )
    except:
        print("Error reading from %s " % serialPort )
        return
    
    if len(s):
        serialPending += s
        paseSerial()
    

#called whenever there is new input to check
serialHistory = ''
mostRecentLine = ''
def parseSerial():
    split = serialPending.split("\r\n")
    if len( split ) > 1:
        for line in split[0:-1]:
            print( line )
            
            #do some stuff with the line, if necessary
            #example:
            mostRecentLine = line
            # in this example, status will show the most recent line

            serialHistory += line

        pending = split[-1]


# send the index file
class IndexHandler(tornado.web.RequestHandler):
    def get(self, url = '/'):
        self.render('index.html')
    def post(self, url ='/'):
        self.render('index.html')


# handle commands sent from the web browser
class CommandHandler(tornado.web.RequestHandler):
    #both GET and POST requests have the same responses
    def get(self, url = '/'):
        print "get"
        self.handleRequest()
        
    def post(self, url = '/'):
        print 'post'
        self.handleRequest()
    
    # handle both GET and POST requests with the same function
    def handleRequest( self ):
        # is op to decide what kind of command is being sent
        op = self.get_argument('op',None)
        
        #received a "checkup" operation command from the browser:
        if op == "checkup":
            #make a dictionary
            status = {"server": True, "mostRecentSerial": mostRecentLine }
            #turn it to JSON and send it to the browser
            self.write( json.dumps(status) )
        
        #operation was not one of the ones that we know how to handle
        else:
            print op
            print self.request
            raise tornado.web.HTTPError(404, "Missing argument 'op' or not recognized")


# adds event handlers for commands and file requests
application = tornado.web.Application([
    #all commands are sent to http://*:port/com
    #each command is differentiated by the "op" (operation) JSON parameter
    (r"/(com.*)", CommandHandler ),
    (r"/", IndexHandler),
    (r"/(index\.html)", tornado.web.StaticFileHandler,{"path": cwd}),
    (r"/(.*\.png)", tornado.web.StaticFileHandler,{"path": cwd }),
    (r"/(.*\.jpg)", tornado.web.StaticFileHandler,{"path": cwd }),
    (r"/(.*\.js)", tornado.web.StaticFileHandler,{"path": cwd }),
    (r"/(.*\.css)", tornado.web.StaticFileHandler,{"path": cwd }),
])


if __name__ == "__main__":
    
    #tell tornado to run checkSerial every 10ms
    serial_loop = tornado.ioloop.PeriodicCallback(checkSerial, 10)
    serial_loop.start()
    
    #start tornado
    application.listen(tornadoPort)
    print("Starting server on port number %i..." % tornadoPort )
    print("Open at http://127.0.0.1:%i/index.html" % tornadoPort )
    tornado.ioloop.IOLoop.instance().start()

Setting up the Browser

To make it easy to send requests to the server, it's really a good idea to use jquery, and the easiest way to use jquery is to use the Google API for it. To add jquery to the webpage, just add this script:

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script> 

So to start, we'll just have a simple webpage that has a div. When the div is clicked, jquery will ask the status of the tornado webserver.

<!DOCTYPE html>
<html>
<head>
<title>Python Tornado Test Page</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script>
function serverResponded( data ) {
    /* 
    log the event data, so you can see what's going on.
    Shows up in the console on your browser. (Chrome: Tools > Developer Tools > Console)
    */
    console.log( data );
    
    // check the server status, and report it on the screen
    if ( data.server === true ) {
        $('#status .value').html("OK");
    }
    else {
        $('#status .value').html("NOT OK");
    }
    
    // add the last serial to the div on the screen
    $('#serial .value').html( data.mostRecentSerial );
}

$(document).ready( function() {
    /* handle the click event on the clickme */
    $('#clickme').click( function() {
        params = { op: "checkup" };
        $.getJSON( 'http://localhost:8888/com' , params, serverResponded );
    });
});
</script>
</head>
<body>
    <div id="clickme" style="cursor: pointer;">CLICK ME</div>
    
    <div id="status">
        Server Status: <span class="value">?</span>
    </div>
    
    <div id="serial">
        Last Serial Input: <span class="value"></span>
    </div>
</body>