Once you've finished setting up your board with CircuitPython, you can access the project code, assets and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button at the top of the code window below.
It will download to your computer as a zipped folder, containing two sets of folders, one each for the current and previous major versions of CircuitPython.
Use the newest version included in the Project Bundle.
# SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries
# SPDX-FileCopyrightText: 2024 Tyeth Gundry for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import os
import time
import wifi
import board
import displayio
import supervisor
import adafruit_connection_manager
import adafruit_requests
from adafruit_io.adafruit_io import IO_HTTP
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import bitmap_label
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
## See TZ Identifier column at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
## If you want to set the timezone, you can do so with the following code, which
## attempts to get timezone from settings.toml or defaults to New York
timezone = os.getenv("ADAFRUIT_AIO_TIMEZONE", "America/New_York")
## Or instead rely on automatic timezone detection based on IP Address
# timezone = None
## The time of the thing!
EVENT_YEAR = 2024
EVENT_MONTH = 8
EVENT_DAY = 16
EVENT_HOUR = 0
EVENT_MINUTE = 0
## we'll make a python-friendly structure
event_time = time.struct_time(
(
EVENT_YEAR,
EVENT_MONTH,
EVENT_DAY,
EVENT_HOUR,
EVENT_MINUTE,
0, # we don't track seconds
-1, # we dont know day of week/year or DST
-1,
False,
)
)
print("Connecting to WiFi...")
wifi.radio.connect(
os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)
## Initialize a requests session using the newer connection manager
## See https://adafruit-playground.com/u/justmobilize/pages/adafruit-connection-manager
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)
## Create an instance of the Adafruit IO HTTP client
io = IO_HTTP(
os.getenv("ADAFRUIT_AIO_USERNAME"), os.getenv("ADAFRUIT_AIO_KEY"), requests
)
## Setup display and size appropriate assets
if board.board_id == "adafruit_qualia_s3_rgb666":
# Display Initialisation for 3.2" Bar display (320x820)
from qualia_bar_display_320x820 import setup_display
display = setup_display()
display.rotation = 90 # Rotate the display
BITMAP_FILE = "/circuitpython_day_2024_820x260_16bit.bmp"
FONT_FILE = "/font_free_mono_bold_48.pcf"
FONT_Y_OFFSET = 30
blinka_bitmap = displayio.OnDiskBitmap(BITMAP_FILE)
PIXEL_SHADER = displayio.ColorConverter(
input_colorspace=displayio.Colorspace.RGB565
)
else:
# Setup built-in display
display = board.DISPLAY
BITMAP_FILE = "/cpday_tft.bmp"
FONT_FILE = "/Helvetica-Bold-16.pcf"
FONT_Y_OFFSET = 13
PIXEL_SHADER = displayio.ColorConverter()
blinka_bitmap = displayio.OnDiskBitmap(BITMAP_FILE)
PIXEL_SHADER = blinka_bitmap.pixel_shader
group = displayio.Group()
font = bitmap_font.load_font(FONT_FILE)
blinka_grid = displayio.TileGrid(blinka_bitmap, pixel_shader=blinka_bitmap.pixel_shader)
scrolling_label = bitmap_label.Label(font, text=" ", y=display.height - FONT_Y_OFFSET)
group.append(blinka_grid)
group.append(scrolling_label)
display.root_group = group
display.auto_refresh = False
refresh_clock = ticks_ms()
refresh_timer = 3600 * 1000 # 1 hour
clock_clock = ticks_ms()
clock_timer = 1000
scroll_clock = ticks_ms()
scroll_timer = 50
first_run = True
finished = False
triggered = False
while True:
# only query the online time once per hour (and on first run)
if ticks_diff(ticks_ms(), refresh_clock) >= refresh_timer or first_run:
try:
print("Getting time from internet!")
now = time.struct_time(io.receive_time(timezone))
print(now)
total_seconds = time.mktime(now)
refresh_clock = ticks_add(refresh_clock, refresh_timer)
except Exception as e: # pylint: disable=broad-except
print("Some error occured, retrying via supervisor.reload in 5seconds! -", e)
time.sleep(5)
# Normally calling microcontroller.reset() would be the way to go, but due to
# a bug causing a reset into tinyUF2 bootloader mode we're instead going to
# disconnect wifi to ensure fresh connection + use supervisor.reload()
wifi.radio.enabled = False
supervisor.reload()
if ticks_diff(ticks_ms(), clock_clock) >= clock_timer:
remaining = time.mktime(event_time) - total_seconds
if remaining < 0:
# calculate time since event
remaining = abs(remaining)
secs_remaining = -(remaining % 60)
remaining //= 60
mins_remaining = -(remaining % 60)
remaining //= 60
hours_remaining = -(remaining % 24)
remaining //= 24
days_remaining = -remaining
finished = True
if not first_run and days_remaining == 0:
scrolling_label.text = (
"It's CircuitPython Day 2024! The snakiest day of the year!"
)
# Check for the moment of the event to trigger something (a NASA snake launch)
if not triggered and (
hours_remaining == 0
and mins_remaining == 0
and secs_remaining <= 1
# Change at/after xx:yy:01 seconds so we've already updated the display
):
# send a signal to an adafruit IO feed, where an Action is listening
print("Launch the snakes! (sending message to Adafruit IO)")
triggered = True
io.send_data("cpday-countdown", "Launch the snakes!")
else:
# calculate time until event
secs_remaining = remaining % 60
remaining //= 60
mins_remaining = remaining % 60
remaining //= 60
hours_remaining = remaining % 24
remaining //= 24
days_remaining = remaining
if not finished or (finished and days_remaining < 0):
# Add 1 to negative days_remaining to count from end of day instead of start
if days_remaining < 0:
days_remaining += 1
# Update the display with current countdown value
scrolling_label.text = (
f"{days_remaining} DAYS, {hours_remaining} HOURS,"
+ f"{mins_remaining} MINUTES & {secs_remaining} SECONDS"
)
total_seconds += 1
clock_clock = ticks_add(clock_clock, clock_timer)
if ticks_diff(ticks_ms(), scroll_clock) >= scroll_timer:
scrolling_label.x -= 1
if scrolling_label.x < -(scrolling_label.width + 5):
scrolling_label.x = display.width + 2
display.refresh()
scroll_clock = ticks_add(scroll_clock, scroll_timer)
first_run = False
Upload the Code and Libraries to the Board
After downloading the Project Bundle, plug your board 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 board's CIRCUITPY drive.
- lib folder
- code.py
- cpday_tft.bmp
- Helvetica-Bold-16.pcf
Additionally if using the Qualia board then copy these files too:
- font_free_mono_bold_48.pcf
- circuitpython_day_2024_820x260_16bit.bmp
- qualia_bar_display_320x820.py
Your board's CIRCUITPY drive should look similar to this after copying the lib folder, image files (.bmp), font files (.pcf), and the two .py circuitpython code files.
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 CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD, along with your Adafruit IO details (username and key), and optionally a time zone (or edit the code.py file).
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here" ADAFRUIT_AIO_USERNAME = "your-adafruit-io-username" ADAFRUIT_AIO_KEY = "your-super-secret-alpha-numeric-key" ADAFRUIT_AIO_TIMEZONE = "GB"
How the CircuitPython Code Works
At the top of the code, you'll edit timezone to reflect your location, or alternatively enter it in the settings.toml file. The event time is also set up. In this case, it's August 16, 2024 at midnight.
## See TZ Identifier column at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
## If you want to set the timezone, you can do so with the following code, which
## attempts to get timezone from settings.toml or defaults to New York
timezone = os.getenv("ADAFRUIT_AIO_TIMEZONE", "America/New_York")
## Or instead rely on automatic timezone detection based on IP Address
# timezone = None
## The time of the thing!
EVENT_YEAR = 2024
EVENT_MONTH = 8
EVENT_DAY = 16
EVENT_HOUR = 0
EVENT_MINUTE = 0
## we'll make a python-friendly structure
event_time = time.struct_time(
(
EVENT_YEAR,
EVENT_MONTH,
EVENT_DAY,
EVENT_HOUR,
EVENT_MINUTE,
0, # we don't track seconds
-1, # we dont know day of week/year or DST
-1,
False,
)
)
WiFi and IO_HTTP
WiFi is setup along with an Adafruit IO instance to represent the HTTP API (IO_HTTP). There is some additional setup of the requests library used by Adafruit IO, handled by the new Adafruit Connection Manager library. The IO_HTTP class has a method to receive_time and will take care of the timing for this project. Your timezone is passed to the receive_time request to reflect the time in your location.
print("Connecting to WiFi...")
wifi.radio.connect(
os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)
## Initialize a requests session using the newer connection manager
## See https://adafruit-playground.com/u/justmobilize/pages/adafruit-connection-manager
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)
## Create an instance of the Adafruit IO HTTP client
io = IO_HTTP(
os.getenv("ADAFRUIT_AIO_USERNAME"), os.getenv("ADAFRUIT_AIO_KEY"), requests
)
Graphics
Next are the display objects for the external display attached to the Qualia, which calls out to a second file to handle the external display setup, or for any board with a built-in display. This takes care of the background bitmap graphic, font, and text element.
## Setup display and size appropriate assets
if board.board_id == "adafruit_qualia_s3_rgb666":
# Display Initialisation for 3.2" Bar display (320x820)
from qualia_bar_display_320x820 import setup_display
display = setup_display()
display.rotation = 90 # Rotate the display
BITMAP_FILE = "/circuitpython_day_2024_820x260_16bit.bmp"
FONT_FILE = "/font_free_mono_bold_48.pcf"
FONT_Y_OFFSET = 30
blinka_bitmap = displayio.OnDiskBitmap(BITMAP_FILE)
PIXEL_SHADER = displayio.ColorConverter(
input_colorspace=displayio.Colorspace.RGB565
)
else:
# Setup built-in display
display = board.DISPLAY
BITMAP_FILE = "/cpday_tft.bmp"
FONT_FILE = "/Helvetica-Bold-16.pcf"
FONT_Y_OFFSET = 13
PIXEL_SHADER = displayio.ColorConverter()
blinka_bitmap = displayio.OnDiskBitmap(BITMAP_FILE)
PIXEL_SHADER = blinka_bitmap.pixel_shader
group = displayio.Group()
font = bitmap_font.load_font(FONT_FILE)
blinka_grid = displayio.TileGrid(blinka_bitmap, pixel_shader=blinka_bitmap.pixel_shader)
scrolling_label = bitmap_label.Label(font, text=" ", y=display.height - FONT_Y_OFFSET)
group.append(blinka_grid)
group.append(scrolling_label)
display.root_group = group
display.auto_refresh = False
Time is Ticking
Finally, three separate ticks timers are created for timekeeping in the loop, along with some variables to hold our state. One boolean variable for if it's the first iteration through the loop (first_run), another for if the event has occurred (finished), and the last one to say if we have sent a message to Adafruit IO to signify the start of the event (triggered).
refresh_clock = ticks_ms() refresh_timer = 3600 * 1000 # 1 hour clock_clock = ticks_ms() clock_timer = 1000 scroll_clock = ticks_ms() scroll_timer = 50 first_run = True finished = False triggered = False
The Loop
In the loop, the time is fetched from the Adafruit IO Time service every hour and stored in now. now is converted to seconds using time.mktime(now). This lets you calculate how much time is remaining until the event.
# only query the online time once per hour (and on first run)
if ticks_diff(ticks_ms(), refresh_clock) >= refresh_timer or first_run:
try:
print("Getting time from internet!")
now = time.struct_time(io.receive_time(timezone))
print(now)
total_seconds = time.mktime(now)
refresh_clock = ticks_add(refresh_clock, refresh_timer)
except Exception as e: # pylint: disable=broad-except
print("Some error occured, retrying via reset in 15seconds! -", e)
time.sleep(15)
microcontroller.reset()
The time is kept by the microcontroller in between polling the Time service. Every second, 1 second is added to the total_seconds value tracking the current time. remaining stores the total seconds remaining until, or since, the event. This is converted to days, hours, minutes, and seconds. These values are added to the scrolling text on the display.
When dealing with time after the event (from the beginning of August 16th at midnight), the next 24 hours are still inside the event day (CircuitPython Day), and so checking days_remaining is zero (and triggered is False) allows us to detect when the trigger should be sent to IO (once) during that time.
Then after the event day the remaining time count will list incorrect values as the event is scheduled for the start of a day (midnight), so as long as the event has passed an offset of 1 day is required. The segments of time will also be positive numbers which feels wrong when talking about a past event so they are altered to be negative values.
if ticks_diff(ticks_ms(), clock_clock) >= clock_timer:
remaining = time.mktime(event_time) - total_seconds
if remaining < 0:
# calculate time since event
remaining = abs(remaining)
secs_remaining = -(remaining % 60)
remaining //= 60
mins_remaining = -(remaining % 60)
remaining //= 60
hours_remaining = -(remaining % 24)
remaining //= 24
days_remaining = -remaining
finished = True
if not first_run and days_remaining == 0:
scrolling_label.text = (
"It's CircuitPython Day 2024! The snakiest day of the year!"
)
# Check for the moment of the event to trigger something (a NASA snake launch)
if not triggered and (
hours_remaining == 0
and mins_remaining == 0
and secs_remaining <= 1
# Change at/after xx:yy:01 seconds so we've already updated the display
):
# send a signal to an adafruit IO feed, where an Action is listening
print("Launch the snakes! (sending message to Adafruit IO)")
triggered = True
io.send_data("cpday-countdown", "Launch the snakes!")
else:
# calculate time until event
secs_remaining = remaining % 60
remaining //= 60
mins_remaining = remaining % 60
remaining //= 60
hours_remaining = remaining % 24
remaining //= 24
days_remaining = remaining
if not finished or (finished and days_remaining < 0):
# Add 1 to negative days_remaining to count from end of day instead of start
if days_remaining < 0:
days_remaining += 1
# Update the display with current countdown value
scrolling_label.text = (
f"{days_remaining} DAYS, {hours_remaining} HOURS,"
+ f"{mins_remaining} MINUTES & {secs_remaining} SECONDS"
)
total_seconds += 1
clock_clock = ticks_add(clock_clock, clock_timer)
The last timer is used to scroll the text by moving the x coordinate of the text by 2 pixels. When the text is offscreen, its x coordinate is reset to start scrolling across again.
At the end of the loop the state variable for first_run is also updated to False
if ticks_diff(ticks_ms(), scroll_clock) >= scroll_timer:
scrolling_label.x -= 1
if scrolling_label.x < -(scrolling_label.width + 5):
scrolling_label.x = display.width + 2
display.refresh()
scroll_clock = ticks_add(scroll_clock, scroll_timer)
first_run = False
Finally, the project will probably survive a bit longer if it's enclosed. The packaging from Adafruit shipments makes for a reasonable project display box with one hole cut for the display.
That's it!
Page last edited January 22, 2025
Text editor powered by tinymce.