Code Setup

CircuitPython Library Installation

First make sure you are running the latest version of Adafruit CircuitPython for your board.

Next you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle matching your version of CircuitPython. PyPortal requires at least CircuitPython version 4.0.0.

Before continuing make sure your board's lib folder has the following files and folders copied over:

  • adafruit_bitmap_font
  • adafruit_io
  • adafruit_sdcard.mpy
  • adafruit_bus_device
  • adafruit_logging.mpy
  • adafruit_seesaw
  • adafruit_display_text
  • adafruit_minimqtt.mpy
  • adafruit_touchscreen.mpy
  • adafruit_esp32spi
  • adafruit_pyportal.mpy
  • neopixel.mpy
  • adafruit_imageload
  • adafruit_requests.mpy
  • simpleio.mpy

Add CircuitPython Code and Project Assets

In the embedded code element below, click on the Download: Project Zip link, and save the .zip archive file to your computer.

Then, uncompress the .zip file, it will unpack to a folder named pyportal_pet_planter.

Copy the contents of pyportal_pet_planter directory to your PyPortal's CIRCUITPY drive.

import time

import board
import busio
from digitalio import DigitalInOut
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager
import adafruit_imageload
import displayio
import neopixel
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label
from adafruit_io.adafruit_io import IO_MQTT
import adafruit_minimqtt as MQTT
from adafruit_pyportal import PyPortal
from adafruit_seesaw.seesaw import Seesaw
from simpleio import map_range

#---| User Config |---------------

# How often to poll the soil sensor, in seconds

# How often to send data to, in minutes

# Maximum soil moisture measurement

# Minimum soil moisture measurement

#---| End User Config |---------------

# Background image
BACKGROUND = "/images/roots.bmp"
# Icons for water level and temperature
ICON_LEVEL = "/images/icon-wetness.bmp"
ICON_TEMP = "/images/icon-temp.bmp"
WATER_COLOR = 0x16549E

# Audio files
wav_water_high = "/sounds/water-high.wav"
wav_water_low = "/sounds/water-low.wav"

# the current working directory (where this file is)
cwd = ("/"+__file__).rsplit('/', 1)[0]

# Get wifi details and more from a file
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in, please add them there!")

# Set up i2c bus
i2c_bus = busio.I2C(board.SCL, board.SDA)

# Initialize soil sensor (s.s)
ss = Seesaw(i2c_bus, addr=0x36)

# PyPortal ESP32 AirLift Pins
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)

spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)

# Initialize PyPortal Display
display = board.DISPLAY

WIDTH = board.DISPLAY.width
HEIGHT = board.DISPLAY.height

