PyPortal CircuitPy Tutorial (AdaBox 011)

Advertisement

Advertisement

Introduction

The PyPortal is an awesome little IoT device that is programmable with CircuitPython. It's got wi-fi, a color touch screen, a speaker and speaker connector, microSD card slot, 8MB flash memory, a light sensor, a temperature sensor, a NeoPixel LED, a few JST connectors, and more!

You can buy a PyPortal at https://www.adafruit.com/product/4116. This tutorial covers the PyPortal that came in AdaBox011. I don't believe there are any differences between the AdaBox version and the one you can buy separately from the shop.

In this tutorial we will take a look at the various components and how to use them in CircuitPython with code examples for each. After following the tutorial you should have a solid grasp on the PyPortal.

Since the hardware and software is open source, you can find the sources online:

PyPortal board components

There are several chips, sensors, and connectors on the board so let's break it down and make sure we understand what it has and what the parts do.

For more details about the specific pins being used by each component, check out https://learn.adafruit.com/adafruit-pyportal/pinouts

SAMD51

The main processor that sits roughly in the center of the board is an ATMEL ATSAMD51J20 Cortex M4 that clocks in at 120MHz and runs off 3.3V power. It comes with 1MB of built-in Flash storage and 256KB of RAM.

QSPI flash storage

The flash storage chip sits between the reset button and the SAMD51 processor and can store up to 8MB of data. This includes the Python source code files as well as any images, sound files, and libraries needed to support your application.

This storage is the 8MB CIRCUITPY removable drive you see when it is connected to your computer.

MicroSD card slot

Even though the board comes with an 8MB flash storage chip, that can be quite limiting because it is fairly small and it is read-only from your source code. By adding the microSD card slot it allows you to add huge amounts of extra storage that you can both read from and write to. You can get microSD cards that can store 1TB of data and you can easily swap them out so it is a very useful addition.

It uses board.SD_CS for the CS pin or Arduino D32. There is also a board.SD_CARD_DETECT/D33 to see if a card is present.

ESP32 wifi processor

The Espressif ESP32 is a Wi-Fi processor that is very common in IoT devices and frequently used with Arduino boards. The main purpose on this board is to provide Wi-Fi that includes encrpytion (TLS/SSL) support allowing you to do all the normal HTTP and HTTPS requests you would expect to be able to do. Since it handles all of the encryption and network processing, the SAMD51 processor is free to handle all other operations.

The board is configured to use board.ESP_CS, board.ESP_BUSY, and board.ESP_RESET, or Arduino pins 8, 5, 7, respectively.

Speaker & Speaker plug

Built in speaker and a 2-pin Molex PicoBlade connector for an 8-16 ohm speaker. It is on pin A0 or board.AUDIO_OUT. There is a solder jumper next to the speaker that sends the electricity to both the internal and external speaker. Cut it to send all the power to the external speaker only.

TFT touch screen

One of the main components is a 3.2" color touch screen with 320x240 resolution. The board uses board.TOUCH_YD, board.TOUCH_YU, board.TOUCH_XL, and board.TOUCH_XR or Arduino pints 4, 6, 5, and 7 respectively.

Additionally, there are control pins: board.TFT_RESET, board.TFT_WR/board.TFT_DC, board.TFT_RD, board.TFT_RS, board.TFT_CS, board.TFT_TE, or Arduino pins 24, 25, 9, 10, 11, 12 respectively.

The backlight can be set with board.TFT_BACKLIGHT or A25.

Light sensor

The light sensor is on pin A2 or board.LIGHT and sits between the ESP32 wifi chip and the microSD card slot.

ADT7410 temperature sensor

This is a temperature sensor from Analog Devices. It doesn't need any calibration. Since it is a chip on all on its own, it uses I2C to communicate.

JST-PH connectors

  • 2 x 3-pin JST-PH connector (board.D3/A1 and board.D4/A3 for GPIO)
  • 1 x 4-pin JST-PH connector (I2C)

MicroUSB connector

There is a microUSB connector that lets you power the board, connect it to a computer as a removable drive to upload code, act as a USB HID device (like a keyboard, mouse, joystick), or as a MIDI sender or receiver, among other things.

NeoPixel LED

On the board is a single NeoPixel LED that can be used as a status indicator. It is on the back of the board so it's not quite visible if you had it mounted on the wall. The NeoPixel is on pin D2 or board.NEOPIXEL.

Other LED

There is also an LED right behind the microUSB connector. This LED is on the standard pin D13 or board.L.

Setup

The PyPortal from the AdaBox already comes with CircuitPy installed, but if you need to wipe the board and start over, double tap the reset button on the back of the board while it is plugged in via USB to a computer. This will put it in to boot mode where you can modify the firmware and you should see a removable drive named PORTALBOOT.

Install CircuitPy firmware

Once you have the PORTALBOOT removable drive available, download the latest version of CircuitPy from https://circuitpython.org/board/pyportal/.

When you have the .uf2 file downloaded, copy it over to the PORTALBOOT drive. It will automatically detect that a file has been copied, and it will reboot itself and install CircuitPy. After it restarts itself, you should then see a removable drive named CIRCUITPY instead of PORTALBOOT.

At this point you have a working CircuitPython setup where you can write Python code and put it in code.py.

