Maker.io main logo

CircuitPython Day 2024 Countdown Clock

2025-09-19 | By Adafruit Industries

License: See Original Project LCD / TFT

Courtesy of Adafruit

Guide by Tyeth Gundry

Overview

adafruit_io_qt-py2

This guide is a demonstration of the new Adafruit Connection Manager library for CircuitPython, along with the new optional time zone argument for the Adafruit IO library.

More Countdown Clocks?

Why on earth might you need another Countdown Clock project? Well maybe you’re planning to remotely trigger another device at the end of the countdown, want the option to count past the event, or maybe because your last countdown clock thought it belonged in another part of the world.

Here at Adafruit, we’ve been offering an easy way for microcontrollers to fetch the current time, via the Time Service integration, as part of Adafruit IO (our IoT platform).

Unfortunately, there have been many cases where the automatic time zone detection has gone haywire, leading to time-based projects not behaving as expected!

Time Zones

Time zones can be tricky, some countries even have a few! That's why when a time zone is not specified, the Adafruit IO Time service uses "automatic time zone detection" based on IP address. This is like having a listing of all the IPs in the world and where they should be located geographically. It's never going to be 100% accurate, as an IP can be shared or recycled or just unlisted. It's estimated to be 99% accurate to the country level.

Instead, the Time Service can accept a requested time zone and respond with exactly the right time for your location/geography. And now the CircuitPython library for Adafruit IO has been updated to add the optional time zone parameter when requesting the time.

Adafruit Connection Manager

The other new kid on the block is the Adafruit Connection Manager library. This library makes getting connected to the internet on different boards a total breeze, by using standardized methods and providing "helpers" for families of hardware (like ESP WiFi or WizNet Ethernet boards) to make programming easier.

Parts

To complete this project, you will need a microcontroller board supported by CircuitPython, with a built-in display (or an external one, possibly with minor code changes).

This project was originally written for the Adafruit Feather ESP32-S2/S3 Reverse TFT, but then adapted to also run on the Adafruit Qualia (ESP32-S3) with the 3.2" bar display (820 x 320 pixels). To support this there is a conditional block that selects a larger font and background image when running on the Qualia board.

It should also work on most other boards, although you may want to use a differently sized background image to better suit your display's size and aspect ratio. You can easily edit the supplied image with your favourite graphics editing software (or an online service).

CircuitPython displays refresh much more slowly with overlapping images and labels. On the Qualia the background image is positioned just above the label with no overlap. This brings the refresh speed from under one frame per second up to 5-10fps. Not such an issue on smaller resolution displays (less data to send).

Either use a board with built in display:

Or for the Qualia version:

With either the 3.2" Rectangle Bar display (820x320 pixels):

Or the touchscreen version (which has a bezel with nice, rounded corners):

Adafruit IO

As this project uses Adafruit IO you’ll need to have setup an Adafruit account, then logged in at io.adafruit.com.

If you’ve never used Adafruit IO before then it would be wise to read these guides:

Adafruit IO Basics

Feeds

A feed is a store for data points, you can read more about them in the guides linked above, but for now, it's enough to know we need one of them.

We'll use the feed we create to receive our countdown completion message, and then automatically trigger an Action to signify CircuitPython Day.

Go to the IO Feeds page (io.adafruit.com/feeds/) and use the + New Feed button to create a new feed, giving it the name of cpday-countdown.

feeds_1

Your new feed will appear in the list of My Feeds, the default group at the top of the feeds page. Clicking its name will take you to the individual feed page, showing all previous data points, and a button to add new data (and download all), along with options for configuring the feed (left sidebar or top menu on small screens).

We'll revisit the cpday-countdown feed page later, to test our automatically triggered Action, by manually adding data to the feed.

Actions

On Adafruit IO there are automatic Actions available. You can set up an action to be triggered by a new feed value, or potentially after a delay, or on a set schedule. Then the Action can perform various processing tasks, followed by an output task like publish to a feed or send an email.

You can read more in this guide to the new Blockly Actions:

How to use Blocky for Actions on Adafruit IO

We will use the gets data matching trigger, with the value set to "Launch the Snakes!", so as soon as that message arrives then our action will trigger.

