Maker.io main logo

PyPortal WFH Busy Sounds Simulator

2022-06-28 | By Adafruit Industries

License: See Original Project Programmers

Courtesy of Adafruit

Guide by Tim C

Overview

Use your PyPortal to play various notification sounds on a loop. Folks ‎will think you are mighty busy with all of those jingly noises going off!

Great for when you are "WFH" but really "TAN" (taking a nap)!‎

 

 

The loop of notification sounds will play automatically. Touch a ‎notification icon to add it to the loop. Touch the stop icon to stop ‎playing and empty the loop.‎

Parts

screen_1

External Speaker

The PyPortal does have a built-in speaker but it's fairly small. It may ‎be easier for people to "overhear" your notifications if you plug in a ‎bigger external speaker.‎

Luckily the PyPortal contains a plug-in adapter for just that!‎

If you do want to use the external speaker you must cut a small ‎jumper connection on the board in order to prevent the built-in ‎speaker from playing in addition to the external one.‎

speaker_2

external_3

CircuitPython Setup

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 code.py and other project ‎files to your CIRCUITPY drive using File Explorer or Finder ‎‎(depending on your operating system).‎

Download Project Bundle

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:‎

files_4

Code

The program code is shown below:‎

Download Project Bundle

Copy Code
# 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
WAIT_TIME = 3.0

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

# last time that we played a sample
LAST_PLAY_TIME = 0

CUR_LOOP_INDEX = 0

# 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
LAST_PRESS_TIME = -1

# Was any icon touched last iteration.
# Used for debouncing.
WAS_TOUCHED = False


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.show(main_group)

# Touchscreen initialization
ts = adafruit_touchscreen.Touchscreen(
board.TOUCH_XL,
board.TOUCH_XR,
board.TOUCH_YD,
board.TOUCH_YU,
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(
bg_bitmap,
pixel_shader=getattr(bg_bitmap, "pixel_shader", displayio.ColorConverter()),
)

# add it to the group that is showing
main_group.append(bg_tile_grid)

# grid to hold the icons
layout = GridLayout(
x=0,
y=0,
width=320,
height=240,
grid_size=(4, 3),
cell_padding=20,
)

# initialize the icons in the grid
for i, icon in enumerate(_icons):
icon_widget = IconWidget(
icon[0],
icon[1],
x=0,
y=0,
on_disk=True,
transparent_index=0,
label_background=0x888888,
)

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
main_group.append(layout)


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
break

# nothing is touched
else:
# 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
check_for_touch(_now)

# 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
audio.play(wave)

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

# check for and process touch events
check_for_touch(_now)

# after done playing. deinit audio output
audio.deinit()

# increment index counter
CUR_LOOP_INDEX = next_index()

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

View on GitHub

Code Walk-Through

Configuration Variable

You can update the WAIT_TIME variable to a different value if you want. ‎This variable represents the amount of time in seconds between ‎each notification sample playback. Lower times means it will play ‎faster; higher times means it will play slower. The default is 3.0, but ‎feel free to experiment with different times.‎

Download File‎

Copy Code
WAIT_TIME = 3.0

Program Variables

These variables are used for various things in the program, they ‎aren't intended for you to change them during the normal operation. ‎The program will update their value when necessary.‎

Download File‎

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

# last time that we played a sample
LAST_PLAY_TIME = 0

# current index that we are on
CUR_LOOP_INDEX = 0

# 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
LAST_PRESS_TIME = -1

# Was any icon touched last iteration.
# Used for debouncing.
WAS_TOUCHED = False

Icons List

This list holds tuples that represent each icon that will be shown on ‎the screen. The tuples contain the string label, the bmp image file to ‎show, and the wave audio file to play. If you wanted to re-purpose ‎this project as a looping soundboard with other sounds, you could ‎change the values here.‎

Download File

Copy Code
# 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", ""),
]

Helper Functions

The program contains two helper functions: ‎