If you want to completely clear the CIRCUITPY drive removing all libraries, static files, and libraries, you have a few options. You can simply delete all of the files from your computer's file explorer, you can use your system tools to format it as a FAT drive, or you can connect to the REPL via serial and run:

>>> import storage
>>> storage.erase_filesystem()

This will give you a clean drive that you can re-install the default software in the next section. Note that this will only format the CIRCUITPY drive and not the PORTALBOOT drive so the CircuitPy firmware is left intact and you can simply drop in a new code.py file and it will execute as expected. Without a code.py file it will default to printing the serial output on the TFT screen.

Install default example program

You can download the default sample program (random quotes display) from https://learn.adafruit.com/adabox011/updating-your-pyportal.

You can clear out the whole drive before copying over the default files.

In the zip file, there is a directory that contains all of the files including the most important file: code.py. Copy all of those files in to the root of the CIRCUITPY drive so it contains code.py and the lib directory are sitting in the root of the drive. The lib directory is where all the libraries go and where Python will look when importing modules.

It will automatically detect when any files change and restart itself.

To get the default code running properly, you will need to update secrets.py to have your Wi-Fi network name and password.

Connect to serial console

To debug or use the Python REPL, you can connect using serial over USB. When it is plugged in to a computer it will be accessible as a serial device. The PyPortal uses a baud rate of 115200. I recommend using one of the following programs to connect over serial:

For more information, check out my tutorial How to Connect to a Serial Console.

Once connected to the serial console you can monitor the serial output or interact with it like a console. There is also a Python REPL that you can use.

Using the REPL

In the REPL, you can execute Python code. You can run some commands to get some more information about the environment:

>>> import sys
>>> sys.version
'3.4.0'
>>> sys.path 
['', '/', '.frozen', '/lib']
>>> sys.implementation
(name='circuitpython', version=(4, 0, 2))
>>> sys.platform
'MicroChip SAMD51'

The sys.path is a list of directories where it will look for modules when calling import. Learn more about Python syspath in my Python import, sys.path, and PYTHONPATH Tutorial.

You can load and execute a Python source file from the REPL like this:

# Execute a Python file from the REPL
>>> exec(open('code.py').read())

Code examples

Let's look at how you would use the various components on the board. You will find some code samples below or links to official code examples.

Interact with serial console

Since the serial console is just using STDIN and STDOUT communication, you can use print() and input() to interact. For example:

# serial_example.py
print('Hello')
name = input('Enter your name: ')
print("Your name is: %s" % name)

Read files from flash storage

You can use regular Python syntax for reading files from the CIRCUITPY drive. The file system is read-only from the source code, but you can put files on the drive when it connected to a computer and it shows up as the removable drive.

with open('code.py') as source_file:
    print('Code.py contents:')
    print(source_file.read())

Use the SD card

The PyPortal does not come with a microSD card but you can insert one in to the connector it provides. Once you insert a microSD card, you can use it by

Get all libraries including the SD card library and its dependencies from GitHub at Adafruit CircuitPython Bundle

You will need at a minimum the libaries in your lib/ directory:

  • adafruit_bus_device/
  • adafruit_sdcard.mpy

I had a few errors along the way to get this working. After making sure I upgraded all of the libraries to the latest version I had better luck. Note that I had trouble using Samsung EVO SD cards and got the error: "OSError: timeout waiting for v2 card", however it worked with generic 8GB and 256GB cards. I was able to read and write files on the 256GB disk, however when trying to do the os.statvfs to calculate the total disk size it would take forever and basically hang the program.

It uses board.SD_CS for the CS pin or Arduino D32. There is also a board.SD_CARD_DETECT/D33 to see if a card is present.

import os
import busio
import digitalio
import board
import storage
import adafruit_sdcard

# See if a card is present
card_detect_pin = digitalio.DigitalInOut(board.SD_CARD_DETECT)
card_detect_pin.direction = digitalio.Direction.INPUT
card_detect_pin.pull = digitalio.Pull.UP
print('SD card present: %s' % card_detect_pin.value)

# Try to connect to the SD card
sdcard = adafruit_sdcard.SDCard(
    busio.SPI(board.SCK, board.MOSI, board.MISO),
    digitalio.DigitalInOut(board.SD_CS)
)

# Mount the card to a directory
virtual_file_system = storage.VfsFat(sdcard)
storage.mount(virtual_file_system, '/sd')

# At this point, you can use it like a normal file system.
# Create files, open files, create directories, etc

# Calculate disk space
# Avoid performing this disk size calculation on very large disks (takes too long)
# Works just fine on 8GB disks, hangs on 256GB disks
vfs_stats = os.statvfs('/sd')
# Total size = block size * number of blocks
total_size_in_mb = int(vfs_stats[0] * vfs_stats[2] / (1024 * 1024))
# Free space = block size * number of available blocks
available_space_in_mb = int(vfs_stats[0] * vfs_stats[3] / (1024 * 1024))
print('Disk size: %s MB' % total_size_in_mb)
print('Free space: %s MB' % available_space_in_mb)
print('Used space %s MB' % (total_size_in_mb - available_space_in_mb))