Start by going to the Actions page and creating a new Action, entering a name and optional description.

Now you're presented with the Blockly Action editor page, with a toolbox full of blocks, and the main diagram workspace to the right, with a single Root block designed to receive trigger and action blocks.

actions_2

Trigger:

Select the When FEED gets data matching = 0 block from the Triggers category in the side panel/toolbox. Drop it into the top Triggers: section of the root block. Then select the cpday-countdown feed from the feed dropdown list.

Now make sure the Operator is set to equals (=) so the action only runs if the new data sent to our feed exactly matches an expected value.

Lastly, you need to get the String Comparison Block from the Triggers category in the toolbox, it is the one with equals speech marks ( = ""). Drag the string comparison block into place where the value block is attached to the trigger, directly on top of the value placeholder block.

Enter the String block by clicking inside and entering the value Launch the snakes!

customer___partner_projects_chrome_RmzAkQ3qa9

trigger_3

trigger_4

Action: (Output)

Next the output, for this example you'll send an email, but you could configure anything your mind can come up with, you can even do multiple outputs in the same action.

Maybe the countdown could trigger a real space launch of CircuitPython-powered hardware...Blinka in Space!

Drag the Email block from the Notifications category in the toolbox, dropping it into the Actions: section of the root block, aligning the bumps and dents until the block gains a yellow outline, causing it to snap into place.

Add your custom subject and body to the email, ensuring you get a reminder for CircuitPython Day wherever you are..

customer___partner_projects_chrome_oZhHkpU9Er

Your finished Blockly Action should look very similar to this:

blocky_5

Testing

To test the countdown, launch action you need to go to the cpday-countdown feed page, by navigating to the Feeds page and then clicking on the cpday-countdown feed name.

Use the Add Data button to add the value Launch the snakes!

You should receive an email nearly instantly, to the primary email address registered on your Adafruit account, reminding you of the snakiest day of the year (or whatever message you intended).

testing_6

IO Secret Key - Required by settings.toml

To get your IO Key (and username) easily, click the View Key option from the menu on small screens, or use the big yellow Key icon for larger screens, from any IO page. You'll see a CircuitPython-compatible set listed, which you'll copy later into your settings.toml file.

key_7

Integrations

There are Power-Ups + Integrations on Adafruit IO, like IFTTT/WeatherKit/Zapier/SMS.

The Time service integration is what we’re using in this project to fetch the initial time and account for any time-zone challenges.

See more details at io.adafruit.com/services/time [Must be logged in].

Look up your time-zone from the table linked there, referring to the TZ Identifier column of the table, and make a note of it as it will be used later in the code.

Speaking of code, it’s now time to get CircuitPython set up and take a tour of how the different sections of the code work.

CircuitPython

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython Quickstart

Follow this step-by-step to quickly get CircuitPython running on your board.

Download the latest version of CircuitPython for this board via circuitpython.org

Click the link above to download the latest CircuitPython UF2 file.

Save it wherever is convenient for you.

download_8

board_9

Plug your board into your computer, using a known-good data-sync cable, directly, or via an adapter if needed.

Double-click the reset button (highlighted in red above), and you will see the RGB status LED(s) turn green (highlighted in green above). If you see red, try another port, or if you're using an adapter or hub, try without the hub, or different adapter or hub.

For this board, tap reset and wait for the LED to turn purple, and as soon as it turns purple, tap reset again. The second tap needs to happen while the LED is still purple.

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

You will see a new disk drive appear called FTHRS3BOOT.

Drag the adafruit_circuitpython_etc.uf2 file to FTHRS3BOOT.

drag_10

drag_11

The BOOT drive will disappear, and a new disk drive called CIRCUITPY will appear.

That’s it!

boot_12

Create Your settings.toml File

CircuitPython works with WiFi-capable boards to enable you to make projects that have network connectivity. This means working with various passwords and API keys. As of CircuitPython 8, there is support for a settings.toml file. This is a file that is stored on your CIRCUITPY drive, that contains all of your secret network information, such as your SSID, SSID password and any API keys for IoT services. It is designed to separate your sensitive information from your code.py file so you are able to share your code without sharing your credentials.

