To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.
Thankfully, we can do this 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. Extract the contents of the zip file, open the directory literary-clock/ and then click on the directory that matches the version of CircuitPython you're using.
Connect your MagTag board to your computer via a known good USB data+power cable. The board should show up as a thumb drive named CIRCUITPY in Explorer or Finder (depending on your operating system). Copy the contents of that directory to your CIRCUITPY drive.
Your CIRCUITPY drive should now look similar to the following image:
# SPDX-FileCopyrightText: 2022 Eva Herrada for Adafruit Industries # SPDX-License-Identifier: MIT import time import ssl import gc import socketpool import wifi import adafruit_minimqtt.adafruit_minimqtt as MQTT from adafruit_io.adafruit_io import IO_MQTT import adafruit_datetime import adafruit_display_text from adafruit_display_text import label import board from adafruit_bitmap_font import bitmap_font import displayio from adafruit_display_shapes.rect import Rect UTC_OFFSET = -4 quotes = {} with open("quotes.csv", "r", encoding="UTF-8") as F: for quote_line in F: split = quote_line.split("|") quotes[split[0]] = split[1:] display = board.DISPLAY splash = displayio.Group() display.root_group = splash arial = bitmap_font.load_font("fonts/Arial-12.pcf") bold = bitmap_font.load_font("fonts/Arial-Bold-12.pcf") LINE_SPACING = 0.8 HEIGHT = arial.get_bounding_box()[1] QUOTE_X = 10 QUOTE_Y = 7 rect = Rect(0, 0, 296, 128, fill=0xFFFFFF, outline=0xFFFFFF) splash.append(rect) quote = label.Label( font=arial, x=QUOTE_X, y=QUOTE_Y, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(quote) time_label = label.Label( font=bold, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(time_label) time_label_2 = label.Label( font=bold, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(time_label_2) after_label = label.Label( font=arial, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(after_label) after_label_2 = label.Label( font=arial, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(after_label_2) author_label = label.Label( font=arial, x=QUOTE_X, y=115, color=0x000000, line_spacing=LINE_SPACING ) splash.append(author_label) try: from secrets import secrets except ImportError: print("WiFi secrets are kept in secrets.py, please add them there!") raise aio_username = secrets["aio_username"] aio_key = secrets["aio_key"] print(f"Connecting to {secrets['ssid']}") wifi.radio.connect(secrets["ssid"], secrets["password"]) print(f"Connected to {secrets['ssid']}!") def get_width(font, text): return sum(font.get_glyph(ord(c)).shift_x for c in text) def smart_split(text, font, width): words = "" spl = text.split(" ") for i, word in enumerate(spl): words += f" {word}" lwidth = get_width(font, words) if width + lwidth > 276: spl[i] = "\n" + spl[i] text = " ".join(spl) break return text def connected(client): # pylint: disable=unused-argument io.subscribe_to_time("iso") def disconnected(client): # pylint: disable=unused-argument print("Disconnected from Adafruit IO!") def update_text(hour_min): quote.text = ( time_label.text ) = time_label_2.text = after_label.text = after_label_2.text = "" before, time_text, after = quotes[hour_min][0].split("^") text = adafruit_display_text.wrap_text_to_pixels(before, 276, font=arial) quote.text = "\n".join(text) for line in text: width = get_width(arial, line) time_text = smart_split(time_text, bold, width) split_time = time_text.split("\n") if time_text[0] != "\n": time_label.x = time_x = QUOTE_X + width time_label.y = time_y = QUOTE_Y + int((len(text) - 1) * HEIGHT * LINE_SPACING) time_label.text = split_time[0] if "\n" in time_text: time_label_2.x = time_x = QUOTE_X time_label_2.y = time_y = QUOTE_Y + int(len(text) * HEIGHT * LINE_SPACING) wrapped = adafruit_display_text.wrap_text_to_pixels( split_time[1], 276, font=arial ) time_label_2.text = "\n".join(wrapped) width = get_width(bold, split_time[-1]) + time_x - QUOTE_X if after: after = smart_split(after, arial, width) split_after = after.split("\n") if after[0] != "\n": after_label.x = QUOTE_X + width after_label.y = time_y after_label.text = split_after[0] if "\n" in after: after_label_2.x = QUOTE_X after_label_2.y = time_y + int(HEIGHT * LINE_SPACING) wrapped = adafruit_display_text.wrap_text_to_pixels( split_after[1], 276, font=arial ) after_label_2.text = "\n".join(wrapped) author = f"{quotes[hour_min][2]} - {quotes[hour_min][1]}" author_label.text = adafruit_display_text.wrap_text_to_pixels( author, 276, font=arial )[0] time.sleep(display.time_to_refresh + 0.1) display.refresh() LAST = None def message(client, feed_id, payload): # pylint: disable=unused-argument global LAST # pylint: disable=global-statement timezone = adafruit_datetime.timezone.utc timezone._offset = adafruit_datetime.timedelta( # pylint: disable=protected-access seconds=UTC_OFFSET * 3600 ) datetime = adafruit_datetime.datetime.fromisoformat(payload[:-1]).replace( tzinfo=timezone ) local_datetime = datetime.tzinfo.fromutc(datetime) print(local_datetime) hour_min = f"{local_datetime.hour:02}:{local_datetime.minute:02}" if local_datetime.minute != LAST: if hour_min in quotes: update_text(hour_min) LAST = local_datetime.minute gc.collect() # Create a socket pool pool = socketpool.SocketPool(wifi.radio) # Initialize a new MQTT Client object mqtt_client = MQTT.MQTT( broker="io.adafruit.com", port=1883, username=secrets["aio_username"], password=secrets["aio_key"], socket_pool=pool, ssl_context=ssl.create_default_context(), ) # Initialize an Adafruit IO MQTT Client io = IO_MQTT(mqtt_client) # Connect the callback methods defined above to Adafruit IO io.on_connect = connected io.on_disconnect = disconnected io.on_message = message # Connect to Adafruit IO print("Connecting to Adafruit IO...") io.connect() while True: try: io.loop() except (ValueError, RuntimeError) as e: print("Failed to get data, retrying\n", e) wifi.reset() io.reconnect() continue time.sleep(1)
The code starts out by importing all the libraries it needs - quite a lot in this case.
import time import ssl import gc import socketpool import wifi import adafruit_minimqtt.adafruit_minimqtt as MQTT from adafruit_io.adafruit_io import IO_MQTT import adafruit_datetime import adafruit_display_text from adafruit_display_text import label import board from adafruit_bitmap_font import bitmap_font import displayio from adafruit_display_shapes.rect import Rect
Then, it sets the UTC offset - you should modify this to your current local UTC offset (you can find that here: https://www.timeanddate.com/time/zone/timezone/utc).
It then imports the quotes file and starts to set up the display and fonts.
UTC_OFFSET = -4 quotes = {} with open("quotes.csv", "r", encoding="UTF-8") as F: for quote_line in F: split = quote_line.split("|") quotes[split[0]] = split[1:] display = board.DISPLAY splash = displayio.Group() display.root_group = splash arial = bitmap_font.load_font("fonts/Arial-12.pcf") bold = bitmap_font.load_font("fonts/Arial-Bold-12.pcf") LINE_SPACING = 0.8 HEIGHT = arial.get_bounding_box()[1] QUOTE_X = 10 QUOTE_Y = 7
Now, the display background and text labels are set up and added to the display group.
rect = Rect(0, 0, 296, 128, fill=0xFFFFFF, outline=0xFFFFFF) splash.append(rect) quote = label.Label( font=arial, x=QUOTE_X, y=QUOTE_Y, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(quote) time_label = label.Label( font=bold, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(time_label) time_label_2 = label.Label( font=bold, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(time_label_2) after_label = label.Label( font=arial, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(after_label) after_label_2 = label.Label( font=arial, color=0x000000, line_spacing=LINE_SPACING, ) splash.append(after_label_2) author_label = label.Label( font=arial, x=QUOTE_X, y=115, color=0x000000, line_spacing=LINE_SPACING ) splash.append(author_label)
After that, the MagTag attempts to connect to the internet.
try: from secrets import secrets except ImportError: print("WiFi secrets are kept in secrets.py, please add them there!") raise aio_username = secrets["aio_username"] aio_key = secrets["aio_key"] print(f"Connecting to {secrets['ssid']}") wifi.radio.connect(secrets["ssid"], secrets["password"]) print(f"Connected to {secrets['ssid']}!")
At this point, we start defining a few helper functions.
The first one, get_width
, is used to get the width of a string, in pixels, when passed the string and the font the string will be displayed in.
The next one, smart_split
, is used to tell the code when to wrap a line when it's not the first label being used in a block of text. This is necessary since the code uses multiple fonts (bold and normal Arial 12pt.) in the same text block.
The last two are functions that are run when Adafruit IO is initially connected to - it subscribes the user to the ISO formatted time feed - and when it is disconnected from, respectively.
def get_width(font, text): return sum(font.get_glyph(ord(c)).shift_x for c in text) def smart_split(text, font, width): words = "" spl = text.split(" ") for i, word in enumerate(spl): words += f" {word}" lwidth = get_width(font, words) if width + lwidth > 276: spl[i] = "\n" + spl[i] text = " ".join(spl) break return text def connected(client): # pylint: disable=unused-argument io.subscribe_to_time("iso") def disconnected(client): # pylint: disable=unused-argument print("Disconnected from Adafruit IO!")
This function is run whenever the quote to be displayed is updated. It's a bit complicated but a very important part of this project.
It starts by wiping all of the labels since we don't use every single label every time.
It then goes on to separate the different parts of the quote so it can set one part of that as bold and the rest as normal and sets the text of the part of the quote prior to the time.
Then the code for setting the location of the time text and the text after the time text is run, which account for the possibility that the first line of that may need to be wrapped over to the next line.
Finally, the display is refreshed with the new quote.
def update_text(hour_min): quote.text = ( time_label.text ) = time_label_2.text = after_label.text = after_label_2.text = "" before, time_text, after = quotes[hour_min][0].split("^") text = adafruit_display_text.wrap_text_to_pixels(before, 276, font=arial) quote.text = "\n".join(text) for line in text: width = get_width(arial, line) time_text = smart_split(time_text, bold, width) split_time = time_text.split("\n") if time_text[0] != "\n": time_label.x = time_x = QUOTE_X + width time_label.y = time_y = QUOTE_Y + int((len(text) - 1) * HEIGHT * LINE_SPACING) time_label.text = split_time[0] if "\n" in time_text: time_label_2.x = time_x = QUOTE_X time_label_2.y = time_y = QUOTE_Y + int(len(text) * HEIGHT * LINE_SPACING) wrapped = adafruit_display_text.wrap_text_to_pixels( split_time[1], 276, font=arial ) time_label_2.text = "\n".join(wrapped) width = get_width(bold, split_time[-1]) + time_x - QUOTE_X if after: after = smart_split(after, arial, width) split_after = after.split("\n") if after[0] != "\n": after_label.x = QUOTE_X + width after_label.y = time_y after_label.text = split_after[0] if "\n" in after: after_label_2.x = QUOTE_X after_label_2.y = time_y + int(HEIGHT * LINE_SPACING) wrapped = adafruit_display_text.wrap_text_to_pixels( split_after[1], 276, font=arial ) after_label_2.text = "\n".join(wrapped) author = f"{quotes[hour_min][2]} - {quotes[hour_min][1]}" author_label.text = adafruit_display_text.wrap_text_to_pixels( author, 276, font=arial )[0] time.sleep(display.time_to_refresh + 0.1) display.refresh()
This function is run whenever the IO feed gets a new value, so roughly once a second. It starts by converting the received UTC time into the local time.
Then it checks to see if the time received is the same hour and minute as the last time received and if a quote entry exists for said time. If it isn't the same time and a quote does exist, the code then sends the time to the function above to update the quote.
LAST = None def message(client, feed_id, payload): # pylint: disable=unused-argument global LAST # pylint: disable=global-statement timezone = adafruit_datetime.timezone.utc timezone._offset = adafruit_datetime.timedelta( # pylint: disable=protected-access seconds=UTC_OFFSET * 3600 ) datetime = adafruit_datetime.datetime.fromisoformat(payload[:-1]).replace( tzinfo=timezone ) local_datetime = datetime.tzinfo.fromutc(datetime) print(local_datetime) hour_min = f"{local_datetime.hour:02}:{local_datetime.minute:02}" if local_datetime.minute != LAST: if hour_min in quotes: update_text(hour_min) LAST = local_datetime.minute gc.collect()
However, before any of those functions can be used, the code needs to set up the Adafruit IO MQTT connection, which the following code does.
# Create a socket pool pool = socketpool.SocketPool(wifi.radio) # Initialize a new MQTT Client object mqtt_client = MQTT.MQTT( broker="io.adafruit.com", port=1883, username=secrets["aio_username"], password=secrets["aio_key"], socket_pool=pool, ssl_context=ssl.create_default_context(), ) # Initialize an Adafruit IO MQTT Client io = IO_MQTT(mqtt_client) # Connect the callback methods defined above to Adafruit IO io.on_connect = connected io.on_disconnect = disconnected io.on_message = message # Connect to Adafruit IO print("Connecting to Adafruit IO...") io.connect()
After it is connected the code runs through this loop to continually check for a new feed update from the Adafruit IO time feed.
while True: try: io.loop() except (ValueError, RuntimeError) as e: print("Failed to get data, retrying\n", e) wifi.reset() io.reconnect() continue time.sleep(1)
Page last edited January 19, 2025
Text editor powered by tinymce.