UK train departure display

A Python service to display quasi-realtime UK railway station departure data in a hauntingly familiar fashion. This is designed for use on a Raspberry Pi and an SSD1322-based 256x64 SPI OLED screen.

This project uses the publicly available Transport API, and is strongly inspired by the work of many others. It is structured for use on the balenaCloud platform, which offers a free tier.

Configuration

Sign up for the Transport API, and generate an app ID and API key (note the free tier has a limit of 1000 requests a day).

These environment variables can be specified using the balenaCloud dashboard, allowing you to set up mutiple signs in one application for different stations.

Key Default Description
activeTimes (none) Optional. One or more time ranges during which the display will show data. See below for more details.
blankTimes (none) Optional. One or more time ranges during which the display will be entirely blank. Takes precedence over activeTimes. See below for more details.
callingAt (none) Optional. A National Rail station code that trains must call at in order to be shown, e.g. BFR.
departFrom (none) Required. A National Rail station code to show departures from, e.g. STP.
displayRotation 0 Optional. How much to rotate the display; either 0 or 180. Useful if your display placement is constrained.
minDepartureMin 0 Optional. If greater than zero, trains departing in less than this many minutes are not shown. This allows you to hide trains if you can't get to the platform in time to catch them.
outOfHoursName (none) Optional. Name shown when current time is outside active hours, e.g. London St Pancras. If set to _clock_, the display will just show the current time. If set to _blank_ then the display will be completely blanked.
slowStations (none) Optional. If a train calls at a station in this list, it is marked "slow".
showCallingAt true Optional. Whether to show "calling at" for the first departure. If false, a fourth departure is shown instead.
showPlatform true Optional. Whether to show platform numbers for departures.
showUpdateCountdown false Optional. Whether to show a visual indicator of how long is left until the data is refreshed.
refreshTime 120 Optional. Seconds between data refresh, during active hours. Values will be clamped to the range 15 - 3600, inclusive.
transportApi_apiKey (none) Required. Transport API key, e.g. feedface8badf00ddecafbaddead.
transportApi_appId (none) Required. Transport API application ID, e.g. 12345678.
TZ Europe/London Optional. An Olsen timezone name to use. You almost certainly don't want to change this.

Time ranges

Active and blank times are a comma-separated list of one or more time ranges. A time range consists of an optional day specifier and a start/end time pair. It has the format [ddd:]hh[[:]mm[[:]ss]]-hh[[:]mm[[:]ss]]:

  • ddd:: An optional case-insensitive day (Sun, Mon, Tue, Wed, Thu, Fri, Sat, weekday, weekend, daily) followed by a colon.
  • hh: A one- or two-digit hour (0-23) with optional leading zero (00-23).
  • mm: A two-digit minute (00-59) preceded by an optional colon.
  • ss: A two-digit second (00-59) preceded by an optional colon.
  • If the day is missing, it will be treated as daily.
  • If the minutes or seconds are missing, they will be treated as zero.

Examples of valid time ranges:

  • 8-22 = every day, 08:00:00 until 22:00:00.
  • 08-22 = every day, 08:00:00 until 22:00:00.
  • 23-01 = every day, 23:00:00 until 01:00:00 the following day.
  • weekday:8-22 = every weekday, 08:00:00 until 22:00:00.
  • weekday:08-22 = every weekday, 08:00:00 until 22:00:00.
  • weekend:1030-1545 = 19:00:00 Friday until 02:45:00 Saturday.
  • fri:1930-0245 = 19:00:00 Friday until 02:45:00 Saturday.

It is permitted for time ranges to overlap. A time range does include its start time, but does not include its end time. This means that an active time of 09:00:00-10:00:00 will become inactive at 10:00:00 exactly.

You can be a bit loose with the day specifiers: Tue, tue, tues, tuesday, and Tuesday all have the same meaning.

Active times, blank times, and out-of-hours times

  • During active times, the display will update with train data (if available) every refreshTime seconds.
  • During blank times, the display will be completely blank, not even showing the time. No train data will be fetched.
  • During out-of-hours times, the display will show out-of-hours content. No train data will be fetched.
    • If outOfHoursName is set to _clock_, then only a clock will be displayed.
    • If outOfHoursName is set to _blank_, then the display will be entirely blank.
    • If outOfHoursName is empty, then it will act as if it was set to the human-readable name of the departFrom station.
    • If outOfHoursName is any other value, a "Welcome to" message will be displayed along with a clock.

If activeTimes is not given, the display will be updated at all times. If blankTimes is not given, the display will not blank itself (unless outOfHoursName is _blank_).

For example, the configuration:

activeTimes=weekday:0730-0930,1715-1930,weekend:0900-2000
blankTimes=weekday:2100-0700,sat:0700-0800,sat:2200-0800,sun:2200-0700
outOfHoursName=My Home

would turn off the display overnight, and display a clock and "Welcome to My Home" message during the middle of a weekday.

Calling at

If no station codes are given, all trains from the departure station are shown. If a station code is given, then a train will be shown only if it calls at that station. Note that simply passing through a station without stopping isn't enough.

Slow stations

If a train calls at a station in this list, it is marked as "slow". For example, TEA would mark any trains calling at Teesside Airport as being "slow". This variable can have multiple stations listed, and any of them will cause the train to be marked as "slow".

There is also an advanced syntax, which looks like SVG=2,PBO=1. This assigns a slowness penalty of 1 to services calling at Peterborough, and a slowness penalty of 2 to services calling at Stevenage. The highest value from all of the applicable penalties is used. This allows fine distinction between different speeds of service, such as "nonstop", "fast" and "slow". If any station has an explicit penalty, then all must have one. Penalties are in the range 1-9, inclusive.

Hardware

While this project can use a pygame-based emulated display for local development, it is designed to work with an SSD1322-based 256x64 SPI display, preferably yellow OLED for an authentic look. I have used displays from AliExpress successfully.

The connections for one of these displays to the Raspberry Pi GPIO header are as follows, but it would be a good idea to check the connections with the datasheet of your particilar display before powering on as there's no guarantee yours will match the pinout of mine.

Display pin Connection Raspberry Pi pin
1 Ground 6 (Ground)
2 V+ (3.3V) 1 (3v3 Power)
4 D0/SCLK 23 (BCM11 SCLK)
5 D1/SDIN 19 (BCM10 MOSI)
14 DC (data/command select) 18 (BCM24)
15 RST (reset) 22 (BCM25)
16 CS (chip select) 24 (BCM8 CE0)

Credits

First of all, thanks to the team over at Balena who blogged about their implementation, put it on Github, and inspired me to go one better.

Thanks also to Chris Hutchinson who originally started this project, and Blake who made some further improvements.

The fonts used were painstakingly put together by DanielHartUK and can be found on GitHub at https://github.com/DanielHartUK/Dot-Matrix-Typeface. Huge thanks for making that resource available!