CircuitPython previously used a secrets.py file for this purpose. The settings.toml file is quite similar.

Your settings.toml file should be stored in the main directory of your CIRCUITPY drive. It should not be in a folder.

CircuitPython settings.toml File

This section will provide a couple of examples of what your settings.toml file should look like, specifically for CircuitPython WiFi projects in general.

The most minimal settings.toml file must contain your WiFi SSID and password, as that is the minimum required to connect to WiFi. Copy this example, paste it into your settings.toml, and update:

  • your_wifi_ssid

  • your_wifi_password

Download File

Copy Code
CIRCUITPY_WIFI_SSID = "your_wifi_ssid"
CIRCUITPY_WIFI_PASSWORD = "your_wifi_password"

Many CircuitPython network-connected projects on the Adafruit Learn System involve using Adafruit IO. For these projects, you must also include your Adafruit IO username and key. Copy the following example, paste it into your settings.toml file, and update:

  • your_wifi_ssid

  • your_wifi_password

  • your_aio_username

  • your_aio_key

Download File

Copy Code
CIRCUITPY_WIFI_SSID = "your_wifi_ssid"
CIRCUITPY_WIFI_PASSWORD = "your_wifi_password"
ADAFRUIT_AIO_USERNAME = "your_aio_username"
ADAFRUIT_AIO_KEY = "your_aio_key"

Some projects use different variable names for the entries in the settings.toml file. For example, a project might use ADAFRUIT_AIO_ID in the place of ADAFRUIT_AIO_USERNAME. If you run into connectivity issues, one of the first things to check is that the names in the settings.toml file match the names in the code.

Not every project uses the same variable name for each entry in the settings.toml file! Always verify it matches the code.

settings.toml File Tips

Here is an example settings.toml file.

Download File

Copy Code
# Comments are supported
CIRCUITPY_WIFI_SSID = "guest wifi"
CIRCUITPY_WIFI_PASSWORD = "guessable"
CIRCUITPY_WEB_API_PORT = 80
CIRCUITPY_WEB_API_PASSWORD = "passw0rd"
test_variable = "this is a test"
thumbs_up = "\U0001f44d"

In a settings.toml file, it's important to keep these factors in mind:

  • Strings are wrapped in double quotes; ex: "your-string-here"

  • Integers are not quoted and may be written in decimal with optional sign (+1, -1, 1000) or hexadecimal (0xabcd).

    • Floats, octal (0o567) and binary (0b11011) are not supported.

  • Use \u escapes for weird characters, \x and \ooo escapes are not available in .toml files

    • Example: \U0001f44d for 👍 (thumbs up emoji) and \u20ac for € (EUR sign)

  • Unicode emoji, and non-ASCII characters, stand for themselves as long as you're careful to save in "UTF-8 without BOM" format

When your settings.toml file is ready, you can save it in your text editor with the .toml extension.

settings_12

Accessing Your settings.toml Information in code.py

In your code.py file, you'll need to import the os library to access the settings.toml file. Your settings are accessed with the os.getenv() function. You'll pass your settings entry to the function to import it into the code.py file.

Download File

Copy Code
import os

print(os.getenv("test_variable"))

copy_13

In the upcoming CircuitPython WiFi examples, you'll see how the settings.toml file is used for connecting to your SSID and accessing your API keys.

Code the Countdown Clock

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.

Download Project Bundle

Copy Code
# 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

View on GitHub

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.

drive_14

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).

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 edit time zone 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 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 request's 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.

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-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.

Download File

Copy Code
## 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).

Download File

Copy Code
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.

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 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.

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!

adafruit_io_x

製造商零件編號 5691
ESP32-S3 FEATHER PCB ANTENNA
Adafruit Industries LLC
製造商零件編號 5800
EVAL BOARD FOR ESP32-S3
Adafruit Industries LLC
製造商零件編號 5828
GRAPHIC DISPLAY TFT RGB 3.2"
Adafruit Industries LLC
製造商零件編號 5797
GRAPHIC DISPLAY TFT RGB 3.2"
Adafruit Industries LLC
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.