CTA Bus Tracking with the Raspberry Pi

CTA Bus Tracking with the Raspberry Pi

I've never been much of a morning person; some days, the snooze button on my alarm clock works harder than I do. I also like to take public transit to work as often as possible. An unfortunate side effect of my erratic waking habits is that an extra 30 second delay in leaving sometimes results in missing the bus - and arriving (another) 10 minutes late to the office. Recently, I pulled my Raspberry Pi out of storage and picked up a cheap TFT display and set out to cure my morning woes.

Project Goals

My goal for this project was to create a simple dashboard that, at a quick glance, could indicate how urgently I needed to leave my apartment and get down to the bus stop. The 3.5" TFT display is quite small, and even up close it can be somewhat difficult to read text, so I decided to use a basic color coding scheme instead of displaying the number of minutes left. As an added benefit, this style of display would still be perfectly readable even without my contacts in or glasses on - a common theme of a rushed morning.

The hardware platform needed to have a composite video output as well as web access to get real-time bus arrival information from the online data source. To speed up development, I wanted to use a device that bundled these features together and supported programming environments with GUI building tools. Developing in Python on a Raspberry Pi fit all of these requirements.

For those who aren't familiar with the Raspberry Pi platform, I would highly recommend buying one and experimenting with it. In essence, it is a very low-cost ($25 and $35 for the available models), credit-card sized computer equipped with USB, HDMI, and Ethernet capability. It has no on-board memory, but instead boots from an SD card, which makes swapping between different operating systems and applications incredibly simple. It is fast enough to output High-Definition video and audio (many people use it as a cheap home theater PC running XBMC) and also has a prototyping area with General Purpose I/O pins that can be accessed directly by application software. In short, it's a hacker's dream and perfect for this project.

For the Operating System, I decided to go with one of the most popular choices, Raspbian. Raspbian is a port of the popular Debian Wheezy OS, and has support for a tremendous variety of software packages and programming languages. To get started, I prepared my SD card with the latest version of Raspbian, configured my machine for remote access via openssh, and then set up X11 forwarding to view GUI windows remotely from puTTY. Time to start development!

Creating a Skeleton GUI Application

Although we're not creating a very complex GUI application, I've always found that creating a skeleton application is a great starting point for simple projects. Functionality can be added incrementally, as opposed to coding everything up front, and then debugging later.

There is a wide variety of GUI toolkits available that play nicely with the Raspberry Pi. I quickly decided to use the Tk Framework, partially because I've used it successfully in the past, and partially because there is a Python package that interfaces with Tk called TkInter that is well-documented and easy to use.

The code for a basic Python/TkInter window with a programmatically settable background color is criminally simple:

from tkinter import *

if __name__ == '__main__':
    root = Tk()
    root.configure(background='red')
    root.geometry("640x480")
    root.mainloop()

The application creates a Tk window called root that has a background color of red and is sized to 640x480 to match the resolution of the TFT display. The code above is targeted for Python 3.3. If Python 2.7 is used, the first import line will throw an exception. The TkInter package name changed between these versions. To resolve the error, use the following instead for the import line:

from Tkinter import *

With the skeleton project working, it's time to color code the background based on actual bus arrival times.

Bus Arrival Prediction

The Chicago Transit Authority (CTA), which is the mass transit operator in Chicago, has a great developer API that provides access to real-time bus and train data. Some time back, I applied for a developer API key on a whim and checked out the capabilities of the system. It turns out that the available data matches nicely with the requirements for this project. Every CTA bus has an on-board device that communicates time-synchronized location data back to a central service every minute, and that central service provides estimates of arrival times for each bus, as well as notifications of delays and special service bulletins.

