Plug the PyPortal into your computer with a known good USB cable (not a tint charge only cable). The PyPortal will appear to your computer as a flash drive named CIRCUITPY.

Download the project files with the Download Project Bundle button below. Unzip the file and copy/paste the and other project files to your CIRCUITPY drive using File Explorer or Finder (depending on your operating system).

Drive Structure

After copying the files, your drive should look like the listing below. It can contain other files as well, but must contain these at a minimum:



The program code is shown below:

# SPDX-FileCopyrightText: 2020 Tim C, written for Adafruit Industries
# SPDX-License-Identifier: Unlicense
PyPortal implementation of Busy Simulator notification sound looper.
import time
import board
import displayio
import adafruit_touchscreen
from adafruit_displayio_layout.layouts.grid_layout import GridLayout
from adafruit_displayio_layout.widgets.icon_widget import IconWidget
from audiocore import WaveFile
from audioio import AudioOut

# How many seconds to wait between playing samples
# Lower time means it will play faster

# List that will hold indexes of notification samples to play
LOOP = []

# last time that we played a sample


# touch events must have at least this long between them
COOLDOWN_TIME = 0.25  # seconds

# last time that the display was touched
# used for debouncing and cooldown enforcement

# Was any icon touched last iteration.
# Used for debouncing.

def next_index():
    return the next index in the LOOP that should get played
    if CUR_LOOP_INDEX + 1 >= len(LOOP):
        return 0

    return CUR_LOOP_INDEX + 1

# list of icons to show
# each entry is a tuple containing:
# (Icon Label, Icon BMP image file, Notification sample wav file
_icons = [
    ("Outlook", "icons/outlook.bmp", "sounds/outlook.wav"),
    ("Phone", "icons/phone.bmp", "sounds/phone.wav"),
    ("Skype", "icons/skype.bmp", "sounds/skype.wav"),
    ("Teams", "icons/teams.bmp", "sounds/teams.wav"),
    ("Discord", "icons/discord.bmp", "sounds/discord.wav"),
    ("Apple Mail", "icons/applemail.bmp", "sounds/applemail.wav"),
    ("iMessage", "icons/imessage.bmp", "sounds/imessage.wav"),
    ("Slack", "icons/slack.bmp", "sounds/slack.wav"),
    ("G Calendar", "icons/gcal.bmp", "sounds/RE.wav"),
    ("G Chat", "icons/gchat.bmp", "sounds/gchat.wav"),
    ("Stop", "icons/stop.bmp", ""),

# Make the display context.
display = board.DISPLAY
main_group = displayio.Group()
display.root_group = main_group

# Touchscreen initialization
ts = adafruit_touchscreen.Touchscreen(
    calibration=((5200, 59000), (5800, 57000)),
    size=(display.width, display.height),

# Setup the file as the bitmap data source
bg_bitmap = displayio.OnDiskBitmap("busysim_background.bmp")

# Create a TileGrid to hold the bitmap
bg_tile_grid = displayio.TileGrid(
    pixel_shader=getattr(bg_bitmap, "pixel_shader", displayio.ColorConverter()),

# add it to the group that is showing

# grid to hold the icons
layout = GridLayout(
    grid_size=(4, 3),

# initialize the icons in the grid
for i, icon in enumerate(_icons):
    icon_widget = IconWidget(

    layout.add_content(icon_widget, grid_position=(i % 4, i // 4), cell_size=(1, 1))

# add the grid to the group showing on the display

def check_for_touch(_now):
    Check the touchscreen and do any actions necessary if an
    icon has been touched. Applies debouncing and cool down
    enforcement to filter out unneeded touch events.

    :param int _now: The current time in seconds. Used for cool down enforcement
    # pylint: disable=global-statement, too-many-nested-blocks, consider-using-enumerate
    global CUR_LOOP_INDEX
    global LOOP
    global LAST_PRESS_TIME
    global WAS_TOUCHED

    # read the touch data
    touch_point = ts.touch_point

    # if anything is touched
    if touch_point:
        # if the touch just began. We ignore further events until
        # after the touch has been lifted
        if not WAS_TOUCHED:

            # set the variable so we know to ignore future events until
            # touch is released
            WAS_TOUCHED = True

            # if it has been long enough time since previous touch event
            if _now - LAST_PRESS_TIME > COOLDOWN_TIME:

                LAST_PRESS_TIME = time.monotonic()

                # loop over the icons
                for _ in range(len(_icons)):
                    # lookup current icon in the grid layout
                    cur_icon = layout.get_cell((_ % 4, _ // 4))

                    # check if it's being touched
                    if cur_icon.contains(touch_point):
                        print("icon {} touched".format(_))

                        # if it's the stop icon
                        if _icons[_][0] == "Stop":

                            # empty out the loop
                            LOOP = []

                            # set current index back to 0
                            CUR_LOOP_INDEX = 0

                        else:  # any other icon
                            # insert the touched icons sample index into the loop
                            LOOP.insert(CUR_LOOP_INDEX, _)

                        # print(LOOP)

                        # break out of the for loop.
                        # if current icon is being touched then no others can be

    # nothing is touched
        # set variable back to false for debouncing
        WAS_TOUCHED = False

# main loop
while True:
    # store current time in variable for cool down enforcement
    _now = time.monotonic()

    # check for and process touch events

    # if it's time to play a sample
    if LAST_PLAY_TIME + WAIT_TIME <= _now:
        # print("time to play")

        # if there are any samples in the loop
        if len(LOOP) > 0:

            # open the sample wav file
            with open(_icons[LOOP[CUR_LOOP_INDEX]][2], "rb") as wave_file:
                print("playing: {}".format(_icons[LOOP[CUR_LOOP_INDEX]][2]))

                # initialize audio output pin
                audio = AudioOut(board.AUDIO_OUT)

                # initialize WaveFile object
                wave = WaveFile(wave_file)

                # play it

                # while it's still playing
                while audio.playing:
                    # update time variable
                    _now = time.monotonic()

                    # check for and process touch events

                # after done playing. deinit audio output

                # increment index counter
                CUR_LOOP_INDEX = next_index()

        # update variable for last time we attempted to play sample
        LAST_PLAY_TIME = _now

This guide was first published on Jan 11, 2022. It was last updated on May 25, 2024.

This page (CircuitPython Setup) was last updated on May 24, 2024.

Text editor powered by tinymce.