Building a Tram-Time Display with AWS Lambda

The real-time information displays at Bus and Tram stops in Nottingham are really useful but for a while I've wanted to get this information before I leave the house/office. I've recently discovered the API which exposes this information and used it to build a reconstruction of the display which can be used on desktop/mobile.

You can find the code on Github

When my wife updated to the latest version of the Robin Hood Network App; she noticed that the changelog showed the addition of real time tram information. I downloaded the app and checked the DNS requests being made on my PiHole DNS dashboard to robinhood.arcticapi.com

I then decided to try and capture the requests being made from the app using OWASP ZAP but this turned out to be a dead-end since newer versions of Android make it difficult to intercept SSL requests due to the inability to install Root CA's to the device.

I stuck to browsing the API endpoints manually in my browser, I proxied through ZAP so that it recorded my requests and made it easy to map out the structure as I explored it.

ZAP's Sites View

After some experimentation I determined that the information that I wanted was under https://robinhood.arcticapi.com/network/stops/STOPID/visits where STOPID is obtained from https://robinhood.arcticapi.com/network/stops?search=search-string

I threw together a python script to grab the JSON from this endpoint and parse it to produce a terminal output

Next step was to turn this into a web app, rather than building this out in Python I decided to make use of AWS Lambda. I've never played with "Serverless" infrastructure so this was a bit of a learning experience! The Free Tier of AWS provides 1,000,000 Lambda invocations and 1,000,000 API gateway requests per month which should be more than enough for my use.

Firstly I created a new Lambda function using Python 3.7 and then added an "API Gateway" trigger to give it an HTTPS endpoint. In order to include external libraries in the python code you need to package them up and upload a zip file. My script uses the requests library to fetch data so I had to bundle this is, fortunately the AWS documentation for this is really helpful.

I had some issues getting this working before reading that the response from the python function has to match a specific format to be accepted by the API gateway. I used the response function from this post to help with that. Once I'd got a JSON response working in my browser I copied in my API code and started adapting it to return HTML.

Adjusting the Content-Type header from JSON to HTML

With a bit of trial & error and a splash of formatting I had an HTML page returned with the latest information for a hard-coded stop. I also added a quick Javascript based clock to the bottom of the page at this point.

Next step was reading the stop from a query string in the URL to pass the stop ID, this is passed in  "queryStringParameters" in the event argument

Then adding a custom domain name to my API endpoint, I followed the official AWS documentation which guided me through issuing an SSL cert through the AWS console and setting up the DNS.

Unfortunately the ID number of the particular stop isn't a very intuitive way of referring to each tram stop, especially since the East/West bound platforms have different codes! I looked at integrating the search feature into the script but unfortunately the set of results returned is typically too broad for the script to be able to determine the correct record, therefore I created a second function specifically for searching the stops. I used the goverment dataset of UK bus-stops and filtered it down to just Nottingham tram stops using Excel and then ingested it into an RDS MySQL database. While this is probably overkill for ~100 tram stops, it's a convenient way of providing a search facility and makes use of another service included in the AWS free tier.

I wrote a short python function to search the database from the query string and present a simple HTML page with hyperlinks to the tram display endpoint.

I also added a second mapping to my custom domain within the API Gateway console to allow access to the search function from the same domain.

In order for the Lambda function to securely access the MySQL instance it needs to be added into the same VPC. You'll need to add VPC permissions to the IAM role used for your lambda function before you're able to do this. Specifically it's the AWSLambdaVPCAccessExecutionRole role that's needed.

So those two lambda functions allow someone to identify their tram-stop and display real-time information for it. I'm going to do more work on improving the formatting and making it responsive for different screens/devices.

You can currently access a hosted demo at

https://tram.rothe.uk/tramdisplay/tramTimes?stop=9400ZZNOLCM1

https://tram.rothe.uk/tramsearch/TramSearch?search=market

However I might have to take these down if I start getting a lot of requests so please don't abuse them.

I hope this is useful, Please get in touch with any feedback, questions or suggestions.


Update:

Since Friday 4th January the API hasn't been returning any real-time results, just timetable schedules. I've updated my function to take this into account and display a warning before the non-real-time data.

Current plans for further development include improving the search formatting and adding in better support for bus-stops