Logging #

Last updated March 17, 2026
circuitpython

The logger framework lets you set log levels and send your logs to multiple outputs. There is a FileHandler class that will log to a file, however it does not flush automatically. I’m not sure when it ever flushes, actually, which is a problem for long running processes that only log occasionally. so I created my own JoshFileHandler that flushes after every log.

visual logging

terminalio – Displays text in a TileGrid — Adafruit CircuitPython 8.2.0-rc.0 documentation

Using plain terminalio with displayio - ValueError: Tile index out of bounds · Issue #7885 · adafruit/circuitpython · GitHub

Dates and Times #

Last updated March 17, 2026
circuitpython

Get the time

import time
now = time.localtime()
month = now[1]
day = now[2]
(hour, minutes, seconds) = now[3:6]

print datetime in isoformat

timestamp.isoformat()

Terminal Graphics #

Last updated March 17, 2026
circuitpythonjavascript

The terminalio.Terminal class implements a traditional 'terminal' with VT100 control codes. It does this by giving you a stream you can print to. It converts the stream of characters into a tilegrid using the bitmap font. It will wrap lines if necessary. Unfortunately it only supports a single color at a time. Multi-colored text isn't supported.

import terminalio
import displayio

# size of each font glyph (this is a monospaced font)
fontx, fonty = terminalio.FONT.get_bounding_box()

# adjust palette if desired
plain_palette = displayio.Palette(2)
plain_palette[0] = 0x000000
plain_palette[1] = 0x33ff33

# create grid using the font 
# width and height in tiles
# x and y in pixels
pgrid = displayio.TileGrid(terminalio.FONT.bitmap,
                           x=0,
                           y=0,
                           width=display.width // fontx,
                           height=display.height // fonty,
                           tile_width=fontx,
                           tile_height=fonty,
                           pixel_shader=plain_palette)

# terminal attached to the tilegrid
ptermx = terminalio.Terminal(pgrid, terminalio.FONT)

# add to the screen
display.root_group.append(pgrid)

Print to the terminal with print:

# with newline
print("some text", file=ptermx, end="\r\n")

# without newline
print("some text", file=ptermx, end="")

e-ink screens #

Last updated March 17, 2026
circuitpythongraphics

E-ink screens show only black and white, but can effectively show grayscale using dithering. Internally the screens use a full color frame buffer, so colors should be specified the same as normal.

Fill the background with white using a single large bitmap

# fill the background with white
background_bitmap = displayio.Bitmap(WIDTH, HEIGHT, 1)
# Map colors in a palette
palette = displayio.Palette(1)
palette[0] = WHITE # 0xffffff
# Create a Tilegrid with the background and put in the displayio group
t = displayio.TileGrid(background_bitmap, pixel_shader=palette)
group.append(t)

Then set the color of labels to be BLACK as 0x000000.

Bitmaps #

Last updated March 17, 2026
circuitpythongraphics

A bitmap is just an empty image. It has a fixed size and number of colors. Pixels can be set directly using x,y coordinates.

import displayio


bitmap = displayio.Bitmap(32,32,2)
palette = displayio.Palette(2)
palette[0] = 0x000000
palette[1] = 0xffffff

# set pixel at x=3, y=4 to color 1
bitmap[3,4] = 1

Bitmaps can only be shown on screen using a tilegrid. To create a tilegrid that shows the bitmap without any repetition, do:

tilegrid = displayio.TileGrid(bitmap, pixel_shader=palette)
display.root_group.append(tilegrid)

convert a PNG to a indexed color bitmap

convert digits.png -colors 64 -type palette -compress None BMP3:digits.bmp

Fonts #

Last updated March 17, 2026
circuitpythongraphics

Use a custom font in a label or button

Overview | Custom Fonts for CircuitPython Displays | Adafruit Learning System

Convert truetype font to bitmap

brew install otf2bdf
otf2bdf FontFile.ttf -p pointsize -o FontFile.bdf

convert bitmap ascii to bitmap binary bdftopcf font converter

Use in python code

from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font

font = bitmap_font.load_font("my_font.bdf")
# draw in red
text_label = label.Label(font, text="Greetings Earthling!", color=0xFF0000)

Text and Labels #

Last updated March 17, 2026
circuitpythongraphics

create a new text label:

from adafruit_display_text import label
 
label = label.Label(
    font=terminalio.FONT,
    text="Greetings Earthlings",
    x=20,
    y=10,
    scale=1
)
display.root_group.append(label)

Display Graphics #

Last updated March 17, 2026
circuitpythongraphics

Everything graphics in CircuitPython is built around the displayio library.

display graphics

First you need a display object, which is usually preconfigured for your board if it has a built in display, as board.display. It will automatically refresh the screen. The root of the screengraph should already be set to a group, so you can append to it.

display = board.display
display.root_group.append(some_gfx_object)

Unit Tests #

Last updated March 17, 2026
circuitpython
import unittest

class BasicParsing(unittest.TestCase):
    def test_thing(self):
        self.assertEqual("foo","bar")

if __name__ == '__main__':
    unittest.main()

Files #

Last updated March 17, 2026
circuitpython

Load JSON from disk

