Getting the Program's Files
To use the application, you need to obtain code.py with the program, and the quotes_data/ folder with zipped CSV files in it to place on the MagTag CIRCUITPY drive.
Thankfully, this can be done in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file.
Connect your board to your computer via a known good data+power USB cable. The board should show up in your File Explorer/Finder (depending on your operating system) as a flash drive named CIRCUITPY.
Extract the contents of the zip file, copy the lib directory files to CIRCUITPY/lib. Copy the code.py and quotes_data/ folder to your CIRCUITPY drive. The program should self start.
This project uses brand new functionality in the adafruit_display_text library. The MagTag device has an older version of the library without the necessary features frozen in to its firmware. In order for the project to work you must place the adafruit_display_text folder in the root of CIRCUITPY instead of inside of the lib/ folder like normal.
# SPDX-FileCopyrightText: 2025 Tim C, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Literature Quotes Clock for the Adafruit MagTag
This project displays the current time by showing a quote from a book
that references the time. Every minute of the day has a different quote.
The current time reference portion of the quote is accented with an outline
to make it easier to see at a glance.
This project was inspired by:
- https://github.com/JohannesNE/literature-clock
- https://www.instructables.com/Literary-Clock-Made-From-E-reader/
The quotes were sourced originally by The Guardian:
https://www.theguardian.com/books/table/2011/apr/21/literary-clock?CMP=twt_gu
"""
import os
import random
import time
import zlib
import displayio
import rtc
import socketpool
import supervisor
import terminalio
import wifi
import adafruit_ntp
from adafruit_display_text.bitmap_label import Label
from adafruit_display_text import wrap_text_to_pixels
# Set the current offset for your timezone
TZ_OFFSET_FROM_UTC = -6
def which_line_contains(all_lines, important_passage):
"""
Find the line that contains the important passage
:param all_lines: The lines to search
:param important_passage: The passage to search for
:return: The index of the line that contains the important passage
or None if it was not found
"""
index_within_spaced_version = " ".join(all_lines).find(important_passage)
working_index = 0
for i in range(len(all_lines)):
line = all_lines[i]
if working_index <= index_within_spaced_version < working_index + len(line):
return i
working_index += len(line) + 1 # extra 1 for the newline
return None
def find_lines_to_show(all_lines, important_passage):
"""
Find the line that contains ``important_passage`` and return
the start of the range of 7 lines that provides the largest possible
context around it.
:param all_lines: The lines to search
:param important_passage: The passage to search for
:return: index of the first line in a range of 7 lines with the widest context.
"""
if len(all_lines) <= 7:
return 0
try:
passage_line = which_line_contains(all_lines, important_passage)
if passage_line <= 3:
return 0
except TypeError as e:
raise TypeError(f"ip: {important_passage} | {all_lines}") from e
if passage_line >= len(all_lines) - 4:
return len(all_lines) - 7
return passage_line - 3
display = supervisor.runtime.display
# WIFI Setup
wifi_ssid = os.getenv("CIRCUITPY_WIFI_SSID")
wifi_password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
if wifi_ssid is None:
print("WiFi credentials are kept in settings.toml, please add them there!")
raise ValueError("SSID not found in environment variables")
try:
wifi.radio.connect(wifi_ssid, wifi_password)
except ConnectionError:
print("Failed to connect to WiFi with provided credentials")
raise
# Wait a few seconds for WIFI to finish connecting and be ready
time.sleep(2)
# Fetch time from NTP
pool = socketpool.SocketPool(wifi.radio)
ntp = adafruit_ntp.NTP(
pool,
server="0.adafruit.pool.ntp.org",
tz_offset=TZ_OFFSET_FROM_UTC,
cache_seconds=3600,
)
# Update the system RTC from the NTP time
rtc.RTC().datetime = ntp.datetime
# main group to hold all other visual elements
main_group = displayio.Group()
# background group used for white background behind the quote
# scale 8x to save memory on the Bitmap
bg_group = displayio.Group(scale=8)
# Create & append Bitmap for the white background
bg_bmp = displayio.Bitmap(display.width // 8, display.height // 8, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0xFFFFFF
bg_tg = displayio.TileGrid(bg_bmp, pixel_shader=bg_palette)
bg_group.append(bg_tg)
main_group.append(bg_group)
# Setup accent palette for the outlined text
accent_palette = displayio.Palette(5)
accent_palette[3] = 0x666666
accent_palette[4] = 0xFFFFFF
# Setup BitmapLabel to show the quote
quote_lbl = Label(
terminalio.FONT, text="", color=0x666666, color_palette=accent_palette
)
quote_lbl.anchor_point = (0, 0)
quote_lbl.anchored_position = (2, 2)
main_group.append(quote_lbl)
# Setup BitmapLabel to show book title and author
book_info_lbl = Label(terminalio.FONT, text="", color=0x666666)
book_info_lbl.anchor_point = (0, 1.0) # place it at the bottom of the display
book_info_lbl.anchored_position = (2, display.height - 2)
main_group.append(book_info_lbl)
# Set main group containing visual elements to show on the display
display.root_group = main_group
while True:
# get the current time from system RTC
now = time.localtime()
# break out the current hour in 24hr format
hour = f"{now.tm_hour:02d}"
# open the data file for the current hour
with open(f"quotes_data/{hour}.csv.gz", "rb") as f:
# read and unzip the data
compressed_data = f.read()
rows = zlib.decompress(compressed_data).split(b"\n")
# break out the current minute
current_minute = f"{now.tm_min:02d}".encode("utf-8")
print(f"hour: {hour} min: {current_minute}")
# list to hold possible quotes for the current time
options = []
# get the previous minute also for alternate choices
previous_minute = f"{now.tm_min - 1:02d}".encode("utf-8")
# list to hold alternate choices
alternates = []
# loop over all rows in the data from the CSV
for row in rows:
# if the current row is for the current time
if row[3:5] == current_minute:
# add the current row as a potential choice to show
options.append(row)
# if the current row is for the previous minute
if row[3:5] == previous_minute:
# add the current row as an alternate choice
alternates.append(row)
# if there is at least one option for the current time
if len(options) > 0:
# make a random choice from the possible quote options
choice = random.choice(options)
else: # No options for current time
# use a random choice from the previous minute instead
choice = random.choice(alternates)
# decode the row of data from bytes to a string
row_str = choice.decode("utf-8")
# split the data on the pipe character
parts = row_str.split("|")
# extract the quote text
quote = parts[2]
# extract the author
author = parts[4]
# extract the book title
title = parts[3]
# extract the current time reference string
time_part = parts[1]
# get start and end indexes of the time reference
time_start_index = quote.find(time_part)
time_end_index = time_start_index + len(time_part)
# split the quote text into lines with a maximum width that fits
# on the MagTag display
quote_lines = wrap_text_to_pixels(
quote,
display.width - 4,
terminalio.FONT,
outline_accent_ranges=[
(time_start_index, time_end_index, quote_lbl.outline_size)
],
)
# remove previous accents
quote_lbl.clear_accent_ranges()
# find the index of the first line we want to show.
# only relevant for long quotes, short ones will be shown in full
first_line_to_show = find_lines_to_show(quote_lines, time_part)
# Temporary version of final visible quote joined with spaces instead of newlines,
# so we can search for the time_part without worrying about potential newlines.
shown_quote_with_spaces = " ".join(
quote_lines[first_line_to_show: first_line_to_show + 7]
)
# find the current time reference within the quote that will be shown
time_start_index = shown_quote_with_spaces.find(time_part)
time_end_index = time_start_index + len(time_part)
# wrap the quote to be shown to multiple lines and set it on the label
quote_lbl.text = "\n".join(quote_lines[first_line_to_show: first_line_to_show + 7])
# accent the part of the quote that references the current time
quote_lbl.add_accent_range(time_start_index, time_end_index, 4, 3, "outline")
# show title and author in the book info label
# allow it to split to two lines if the quote is short enough
if len(quote_lines) <= 6:
book_info = f"{title} - {author}"
book_info_lbl.text = "\n".join(wrap_text_to_pixels(
book_info,
display.width - 4,
terminalio.FONT,
))
else:
book_info_lbl.text = f"{title} - {author}"
# update the display and wait 60 seconds
display.refresh()
time.sleep(60)
Drive Structure
After copying the files, your drive should look like the listing below. It can contain other files as well, but must contain these at a minimum.
This project uses brand new functionality in the adafruit_display_text library. The MagTag device has an older version of the library without the necessary features frozen in to its firmware. In order for the project to work you must place the adafruit_display_text folder in the root of CIRCUITPY instead of inside of the lib/ folder like normal.
Page last edited January 16, 2026
Text editor powered by tinymce.