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 theDownload Project Bundlebutton 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.
Download Project Bundle
Copy Code
# SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries# SPDX-FileCopyrightText: 2024 Tyeth Gundry for Adafruit Industries## SPDX-License-Identifier: MITimport osimport timeimport wifiimport boardimport displayioimport microcontrollerimport adafruit_connection_managerimport adafruit_requestsfrom adafruit_io.adafruit_io import IO_HTTPfrom adafruit_bitmap_font import bitmap_fontfrom adafruit_display_text import bitmap_labelfrom 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 Yorktimezone = 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 = 2024EVENT_MONTH = 8EVENT_DAY = 16EVENT_HOUR = 0EVENT_MINUTE = 0## we'll make a python-friendly structureevent_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-managerpool = 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 clientio = IO_HTTP( os.getenv("ADAFRUIT_AIO_USERNAME"), os.getenv("ADAFRUIT_AIO_KEY"), requests)## Setup display and size appropriate assetsif 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_shadergroup = 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 = groupdisplay.auto_refresh = Falserefresh_clock = ticks_ms()refresh_timer = 3600 * 1000 # 1 hourclock_clock = ticks_ms()clock_timer = 1000scroll_clock = ticks_ms()scroll_timer = 50first_run = Truefinished = Falsetriggered = Falsewhile 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 reset in 15seconds! -", e) time.sleep(15) microcontroller.reset() 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) calledCIRCUITPY. Unzip the folder and copy the following items to the board'sCIRCUITPYdrive.
- libfolder
- 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'sCIRCUITPY drive should look similar to this after copying thelibfolder,image files (.bmp), font files (.pcf),and the two .py circuitpython code files.
Add Yoursettings.tomlFile
As of CircuitPython 8.0.0, there is support forEnvironment Variables. Environment variables are stored in asettings.tomlfile. Similar tosecrets.py, thesettings.tomlfile separates your sensitive information from your maincode.pyfile. Add yoursettings.tomlfile as described in theCreate Your settings.toml File pageearlier in this guide. You'll need to include yourCIRCUITPY_WIFI_SSID
andCIRCUITPY_WIFI_PASSWORD
, along with your Adafruit IO details (username and key), and optionally a time zone (or edit the code.py file).
Download File
Copy Code
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 edittimezone
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.
Download File
Copy Code
## 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 Yorktimezone = 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 = 2024EVENT_MONTH = 8EVENT_DAY = 16EVENT_HOUR = 0EVENT_MINUTE = 0## we'll make a python-friendly structureevent_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. TheIO_HTTP class has a method to receive_time and will take care of the timing for this project. Yourtimezone
is passed to the receive_time request to reflect the time in your location.
Download File
Copy Code
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-managerpool = 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 clientio = 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.
Download File
Copy Code
## Setup display and size appropriate assetsif 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_shadergroup = 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 = groupdisplay.auto_refresh = False
Time is Ticking
Finally, three separateticks
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
).
Download File
Copy Code
refresh_clock = ticks_ms()refresh_timer = 3600 * 1000 # 1 hourclock_clock = ticks_ms()clock_timer = 1000scroll_clock = ticks_ms()scroll_timer = 50first_run = Truefinished = Falsetriggered = False
The Loop
In the loop, the time is fetched from the Adafruit IO Time service every hour and stored innow
.now
is converted to seconds usingtime.mktime(now)
. This lets you calculate how much time is remaining until the event.
Download File
Copy Code
# 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.
Download File
Copy Code
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 thex
coordinate of the text by 2 pixels. When the text is offscreen, itsx
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
Download File
Copy Code
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!
This guide was first published on Aug 14, 2024. It was lastupdated on Aug 14, 2024.
This page (Code the Countdown Clock) was last updated on Aug 14, 2024.
Text editor powered by tinymce.