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 time
import os
import random
import board
import pwmio
import audiocore
import audiobusio
from adafruit_debouncer import Button
from digitalio import DigitalInOut, Direction, Pull
import neopixel
import adafruit_lis3dh
import simpleio
# CUSTOMIZE SENSITIVITY HERE: smaller numbers = more sensitive to motion
HIT_THRESHOLD = 120
SWING_THRESHOLD = 130
RED = (255, 0, 0)
YELLOW = (125, 255, 0)
GREEN = (0, 255, 0)
CYAN = (0, 125, 255)
BLUE = (0, 0, 255)
PURPLE = (125, 0, 255)
WHITE = (255, 255, 255)
COLORS = [RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE]
SABER_COLOR = 3
CLASH_COLOR = 6
# 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
wavs = []
for filename in os.listdir('/sounds'):
if filename.lower().endswith('.wav') and not filename.startswith('.'):
wavs.append("/sounds/"+filename)
wavs.sort()
print(wavs)
print(len(wavs))
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
def play_wav(num, loop=False):
"""
Play a WAV file in the 'sounds' directory.
:param name: partial file name string, complete name will be built around
this, e.g. passing 'foo' will play file 'sounds/foo.wav'.
:param loop: if True, sound will repeat indefinitely (until interrupted
by another sound).
"""
try:
n = wavs[num]
wave_file = open(n, "rb")
wave = audiocore.WaveFile(wave_file)
audio.play(wave, loop=loop)
except: # pylint: disable=bare-except
return
# external button
pin = DigitalInOut(board.EXTERNAL_BUTTON)
pin.direction = Direction.INPUT
pin.pull = Pull.UP
switch = Button(pin, long_duration_ms = 1000)
switch_state = False
# external neopixels
num_pixels = 100
pixels = neopixel.NeoPixel(board.EXTERNAL_NEOPIXELS, num_pixels, auto_write=True)
pixels.brightness = 0.8
# onboard LIS3DH
i2c = board.I2C()
int1 = DigitalInOut(board.ACCELEROMETER_INTERRUPT)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1)
# Accelerometer Range (can be 2_G, 4_G, 8_G, 16_G)
lis3dh.range = adafruit_lis3dh.RANGE_2_G
lis3dh.set_tap(1, HIT_THRESHOLD)
red_led = pwmio.PWMOut(board.D10)
green_led = pwmio.PWMOut(board.D11)
blue_led = pwmio.PWMOut(board.D12)
def set_rgb_led(color):
# convert from 0-255 (neopixel range) to 65535-0 (pwm range)
red_led.duty_cycle = int(simpleio.map_range(color[0], 0, 255, 65535, 0))
green_led.duty_cycle = int(simpleio.map_range(color[1], 0, 255, 65535, 0))
blue_led.duty_cycle = int(simpleio.map_range(color[2], 0, 255, 65535, 0))
set_rgb_led(COLORS[SABER_COLOR])
mode = 0
swing = False
hit = False
while True:
switch.update()
# startup
if mode == 0:
print(mode)
play_wav(0, loop=False)
for i in range(num_pixels):
pixels[i] = COLORS[SABER_COLOR]
pixels.show()
time.sleep(1)
play_wav(1, loop=True)
mode = 1
# default
elif mode == 1:
x, y, z = lis3dh.acceleration
accel_total = x * x + z * z
if lis3dh.tapped:
print("tapped")
mode = "hit"
elif accel_total >= SWING_THRESHOLD:
print("swing")
mode = "swing"
if switch.short_count == 1:
mode = 3
if switch.long_press:
audio.stop()
play_wav(19, loop=True)
print("change color")
mode = 5
# clash or move
elif mode == "hit":
audio.stop()
play_wav(random.randint(3, 10), loop=False)
while audio.playing:
pixels.fill(WHITE)
pixels.show()
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
play_wav(1, loop=True)
mode = 1
elif mode == "swing":
audio.stop()
play_wav(random.randint(11, 18), loop=False)
while audio.playing:
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
play_wav(1, loop=True)
mode = 1
# turn off
elif mode == 3:
audio.stop()
play_wav(2, loop=False)
for i in range(99, 0, -1):
pixels[i] = (0, 0, 0)
pixels.show()
time.sleep(1)
external_power.value = False
mode = 4
# go to startup from off
elif mode == 4:
if switch.short_count == 1:
external_power.value = True
mode = 0
# change color
elif mode == 5:
if switch.short_count == 1:
SABER_COLOR = (SABER_COLOR + 1) % 6
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
set_rgb_led(COLORS[SABER_COLOR])
if switch.long_press:
play_wav(1, loop=True)
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
set_rgb_led(COLORS[SABER_COLOR])
mode = 1
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
- sounds folder
- code.py
Your RP2040 Prop-Maker Feather CIRCUITPY drive should look like this after copying the lib folder, sounds folder and the code.py file.
How the CircuitPython Code Works
At the top of the code, you can customize a few attributes for your lightsaber. HIT_THRESHOLD and SWING_THRESHOLD affect the force needed to trigger a hit or swing for the lightsaber. The COLORS array has all of the available colors for the NeoPixels. You can change the value of SABER_COLOR to match the index for the color that you want your lightsaber to be. CLASH_COLOR is the color that the lightsaber turns when a hit is detected.
# CUSTOMIZE SENSITIVITY HERE: smaller numbers = more sensitive to motion HIT_THRESHOLD = 120 SWING_THRESHOLD = 130 RED = (255, 0, 0) YELLOW = (125, 255, 0) GREEN = (0, 255, 0) CYAN = (0, 125, 255) BLUE = (0, 0, 255) PURPLE = (125, 0, 255) WHITE = (255, 255, 255) COLORS = [RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE] SABER_COLOR = 3 CLASH_COLOR = 6
Sound FX
Audio is played back through the onboard I2S amp on the Feather. The play_wav() function opens a WAV file from the sounds folder and then plays it.
wavs = []
for filename in os.listdir('/sounds'):
if filename.lower().endswith('.wav') and not filename.startswith('.'):
wavs.append("/sounds/"+filename)
wavs.sort()
print(wavs)
print(len(wavs))
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
def play_wav(num, loop=False):
"""
Play a WAV file in the 'sounds' directory.
:param name: partial file name string, complete name will be built around
this, e.g. passing 'foo' will play file 'sounds/foo.wav'.
:param loop: if True, sound will repeat indefinitely (until interrupted
by another sound).
"""
try:
n = wavs[num]
wave_file = open(n, "rb")
wave = audiocore.WaveFile(wave_file)
audio.play(wave, loop=loop)
except: # pylint: disable=bare-except
return
Button, NeoPixels, and Accelerometer
The button and NeoPixels are connected to the external pins in the terminal block. The button uses the Debounce library so that long press and short press can be monitored in the loop. The onboard LIS3DH accelerometer is used to read movement and tap detection for the swing and hit functionality.
# external button pin = DigitalInOut(board.EXTERNAL_BUTTON) pin.direction = Direction.INPUT pin.pull = Pull.UP switch = Button(pin, long_duration_ms = 1000) switch_state = False # external neopixels num_pixels = 100 pixels = neopixel.NeoPixel(board.EXTERNAL_NEOPIXELS, num_pixels, auto_write=True) pixels.brightness = 0.8 # onboard LIS3DH i2c = board.I2C() int1 = DigitalInOut(board.ACCELEROMETER_INTERRUPT) lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1) lis3dh.range = adafruit_lis3dh.RANGE_2_G lis3dh.set_tap(1, HIT_THRESHOLD)
RGB LED
Inside the button is an RGB LED. The color of the LED matches the color of the NeoPixels with the help of the set_rgb_led() function.
red_led = pwmio.PWMOut(board.D10)
green_led = pwmio.PWMOut(board.D11)
blue_led = pwmio.PWMOut(board.D12)
def set_rgb_led(color):
# convert from 0-255 (neopixel range) to 65535-0 (pwm range)
red_led.duty_cycle = int(simpleio.map_range(color[0], 0, 255, 65535, 0))
green_led.duty_cycle = int(simpleio.map_range(color[1], 0, 255, 65535, 0))
blue_led.duty_cycle = int(simpleio.map_range(color[2], 0, 255, 65535, 0))
set_rgb_led(COLORS[SABER_COLOR])
The Loop
At the top of the loop, the button is monitored for any change in state with switch.update(). The rest of the loop is a series of states defined by the value of mode. When mode is 0, the lightsaber boots up by playing the start-up sound and having the NeoPixels light-up one by one.
switch.update()
# startup
if mode == 0:
print(mode)
play_wav(0, loop=False)
for i in range(num_pixels):
pixels[i] = COLORS[SABER_COLOR]
pixels.show()
time.sleep(1)
play_wav(1, loop=True)
mode = 1
Default Mode
When mode is 1, the lightsaber is idle, playing the idling sound on a loop. The LIS3DH is monitored for any changes that match or exceed the thresholds defined at the top of the code for a hit or swing.
# default
elif mode == 1:
x, y, z = lis3dh.acceleration
accel_total = x * x + z * z
if lis3dh.tapped:
print("tapped")
mode = "hit"
elif accel_total >= SWING_THRESHOLD:
print("swing")
mode = "swing"
The button is also monitored for a short press or long press. When a short press is detected, the lightsaber goes into mode 3, which is the shutdown mode. When a long press is detected, the lightsaber goes into mode 5, which lets you change the color of the lightsaber.
if switch.short_count == 1:
mode = 3
if switch.long_press:
audio.stop()
play_wav(19, loop=True)
print("change color")
mode = 5
Lightsaber Battle
If a swing or hit is detected, a randomized matching sound effect is played. In the case of a hit, the NeoPixels change in color to white. After the swing or hit has finished, the idle sound begins playing again on a loop.
# clash or move
elif mode == "hit":
audio.stop()
play_wav(random.randint(3, 10), loop=False)
while audio.playing:
pixels.fill(WHITE)
pixels.show()
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
play_wav(1, loop=True)
mode = 1
elif mode == "swing":
audio.stop()
play_wav(random.randint(11, 18), loop=False)
while audio.playing:
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
play_wav(1, loop=True)
mode = 1
Power Down
If a short press is detected in idle mode, the lightsaber powers down by playing the shutdown sound and turning off the NeoPixels one by one. The external power pin is also turned off to conserve battery power.
If a short press is detected in this mode, mode is set to 4 and the external power pin is turned back on. Then, mode is set to 0 to return to the start-up mode.
# turn off
elif mode == 3:
audio.stop()
play_wav(2, loop=False)
for i in range(99, 0, -1):
pixels[i] = (0, 0, 0)
pixels.show()
time.sleep(1)
external_power.value = False
mode = 4
# go to startup from off
elif mode == 4:
if switch.short_count == 1:
external_power.value = True
mode = 0
Choose Your Color
A long press in idle mode changes the mode to 5, which lets you change the color of the NeoPixels. With every short press, the selected index in the COLORS array advances by 1. The RGB LED also changes its color to match. You'll use a long press to exit color change mode with your new color and return to idle mode.
# change color
elif mode == 5:
if switch.short_count == 1:
SABER_COLOR = (SABER_COLOR + 1) % 6
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
set_rgb_led(COLORS[SABER_COLOR])
if switch.long_press:
play_wav(1, loop=True)
pixels.fill(COLORS[SABER_COLOR])
pixels.show()
set_rgb_led(COLORS[SABER_COLOR])
mode = 1
Page last edited January 22, 2025
Text editor powered by tinymce.