# Create/open a file and write to it
with open('/sd/test.txt', 'w') as output_file:
    output_file.write('Hello, world!\n')

# Print contents of SD card root directory
for file_name in os.listdir('/sd'):
    stats = os.stat('/sd/' + file_name)
    file_size = stats[6]  # Size in bytes
    is_dir = stats[0] & 0x4000  # Bitmask
    if is_dir:
        print('Dir: %s' % file_name)
    else:
        print('File: %s (%s bytes)' % (file_name, file_size))

# Unmount the SD card
storage.umount('/sd')

Use "regular" LED

There is an LED on D13 or board.L. This example shows how to use the 'regular' LED. We will look at using the NeoPixel LED in the next example.

For more information on using digitalio, check out the digitalio CircuitPy documentation.

There are no library dependencies needed in the lib directory. It is built in to CircuitPython.

import digitalio
import board
import time

led = digitalio.DigitalInOut(board.L)  # Or board.D13
led.direction = digitalio.Direction.OUTPUT

while True:
    led.value = True
    time.sleep(1)
    led.value = False
    time.sleep(1)

Use NeoPixel LED

The NeoPixel is on board.NEOPIXEL/D2 and is an RGB LED with controllable brightness. For more information on working with the NeoPixel check out the CircuitPython NeoPixel documentation.

There is one dependency that you will need in your lib directory, which you can get the library from Adafruit CircuitPython Bundle.

  • neopixel.mpy
import board
import neopixel
import time

# Connect to the NeoPixel (there is only one so it is index 0)
# auto_write means we don't have to call pixels.show() each time
pixels = neopixel.NeoPixel(board.NEOPIXEL, 1, auto_write=True)

while True:
    pixels[0] = (255, 0, 0)  # Red, green, blue
    time.sleep(.25)
    pixels[0] = (0, 0, 255)
    time.sleep(.25)

Use the speaker

The audio output is on pin A0 or board.AUDIO_OUT. By default it will use the small built-in speaker, but you can also use the 2-pin Molex PicoBlade to connect an 8-16 ohm speaker. If you plug in a speaker it will go to both the buit-in speaker and external speaker unless you cut the solder jumper that is right next to the speaker. Cutting it will also allow more of the electricity to flow to the speaker, making it louder.

In CircuitPython, you can use the audioio module to output sound. You can output raw waveforms or .wav files.

You can learn more about using audio out with CircuitPython and examples at https://learn.adafruit.com/circuitpython-essentials/circuitpython-audio-out.

Play .wav files

This example shows how to play a .wav file. It uses the pyportal_startup.wav file which you can download from here: PyPortal demo files. The audio file should be in the root of the CIRCUITPY drive.

There are no library dependencies needed in the lib directory since audioio is a built-in CircuitPy library.

import audioio
import board
import time

def play_sound_file(file_path):
    try:
        with open(file_path, "rb") as f:
            wave = audioio.WaveFile(f)
            audio.play(wave)
            while audio.playing:
                time.sleep(0.005)
    except OSError as e:
        print('Error opening file: %s' % e)

with audioio.AudioOut(board.AUDIO_OUT) as audio:  # or board.A0
    play_sound_file("pyportal_startup.wav")

Play raw audio

This example shows how to generate and play a sine wave through the board.AUDIO_OUT/board.A0. There are no library dependencies needed in the lib directory.

# Adapted from examples at:
# https://circuitpython.readthedocs.io/en/latest/shared-bindings/audioio/AudioOut.html
import time
import array
import math
import audioio
import board

SAMPLERATE = 8000  # 8000 samples per second

def generate_sine_wave(frequency=440):
    length = SAMPLERATE // frequency
    sine_wave_data = array.array("H", [0] * length)
    for i in range(length):
        sine_wave_data[i] = int(math.sin(math.pi*2*i/18) * (2**15) + 2**15)
    sound_sample = audioio.RawSample(sine_wave_data)
    return sound_sample

sample = generate_sine_wave()
audio = audioio.AudioOut(board.AUDIO_OUT)  # or board.A0
audio.play(sample, loop=True)

while True:  # Stay running
    time.sleep(300)

Use GPIO pins

The two 3-pin JST-PH connectors on the side of the board can be used for digital or analog IO.

The connects are labeled D3 and D4 which correspond to board.D3/A1 and board.D4/A3.

Check out the documentation for the respective modules:

Use I2C

The 4-pin JST-PH connector on the side of the board is for I2C communication. I2C is also used to communicate with the on-board ADT7410 temperature sensor.

According to the PyPortal Pinouts page, you can switch between 5V and 3.3V for the I2C connector by cutting the solder jumper right behind the connector and connecting the 3V side. Apparently this is required or the board can hang (so why is the 5V even an option much less the default connection?).

For an code example of using I2C, see the next section with an example of using the ADT7410 temperature sensor which communicates using I2C. Also check out the Adafruit I2C protocol tutorial.

Use ADT7410 temperature sensor

Since the ADT7410 is an chip all on its own, it uses I2C to communicate data instead of providing raw analog values.

There are three library dependency you will need in your lib directory:

  • adafruit_bus_device/
  • adafruit_register/
  • adafruit_adt7410.mpy

You can get the libraries from: Adafruit CircuitPython Bundle.

Values will be represented as a floating point number specifying the temperature in Celsius degrees.

This example will get the temperature and print it out once per second.

import board
import busio
import adafruit_adt7410
import time

temp_sensor = adafruit_adt7410.ADT7410(
    busio.I2C(board.SCL, board.SDA),
    address=0x48,  # Specific device address for ADT7410
)
temp_sensor.high_resolution = True

while True:
    # Read temperature
    degrees_celsius = temp_sensor.temperature
    print('Temperature: %s C' % degrees_celsius)
    time.sleep(1)

Use light sensor

The light sensor uses pin board.A2 or board.LIGHT and can be read using the analogio module. Check out the analogio CircuitPy documentation.

This example will print out the light value every second. Values will range from 0 to 65535.

There are no library dependencies need in the lib directory since it uses regular analog input that is built-in to CircuitPython.

import board
import analogio
import time

light_sensor = analogio.AnalogIn(board.LIGHT)

while True:
    # Get analog value from light sensor
    # Range will be 0 - 65535
    light_value = light_sensor.value
    print('Light sensor value: %s' % light_value)
    time.sleep(1)

Use TFT screen

The TFT screen can display color but also detects touch input. We will look at examples of how to control the screen brightness, display bitmap images, control individual pixels, write text, use custom bitmap fonts, draw shapes, and detect touch inputs.

Change backlight brightness

You can easily change the TFT display brightness by referencing board.DISPLAY.brightness and setting it to a value between 0 and 1. The last value set will remain until changed.

This example will slowly increase the brightness to maximum when the board is restarted.

There are no library dependencies needed in the lib/ directory.

import board
import time

i = 0
while i <= 1:
    board.DISPLAY.brightness = i
    time.sleep(.25)
    i += 0.1

If you want to drop down to a lower level, you can control the backlight pin (board.TFT_BACKLIGHT/A25) directly with PWM. The backlight pin accepts PWM values from 0-65535 but the min to max brightness is 0-32768. Note if you don't call displayio.release_displays() it will say TFT_BACKLIGHT is in use. However, calling the release function will completely uninitialize the display and make it difficult to use again without manual reinitialization. The CircuitPy firmware for the PyPortal does all that setup so I really don't recommend calling displayio.release_displays() unless you know exactly how to set it up again.

Read more about using PWM with CircuitPython at https://circuitpython.readthedocs.io/en/4.x/shared-bindings/pulseio/init.html.

Get screen dimensions

This example will print out the screen resolution.

There are no library dependencies.

import board

print('Resolution: %sx%s' %
      (board.DISPLAY.width, board.DISPLAY.height))

Display text

There is one dependent library you will need in your lib/ directory which you can get from the Adafruit CircuitPython library bundle.

  • adafruit_display_text/

Read more about the text display library at https://circuitpython.readthedocs.io/projects/display-text/en/latest/

Also check out the Adafruit text tutorial.

This example will print some yellow text in the top-left corner, slowly move it closer to the center, then change the color and add more text.

import board
import terminalio
from adafruit_display_text import label
import time

# You must provide the text or the max_glyphs length, or both.
# If no max_glyphs specified, the maximum is set to length of text
# max_glyphs is the max amount of characters the text can contain
text_area = label.Label(
    terminalio.FONT,
    text="PyPortal\nRocks",
    max_glyphs=50,  # Optionally allow longer text to be added
    color=0xFFFF00,
    x=20,  # Pixel offsets from (0, 0) the top left
    y=20,
    line_spacing=1,  # Distance between lines
)

board.DISPLAY.show(text_area)

# You can modify the x and y coordinates and it will
# immediately update the position
for i in range(0, 50, 10):
    text_area.y += i
    text_area.x += i
    time.sleep(.5)

# Change the text color
text_area.color = 0xFF0000
# Add to text
text_area.text = text_area.text + '!!1'

# Keep the program running, otherwise the display is cleared
while True:
    time.sleep(300)
Use custom bitmap fonts

In the previous example we used the built in terminal font. You can also use custom .bdf fonts.

Building on the previous example, we will look at using a custom font by leveraging the Adafruit CircuitPython Bitmap Font library (source) (documentation).

Also check out the Adafruit text tutorial and the Adafruit custom fonts tutorial.

The default example PyPortal files come with one custom font you can use for testing. The sample font is fonts/Arial-ItalicMT-17.bdf. You can use any .bdf font though.

The major difference here is that instead of using terminalio.FONT, we load a custom font and pass that to the Label constructor. Otherwise you treat the text the same way we did in the previous example.

There are two dependencies you will need in your lib/ directory which you can get from the Adafruit CircuitPython library bundle:

  • adafruit_display_text/
  • adafruit_bitmap_font/

Note it make take a moment on startup to load the font.

import board
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
import time

custom_font = bitmap_font.load_font("fonts/Arial-ItalicMT-17.bdf")

text_area = label.Label(
    custom_font,
    text="PyPortal Rocks",
    color=0x00FFFF,
    x=50,  # Pixel offsets from (0, 0) the top left
    y=50,
    line_spacing=1,  # Distance between lines
)

board.DISPLAY.show(text_area)

# Keep program running so text remains displayed
while True:
    time.sleep(300)

Display bitmap images

