Once you've finished setting up your reverse TFT Feather ESP32-S2 with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.

To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.

# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries
# SPDX-License-Identifier: MIT

import time
import ssl
import os
import json
import socketpool
import wifi
import board
import digitalio
import terminalio
import adafruit_requests
from adafruit_io.adafruit_io import IO_HTTP, AdafruitIO_RequestError
import displayio
from adafruit_progressbar.horizontalprogressbar import (
    HorizontalProgressBar,
    HorizontalFillDirection,
)
from adafruit_display_shapes.rect import Rect
from adafruit_display_text import bitmap_label,  wrap_text_to_lines
import neopixel
from adafruit_led_animation.animation.rainbow import Rainbow
from adafruit_led_animation.animation.blink import Blink

aio_username = os.getenv('aio_username')
aio_key = os.getenv('aio_key')

wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))

# Make the display context
splash = displayio.Group()
board.DISPLAY.show(splash)

# set progress bar width and height relative to board's display
width = 183
height = 30

x = 50
#y = board.DISPLAY.height // 3
y = 100

# Create a new progress_bar object at (x, y)
progress_bar = HorizontalProgressBar(
    (x, y),
    (width, height),
    fill_color=0x000000,
    outline_color=0xFFFFFF,
    bar_color=0x13c100,
    direction=HorizontalFillDirection.LEFT_TO_RIGHT
)

# Append progress_bar to the splash group
splash.append(progress_bar)

rect = Rect(40, 0, 2, 135, fill=0xFFFFFF)
splash.append(rect)

img = displayio.OnDiskBitmap("octoprint_logo.bmp")
idle_icons = displayio.OnDiskBitmap("idle_icons.bmp")
printing_icons = displayio.OnDiskBitmap("printing_icons.bmp")
finished_icon = displayio.OnDiskBitmap("finished_icon.bmp")

tile_grid = displayio.TileGrid(bitmap=img, pixel_shader=img.pixel_shader, x = 185, y=5)
splash.append(tile_grid)

icon_grid = displayio.TileGrid(bitmap=idle_icons, pixel_shader=idle_icons.pixel_shader, x = 0, y=0)
splash.append(icon_grid)

text = bitmap_label.Label(terminalio.FONT, text="Connecting", scale=2, x=55, y=45)
splash.append(text)

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

button0 = digitalio.DigitalInOut(board.D0)
button0.direction = digitalio.Direction.INPUT
button0.pull = digitalio.Pull.UP

button1 = digitalio.DigitalInOut(board.D1)
button1.direction = digitalio.Direction.INPUT
button1.pull = digitalio.Pull.DOWN

button2 = digitalio.DigitalInOut(board.D2)
button2.direction = digitalio.Direction.INPUT
button2.pull = digitalio.Pull.DOWN
# Our array of key objects
button0_state = False
button1_state = False
button2_state = False

pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness = 0.6)

# Create a socket pool
pool = socketpool.SocketPool(wifi.radio)

requests = adafruit_requests.Session(pool, ssl.create_default_context())
# Initialize an Adafruit IO HTTP API object
io = IO_HTTP(aio_username, aio_key, requests)

try:
    # get feed
    # printing monitors the printer progress feed
    printing_status = io.get_feed("printing")
except AdafruitIO_RequestError:
    # if no feed exists, create one
    printing_status = io.create_new_feed("printing")
try:
    print_done = io.get_feed("printdone")
except AdafruitIO_RequestError:
    print_done = io.create_new_feed("printdone")
try:
    printer_state = io.get_feed("printerstatechanged")
except AdafruitIO_RequestError:
    printer_state = io.create_new_feed("printerstatechanged")
try:
    shutdown = io.get_feed("shutdown")
except AdafruitIO_RequestError:
    shutdown = io.create_new_feed("shutdown")
try:
    heat_up = io.get_feed("heatup")
except AdafruitIO_RequestError:
    heat_up = io.create_new_feed("heatup")
try:
    cooldown = io.get_feed("cooldown")