config = json.load(open("config.json",'rb'))
print(config['foo']['bar'])

read text file to string

with open("./blog.html", "r") as txt:
    html = txt.read()

Wifi #

Last updated March 17, 2026
circuitpython

list wifi

for network in wifi.radio.start_scanning_networks():
    print(f"{network.ssid} [Ch:{network.channel}] RSSI: {network.rssi}")

connect to wifi

wifi.radio.connect(ssid, password)

print wifi info

if wifi.radio.connected:
    print("wifi SSID info = ",wifi.radio.ap_info.ssid)
    print('transmit power', wifi.radio.tx_power)
    print(f"My MAC address: {[hex(i) for i in wifi.radio.mac_address]}")
    print("radio enabled", wifi.radio.enabled)
    print("host name", wifi.radio.hostname)

fetch HTTP request

# Initalize Wifi, Socket Pool, Request Session
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)

with requests.get("http://webpage.site") as response:
    print(response.text)

CircuitPython Tricks

get the network time

# specific to the MatrixPortal M4
pool = adafruit_connection_manager.get_radio_socketpool(matrix.network._wifi.esp)
# most modern boards
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio),

# get the time
ntp = adafruit_ntp.NTP(socketpool=pool)

# print the time
print(ntp.datetime)

For the MagTag use magtag.get_local_time(), but you’ll need to setup the Adafruit IO credentials in settings.toml.

magtag.get_local_time() # sync with the network time
now = time.localtime()

USB Host #

Last updated March 17, 2026
circuitpython

RP2040 with USB Host

usb_host.Port(board.USB_HOST_DATA_PLUS, board.USB_HOST_DATA_MINUS)
if supervisor.runtime.usb_connected:
    print("USB<host>!")
else:
    print("!USB<host>")

while True:
    print(sys.stdin.read(1))

Scan for devices on USB host

while True:
    print("searching for devices")
    for device in usb.core.find(find_all=True):
        print("pid", hex(device.idProduct))
        print("vid", hex(device.idVendor))
        print("man", device.manufacturer)
        print("product", device.product)
        print("serial", device.serial_number)
        print("config[0]:")
        config_descriptor = adafruit_usb_host_descriptors.get_configuration_descriptor(
            device, 0
        )

        i = 0
        while i < len(config_descriptor):
            descriptor_len = config_descriptor[i]
            descriptor_type = config_descriptor[i + 1]
            if descriptor_type == adafruit_usb_host_descriptors.DESC_CONFIGURATION:
                config_value = config_descriptor[i + 5]
                print(f" value {config_value:d}")
            elif descriptor_type == adafruit_usb_host_descriptors.DESC_INTERFACE:
                interface_number = config_descriptor[i + 2]
                interface_class = config_descriptor[i + 5]
                interface_subclass = config_descriptor[i + 6]
                print(f" interface[{interface_number:d}]")
                print(
                    f"  class {interface_class:02x} subclass {interface_subclass:02x}"
                )
            elif descriptor_type == adafruit_usb_host_descriptors.DESC_ENDPOINT:
                endpoint_address = config_descriptor[i + 2]
                if endpoint_address & DIR_IN:
                    print(f"  IN {endpoint_address:02x}")
                else:
                    print(f"  OUT {endpoint_address:02x}")
            i += descriptor_len
        print()
    time.sleep(5)

General Python #

Last updated March 17, 2026
circuitpython
  • turn an exception into an array of strings with traceback.format_exception(e)
  • format an array of strings into a single string with ”some string”.join(arr)
  • catch an exception:
    try:
        save_pixels('/screenshot.bmp',pixel_source=display)
        logger.info("saved the screenshot")
    except Exception as e:
        logger.error("couldnt take screenshot")
        logger.error(''.join(traceback.format_exception(e)))

  • format a float: print(f”num = {num}”).

  • format a float with leading zeros up to two chars wide: print(f”num = {num:02}”).

CYB Cheap Yellow Board #

Last updated March 17, 2026
circuitpython

Also called the Sunton ESP32-2432S028. Like many ESP32 boards you must install CircuitPython using the web install using Chrome, or the commandline esp32 tool.

Lilygo T-Display S3 #

Last updated March 17, 2026
circuitpython

This little board is ESP based and doesn't come from Adafruit so you can't enter bootmode to open up a local drive. You have to install circuitpython over serial connection. The easiest way to do this is from the CP install page's new 'open installer button'. You must do this part on Chrome.

  1. plug the device into your computer using a USB cable. make sure it's a data cable, not just power.
  2. Hold the boot button, click the reset button, then release the boot button. It should be in bootmode. From the commandline on a a mac you should be able to see the serial port using ls -l /dev/tty.*.
  3. Go to the CircuitPython release page using Chrome.
  4. Click the 'open installer' button (on the right side below the purple download buttons). image here
  5. Click the 'Upgrade CircuitPython 9.2.1 Bin Only' link in the dialog and follow the prompts. Assuming the device's serial port was found you should have CP installed after a minute or two.
  6. Reboot the device and the CIRCUITPY drive should come up.

Adafruit Magtag #

Last updated March 17, 2026
circuitpython