next_index() - This function will return the next valid index for a ‎notification sample in the LOOP list. If the current index is the last ‎available one, then it will wrap back around to 0.‎

check_for_touch(_now) - This function accepts a parameter whose ‎value will be the current time as fetched from time.monotonic(). It will ‎check the touch screen to see if there are any touch events on icons ‎currently and take the appropriate action if there are. The time is ‎used to enforce a cool-down behavior so it will not register several ‎touch events rapidly. This will get called during the main loop, and ‎during time that an audio sample is playing.‎

User Interface

The GUI is created with displayio. It uses a GridLayout object loaded ‎up with IconWidgets from a for loop that references the above icons ‎list.‎

‎Download File‎

Copy Code
# grid to hold the icons
layout = GridLayout(
x=0,
y=0,
width=320,
height=240,
grid_size=(4, 3),
cell_padding=20,
)

# initialize the icons in the grid
for i, icon in enumerate(_icons):
icon_widget = IconWidget(
icon[0],
icon[1],
x=0,
y=0,
on_disk=True,
transparent_index=0,
label_background=0x888888,
)

Main Loop

The main loop contains two high level actions:‎

‎1) Check if the user has touched any icons. If they touch a notification ‎icon, the index for the one they touched gets added to the LOOP list. If ‎they touch the stop icon, the LOOP list is emptied and ‎the CUR_LOOP_INDEX reset to 0.

‎‎2) Check the current time, if the WAIT_TIME has passed then load and ‎play the current sample from the LOOP list. Set the index to the next ‎one for next time. During the time that a sample is being played ‎there is an internal loop that will carry out action 1, so that touch ‎events during the sample playback get handled appropriately.‎

Further Detail

The source code is thoroughly commented to explain what the ‎statements are doing. You can read through the code and ‎comments to gain a deeper understanding of how it functions or ‎modify parts of it to suit your needs.‎

‎Download Project Bundle‎

Copy Code
# 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
WAIT_TIME = 3.0

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

# last time that we played a sample
LAST_PLAY_TIME = 0

CUR_LOOP_INDEX = 0

# 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
LAST_PRESS_TIME = -1

# Was any icon touched last iteration.
# Used for debouncing.
WAS_TOUCHED = False


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.show(main_group)

# Touchscreen initialization
ts = adafruit_touchscreen.Touchscreen(
board.TOUCH_XL,
board.TOUCH_XR,
board.TOUCH_YD,
board.TOUCH_YU,
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(
bg_bitmap,
pixel_shader=getattr(bg_bitmap, "pixel_shader", displayio.ColorConverter()),
)

# add it to the group that is showing
main_group.append(bg_tile_grid)

# grid to hold the icons
layout = GridLayout(
x=0,
y=0,
width=320,
height=240,
grid_size=(4, 3),
cell_padding=20,
)

# initialize the icons in the grid
for i, icon in enumerate(_icons):
icon_widget = IconWidget(
icon[0],
icon[1],
x=0,
y=0,
on_disk=True,
transparent_index=0,
label_background=0x888888,
)

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
main_group.append(layout)


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
break

# nothing is touched
else:
# 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
check_for_touch(_now)

# 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
audio.play(wave)

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

# check for and process touch events
check_for_touch(_now)

# after done playing. deinit audio output
audio.deinit()

# increment index counter
CUR_LOOP_INDEX = next_index()

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

View on GitHub

制造商零件编号 4116
PYPORTAL - CIRCUITPYTHON POWERED
Adafruit Industries LLC
¥447.30
Details
制造商零件编号 4444
ADAFRUIT PYPORTAL TITANO
Adafruit Industries LLC
¥488.00
Details
制造商零件编号 4146
PYPORTAL DESKTOP STAND ENCLOSURE
Adafruit Industries LLC
¥85.61
Details
制造商零件编号 4227
SPEAKER 8OHM 1W TOP PORT 96DB
Adafruit Industries LLC
¥15.87
Details
Add all DigiKey Parts to Cart
TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum