Maker.io main logo

Simplifying Qualia CircuitPython Projects

131

2025-02-28 | By Adafruit Industries

License: See Original Project Board Specific Displays LCD / TFT Touch ESP32

Courtesy of Adafruit

Guide by M. LeBlanc-Williams

Overview

dotclock_1

The Qualia ESP32-S3 board is capable of driving RGB 666 dot clock displays, but the code ‎to initialize them can be a bit long and most people don't want the first 50 to 100 lines of ‎their code dedicated to just initializing the display. You can find more information about ‎their usage in the Adafruit Qualia ESP32-S3 for RGB-666 Displays guide.‎

The Qualia helper library removes a lot of the overhead work of getting the display up and ‎running allowing you to concentrate on your project code instead of trying out a myriad of ‎drivers, initialization codes, and timings just to get the display to show something.‎

It works by initializing the display as well as the appropriate touch driver if there is one for ‎the display. The Qualia helper library is also built on top of the PortalBase library, which ‎gives it many of the functions available to boards such as the PyPortal and MatrixPortal.‎

This guide will go overusing the library as well as covering the examples included with the ‎library.‎

Parts

Also, compatible displays, under Featured Products or as listed under Qualia.‎

Create Your settings.toml File

CircuitPython works with WiFi-capable boards to enable you to make projects that have ‎network connectivity. This means working with various passwords and API keys. As ‎of CircuitPython 8, there is support for a settings.toml file. This is a file that is stored on ‎your CIRCUITPY drive, which contains all of your secret network information, such as your ‎SSID, SSID password and any API keys for IoT services. It is designed to separate your ‎sensitive information from your code.py file so you are able to share your code without ‎sharing your credentials.‎

CircuitPython previously used a secrets.py file for this purpose. The settings.toml file is ‎quite similar.‎

Your settings.toml file should be stored in the main directory of your CIRCUITPY drive. It ‎should not be in a folder.‎

CircuitPython settings.toml File

This section will provide a couple of examples of what your settings.toml file should look ‎like, specifically for CircuitPython WiFi projects in general.‎

The most minimal settings.toml file must contain your WiFi SSID and password, as that is ‎the minimum required to connect to WiFi. Copy this example, paste it into ‎your settings.toml, and update:‎

  • your_wifi_ssid

  • your_wifi_password

Download File

Copy Code
CIRCUITPY_WIFI_SSID = "your_wifi_ssid"
CIRCUITPY_WIFI_PASSWORD = "your_wifi_password"

Many CircuitPython network-connected projects on the Adafruit Learn System involve using ‎Adafruit IO. For these projects, you must also include your Adafruit IO username and key. ‎Copy the following example, paste it into your settings.toml file, and update:‎

  • your_wifi_ssid

  • your_wifi_password

  • your_aio_username

  • your_aio_key

Download File

Copy Code
CIRCUITPY_WIFI_SSID = "your_wifi_ssid"
CIRCUITPY_WIFI_PASSWORD = "your_wifi_password"
ADAFRUIT_AIO_USERNAME = "your_aio_username"
ADAFRUIT_AIO_KEY = "your_aio_key"

Some projects use different variable names for the entries in the settings.toml file. For ‎example, a project might use ADAFRUIT_AIO_ID in the place ‎of ADAFRUIT_AIO_USERNAME. If you run into connectivity issues, one of the first things ‎to check is that the names in the settings.toml file match the names in the code.‎

Not every project uses the same variable name for each entry in the settings.toml file! ‎Always verify it matches the code.‎

settings.toml File Tips

Here is an example settings.toml file.‎

Download File

Copy Code
# Comments are supported
CIRCUITPY_WIFI_SSID = "guest wifi"
CIRCUITPY_WIFI_PASSWORD = "guessable"
CIRCUITPY_WEB_API_PORT = 80
CIRCUITPY_WEB_API_PASSWORD = "passw0rd"
test_variable = "this is a test"
thumbs_up = "\U0001f44d"

In a settings.toml file, it's important to keep these factors in mind:‎

  • Strings are wrapped in double quotes; ex: "your-string-here"‎

  • Integers are not quoted and may be written in decimal with optional sign (+1, -‎‎1, 1000) or hexadecimal (0xabcd).‎

o Floats, octal (0o567) and binary (0b11011) are not supported.‎
  • Use \u escapes for weird characters, \x and \ooo escapes are not available ‎in .toml files

o Example: \U0001f44d for 👍 (thumbs up emoji) and \u20ac for € (EUR sign)‎
  • Unicode emoji, and non-ASCII characters, stand for themselves as long as you're ‎careful to save in "UTF-8 without BOM" format‎

When your settings.toml file is ready, you can save it in your text editor with ‎the .toml extension.‎

settings_2

Accessing Your settings.toml Information in code.py

In your code.py file, you'll need to import the os library to access the settings.toml file. ‎Your settings are accessed with the os.getenv() function. You'll pass your settings entry to ‎the function to import it into the code.py file.‎

Download File

Copy Code
import os

print(os.getenv("test_variable"))

code_3

In the upcoming CircuitPython WiFi examples, you'll see how the settings.toml file is used ‎for connecting to your SSID and accessing your API keys.‎

Usage

Choosing your layers is an important part of creating a project with regards to the Portal-‎style libraries since it's easy to accidentally choose layers that end up duplicating some of ‎the functions. This guide is intended to help clarify your understanding of the layout so you ‎can make the best choices for your needs.‎

The PyPortal library, which is what inspired this library was written as a single layer which ‎had the advantage of making it really simple to use for a certain type of project and it ‎worked well for the PyPortal because the hardware setup varies very little between the ‎different models. As more boards were written in this style of library, a base library called ‎PortalBase was created to make it easier to maintain multiple libraries. The libraries were ‎originally broken up into layers to allow for loading only the parts that were needed for a ‎project with the advantage of saving memory when there wasn't much to spare.‎

For the Qualia ESP32-S3, there is plenty of PSRAM available, so you could just load the ‎topmost layer. However, with continuing the tradition of layers and the fact that some of the ‎huge displays can take up a good chunk of the RAM, not loading more than needed is still a ‎good approach.‎

Mixing and Matching Layers

mixing_4

Which of the layers you choose to use for your project depends on the amount of ‎customization and memory management you would like in your project. The higher level up ‎you go in the library layer hierarchy, the more automatic functions you will have available to ‎you, but it also takes away your ability to customize things and uses more memory.‎

In general, you will likely want at least one of the Graphics layers and optionally one of the ‎Network layers. If you plan on using the peripherals specific to the board such as the ‎buttons, you will want the peripherals layer as well.‎

Graphics Layers

graphics_5

For the Qualia library having multiple possible displays, a slightly different approach was ‎taken with writing Graphics layers. There is a folder of displays that contain both the ‎DotClockDisplay base class and the display-specific classes.‎

There is also a Displays class, which can be found alongside the Graphics class that was ‎written for the purpose of finding all of the display-specific classes and loading the ‎filename as an attribute in all uppercase. This class has only static functions because it is ‎meant to be used without instantiating it first.‎

This makes it easy to add new displays to the library since everything is just kept in one ‎place.‎

Network Layers

network_6

On the network functionality side of things, you will want to include the Network layer, ‎which includes some convenient functions such as fetch for data and wget for ‎downloading files. With plenty of RAM, the Qualia should be able to handle most ‎downloads.‎

Peripherals Layer

peripherals_7

To use the peripheral functionality, if you just wanted to initialize the buttons or control the ‎display's backlight, then you would want to use the Peripherals layer. Compared to some of ‎the other Portal-style boards, the Qualia ESP32-S3 has very few peripherals.‎

Top Layer

top_8

If you wanted everything along with some great functionality that ties all the legs of the ‎hierarchy together then you would want the very top layer, which is the Qualia layer. This ‎layer is the all-inclusive layer. To access the lower layers, you can use the following ‎attributes:‎

  • peripherals - The Peripherals Layer

  • graphics - The Graphics Layer

  • network - The Network Layer

  • display - The FrameBufferDisplay layer

  • graphics.dotclockdisplay - The DotClockDisplay layer

Remember that if you go with this layer, you should not import any of the lower layers with ‎the exception of the Displays layer.‎

Importing your layers

Displays Layer

This layer is special since it will automatically enumerate the displays upon import, and you ‎will need it in order to instantiate the Top or Graphics layers

Download File

Copy Code
from adafruit_qualia.graphics import Displays

To refer to a specific display, you would refer to it starting with Displays. followed by the ‎filename in all capital letters without the .py at the end. For example:‎

  • Displays.ROUND21 - Round 2.1" Display

  • Displays.SQUARE34 - Square 3.4" Display

  • Displays.BAR320X820 - 320x820 Bar Display‎

These are only a few of the supported displays. Check the displays folder for a complete list ‎of available displays. It doesn't matter whether your display has touch or not as it will ‎attempt to initialize the appropriate touch driver, but if the chip isn't found, it will still load.‎

Alternatively, you could just use a string with the filename in all lowercase without ‎the .py extension. For example, "round21".‎

Top Layer

To import the top-level layer only, you would simply just import it like this:‎

Download File

Copy Code
from adafruit_qualia import Qualia

If you would like access to the lower layers, you can directly access them as attributes. For ‎instance, if you instantiated the top layer as qualia, then you could access the layers.‎

Download File

Copy Code
qualia = Qualia(DISPLAY)
network = qualia.network
graphics = qualia.graphics
peripherals = qualia.peripherals

Replace with DISPLAY with the display you have connected such as Displays.ROUND21. ‎See the Displays Layer for more information.‎

If you would prefer, you don't even need to assign them to variable and can just directly ‎access the attributes when needed.‎

Sub-Layers

To only import sub-layers such as the Graphics and Network layers, you would import it like ‎this:‎

Download File

Copy Code
from adafruit_qualia.graphics import Graphics
from adafruit_qualia.network import Network

After they're imported, you would just instantiate each of the classes separately.‎

Download File

Copy Code
graphics = Graphics(DISPLAY)
network = Network()

Replace with DISPLAY with the display you have connected such as Displays.ROUND21. ‎See the Displays Layer for more information.‎

Code Examples

Here is the code from one of the examples that are included with the library. To run the ‎examples, simply rename them as code.py and place them in the root of ‎your CIRCUITPY drive.‎

Simple Test

This example was written to use the square 3.4" display, but should be able to work with any ‎of the displays. It uses the top-level Qualia layer and makes use of the graphics and ‎network. It connects to your WiFi, downloads some test data, and displays the data in the ‎REPL.‎

Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
#
# NOTE: Make sure you've set up your settings.toml file before running this example
# https://learn.adafruit.com/getting-started-with-web-workflow-using-the-code-editor/

from adafruit_qualia import Qualia
from adafruit_qualia.graphics import Displays

# Set a data source URL
TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html"

# Create the Qualia object
qualia = Qualia(Displays.SQUARE34, url=TEXT_URL)

# Go get that data
print("Fetching text from", TEXT_URL)
data = qualia.fetch()

# Print out what we got
print("-" * 40)
print(data)
print("-" * 40)

View on GitHub

fetch_9

Quotes Example

The quotes example is more like how the PyPortal works in that a data source is defined, ‎two text fields are created, and the quote and author data are displayed. This example was ‎also written for the square 3.4" display but could be modified to run on other displays by ‎adjusting the text field settings such as text_wrap.‎

Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2019 Limor Fried for Adafruit Industries
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import time
from adafruit_qualia import Qualia
from adafruit_qualia.graphics import Displays

# Set up where we'll be fetching data from
DATA_SOURCE = "https://www.adafruit.com/api/quotes.php"
QUOTE_LOCATION = [0, "text"]
AUTHOR_LOCATION = [0, "author"]

qualia = Qualia(
    Displays.SQUARE34,
    url=DATA_SOURCE,
    json_path=(QUOTE_LOCATION, AUTHOR_LOCATION),
    default_bg=0x333333,
)

qualia.add_text(
    text_position=(20, 120),  # quote location
    text_color=0xFFFFFF,  # quote text color
    text_wrap=25,  # characters to wrap for quote
    text_maxlen=180,  # max text size for quote
    text_scale=3,  # quote text size
)

qualia.add_text(
    text_position=(5, 240),  # author location
    text_color=0x8080FF,  # author text color
    text_wrap=0,  # no wrap for author
    text_maxlen=180,  # max text size for quote & author
    text_scale=3,  # author text size
)

while True:
    try:
        value = qualia.fetch()
        print("Response is", value)
    except (ValueError, RuntimeError, ConnectionError, OSError) as e:
        print("Some error occured, retrying! -", e)
    time.sleep(60)

View on GitHub

mistakes_10

QR Code Example

The QR Code Generation example generates a QR code and displays it in the center of the ‎display. This example was written for the round 2.1" display but could easily be adapted for ‎the other displays.‎

Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2021 Jose David M.
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT

# NOTE: Make sure you've set up your settings.toml file before running this example
# https://learn.adafruit.com/getting-started-with-web-workflow-using-the-code-editor/
"""
This example shows a web address QR on the display
"""

import time
from adafruit_qualia.graphics import Graphics, Displays
from adafruit_qualia.peripherals import Peripherals

# Background Information
base = Graphics(Displays.ROUND21, default_bg=0x990099)

# Set up Peripherals
peripherals = Peripherals(i2c_bus=base.i2c_bus)

# Set display to show
display = base.display

# WebPage to show in the QR
webpage = "http://www.adafruit.com"

# QR size Information
qr_size = 9  # Pixels
scale = 10

# Create a barcode
base.qrcode(
    webpage,
    qr_size=scale,
    x=(display.width // 2) - ((qr_size + 5) * scale),
    y=(display.height // 2) - ((qr_size + 4) * scale),
)

while True:
    if peripherals.button_up:
        peripherals.backlight = True
    if peripherals.button_down:
        peripherals.backlight = False
    time.sleep(0.1)

View on GitHub

paint_11

Paint Example

This last example is the most complex one and will run on any of the displays with a ‎touchscreen. This was adapted from an example included in the FocalTouch library and ‎ends up being around 30 lines less, but supporting many more displays. This example only ‎uses the Graphics layer and shows how to make use of the touch screen.‎

‎Download Project Bundle

Copy Code
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
Simple painting demo that works with on any touch display
"""
import displayio
from adafruit_qualia.graphics import Graphics, Displays

# For other displays:
# 2.1" Round = Displays.ROUND21
# 3.4" Square = Displays.SQUARE34
# 320 x 820 Bar - Displays.BAR320X820
# 320 x 960 Bar - Displays.BAR320X960
graphics = Graphics(Displays.SQUARE40, default_bg=None, auto_refresh=False)

if graphics.touch is None:
    raise RuntimeError("This example requires a touch screen.")

# Main Program
pixel_size = 6
palette_width = 160
palette_height = graphics.display.height // 8

bitmap = displayio.Bitmap(graphics.display.width, graphics.display.height, 65535)

# Create a TileGrid to hold the bitmap
tile_grid = displayio.TileGrid(
    bitmap,
    pixel_shader=displayio.ColorConverter(input_colorspace=displayio.Colorspace.RGB565),
)

# Add the TileGrid to the Group
graphics.splash.append(tile_grid)

# Add the Group to the Display
graphics.display.root_group = graphics.splash

current_color = displayio.ColorConverter().convert(0xFFFFFF)

for i in range(palette_width):
    color_index = i * 255 // palette_width
    rgb565 = displayio.ColorConverter().convert(
        color_index | color_index << 8 | color_index << 16
    )
    r_mask = 0xF800
    g_mask = 0x07E0
    b_mask = 0x001F
    for j in range(palette_height):
        bitmap[i, j + palette_height] = rgb565 & b_mask
        bitmap[i, j + palette_height * 2] = rgb565 & (b_mask | g_mask)
        bitmap[i, j + palette_height * 3] = rgb565 & g_mask
        bitmap[i, j + palette_height * 4] = rgb565 & (r_mask | g_mask)
        bitmap[i, j + palette_height * 5] = rgb565 & r_mask
        bitmap[i, j + palette_height * 6] = rgb565 & (r_mask | b_mask)
        bitmap[i, j + palette_height * 7] = rgb565

graphics.display.auto_refresh = True

while True:
    if graphics.touch.touched:
        try:
            for touch in graphics.touch.touches:
                x = touch["x"]
                y = touch["y"]
                if (
                    not 0 <= x < graphics.display.width
                    or not 0 <= y < graphics.display.height
                ):
                    continue  # Skip out of bounds touches
                if x < palette_width:
                    current_color = bitmap[x, y]
                else:
                    for i in range(pixel_size):
                        for j in range(pixel_size):
                            x_pixel = x - (pixel_size // 2) + i
                            y_pixel = y - (pixel_size // 2) + j

                            if (
                                0 <= x_pixel < graphics.display.width
                                and 0 <= y_pixel < graphics.display.height
                            ):
                                bitmap[x_pixel, y_pixel] = current_color
        except RuntimeError:
            pass

View on GitHub

display_12

Mfr Part # 5800
EVAL BOARD FOR ESP32-S3
Adafruit Industries LLC
More Info
View More Details
Mfr Part # 4474
CABLE A PLUG TO C PLUG 3'
Adafruit Industries LLC
More Info
View More Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.