Showcasing the Contentful blog with E-Ink, CircuitPython and IoT

Shy Ruparel - Mar 18 '21 - - Dev Community

In 2007, the New York Times curated an art installation by Mark Hansen and Ben Rubin called “Moveable Type.” Mark and Ben had taken headlines and quotes from the newspaper’s more than 150-year-old archive and rendered them onto 560 small screens across the lobby of the building. After seeing the exhibit for the first time in 2014, I’ve made a point to visit every time I pass through Times Square.

Image of NYTimes Lobby
Photo by Ben Rubin

I had a chance to hear a collaborator, Marty Chafkin from Perfection Electricks, talk about the project at Brooklyn Research back in 2019. He mentioned the data collection process and how once they’d pulled the data out of the NYT archive and placed it into each device, there wasn’t an easy way to update each screen. I also noticed that it would be difficult to find the articles referenced on the screens.

At the time, I thought, “Wow, Contentful would be a great use case for this.” But not having access to any low-power screens at the time, I assumed this was a project I wouldn’t have a chance to work on anytime soon.

Late last year, Adafruit, an open-source hardware company based in New York City that creates products to help folks learn electronics, released the Adafruit MagTag. The MagTag is a 2.9-inch E-ink display combined with an ESP32-S2 wireless module. It’s a low-power IoT display that you can code using Python. It has four built-in colorful miniature LEDs alongside four buttons.

I ordered one as soon as I could, deciding that this would be the opportunity to revisit making my version of Moveable Type.

The MagTag buttons would make it possible to send the user a notification that included a link to whatever content the screen was displaying. I reached out to Aydrian Howard from Courier to help me with this final part. Aydrian and I did some live coding together back in November of 2020, and he shared how you could use the Courier API to design and send notifications. He recommended sharing the content via Discord and, luckily, he was free to build the project with me live on stream.

Let’s look at the code

If you’d rather see the code in full, head over to GitHub.

One of the reasons that I like the MagTag so much is that it supports CircuitPython. CircuitPython is a version of Python that adds hardware support to the standard Python implementation. Unlike a traditional Python project, you install dependencies by dragging .mpy files onto the board of the Microcontroller rather than using a package manager like pip. Since most IoT circuit boards don’t come with a ton of memory anyway — and I didn’t want to compile the Contentful Python SDK into an .mpy file — I chose to reach out to the Contentful API via HTTP.

Screenshot of MagTag mounted as a USB drive

Adafruit provides some precompiled .mpy files of the most useful libraries for CircuitPython. We’ll be using the Adafruit version of requests since it’s precompiled into an .mpy file. To get started, import all of the required dependencies, grab your environment variables and connect to the WiFi.

import time
import gc
import wifi
import random
import adafruit_requests
import ssl
import socketpool
import terminalio
from adafruit_magtag.magtag import MagTag

magtag = MagTag()

# Add a secrets.py to your filesystem that has a dictionary called secrets with "ssid" and
# "password" keys with your WiFi credentials. DO NOT share that file or commit it into Git or other
# source control.
# pylint: disable=no-name-in-module,wrong-import-order
try:
    from secrets import secrets
except ImportError:
    print("Credentials and tokens are kept in secrets.py, please add them there!")
    raise

