Once you've finished setting up your Qualia ESP32-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: 2023 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# Written by Liz Clark (Adafruit Industries) with OpenAI ChatGPT v4 Aug 3rd, 2023 build
# https://help.openai.com/en/articles/6825453-chatgpt-release-notes
# https://chat.openai.com/share/63cbe4c6-007f-4934-a458-a9c8a521620e
# https://chat.openai.com/share/674c0f13-bc78-4d1e-be79-3bc777e29991
import time
from math import pi, cos, sin
import os
import ssl
import wifi
import socketpool
import adafruit_requests
import board
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
import vectorio
import displayio
from adafruit_io.adafruit_io import IO_HTTP
from jepler_udecimal import Decimal
import keypad
from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font
from adafruit_qualia.graphics import Graphics, Displays
# timezone offset for calculating mars time
timezone = -5
key = keypad.Keys((board.A0,), value_when_pressed=False, pull=True)
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
aio_username = os.getenv('ADAFRUIT_AIO_USERNAME')
aio_key = os.getenv('ADAFRUIT_AIO_KEY')
context = ssl.create_default_context()
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, context)
io = IO_HTTP(aio_username, aio_key, requests)
earth_bitmap = displayio.OnDiskBitmap("/earth.bmp")
mars_bitmap = displayio.OnDiskBitmap("/mars.bmp")
graphics = Graphics(Displays.ROUND40, default_bg=None, auto_refresh=True)
earth_grid = displayio.TileGrid(earth_bitmap, pixel_shader=earth_bitmap.pixel_shader)
earth_group = displayio.Group()
earth_group.append(earth_grid)
mars_grid = displayio.TileGrid(mars_bitmap, pixel_shader=mars_bitmap.pixel_shader)
mars_group = displayio.Group()
mars_group.append(mars_grid)
def center(grid, bitmap):
# center the image
grid.x -= (bitmap.width - graphics.display.width) // 2
grid.y -= (bitmap.height - graphics.display.height) // 2
center(mars_grid, mars_bitmap)
center(earth_grid, earth_bitmap)
graphics.display.root_group = mars_group
# pointer using vectorio, first the hub
pointer_pal = displayio.Palette(4)
pointer_pal[0] = 0xff0000
pointer_pal[1] = 0x000000
pointer_pal[2] = 0x0000ff
pointer_pal[3] = 0xffffff
pointer_hub = vectorio.Circle(pixel_shader=pointer_pal, radius=26, x=0, y=0)
pointer_hub.x = graphics.display.width // 2
pointer_hub.y = graphics.display.height // 2
# minute hand
mw = 23
mh = 225
min_points = [(mw,0), (mw,-mh), (-mw,-mh), (-mw,0)]
min_hand = vectorio.Polygon(pixel_shader=pointer_pal, points=min_points, x=0,y=0)
min_hand.x = graphics.display.width // 2
min_hand.y = graphics.display.height // 2
mars_group.append(min_hand)
earth_group.append(min_hand)
# hour hand
hw = 25
hh = 175
hour_points = [(hw,0), (hw,-hh), (-hw,-hh), (-hw,0)]
hour_hand = vectorio.Polygon(pixel_shader=pointer_pal, points=hour_points,
x=0, y=0, color_index=1)
hour_hand.x = graphics.display.width // 2
hour_hand.y = graphics.display.height // 2
mars_group.append(hour_hand)
earth_group.append(hour_hand)
# add numbers to the clock face
def calculate_number_position(number, center_x, center_y, radius):
angle = (360 / 12) * (number - 3) # -3 adjusts the angle to start at 12 o'clock
rad_angle = pi * angle / 180
if number >=8:
x = int(center_x + cos(rad_angle) * radius-40)
x = int(center_x + cos(rad_angle) * radius)
y = int(center_y + sin(rad_angle) * radius)
return x, y
clock_face_numbers = {i: calculate_number_position(i, graphics.display.width // 2,
graphics.display.height // 2, 300) for i in range(1, 13)}
font_file = "/Roboto-Regular-47.pcf"
for i in range(1, 13):
mars_c = vectorio.Circle(pixel_shader=pointer_pal, radius=30, x=clock_face_numbers[i][0]+12,
y=clock_face_numbers[i][1], color_index=1)
earth_c = vectorio.Circle(pixel_shader=pointer_pal, radius=30, x=clock_face_numbers[i][0]+12,
y=clock_face_numbers[i][1], color_index=3)
if i >= 10:
mars_c.x = mars_c.x + 14
earth_c.x = earth_c.x + 14
mars_group.append(mars_c)
earth_group.append(earth_c)
text = str(i)
font = bitmap_font.load_font(font_file)
mars_text = label.Label(font, text=text, color=0xFFFFFF)
earth_text = label.Label(font, text=text, color=0x000000)
mars_text.x = clock_face_numbers[i][0]
mars_text.y = clock_face_numbers[i][1]
earth_text.x = clock_face_numbers[i][0]
earth_text.y = clock_face_numbers[i][1]
mars_group.append(mars_text)
earth_group.append(earth_text)
mars_group.append(pointer_hub)
earth_group.append(pointer_hub)
# get time from adafruit io
# called once an hour in the loop
def update_time():
print("time")
now = io.receive_time()
return now
def convert_time(the_time):
h = the_time[3]
if h >= 12:
h -= 12
a = "PM"
else:
a = "AM"
if h == 0:
h = 12
return h, a
# get mars time
def mars_time():
dt = io.receive_time()
print(dt)
utc_offset = 3600 * -timezone
tai_offset = 37
millis = time.mktime(dt)
unix_timestamp = millis + utc_offset
# Convert to MSD
msd = (unix_timestamp + tai_offset) / Decimal("88775.244147") + Decimal("34127.2954262")
print(msd)
# Convert MSD to MTC
mtc = (msd % 1) * 24
mtc_hours = int(mtc)
mtc_minutes = int((mtc * 60) % 60)
mtc_seconds = int(((mtc * 3600)) % 60)
print(f"Mars Time: {mtc_hours:02d}:{mtc_minutes:02d}:{mtc_seconds:02d}")
return mtc_minutes, mtc_hours
def time_angle(m, the_hour):
m_offset = 25 if 12 <= m < 18 or 42 <= m < 48 else 5
h_offset = 25 if 2 <= the_hour % 12 < 4 or 8 <= the_hour % 12 < 10 else 5
# Adjusted angle calculation for minute hand
theta_minute = 360 - (m / 60) * 360
theta_hour = ((the_hour / 12) + (m / (12 * 60))) * 360
# Calculate coordinates for minute hand (mirrored)
minute_x = -int(cos(pi * (theta_minute - 90) / 180) * mh)
minute_y = int(sin(pi * (theta_minute + 90) / 180) * mh)
hour_x = int(cos(pi * (theta_hour - 90) / 180) * hh)
hour_y = int(sin(pi * (theta_hour + 90) / 180) * hh)
min_hand.points = [(mw, 0), (minute_x + m_offset, -minute_y),
(minute_x - m_offset, -minute_y), (-mw, 0)]
hour_hand.points = [(hw, 0), (hour_x + h_offset, -hour_y),
(hour_x - h_offset, -hour_y), (-hw, 0)]
clock_timer = 1 * 1000
clock_clock = ticks_ms()
clock = update_time()
hour, am_pm = convert_time(clock)
tick = clock[5]
minute = clock[4]
mars_min, mars_hour = mars_time()
show_earth = True
time_angle(minute, hour)
while True:
event = key.events.get()
# swap between earth or mars time
if event:
if event.pressed:
print("updating display")
show_earth = not show_earth
# update background image
# change minute hand color depending on background
if show_earth:
if min_hand.color_index != 2:
time_angle(minute, hour)
graphics.display.root_group = earth_group
min_hand.color_index = 2
pointer_hub.color_index = 2
else:
if min_hand.color_index != 0:
time_angle(mars_min, mars_hour)
graphics.display.root_group = mars_group
min_hand.color_index = 0
pointer_hub.color_index = 0
# use ticks for timekeeping
# every minute update clock hands
# recheck IO time every hour
if ticks_diff(ticks_ms(), clock_clock) >= clock_timer:
tick += 1
if tick > 59:
tick = 0
minute += 1
if minute > 59:
clock = update_time()
hour, am_pm = convert_time(clock)
tick = clock[5]
minute = clock[4]
print(f"{hour}:{minute:02} {am_pm}")
mars_min, mars_hour = mars_time()
if show_earth:
time_angle(minute, hour)
else:
time_angle(mars_min, mars_hour)
clock_clock = ticks_add(clock_clock, clock_timer)
Upload the Code and Libraries to the Qualia ESP32-S3
After downloading the Project Bundle, plug your Qualia ESP32-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 Qualia ESP32-S3's CIRCUITPY drive.
- lib folder
- code.py
- earth.bmp
- mars.bmp
- Roboto-Regular-47.pcf
Your Qualia ESP32-S3 CIRCUITPY drive should look like this after copying the lib folder, earth.bmp file, mars.bmp file, Roboto-Regular-47.pcf file and the code.py file.
Install the udecimal Library from the Community Bundle
This project uses one additional library from the Community Bundle: the jepler_udecimal library. You'll need to download the Community Bundle and copy the /jepler_udecimal library folder to your /lib folder or use circup to install with:
circup install jepler-circuitpython-udecimal
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 ADAFRUIT_AIO_USERNAME, ADAFRUIT_AIO_KEY, CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD.
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here" ADAFRUIT_AIO_USERNAME = "your-io-username-here" ADAFRUIT_AIO_KEY = "your-io-key-here"
How the CircuitPython Code Works
At the top of the code, you'll edit the timezone variable to equal your UTC time zone offset. This is used when calculating Mars time (MTC).
# timezone offset for calculating mars time timezone = -5
After that, pin A0 is setup as a keypad object. Pressing the button attached to this pin will switch the display between Earth and Mars time. Then, the board connects to your WiFi SSID and Adafruit IO.
key = keypad.Keys((board.A0,), value_when_pressed=False, pull=True)
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
print(f"Connected to {os.getenv('CIRCUITPY_WIFI_SSID')}")
aio_username = os.getenv('ADAFRUIT_AIO_USERNAME')
aio_key = os.getenv('ADAFRUIT_AIO_KEY')
context = ssl.create_default_context()
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, context)
io = IO_HTTP(aio_username, aio_key, requests)
Graphics
The display is instantiated with the Qualia Graphics library. Then, the Earth and Mars are loaded as OnDiskBitmap's. Display groups are created for each planet.
earth_bitmap = displayio.OnDiskBitmap("/earth.bmp")
mars_bitmap = displayio.OnDiskBitmap("/mars.bmp")
graphics = Graphics(Displays.ROUND40, default_bg=None, auto_refresh=True)
earth_grid = displayio.TileGrid(earth_bitmap, pixel_shader=earth_bitmap.pixel_shader)
earth_group = displayio.Group()
earth_group.append(earth_grid)
mars_grid = displayio.TileGrid(mars_bitmap, pixel_shader=mars_bitmap.pixel_shader)
mars_group = displayio.Group()
mars_group.append(mars_grid)
def center(grid, bitmap):
# center the image
grid.x -= (bitmap.width - graphics.display.width) // 2
grid.y -= (bitmap.height - graphics.display.height) // 2
center(mars_grid, mars_bitmap)
center(earth_grid, earth_bitmap)
graphics.display.root_group = mars_group
vectorio shapes are used for the clock face graphics. A circle is created for the center of the display for the minute and hour hands to fan out from.
# pointer using vectorio, first the hub pointer_pal = displayio.Palette(4) pointer_pal[0] = 0xff0000 pointer_pal[1] = 0x000000 pointer_pal[2] = 0x0000ff pointer_pal[3] = 0xffffff pointer_hub = vectorio.Circle(pixel_shader=pointer_pal, radius=26, x=0, y=0) pointer_hub.x = graphics.display.width // 2 pointer_hub.y = graphics.display.height // 2
The minute and hour hands are polygon shapes. The clock hands are added to both display groups.
# minute hand
mw = 23
mh = 225
min_points = [(mw,0), (mw,-mh), (-mw,-mh), (-mw,0)]
min_hand = vectorio.Polygon(pixel_shader=pointer_pal, points=min_points, x=0,y=0)
min_hand.x = graphics.display.width // 2
min_hand.y = graphics.display.height // 2
mars_group.append(min_hand)
earth_group.append(min_hand)
# hour hand
hw = 25
hh = 175
hour_points = [(hw,0), (hw,-hh), (-hw,-hh), (-hw,0)]
hour_hand = vectorio.Polygon(pixel_shader=pointer_pal, points=hour_points,
x=0, y=0, color_index=1)
hour_hand.x = graphics.display.width // 2
hour_hand.y = graphics.display.height // 2
mars_group.append(hour_hand)
earth_group.append(hour_hand)
The clock face numbers are placed around the perimeter of the circular display. These coordinates are calculated with the calculate_number_position() function. In a for loop, vectorio circles are created to act as a background for the clock numbers. The clock numbers are added as text labels. There are two instances of these circle and number pairs created for the display groups. The colors differ between the two to better complement the planet backgrounds.
# add numbers to the clock face
def calculate_number_position(number, center_x, center_y, radius):
angle = (360 / 12) * (number - 3) # -3 adjusts the angle to start at 12 o'clock
rad_angle = pi * angle / 180
if number >=8:
x = int(center_x + cos(rad_angle) * radius-40)
x = int(center_x + cos(rad_angle) * radius)
y = int(center_y + sin(rad_angle) * radius)
return x, y
clock_face_numbers = {i: calculate_number_position(i, graphics.display.width // 2,
graphics.display.height // 2, 300) for i in range(1, 13)}
font_file = "/Roboto-Regular-47.pcf"
for i in range(1, 13):
mars_c = vectorio.Circle(pixel_shader=pointer_pal, radius=30, x=clock_face_numbers[i][0]+12,
y=clock_face_numbers[i][1], color_index=1)
earth_c = vectorio.Circle(pixel_shader=pointer_pal, radius=30, x=clock_face_numbers[i][0]+12,
y=clock_face_numbers[i][1], color_index=3)
if i >= 10:
mars_c.x = mars_c.x + 14
earth_c.x = earth_c.x + 14
mars_group.append(mars_c)
earth_group.append(earth_c)
text = str(i)
font = bitmap_font.load_font(font_file)
mars_text = label.Label(font, text=text, color=0xFFFFFF)
earth_text = label.Label(font, text=text, color=0x000000)
mars_text.x = clock_face_numbers[i][0]
mars_text.y = clock_face_numbers[i][1]
earth_text.x = clock_face_numbers[i][0]
earth_text.y = clock_face_numbers[i][1]
mars_group.append(mars_text)
earth_group.append(earth_text)
Time Functions
The internet time is retrieved using the io.receive_time() function. This returns a struct_time timestamp in the same time zone as your IP address.
# get time from adafruit io
# called once an hour in the loop
def update_time():
print("time")
now = io.receive_time()
return now
The convert_time() function is used to convert the struct_time from 24 hour to 12 hour time.
def convert_time(the_time):
h = the_time[3]
if h >= 12:
h -= 12
a = "PM"
else:
a = "AM"
if h == 0:
h = 12
return h, a
The mars_time() function converts the current time to the Unix timestamp and calculates coordinated Mars time (MTC). This function makes use of the jepler_udecimal library. This library is a subset of the CPython Decimal library, which is designed for arithmetic and greater accuracy over float numbers. CircuitPython does not have an accurate enough float accuracy for this calculation otherwise.
A more in-depth explanation on calculating Mars time with CircuitPython can be found in this Playground Note.
# get mars time
def mars_time():
dt = io.receive_time()
print(dt)
utc_offset = 3600 * -timezone
tai_offset = 37
millis = time.mktime(dt)
unix_timestamp = millis + utc_offset
# Convert to MSD
msd = (unix_timestamp + tai_offset) / Decimal("88775.244147") + Decimal("34127.2954262")
print(msd)
# Convert MSD to MTC
mtc = (msd % 1) * 24
mtc_hours = int(mtc)
mtc_minutes = int((mtc * 60) % 60)
mtc_seconds = int(((mtc * 3600)) % 60)
print(f"Mars Time: {mtc_hours:02d}:{mtc_minutes:02d}:{mtc_seconds:02d}")
return mtc_minutes, mtc_hours
The time_angle() function calculates the angle of the minute and hour hand polygon shapes by updating their coordinates.
def time_angle(m, the_hour):
m_offset = 25 if 12 <= m < 18 or 42 <= m < 48 else 5
h_offset = 25 if 2 <= the_hour % 12 < 4 or 8 <= the_hour % 12 < 10 else 5
# Adjusted angle calculation for minute hand
theta_minute = 360 - (m / 60) * 360
theta_hour = ((the_hour / 12) + (m / (12 * 60))) * 360
# Calculate coordinates for minute hand (mirrored)
minute_x = -int(cos(pi * (theta_minute - 90) / 180) * mh)
minute_y = int(sin(pi * (theta_minute + 90) / 180) * mh)
hour_x = int(cos(pi * (theta_hour - 90) / 180) * hh)
hour_y = int(sin(pi * (theta_hour + 90) / 180) * hh)
min_hand.points = [(mw, 0), (minute_x + m_offset, -minute_y),
(minute_x - m_offset, -minute_y), (-mw, 0)]
hour_hand.points = [(hw, 0), (hour_x + h_offset, -hour_y),
(hour_x - h_offset, -hour_y), (-hw, 0)]
The Loop
In the loop, if the button attached to A0 is pressed, then the display swaps to either show the Earth time or Mars time. When the display swaps, the background images changes, the clock hand colors change and the associated time is displayed.
while True:
event = key.events.get()
# swap between earth or mars time
if event:
if event.pressed:
print("updating display")
show_earth = not show_earth
# update background image
# change minute hand color depending on background
if show_earth:
if min_hand.color_index != 2:
time_angle(minute, hour)
graphics.display.root_group = earth_group
min_hand.color_index = 2
pointer_hub.color_index = 2
else:
if min_hand.color_index != 0:
time_angle(mars_min, mars_hour)
graphics.display.root_group = mars_group
min_hand.color_index = 0
pointer_hub.color_index = 0
Time Keeping
After the initial time fetch, ticks is used to keep time. Every minute, the angle of the clock hands are updated. Every hour, Adafruit IO is pinged to update the time to keep ticks accurate.
# use ticks for timekeeping
# every minute update clock hands
# recheck IO time every hour
if ticks_diff(ticks_ms(), clock_clock) >= clock_timer:
tick += 1
if tick > 59:
tick = 0
minute += 1
if minute > 59:
clock = update_time()
hour, am_pm = convert_time(clock)
tick = clock[5]
minute = clock[4]
print(f"{hour}:{minute:02} {am_pm}")
mars_min, mars_hour = mars_time()
if show_earth:
time_angle(minute, hour)
else:
time_angle(mars_min, mars_hour)
clock_clock = ticks_add(clock_clock, clock_timer)
Page last edited January 22, 2025
Text editor powered by tinymce.