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:
- EagleCAD PCB files
- Schematic files
- Fritzing PyPortal object
- Laser cutting files for stand
- CircuitPython source
- CircuitPython PyPortal libary
- Adafruit CircuitPython libraries
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
andboard.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:
- PuTTy (Windows/Linux)
- MobaXTerm (Windows)
- GNU Screen (Linux/Mac)
- Arduino IDE (Windows/Linux/Mac)
- Android Serial Console (Android)
- Android Serial USB Terminal (Android)
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:
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
- PyPortal product on Adafruit
- AdaBox 011 Projects and Tutorials
- PyPortal CircuitPy firmware
- Adafruit tutorial on updating ESP32 firmare
- ESP32 firmware download
- esptool
- PyPortal Tutorials
- PyPortal Pinouts
- PyPortal EagleCAD PCB files
- Schematic files
- Fritzing PyPortal object
- Laser cutting files for stand
- CircuitPython PyPortal library on GitHub
- CircuitPython source on GitHub
- Adafruit CircuitPython libraries bundle
- PyPortal CircuitPy Firmware .uf2
- PyPortal default example files
- CircuitPy libraries documentation