except AdafruitIO_RequestError:
    cooldown = io.create_new_feed("cooldown")
try:
    resume = io.get_feed("printresumed")
except AdafruitIO_RequestError:
    resume = io.create_new_feed("printresumed")
try:
    pause = io.get_feed("printpaused")
except AdafruitIO_RequestError:
    pause = io.create_new_feed("printpaused")
try:
    cancelled = io.get_feed("printcancelled")
except AdafruitIO_RequestError:
    cancelled = io.create_new_feed("printcancelled")

read_feeds = [printing_status, printer_state, print_done]
send_while_idle_feeds = [cooldown, heat_up, shutdown]
send_while_printing_feeds = [pause, resume, cancelled]
new_feed_msg = ["None", "None", "None"]
last_feed_msg = ["none","none","none"]
msg_json = [{"path": "none"}, {"state_id": "NONE"}, {"path": "none"}]
print_progress = 0
current_state = 0
last_state = None
state_value = 0
current_file = None
finished_file = None
red = (255, 0, 0)
green = (0, 255, 0)
blue = (0, 0, 255)
cyan = (0, 255, 255)
purple = (255, 0, 255)
yellow = (255, 255, 0)

printer_state_options = ["OPEN_SERIAL", "DETECT_SERIAL",
"DETECT_BAUDRATE", "CONNECTING", "OPERATIONAL", "PRINTING", "PAUSING", "PAUSED",
"CLOSED", "ERROR", "FINISHING", "CLOSED_WITH_ERROR", "TRANSFERING_FILE", "OFFLINE", "STARTING",
"CANCELLING", "UNKNOWN", "NONE"]
colors = [green, yellow, cyan, yellow,
          green, purple, yellow, yellow, red,
          red, blue, red, yellow, red,
          purple, red, red, red]

clock = 5

rainbow = Rainbow(pixel, speed=0.1, period=2)
blink = Blink(pixel, speed=0.5, color=green)

while True:
    if button0.value and button0_state:
        led.value = False
        button0_state = False
    if not button1.value and button1_state:
        led.value = False
        button1_state = False
    if not button2.value and button2_state:
        led.value = False
        button2_state = False

    if current_state in ("PRINTING", "PAUSED", "PAUSING"):
        rainbow.animate()
        if not button0.value and not button0_state:
            led.value = True
            io.send_data(send_while_printing_feeds[0]["key"], "ping")
            button0_state = True
        if button1.value and not button1_state:
            led.value = True
            io.send_data(send_while_printing_feeds[1]["key"], "ping")
            button1_state = True
        if button2.value and not button2_state:
            led.value = True
            io.send_data(send_while_printing_feeds[2]["key"], "ping")
            button2_state = True
    else:
        blink.color=colors[state_value]
        blink.animate()
        if not button0.value and not button0_state:
            if finished_file == current_file:
                current_file = "None"
                progress_bar.value = 100
                progress_bar.bar_color = colors[state_value]
                text.text = "\n".join(wrap_text_to_lines("Status: %s" % current_state, 11))
                icon_grid.bitmap = idle_icons
                icon_grid.pixel_shader = idle_icons.pixel_shader
                button0_state = True
            else:
                led.value = True
                io.send_data(send_while_idle_feeds[0]["key"], "ping")
                button0_state = True
        if button1.value and not button1_state:
            led.value = True
            io.send_data(send_while_idle_feeds[1]["key"], "ping")
            button1_state = True
        if button2.value and not button2_state:
            led.value = True
            io.send_data(send_while_idle_feeds[2]["key"], "ping")
            button2_state = True
    if (time.monotonic() - clock) > 15:
        #  get data
        for feed in range(3):
            try:
                data = io.receive_data(read_feeds[feed]["key"])
            except AdafruitIO_RequestError:
                print("Check that OctoPrint is sending data! Check your IO dashboard.")
            #  if a new value is detected
            if data["value"] != last_feed_msg[feed]:
                #  assign value to new_msg
                new_feed_msg[feed] = data["value"]
                msg_json[feed] = json.loads(data["value"])
                #  set servo angle
                print(read_feeds[feed]["key"])
                print()
                print(new_feed_msg[feed])
                print()
                #time.sleep(1)
                print_progress = int(msg_json[0]['progress'])
                current_file = str(msg_json[0]['path'])
                current_state = str(msg_json[1]['state_id'])
                finished_file = str(msg_json[2]['path'])
                state_value = printer_state_options.index(current_state)
                #  log msg
                last_feed_msg[feed] = new_feed_msg[feed]
            #time.sleep(1)
        if current_state == "PRINTING":
            #print_progress = int(msg_json[0]['progress'])
            progress_bar.value = print_progress
            #octoprint green
            progress_bar.bar_color = 0x13c100
            text.text = "\n".join(wrap_text_to_lines("%d%% Printed" % print_progress, 7))
            icon_grid.bitmap = printing_icons
            icon_grid.pixel_shader = printing_icons.pixel_shader
        elif current_state in ("PAUSED", "PAUSING"):
            progress_bar.value = print_progress
            progress_bar.bar_color = colors[state_value]
            text.text = "\n".join(wrap_text_to_lines("Status: %s" % current_state, 11))
            icon_grid.bitmap = printing_icons
            icon_grid.pixel_shader = printing_icons.pixel_shader
        # when a print is finished:
        elif finished_file == current_file and print_progress == 100:
            progress_bar.value = 100
            progress_bar.bar_color = purple
            text.text = "\n".join(wrap_text_to_lines("Print Finished!", 11))
            icon_grid.bitmap = finished_icon
            icon_grid.pixel_shader = finished_icon.pixel_shader
        # when printer is idle, display status
        else:
            progress_bar.value = 100
            progress_bar.bar_color = colors[state_value]
            text.text = "\n".join(wrap_text_to_lines("Status: %s" % current_state, 11))
            icon_grid.bitmap = idle_icons
            icon_grid.pixel_shader = idle_icons.pixel_shader
        #  reset clock
        clock = time.monotonic()

Upload the Code and Libraries to the Reverse TFT Feather ESP32-S2

After downloading the Project Bundle, plug your reverse TFT Feather ESP32-S2 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the reverse TFT Feather ESP32-S2's CIRCUITPY drive. 

  • lib folder
  • code.py
  • octoprint_logo.bmp
  • finished_icon.bmp
  • idle_icons.bmp
  • printing_icons.bmp

Your reverse TFT Feather ESP32-S2 CIRCUITPY drive should look like this after copying the lib folder, octoprint_logo.bmp, finished_icon.bmp, idle_icons.bmp, printing_icons.bmp image files and the code.py file.

CIRCUITPY

Add Your settings.toml File

As of CircuitPython 8.0.0, there is support for Environment Variables. These Environmental Variables are stored in a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file. Add your settings.toml file as described in the Create Your settings.toml File page earlier in this guide. You'll need to include your aio_username, aio_key, CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD.

CIRCUITPY_WIFI_SSID = "your-ssid-here"
CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here"

aio_username = "your-aio-username-here"
aio_key = "your-aio-key-here"

How the CircuitPython Code Works

The code begins by creating a HorizontalProgressBar() object. This progress bar will display the progress of an active print and will act as an additional visual cue for the current status of the printer when it is idle.

# Make the display context
splash = displayio.Group()
board.DISPLAY.show(splash)

width = 165
height = 30

x = 70
y = 100

# Create a new progress_bar object at (x, y)
progress_bar = HorizontalProgressBar(
    (x, y),
    (width, height),
    fill_color=0x000000,
    outline_color=0xFFFFFF,
    bar_color=0x13c100,
    direction=HorizontalFillDirection.LEFT_TO_RIGHT
)

# Append progress_bar to the splash group
splash.append(progress_bar)

Display Attributes

Next, a rectangle is created to act as a display divider. It appears as a thick white line on the display to separate the button icons from the progress bar and status message. Then, the OctoPrint logo and idle icons are imported as TileGrid objects. 

rect = Rect(60, 0, 2, 135, fill=0xFFFFFF)
splash.append(rect)

img = displayio.OnDiskBitmap("octoprint_logo.bmp")
idle_icons = displayio.OnDiskBitmap("idle_icons.bmp")
printing_icons = displayio.OnDiskBitmap("printing_icons.bmp")
finished_icon = displayio.OnDiskBitmap("finished_icon.bmp")

tile_grid = displayio.TileGrid(bitmap=img, pixel_shader=img.pixel_shader, x = 185, y=5)
splash.append(tile_grid)

text = bitmap_label.Label(terminalio.FONT, text="Connecting", scale=2, x=75, y=45)
splash.append(text)

Buttons and NeoPixel

The front buttons are setup as inputs and the onboard NeoPixel is setup as a NeoPixel object.

button0 = digitalio.DigitalInOut(board.D0)
button0.direction = digitalio.Direction.INPUT
button0.pull = digitalio.Pull.UP

button1 = digitalio.DigitalInOut(board.D1)
button1.direction = digitalio.Direction.INPUT
button1.pull = digitalio.Pull.DOWN

button2 = digitalio.DigitalInOut(board.D2)
button2.direction = digitalio.Direction.INPUT
button2.pull = digitalio.Pull.DOWN

button0_state = False
button1_state = False
button2_state = False

pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness = 0.6)

Feeds

After connecting to Adafruit IO, the code tries to get the feeds corresponding to the MQTT topics defined in the OctoPrint MQTT plugins. If the feeds do not exist, they are created.

pool = socketpool.SocketPool(wifi.radio)

requests = adafruit_requests.Session(pool, ssl.create_default_context())
io = IO_HTTP(aio_username, aio_key, requests)

try:
    # get feed
    printing_status = io.get_feed("printing")
    print_done = io.get_feed("printdone")
    printer_state = io.get_feed("printerstatechanged")
    shutdown = io.get_feed("shutdown")
    heat_up = io.get_feed("heatup")
    cooldown = io.get_feed("cooldown")
    resume = io.get_feed("printresumed")
    pause = io.get_feed("printpaused")
    cancelled = io.get_feed("printcancelled")

except AdafruitIO_RequestError:
    # if no feed exists, create one
    printing_status = io.create_new_feed("printing")
    print_done = io.create_new_feed("printdone")
    printer_state = io.create_new_feed("printerstatechanged")
    shutdown = io.create_new_feed("shutdown")
    heat_up = io.create_new_feed("heatup")
    cooldown = io.create_new_feed("cooldown")
    resume = io.create_new_feed("printresumed")
    pause = io.create_new_feed("printpaused")
    cancelled = io.create_new_feed("printcancelled")

The Loop

In the loop, the code functionality is determined by the current state of the printer. This status is determined by the incoming messages on the PrinterStateChanged topic. If the printer is PRINTING, PAUSED or PAUSING, then the onboard NeoPixel displays the rainbow swirl animation and the buttons have the ability to send pause, resume and cancel messages to OctoPrint.

if current_state in ("PRINTING", "PAUSED", "PAUSING"):
        rainbow.animate()
        if not button0.value and not button0_state:
            led.value = True
            io.send_data(send_while_printing_feeds[0]["key"], "ping")
            button0_state = True
        if button1.value and not button1_state:
            led.value = True
            io.send_data(send_while_printing_feeds[1]["key"], "ping")
            button1_state = True
        if button2.value and not button2_state:
            led.value = True
            io.send_data(send_while_printing_feeds[2]["key"], "ping")
            button2_state = True

For any other state, the onboard NeoPixel will blink a color that matches the status message. For example, red for disconnected and green for operational.

The buttons can send heat up, cooldown and reboot messages to OctoPrint unless a print has just finished. In that case, the D0 button confirms that the print has completed and that updates the display and button abilities. 

The button icons are updated by changing the icon_grid bitmap and pixel_shader attributes to the different icon bitmap files.

else:
        blink.color=colors[state_value]
        blink.animate()
        if not button0.value and not button0_state:
            if finished_file == current_file:
                current_file = "None"
                progress_bar.value = 100
                progress_bar.bar_color = colors[state_value]
                text.text = "\n".join(wrap_text_to_lines("Status: %s" % current_state, 11))
                icon_grid.bitmap = idle_icons
                icon_grid.pixel_shader = idle_icons.pixel_shader
                button0_state = True
            else:
                led.value = True
                io.send_data(send_while_idle_feeds[0]["key"], "ping")
                button0_state = True
        if button1.value and not button1_state:
            led.value = True
            io.send_data(send_while_idle_feeds[1]["key"], "ping")
            button1_state = True
        if button2.value and not button2_state:
            led.value = True
            io.send_data(send_while_idle_feeds[2]["key"], "ping")
            button2_state = True

Ping IO

Every fifteen seconds, Adafruit IO is checked for a new message from OctoPrint. If a new message has been logged in a feed, then that message is converted to a JSON entry so that it can be parsed.

if (time.monotonic() - clock) > 15:
        #  get data
        for feed in range(3):
            try:
                data = io.receive_data(read_feeds[feed]["key"])
            except AdafruitIO_RequestError:
                print("Check that OctoPrint is sending data! Check your IO dashboard.")
            #  if a new value is detected
            if data["value"] != last_feed_msg[feed]:
                #  assign value to new_msg
                new_feed_msg[feed] = data["value"]
                msg_json[feed] = json.loads(data["value"])
                print(read_feeds[feed]["key"])
                print()
                print(new_feed_msg[feed])
                print()
                print_progress = int(msg_json[0]['progress'])
                current_file = str(msg_json[0]['path'])
                current_state = str(msg_json[1]['state_id'])
                finished_file = str(msg_json[2]['path'])
                state_value = printer_state_options.index(current_state)
                #  log msg
                last_feed_msg[feed] = new_feed_msg[feed]

If the printer is printing, then the progress bar and status text is updated with the print progress percentage.

if current_state == "PRINTING":
            progress_bar.value = print_progress
            #octoprint green
            progress_bar.bar_color = 0x13c100
            text.text = "\n".join(wrap_text_to_lines("%d%% Printed" % print_progress, 7))
            icon_grid.bitmap = printing_icons
            icon_grid.pixel_shader = printing_icons.pixel_shader

If the printer is paused, then the progress bar's color is updated to yellow.

elif current_state in ("PAUSED", "PAUSING"):
            progress_bar.value = print_progress
            progress_bar.bar_color = colors[state_value]
            text.text = "\n".join(wrap_text_to_lines("Status: %s" % current_state, 11))
            icon_grid.bitmap = printing_icons
            icon_grid.pixel_shader = printing_icons.pixel_shader

The code determines if a print has finished by comparing the file name on the printing topic and the PrintDone topic. If those file names match and the print progress is 100%, then the display updates to show that a print is finished.

# when a print is finished:
        elif finished_file == current_file and print_progress == 100:
            progress_bar.value = 100
            progress_bar.bar_color = purple
            text.text = "\n".join(wrap_text_to_lines("Print Finished!", 11))
            icon_grid.bitmap = finished_icon
            icon_grid.pixel_shader = finished_icon.pixel_shader

If the printer is idle, then the progress bar displays a color that matches the blinking NeoPixel and the button icon bitmap changes to match their functionality.

# when printer is idle, display status
        else:
            progress_bar.value = 100
            progress_bar.bar_color = colors[state_value]
            text.text = "\n".join(wrap_text_to_lines("Status: %s" % current_state, 11))
            icon_grid.bitmap = idle_icons
            icon_grid.pixel_shader = idle_icons.pixel_shader

This guide was first published on Feb 08, 2023. It was last updated on Feb 13, 2023.

This page (Coding the OctoPrint Status Viewer) was last updated on Mar 22, 2023.

Text editor powered by tinymce.