You can display bitmap (.bmp) images using the displayio library. You can easily make your own bitmap files using Microsoft Paint, GIMP, KolourPaint, or a similar program.

The default example PyPortal files also come with a couple BMP files like pyportal_startup.bmp.

There are no library dependencies needed.

import board
import displayio
import time

image_file = open("pyportal_startup.bmp", "rb")
bitmap_contents = displayio.OnDiskBitmap(image_file)

tile_grid = displayio.TileGrid(
    bitmap_contents,
    pixel_shader=displayio.ColorConverter(),
    default_tile=0,
    x=0,  # Position relative to its parent group
    y=0,
    width=1,  # Number of tiles in the grid
    height=1,
    # tile_width=None,  # Number of tiles * tile size must match BMP size
    # tile_height=None,  # None means auto size the tiles
)

group = displayio.Group()
group.append(tile_grid)
board.DISPLAY.show(group)

# Move the whole group (which includes the TileGrid that has our image)
# The TileGrids inside the group have a relative position to the
# position of the group.
for i in range(0, 25, 5):
    group.x = i
    group.y = i
    time.sleep(.1)
# Then reset it back to 0,0
group.x, group.y = 0, 0

# You can scale groups by integer values, default is 1
group.scale = 2

# Each TileGrid inside a group has its own position relative to
# the position of the parent group.
# Move the TileGrid only, leaving group in same spot
for i in range(0, 25, 5):
    tile_grid.x = i
    tile_grid.y = i
    time.sleep(.1)

# If you had more TileGrids, you could add or remove them with:
# group.append()
# group.pop()
# group.insert()

# If you close the file, it will not be able to display any more
# image_file.close()

# Keep program running so image stays up
while True:
    time.sleep(300)

By default the TileGrid just has one big tile. Understanding the TileGrid is a really important concept to understand. The TileGrid allows you to swap out sections of the grid with other sections of the bitmap. This allows you essentially create sprite sheets. You can create a tile grid with a single tile, but load a bitmap that contains a grid of sprites. Then, assign whichever sprite tile you want to the visible tile. You can learn more from the Adafruit TileGrid and Group tutorial and the section on sprite sheets.

Control individual pixels

Instead of loading a bitmap file from disk like the previous example, you can create an in-memory bitmap and set the values as you please. Since we know the screen is 320x240, we can create a bitmap with those dimensions in memory.

In addition, we have to specify a palette of colors for the bitmap to use. It is required to set up the palette with the list of colors that will be used in the bitmap.

This example will create a palette, a bitmap. Then it will create a tile grid that uses the palette and bitmap. Then it will create a group and put the tile grid in the group. Then it will display the group. Then we can manipulate individual pixels of the bitmap.

There are no library dependencies needed in the lib/ directory.

import board
import displayio
import time

number_of_colors = 3

palette = displayio.Palette(number_of_colors)  # Palette with 3 colors max
palette[0] = 0xFF0000  # Red
palette[1] = 0xFFFFFF  # White
palette[2] = 0x0000FF  # Blue

bitmap = displayio.Bitmap(
    board.DISPLAY.width,
    board.DISPLAY.height,
    number_of_colors,
)

# Now that we have a palette and a bitmap ready, we can create and use
# a TileGrid just like the previous example.
# The entire bitmap will be filled with palette[0] color on initialization
tile_grid = displayio.TileGrid(bitmap, pixel_shader=palette)

group = displayio.Group()
group.append(tile_grid)

board.DISPLAY.show(group)

# At this point the bitmap is being displayed and we can manipulate it
# When assigning a bitmap pixel value, you don't specify an RGB color
# you specify the palette index which already has an RGB color assigned.
# In this case, 0 = Red, 1 = White, and 2 = Blue as defined in the palette.
bitmap[0, 0] = 1  # Top left pixel
bitmap[319, 239] = 1  # Bottom right pixel
bitmap[319, 0] = 1  # Top right pixel
bitmap[0, 239] = 1  # Bottom left pixel
bitmap[160, 120] = 2  # Center pixel

# Keep program running so our bitmap remains visible
while True:
    time.sleep(300)

Display shapes

Adafruit provides a library for drawing simple shapes like circles, rectangles, and rectangles with rounded edges to the display.

One dependency needed in your lib/ directory which you can get from the Adafruit CircuitPython library bundle.

  • adafruit_display_shapes/

You can view the Adafruit_CircuitPython_Display_Shapes source on GitHub

This example will demonstrate how to draw the available shapes. These can be useful for drawing shapes when making a game, or creating the appearance of buttons, meters, progress bars, etc.

# Adapted from example at:
# https://github.com/ladyada/Adafruit_CircuitPython_Display_Shapes
import board
import displayio
import time
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.roundrect import RoundRect

group = displayio.Group(max_size=10)  # Up to 10 shapes/tile grids

board.DISPLAY.show(group)

# Create a rectangle at (50, 50) with size of 10x25 pixels
# The default color is black, so it won't be
# visible against a black background
simple_rectangle = Rect(50, 50, 10, 25)

