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)
Text editor powered by tinymce.