# Initialize new PyPortal object
pyportal = PyPortal(esp=esp,

# Set backlight level

# Create a new DisplayIO group
splash = displayio.Group(max_size=15)

# show splash group

# Palette for water bitmap
palette = displayio.Palette(2)
palette[0] = 0x000000
palette[1] = WATER_COLOR

# Create water bitmap
water_bmp = displayio.Bitmap(display.width, display.height, len(palette))
water = displayio.TileGrid(water_bmp, pixel_shader=palette)

print("drawing background..")
# Load background image
    bg_bitmap, bg_palette = adafruit_imageload.load(BACKGROUND,
# Or just use solid color
except (OSError, TypeError):
    BACKGROUND = BACKGROUND if isinstance(BACKGROUND, int) else 0x000000
    bg_bitmap = displayio.Bitmap(display.width, display.height, 1)
    bg_palette = displayio.Palette(1)
    bg_palette[0] = BACKGROUND
background = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)

# Add background to display

print('loading fonts...')
# Fonts within /fonts/ folder
font = cwd+"/fonts/GothamBlack-50.bdf"
font_small = cwd+"/fonts/GothamBlack-25.bdf"

# pylint: disable=syntax-error
data_glyphs = b'0123456789FC-* '
font = bitmap_font.load_font(font)

font_small = bitmap_font.load_font(font_small)
full_glyphs = b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.: '

# Label to display Adafruit IO status
label_status = Label(font_small, max_glyphs=20)
label_status.x = 305
label_status.y = 10

# Create a label to display the temperature
label_temp = Label(font, max_glyphs=4)
label_temp.x = 35
label_temp.y = 300

# Create a label to display the water level
label_level = Label(font, max_glyphs=4)
label_level.x = display.width - 130
label_level.y = 300

print('loading icons...')
# Load temperature icon
icon_tmp_bitmap, icon_palette = adafruit_imageload.load(ICON_TEMP,
icon_tmp_bitmap = displayio.TileGrid(icon_tmp_bitmap,
                                     x=0, y=280)

# Load level icon
icon_lvl_bitmap, icon_palette = adafruit_imageload.load(ICON_LEVEL,
icon_lvl_bitmap = displayio.TileGrid(icon_lvl_bitmap,
                                     x=315, y=280)

# Connect to WiFi
label_status.text = "Connecting..."
while not esp.is_connected:
    except RuntimeError as e:
        print("could not connect to AP, retrying: ",e)
print("Connected to WiFi!")

# Initialize MQTT interface with the esp interface
MQTT.set_socket(socket, esp)

# Initialize a new MQTT Client object
mqtt_client = MQTT.MQTT(broker="",

# Adafruit IO Callback Methods
# pylint: disable=unused-argument
def connected(client):
    # Connected function will be called when the client is connected to Adafruit IO.
    print('Connected to Adafruit IO!')

def subscribe(client, userdata, topic, granted_qos):
    # This method is called when the client subscribes to a new feed.
    print('Subscribed to {0} with QOS level {1}'.format(topic, granted_qos))

# pylint: disable=unused-argument
def disconnected(client):
    # Disconnected function will be called if the client disconnects
    # from the Adafruit IO MQTT broker.
    print("Disconnected from Adafruit IO!")

# Initialize an Adafruit IO MQTT Client
io = IO_MQTT(mqtt_client)

# Connect the callback methods defined above to the Adafruit IO MQTT Client
io.on_connect = connected
io.on_subscribe = subscribe
io.on_disconnect = disconnected

# Connect to Adafruit IO
print("Connecting to Adafruit IO...")
label_status.text = " "

fill_val = 0.0
def fill_water(fill_percent):
    """Fills the background water.
    :param float fill_percent: Percentage of the display to fill.

    assert fill_percent <= 1.0, "Water fill value may not be > 100%"
    # pylint: disable=global-statement
    global fill_val

    if fill_val > fill_percent:
        for _y in range(int((board.DISPLAY.height-1) - ((board.DISPLAY.height-1)*fill_val)),
                        int((board.DISPLAY.height-1) - ((board.DISPLAY.height-1)*fill_percent))):
            for _x in range(1, board.DISPLAY.width-1):
                water_bmp[_x, _y] = 0
        for _y in range(board.DISPLAY.height-1,
                        (board.DISPLAY.height-1) - ((board.DISPLAY.height-1)*fill_percent), -1):
            for _x in range(1, board.DISPLAY.width-1):
                water_bmp[_x, _y] = 1
    fill_val = fill_percent

def display_temperature(temp_val, is_celsius=False):
    """Displays the temperature from the STEMMA soil sensor
    on the PyPortal Titano.
    :param float temp: Temperature value.
    :param bool is_celsius:

    if not is_celsius:
        temp_val = (temp_val * 9 / 5) + 32 - 15
        print('Temperature: %0.0fF'%temp_val)
        label_temp.text = '%0.0fF'%temp_val
        return int(temp_val)
        print('Temperature: %0.0fC'%temp_val)
        label_temp.text = '%0.0fC'%temp_val
        return int(temp_val)

# initial reference time
initial = time.monotonic()
while True:
    # Explicitly pump the message loop
    # to keep the connection active
    except (ValueError, RuntimeError) as e:
        print("Failed to get data, retrying...\n", e)
    now = time.monotonic()

    print("reading soil sensor...")
    # Read capactive
    moisture = ss.moisture_read()
    label_level.text = str(moisture)

    # Convert into percentage for filling the screen
    moisture_percentage = map_range(float(moisture), SOIL_LEVEL_MIN, SOIL_LEVEL_MAX, 0.0, 1.0)

    # Read temperature
    temp = ss.get_temp()
    temp = display_temperature(temp)

    # fill display
    print("filling disp..")
    print("disp filled..")

    print("temp: " + str(temp) + "  moisture: " + str(moisture))

    # Play water level alarms
    if moisture <= SOIL_LEVEL_MIN:
        print("Playing low water level warning...")
    elif moisture >= SOIL_LEVEL_MAX:
        print("Playing high water level warning...")

    if now - initial > (DELAY_PUBLISH * 60):
            print("Publishing data to Adafruit IO...")
            label_status.text = "Sending to IO..."
            io.publish("moisture", moisture)
            io.publish("temperature", temp)
            label_status.text = "Data Sent!"

            # reset timer
            initial = now
        except (ValueError, RuntimeError) as e:
            label_status.text = "ERROR!"
            print("Failed to get data, retrying...\n", e)

This is what the final contents of the CIRCUITPY drive will look like:

Install the Mu Editor 

This guide requires you to edit and interact with CircuitPython code. While you can use any text editor of your choosing, Mu is a simple code editor that works with the Adafruit CircuitPython boards. It's written in Python and works on Windows, MacOS, Linux and Raspberry Pi. The serial console is built right in, so you get immediate feedback from your board's serial output!

Before proceeding, click the button below to install the Mu Editor. There are versions for PC, mac, and Linux.

Secrets File Setup

Open the file on your CIRCUITPY drive using Mu or your own text editor. You're going to edit the file to enter your local WiFi credentials along with your Adafruit IO credentials.

Make the following changes to the code below in the file:

  • Replace MY_WIFI_SSID with the name of your WiFi SSID
  • Replace MY_WIFI_PASSWORD with your WiFi's password
  • Replace MY_ADAFRIOT_IO_USERNAME with your Adafruit IO username.
  • Replace MY_LONG_ADAFRUIT_IO_KEY with your Adafruit IO Active Key.
Download: file
# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it
# which would be not great. So, instead, keep it all in this one file and
# keep it a secret.

secrets = {
    'ssid' : 'MY_WIFI_SSID',             
    'password' : 'MY_WIFI_PASSWORD',       
    'timezone' : "America/New_York",
    'aio_username' : 'MY_ADAFRIOT_IO_USERNAME',
    'aio_key' : 'MY_LONG_ADAFRUIT_IO_KEY',
This guide was first published on Feb 05, 2020. It was last updated on Feb 05, 2020. This page (Code Setup) was last updated on May 24, 2020.