Overview
You can build a large RGB LED matrix display to follow along with the FIFA World Cup. A Matrix Portal S3 running CircuitPython requests data from the ESPN API to show the tournament gameplay data alongside country flags that are resized and gamma corrected to look crisp and bright on the matrices.
Hardware and Power Requirements
The ESPN API generates a JSON response that is huge. It also takes a lot of processing power to interface with not one, not two, but four 64x32 RGB LED matrices. Luckily the ESP32-S3 on the Matrix Portal S3 is able to handle the JSON and the matrices with its 8MB of flash and 2MB of SRAM. Previously this project would not have been possible with less powerful chips, like the SAMD51 on the original Matrix Portal. TL;DR: make sure you are using a Matrix Portal S3 for this project.
On top of processing power, four RGB LED matrices require a good power supply to ensure top pixel performance. In working on this project, the best results were seen using two 5V 4A power supplies: one for the two top panels and one for the two bottom panels. In this scenario, the Matrix Portal S3 is powered via its USB-C port, separately from the matrices.
Page last edited April 28, 2026
Text editor powered by tinymce.
3D Printing
You can 3D print brackets to hold the matrices together. Note that these have been designed to fit the 4mm pitch matrices. You'll print one center bracket and six of the smaller 1x2 brackets. The parts can be downloaded from Printables or directly below.
A plus sign shaped bracket fits over the intersection in the middle of the four matrices. It is secured with four M3 screws.
Page last edited April 28, 2026
Text editor powered by tinymce.
Install CircuitPython
CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.
Set up CircuitPython Quick Start!
Follow this quick step-by-step for super-fast Python power :)
Further Information
For more detailed info on installing CircuitPython, check out Installing CircuitPython.
Click the link above and download the latest UF2 file.
Download and save it to your desktop (or wherever is handy).
Plug your MatrixPortal S3 into your computer using a known-good USB cable.
A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.
Click the Reset button (indicated by the green arrow) on your board. When you see the NeoPixel RGB LED (indicated by the magenta arrow) turn purple, press it again. At that point, the NeoPixel should turn green. If it turns red, check the USB cable, try another USB port, etc.
If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!
You will see a new disk drive appear called MATRXS3BOOT.
Drag the adafruit_circuitpython_etc.uf2 file over to MATRXS3BOOT.
Page last edited April 28, 2026
Text editor powered by tinymce.
Create Your settings.toml File
CircuitPython works with WiFi-capable boards to enable you to make projects that have network connectivity. This means working with various passwords and API keys. As of CircuitPython 8, there is support for a settings.toml file. This is a file that is stored on your CIRCUITPY drive, that contains all of your secret network information, such as your SSID, SSID password and any API keys for IoT services. It is designed to separate your sensitive information from your code.py file so you are able to share your code without sharing your credentials.
CircuitPython previously used a secrets.py file for this purpose. The settings.toml file is quite similar.
CircuitPython settings.toml File
This section will provide a couple of examples of what your settings.toml file should look like, specifically for CircuitPython WiFi projects in general.
The most minimal settings.toml file must contain your WiFi SSID and password, as that is the minimum required to connect to WiFi. Copy this example, paste it into your settings.toml, and update:
your_wifi_ssidyour_wifi_password
CIRCUITPY_WIFI_SSID = "your_wifi_ssid" CIRCUITPY_WIFI_PASSWORD = "your_wifi_password"
Many CircuitPython network-connected projects on the Adafruit Learn System involve using Adafruit IO. For these projects, you must also include your Adafruit IO username and key. Copy the following example, paste it into your settings.toml file, and update:
your_wifi_ssidyour_wifi_passwordyour_aio_usernameyour_aio_key
CIRCUITPY_WIFI_SSID = "your_wifi_ssid" CIRCUITPY_WIFI_PASSWORD = "your_wifi_password" ADAFRUIT_AIO_USERNAME = "your_aio_username" ADAFRUIT_AIO_KEY = "your_aio_key"
Some projects use different variable names for the entries in the settings.toml file. For example, a project might use ADAFRUIT_AIO_ID in the place of ADAFRUIT_AIO_USERNAME. If you run into connectivity issues, one of the first things to check is that the names in the settings.toml file match the names in the code.
Here is an example settings.toml file.
# Comments are supported CIRCUITPY_WIFI_SSID = "guest wifi" CIRCUITPY_WIFI_PASSWORD = "guessable" CIRCUITPY_WEB_API_PORT = 80 CIRCUITPY_WEB_API_PASSWORD = "passw0rd" test_variable = "this is a test" thumbs_up = "\U0001f44d"
In a settings.toml file, it's important to keep these factors in mind:
- Strings are wrapped in double quotes; ex:
"your-string-here" - Integers are not quoted and may be written in decimal with optional sign (
+1,-1,1000) or hexadecimal (0xabcd).- Floats (decimal numbers), octal (
0o567) and binary (0b11011) are not supported.
- Floats (decimal numbers), octal (
- Use
\uescapes for weird characters,\xand\oooescapes are not available in .toml files- Example:
\U0001f44dfor 👍 (thumbs up emoji) and\u20acfor € (EUR sign)
- Example:
- Unicode emoji, and non-ASCII characters, stand for themselves as long as you're careful to save in "UTF-8 without BOM" format
When your settings.toml file is ready, you can save it in your text editor with the .toml extension.
In your code.py file, you'll need to import the os library to access the settings.toml file. Your settings are accessed with the os.getenv() function. You'll pass your settings entry to the function to import it into the code.py file.
import os
print(os.getenv("test_variable"))
In the upcoming CircuitPython WiFi examples, you'll see how the settings.toml file is used for connecting to your SSID and accessing your API keys.
Page last edited April 28, 2026
Text editor powered by tinymce.
Code the Scoreboard
Once you've finished setting up your Matrix Portal S3 with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2026 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import os
import gc
import ssl
import time
import wifi
import socketpool
import adafruit_requests
import adafruit_display_text.label
import board
import terminalio
import displayio
import framebufferio
import rgbmatrix
import microcontroller
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
from adafruit_datetime import datetime, timedelta
import neopixel
displayio.release_displays()
# font color for text on matrix
font_color = 0xFFFFFF
# your timezone UTC offset and timezone name
timezone_info = [-4, "EDT"]
# how often the API should be fetched
fetch_timer = 300 # seconds
# how often the display should update
display_timer = 30 # seconds
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness = 0.3, auto_write=True)
# matrix setup
base_width = 64
base_height = 32
chain_across = 2
tile_down = 2
DISPLAY_WIDTH = base_width * chain_across
DISPLAY_HEIGHT = base_height * tile_down
matrix = rgbmatrix.RGBMatrix(
width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, bit_depth=4,
rgb_pins=[
board.MTX_R1,
board.MTX_G1,
board.MTX_B1,
board.MTX_R2,
board.MTX_G2,
board.MTX_B2
],
addr_pins=[
board.MTX_ADDRA,
board.MTX_ADDRB,
board.MTX_ADDRC,
board.MTX_ADDRD
],
clock_pin=board.MTX_CLK,
latch_pin=board.MTX_LAT,
output_enable_pin=board.MTX_OE,
tile=tile_down, serpentine=True,
doublebuffer=False
)
display = framebufferio.FramebufferDisplay(matrix)
# connect to WIFI
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
# add API URLs
SPORT_URL = "https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.world/scoreboard"
context = ssl.create_default_context()
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, context)
# arrays for games and display groups
# the length and entries for both will vary depending on API response
games = []
groups = []
# takes UTC time from JSON and reformats how its displayed
def convert_date_format(date, tz_information):
# Manually extract year, month, day, hour, and minute from the string
year = int(date[0:4])
month = int(date[5:7])
day = int(date[8:10])
hour = int(date[11:13])
minute = int(date[14:16])
# Construct a datetime object using the extracted values
dt = datetime(year, month, day, hour, minute)
# Adjust the datetime object for the target timezone offset
dt_adjusted = dt + timedelta(hours=tz_information[0])
# Extract fields for output format
month = dt_adjusted.month
day = dt_adjusted.day
hour = dt_adjusted.hour
minute = dt_adjusted.minute
# Convert 24-hour format to 12-hour format and determine AM/PM
am_pm = "AM" if hour < 12 else "PM"
hour_12 = hour if hour <= 12 else hour - 12
minute = f"{minute:02}"
# Determine the timezone abbreviation based on the offset
time_zone_str = tz_information[1]
return f"{month}/{day} - {hour_12}:{minute} {am_pm} {time_zone_str}"
def get_data(data, dictionary):
dictionary.clear()
pixel.fill((0, 0, 255))
print(f"Fetching data from {data}")
# make the request to the API
resp = requests.get(data)
# json
json_data = resp.json()
for i in range(len(json_data["events"])):
match_name = json_data["events"][i]["shortName"]
print(match_name)
date = json_data["events"][i]["date"]
date = convert_date_format(date, timezone_info)
home_team = match_name[0:3]
away_team = match_name[6:9]
score_home = json_data["events"][i]["competitions"][0]["competitors"][0]["score"]
score_away = json_data["events"][i]["competitions"][0]["competitors"][1]["score"]
clock = json_data["events"][i]["status"]["displayClock"]
location = json_data["events"][i]["competitions"][0]["venue"]["address"]["city"]
status = json_data["events"][i]["status"]["type"]["shortDetail"]
dictionary.append({"home": home_team, "away": away_team, "score_home": score_home,
"score_away": score_away, "date": date, "clock": clock, "status": status,
"location": location})
# debug printing
# print(dictionary)
got_data = True
return dictionary, got_data
def make_gfx(dictionary, grps): # pylint: disable=too-many-locals
grps.clear()
for i in range(len(dictionary)):
# check if it's pre-game
if dictionary[i]["status"] == "Scheduled":
status = "pre"
print("match, pre-game")
else:
status = dictionary[i]["status"]
# make a display group
grp = displayio.Group()
# load in logos
logo0 = "/team_logos/" + dictionary[i]["home"] + ".bmp"
bitmap0 = displayio.OnDiskBitmap(logo0)
grid0 = displayio.TileGrid(bitmap0, pixel_shader=bitmap0.pixel_shader, x = 2)
grp.append(grid0) # index 0
logo1 = "/team_logos/" + dictionary[i]["away"] + ".bmp"
bitmap1 = displayio.OnDiskBitmap(logo1)
grid1 = displayio.TileGrid(bitmap1, pixel_shader=bitmap1.pixel_shader, x = 94)
grp.append(grid1) # index 1
home_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=" ")
home_text.text=dictionary[i]["home"]
home_text.anchor_point = (0.0, 0.5)
home_text.anchored_position = (10, 32)
grp.append(home_text) # index 2
away_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=" ")
away_text.text=dictionary[i]["away"]
away_text.anchor_point = (1.0, 0.5)
away_text.anchored_position = (120, 32)
grp.append(away_text) # index 3
vs_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=" ")
vs_text.anchor_point = (0.5, 0.0)
vs_text.anchored_position = (DISPLAY_WIDTH / 2, 14)
grp.append(vs_text) # index 4
info_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=" ")
info_text.anchor_point = (0.5, 1.0)
info_text.anchored_position = (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT)
grp.append(info_text) # index 5
location_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=f"{dictionary[i]['location']}")
location_text.anchor_point = (0.5, 1.0)
location_text.anchored_position = (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT - 12)
grp.append(location_text) # index 6
if status == "pre":
vs_text.text="VS"
info_text.text=dictionary[i]["date"]
# if it's active or final show score
else:
info_text.text=f"Clock: {dictionary[i]['clock']}"
vs_text.text=f"{dictionary[i]['score_home']} - {dictionary[i]['score_away']}"
grps.append(grp)
return grps
# clock for fetching
fetch_timer = fetch_timer * 1000
# index and clock for updating display
display_index = 0
display_timer = display_timer * 1000
# initial data fetch
try:
games, just_fetched = get_data(SPORT_URL, games)
groups = make_gfx(games, groups)
display.root_group = groups[display_index]
# pylint: disable=broad-except
except Exception as Error:
print(f"Error: {Error}")
time.sleep(10)
gc.collect()
time.sleep(5)
microcontroller.reset()
# start clocks
fetch_clock = ticks_ms()
display_clock = ticks_ms()
while True:
try:
if not just_fetched:
# garbage collection for display groups
gc.collect()
# fetch the json for the next team
games, just_fetched = get_data(SPORT_URL, games)
groups = make_gfx(games, groups)
# reset clocks
fetch_clock = ticks_add(fetch_clock, fetch_timer)
display_clock = ticks_add(display_clock, display_timer)
# update display seperate from API request
if ticks_diff(ticks_ms(), display_clock) >= display_timer:
print("updating display")
display.root_group = groups[display_index]
display_index = (display_index + 1) % len(games)
info_clock = ticks_ms()
display_clock = ticks_add(display_clock, display_timer)
# cleared for fetching after time has passed
if ticks_diff(ticks_ms(), fetch_clock) >= fetch_timer:
just_fetched = False
# pylint: disable=broad-except
except Exception as Error:
print(f"Error: {Error}")
time.sleep(10)
gc.collect()
time.sleep(5)
microcontroller.reset()
Upload the Code and Libraries to the Matrix Portal S3
After downloading the Project Bundle, plug your Matrix Portal S3 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Matrix Portal S3's CIRCUITPY drive.
- lib folder
- team_logos folder
- code.py
Your Matrix Portal S3 CIRCUITPY drive should look like this after copying the lib folder, team_logos folder and the code.py file.
Add Your settings.toml File
As of CircuitPython 8.0.0, there is support for Environment Variables. Environment variables are stored in a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file. Add your settings.toml file as described in the Create Your settings.toml File page earlier in this guide. You'll need to include your CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD.
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here"
How the CircuitPython Code Works
At the top of the code are a few variables that are used throughout:
-
font_color- the color of the font used for all of the text on the Matrix. It defaults to white (0xFFFFFF) -
timezone_info- your timezone UTC offset and timezone name. Edit these if needed to match your timezone -
fetch_timer- how often the ESPN API should be pinged to update the display (defaults to 5 minutes (300 seconds)) -
display_timer- how often the display should update to rotate through the different games (defaults to 30 seconds)
# font color for text on matrix font_color = 0xFFFFFF # your timezone UTC offset and timezone name timezone_info = [-4, "EDT"] # how often the API should be fetched fetch_timer = 300 # seconds # how often the display should update display_timer = 30 # seconds
Matrix
An RGBMatrix object is instantiated to be 128 pixels wide by 64 pixels high. It is then passed to be a FramebufferDisplay.
# matrix setup
base_width = 64
base_height = 32
chain_across = 2
tile_down = 2
DISPLAY_WIDTH = base_width * chain_across
DISPLAY_HEIGHT = base_height * tile_down
matrix = rgbmatrix.RGBMatrix(
width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, bit_depth=4,
rgb_pins=[
board.MTX_R1,
board.MTX_G1,
board.MTX_B1,
board.MTX_R2,
board.MTX_G2,
board.MTX_B2
],
addr_pins=[
board.MTX_ADDRA,
board.MTX_ADDRB,
board.MTX_ADDRC,
board.MTX_ADDRD
],
clock_pin=board.MTX_CLK,
latch_pin=board.MTX_LAT,
output_enable_pin=board.MTX_OE,
tile=tile_down, serpentine=True,
doublebuffer=False
)
display = framebufferio.FramebufferDisplay(matrix)
API URL
The SPORT_URL is the API URL for accessing the scoreboard data for the World Cup. The games array will hold a dictionary with information from the API requests and the groups array will hold displayio groups to show on the matrix.
# connect to WIFI
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
# add API URLs
SPORT_URL = "https://site.api.espn.com/apis/site/v2/sports/soccer/fifa.world/scoreboard"
context = ssl.create_default_context()
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, context)
# arrays for games and display groups
# the length and entries for both will vary depending on API response
games = []
groups = []
UTC to Your Time Zone
The convert_date_format() function is used to reformat the time that is fetched from the ESPN API. It rearranges the string to be formatted as "MM/DD - HH:MM AM/PM TZ" and converts the time from UTC to your defined time zone.
# takes UTC time from JSON and reformats how its displayed
def convert_date_format(date, tz_information):
# Manually extract year, month, day, hour, and minute from the string
year = int(date[0:4])
month = int(date[5:7])
day = int(date[8:10])
hour = int(date[11:13])
minute = int(date[14:16])
# Construct a datetime object using the extracted values
dt = datetime(year, month, day, hour, minute)
# Adjust the datetime object for the target timezone offset
dt_adjusted = dt + timedelta(hours=tz_information[0])
# Extract fields for output format
month = dt_adjusted.month
day = dt_adjusted.day
hour = dt_adjusted.hour
minute = dt_adjusted.minute
# Convert 24-hour format to 12-hour format and determine AM/PM
am_pm = "AM" if hour < 12 else "PM"
hour_12 = hour if hour <= 12 else hour - 12
minute = f"{minute:02}"
# Determine the timezone abbreviation based on the offset
time_zone_str = tz_information[1]
return f"{month}/{day} - {hour_12}:{minute} {am_pm} {time_zone_str}"
Fetch the Data
The get_data() function handles making a request to the ESPN API and populating the games list with a dictionary from the returned JSON feed. The following information is used for the display:
- Name of the match (
shortName)- The team names are extracted from this string
- Date
- Score
- Game clock
- Location (City)
- Game status
You can update this function to get different information from the API depending on what matters most to you.
def get_data(data, dictionary):
dictionary.clear()
pixel.fill((0, 0, 255))
print(f"Fetching data from {data}")
# make the request to the API
resp = requests.get(data)
# json
json_data = resp.json()
for i in range(len(json_data["events"])):
match_name = json_data["events"][i]["shortName"]
print(match_name)
date = json_data["events"][i]["date"]
date = convert_date_format(date, timezone_info)
home_team = match_name[0:3]
away_team = match_name[6:9]
score_home = json_data["events"][i]["competitions"][0]["competitors"][0]["score"]
score_away = json_data["events"][i]["competitions"][0]["competitors"][1]["score"]
clock = json_data["events"][i]["status"]["displayClock"]
location = json_data["events"][i]["competitions"][0]["venue"]["address"]["city"]
status = json_data["events"][i]["status"]["type"]["shortDetail"]
dictionary.append({"home": home_team, "away": away_team, "score_home": score_home,
"score_away": score_away, "date": date, "clock": clock, "status": status,
"location": location})
# debug printing
# print(dictionary)
got_data = True
return dictionary, got_data
Show the Data
The make_gfx() function takes the dictionary and creates text labels with the information from the API. The country flag is loaded in as a bitmap depending on who is playing in the game. All of the graphics elements are pushed to a group that is then added to the groups list to be accessed for showing on the matrix.
def make_gfx(dictionary, grps): # pylint: disable=too-many-locals
grps.clear()
for i in range(len(dictionary)):
# check if it's pre-game
if dictionary[i]["status"] == "Scheduled":
status = "pre"
print("match, pre-game")
else:
status = dictionary[i]["status"]
# make a display group
grp = displayio.Group()
# load in logos
logo0 = "/team_logos/" + dictionary[i]["home"] + ".bmp"
bitmap0 = displayio.OnDiskBitmap(logo0)
grid0 = displayio.TileGrid(bitmap0, pixel_shader=bitmap0.pixel_shader, x = 2)
grp.append(grid0) # index 0
logo1 = "/team_logos/" + dictionary[i]["away"] + ".bmp"
bitmap1 = displayio.OnDiskBitmap(logo1)
grid1 = displayio.TileGrid(bitmap1, pixel_shader=bitmap1.pixel_shader, x = 94)
grp.append(grid1) # index 1
home_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=" ")
home_text.text=dictionary[i]["home"]
home_text.anchor_point = (0.0, 0.5)
home_text.anchored_position = (10, 32)
grp.append(home_text) # index 2
away_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=" ")
away_text.text=dictionary[i]["away"]
away_text.anchor_point = (1.0, 0.5)
away_text.anchored_position = (120, 32)
grp.append(away_text) # index 3
vs_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=" ")
vs_text.anchor_point = (0.5, 0.0)
vs_text.anchored_position = (DISPLAY_WIDTH / 2, 14)
grp.append(vs_text) # index 4
info_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=" ")
info_text.anchor_point = (0.5, 1.0)
info_text.anchored_position = (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT)
grp.append(info_text) # index 5
location_text = adafruit_display_text.label.Label(terminalio.FONT, color=font_color,
text=f"{dictionary[i]['location']}")
location_text.anchor_point = (0.5, 1.0)
location_text.anchored_position = (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT - 12)
grp.append(location_text) # index 6
if status == "pre":
vs_text.text="VS"
info_text.text=dictionary[i]["date"]
# if it's active or final show score
else:
info_text.text=f"Clock: {dictionary[i]['clock']}"
vs_text.text=f"{dictionary[i]['score_home']} - {dictionary[i]['score_away']}"
grps.append(grp)
return grps
Clocks and First Fetch
Before the loop, the get_data() and make_gfx() functions are called to get the initial API info and graphics. ticks is used in the loop for timekeeping. The fetch_clock and display_clock are setup as ticks clocks.
# clock for fetching
fetch_timer = fetch_timer * 1000
# index and clock for updating display
display_index = 0
display_timer = display_timer * 1000
# initial data fetch
try:
games, just_fetched = get_data(SPORT_URL, games)
groups = make_gfx(games, groups)
display.root_group = groups[display_index]
# pylint: disable=broad-except
except Exception as Error:
print(f"Error: {Error}")
time.sleep(10)
gc.collect()
time.sleep(5)
microcontroller.reset()
# start clocks
fetch_clock = ticks_ms()
display_clock = ticks_ms()
The Loop
Two processes are happening concurrently in the loop with the help of ticks. The matrices are cycling through the display groups by advancing through the groups array. The API is fetched on a different timer. The make_gfx() function is called immediately after, which allows for the graphics being shown to be updated.
while True:
try:
if not just_fetched:
# garbage collection for display groups
gc.collect()
# fetch the json for the next team
games, just_fetched = get_data(SPORT_URL, games)
groups = make_gfx(games, groups)
# reset clocks
fetch_clock = ticks_add(fetch_clock, fetch_timer)
display_clock = ticks_add(display_clock, display_timer)
# update display seperate from API request
if ticks_diff(ticks_ms(), display_clock) >= display_timer:
print("updating display")
display.root_group = groups[display_index]
display_index = (display_index + 1) % len(games)
info_clock = ticks_ms()
display_clock = ticks_add(display_clock, display_timer)
# cleared for fetching after time has passed
if ticks_diff(ticks_ms(), fetch_clock) >= fetch_timer:
just_fetched = False
# pylint: disable=broad-except
except Exception as Error:
print(f"Error: {Error}")
time.sleep(10)
gc.collect()
time.sleep(5)
microcontroller.reset()
Page last edited April 28, 2026
Text editor powered by tinymce.
Wiring and Assembly
Making sure your matrices are laid out in the correct order can be confusing. Before plugging in any cables, lay them out to make sure they are oriented properly. Each matrix has arrow markings which you can use to help during layout.
- Matrix 1 - This matrix will have the Matrix Portal S3 plugged into its IDC port on the left. Its arrow markings will be pointing up and to the right.
- Matrix 2 - This matrix is placed to the right of Matrix 1. Its arrow markings will be pointing up and to the right.
- Matrix 3 - This matrix is placed below Matrix 2. Its arrow markings will be pointing down and to the left.
- Matrix 4 - This matrix is placed to the left of Matrix 3 and below Matrix 1. Its arrow markings will be pointing down and to the left.
Once you have your four matrices laid out in the correct order you can start plugging in the IDC cables.
Plug the last IDC cable into the left-hand port on the Matrix 3 and the right-hand port on the Matrix 4. This completes the data wiring for the matrices.
Place the center bracket over the intersection in the middle of the four matrices. Use the bracket to make sure that the matrices are aligned with each other. Secure it with four M3 screws.
Use four 2x1 brackets to join the matrices together to the left and right of the center bracket. Secure the brackets with M3 screws.
Use two 2x1 brackets to secure the matrices above and below the center bracket. Secure the brackets with M3 screws.
Gather two power cables that came with your matrices. Plug one of the cables into the two power inputs on the top two matrices. Plug the other cable into the two power inputs on the bottom two matrices.
Secure the positive cable (red wire) into the positive terminal on the DC jack adapter (labeled with a raised +). Secure the ground cable (black wire) into the negative terminal on the DC jack adapter (labeled with a raised -). Repeat this for the second power cable.
Now you can plug both sets of two matrices into 5V 4A power supplies.
Page last edited April 28, 2026
Text editor powered by tinymce.
Use
Update Your Time Zone
The ESPN API stores the timestamp for games in UTC. There is a function in the code that converts the UTC time to your defined time zone. You'll add your time zone UTC offset and time zone name into the timezone_info array at the top of the code:
# your timezone UTC offset and timezone name timezone_info = [-4, "EDT"]
Power up the Matrix Portal S3 via USB and the LED matrices with their power supplies. You'll see CircuitPython boot up, followed by showing the first result from the API request.
Page last edited April 28, 2026
Text editor powered by tinymce.