# Create a rectangle with all of the options
# Note that the outline fills inward, and does
# not affect the width/height of the shape
white_rectangle = Rect(
    x=0,
    y=0,
    width=25,
    height=25,
    fill=0xFFFFFF,  # Optionally fill the shape with a color
    outline=0xFF0000,  # Optionally outline with a color
    stroke=5,  # Thickness of the outline (default 1 pixel)
)

red_circle = Circle(
    x0=board.DISPLAY.width,  # Position of center of circle
    y0=board.DISPLAY.height,  # Position it in the bottom right
    r=20,
    fill=0xFF0000,
    outline=0xFFFFFF,
)

rounded_edge_purple_rectangle = RoundRect(
    x=50,
    y=100,
    width=100,
    height=30,
    r=15,  # Radius of the rounded corners
    fill=0x888888,
    outline=0xFFFFFF,
    stroke=3,
)

# Append shapes to the group, actually making them visible
group.append(simple_rectangle)  # Defaults to black so won't be visible
group.append(white_rectangle)
group.append(red_circle)
group.append(rounded_edge_purple_rectangle)

# Move one of the shapes around
# Changes will immediately be displayed
while True:
    rounded_edge_purple_rectangle.x += 5
    if rounded_edge_purple_rectangle.x > board.DISPLAY.width:
        rounded_edge_purple_rectangle.x = 0
    time.sleep(0.1)

Detect touches

The TFT screen is not only useful for displaying text, bitmap fonts, bitmap images, and shapes, but it can also be used to detect touch inputs!

Not only will it detect touch input, it can also sense how hard the touch is! Be careful not to press too hard though because you don't want to damage the screen. A light touch is all that is needed to trigger a press.

You can view the Adafruit_CircuitPython_Touchscreen source on GitHub and refer to the official documentation.

There is one dependency you will need in your lib/ directory which you can get from the Adafruit CircuitPython library bundle.

  • adafruit_touchscreen/
# Adapted from example at:
# https://circuitpython.readthedocs.io/projects/touchscreen/en/latest/examples.html
import board
import adafruit_touchscreen

touchscreen = adafruit_touchscreen.Touchscreen(
    board.TOUCH_XL,
    board.TOUCH_XR,
    board.TOUCH_YD,
    board.TOUCH_YU,
    # calibration=((9000, 59000), (8000, 57000)),
    # resistance=None
    size=(board.DISPLAY.width, board.DISPLAY.height),
)

while True:
    point_being_touched = touchscreen.touch_point
    if point_being_touched:
        print(
            'Touch at (%s, %s) Pressure: %s' %
            (point_being_touched[0],
             point_being_touched[1],
             point_being_touched[2]),
        )

Use ESP32 wi-fi

The ESP32 chip handles all the wi-fi processing and encryption. We will look at some examples of how to update the firmware, scan for access points, connect to an access point, ping, perform DNS lookups, and make HTTP(S) requests.

Links: - Adafruit_CircuitPython_ESP32SPI source on GitHub - Official documentation - Official examples

You will need the ESP32 and SPI depdendencies in your lib/ directory which you can get from the Adafruit CircuitPython library bundle.

  • adafruit_esp32spi/
  • adafruit_bus_device/

Update ESP32 firmware

In order to update the ESP32 firmware, you will first need to install different firmware. This will replace the CircuitPy firmware and install firmware that makes the main chip (SAMD51) act as a simple serial passthru so you can talk directly to the ESP32 over serial.

You can download the firmware and read the official on Adafruit tutorial on updating ESP32 firmare.

Download the PyPortal_ESP32_Passthru.UF2 file, double tap reset to get the PORTALBOOT drive, and then copy the file over. When it restarts you will not see a PORTALBOOT or a CIRCUITPY drive this time. You will have a serial device that you can connect to though.

Now, you will need to download the latest ESP32 firmware, which you can get from https://learn.adafruit.com/adding-a-wifi-co-processor-to-circuitpython-esp8266-esp32/firmware-files#esp32-only-spi-firmware-3-8. Download the latest NINA firmware and get the .bin file.

Next, you will need the esptool to upload the firmware over serial. You can get it from https://github.com/espressif/esptool or install it using Python's pip with:

python -m pip install esptool

Press the reset button once to get the In this example a Windows serial port COM5 is used but in Linux it would be something like /dev/ttyACM*. Replace NINA_W102.bin with the binary file you downloaded. The command to run will look like this:

python -m esptool --port COM8 --before no_reset --baud 115200 write_flash 0 NINA_W102-1.3.0.bin

After it is burning the firmware, which will take a few minutes, you can verify it is installed by double-tapping the reset button and reinstalling the CircuiyPy firmware. Then run the example in the next section to print out the firmware version.

Print ESP32 firmware version

You can check the installed firmware version with this example.

It has two dependencies:

  • adafruit_esp32spi/
  • adafruit_bus_device/
import board
from digitalio import DigitalInOut
import busio
from adafruit_esp32spi import adafruit_esp32spi

esp32_cs_pin = DigitalInOut(board.ESP_CS)
esp32_ready_pin = DigitalInOut(board.ESP_BUSY)
esp32_reset_pin = DigitalInOut(board.ESP_RESET)

spi = busio.SPI(board.SCK, board.MOSI, board.MISO)

esp = adafruit_esp32spi.ESP_SPIcontrol(
    spi,
    esp32_cs_pin,
    esp32_ready_pin,
    esp32_reset_pin,
)

