Libraries

We'll need to make sure we have these libraries installed. (Check out this link on installing libraries if needed.)

  • adafruit_bitmap_font
  • adafruit_bus_device
  • adafruit_display_text
  • adafruit_fakerequests.mpy
  • adafruit_portalbase
  • adafruit_matrixportal
  • adafruit_esp32spi
  • adafruit_io
  • adafruit_matrixportal
  • adafruit_requests.mpy
  • neopixel.mpy

Connect to the Internet

Once you have CircuitPython setup and libraries installed we can get your board connected to the Internet. The process for connecting can be found here. This includes important info on the secrets.py file, adafruit IO keys, and more!

Text Editor

Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide including info on how to use the REPL/serial terminal.

Alternatively, you can use any text editor that saves simple text files.

Code

Copy the code from the code-block below and paste it into the Mu editor and save it to your Metro M4 Airlift as code.py (or copy code.py from the zip file and place on the CIRCUITPY drive).

# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT

# Metro Matrix Clock
# Runs on Airlift Metro M4 with 64x32 RGB Matrix display & shield

import time
import board
import displayio
import terminalio
from adafruit_display_text.label import Label
from adafruit_bitmap_font import bitmap_font
from adafruit_matrixportal.network import Network
from adafruit_matrixportal.matrix import Matrix

BLINK = True
DEBUG = False

# Get wifi details and more from a secrets.py file
try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise
print("    Metro Minimal Clock")
print("Time will be set for {}".format(secrets["timezone"]))

# --- Display setup ---
matrix = Matrix()
display = matrix.display
network = Network(status_neopixel=board.NEOPIXEL, debug=False)

# --- Drawing setup ---
group = displayio.Group()  # Create a Group
bitmap = displayio.Bitmap(64, 32, 2)  # Create a bitmap object,width, height, bit depth
color = displayio.Palette(4)  # Create a color palette
color[0] = 0x000000  # black background
color[1] = 0xFF0000  # red
color[2] = 0xCC4000  # amber
color[3] = 0x85FF00  # greenish

# Create a TileGrid using the Bitmap and Palette
tile_grid = displayio.TileGrid(bitmap, pixel_shader=color)
group.append(tile_grid)  # Add the TileGrid to the Group
display.show(group)

if not DEBUG:
    font = bitmap_font.load_font("/IBMPlexMono-Medium-24_jep.bdf")
else:
    font = terminalio.FONT

clock_label = Label(font)


def update_time(*, hours=None, minutes=None, show_colon=False):
    now = time.localtime()  # Get the time values we need
    if hours is None:
        hours = now[3]
    if hours >= 18 or hours < 6:  # evening hours to morning
        clock_label.color = color[1]
    else:
        clock_label.color = color[3]  # daylight hours
    if hours > 12:  # Handle times later than 12:59
        hours -= 12
    elif not hours:  # Handle times between 0:00 and 0:59
        hours = 12

    if minutes is None:
        minutes = now[4]

    if BLINK:
        colon = ":" if show_colon or now[5] % 2 else " "
    else:
        colon = ":"

    clock_label.text = "{hours}{colon}{minutes:02d}".format(
        hours=hours, minutes=minutes, colon=colon
    )
    bbx, bby, bbwidth, bbh = clock_label.bounding_box
    # Center the label
    clock_label.x = round(display.width / 2 - bbwidth / 2)
    clock_label.y = display.height // 2
    if DEBUG:
        print("Label bounding box: {},{},{},{}".format(bbx, bby, bbwidth, bbh))
        print("Label x: {} y: {}".format(clock_label.x, clock_label.y))


last_check = None
update_time(show_colon=True)  # Display whatever time is on the board
group.append(clock_label)  # add the clock label to the group

