Voyage: a better way to display train information.

The Problem: I commute by train. There are three stations near where I live that I can start from, and four in the city I commute to, where I can end my journey. National Rail Eqnquiries (nationalrail.co.uk) doesn’t offer any way for me to view all of these possible journeys at once, making deciding which station to go to first thing in the morning a massive pain. Especially when I’m still half asleep!

The solution: A low cost-at home display, using Python, a Raspberry Pi, and an e-ink display.

Background

In more normal, non-COVID times, I commuted every day from my home in Wigan, to central Manchester. Because I don’t like sitting in traffic jams, I travelled by train.

When I started doing this, back in 2015, it was a fairly reliable journey. But the realiability of the trains became worse and worse, making it necessary to check for delays and cancellations before every journey. For most, that wouldn’t be a problem but, for me, there are three possible starting stations:

  • Pemberton (a tiny, one train an hour station within walking distance of my house)
  • Wigan Wallgate
  • Wigan North Western

And, there are four possible destiantions:

  • Manchester Victoria
  • Manchester Oxford Road
  • Deansgate
  • Manchester Piccadilly

Because National Rail Enquiries (and RealTimeTrains, my site of choice for train information) don’t offer a way to view all of this information in a quick, simple, easy to understand way, I decided to create my own.

What to display, and how to display it?

There were three criteria that the display had to meet:

  1. No backlighting, so that I could have it in my bedroom to see when I wake up in the mornings.
  2. Easy to use with a Raspberry Pi Zero WH (preferably with Python), since that is what I had to hand.
  3. Capable of displaying at least two colours, in an easy to read format.

The InkyPHAT display from Pimeroni meets all of those requirements, as well as being excellent value at under £25, so that was my display of choice.

Initially, I’ve kept the display really simple. Displaying just the routes where services are running. If all is running according to the timetable, the display simply shows the CRS codes of the from and to stations in black.

If there are delays or cancellations on the route, the writing is instead displayed in red. This means I can see at a glance which routes have a good service, and so which station to catch my train from.

Getting the data.

There are various different data feeds for train running information. I won’t go into the pros and cons of each, because it’s complicated and not really relevant. In the end, I settled on using the “LDBWS” feed from National Rail, because it has all the data I want to display in one source. JPSingleton (GitHub) has provided an excellent JSON proxy for this data feed, which is easy to run on an Azure instance, and makes the National Rail data much simpler to work with, so that is what I have used to provide an HTTPS API endpoint from which I can fetch the data I need.

A simple python script running on the Pi queries my API, which then returns the data I need in JSON format:

def get (origin, destination, huxley):
    trains = [] # creates an empty list to store the data
    for o in origin: # iterates through each possible pair of origins and destinations from the lists passed to the function
        for d in destination:
            url = ('https://'+huxley+'/delays/'+o+'/to/'+d) # creates url to fetch the data
            #print (url)
            response = requests.get(url) # stores response as 'response'
            data = json.loads(response.text) # converts from json to text
            #Prettyprint data for debugging
            #print(json.dumps(data, indent=4, sort_keys=True))
            # append new data to 'data' as a list of dicts, tailoring what information is stored based on the delay status from Huxley
            if data['delays'] == True and data['totalDelayMinutes'] > 0:
                trains.append({"origin": o, "destination": d, "string": "Delays", "delay": 0, "delayMinutes": data["totalDelayMinutes"]})
            elif data['totalTrains'] == 0:
                trains.append({"origin": o, "destination": d, "string": "No Service", "delay": 2})
            else:
                trains.append({"origin": o, "destination": d, "string": "Good Service", "delay": 1})
    return trains

Another script, combined with boilerplate code from Pimoroni to run the InkyPHAT display, can then display the information:

def display (trains):
    inky_display.set_border(inky_display.WHITE)
    img = Image.new("P", (inky_display.WIDTH, inky_display.HEIGHT))
    draw = ImageDraw.Draw(img)

    i = 0
    row = i
    count = 0

    header = "Pem & Wigan to Manchester Stns" # This needs to be created dynamically in future releases

    w, h = font_header.getsize(header) # works out width of header so it can be centred
    draw.text((((inky_display.WIDTH / 2) - (w / 2)), 0), header, inky_display.BLACK, font_header) # displays header

    for t in trains: # iterates through each train service with data and displays information in two columns
        name = t["origin"] + "-" + t["destination"]
        x_left = 0
        x_right = inky_display.WIDTH / 2
        if i % 2 == 0:
            if t["delay"] == 1:
                row = row + 1
                y = row*16
                draw.text((x_left, y), name, inky_display.BLACK, font)
                i = i+1
                count = count + 1
            elif t["delay"] == 0:
                row = row + 1
                y = row*16
                draw.text((x_left, y), name + " (" + str(t["delayMinutes"]) + ")", inky_display.RED, font)
                i = i+1
                count = count + 1
            else:
                pass
        else:
            if t["delay"] == 1:
                y = row*16
                draw.text((x_right, y), name, inky_display.BLACK, font)
                i = i+1
                count = count + 1
            elif t["delay"] == 0:
                y = row*16
                draw.text((x_right, y), name + " (" + str(t["delayMinutes"]) + ")", inky_display.RED, font)
                i = i+1
                count = count + 1
            else:
                pass

As you can see, this is really clunky, repetative code which needs a lot of work before it would be ready for any kind of production. But, as a prototype that I can work on and develop further, it does the job for me.

All the code is there for all to see on GitHub. If you find any use for it, feel free to drop me an email.