print('ESP32 firmware version: %s' % esp.firmware_version)

Scan for access points

This example demonstrates how to scan for wireless network access points and prints the network name, signal strength, and encryption level (4=WPA).

It has two dependencies:

  • adafruit_esp32spi/
  • adafruit_bus_device/
import board
from digitalio import DigitalInOut
import busio
from adafruit_esp32spi import adafruit_esp32spi

esp32_cs_pin = DigitalInOut(board.ESP_CS)
esp32_ready_pin = DigitalInOut(board.ESP_BUSY)
esp32_reset_pin = DigitalInOut(board.ESP_RESET)

spi = busio.SPI(board.SCK, board.MOSI, board.MISO)

esp = adafruit_esp32spi.ESP_SPIcontrol(
    spi,
    esp32_cs_pin,
    esp32_ready_pin,
    esp32_reset_pin,
)

if esp.status == adafruit_esp32spi.WL_IDLE_STATUS:
    print('ESP32 found and in idle mode')

print("ESP32 MAC address: %s" %
      ':'.join(['{:02X}'.format(byte) for byte in esp.MAC_address]))

print('Access points detected:')
print(' - SSID | Strength | Encryption')
for ap in esp.scan_networks():
    print("- %s | %s | %s" %
          (str(ap['ssid'], 'utf8'), ap['rssi'], ap['encryption'])
    )

Connect to an access point

This example shows how to connect to an access point. It will retry a few times in case it fails the first time. It should retry 10 times by default. Once it is connected, it will get the SSID of the connected network, its signal strength, and your IP address on the network.

It has the same two dependencies as the previous example:

  • adafruit_esp32spi/
  • adafruit_bus_device/
import board
from digitalio import DigitalInOut
import busio
from adafruit_esp32spi import adafruit_esp32spi

SSID = 'wifi_network_name'
SSID_PASSWORD = 'wifi_password'

esp32_cs_pin = DigitalInOut(board.ESP_CS)
esp32_ready_pin = DigitalInOut(board.ESP_BUSY)
esp32_reset_pin = DigitalInOut(board.ESP_RESET)

spi = busio.SPI(board.SCK, board.MOSI, board.MISO)

esp = adafruit_esp32spi.ESP_SPIcontrol(
    spi,
    esp32_cs_pin,
    esp32_ready_pin,
    esp32_reset_pin,
)

if esp.status == adafruit_esp32spi.WL_IDLE_STATUS:
    print('ESP32 found and in idle mode')

print('Connecting to access point: %s' % str(SSID, 'utf8'))
while not esp.is_connected:
    try:
        esp.connect_AP(SSID, SSID_PASSWORD)
    except RuntimeError as e:
        print(e)
        continue

# Get network information
print('Connected to %s' % str(esp.ssid, 'utf-8'))
print('Signal strength: %s' % esp.rssi)
print('ESP32 IP address: %s' % esp.pretty_ip(esp.ip_address))
print("ESP32 MAC address: %s" %
      ':'.join(['{:02X}'.format(byte) for byte in esp.MAC_address]))

# At this point, you are connected and can do things like
# ping, do DNS lookups, and make HTTP requests

Ping & DNS lookups

This example shows you how to perform a ping and DNS lookups after connecting to a wireless access point. This example will perform a lookup to get the IP address for www.devdungeon.com and also do three pings to check response time.

One potential use for this is to perform ping sweeps on the subnet and look for hosts on the network.

It has the same two dependencies as the previous example:

  • adafruit_esp32spi/
  • adafruit_bus_device/
import board
from digitalio import DigitalInOut
import busio
from adafruit_esp32spi import adafruit_esp32spi


SSID = 'wifi_network_name'
SSID_PASSWORD = 'wifi_password'

esp32_cs_pin = DigitalInOut(board.ESP_CS)
esp32_ready_pin = DigitalInOut(board.ESP_BUSY)
esp32_reset_pin = DigitalInOut(board.ESP_RESET)

spi = busio.SPI(board.SCK, board.MOSI, board.MISO)

esp = adafruit_esp32spi.ESP_SPIcontrol(
    spi,
    esp32_cs_pin,
    esp32_ready_pin,
    esp32_reset_pin,
)

if esp.status == adafruit_esp32spi.WL_IDLE_STATUS:
    print('ESP32 found and in idle mode')

print('Connecting to access point: %s' % str(SSID, 'utf8'))
while not esp.is_connected:
    try:
        esp.connect_AP(SSID, SSID_PASSWORD)
    except RuntimeError as e:
        print(e)
        continue

# At this point, you are connected and can do things like
# ping, do DNS lookups, and make HTTP requests

print("DNS lookup www.devdungeon.com: %s" %
      esp.pretty_ip(esp.get_host_by_name("www.devdungeon.com")))

for i in range(3):
    print("Ping www.devdungeon.com: %d ms" %
          esp.ping("www.devdungeon.com"))

TCP Sockets

You can use raw TCP sockets to send and receive data if desired. This is a lower level interface than using HTTP requests. See the next section for an example of making HTTP requests including with JSON parsing. Use this method only if you need to.

The adafruit-esp32spi-socket module includes support for TLS sockets.

