Once you've finished setting up your RP2040 Prop-Maker Feather with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import os
import random
import board
import audiocore
import audiobusio
import audiomixer
import pwmio
import neopixel
import adafruit_lis3dh
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
from digitalio import DigitalInOut, Direction, Pull
from adafruit_motor import servo
from adafruit_led_animation.animation.comet import Comet
from adafruit_led_animation.animation.pulse import Pulse
from adafruit_led_animation.animation.sparkle import Sparkle
from adafruit_led_animation.color import RED, BLUE, BLACK
# enable external power pin
# provides power to the external components
external_power = DigitalInOut(board.EXTERNAL_POWER)
external_power.direction = Direction.OUTPUT
external_power.value = True
i2c = board.I2C()
int1 = DigitalInOut(board.ACCELEROMETER_INTERRUPT)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1)
lis3dh.range = adafruit_lis3dh.RANGE_2_G
switch = DigitalInOut(board.EXTERNAL_BUTTON)
switch.direction = Direction.INPUT
switch.pull = Pull.UP
switch_state = False
wavs = []
for filename in os.listdir('/WAVs'):
if filename.lower().endswith('.wav') and not filename.startswith('.'):
wavs.append("/WAVs/"+filename)
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=32768)
mixer.voice[0].level = 1
track_number = 0
wav_filename = wavs[track_number]
wav_file = open(wav_filename, "rb")
wave = audiocore.WaveFile(wav_file)
audio.play(mixer)
mixer.voice[0].play(wave)
def open_audio(num):
n = wavs[num]
f = open(n, "rb")
w = audiocore.WaveFile(f)
return w
PIXEL_PIN = board.EXTERNAL_NEOPIXELS
SERVO_PIN = board.EXTERNAL_SERVO
NUM_PIXELS = 8
ORDER = neopixel.GRB
BRIGHTNESS = 0.3
PWM = pwmio.PWMOut(SERVO_PIN, duty_cycle=2 ** 15, frequency=50)
SERVO = servo.Servo(PWM)
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel.brightness = 1
PIXELS = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, auto_write=False,
pixel_order=ORDER)
LARSON = Comet(PIXELS, bounce=True, speed=0.07,
tail_length=NUM_PIXELS//2,
color=(BLUE[0] * BRIGHTNESS,
BLUE[1] * BRIGHTNESS,
BLUE[2] * BRIGHTNESS))
pulse = Pulse(PIXELS, speed=0.05,
color=(BLUE[0] * BRIGHTNESS,
BLUE[1] * BRIGHTNESS,
BLUE[2] * BRIGHTNESS), period=3)
sparkle = Sparkle(PIXELS, speed=0.2,
color=(RED[0] * BRIGHTNESS,
RED[1] * BRIGHTNESS,
RED[2] * BRIGHTNESS), num_sparkles=10)
SERVO.angle = POSITION = NEXT_POSITION = 90
MOVING = False
START_TIME = ticks_ms()
DURATION = 1000
adabot_talk = False
clock = ticks_ms()
prop_time = 1000
adabot_nap = False
mixer.voice[0].play(wave)
while mixer.playing:
LARSON.animate()
while True:
if ticks_diff(ticks_ms(), clock) >= prop_time:
x, y, z = [
value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration
]
if z > 0.9:
adabot_nap = True
SERVO.angle = POSITION = NEXT_POSITION = 90
else:
adabot_nap = False
if not adabot_nap:
MOVING = not MOVING
if MOVING:
POSITION = NEXT_POSITION
while abs(POSITION - NEXT_POSITION) < 10:
NEXT_POSITION = random.uniform(0, 180)
DURATION = 0.2 + 0.6 * abs(POSITION - NEXT_POSITION) / 180
else:
SERVO.angle = NEXT_POSITION
DURATION = random.uniform(0.5, 2.5)
clock = ticks_add(clock, prop_time)
if MOVING:
FRACTION = 0.0 / DURATION
FRACTION = (3 * FRACTION ** 2) - (2 * FRACTION ** 3)
SERVO.angle = POSITION + (NEXT_POSITION - POSITION) * FRACTION
if adabot_talk:
wave = open_audio(random.randint(1, 17))
mixer.voice[0].play(wave)
while mixer.playing:
sparkle.animate()
if not mixer.playing:
adabot_talk = False
PIXELS.fill(BLACK)
PIXELS.show()
elif adabot_nap:
pulse.animate()
else:
LARSON.animate()
if not switch.value and switch_state is False:
PIXELS.fill(BLACK)
PIXELS.show()
adabot_talk = True
switch_state = True
if switch.value and switch_state is True:
switch_state = False
Upload the Code and Libraries to the RP2040 Prop-Maker Feather
After downloading the Project Bundle, plug your RP2040 Prop-Maker Feather into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the RP2040 Prop-Maker Feather's CIRCUITPY drive.
- lib folder
- WAVs folder
- code.py
Your RP2040 Prop-Maker Feather CIRCUITPY drive should look like this after copying the lib folder, WAVs folder and the code.py file.
How the CircuitPython Code Works
The code begins by enabling the EXTERNAL_POWER pin on the Feather. This pin enables power to all of the external components. Next, the onboard LIS3DH accelerometer is instantiated over I2C and the EXTERNAL_BUTTON pin is setup as an input.
# enable external power pin # provides power to the external components external_power = DigitalInOut(board.EXTERNAL_POWER) external_power.direction = Direction.OUTPUT external_power.value = True i2c = board.I2C() int1 = DigitalInOut(board.ACCELEROMETER_INTERRUPT) lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1) lis3dh.range = adafruit_lis3dh.RANGE_2_G switch = DigitalInOut(board.EXTERNAL_BUTTON) switch.direction = Direction.INPUT switch.pull = Pull.UP switch_state = False
Audio!
Next is getting the audio ready. All of the audio files are in a folder called WAVs. These file names are added to the wavs array. The onboard I2S amp is being used for audio playback along with a Mixer. The Mixer lets you control playback and volume level in software.
Note: to make your own compatible wav files, check out this guide.
The open_audio() function is used in the main loop to play a random audio file from the WAVs folder. You'll pass a track number to the function, which will return an opened WaveFile to play through the Mixer.
wavs = []
for filename in os.listdir('/WAVs'):
if filename.lower().endswith('.wav') and not filename.startswith('.'):
wavs.append("/WAVs/"+filename)
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=32768)
mixer.voice[0].level = 1
track_number = 0
wav_filename = wavs[track_number]
wav_file = open(wav_filename, "rb")
wave = audiocore.WaveFile(wav_file)
audio.play(mixer)
mixer.voice[0].play(wave)
def open_audio(num):
n = wavs[num]
f = open(n, "rb")
w = audiocore.WaveFile(f)
return w
Lights!
The NeoPixel stick is attached to the EXTERNAL_NEOPIXELS terminal block pin. The NeoPixels are using the LED Animations library to show fancy effects. LARSON is a modified Comet animation to mimic a Larson scanner effect and Pulse shows a calming blink that fades in and out.
PIXEL_PIN = board.EXTERNAL_NEOPIXELS
NUM_PIXELS = 8
ORDER = neopixel.GRB
BRIGHTNESS = 0.6
PIXELS = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, auto_write=False,
pixel_order=ORDER)
LARSON = Comet(PIXELS, bounce=True, speed=0.6/NUM_PIXELS,
tail_length=NUM_PIXELS//2,
color=(RED[0] * BRIGHTNESS,
RED[1] * BRIGHTNESS,
RED[2] * BRIGHTNESS))
pulse = Pulse(PIXELS, speed=0.1, color=BLUE, period=3)
SERVO_PIN = board.EXTERNAL_SERVO PWM = pwmio.PWMOut(SERVO_PIN, duty_cycle=2 ** 15, frequency=50) SERVO = servo.Servo(PWM)
Variables and Time
The adafruit_ticks library is used for non-blocking time tracking in the loop. A few Boolean states are used to track various modes in the loop.
SERVO.angle = POSITION = NEXT_POSITION = 90 MOVING = False START_TIME = ticks_ms() DURATION = 1000 adabot_talk = False clock = ticks_ms() prop_time = 1000 adabot_nap = False
Adabot Speaks
Right before the loop, Adabot says hello. While the Mixer is playing audio, the NeoPixel mouth shows the LARSON animation in RED.
mixer.voice[0].play(wave)
while mixer.playing:
LARSON.animate()
The Loop
Every second, the LIS3DH is read. If the Z axis registers that Adabot is lying down, then the adabot_nap state is set to True. When Adabot is napping, the NeoPixels display the LARSON animation in BLUE and the servo stops moving.
if ticks_diff(ticks_ms(), clock) >= prop_time:
x, y, z = [
value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration
]
if z > 0.9:
adabot_nap = True
SERVO.angle = POSITION = NEXT_POSITION = 90
LARSON.color=(BLUE[0] * BRIGHTNESS,
BLUE[1] * BRIGHTNESS,
BLUE[2] * BRIGHTNESS)
If Adabot is not napping, then the servo alternates between moving and not moving, essentially moving every two seconds. The movement is random and tracks its previous position.
if not adabot_nap:
MOVING = not MOVING
if MOVING:
POSITION = NEXT_POSITION
while abs(POSITION - NEXT_POSITION) < 10:
NEXT_POSITION = random.uniform(0, 180)
DURATION = 0.2 + 0.6 * abs(POSITION - NEXT_POSITION) / 180
else:
SERVO.angle = NEXT_POSITION
DURATION = random.uniform(0.5, 2.5)
clock = ticks_add(clock, prop_time)
if MOVING:
FRACTION = 0.0 / DURATION
FRACTION = (3 * FRACTION ** 2) - (2 * FRACTION ** 3)
SERVO.angle = POSITION + (NEXT_POSITION - POSITION) * FRACTION
Chat with Adabot
When you press the button, Adabot will speak. adabot_talk is set to True and the open_audio function is used to load up one of the Adabot phrases from the WAVs folder. While the Mixer is playing the audio, the NeoPixels show the LARSON animation in RED. After the audio finishes, adabot_talk is set to False and the pulse animation begins again.
if adabot_talk:
wave = open_audio(random.randint(1, 7))
mixer.voice[0].play(wave)
while mixer.playing:
LARSON.animate()
if not mixer.playing:
adabot_talk = False
PIXELS.fill(BLACK)
PIXELS.show()
elif adabot_nap:
LARSON.animate()
else:
pulse.animate()
if not switch.value and switch_state is False:
PIXELS.fill(BLACK)
PIXELS.show()
adabot_talk = True
switch_state = True
if switch.value and switch_state is True:
switch_state = False
Page last edited January 21, 2025
Text editor powered by tinymce.