CircuitPython Day 2024 Countdown Clock (2024)

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_SSIDandCIRCUITPY_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 edittimezoneto 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. Yourtimezoneis 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 separatetickstimers 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.nowis 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_secondsvalue 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 thexcoordinate 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!

Your browser does not support the video tag.

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.

CircuitPython Day 2024 Countdown Clock (2024)
Top Articles
Latest Posts
Article information

Author: Kelle Weber

Last Updated:

Views: 5716

Rating: 4.2 / 5 (53 voted)

Reviews: 92% of readers found this page helpful

Author information

Name: Kelle Weber

Birthday: 2000-08-05

Address: 6796 Juan Square, Markfort, MN 58988

Phone: +8215934114615

Job: Hospitality Director

Hobby: tabletop games, Foreign language learning, Leather crafting, Horseback riding, Swimming, Knapping, Handball

Introduction: My name is Kelle Weber, I am a magnificent, enchanting, fair, joyous, light, determined, joyous person who loves writing and wants to share my knowledge and understanding with you.