I will not provide an example here but it is important to know it is an option, and you can refer to the official documentation for more detais.

Make an HTTP(S) request

Now that we know how to scan for networks, connect to a network, get IP addresses, and verify connectivity with a ping, it's time to do some actual HTTP requests.

It has the same two dependencies as the previous example:

  • adafruit_esp32spi/
  • adafruit_bus_device/

The main difference in this example versus the previous one is that it needs an additional import for import adafruit_esp32spi.adafruit_esp32spi_requests as requests and you need to call requests.set_interface(esp) before making any requests.

Once you have imported the requests library and set the interface, then you can call any of the request methods available including:

  • requests.request()
  • requests.head()
  • requests.get()
  • requests.post()
  • requests.delete()
  • requests.patch()
  • requests.put()

You can view the full API at https://circuitpython.readthedocs.io/projects/esp32spi/en/latest/api.html#adafruit-esp32spi-requests.

import board
from digitalio import DigitalInOut
import busio
from adafruit_esp32spi import adafruit_esp32spi
import adafruit_esp32spi.adafruit_esp32spi_requests as requests

SSID = 'wifi_network_name'
SSID_PASSWORD = 'wifi_password'

esp32_cs_pin = DigitalInOut(board.ESP_CS)
esp32_ready_pin = DigitalInOut(board.ESP_BUSY)
esp32_reset_pin = DigitalInOut(board.ESP_RESET)

spi = busio.SPI(board.SCK, board.MOSI, board.MISO)

esp = adafruit_esp32spi.ESP_SPIcontrol(
    spi,
    esp32_cs_pin,
    esp32_ready_pin,
    esp32_reset_pin,
)

if esp.status == adafruit_esp32spi.WL_IDLE_STATUS:
    print('ESP32 found and in idle mode')

print('Connecting to access point: %s' % str(SSID, 'utf8'))
while not esp.is_connected:
    try:
        esp.connect_AP(SSID, SSID_PASSWORD)
    except RuntimeError as e:
        print(e)
        continue

requests.set_interface(esp)

# It will fail if the response is too large
try:
    # You can optionally pass named paremeters for `data`, `json`, and `headers`
    response = requests.get('https://www.devdungeon.com/sitemap.xml', timeout=30)
    print(response.text)
    # or response.json() to parse json response
    response.close()
except Exception as e:
    print("Error making request: %s" % e)

Control Philips Hue lights

Adafruit has a CircuitPy library for for interacting with Philips Hue lights. You can view the documentation at https://circuitpython.readthedocs.io/projects/hue/en/latest/

Get all libraries from GitHub Adafruit CircuitPython Bundle.

For this example you will need at least these modules/packages in your lib directory:

  • adafruit_bus_device/
  • adafruit_esp32spi/
  • adafruit_hue.mpy
  • simpleio.mpy
  • neopixel.mpy

Before controlling the lights, you will need to get your Hue bridge IP address and get a valid username. The sample code linked below will actually help you set this up automatically, but it is good to know how to do it yourself.

To get your bridge IP, try visiting one of these URLs:

To generate a username, you will need to press the button on your bridge and then send a POST request like this (done from your desktop/laptop not from the PyPortal board):

import requests  # pip install requests

# Press the button on the bridge first
ip = '192.168.1.114'  # Or whatever your bridge IP address is
response = requests.post(
    'http://' + ip + '/api/',
    json={"devicetype":"my_test_app"}
)
print(response.content)

After you have the IP address and a valid username (which looks like a long string of random numbers and letters) then you can update your secrets.py file to include your hue_username and bridge_ip. Your secrets.py will look something similar to:

# Example secrets.py
secrets = {
    'ssid': 'our-family-wifi',
    'password': '$3cr3tPassword',
    'hue_username': 'cjrSC7oifRt21quCaDA2IeC7uTPL78AfWcftVnqM',
    'bridge_ip': '192.168.1.114',
}

Here is the example code to control the Hue lights:

https://github.com/adafruit/Adafruit_CircuitPython_Hue/blob/131cd3bad716df113f91855f854f1a3a6f5d8229/examples/hue_simpletest.py

Act like a USB HID device (keyboard, mouse, gamepad)

You can have the PyPortal board act like a generic USB HID device like a keyboard, mouse, or joystick/gamepad. I have written about this previously.

Check out the examples from NeoTrellis (AdaBox 010) Tutorial at https://www.devdungeon.com/content/neotrellis-m4-circuitpy-tutorial-adabox-010#toc-31 and check out the official source code for the library at https://github.com/adafruit/Adafruit_CircuitPython_HID.

Act like a MIDI device

You can have the PyPortal board act like a MIDI device over USB using the CircuitPy MIDI library. It can act as a sender or receiver of MIDI signals. I have written about this previously.

Check out the MIDI examples from NeoTrellis (AdaBox 010) Tutorial at https://www.devdungeon.com/content/neotrellis-m4-circuitpy-tutorial-adabox-010#toc-33 and check out the GitHub sources at https://github.com/adafruit/Adafruit_CircuitPython_MIDI.

Conclusion

After reading this you should have a solid understanding of the PyPortal board, its components, CircuitPy and how to use its components.

References

Advertisement

Advertisement