Arduino TFL arrival board
Let me start by outlining a problem I had. I live in a very unique part of London. I live equidistant from 2 district line stations. This is not unique, the uniqueness comes from both stations being on separate branches of the district line which join when going eastbound. This means that every time I leave the house to go eastbound, I need to check which station to go to to arrive faster. This means walking down my road with my phone out frantically searching on google maps to find the faster route (just finding the directions isnt efficient as it normally sets your departure time a few minutes after your actual departure time). I appreciate this is a small problem, but I am fortunate enough to have the time and skills to solve it, so I gave it a go.
My solution was to build a TFL arrival board for my house using an arduino. Below is an account of how I did it.
Firstly, TFL has a brilliant free to use API, so step one was trying to decode the outputs from that.
import requests # Base URL of the API url = "https://api.tfl.gov.uk/line/district/arrivals" # Make a GET request to the API try: response = requests.get(url) # Check if the request was successful response.raise_for_status() # This will raise an HTTPError if the response was an unsuccessful status except requests.exceptions.HTTPError as http_err: print(f"HTTP error occurred: {http_err}") # Handle specific HTTP errors except Exception as err: print(f"Other error occurred: {err}") # Handle other types of exceptions else: # Parse the JSON response data = response.json() print(data[0])
Great! now we have an output. So it looks like here we have 'currentLocation', 'platformName', 'destinationName' (important for lines with branches) and 'expectedArrival' which are important fields.
Lets decode what is going on here: This API response is predicting the arrival of a District Line train at Dagenham East (Eastbound, Platform 2). The train is currently past Plaistow, heading towards Upminster, and is expected to arrive at 09:37:41 UTC (≈19.6 minutes from the timestamp).
So this is great now we simply have to filter the response to only include the two stations of interest, then rank them by arrival time
import pandas # Select fields for the table selected_fields = ['vehicleId', 'stationName', 'lineName', 'platformName', 'expectedArrival', 'towards'] # Create a list of rows with selected fields table_data = [{field: item[field] for field in selected_fields} for item in data] # Create a DataFrame from the table data df = pd.DataFrame(table_data) # Convert 'expectedArrival' to datetime df['expectedArrival'] = pd.to_datetime(df['expectedArrival']) # Filter DataFrame to include only specific stations stations_of_interest = ["Gunnersbury Underground Station", "Chiswick Park Underground Station" ] df_filtered = df[df['stationName'].isin(stations_of_interest)] print(df_filtered)
vehicleId stationName lineName \ 299 046 Chiswick Park Underground Station District 340 026 Chiswick Park Underground Station District 405 002 Gunnersbury Underground Station District 452 126 Chiswick Park Underground Station District 504 125 Chiswick Park Underground Station District platformName expectedArrival towards 299 Eastbound - Platform 2 2025-03-11 10:48:46+00:00 Upminster 340 Westbound - Platform 1 2025-03-11 10:49:46+00:00 Ealing Broadway 405 Westbound - Platform 1 2025-03-11 10:53:15+00:00 Richmond 452 Westbound - Platform 1 2025-03-11 10:58:16+00:00 Ealing Broadway 504 Westbound - Platform 1 2025-03-11 10:46:46+00:00 Ealing Broadway
Ok now we have the stations we want to observe and the expected arrival time.
Next, we process the raw API response to sort the trains by arrival time and west vs east bound. Here's how we do it:
# Sort DataFrame by 'expectedArrival' df_filtered['is_eastbound'] = df_filtered['platformName'].str.contains('Eastbound', case=False) # Sort with eastbound trains first, then by expectedArrival df_sorted = df_filtered.sort_values(by=['is_eastbound', 'expectedArrival'], ascending=[False, True]) # Drop the helper column as it's no longer needed df_sorted = df_sorted.drop(columns='is_eastbound') df_sorted.loc[ df_sorted['stationName'] == 'Gunnersbury Underground Station', 'stationName' ] = 'Gunnersbury' df_sorted.loc[ df_sorted['stationName'] == 'Chiswick Park Underground Station', 'stationName' ] = 'Chiswick Park' print(df_sorted)
My goal is to have this code export to an LCD screen on my arduino. To do this I will need to write a function that will export the df in a way which can be decoded by an arduino. So in this example we are seperating left and right alignment with a comma and each new line is seperated with a '|'.
from datetime import datetime, timedelta def send_to_lcd(df): current_time = datetime.now(pytz.utc) lines_to_display = [] lines_to_display.append('EASTBOUND,') show=True count= 1 for _, row in df.iterrows(): if show: if "Westbound" in row['platformName']: show=False lines_to_display.append('WESTBOUND,') count = 1 station = row['stationName'] arrival_time = row['expectedArrival'] minutes_until = round((arrival_time - current_time).total_seconds() / 60) # Only show trains arriving in 5+ mins if minutes_until > 0: display_line = f"{count} {station[:14]},{minutes_until}min" lines_to_display.append(display_line) count+=1 # Combine lines with a '|' delimiter and end with newline message = "|".join(lines_to_display)[:200] + "\n" #ser.write(message.encode('utf-8')) print(message) send_to_lcd(df_sorted)
EASTBOUND,|WESTBOUND,|1 Gunnersbury,1min|2 Chiswick Park,4min|3 Gunnersbury,6min|4 Chiswick Park,8min
So that is a pretty good place to be, next steps will be wiring up the LCD to the arduino and builidng a .ino file for building the image correctly.
First the arduino environment is set up.
#include <SPI.h> #include <Adafruit_GFX.h> #include <Adafruit_ILI9341.h> // Include the correct font #include "LondonUnderground12pt7b.h" // LCD pins #define TFT_CS 10 #define TFT_RST 9 #define TFT_DC 8 Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST); String incomingData = ""; void setup() { Serial.begin(9600); tft.begin(); tft.setRotation(3); tft.setFont(&LondonUndergroundRegular8pt7b); tft.fillScreen(ILI9341_BLACK); tft.setTextColor(ILI9341_WHITE); tft.setTextSize(1); tft.setCursor(10, 18); tft.println("Waiting for data..."); }
The london underground font was taken from an excellent resource from GitHub. This font was transformed from the .svg to the .h needed by the adafruit package by using this font convert. The pins are dependant on your own setup, but these pins will work for the wiring diagram in this blog.
void loop() { // Read full message until newline while (Serial.available() > 0) { char c = Serial.read(); if (c == '\n') { displayData(incomingData); incomingData = ""; } else { incomingData += c; } } } void displayData(String data) { // Clear screen once tft.fillScreen(ILI9341_BLACK); tft.setTextColor(ILI9341_YELLOW); tft.setFont(&LondonUndergroundRegular8pt7b); tft.setTextSize(1); int yPosition = 18; // Split the message using '|' int startIndex = 0; int delimiterIndex = data.indexOf('|'); while (delimiterIndex != -1) { String line = data.substring(startIndex, delimiterIndex); // Find space between station and time int separatorIndex = line.lastIndexOf(','); if (separatorIndex != -1) { String station = line.substring(0, separatorIndex); String timeStr = line.substring(separatorIndex + 1); // Calculate width dynamically for right alignment int timeWidth = timeStr.length() * 10; // Adjust based on font int xPosition = 320 - timeWidth - 8; // Right-align with 10px margin // Print station name on the left tft.setCursor(10, yPosition); tft.print(station); // Print time aligned to the right tft.setCursor(xPosition, yPosition); tft.print(timeStr); } yPosition += 25; startIndex = delimiterIndex + 1; delimiterIndex = data.indexOf('|', startIndex); // Prevent text overflow if (yPosition > 300) break; } }
Next, the output from the serial is decoded to look like a traditional london underground arrivals board. Finally load this .ino file on the arduino and un comment the line in the send to LCD function.
You will need to find out which port the arduino is connected to this can be done with running ls /dev/tty* in your terminal. the arduino will be called something with USB. Once you have the port name adjust the following code and add it to the top of the python project. This will mean the output is being sent to the arduino via the COM port.
import serial ser = serial.Serial('/dev/tty.usbmodemxxxx', 9600, timeout=1) # Adjust '/dev/tty.usbmodemxxx'
The next step is to add a wifi module and containerise the code so the project can run autonomously.