print("Connecting to %s" % secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print("Connected to %s!" % secrets["ssid"])

pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
Enter fullscreen mode Exit fullscreen mode

Luckily my last post was all about using GraphQL via HTTP. I reused my previous Python code and made a few changes to account for the different queries and the API keys of the production Contentful blog. Using our GraphQL API rather than the traditional Contentful Rest API came with the benefit of reducing the number of calls I would need to make to the Contentful API, which reduced the payload of the response.

Fetching content

To get started, make a call to grab the last 100 blog posts from contentful.com/blog.

endpoint = "https://graphql.contentful.com/content/v1/spaces/%s/" % (
    secrets["space_id"]
)
headers = {"Authorization": "Bearer %s" % (secrets["CDA_token"])}

# Get Dev Blog Posts
query = """query {
  blogPostCollection {
    items {
      sys {
        id
      }
    }
  }
}
"""

print("Making blog post collection query.")
response = requests.post(endpoint, json={"query": query}, headers=headers)
Enter fullscreen mode Exit fullscreen mode

Next, pick one of those blog posts at random and grab all of the information we need to display the article on the E-ink screen. Since memory is a constraint, overwrite the previously used queries and requests rather than setting a new variable.

# Get Individual Post
query = """{
  blogPost(id: \"%s\") {
    title
    publishDate
    slug
    authorsCollection {
      items {
        name
      }
    }
    introduction
  }
}
""" % (
    random.choice(response.json()["data"]["blogPostCollection"]["items"])["sys"]["id"]
)

print("Making blog post query.")
response = requests.post(endpoint, json={"query": query}, headers=headers)
Enter fullscreen mode Exit fullscreen mode

With that request, we’re able to parse everything into a human-readable format and display it on the screen. Since the screen itself is only 2.9 inches, we’ll only use the first 170 characters of each blog post.

The MagTag comes with functions to create and set text boxes. When the box is ready, we’re able to set the text and let the screen know if it needs to refresh. Since we’re editing four text boxes for each item we’ll be displaying on screen, we can hold off triggering the refresh event until the final text item is set.

# Formatting for the author text
magtag.add_text(
    text_font="/fonts/Arial-Bold-12.pcf",
    text_position=(10, 38),
)

author_string = ""
for author in response.json()["data"]["blogPost"]["authorsCollection"]["items"]:
    if author_string == "":
        author_string = author["name"]
    else:
        author_string = author_string + " & " + author["name"]

magtag.set_text(
    val=author_string,
    index=1,
    auto_refresh=False,
)
Enter fullscreen mode Exit fullscreen mode

In this snippet, I created a text box for the author field. Next I generated a single string for our authors, looping over the author array as it’s possible a blog post has multiple authors. Lastly, I set the text box with the generated string. Since this is only the second of four text boxes we’ll need to change, we tell the display not to update. If you want to see the full display code, head over to GitHub.

Photo of the magtag integrating with the Contentful

Sending information to the Courier API

I wanted to send the user a link to the full article when they hit one of the MagTag buttons. Aydrian proved essential for achieving this. He let me know that we could take advantage of the Courier API’s ability to send notifications via a single HTTP request.

Just like before, we overwrite the previously used variables to make a request. Since we’ll be sharing information from the article, include that information in the request body. We’ll be able to use that information from Courier to design a notification later on. Lastly, make sure to set an event. Courier will use the information to figure out what should specifically happen to that specific information.

endpoint = "https://api.courier.com/send/"
headers["Authorization"] = "Bearer %s" % (secrets["courier_token"])
headers["Accept"] = "application/json"
headers["Content-Type"] = "application/json"

courier_JSON = {
    "event": "MAGTAG_NOTIFICATION",
    "recipient": "discord_channel",
    "data": {
        "title": response.json()["data"]["blogPost"]["title"],
        "introduction": response.json()["data"]["blogPost"]["introduction"][0:170]
        + "...",
        "url": blog_url,
        "author": author_string,
        "publish_date": strdate,
    },
}
Enter fullscreen mode Exit fullscreen mode

Next, we set a loop to start listening for button press events. If a button is pressed, we display some colors to give feedback that the board is doing something.Then make the request to Courier.

print("Starting the loop, go ahead and press some buttons. :)")
while True:
    if magtag.peripherals.button_a_pressed:
        if button_a_pressed == False:
            for i, b in enumerate(magtag.peripherals.buttons):
                magtag.peripherals.neopixel_disable = False
                magtag.peripherals.neopixels[i] = button_colors[i]
                time.sleep(0.25)
                magtag.peripherals.neopixels[i] = (0, 0, 0)
            print("Making request to Courier")
            response = requests.post(endpoint, json=courier_JSON, headers=headers)
            button_a_pressed = True
            print("Courier response: ")
            print(response.json())

    if magtag.peripherals.button_d_pressed:
        for i, b in enumerate(magtag.peripherals.buttons):
            magtag.peripherals.neopixel_disable = False
            magtag.peripherals.neopixels[i] = button_colors[i]
            time.sleep(0.25)
            magtag.peripherals.neopixels[i] = (0, 0, 0)
            # magtag.peripherals.play_tone(button_tones[i], 0.25)
    else:
        button_a_pressed = False
        magtag.peripherals.neopixel_disable = True
pass
Enter fullscreen mode Exit fullscreen mode

Hit the button to make the board light up and send your first request!

Configuring Courier

Courier supports multiple notification streams including Discord, SendGrid and Twilio. On the Courier website, connect to an integration platform (in my case, I chose Discord) by clicking and following the instructions. For this example, the specific integration you pick doesn’t matter, but you will need to provide API Keys from the service you want to use to Courier.

Screenshot of integrations tab

Next, create a new notification. Since we’ve made a request from the board first, we’ll be able to use the record of that request when setting up our notification.

Screenshot of the notification creation menu in Designer

Select the integration platform you previously used (in my case, Discord), and add it as a channel. From here you’ll be able to design what your notification will look like when it’s delivered. Since we’re passing along the information from the article in our JSON request, we’ll be able to use that data in our notification design.

Designing the notification

To wrap things up in the Courier dashboard, head to the Data tag and click on one of the records that matches the event you set in the POST request from the MagTag. Since the request won’t have a notification associated with it at this point, you can click the Map to Notifications and then select the notification you just designed.

Screenshot of the mapping process for the notification

Save the record as a test event and head back to the notification designer. Hit preview on the notification you’ve designed, and Courier will use the record to show what the notification will look like when you send it.

Screenshot of preview of the notification designer

Finally head back to your MagTag and hit the button to make a request. The MagTag will make a call to Courier, which will in turn process the request and create a notification to your connected integration platform.

Screenshot of magrag final result

That’s everything the MagTag needs to display content on the screen and send out a link to the full URL if a user hits a button.

Photo of the magtag displaying Contentful blog on the fridge

If you’ve got a MagTag, head over to GitHub for a link to the full source code with dependencies, an exported content model, instructions on how to set up Courier and a guide on how to install it on your device. Next up for me is to convince our CFO to let me order 559 more MagTags so I can cover the lobby of the Berlin Office with our Contentful version of moveable type. Until then I’ll be keeping my MagTag up on my fridge!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .