Countdown

Build an IoT countdown clock using Adafruit QT Py ESP32-S2, alphanumeric STEMMA QT displays and CircuitPython! 

The alphanumeric displays text that scrolls the time remaining until your next exciting event!

This uses three alphanumeric displays daisy chained together using I2C STEMMA QT cables.

The electronics are housed in a 3D printed stand that snap fits together for any easy assembly.

Necessary Hardware

The following hardware is needed to complete this project.

QT Py ESP32-S2 Microcontroller

You need only one of the following boards.

Angled shot of small square purple dev board.
What has your favorite Espressif WiFi microcontroller, comes with our favorite connector - the STEMMA QT, a chainable I2C port, and has...
Out of Stock

Quad Alphanumeric Display STEMMA QT Backpacks

This project requires three alphanumeric display backpacks and three pairs of the alphanumeric displays. The backpacks are bundled with the displays.

The bundles are available with displays in red, green, blue, white, and yellow. Only blue is included below. Check the Featured Products on the right side of this page to find links to the others.

Overhead video of an assembled 14-segment LED backpack, emitting the follow text in blue LEDS: "AdaFruit 14-Segment Backpack"
Display, elegantly, 012345678 or 9! Gaze, hypnotized, at ABCDEFGHIJKLM - well it can display the whole alphabet. You get the point. This is a nice, bright alphanumeric display that...
Out of Stock

STEMMA QT Cable

You will need three of the following cables.

Any length will work, but the 50mm cable is the best option because it leaves the least amount of excess cable behind the displays.

Angled of of JST SH 4-Pin Cable.
This 4-wire cable is 50mm / 1.9" long and fitted with JST SH female 4-pin connectors on both ends. Compared with the chunkier JST PH these are 1mm pitch instead of 2mm, but...
$0.95
In Stock

Power

To power the project, you can use a USB C cable plugged into a USB battery or a USB port on your computer.

Angled shot of coiled pink and purple USB cable with USB A and USB C connectors.
This cable is not only super-fashionable, with a woven pink and purple Blinka-like pattern, it's also made for USB C for our modernized breakout boards, Feathers, and...
$2.95
In Stock
Angled shot of a blue long rectangular USB battery pack.
A smaller-sized rechargeable battery pack for your Raspberry Pi or Raspberry...
$14.95
In Stock

CAD Parts List

STL files for 3D printing are oriented to print "as-is" on FDM style machines. Parts are designed to 3D print without any support material with PLA filament. Original design source may be downloaded using the links below:

  • cdt-plate.stl
  • cdt-qt-mount.stl
  • cdt-base.stl

Build Volume

The parts require a 3D printer with a minimum build volume.

  • 166mm (X) x 78mm (Y) x 50mm (Z)

CAD Assembly

The QT Py is snap fitted into the QT Py mount. the QT Py mount is secured to the base with two M2.5 screws and hex nuts. The three alphanumeric displays are press fitted into the open cutouts on the plate. The plate is press fitted into the slot on the base. 

Design Source Files

The project assembly was designed in Fusion 360. This can be downloaded in different formats like STEP, STL and more. Electronic components like Adafruit's boards, displays, connectors and more can be downloaded from the Adafruit CAD parts GitHub Repo.

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.

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

Save it wherever is convenient for you.

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

Click the reset button once (highlighted in red above), and then click it again when you see the RGB status LED(s) (highlighted in green above) turn red (approximately half a second later). Sometimes it helps to think of it as a "slow double-click" of the reset button.

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.

Once successful, 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.

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.

If after several tries, and verifying your USB cable is data-ready, you still cannot get to the bootloader, it is possible that the bootloader is missing or damaged. Check out the Install UF2 Bootloader page for details on resolving this issue.

You will see a new disk drive appear called QTPYS2BOOT.

 

 

Drag the adafruit_circuitpython_etc.uf2 file to QTPYS2BOOT.

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

That's it!

