Homebrew Oxontime RTI display with Raspberry Pi

Update: As of 2016-03-31 the Oxontime service has changed provider and the API is no longer functional. Presumably a new API will be made available at some point. This post remains for posterity.

The Oxontime Real Time Information (RTI) system provides real time predictions for bus departures in Oxfordshire, which are displayed on screens at many bus stops. The data is also available via an API. I thought it would be fun to use an Adafruit 16x2 LCD display and a Raspberry Pi to make my own personal display (photo below).

The LCD display ships as a kit so you'll need to do some soldering before you can use it. There are assembly instructions available on the Adafruit website on how to do this. These instructions also include how to install the RPi.GPIO and Adafruit_CharLCD modules required to control the display. This was my first time soldering something and I found the common soldering problems page on the Adafruit website very helpful when things weren't working.

The first step is to find the "system code number" (SCN) for the stop you want to download the departures for. This is not the same as the SMS number displayed at the stops. You can use the script below to print out the SCN, SMS number and name of all of the available stops.

import requests
import json

def download_markers():
    headers = {'Content-type': 'application/json'}
    url = 'http://www.oxontime.com/MapWebService.asmx/GetMarker'
    data = {
        "Layers": "naptanbus",
        "DateType": 0,
        "FromDate": "",
        "ToDate": "",
        "ZoomLevel": "3.0",
        "Easting": 402451,
        "Northing": 149757,
        "EastingEnd": 530431,
        "NorthingEnd": 263157,
        "IsLonLat": False}
    response = requests.post(url=url, data=json.dumps(data), headers=headers)
    response_json = json.loads(response.text)['d']
    return response_json

def parse_markers(markers):
    for marker in markers:
        for cluster in marker['Clusters']:
            for marker in cluster['Markers']:
                scn, name, sms = marker['Summary'][0:3]
                yield(scn, name, sms)

if __name__ == '__main__':
    markers = download_markers()
    for scn, name, sms in parse_markers(markers):
        print('{} {} {}'.format(scn, sms, name))

This script will print the SCN, SMS number and name for all available stops in the format below:

300000037G 68423286 Market Place
300000037VR 68438743 Valley Road
300000037JC 68439374 Jarvis Court

We can use grep and the SMS number of the stop to find the corresponding SCN. For example, the SMS number of one of the stops on St Clements Street is 69323265, which has an SCN of "340001126YOR". Some trial an error may be reqired, as stops on opposite sides of a street will often have the same name.

python markers.py > markers.txt
grep 69323265 markers.txt

The next step is to use the SCN to download the departures information, and display it on the LCD screen. This is done using the script below:

import time
import requests
import json

def download_departures(scn):
    data = {'SystemCodeNumber': scn}
    headers = {'Content-type': 'application/json'}
    url = 'http://www.oxontime.com/MapWebService.asmx/GetDepartures'
    response = requests.post(url=url, data=json.dumps(data), headers=headers)
    response_json = json.loads(response.text)['d']
    return response_json

def parse_departures(departures):
    for departure in departures:
        service = departure['Service']
        if service is not None:
            destination = departure['Destination'].replace(' ', ' ')
            time = departure['Time'].replace(' ', ' ')
            yield({'service': service, 'destination': destination, 'time': time})
    return

def format_departures_for_lcd(departures, display='time'):
    text = ''
    for departure in departures[0:2]:
        right = departure[display][0:(15-len(departure['service']))]
        text += '{service:{w}}{r}\n'.format(w=(16-len(right)), r=right, **departure)
    return text

if __name__ == '__main__':
    import sys
    scn = sys.argv[-1]  # get scn from command line arguments
    import Adafruit_CharLCD as LCD
    lcd = LCD.Adafruit_CharLCDPlate()
    lcd.set_color(1,1,1)
    lcd.clear()
    try:
        while True:
            # download the departures information
            data = download_departures(scn)
            departures = list(parse_departures(data))
            for n in range(0, 8):
                if n % 2 == 0:
                    text = format_departures_for_lcd(departures, 'time')
                    wait = 10
                else:
                    text = format_departures_for_lcd(departures, 'destination')
                    wait = 5
                # update the display
                lcd.clear()
                lcd.message(text)
                time.sleep(wait)
    except KeyboardInterrupt:
        pass
    finally:
        # don't forget to turn the display off!
        lcd.clear()
        lcd.set_color(0,0,0)

The download_departures function downloads the departures information for the stop requested, returning the raw JSON data. The parse_departures function parses the data to make it easier to work with. The format_departures_for_lcd function constructs a 16 column by 2 row string, ready to be shown on the LCD display. Finally, the functions above are tied together in a loop that displays the times for 10 seconds, then the destinations for 5 seconds. In this example the departures data is only downloaded once every 60 seconds; don't abuse the service by accessing it more than is needed!

Don't forget that to use the Adafruit_CharLCD module you will need to run Python as superuser:

sudo python oxontime.py 340001126YOR