If your magtag already has the U2F bootloader on it, you can just double-click the reset button to enter bootmode, then drag the .uf2 file with the latest release of CircuitPython onto the magtag bootloader drive that appears. Otherwise you will need to use the ESP32 bin tool to install it.

The magtag has peripherals and here is the printout.

Init the MagTag object:

from adafruit_magtag.magtag import MagTag
magtag = MagTag()

The MagTag has a buzzer. Play tones with:

magtag.peripherals.play_tone(<frequency>,<seconds>)

Check the battery level by looking at the magtag.peripherals.battery property.

print("battery is", magtag.peripherals.battery)

Deep Sleep

The MagTag can enter a super deep sleep for a certain number of seconds using the exit_and_deep_sleep function. When the time is up the MagTag will reboot and run the contents of code.py again.

magtag.exit_and_deep_sleep(15*60) # 15 minutes

To do a lighter sleep and wake up rather than restarting use the enter_light_sleep function.

magtag.enter_light_sleep(60) # 60 seconds

Sharp Memory display #

Last updated March 17, 2026
circuitpython

Assuming the display is connected to the standard SPI connections on an RP2040

displayio.release_displays()
bus = busio.SPI(board.SCK, MOSI=board.MOSI)
chip_select_pin = board.D25
framebuffer = sharpdisplay.SharpMemoryFramebuffer(bus, chip_select_pin, width=144, height=168)
display = framebufferio.FramebufferDisplay(framebuffer)
display.root_group = displayio.CIRCUITPYTHON_TERMINAL
while True:
    print("hello")
    pass

Waveshare round 1.28 LCD RP2040 #

Last updated March 17, 2026
circuitpython

The Waveshare round 1.28 LCD is a set of cheap devices which run CircuitPython and have a USB connection. They have a lot of power in a tiny formfactor.

Code

Boot

Hold down the boot button on the back of the device while plugging in the USB-C cable to your laptop.

Display

To access the display you need to install gc9a01, a separate driver library, with circup install gc9a01 then initialize it. Note that the touch and non-touch versions are slightly different. On the non-touch version reset is set to LCD_RST. On the touch version it is Pin 13 (board.GP13), so initialize it like this:

spi = busio.SPI(clock=board.LCD_CLK, MOSI=board.LCD_DIN)
# LCD_RST is 12 in regular version
# but we need 13 for the touch version
display_bus = displayio.FourWire(spi, 
   command=board.LCD_DC, 
   chip_select=board.LCD_CS,
   reset=board.GP13)
display = gc9a01.GC9A01(display_bus, 
   width=240, 
   height=240, 
   backlight_pin=board.LCD_BL)

Lilygo T-Deck Firmware #

Last updated March 17, 2026
circuitpythonlilygo

The T-Deck firmware can be downloaded from the usual location but it cannot enter bootloader mode as a USB drive like most other devices. Instead you must install CircuitPython using WebSerial, which only works in Chrome.

To install it, go to the CircuitPython download page in Chrome and click the Open Installer button. A dialog will appear. Select 'Upgrade CircuitPython 9.2.1 Bin Only', choose the device in the Chrome web-serial port selection dialog, and follow the prompts. This will download and reflash the device. Then turn the device on and off with the power switch and it should boot up with the usual CIRCUITPY drive.

Pycharm setup with CircuitPython #

Last updated March 17, 2026
circuitpython

Under Project Settings / Python interpreter click the + button to install new packages. Install circuitpython-stubs, and then the libraries you are using. common ones include:

  • adafruit-circuitpython-connectionmanager
  • adafruit-circuitpython-requests
  • adafruit-circuitpython-ntp

Full details here.

Circup #

Last updated March 17, 2026
circuitpython

Install Circup

pip install setuptools
pip install circup
circup --version

Commands

  • List all out of date modules: circup list
  • update all out of date modules: circup update
  • install a new module: circup install <module_name>
  • show circup show

Circup failure

If the latest build of CP is bad for some reason, Circup can fail because it will always try to download the latest release. There should be an option to not check for the latest release. If you are installing a lib then you probably want the version of that lib that matches the CP on your board. Something like this:

circup --use-current-version install adafruit_bitmapsaver

Install circup #

Last updated March 17, 2026
circuitpython
pip install setuptools
pip install circup
circup --version

Correctly Install Python on MacOS #

Last updated March 17, 2026
macospythoncircuitpython

Python on Mac is a pain because there are so many different ways to do it. I've found that pyenv is the best way to manage python versions on MacOS

# install pyenv
brew install pyenv pyenv-virtualenv

# list all available python variants
pyenv install -l

# install latest 3.x.x python
pyenv install 3 

# add to the shell
echo 'PATH=$(pyenv root)/shims:$PATH' >> ~/.zshrc

Now restart your shell / open a new terminal window.

You can manage python with these commands:

  • See the current global version: pyenv versions
  • See where the current python versions come from: pyenv versions
  • See what is the current local version: pyenv local
  • List all possible versions of python to install pyenv install --list
  • Install (download and build) a particular version of python pyenv install 3.12.8
  • Set the current local version to 3.12.8: pyenv local 3.12.8