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
Text editor powered by tinymce.