Once you've finished setting up your QT Py ESP32-S3 with CircuitPython, you can access the code, images, font 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: 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import os
import time
import ssl
import board
import wifi
import socketpool
import microcontroller
import displayio
from adafruit_display_text.bitmap_label import Label
from adafruit_bitmap_font import bitmap_font
import adafruit_imageload
from fourwire import FourWire
import adafruit_requests
from adafruit_gc9a01a import GC9A01A
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
cad_url = ("https://ssd-api.jpl.nasa.gov/cad.api?"
"des=2024%20YR4&body=ALL&"
"date-min=2030-01-01&date-max=2060-01-01")
sentry_url = "https://ssd-api.jpl.nasa.gov/sentry.api?des=2024%20YR4"
# connect to wifi
try:
wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))
except TypeError:
print("Could not find WiFi info. Check your settings.toml file!")
raise
context = ssl.create_default_context()
with open("/ssd-api-jpl-nasa-gov-chain.pem", "rb") as certfile:
context.load_verify_locations(cadata=certfile.read())
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, context)
spi = board.SPI()
tft_cs = board.TX
tft_dc = board.RX
tft_reset = None
displayio.release_displays()
display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_reset)
display = GC9A01A(display_bus, width=240, height=240, auto_refresh=False)
main_group = displayio.Group()
display.root_group = main_group
bitmap_bg, palette_bg = adafruit_imageload.load("/earth_bg.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
grid_bg = displayio.TileGrid(bitmap_bg, pixel_shader=palette_bg)
main_group.append(grid_bg)
font = bitmap_font.load_font('/Arial-14.bdf')
name_area = Label(font, text="2024 YR4", color=0xFFFFFF, background_color=0x000000)
name_area.anchored_position = (display.width / 2, 0)
name_area.anchor_point = (0.5, 0.0)
date_area = Label(font, text="2032-12-22", color=0xFFFFFF, background_color=0x000000)
date_area.anchored_position = (display.width / 2, name_area.height+10)
date_area.anchor_point = (0.5, 0.0)
moon_area = Label(font, text="Moon: ", color=0xFFFFFF, background_color=0x000000)
moon_area.anchored_position = (display.width / 2, name_area.height+10 + date_area.height+5)
moon_area.anchor_point = (0.5, 0.0)
earth_area = Label(font, text="Earth: ", color=0xFFFFFF, background_color=0x000000)
earth_area.anchored_position = (display.width / 2, name_area.height+10 +
moon_area.height+5 +
date_area.height + 5)
earth_area.anchor_point = (0.5, 0.0)
impact_area = Label(font, text="Earth Impact: 0.0000%", color=0xFFFFFF, background_color=0x000000)
impact_area.anchored_position = (display.width / 2, name_area.height+10 +
moon_area.height+5 +
earth_area.height + 5 +
date_area.height + 5)
impact_area.anchor_point = (0.5, 0.0)
main_group.append(impact_area)
main_group.append(earth_area)
main_group.append(moon_area)
main_group.append(date_area)
main_group.append(name_area)
bit_asteroid, pal_asteroid = adafruit_imageload.load("/asteroid.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
asteroid = displayio.TileGrid(bit_asteroid, pixel_shader=pal_asteroid,
x = 25, y=100)
pal_asteroid.make_transparent(0)
main_group.append(asteroid)
def diagonal_travel(bitmap_object, start_x=-59, start_y=-59, end_x=240, end_y=240, delay=0.01):
# Set initial position
bitmap_object.x = start_x
bitmap_object.y = start_y
# Calculate total movement distance
distance_x = end_x - start_x
distance_y = end_y - start_y
# Calculate number of steps (use the larger distance)
steps = max(abs(distance_x), abs(distance_y)) // 1
# Calculate step size for each axis to maintain diagonal movement
step_x = distance_x / steps
step_y = distance_y / steps
# Animate the movement
for i in range(steps + 1):
# Update position
bitmap_object.x = int(start_x + (step_x * i))
bitmap_object.y = int(start_y + (step_y * i))
display.refresh()
# Pause to control animation speed
time.sleep(delay)
def au_to_miles(au):
# 1 AU = 92,955,807 miles
miles_per_au = 92955807
return au * miles_per_au
timer_clock = ticks_ms()
timer = 3600 * 1000
first_run = True
while True:
try:
if first_run or ticks_diff(ticks_ms(), timer_clock) >= timer:
sentry_response = requests.get(sentry_url)
sentry_json = sentry_response.json()
impact = sentry_json['summary']['ip']
sentry_response.close()
overall_ip = float(impact) * 100
cad_response = requests.get(cad_url)
cad_json = cad_response.json()
earth_distance = au_to_miles(float(cad_json['data'][0][4]))
earth_area.text = f"{cad_json['data'][0][10]}: {int(earth_distance)} mi"
moon_distance = au_to_miles(float(cad_json['data'][1][4]))
moon_area.text = f"{cad_json['data'][1][10]}: {int(moon_distance)} mi"
date = cad_json['data'][0][3]
date = date.split()
date_area.text = f"{date[0]}"
cad_response.close()
impact_area.text = f"Earth Impact: {overall_ip:.4f}%"
display.refresh()
timer_clock = ticks_add(timer_clock, timer)
diagonal_travel(asteroid, start_x=-45, start_y=300, end_x=300, end_y=-45)
time.sleep(0.1)
# pylint: disable=broad-except
except Exception as e:
print("Error:\n", str(e))
print("Resetting microcontroller in 10 seconds")
time.sleep(10)
microcontroller.reset()
Upload the Code and Libraries to the QT Py
After downloading the Project Bundle, plug your QT Py 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 QT Py's CIRCUITPY drive.
- lib folder
- code.py
- asteroid.bmp
- earth_bg.bmp
- Arial-14.bdf
Your QT Py ESP32-S3 CIRCUITPY drive should look like this after copying the lib folder, image files, font file 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 values for your CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD.Â
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here"
How the Code Works
The asteroid tracking information is available via two NASA JPL API's: the Sentry API that tracks objects that pose a threat to Earth and the Close Approach API that tracks objects that will pass near the planets in the solar system and our moon.
Both URLs are setup to track the 2024 YR4 asteroid that is estimated to make a close approach to the moon and Earth in 2032.
cad_url = ("https://ssd-api.jpl.nasa.gov/cad.api?"
"des=2024%20YR4&body=ALL&"
"date-min=2030-01-01&date-max=2060-01-01")
sentry_url = "https://ssd-api.jpl.nasa.gov/sentry.api?des=2024%20YR4"
# connect to wifi
try:
wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))
except TypeError:
print("Could not find WiFi info. Check your settings.toml file!")
raise
context = ssl.create_default_context()
with open("/ssd-api-jpl-nasa-gov-chain.pem", "rb") as certfile:
context.load_verify_locations(cadata=certfile.read())
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, context)
Graphics
The round display is connected via SPI with the EYESPI BFF cable. The background bitmap image positions the Earth in the bottom half of the screen. Five text objects are created to display the asteroid name, estimated date, the distance it will be from the moon, the distance it will be from Earth and the percentage of a likely impact with Earth. The second bitmap is a small asteroid. Its background color is made transparent with its palette (pal_asteroid.make_transparent(0)).
spi = board.SPI()
tft_cs = board.TX
tft_dc = board.RX
tft_reset = None
displayio.release_displays()
display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_reset)
display = GC9A01A(display_bus, width=240, height=240, auto_refresh=False)
main_group = displayio.Group()
display.root_group = main_group
bitmap_bg, palette_bg = adafruit_imageload.load("/earth_bg.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
grid_bg = displayio.TileGrid(bitmap_bg, pixel_shader=palette_bg)
main_group.append(grid_bg)
font = bitmap_font.load_font('/Arial-14.bdf')
name_area = Label(font, text="2024 YR4", color=0xFFFFFF, background_color=0x000000)
name_area.anchored_position = (display.width / 2, 0)
name_area.anchor_point = (0.5, 0.0)
date_area = Label(font, text="2032-12-22", color=0xFFFFFF, background_color=0x000000)
date_area.anchored_position = (display.width / 2, name_area.height+10)
date_area.anchor_point = (0.5, 0.0)
moon_area = Label(font, text="Moon: ", color=0xFFFFFF, background_color=0x000000)
moon_area.anchored_position = (display.width / 2, name_area.height+10 + date_area.height+5)
moon_area.anchor_point = (0.5, 0.0)
earth_area = Label(font, text="Earth: ", color=0xFFFFFF, background_color=0x000000)
earth_area.anchored_position = (display.width / 2, name_area.height+10 +
moon_area.height+5 +
date_area.height + 5)
earth_area.anchor_point = (0.5, 0.0)
impact_area = Label(font, text="Earth Impact: 0.0000%", color=0xFFFFFF, background_color=0x000000)
impact_area.anchored_position = (display.width / 2, name_area.height+10 +
moon_area.height+5 +
earth_area.height + 5 +
date_area.height + 5)
impact_area.anchor_point = (0.5, 0.0)
main_group.append(impact_area)
main_group.append(earth_area)
main_group.append(moon_area)
main_group.append(date_area)
main_group.append(name_area)
bit_asteroid, pal_asteroid = adafruit_imageload.load("/asteroid.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
asteroid = displayio.TileGrid(bit_asteroid, pixel_shader=pal_asteroid,
x = 25, y=100)
pal_asteroid.make_transparent(0)
main_group.append(asteroid)
Diagonally Across
The asteroid bitmap moves diagonally across the display every few seconds with the help of the diagonal_travel() function. You can change the starting and ending x and y coordinates to affect the direction of the bitmap.
def diagonal_travel(bitmap_object, start_x=-59, start_y=-59, end_x=240, end_y=240, delay=0.01):
# Set initial position
bitmap_object.x = start_x
bitmap_object.y = start_y
# Calculate total movement distance
distance_x = end_x - start_x
distance_y = end_y - start_y
# Calculate number of steps (use the larger distance)
steps = max(abs(distance_x), abs(distance_y)) // 1
# Calculate step size for each axis to maintain diagonal movement
step_x = distance_x / steps
step_y = distance_y / steps
# Animate the movement
for i in range(steps + 1):
# Update position
bitmap_object.x = int(start_x + (step_x * i))
bitmap_object.y = int(start_y + (step_y * i))
display.refresh()
# Pause to control animation speed
time.sleep(delay)
Convert Astronomical Units
Another helper function is au_to_miles() this converts astronomical units to miles. The API results are delivered in au.
def au_to_miles(au):
# 1 AU = 92,955,807 miles
miles_per_au = 92955807
return au * miles_per_au
The Loop
In the loop, ticks are used to keep track of time. Every hour, requests are made to the two API's. The retrieved data is used to update the text elements. Outside of ticks, the diagonal_travel() function is used to animate the asteroid bitmap across the screen. All of this is wrapped in a try/except to reboot the QT Py if there are any connection issues or errors.
while True:
try:
if first_run or ticks_diff(ticks_ms(), timer_clock) >= timer:
sentry_response = requests.get(sentry_url)
sentry_json = sentry_response.json()
impact = sentry_json['summary']['ip']
sentry_response.close()
overall_ip = float(impact) * 100
cad_response = requests.get(cad_url)
cad_json = cad_response.json()
earth_distance = au_to_miles(float(cad_json['data'][0][4]))
earth_area.text = f"{cad_json['data'][0][10]}: {int(earth_distance)} mi"
moon_distance = au_to_miles(float(cad_json['data'][1][4]))
moon_area.text = f"{cad_json['data'][1][10]}: {int(moon_distance)} mi"
date = cad_json['data'][0][3]
date = date.split()
date_area.text = f"{date[0]}"
cad_response.close()
impact_area.text = f"Earth Impact: {overall_ip:.4f}%"
display.refresh()
timer_clock = ticks_add(timer_clock, timer)
diagonal_travel(asteroid, start_x=-45, start_y=300, end_x=300, end_y=-45)
time.sleep(0.1)
# pylint: disable=broad-except
except Exception as e:
print("Error:\n", str(e))
print("Resetting microcontroller in 10 seconds")
time.sleep(10)
microcontroller.reset()
Page last edited April 07, 2025
Text editor powered by tinymce.