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.