while True:
    if last_check is None or time.monotonic() > last_check + 3600:
        try:
            update_time(
                show_colon=True
            )  # Make sure a colon is displayed while updating
            network.get_local_time()  # Synchronize Board's clock to Internet
            last_check = time.monotonic()
        except RuntimeError as e:
            print("Some error occured, retrying! -", e)

    update_time()
    time.sleep(1)

Using the WiFi and Adafruit IO credentials you entered into the secrets.py file and copied to the CIRCUITPY drive, your sign will connect to your WiFi, connect to Adafruit IO to get the time, and display it!

How it Works

Libraries

First we import the libraries we'll need, including time, for timekeeping, board for pin definitions on the Metro M4 Airlift, and displayio and terminalio for some of the display and basic font features.

The adafruit_display_text.label and adafruit_bitmap_font libraries are imported so we can use the text label commands and incorporate a bitmap font.

Finally adafruit_matrixportal.network and adafruit_matrixportal.matrix are imported to handle getting online through the WiFi access point to check the Adafruit IO timeserver, and to handle the lower level matrix display tasks.

Settings

Two user set variables are created next:

BLINK = True
DEBUG = False

These are at the top of the program where it's easy to change them if needed. The BLINK variable sets weather or not the colon ':' glyph will blink on and off each second.

DEBUG can be set True in order to switch from the bitmap font to the simpler terminalio font, as well as to print label bounding box and x, y coordinate values to the serial output, which is helpful when fine-tuning size and position of a bitmap font.

Check for Secrets

Next, the program checks to make sure there is SSID and password info stored in the secrets.py file that will be needed to go online. If this fails it will print an error message to the serial output.

Display & Network Setup

The matrix display and matrix objects are created next with these commands:

matrix = Matrix()
display = matrix.display
network = Network(status_neopixel=board.NEOPIXEL, debug=False)

The displayio Group, Bitmap, Palette, and TileGrid are created so we have the proper objects and hierarchy to display the time on the display. For info on this, check out the excellent displayio guide!

BDF Font Load

Now we'll load the BDF font glyphs from the Metro M4 Airlift's storage, in this case the lovely IBM Plex Mono medium in 24 point. (If we're in DEBUG mode, the terminalio font is used instead.)

The clock_label text object is created using the font so we have an object to display the time.

Update Time

The update_time() function will do all of the heavy lifting of parsing the current time value into discreet hours, minutes, and optional colon chunks that can be fed as arguments into the clock_label.text command.

This function also handles the blinking logic for the second hand colon, and sets the color of the text label to day vs. evening hours.

Text Centering

There's a very handy command we'll use for centering the text on the display: clock_label.bounding_box. This returns the x- and y-positions of the label, as well as the width and height of it's bounding box (an imaginary rectangle that encompasses the label).

With this we can then use a couple of simple formulas to set the label's x and y coordinates to be centered on the display. These essentially divide the display in half an the label in half to find their centers.

# Center the label
    clock_label.x = round(display.width / 2 - bbwidth / 2)
    clock_label.y = display.height // 2

Initial Display

The last_check variable is created to keep track of the state of time elapsed since last lookup of Adafruit IO timeserver time.

The update_time() function is called, and then the clock_label is appended to the display group so it will appear. At first this will be based on the Metro M4 time, which will be 12:00 when first powered on. But soon, we'll run the network time check to get accurate!

Main Loop

After all of that setup, it's time to get the time! This is the main loop of the program that runs over and over. The first thing to do is check the time if the last_check variable is either None or one hour has elapsed (3600 seconds).

The network.get_local_time() command uses the WiFi connection to synchronize to Adafruit IO timeserver time. Then the last_check value is reset to the current time.monotonic() (think of it as a continuously running ticker).

The display is then refreshed with the current time, and the process is repeated every second, taking a look each hour at the timeserver.

This guide was first published on Aug 19, 2020. It was last updated on Aug 19, 2020.

This page (Code the Matrix Clock) was last updated on Sep 20, 2023.

Text editor powered by tinymce.