MagTag Literary Quote Clock
2023-02-07 | By Adafruit Industries
License: See Original Project
Courtesy of Adafruit
Guide by Eva Herrada
Overview
This project is perfect for the book lover in all of us. In this project you ‎will create a clock that tells the time using quotes from books. It'll ‎update every five or so minutes (there are enough quotes to update ‎more often but doing so has the potential to wear out your eInk ‎display). Each time, it will display a quote that has the time ‎highlighted in bold.‎
The code for this project also has some useful functions for mixing ‎multiple fonts in a relatively seamless text block, so if you're trying to ‎do that check out the code attached.‎
Parts
‎5V 2A Switching Power Supply w/ USB-A Connector
If you'd like to mount yours like I did in the pictures:‎
This project is not designed with low power consumption in mind. I ‎would not recommend running it off of a LiPo.
If you would like an acrylic frame:‎
Install CircuitPython
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.‎
Set Up CircuitPython
Follow the steps to get CircuitPython installed on your MagTag.‎
Download the latest CircuitPython for your board from ‎circuitpython.org
Click the link above and download the latest .BIN and .UF2 file
‎(Depending on how you program the ESP32S2 board you may need ‎one or the other, might as well get both.)‎
Download and save it to your desktop (or wherever is handy.)‎
Plug your MagTag into your computer using a known-good USB ‎cable.‎
A lot of people end up using charge-only USB cables and it is very ‎frustrating! So, make sure you have a USB cable you know is good ‎for data sync.‎
Option 1 - Load with UF2 ‎Bootloader
This is by far the easiest way to load CircuitPython. However, it ‎requires your board has the UF2 bootloader installed. Some early ‎boards do not (we hadn't written UF2 yet!) - in which case you can ‎load using the built in ROM bootloader.‎
Still, try this first!‎
Try Launching UF2 Bootloader
Loading CircuitPython by drag-n-drop UF2 bootloader is the easier ‎way and we recommend it. If you have a MagTag where the front of ‎the board is black, your MagTag came with UF2 already on it.‎
Launch UF2 by double-clicking the Reset button (the one next to ‎the USB C port). You may have to try a few times to get the timing ‎right.‎
If the UF2 bootloader is installed, you will see a new disk drive ‎appear called MAGTAGBOOT.‎
Copy the UF2 file you downloaded at the first step of this tutorial ‎onto the MAGTAGBOOT drive.‎
If you're using Windows and you get an error at the end of the file ‎copy that says Error from the file copy, Error 0x800701B1: A device ‎which does not exist was specified. You can ignore this error, the ‎bootloader sometimes disconnects without telling Windows, the ‎install completed just fine and you can continue. If its really ‎annoying, you can also upgrade the bootloader (the latest version of ‎the UF2 bootloader fixes this warning.)‎
Your board should auto-reset into CircuitPython, or you may need to ‎press reset. A CIRCUITPY drive will appear. You're done! Go to the ‎next pages.‎
Option 2 - Use esptool to load BIN ‎File
If you have an original MagTag with while soldermask on the front, ‎we didn't have UF2 written for the ESP32S2 yet so it will not come ‎with the UF2 bootloader.‎
You can upload with esptool to the ROM (hardware) bootloader ‎instead!‎
Follow the initial steps found in the Run esptool and check ‎connection section of the ROM Bootloader page to verify your ‎environment is set up, your board is successfully connected, and ‎which port it's using.‎
In the final command to write a binary file to the board, replace ‎the port with your port, and replace "firmware.bin" with the file ‎you downloaded above.‎
The output should look something like the output in the image.‎
Press reset to exit the bootloader.‎
Your CIRCUITPY drive should appear!‎
You're all set! Go to the next pages.‎
Option 3 - Use Chrome Browser to ‎Upload BIN File
If for some reason you cannot get esptool to run, you can always try ‎using the Chrome-browser version of esptool we have written. This is ‎handy if you don't have Python on your computer, or something is ‎really weird with your setup that makes esptool not run (which ‎happens sometimes and isn't worth debugging!) You can follow ‎along on the Web Serial ESPTool page and either load the UF2 ‎bootloader and then come back to Option 1 on this page, or you can ‎download the CircuitPython BIN file directly using the tool in the ‎same manner as the bootloader.‎
CircuitPython Internet Test
One of the great things about the ESP32 is the built-in Wi-Fi ‎capabilities. This page covers the basics of getting connected using ‎CircuitPython.‎
The first thing you need to do is update your code.py to the following. ‎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 and copy the entire lib folder and ‎the code.py file to your CIRCUITPY drive.‎
# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import ipaddress
import ssl
import wifi
import socketpool
import adafruit_requests
# URLs to fetch from
TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html"
JSON_QUOTES_URL = "https://www.adafruit.com/api/quotes.php"
JSON_STARS_URL = "https://api.github.com/repos/adafruit/circuitpython"
# 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("ESP32-S2 WebClient Test")
print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])
print("Available WiFi networks:")
for network in wifi.radio.start_scanning_networks():
print("\t%s\t\tRSSI: %d\tChannel: %d" % (str(network.ssid, "utf-8"),
network.rssi, network.channel))
wifi.radio.stop_scanning_networks()
print("Connecting to %s"%secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print("Connected to %s!"%secrets["ssid"])
print("My IP address is", wifi.radio.ipv4_address)
ipv4 = ipaddress.ip_address("8.8.4.4")
print("Ping google.com: %f ms" % (wifi.radio.ping(ipv4)*1000))
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
print("Fetching text from", TEXT_URL)
response = requests.get(TEXT_URL)
print("-" * 40)
print(response.text)
print("-" * 40)
print("Fetching json from", JSON_QUOTES_URL)
response = requests.get(JSON_QUOTES_URL)
print("-" * 40)
print(response.json())
print("-" * 40)
print()
print("Fetching and parsing json from", JSON_STARS_URL)
response = requests.get(JSON_STARS_URL)
print("-" * 40)
print("CircuitPython GitHub Stars", response.json()["stargazers_count"])
print("-" * 40)
print("done")Your CIRCUITPY drive should resemble the following.‎
To get connected, the next thing you need to do is update ‎the secrets.py file.‎
Secrets File
We expect people to share tons of projects as they build ‎CircuitPython Wi-Fi widgets. What we want to avoid is people ‎accidentally sharing their passwords or secret tokens and API keys. ‎So, we designed all our examples to use a secrets.py file, which is on ‎your CIRCUITPY drive, to hold secret/private/custom data. That way ‎you can share your main project without worrying about accidentally ‎sharing private stuff.‎
The initial secrets.py file on your CIRCUITPY drive should look like ‎this:‎
# SPDX-FileCopyrightText: 2020 Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it
secrets = {
'ssid' : 'home_wifi_network',
'password' : 'wifi_password',
'aio_username' : 'my_adafruit_io_username',
'aio_key' : 'my_adafruit_io_key',
'timezone' : "America/New_York", # http://worldtimeapi.org/timezones
}Inside is a Python dictionary named secrets with a line for each entry. ‎Each entry has an entry name (say 'ssid') and then a colon to ‎separate it from the entry key ('home_wifi_network') and finally a ‎comma (,).‎
At a minimum you'll need to adjust the ssid and password for your ‎local Wi-Fi setup so do that now!‎
As you make projects you may need more tokens and keys, just add ‎them one line at a time. See for example other tokens such as one ‎for accessing GitHub or the Hackaday API. Other non-secret data like ‎your timezone can also go here, just cause it’s called secrets doesn't ‎mean you can't have general customization data in there!‎
For the correct time zone string, look ‎at http://worldtimeapi.org/timezones and remember that if your city ‎is not listed, look for a city in the same time zone, for example ‎Boston, New York, Philadelphia, Washington DC, and Miami are all ‎on the same time as New York.‎
Of course, don't share your secrets.py - keep that out of GitHub, ‎Discord or other project-sharing sites.‎
Don't share your secrets.py file, it has your passwords and API keys in ‎it!‎
If you connect to the serial console, you should see something like ‎the following:‎
In order, the example code...‎
Checks the ESP32's MAC address.‎
print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])Performs a scan of all access points and prints out the access point's ‎name (SSID), signal strength (RSSI), and channel.‎
print("Avaliable WiFi networks:")
for network in wifi.radio.start_scanning_networks():
print("\t%s\t\tRSSI: %d\tChannel: %d" % (str(network.ssid, "utf-8"),
network.rssi, network.channel))
wifi.radio.stop_scanning_networks()Connects to the access point you defined in the secrets.py file, prints ‎out its local IP address, and attempts to ping google.com to check its ‎network connectivity. ‎
print("Connecting to %s"%secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print(print("Connected to %s!"%secrets["ssid"]))
print("My IP address is", wifi.radio.ipv4_address)
ipv4 = ipaddress.ip_address("8.8.4.4")
print("Ping google.com: %f ms" % wifi.radio.ping(ipv4))The code creates a socketpool using the wifi radio's available sockets. ‎This is performed so we don't need to re-use sockets. Then, it ‎initializes a new instance of the requests interface - which makes ‎getting data from the internet really really easy.‎
pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context())
To read in plain-text from a web URL, call requests.get - you may ‎pass in either a http, or a https url for SSL connectivity. ‎
print("Fetching text from", TEXT_URL)
response = requests.get(TEXT_URL)
print("-" * 40)
print(response.text)
print("-" * 40)Requests can also display a JSON-formatted response from a web ‎URL using a call to requests.get. ‎
print("Fetching json from", JSON_QUOTES_URL)
response = requests.get(JSON_QUOTES_URL)
print("-" * 40)
print(response.json())
print("-" * 40)Finally, you can fetch and parse a JSON URL using requests.get. This ‎code snippet obtains the stargazers_count field from a call to the ‎GitHub API.‎
print("Fetching and parsing json from", JSON_STARS_URL)
response = requests.get(JSON_STARS_URL)
print("-" * 40)
print("CircuitPython GitHub Stars", response.json()["stargazers_count"])
print("-" * 40)OK you now have your ESP32 board set up with a ‎proper secrets.py file and can connect over the Internet. If not, check ‎that your secrets.py file has the right ssid and password and retrace ‎your steps until you get the Internet connectivity working!‎
Getting The Date & Time
A very common need for projects is to know the current date and ‎time. Especially when you want to deep sleep until an event, or you ‎want to change your display based on what day, time, date, etc. it is.
Determining the correct local time is really really hard. There are ‎various time zones, Daylight Savings dates, leap seconds, ‎etc. Trying to get NTP time and then back-calculating what the local ‎time is, is extraordinarily hard on a microcontroller just isn't worth ‎the effort and it will get out of sync as laws change anyways.‎
For that reason, we have the free adafruit.io time service. Free for ‎anyone with a free adafruit.io account. You do need an ‎account because we have to keep accidentally mis-programmed-‎board from overwhelming adafruit.io and lock them out temporarily. ‎Again, it's free!‎
There are other services like WorldTimeAPI, but we don't use those ‎for our guides because they are nice people, and we don't want to ‎accidentally overload their site. Also, there's a chance it may ‎eventually go down or also require an account.‎
Step 1) Make an Adafruit account
It's free! Visit https://accounts.adafruit.com/ to register and make an ‎account if you do not already have one.
Step 2) Sign into Adafruit IO
Head over to io.adafruit.com and click Sign In to log into IO using ‎your Adafruit account. It's free and fast to join.‎
Step 3) Get your Adafruit IO Key
Click on My Key in the top bar.
You will get a popup with your Username and Key (In this ‎screenshot, we've covered it with red blocks.)‎
Go to your secrets.py file on your CIRCUITPY drive and add three ‎lines for aio_username, aio_key and timezone so you get something like ‎the following:‎
# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it
secrets = {
'ssid' : 'home_wifi_network',
'password' : 'wifi_password',
'aio_username' : 'my_adafruit_io_username',
'aio_key' : 'my_adafruit_io_key',
'timezone' : "America/New_York", # http://worldtimeapi.org/timezones
}The timezone is optional, if you don't have that entry, adafruit.io will ‎guess your timezone based on geographic IP address lookup. You ‎can visit http://worldtimeapi.org/timezones to see all the time zones ‎available (even though we do not use Worldtime for timekeeping, ‎we do use the same time zone table.)
Step 4) Upload Test Python Code
This code is like the Internet Test code from before, but this time it ‎will connect to adafruit.io and get the local time.‎
import ipaddress
import ssl
import wifi
import socketpool
import adafruit_requests
import secrets
TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html"
JSON_QUOTES_URL = "https://www.adafruit.com/api/quotes.php"
JSON_STARS_URL = "https://api.github.com/repos/adafruit/circuitpython"
# 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
# Get our username, key and desired timezone
aio_username = secrets["aio_username"]
aio_key = secrets["aio_key"]
location = secrets.get("timezone", None)
TIME_URL = "https://io.adafruit.com/api/v2/%s/integrations/time/strftime?x-aio-key=%s&tz=%s" % (aio_username, aio_key, location)
TIME_URL = "&fmt=%Y-%m-%d %H:%M:%S.%L %j %u %z %Z"
print("ESP32-S2 Adafruit IO Time test")
print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])
print("Available WiFi networks:")
for network in wifi.radio.start_scanning_networks():
print("\t%s\t\tRSSI: %d\tChannel: %d" % (str(network.ssid, "utf-8"),
network.rssi, network.channel))
wifi.radio.stop_scanning_networks()
print("Connecting to %s"%secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print("Connected to %s!"%secrets["ssid"])
print("My IP address is", wifi.radio.ipv4_address)
ipv4 = ipaddress.ip_address("8.8.4.4")
print("Ping google.com: %f ms" % wifi.radio.ping(ipv4))
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
print("Fetching text from", TIME_URL)
response = requests.get(TIME_URL)
print("-" * 40)
print(response.text)
print("-" * 40)After running this, you will see something like the below text. We ‎have blocked out the part with the secret username and key data!‎
Note at the end you will get the date, time, and your time zone! If so, ‎you have correctly configured your secrets.py and can continue to ‎the next steps!‎
Code the MagTag Quote Clock
Installing Project Code
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:‎
The below image does not show quotes.csv, this is a bug. It should ‎be in the zip file you download on the same level as code.py
# 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.show(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)Code run-through
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.show(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 = 7Now, 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)Using The MagTag Quote Clock
If you've already loaded all the code onto the board, all that's left to ‎do is update the UTC offset on line 20 of code.py to reflect your local ‎time. UTC is 4 hours ahead of my local time, so I set my UTC offset ‎to -4.‎
After that, plug your MagTag into a power source and you should be ‎good to go!‎

