Before anything else, plug the battery into the MagTag. The jack is right next to the Adafruit logo on the back of the board. If you would like to use a double-sided adhesive, put it where you want the battery to go, press down firmly, then remove the piece of paper covering the exposed side and stick the battery to it.

Installing the Project Code

Download a zip of the project code by clicking 'Download: Project Zip' in the preview of code.py below.

After unzipping the file, copy its contents to the CIRCUITPY drive which appears when the MagTag is connected to your computer via a USB cable and turned on via a small on/off switch onboard.

You don't need to install any other libraries than the ones you installed in the MagTag-Specific CircuitPython Libraries page.

After you've copied everything over, your CIRCUITPY drive, it should look something like this:

CIRCUITPY

You'll need to get an OSHWA API key and paste it in on line 34 of code.py.

# SPDX-FileCopyrightText: 2021 Dylan Herrada for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import random
import ssl
import gc
import wifi
import socketpool
import adafruit_requests as requests
from adafruit_magtag.magtag import MagTag

# 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

# Initialize magtag object
magtag = MagTag()

magtag.set_background("bmps/oshwa_full.bmp")

# Set up WiFi
wifi.radio.connect(secrets["ssid"], secrets["password"])
print(f"Connected to {secrets['ssid']}!")
print("My IP address is", wifi.radio.ipv4_address)

socket = socketpool.SocketPool(wifi.radio)
https = requests.Session(socket, ssl.create_default_context())

# Paste your API token below
TOKEN = "YOUR_API_TOKEN"


def font_width_to_dict(font):
    # Reads the font file to determine how wide each character is
    # Used to avoid bad wrapping breaking the QR code
    chars = {}
    with open(font, "r") as file:
        for line in file:
            if "FONTBOUNDINGBOX" in line:
                size = int(line.split(" ")[1])
            if "ENCODING" in line and "_ENCODING" not in line:
                character = chr(int(line.split(" ")[1][:-1]))
                chars[character] = None
            if "SWIDTH" in line:
                swidth = (int(line.split(" ")[1]) / 1000) * size
            if "DWIDTH" in line:
                chars[character] = int(int(line.split(" ")[1]) + swidth)
    return chars


def wrap(text, max_width, max_lines, font):
    # Used to wrap the title and description to avoid breaking the QR code
    lines = []
    ellipsis = 3 * font["."]
    line = ""
    line_width = 0
    for word in text.split(" "):
        for character in word:
            line_width += font[character]
            if (
                len(lines) + 1 != max_lines
                or sum(font[i] for i in word) + line_width <= max_width
            ):
                if line_width > max_width:
                    print(str(line_width) + line)
                    line_width = sum(font[i] for i in word)
                    lines.append(line.strip())
                    line = word + " "
                    break
            else:
                for char_1 in word:
                    if line_width + ellipsis + font[char_1] > max_width:
                        line = line + "..."
                        print(str(line_width) + line)
                        lines.append(line)
                        return "\n".join(lines[:max_lines])
                    line = line + char_1
                    line_width += font[char_1]

        else:
            line = line + word + " "

    lines.append(line.strip())
    return "\n".join(lines[:max_lines])


# Get first 300 items, saving only the OSHWA UIDs. The first 300 are also used to find the
# number of requests that will need to be made.
# This was done this way since if the items themselves were all asked for and stored, the MagTag
# would run out of memory. If we just got the number of total projects and chose a random number,
# that also wouldn't work as you can only get individual projects with an OSHWA UID and these UIDs
# are prefixed by the country they were registered in, thus making getting it with a simple number
# in-between 1 and the total number of registered projects impossible.
URL = "https://certificationapi.oshwa.org/api/projects?limit=300"

print(URL)

payload = {}
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}"}

oshwaID = []

print("Getting number of projects and first set of 300 projects")
with https.get(URL, headers=headers, data=payload) as response:
    R_JSON = response.json()
    total = int(R_JSON["total"])
    print(f"{total} Projects")
    for i in R_JSON["items"]:
        oshwaID.append(i["oshwaUid"])
    R_JSON.clear()
    R_JSON = None
    gc.collect()

# Gets the rest of the OSHWA UIDs
print(len(oshwaID))
for i in range(int(total / 300)):
    print(f"Getting request {i+2}")
    url = (
        f"https://certificationapi.oshwa.org/api/projects?limit=300&offset={3*(i+1)}00"
    )
    with https.get(url, headers=headers, data=payload) as response:
        R_JSON = response.json()
        for item in R_JSON["items"]:
            oshwaID.append(item["oshwaUid"])
        R_JSON.clear()
        R_JSON = None
        gc.collect()
    print(f"{len(oshwaID)} IDs gathered")

