Astronaut Count and Names
This project uses CircuitPython code on tan Adafruit PyPortal to show the current number of astronauts in space according to the thespacedevs.com API. A previous API used in this guide (open-notify.org) is no longer updated.
The PyPortal will do the following:
- Display a custom background .bmp image
- Check for the current number, and names of people in space
- Display the current number of astronauts and list their names and affiliation.
The code is set to update the display every hour at the moment. So it will not be accurate just after a launch or splashdown. This is to lessen the amount of data that is pulled from this free access server.
Install CircuitPython Code and Assets
In the embedded code element below, click on the Download: Project Zip link, and save the .zip archive file to your computer.
Then, uncompress the .zip file, it will unpack to a folder named PyPortal_Astronauts.
Copy the contents of the PyPortal_Astronauts directory to your PyPortal's CIRCUITPY drive.
This is what the final contents of the CIRCUITPY drive will look like:
Note the font file is different from the ones used in the previous version of the project and the background bitmap is also different (and new code, of course).
# SPDX-FileCopyrightText: 2019 Limor Fried for Adafruit Industries
# SPDX-FileCopyrightText: 2026 Anne Barela for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
This example will access the Space Devs astronaut API, display the number of
humans currently in space and their names and agency on a PyPortal screen.
"""
import gc
import json
import time
import board
import supervisor
from adafruit_pyportal import PyPortal
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label
from adafruit_portalbase.network import HttpError
# Set up where we'll be fetching data from
DATA_SOURCE = (
"https://ll.thespacedevs.com/2.2.0/astronaut/"
"?mode=list&in_space=true&limit=16&type=human"
)
# Determine the current working directory
cwd = ("/"+__file__).rsplit('/', 1)[0]
# Initialize the pyportal object - no json_path since we parse manually
pyportal = PyPortal(url=DATA_SOURCE,
status_neopixel=board.NEOPIXEL,
default_bg=cwd+"/astronauts_background.bmp")
gc.collect()
# Font for names and count label
names_font = bitmap_font.load_font(cwd+"/fonts/Helvetica-Bold-10.bdf")
names_font.load_glyphs(
b'abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789- ()'
)
# Display positions and colors
names_position = (6, 140)
names_color = 0xFF00FF
count_color = 0xFFFFFF
count_position = (180, 108)
# Layout constants
screen_width = 320
col_gap = 6
char_width = 7 # slightly wider estimate for Helvetica Bold proportional font
font_height = 12
max_rows = 8
# Agency name to abbreviation lookup
AGENCY_ABBREV = {
"National Aeronautics and Space Administration": "USA",
"Russian Federal Space Agency (ROSCOSMOS)": "RUS",
"European Space Agency": "ESA",
"China National Space Administration": "CHN",
"Japan Aerospace Exploration Agency": "JAX",
"Canadian Space Agency": "CSA",
"SpaceX": "SpX",
"Boeing": "Boe",
}
gc.collect()
def make_count_label(num_people):
lbl = Label(names_font, text="{} People in Space".format(num_people))
lbl.color = count_color
lbl.x = count_position[0]
lbl.y = count_position[1]
return lbl
def fetch_astronauts():
"""Fetch and parse astronaut data, filtering out non-humans."""
gc.collect()
raw = pyportal.fetch()
if isinstance(raw, str):
data = json.loads(raw)
else:
data = raw
humans = [
a for a in data["results"]
if a.get("type", {}).get("name") != "Non-Human"
]
num_people = len(humans)
del data
del raw
gc.collect()
return num_people, humans
def build_name_list(astronaut_list):
"""Build display strings from astronaut records."""
result = []
for a in astronaut_list:
agency_full = a.get("agency", "")
abbrev = AGENCY_ABBREV.get(agency_full, agency_full[:4])
result.append("%s (%s)" % (a["name"], abbrev))
return result
def calculate_columns(names):
"""Calculate column widths and positions based on name lengths.
Col1 names are truncated to fit; col2 start is fixed from col1 width.
"""
col1_x = names_position[0]
col2_names = names[max_rows:]
col2_max_px = (
max(len(n) * char_width for n in col2_names) if col2_names else 0
)
total_available = screen_width - col1_x - col_gap
if col2_names:
# Allocate col2 what it needs, give the rest to col1
col2_width = min(col2_max_px, total_available // 2)
col2_width = max(col2_width, 0)
col1_width = total_available - col2_width - col_gap
col2_x = col1_x + col1_width + col_gap
else:
col1_width = total_available
col2_x = screen_width # unused
# col1 max_chars is the hard truncation limit
max_chars_col1 = col1_width // char_width
max_chars_col2 = col2_width // char_width if col2_names else 0
return col2_x, max_chars_col1, max_chars_col2
def display_astronauts(num_people, names):
"""Create and append all labels, return list for later cleanup."""
label_list = []
# Count label
label_list.append(make_count_label(num_people))
pyportal.root_group.append(label_list[-1])
col2_x, max_chars_col1, max_chars_col2 = calculate_columns(names)
y_start = names_position[1]
for i, name in enumerate(names):
in_col2 = i >= max_rows
max_chars = max_chars_col2 if in_col2 else max_chars_col1
if len(name) > max_chars:
name = name[:max_chars - 1] + "~"
new_lbl = Label(names_font, text=name)
new_lbl.color = names_color
new_lbl.x = col2_x if in_col2 else names_position[0]
new_lbl.y = (
y_start + (i - max_rows) * font_height
if in_col2
else y_start + i * font_height
)
pyportal.root_group.append(new_lbl)
label_list.append(new_lbl)
return label_list
def clear_labels(label_list):
"""Remove all labels from the display group."""
for _lbl in label_list:
pyportal.root_group.pop()
# Main loop
while True:
try:
people_count, fetched_list = fetch_astronauts()
name_list = build_name_list(fetched_list)
del fetched_list
gc.collect()
print("People in space:", people_count)
for n in name_list:
print(" ", n)
except HttpError as e:
print("Rate limited, backing off! -", e)
time.sleep(300) # wait 5 minutes before retrying
supervisor.reload()
except (RuntimeError, KeyError, ValueError) as e:
print("Fetch error, retrying! -", e)
time.sleep(30)
supervisor.reload()
active_labels = display_astronauts(people_count, name_list)
time.sleep(3600) # display for 1 hour - data changes infrequently
clear_labels(active_labels)
gc.collect()
supervisor.reload()
How it Works
The PyPortal Astronauts Display does the following things to keep you in the know about non-Earthbound humans:
Background
First, it displays a bitmap graphic named astronauts_background.bmp as the screen's background. This is a 320 x 240 pixel RGB 16-bit raster graphic in .bmp format.
Font
To display information on the screen, the PyPortal code will use a bitmapped font overlayed on top of the background. The font used here is are bitmap fonts made from the Helvetica typeface. You can learn more about converting type in this guide.
In order to speed up the display of text, the pyportal.preload_font() command is used to place the needed glyphs into memory.
JSON
How does the PyPortal know how many people are in space, as well as their names and locations? Is it talking to them when we're not looking?
That would be awesome, but actually it's not the case. Instead, the PyPortal is fetching a JSON file from the thespacedevs.com API.
Here is a partial return showing just one astronaut - there is so much information! The count of astronauts is at the top (subtract 1 for the non-human "Starman" that SpaceX put up). Name and agency are used for each real person ("human").
Unlike the original project, the JSON data is too complex and large to parse with the PyPortal module so it's done separately.
Text Positioning
The number of astronauts in space has grown significantly and now requires two columns to display them all. A fair amount of code is used to put the names on the display in two columns. If the name and agency is too large, a ~ character is shown to indicate that.
Background Image
If you would like to create your own background, awesome! You'll want to save the file with these specifications:
- 320 x 240 pixels
- 16-bit RGB color (8-bits per channel)
- Save file as .bmp format
You can then copy the .bmp file to the root level of the CIRCUITPY drive. Make sure you refer to this new filename in the pyportal constructor line:
default_bg=cwd+"/astronaut_background.bmp"
Change that line to use the new filename name, such as:
default_bg=cwd+"/my_new_background.bmp"
Page last edited April 02, 2026
Text editor powered by tinymce.