If you've worked on WiFi projects with CircuitPython before, you're probably familiar with the secrets.py file. This file is a Python file that is stored on your CIRCUITPY drive that contains all of your secret WiFi information, such as your SSID, SSID password and any API keys for IoT services. 

As of CircuitPython 8, there is support for a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file.

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

settings.toml File Example

Here is an example on how to format your settings.toml file.

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

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.

import os

print(os.getenv("test_variable"))

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.

Once you've finished setting up your QT Py with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.

To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.

# SPDX-FileCopyrightText: 2022 Liz Clark for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
CircuitPython Quad-Alphanumeric Display Holiday Countdown.

This demo requires a separate file named settings.toml on your CIRCUITPY drive, which
should contain your WiFi credentials and Adafruit IO credentials.
"""

import os
import time
import ssl
import wifi
import socketpool
import microcontroller
import board
import adafruit_requests
from adafruit_ht16k33.segments import Seg14x4
from adafruit_io.adafruit_io import IO_HTTP, AdafruitIO_RequestError # pylint: disable=unused-import

# Date and time of event. Update YEAR, MONTH, DAY, HOUR, MINUTE to match the date and time of the
# event to which you are counting down. Update NAME to the name of the event. Update MSG to the
# message you'd like to display when the countdown has completed and the event has started.
EVENT_YEAR = 2022
EVENT_MONTH = 12
EVENT_DAY = 25
EVENT_HOUR = 0
EVENT_MINUTE = 0
EVENT_NAME = "Christmas"
EVENT_MSG = "Merry Christmas * "

# The speed of the text scrolling on the displays. Increase this to slow down the scrolling.
# Decrease it to speed up the scrolling.
scroll_speed = 0.25

# Create the I2C object using STEMMA_I2C()
i2c = board.STEMMA_I2C()
# Alphanumeric segment display setup using three displays in series.
display = Seg14x4(i2c, address=(0x70, 0x71, 0x72))
# Display brightness is a number between 0.0 (off) and 1.0 (maximum). Update this if you want
# to alter the brightness of the characters on the displays.
display.brightness = 0.2
# The setup-successful message. If this shows up on your displays, you have wired them up
# properly and the code setup is correct.
display.print("HELLO WORLD")


def reset_on_error(delay, error):
    """Resets the code after a specified delay, when encountering an error."""
    print("Error:\n", str(error))
    display.print("Error :(")
    print("Resetting microcontroller in %d seconds" % delay)
    time.sleep(delay)
    microcontroller.reset()


try:
    wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
# any errors, reset MCU
except Exception as e:  # pylint: disable=broad-except
    reset_on_error(10, e)

aio_username = os.getenv("aio_username")
aio_key = os.getenv("aio_key")
location = os.getenv("aio_location")

pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
# Initialize an Adafruit IO HTTP API object
try:
    io = IO_HTTP(aio_username, aio_key, requests)
except Exception as e:  # pylint: disable=broad-except
    reset_on_error(10, e)
print("Connected to Adafruit IO")
display.print("Connected IO")

clock = time.monotonic()

event_time = time.struct_time(
    (EVENT_YEAR, EVENT_MONTH, EVENT_DAY, EVENT_HOUR, EVENT_MINUTE, 0, -1, -1, False)
)
scroll_time = 0

while True:
    try:
        if (clock + scroll_time) < time.monotonic():
            now = io.receive_time()
            # print(now)
            # print(event_time)
            remaining = time.mktime(event_time) - time.mktime(now)
            # if it's the day of the event...
            if remaining < 0:
                # scroll the event message on a loop
                display.marquee(EVENT_MSG, scroll_speed, loop=True)
            # calculate the seconds remaining
            secs_remaining = remaining % 60
            remaining //= 60
            # calculate the minutes remaining
            mins_remaining = remaining % 60
            remaining //= 60
            # calculate the hours remaining
            hours_remaining = remaining % 24
            remaining //= 24
            # calculate the days remaining
            days_remaining = remaining
            # pack the calculated times into a string to scroll
            countdown_string = (
                "* %d Days, %d Hours, %d Minutes & %s Seconds until %s *"
                % (
                    days_remaining,
                    hours_remaining,
                    mins_remaining,
                    secs_remaining,
                    EVENT_NAME,
                )
            )
            # get the length of the packed string
            display_length = len(countdown_string)
            # print(display_length)
            # calculate the amount of time needed to scroll the string
            scroll_time = display_length * scroll_speed
            # print(scroll_time)
            # reset the clock
            clock = time.monotonic()
            # scroll the string once
            display.marquee(countdown_string, scroll_speed, loop=False)

    # any errors, reset MCU
    except Exception as e:  # pylint: disable=broad-except
        reset_on_error(10, e)

Upload the Code and Libraries to the QT Py

After downloading the Project Bundle, plug your QT Py ESP32-S2 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 folder and file to the QT Py's CIRCUITPY drive.

  • /lib
  • code.py

Copy the entire lib folder, including all of its contents.

Your QT Py CIRCUITPY drive should resemble the following after copying the lib folder and the code.py file.

CIRCUITPY

Add Your settings.toml File

Remember to add your settings.toml file as described in the Create Your settings.toml File page earlier in the guide. You'll need to include your CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD, aio_username and aio_key in the file.

CIRCUITPY_WIFI_SSID = "your-wifi-ssid-here"
CIRCUITPY_WIFI_PASSWORD = "your-wifi-password-here"

aio_username = "your-Adafruit-IO-username-here"
aio_key = "your-Adafruit-IO-key-here"

How the CircuitPython Code Works

The code begins by assigning a series of variables and a separate single variable. These variables are assigned at the top to enable the user to easily find them in the event they would like to update them.

The series of variables is used to indicate the name, time and date of the event, and the message that will be displayed when the event has started.

EVENT_YEAR = 2022
EVENT_MONTH = 12
EVENT_DAY = 25
EVENT_HOUR = 0
EVENT_MINUTE = 0
EVENT_NAME = "Christmas"
EVENT_MSG = "Merry Christmas * "

The separate single variable sets the scroll speed of the text across the displays.

scroll_speed = 0.25

Alphanumeric Setup

The next section sets up the three displays in series on I2C via the STEMMA QT connector on the QT Py.

The provided I2C addresses assume you did not solder any address jumpers on the back of the first display, soldered A0 on the back of the second display, and soldered A1 on the back of the third display. If you did not do the same, you may need to change the addresses to match.

i2c = board.STEMMA_I2C()
display = Seg14x4(i2c, address=(0x70, 0x71, 0x72))
The I2C addresses above assume the first display's address jumpers are unsoldered, A0 is soldered on the second display, and A1 is soldered on the third display.

Then you set the brightness to 0.2 out of a range of 0.0 to 1.0, and you print "HELLO WORLD" to the display to give you an indicator of whether everything is wired up and initiated properly.

display.brightness = 0.2
display.print("HELLO WORLD")

Reset On Error

Next is the reset_on_error() function. When the code encounters an error, this function resets the microcontroller after a specified delay. This resets everything, and gives it an opportunity to begin fresh, which can clear up many potential errors.

def reset_on_error(delay, error):
    print("Error:\n", str(error))
    display.print("Error :(")
    print("Resetting microcontroller in %d seconds" % delay)
    time.sleep(delay)
    microcontroller.reset()

Connect

Then, the QT Py connects to your WiFi network, followed by Adafruit IO. Both of these connection attempts are wrapped in try/except loops so that the code does not get stuck in the event of an error. Rather, the board will reset and try connecting again. 

try:
    wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
# any errors, reset MCU
except Exception as e:  # pylint: disable=broad-except
    reset_on_error(10, e)

aio_username = os.getenv("aio_username")
aio_key = os.getenv("aio_key")
location = os.getenv("aio_location")

pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
# Initialize an Adafruit IO HTTP API object
try:
    io = IO_HTTP(aio_username, aio_key, requests)
except Exception as e:  # pylint: disable=broad-except
    reset_on_error(10, e)
print("Connected to Adafruit IO")
display.print("Connected IO")

Timekeeping

Right before the loop, clock is setup as a time.monotonic() device for timekeeping, event_time packs the event variables into a struct_time() format and scroll_time is set to 0. scroll_time is used to dynamically adjust the length of time needed to scroll the countdown information across the displays. It's used to loop the countdown as seamlessly as possible.

clock = time.monotonic()

event_time = time.struct_time(
    (EVENT_YEAR, EVENT_MONTH, EVENT_DAY, EVENT_HOUR, EVENT_MINUTE, 0, -1, -1, False)
)
scroll_time = 0

The Loop

In the loop, after scroll_time has passed, Adafruit IO is pinged to get the current time. Then the current time is subtracted from the event time to find how much time is remaining. This calculation is done by passing event_time and now to the mktime() function, which converts struct_time() format to seconds. The result of this is stored in remaining.

if (clock + scroll_time) < time.monotonic():
            now = io.receive_time()
            # print(now)
            # print(event_time)
            remaining = time.mktime(event_time) - time.mktime(now)

If remaining is less than zero, then that means that the day of your event is here and the text you stored in EVENT_MSG is scrolled across the displays on a loop.

# if it's the day of the event...
if remaining < 0:
      # scroll the event message on a loop
      display.marquee(EVENT_MSG, scroll_speed, loop=True)

Otherwise, the seconds remaining are calculated back into days, hours, minutes and seconds. These values are packed into the countdown_string.

# calculate the seconds remaining
            secs_remaining = remaining % 60
            remaining //= 60
            # calculate the minutes remaining
            mins_remaining = remaining % 60
            remaining //= 60
            # calculate the hours remaining
            hours_remaining = remaining % 24
            remaining //= 24
            # calculate the days remaining
            days_remaining = remaining
            # pack the calculated times into a string to scroll
            countdown_string = (
                "* %d Days, %d Hours, %d Minutes & %s Seconds until %s *"
                % (
                    days_remaining,
                    hours_remaining,
                    mins_remaining,
                    secs_remaining,
                    EVENT_NAME,
                )
            )

Looping the Marquee

The length of the countdown_string is multiplied by the scroll_speed variable to calculate scroll_time. This means that scroll_time has the length of time needed to scroll each character in the string across the displays.

clock is reset right before the marquee() function begins to scroll the text. After the marquee() scroll finishes, then scroll_time will have elapsed and the process can begin again. This allows for the displays to continuously loop the countdown information while updating.

# get the length of the packed string
display_length = len(countdown_string)
# print(display_length)
# calculate the amount of time needed to scroll the string
scroll_time = display_length * scroll_speed
# print(scroll_time)
# reset the clock
clock = time.monotonic()
# scroll the string once
display.marquee(countdown_string, scroll_speed, loop=False)

Solder Backpacks

Follow the documentation in the main product page to learn how to solder the display backpacks to the breakout boards.

Setup Breakouts

Two of the three breakout boards will need their I2C address changes via solder jumper. 

The order of the displays are depicted with the PCB side facing up. The order will be reversed when mounted to the 3D printed plate to show the display characters in the correct orientation.

Display #1

This display does not need any jumpers solder and will use the default I2C address.

Display #2

This display will need the A0 jumper soldered to change the I2C address.

Display #3

This display will need the A1 jumper soldered to change the I2C address.

You must follow the steps on the Display Setup page before following the steps below.

Make sure you wire them up in the correct order based on the status of the address jumpers on the back of each backpack.

QT Py and First Display (no address jumper soldered closed on the back)

  • QT Py STEMMA QT connector to first display right STEMMA QT connector using a STEMMA QT cable.

First Display and Second Display (A0 jumper soldered closed on the back)

  • First display left STEMMA QT connector to second display right STEMMA QT connector using a STEMMA QT cable.

Second Display to Third Display (A1 jumper soldered closed on the back)

  • Second display left STEMMA QT connector to third display right STEMMA QT connector using a STEMMA QT cable.

Install Base Plate

The 3D printed base and plate snap fit together.

Insert the plate into the base's clip with the notches lined up.

Press the two parts together to fully seat the plate to the base. 

Displays Installed

The three displays are press fitted into the cutouts on the 3D printed plate.

Reference the image for thecorrect order and orientation of displays.

From left to right, Display #3, Display #2 and Display #1.

Installing Displays

The displays are press fitted into the cutouts on the 3D printed panel with the correct orientation and order.

Connecting Stemma Cables

The displays are chained together using short STEMMA QT cables.

Begin connecting display #3 to display #2. Then, display #2 to display #1.

Connected Displays

Take a moment to ensure the STEMMA QT cable have been properly connected.

Reference the image for the correct placement.

Holder for QT Py

The QT Py board will be press-fitted into the 3D printed holder.

The QT Py board is oriented with the USB-C port lined up with the tabs on the 3D printed holder.

Install QT Py

Insert the QT Py at an angle with the corners fitted underneath the clips on the back of the holder.

Using your fingers, slightly bend the front side to allow the front clips to fit over the front of the QT Py.

Install STEMMA QT Cable

Connect the remaining STEMMA QT cable to the QT Py board.

 

Installing QT Py Holder

Place the QT Py holder over the base plate assembly with the mounting holes lined up with the tabs.

Secure QT Py Holder

Use 2x M2.5 x 10mm long screws and hex nuts to secure the QT Py holder to the base plate assembly.

Secured QT Py Holder

The screws are inserted with the screw heads facing down and hex nuts facing up.

Finger tighten the hex nuts to avoid stripping them. 

Connect QT Py to Displays 

Connect the remaining STEMMA QT cable to the remaining STEMMA QT port on display #1.

Final Build

Congratulations on your build!

Power QT Py

Connect USB-C cable to a 5V power supply 5V battery bank or USB hub to power up the QT Py.

Countdown to Something New

Eventually, the timer will reach zero, and your event will start. What can you do with your countdown timer after that? Count down to another event!

There are two things you can easily customise in the example code: event info, and scroll speed.

Event Info

At the top of the example code, you'll find a list of variables. These are used to determine the future event date, and subsequently how much time should show on the timer as the event gets closer. You can change these to any future date, and update the event name and message to match your countdown.

  • EVENT_YEAR - The year of your event, using the full 4-digit year number.
  • EVENT_MONTH - The month of your event.
  • EVENT_DAY - The day of your event.
  • EVENT_HOUR - The hour part of the time of your event, if it has a specific time. Otherwise, for an all-day event, you can leave this at 0.
  • EVENT_MINUTE - The minute part of the time of your event, if it has a specific time. Otherwise, for an all-day event, you can leave this at 0.
  • EVENT_NAME - The name of your event. Must be wrapped in ".
  • EVENT_MSG - The message you would like to display when it's time for the event. Must be wrapped in ".
EVENT_YEAR = 2022
EVENT_MONTH = 12
EVENT_DAY = 25
EVENT_HOUR = 0
EVENT_MINUTE = 0
EVENT_NAME = "Christmas"
EVENT_MSG = "Merry Christmas * "

Scroll Speed

This sets the amount of time each character is shown on the right-most segment of the three displays before the it is bumped one to the left and the next character is displayed. More simply, it controls the speed at which text scrolls on the display.

Increase the value to slow down the scrolling. Decrease the value to speed up the scrolling. It can be any value 0 or above, including both integers and floats. That said, 0 is too fast to read, and 1 takes forever. Keep that in mind, and find a speed that works for you!

scroll_speed = 0.25

This guide was first published on Dec 20, 2022. It was last updated on Jan 07, 2023.