# Select the UID that will be displayed
selected = random.choice(oshwaID)

# Get the project that will be displayed
url = f"https://certificationapi.oshwa.org/api/projects/{selected}"
response = https.get(url, headers=headers, data=payload)

selected = response.json()[0]

# Filters out characters that the API or the MagTag itself isn't handling correctly
for char in range(1, 32):
    selected["projectDescription"].replace(chr(char), "")

selected["projectDescription"] = (
    selected["projectDescription"]
    .replace("&#x27;", "'")
    .replace("&amp;#x27;", "'")
    .replace("&#x2F;", "/")
    .replace("&quot;", '"')
    .replace("’", "'")
)

# Add the two text fields
magtag.add_text(
    text_font="fonts/Arial-Bold-12.bdf",
    text_position=(5, 0),
    text_scale=1,
    line_spacing=0.7,
    text_anchor_point=(0, 0),
)

magtag.add_text(
    text_font="fonts/ArialMT-9.bdf",
    text_position=(5, 38),
    text_scale=1,
    line_spacing=0.6,
    text_anchor_point=(0, 0),
)

# Create the QR code
url = f"https://certification.oshwa.org/{selected['oshwaUid'].lower()}.html"
magtag.graphics.qrcode(url, qr_size=4, x=173, y=3)

# Prepare to wrap the text correctly by getting the width of each character for every font
arial_12 = font_width_to_dict("fonts/Arial-Bold-12.bdf")
arial_9 = font_width_to_dict("fonts/ArialMT-9.bdf")

# Set the text. On some characters, this fails. If so, run the whole file again in 5 seconds
try:
    magtag.set_text(wrap(selected["projectName"], 545, 2, arial_12), 0, False)
    magtag.set_text(wrap(selected["projectDescription"], 530, 19, arial_9), 1)
    magtag.exit_and_deep_sleep(3600)
except Exception:  # pylint: disable=broad-except
    print("Could not set title or description: unsupported glyphs.")
    print("Trying again in 10 seconds.")
    magtag.exit_and_deep_sleep(10)

Code Run Through

First, the code imports all the required libraries.

import random
import ssl
import gc
import wifi
import socketpool
import adafruit_requests as requests
from adafruit_magtag.magtag import MagTag

Next, the code gets the WiFi details and Adafruit IO API token from the secrets.py file.

try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise

Now, the MagTag object is initialized.

magtag = MagTag()

magtag.set_background("bmps/oshwa_full.bmp")

After that, the WiFi is set up and connected to.

# Set up WiFi
wifi.radio.connect(secrets["ssid"], secrets["password"])
print(f"Connected to {secrets['ssid']}!")
print("My IP address is", wifi.radio.ipv4_address)

socket = socketpool.SocketPool(wifi.radio)
https = requests.Session(socket, ssl.create_default_context())

This is the part where you'll paste your API token. If you haven't already done so, you can get it here.

# Paste your API token below
TOKEN = "YOUR_API_TOKEN"

The next two functions are used to wrap the description text by pixels instead of characters. This is because when it was being done by characters, the number of lines would vary greatly and the lines would sometimes go into the QR code, making it unreadable. This first function opens the font file and gets the width of every character.

def font_width_to_dict(font):
    # Reads the font file to determine how wide each character is
    # Used to avoid bad wrapping breaking the QR code
    chars = {}
    with open(font, "r") as file:
        for line in file:
            if "FONTBOUNDINGBOX" in line:
                size = int(line.split(" ")[1])
            if "ENCODING" in line and "_ENCODING" not in line:
                character = chr(int(line.split(" ")[1][:-1]))
                chars[character] = None
            if "SWIDTH" in line:
                swidth = (int(line.split(" ")[1]) / 1000) * size
            if "DWIDTH" in line:
                chars[character] = int(int(line.split(" ")[1]) + swidth)
    return chars

The second function does the actual wrapping of the text. It takes the text to format, the max width of the line, the max number of lines, as well as the dictionary that the previous function returns.

def wrap(text, max_width, max_lines, font):
    # Used to wrap the title and description to avoid breaking the QR code
    lines = []
    ellipsis = 3 * font["."]
    line = ""
    line_width = 0
    for word in text.split(" "):
        for character in word:
            line_width += font[character]
            if (
                len(lines) + 1 != max_lines
                or sum(font[i] for i in word) + line_width <= max_width
            ):
                if line_width > max_width:
                    print(str(line_width) + line)
                    line_width = sum(font[i] for i in word)
                    lines.append(line.strip())
                    line = word + " "
                    break
            else:
                for char_1 in word:
                    if line_width + ellipsis + font[char_1] > max_width:
                        line = line + "..."
                        print(str(line_width) + line)
                        lines.append(line)
                        return "\n".join(lines[:max_lines])
                    line = line + char_1
                    line_width += font[char_1]

        else:
            line = line + word + " "

    lines.append(line.strip())
    return "\n".join(lines[:max_lines])

