Python Web UI with Tornado
Python Web UI with TornadoIntroduction
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>