Objective
Bring the excitement of going to a sporting event in-person to the comfort of your own home! Using a Raspberry Pi connected to a 64x32 LED matrix, this miniature scoreboard pulls and displays live sports scores from the ESPN API. The scoreboard allows the user to select from many sports and teams, both collegiate and professional, using the built-in PiTFT touch screen. The scoreboard displays pre-game information, in-game scores, and post-game results.
Introduction
This project emulates a scoreboard by using a Raspberry Pi connected to a 64x32 LED matrix. Using
the touch
screen on the PiTFT, the user is able to select a team to display from collegiate and professional
hockey,
football, and basketball. The user can also
select to rotate between teams instead of displaying only one team. The system then queries the ESPN
API to
get live scores from the internet according
to the user's settings. Finally, the team names, logos, and scores are displayed on the LED matrix.
The
scoreboard displays a celebration whenever a team scores in Football or Hockey.
When the selected team is not currently in a live game, the scoreboard displays the results of the
previous
game or the date, time, and opponent of the next game, depending on which is closer in time. Using
cron, the service starts on boot and defaults to displaying Cornell Men's Hockey.
Demo Video
Design & Testing
Hardware Design and Testing
As seen in Figure 1, the hardware consists of three major components: a Raspberry Pi 4, a touchscreen PiTFT, and a 64x32 LED Matrix. The Raspberry Pi contains a 16 GB SD card with the Lab 3 kernel installed, and is connected to power and the PiTFT in the same way as in Lab 3. The LED Matrix requires an additional 13 GPIOs to connect, fortunately none of which conflict with the PiTFT. To power the LED Matrix, the included Power and Ground connector cable gets plugged into the back of the LED matrix on one end and screwed into a Female DC Power adapter on the other. Finally, the DC power adapter is plugged into a 5V 2A power cable.
Figure 1: System Overview
Table 1 shows the correct pinout for connecting the LED Matrix to the Raspberry Pi [1]. LED Matrix pins are numbered left to right, top to bottom.
Table 1: Raspberry Pi to LED Matrix Wiring
Pi Pin | Matrix Pin | Signal Name |
---|---|---|
GPIO 5 | 1 | Red Row 1 |
GPIO 13 | 2 | Green Row 1 |
GPIO 6 | 3 | Blue Row 1 |
GPIO 12 | 5 | Red Row 2 |
GPIO 16 | 6 | Green Row 2 |
GPIO 23 | 7 | Blue Row 2 |
GPIO 22 | 9 | Line Selection A |
GPIO 26 | 10 | Line Selection B |
GPIO 27 | 11 | Line Selection C |
GPIO 20 | 12 | Line Selection D |
GPIO 17 | 13 | CLK |
GPIO 21 | 14 | Latch |
GPIO 4 | 15 | Output Enable |
GND | 4, 8, 16 | GND |
Testing the hardware involved wiring everything up and seeing if the matrix would display simple text. Initially, one source gave incorrect information, saying Pin 12 on the LED matrix should be connected to GND. This wiring is only true for 32x16 LED matrices. For the larger 64x32 matrix, connecting this pin to GND resulted in the bottom half of the matrix being dark and the top half working perfectly. After changing Pin 12 to GPIO 20, this issue was resolved. Figure 2 shows an image of the system after it has been wired correctly.
Figure 2: Final System and Wiring
Software Design and Testing
Installation
The LED Matrix requires one additional Python module called rpi-rgb-led-matrix
which
contains
functions to draw text and images on the LED Matrix. To install the tool, download the shell script
from
Adafruit's official GitHub page here
[2]. Once
the script is downloaded, run the script and select "Adafruit RGB Matrix Bonnet" when prompted to
choose
between two different Raspberry Pi Wiring configurations. After rebooting, the packages are
installed and
import rgbmatrix
in Python will succeed.
Matrix Object
The Matrix Object, found in matrix.py
, is a general class for configuring the LED
matrix. The
class initialization configures the many rgbmatrix
options for the specific LED Matrix
that
differ from the library's default values. Rows were set to 32, columns set to 64, and brightness set
to 80.
Finally, rpi-gpio-slowdown
, which is used to ensure the fast RPi 4 does not write to
the LED
Matrix too quickly, is set to the maximum slowdown, 4. The initialization of the class also defines
small,
medium, and large fonts as well as black and white colors. After these values are set, the LED
Matrix is ready
to be used. This class also contains some helper functions to be used by the scoreboard to reduce
code
duplication. Resize logo takes an image and
resizes it to a new height. Draw box uses the led matrix draw_line
function to draw a
box with
given dimensions and color. Hex to RGB converts hex colors to RGB colors. Invert logo takes an
all-black
image, which cannot be displayed on the Matrix, and converts it to an all-white image. Finally,
ordinal takes
a number, like 1, and converts it to 1st.
Testing the matrix object involved displaying random text and images in various sizes and colors, as
seen in
Figure 3. At first, the text was
illegible, which was because rpi-gpio-slowdown
was not set to slow enough. After
correcting this
mistake, the text could be seen. The text was then drawn in different sizes and colors to test the
various
other functions and fonts. Finally, the Cornell Athletics logo was resized and displayed to ensure
the image
functions were working properly. One issue with resizing images was that there was heavy aliasing
when
downsizing an image to just 20x20. Using the Python Image Library (PIL), the resize image function
was edited
to include Bicubic Interpolation, which created smoother and more visually pleasing imaged on the
tiny LED
Matrix screen. After the Matrix object was complete, it was time to move onto making the actual
Scoreboard.
Figure 3: Testing the Matrix by Displaying Text and Images
The Scoreboard
Parsing Data
The Scoreboard calls the matrix object as a subclass, so it gets initialized with all of the options,
fonts,
colors, and helper functions configured previously. Upon launch, the scoreboard reads the default
values for
sport, league, and team from a configuration file called config.yml
. For this project,
these
values were configured respectively to be Hockey, Men's NCAA Hockey, and Cornell Hockey. The
scoreboard
proceeds to send a
HTTP GET request to ESPN's API using the provided values of sport and league. ESPN's API is
unofficial, but
other people have discovered that the format for such an API request is
http://site.api.espn.com/apis/site/v2/sports/{SPORT}/{LEAGUE}/scoreboard
[3]. This
project
implements the three sports Hockey, Basketball, and Football for the sport's American professional
league as
well as the men's and women's (where applicable) NCAA league.
The HTTP GET request to the API returns a JSON that must be parsed in order to display the scores on
the
scoreboard. This JSON parsing required lots of testing and trial and error, since the values
returned by the
API were sport-dependent and status dependent. For example, timeouts-remaining
is
applicable for
Basketball and Football but not for Hockey. However, if the game is not in progress but instead is
scheduled
or has completed, there is no key in the JSON for timeouts remaining. The strategy used to solve
this issue
was to inject default values for each key that is missing. Then, the JSON can be parsed without
throwing a
KeyNotFoundError
and crashing the entire script.
The JSON contains a list of each team in the league's next or most previous competition. The list is
looped
through until the team matches the team given in config.yml
. If for some reason that
team is not
present in the list, the first team's competition is instead returned.
Now that the competition has been extracted from the JSON, it can get parsed into custom structs,
which are
found in consts.py
. These structs, TeamData
and Situation, contain all of
the data
necessary to fully describe the competition. The competition's two teams get parsed into
TeamData
, which contains the team's colors, rank, record, logo, 3-4 character
abbreviation, and
score. Situation
contains the data about the competition, such as time, period,
possession,
timeouts remaining, last play, and ball location. While not all of these values apply to every
sport, setting
a value to None will cause it to not be inappropriately displayed. Now that all data has been
extracted from
the JSON, it is time to finally display the data on the LED Matrix.
Displaying Data
Data is displayed based on the status of the game which is one of pre-game, in-game, or post-game. If
the
game status is STATUS_SCHEDULED
then the game has
not started yet and there are no scores to display. The team logos, abbreviations, ranks, and
records are
displayed
along with the date and time of the event. Not all of this data can fit on the screen at once, as
shown
in Figure 4. Instead, data is cycled through in 8-second periods. The date of the matchup and
the team
abbreviations are shown for 4 seconds, then the time
of the matchup and the team records are shown for 4 seconds. The sport of the game is represented
by either a small football,
basketball, or hockey sticks in the center of the screen.
Figure 4: Pre-Game Display of a Cornell Hockey Matchup
During the game, when status is STATUS_IN_PROGRESS
is when the most data is needed to
fully
describe the situation. Therefore, when in-game, the
logos are shrunk to 1/4th the size as they were in pre-game and placed on the right side of the
screen. Boxes
encapsulating the logos, team abbreviations, and scores are drawn according to the team's color.
Timeouts
remaining and possession,
if applicable, are indicated with dashes and dots respectively, just as they are on a real
scoreboard. The
left side of the
screen lists the period and time remaining in the period, as well as the down & yards to go, and
location of
the ball if applicable. Figure 5 shows an in-game Cornell Hockey game and Figure 6
shows an
in-game NFL
game
which therefore also includes down & yards to go, location of the football, timeouts remaining for
each team,
and possession.
Figure 5: In-Game Display of a Cornell Hockey Matchup
Figure 6: In-Game Display of a Professional Football Matchup
When the previous_play
field during an in-game Hockey or Football matchup indicates
that points
have been scored, a celebration animation is displayed. The animation displays blinks the name of
the play
(goal,
touchdown, etc.), the name of the team, and a full-screen team logo in 1-second intervals. After 5
blinks, the
animation is over and the score of the game returns. The animation can be seen in Figure 7.
Figure 7: Scoring Animation for Cornell Hockey
At the end of the game, when status is STATUS_FINAL
, the final score must be displayed.
Shown in
Figure 8, the status of the game is seen at the top of the LED Matrix, with the logos in a
similar
position to
the pre-game view. The scores and the sport logo are shown in the center of the screen.
Figure 8: Final Score for Cornell Hockey
To test these various states and display configurations, JSONs were downloaded from the API and
loaded into
the Scoreboard instead of being pulled from the API in real-time. This hack kept all parameters
static so
modes could be tested and verified without changing values. One issue encountered is some team's
colors were
so bright, or white, that when using this color as a background for in-game display, the score could
not be
seen on top of it. The solution was to make a function that decreased the brightness of the
background
rectangle so the text on top could be seen better. Another quick and dirty solution is a
teams_to_invert
list in Scoreboard class. Members of this list, such as the Iowa
Hawkeyes, have
either black logos or white team color. After inversion, the team logo is changed to white and the
team color
is changed to black to promote better visibility of the team's logo and score.
Another problem faced occurred when running the system for an extended period of time on RedRover, the firewall started to block the POST requests to ESPN. The solution was to connect the Raspberry Pi to eduroam based on instructions from UCSC engineering [4].
Pygame on the PiTFT
While the scoreboard displays competition data, the PiTFT runs a touch-screen GUI using Pygame to
allow the
user to select
the sport, league, and team to display. Shown in Figure 9, the GUI offers three sets of arrow
keys, one
for each sport, league, and team. The options for league depend on the selection for sport, and the
options
for team depend on the selection for league. When the desired parameters are reached, the
SEND PARAMETERS
button is pressed to send the new values to the scoreboard. This button
updates
the global variables that are read by the scoreboard when pulling from the API and selecting a
competition
from the returned JSON.
Figure 9: Touch-Screen PyGame GUI for selecting Sport, League, and Team
The final button on the GUI, START DEMO
, starts the demo. When the button is pushed, a
the global
variable
in_demo
is set to True. In this state, instead of querying the ESPN API for new JSONs,
the JSONs
are pulled from the local filesystem. Pulling from the local filesystem to maintain consistency was
used for
previous testing, so the only additional code that had to be done was pull the correct JSON for the
given
state of the demo. The demo intends to highlight the four different phases of a Cornell Hockey game:
pre-game,
in-game, goal scored, and final. Shown in Figure 10, the user can click back and forth
between states
and go back to live scores by ending the demo when finished.
Figure 10: The System in Demo Mode
Given a sport and league, the GUI also allows the user to select "All" or a specific league instead of one team. If one of these special options are selected, the scoreboard will display each competition in the pulled JSON for 8 seconds, looping back to the beginning after all matchups have been displayed. Figure 11 shows the system when Ivy League men's basketball has been selected. Each upcoming men's basketball game involving an Ivy League is shown for 8 seconds in a loop.
Figure 11: Rotating Through All Teams in Ivy League Basketball
start.sh
and cron
Since this is an embedded system, the scoreboard must start up on boot and not require a keyboard
and mouse to
activate. Using cron, start.sh
is called on reboot and starts up the system. The
two-line
start.sh
accepts inputs for delay and username, allowing optional arguments for
brightness, size
of the matrix, and rpi-gpio-slowdown
. Line 1 delays for the given number of seconds to
allow the
system time to connect to internet before attempting to pull from the ESPN API. Line 2 is a sudo
call to
launch main.py
with the given input for username. The intention is to use absolute
filepaths for
both main.py
and the fonts used on the matrix, so the user must specify their username
if theirs
is different
than the default value of pi
.
Testing is straightforward, just as learned in lecture. By running and confirming
start.sh
works
from the /tmp
folder, it can be ensured that start.sh
will work from wherever cron runs it from.
Results
As a whole, everything performed as expected and the goals outlined in the initial project description were met. The scoreboard pulls live scores from the ESPN API and displays them on the LED Matrix as planned. The Raspberry Pi, PiTFT, and LED Matrix work together in harmony to provide an embedded system complete with a GUI and plenty of compute and memory to spare.
The scoreboard can transition between the three states of pre-game, in-game, and post-game without the need for human interaction. The demo mode works very well for showcasing the basic features of the system, but definitely could have been improved to showcase other sports or leagues. The extra features implemented, such as displaying other sports besides hockey, rotating through multiple teams, and flashing the team logo when a goal is scored, all served to make the end result even better. The ESPN API along with the 4 second refresh rate provided the perfect amount of delay when watching games. The scoreboard and television were in sync enough that the scoreboard was not spoiling the action by being ahead or unrelated to the action by being too far behind.
There were many bugs experienced when relying on an external API to generate the JSONs needed for displaying data. In leagues such as NCAA Women's Hockey, which is not nearly as popular as the NFL, ESPN is often missing games or details. For these less popular leagues, the pre-game and post-game results may be shown, but ESPN does not show live scores for in-game. Errors were handled very well, allowing top-notch performance and reliability. Default values were assigned when parsing JSONs so the script would never exit. Furthermore, if the user selects a team that is not found in the JSON, the scoreboard will simply display the first competition in the list for the selected sport and league.
Taking photos of the system was surprisingly difficult. Although impossible to see with the naked eye, the LED Matrix does not light up all LEDs at the same time. As seen in Figure 12, only half of the screen is lit up at any instant. Most pictures taken of the matrix look similar to this, and the only way to take good pictures of the Matrix was to decrease shutter speed for a longer exposure.
Figure 12: Most Photos of the System Have Missing Pixels
Hardware is not my expertise, and mounting/casing was admittedly an afterthought. Fortunately, the LED Matrix came with magnets which were screwed into the back. After taping the PiTFT and Pi in place under the LED Matrix, the entire scoreboard system could be magnetically suspended on any surface. The system is now an all-in-one, portable scoreboard ready to be mounted anywhere, as shown in Figure 13.
Figure 13: System Mounting
Conclusions
The goal of bringing the excitement of an in-person sporting event to the comfort of the home by means of a miniature scoreboard was met. The scoreboard pulls live scores from ESPN API and displays them in real-time to be viewed at a glance. The user can select from over 200 different teams across three different sports with the use of an intuitive touch-screen GUI. The user can also choose to cycle through all teams of a given sport and league or teams in a specific conference. The user can then watch one game on television while keeping up-to-date with other games occurring simultaneously.
At first, a 32x16 LED Matrix was attempted to be used for this project, which has just 1/4 the number of pixels as the 64x32 matrix. The smallest dimensions to make a 26-character alphabet is 4x6 so it only possible to display 8 characters on one line and less than 3 lines of text total [5]. Therefore, it was absolutely necessary to use the larger 64x32 matrix. A higher resolution matrix was considered but did not line up with the goals of this project, which was to bring a retro scoreboard feel to the home environment.
The scoreboard and GUI leverage cron to begin on reboot and function as expected without interruption.
Future Work
This project uses very little computation and memory from the Raspberry Pi 4. One hardware improvement to be made to the system that cuts down on cost is to use a Raspberry Pi Zero W. Continued work could include swapping out the $35 Raspberry Pi 4 for a $15 Raspberry Pi Zero W and verifying the system still works as expected.
One software feature that is nice to include in the future is to allow the user to rotate between a custom list of teams from various leagues. While the system currently allows the user to rotate between different games in a given league, such as rotating through Ivy League Basketball, the system does not allow for customization. The proposed feature allows the user to add teams to a list to be rotated through, such as all sports teams from a given city or university.
Finally, one thing that I wish I had time for is to add a speaker to the system. To more closely mimic in-person sporting events, a siren could go off whenever a goal is scored in hockey, and cheering or the team's anthem could be played whenever a touchdown is scored in football. This feature would require both a hardware and a software addition.
Budget and Bill of Materials
Item | Quantity | Total Cost ($) |
---|---|---|
Raspberry Pi 4 | 1 | 35 |
16 GB microSD Card | 1 | 8 |
15W USB-C Power Supply | 1 | 8 |
Resistive Touch PiTFT - 320x240 2.8" TFT+Touchscreen | 1 | 35 |
64x32 P4 RGB LED Matrix Panel | 1 | 36 |
Female DC Power adapter | 1 | 2 |
5V 2A DC Power Adapter | 1 | 9 |
Pin to Socket Jumper Cable | 16 | 2 |
TOTAL | -- | 135 |
ECE 5725 Basic Kit (Pi, PiTFT, SD Card, USB-C) | 1 | (86) |
TOTAL EXCLUDING ECE 5725 BASIC KIT | -- | 49 |
References
[1] Adafruit Industries, "Adafruit RGB Matrix Bonnet for Raspberry Pi," 2023. https://learn.adafruit.com/adafruit-rgb-matrix-bonnet-for-raspberry-pi/?view=all (accessed November 6, 2023).
[2] Adafruit Industries, "Raspberry-Pi-Installer-Scripts," 2023. https://github.com/adafruit/Raspberry-Pi-Installer-Scripts/blob/main/rgb-matrix.sh (accessed November 6, 2023).
[3] A. Easwaran, "ESPN hidden API Docs," 2019. https://gist.github.com/akeaswaran/b48b02f1c94f873c6655e7129910fc3b (accessed November 29, 2023).
[4] Internetworking Research Group — UCSC, "HOWTO Connect Raspberry Pi to Eduroam Wi-Fi," 2019. https://inrg.engineering.ucsc.edu/howto-connect-raspberry-to-eduroam/ (accessed November 10, 2023).
[5] robey, "A very tiny, monospace, bitmap font," 2010. https://robey.lag.net/2010/01/23/tiny-monospace-font.html (accessed December 6, 2023).
[6] Pygame, "pygame v2.6.0 documentation - pygame.display," 2023. https://www.pygame.org/docs/ref/display.html (accessed November 20, 2023)
[7] J. Skovira, “ECE 5725 Lab 3,” 2023. Lab 3_Fall2023_v1.pdf (accessed November 17, 2023).