# Next MBTA Ride Sign
# 2025 Paul M
# SPDX-License-Identifier: MIT
# Creates a sign that shows when the next bus or subway train will arrive at an MBTA stop
# Stops can be commuter rail stops, subway stations, or bus stops
import time
import board
import terminalio
from adafruit_matrixportal.matrixportal import MatrixPortal
import json
from adafruit_datetime import datetime
import supervisor
# Configure one of these examples to create a sign for a stop or station
# Use only one set of LABEL, DIRECTION, ROUTE, and STOP_ID array variables
# Each comma separated and quoted entry set of
# LABEL, DIRECTION, ROUTE, and STOP_ID defines a SIGN
# ARRAY_VARIABLE = ["Sign0","Sign1","Sign2","Etc"]
# Makes three signs for routes at two different stops with scrolling labels for each route sign
# The sign changes to a new bus route when CYCLE_SIGN is True
# LABEL = ["Next 108 bus to Malden Center ","Next 411 bus to Malden Center ","Next 430 bus to Saugus Center "]
# DIRECTION = ["1","1","0"] # Stop ID directions of travel
# ROUTE = ["108","411","430"] # Train or bus routes for your sign
# STOP_ID = ["9021","9021","9063"] # Stop ID(s) from the MBTA https://api-v3.mbta.com/stops
# Makes two signs for the Malden center subway station showing arriving trains in both directions
# The scrolling sign changes direction while CYCLE_SIGN is True
# LABEL = ["Forest Hills ","Oak Grove "] # Direction of travel labels
# DIRECTION = ["0","1"] # Stop direction of travel
# ROUTE = ["Orange","Orange"] # Train line for your sign
# STOP_ID = ["place-mlmnl","place-mlmnl"] # Stop ID(s) from the MBTA
# Makes a single sign for one or more routes at a stop
# Set CYCLE_SIGN to false so that the scroll won't reset and restart
# The scroll will pause during sign updates, but not restart
LABEL = ["Next bus to Malden Center "] # Stop scroll, set CYCLE_SIGN to False when there's only one
DIRECTION = ["1"] # Stop direction of travel
ROUTE = ["108,411,430"] # Bus route(s) for your sign
STOP_ID = ["9021"] # Stop ID(s) from the MBTA
# Pick an image that's appropriate for your sign
BACKGROUND_IMAGE = "/t_bus.bmp" # t_bus, t_blu, t_orn, t_red, t_grn, t_crl, or t_slv
CYCLE_SIGN = "False" # If True, cycle through the defined SIGNs, set to False if you make a single sign
UPDATE_INTERVAL = 30 # Seconds between sign updates
network = None # Force a network connect at startup
last_time_sync = None # Force a time sync at startup
sync_interval = 3600 # Reset the time every hour
current_ride = 0 # Start with ride 0
# Set debug to True to see sign changes in the serial monitor
matrixportal = MatrixPortal(status_neopixel=board.NEOPIXEL, debug=False)
# Requires an adafruit AIO account to properly work
# and a settings.toml file with the proper credentials and timezone
if network is None:
network = (matrixportal.network)
matrixportal.set_background(BACKGROUND_IMAGE) # Maybe make this part of 30 second updates too?
matrixportal.add_text( # Add text for the label line at the top
text_font=terminalio.FONT,
text_position=(18,6), # Adjust to center a static label if scrolling is False
text_scale=1,
text_color=0xFFAC1C,
scrolling=True, # Scrolls the sign label, can also be False if your label fits on the sign
)
matrixportal.add_text( # Add text for the first ride time
text_font=terminalio.FONT,
text_position=(22, 16),
text_scale=1,
)
matrixportal.add_text( # Add text for the second ride time
text_font=terminalio.FONT,
text_position=(22, 26),
text_scale=1,
text=("Loading")
)
def get_arrival_in_minutes_from_now(now, date_str):
# print(f"now: {now}, date_str: {date_str}") # Uncomment to troubleshoot date and time setting problems
ride_date = datetime.fromisoformat(date_str).replace(tzinfo=None)
return round((ride_date - now).total_seconds() / 60.0)
def get_next_ride_times(current_ride):
times = []
now = datetime.now()
try:
# Add a page limit to the line below if you get large fetch results from the API to keep your sign from having memory problems
DATA_SOURCE = f"https://api-v3.mbta.com/predictions?filter[stop]={STOP_ID[current_ride]}&filter[route]={ROUTE[current_ride]}&sort=departure_time"# &page[limit]=3"
response = network.fetch_data(DATA_SOURCE + f"&filter[direction_id]={DIRECTION[current_ride]}")
# print(DATA_SOURCE + f"&filter[direction_id]={DIRECTION[current_ride]}") # Uncomment to troubleshoot API format problems
data = json.loads(response)
num_avail_times = len(data["data"])
if num_avail_times < 1:
return []
for i in range(len(data["data"])):
try:
departure_time = data["data"][i]["attributes"]["departure_time"]
if departure_time:
minute_difference = get_arrival_in_minutes_from_now(now, departure_time)
if (minute_difference > 0): # Only add positive times to avoid erroneous old values (sometimes the api returns these)
times.append(minute_difference)
if len(times) == 3 or len(times) >= num_avail_times:
break
except (KeyError, IndexError):
# TODO maybe do more error handling here for bad fetch
# Right now just skips to the next departure_time
continue
except Exception as e:
print(f"Error fetching or parsing data: {e}")
supervisor.reload()
return times # Return the calculated time(s) in an array variable with one or two entries
last_update = time.monotonic() - UPDATE_INTERVAL
matrixportal.set_text(f"{LABEL[current_ride]}", 0) # Set the initial sign scroll
while True:
if time.monotonic() - last_update >= UPDATE_INTERVAL:
if last_time_sync is None or time.monotonic() - last_time_sync >= sync_interval:
try:
# Set the device time from the network
# Be sure to include
# timezone = "US/Eastern"
# in settings.toml or the system time may be set to GMT (UTC? Zulu?)
network.get_local_time()
last_time_sync = time.monotonic()
except Exception as e:
print("Failed to get the current time, error:", e)
supervisor.reload()
now = datetime.now()
ride_times = get_next_ride_times(current_ride)
matrixportal.set_text(f"{ride_times[0]:2d} min" if len(ride_times) > 0 else "None", 1) # Update the first ride time
matrixportal.set_text(f"{ride_times[1]:2d} min" if len(ride_times) > 1 else "", 2) # Update the second ride time
last_update = time.monotonic()
if CYCLE_SIGN == "True":
matrixportal.set_text(f"{LABEL[current_ride]}", 0) # Update the sign scroll
if current_ride < (len(ROUTE)-1):
current_ride += 1
else:
current_ride = 0
continue
matrixportal.scroll()
time.sleep(0.05) # Change the scroll speed here, larger numbers are slower
An LED matrix device to show predictions of arriving buses and trains using the MBTA v3 API
To get to the Git hub repository for this project click here: https://github.com/dufus2506/MBTA-bus-train-stop-prediction-sign
Built on the excellent work of others I've seen, I have designed my own MBTA sign to tell you when the bus or train will be leaving a stop. This sign can show predictions for for bus lines, subway stations, and commuter rail stations.
This is designed to be used on the Adafruit Matrixportal M4 running Circuitpython https://learn.adafruit.com/adafruit-matrixportal-m4 and using a 64x32 RGB LED matrix. https://www.adafruit.com/category/327 Pick whatever size you like.
I managed to keep the code very small, and with a minimum number of libraries so the limited memory of the M4 would not be a problem. And I improved on the code comments a bit to make it easier for you to configure your own sign.
I removed all but the most essential libraries from the board. I have only the libraries shown in the image on my board.
Apart from that I tried to make the sign as useful as possible with the ability to see predictions for more than one stop or bus route. You can add as many stops/routes/lines to a sign as you need, though it's not very practical if the sign has too many. You don't want to be waiting too long for sign updates to see when your ride is leaving.
Because the sign updates every thirty seconds, you won't need an MBTA API Key to make it work. You need one if your project will be checking the API multiple times a second. That's not very practical for this sign, so no API key necessary here. But you will need an Adafruit AIO account so that you can set the clock properly. The free level is just fine for clock setting. Get one here: https://io.adafruit.com/
Your settings.toml file should have the following entries:
CIRCUITPY_WIFI_SSID = "Your_SSID"
CIRCUITPY_WIFI_PASSWORD = "Your_Password"
AIO_USERNAME = "Your_AIO_Username"
AIO_KEY = "aio_Your_Really_Long_AIO_User_Key"
timezone = "US/Eastern"
Don't forget the timezone entry, or your clock may be set to GMT. WiFi settings are case sensitive in the settings.toml file, so be sure to match everything correctly for your WiFi network.
When I was done I had the files and directories seen in the image below on the board. All but the lib directory are empty.
See the code.py file for configuration examples. You only want one set of array variables for your configuration. You'll need some stop information to make the correct entries in your array variables. You can find the necessary settings here: https://api-v3.mbta.com/stops Click the "pretty print" box when the page opens, and search for your stop.
This page (Boston MBTA stop prediction sign) was last updated on April 07, 2025.