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)

This guide was first published on Dec 20, 2022. It was last updated on Mar 28, 2024.

This page (Code the Timer) was last updated on Mar 28, 2024.

Text editor powered by tinymce.