As a bonus, the API is very easy to use. To receive, for example, bus arrival predictions for bus stop 6013 (the southbound Ashland/#9 bus at Webster, which one might use to get to the Clybourn Metra train station from the DMC office), you make the following HTTP GET:

http://www.ctabustracker.com/bustime/api/v1/getpredictions?key=[APIKEY]&stpid=6013

I've replaced my API key with [APIKEY], but with a valid one inserted, the response will be a well-formed XML document with a schema that can be obtained from the CTA Developer documentation. Below is the document tree as displayed in Firefox.

 

I found a very handy Python library called Requests that wraps the built-in python web libraries and dramatically simplifies HTTP functionality. Using this library, I only needed a single line to make this GET and retrieve the XML data:

r = requests.get("http://www.ctabustracker.com/bustime/api/v1/getpredictions?key=" + apiKey + "&rt=" + homeRt + "&stpid=" + homeStpid

To make the request easier to read, I created global string variables in the Python program that contain the HTTP GET arguments and are concatenated together to form the entire URL. The structure contains the content of the response as well as a host of other information that we don't need for the first iteration of this application.

Since the response from the CTA web API is a well-formed XML document, I needed an XML parser to extract out the arrival time of the next bus from the bus stop.  Python includes a package called ElementTree which can read an XML document into memory and programmatically read/write attributes and node content.

The XML response for the getpredictions API query typically returns multiple buses and predictions. Our application, however, really only cares about the bus that will arrive the soonest. So, the strategy when parsing this XML document will be the following:

  1. Check to make sure the API hasn't reported an error (the schema specifies how an error will be reported, so this can be done reliably)
  2. Check to make sure at least one prediction has been reported
  3. Get the predicted arrival time and current system time from the bus that will arrive the soonest
  4. Calculate the amount of time, in minutes, until the next arrival

The code for this parsing function looks like the snippet below. Note that I'm using the DateTime package to handle the time interval calculations without reinventing the wheel.

def parseXmlResponse(resp):
    treeRoot = ET.fromstring(resp)
    predList = treeRoot.findall('prd')

    # Check for an error response first
    if (treeRoot.find('error') == "None") or (len(predList) == 0):
        print("CTA API Reported Error")
        root.configure(background='gray')
        return -1
    else:
        # Get the time difference in minutes
        currTime = predList[0].find('tmstmp').text
        predTime = predList[0].find('prdtm').text
        t1 = datetime.datetime.strptime(currTime.split(' ')[1],'%H:%M')
        t2 = datetime.datetime.strptime(predTime.split(' ')[1],'%H:%M')
        arrMin = (t2 - t1).seconds / 60.0

        return arrMin

Now, all we have to do is change the background color of the window based on the number of minutes left until the bus arrives. I added the following code just before the return statement of the parseXmlResponse function:

        # Color the background appropriately depending on
        # the amount of time left before the bus arrives
        if (arrMin >= 8) or (arrMin <= 2):
            root.configure(background='red')
        elif arrMin < 5:
            root.configure(background='yellow')
        else:
            root.configure(background='green')

Automatic Updates

Despite all of the code we've implemented so far to get the predicted times from the CTA web API and shade the background of the screen accordingly, at the moment, nothing will ever kick off the updates :-(

Luckily, there is a lot of support in Python for threading, and we can utilize this functionality to create a thread that runs the update code on a regular interval.

The first thing we need to do is create a class that inherits from the Threading.Thread class and knows which function to call when the thread is started. I have called the thread TimeUpdater, and the code looks like the following:

class TimeUpdater(threading.Thread):
    def __init__(self, updateFn):
        threading.Thread.__init__(self)
        self.runnable = updateFn

    def run(self):
        self.runnable()

The class is simple. Now, we need to write the code that will be executed at the given interval. The strategy I used was to create a function with an infinite loop of the following:

  1. Query the CTA API
  2. Parse the response XML and update background color
  3. (Optional) print some debug information to the console
  4. Sleep for the update interval time

The code looks like the following:

def updateArrivalTime():
    while True:
        r = requests.get("http://www.ctabustracker.com/bustime/api/v1/getpredictions?key=" +
                         apiKey + "&rt=" + homeRt + "&stpid=" + homeStpid)
        parseXmlResponse(r.content)
        print('Data Updated at ' + str(time.time()))
        time.sleep(updateInterval)

Only one more step remains: we have to start the update thread when the application is started. I created a function to handle this and called it just before the call to the root window's mainloop function:

def startUpdateThread():
    updateThread = TimeUpdater(updateArrivalTime)
    updateThread.start()

Optimization

What we've got here is.... an application that fits all of the design objectives that we started with. However, there was one thing that really bothered me throughout the development of this Python code: hard-coded data. The API Key, home bus stop number, update interval, time delta thresholds for the background colors, etc.. they're all coded as magic numbers. Even with descriptive variable names, I don't like the idea of needing to edit the application source in order to change the user's current location and display preferences.

With a little searching, I was able to discover the ConfigParser library, which is bundled along with Python. This module handles the import and parsing of config files that can be bundled with the Python application. When I originally set up my Raspberry Pi, I enabled file transfer over ssh using sshfs. Using this, I can update the configuration of the dashboard application over the web without ever plugging a keyboard into the Pi.

Let's first take a look at the structure of the configuration file that ConfigParser will look for. I (somewhat arbitrarily) separated the parameters into two subheadings: Tracker Settings and Color Settings.

[Tracker Settings]
api-key = APIKEY
home-stpid = 6013
home-rt    = 9
update-interval = 30.0

[Color Settings]
too-fast = 2.0
too-slow = 8.0
just-right = 5.0

The ConfigParser module reads in the entire config file and then can be searched explicitly for parameters in specific headings. So, for example, to get the update interval of the application, I can execute the following line:

updateInterval = config.getfloat('Tracker Settings', 'update-interval')

It didn't take much time at all to get this working in my application, and I was able to do a little refactoring as well. I created a function called startApp that reads in the relevant config parameters and kicks off the update thread. This is called immediately after the application starts as before.

Show Me the Money!

With the application fully coded, it's time to test it out on the Raspberry Pi! If you're interested in taking a look at the full code for this project, you can find it at the DMC GitHub account. Below is a video I took of the application running alongside the official CTA bus tracker web application to confirm that the data is updating correctly and in real-time (now with 40% more props!). Now I'll never be late for another bus again!

Future Improvements

Like any hobby project, there never seems to be any end to the improvements that can be made. One big improvement that I need to make is to automatically launch the application in a graphical window every time the Raspberry Pi starts. I experimented with this using cron, which is a task scheduler application that exists on Unix-based operating systems (including Raspbian). However, the sticker for me is that you need to start up the X-Window environment first before launching an application that contains a GUI. I'm sure someone has solved this issue before, and the solution is documented somewhere online.

The other thing I'd like to do is include a display that indicates what the weather will be like during my transit hours (7-9am and 5-7pm). There are many data sources available online, and adding this functionality shouldn't require any major new components, since we're already doing web API requests, response parsing, and graphical updates!

Eventually, I would like to configure this application entirely through the web by hosting a RESTful API on the Raspberry Pi and connecting to it on a web browser. This would probably replace my preferences file and require the addition of a database to store data persistently, although I could take the easy road and edit the configuration file as the preferences are updated. Decisions, decisions!

Once I get a chance to follow through with these improvements, I'll update everyone here on the DMC blog. Until then, I highly recommend checking out the Raspberry Pi on your own. Happy hacking!

Comments

There are currently no comments, be the first to post one.

Post a comment

Name (required)

Email (required)

CAPTCHA image
Enter the code shown above:

Copyright 2014 DMC, Inc. | All Rights Reserved Trademarks