This next bit of code gets first 300 items, saving only the OSHWA UIDs. The first 300 are also used to find the number of requests that will need to be made. This was done since if the items themselves were all stored at once, the MagTag would run out of memory. If we just got the number of total projects and chose a random number, that also wouldn't work as you can only get individual projects with an OSHWA UID and these UIDs are prefixed by the country they were registered in, thus making getting it with a simple number in-between 1 and the total number of registered projects impossible unless you only wanted to get projects registered in one country.

URL = "https://certificationapi.oshwa.org/api/projects?limit=300"

print(URL)

payload = {}
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}"}

oshwaID = []

print("Getting number of projects and first set of 300 projects")
with https.get(URL, headers=headers, data=payload) as response:
    R_JSON = response.json()
    total = int(R_JSON["total"])
    print(f"{total} Projects")
    for i in R_JSON["items"]:
        oshwaID.append(i["oshwaUid"])
    R_JSON.clear()
    R_JSON = None
    gc.collect()

This loop gets the rest of the OSHWA UIDs in a way that won't fill up all the memory of the MagTag.

print(len(oshwaID))
for i in range(int(total / 300)):
    print(f"Getting request {i+2}")
    url = (
        f"https://certificationapi.oshwa.org/api/projects?limit=300&offset={3*(i+1)}00"
    )
    with https.get(url, headers=headers, data=payload) as response:
        R_JSON = response.json()
        for item in R_JSON["items"]:
            oshwaID.append(item["oshwaUid"])
        R_JSON.clear()
        R_JSON = None
        gc.collect()
    print(f"{len(oshwaID)} IDs gathered")

Next, a random UID from the list of OSHWA UIDs is chosen, and a request is sent over the API for that specific project.

selected = random.choice(oshwaID)

url = f"https://certificationapi.oshwa.org/api/projects/{selected}"
response = https.get(url, headers=headers, data=payload)

selected = response.json()[0]

The combination of the OSHWA API not handling all non-alphanumeric characters correctly and the fonts used on the MagTag only having a subset of all characters that could have been used results in the need to remove all the special characters and replace all the punctuation marks that aren't being handled correctly.

# Filters out characters that the API or the MagTag itself isn't handling correctly
for char in range(1, 32):
    selected["projectDescription"].replace(chr(char), "")

selected["projectDescription"] = (
    selected["projectDescription"]
    .replace("&#x27;", "'")
    .replace("&amp;#x27;", "'")
    .replace("&#x2F;", "/")
    .replace("&quot;", '"')
    .replace("’", "'")
)

Now, the code creates the two text fields. The first one is for the title, and the second one is for the description.

# Add the two text fields
magtag.add_text(
    text_font="fonts/Arial-12.bdf",
    text_position=(5, -2),
    text_scale=1,
    line_spacing=0.6,
    text_anchor_point=(0, 0),
)

magtag.add_text(
    text_font="fonts/ArialMT-9.bdf",
    text_position=(5, 30),
    text_scale=1,
    line_spacing=0.6,
    text_anchor_point=(0, 0),
)

After that, the QR code is created and added to the display.

# Create the QR code
url = f"https://certification.oshwa.org/{selected['oshwaUid'].lower()}.html"
magtag.graphics.qrcode(url, qr_size=4, x=173, y=3)

Before adding the text, the code prepares to wrap it correctly by getting the width of each character for the two fonts that will be used.

arial_12 = font_width_to_dict("fonts/Arial-12.bdf")
arial_9 = font_width_to_dict("fonts/ArialMT-9.bdf")

Finally, the code sets the text. On some characters which aren't included in the fonts for memory reasons, this fails. If this occurs, the code runs again in 5 seconds.

try:
    magtag.set_text(wrap(selected["projectName"], 530, 2, arial_12), 0, False)
    magtag.set_text(wrap(selected["projectDescription"], 530, 10, arial_9), 1)
    magtag.exit_and_deep_sleep(3600)
except Exception: # pylint: disable=broad-except
    print("Could not set title or description: unsupported glyphs.")
    print("Trying again in 10 seconds.")
    magtag.exit_and_deep_sleep(10)

This guide was first published on Oct 13, 2021. It was last updated on 2021-10-13 12:02:36 -0400.

This page (Code the Project Display) was last updated on Dec 01, 2021.

Text editor powered by tinymce.