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