Maker.io main logo

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

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.)‎

link_2

link_3

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.‎

magtag_4

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.‎

loading_5

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.‎

launch_6

If the UF2 bootloader is installed, you will see a new disk drive ‎appear called MAGTAGBOOT.‎

drive_7

Copy the UF2 file you downloaded at the first step of this tutorial ‎onto the MAGTAGBOOT drive.‎

copy_8

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.‎

drive_9

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.‎

output_10

Press reset to exit the bootloader.‎

Your CIRCUITPY drive should appear!‎

You're all set! Go to the next pages.‎

pages_11

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.‎

Download Project Bundle

Copy Code
# 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")

View on GitHub

Your CIRCUITPY drive should resemble the following.‎

pyfile_12

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:‎

Download Project Bundle

Copy Code
# 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
}

View on GitHub

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:‎

console_13

In order, the example code...‎

Checks the ESP32's MAC address.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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. ‎

Download File

Copy Code
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.‎

Download File

Copy Code
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. ‎

Download File

Copy Code
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. ‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.

key_14

You will get a popup with your Username and Key (In this ‎screenshot, we've covered it with red blocks.)‎

popup_15

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:‎

Download File

Copy Code
# 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.‎

Download File

Copy Code
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!‎

text_16

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

bug_17

Download Project Bundle

Copy Code
# 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)

View on GitHub

Code run-through

The code starts out by importing all the libraries it needs - quite a lot ‎in this case.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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

Now, the display background and text labels are set up and added to ‎the display group.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
# 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.‎

Download File

Copy Code
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.‎

Find your UTC offset here

offset_18

After that, plug your MagTag into a power source and you should be ‎good to go!‎

complete_19

制造商零件编号 4800
ADAFRUIT MAGTAG - 2.9" GRAYSCALE
Adafruit Industries LLC
¥294.48
Details
制造商零件编号 4474
CABLE A PLUG TO C PLUG 3'
Adafruit Industries LLC
¥40.29
Details
制造商零件编号 4631
MINI-MAGNET FEET FOR RGB LED MAT
Adafruit Industries LLC
¥21.06
Details
制造商零件编号 4807
ACRYLIC + HARDWARE KIT FOR ADAFR
Adafruit Industries LLC
¥51.20
Details
Add all DigiKey Parts to Cart
TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum