The code is easily adaptable and because it's CircuitPython, you can make an edit, hit save and quickly see your changes without having to compile the code!
# SPDX-FileCopyrightText: 2019 Kattni Rembor for Adafruit Industries # SPDX-FileCopyrightText: 2019 Limor Fried for Adafruit Industries # # SPDX-License-Identifier: MIT """ RP2040 Prop-Maker Feather Energy Sword Adafruit invests time and resources providing this open source code. Please support Adafruit and open source hardware by purchasing products from Adafruit! Written by Kattni Rembor & Limor Fried for Adafruit Industries Copyright (c) 2019-2020 Adafruit Industries Licensed under the MIT license. All text above must be included in any redistribution. """ import time import random import digitalio import audiocore import audiobusio import board import neopixel import adafruit_lis3dh # CUSTOMISE COLORS HERE: COLOR = (0, 120, 120) # Default idle is light blue ALT_COLOR = (255, 0, 250) # hit color is purple # CUSTOMISE IDLE PULSE SPEED HERE: 0 is fast, above 0 slows down IDLE_PULSE_SPEED = 0 # Default is 0 seconds SWING_BLAST_SPEED = 0.0007 # CUSTOMISE BRIGHTNESS HERE: must be a number between 0 and 1 IDLE_PULSE_BRIGHTNESS_MIN = 0.2 # Default minimum idle pulse brightness IDLE_PULSE_BRIGHTNESS_MAX = 1 # Default maximum idle pulse brightness # CUSTOMISE SENSITIVITY HERE: smaller numbers = more sensitive to motion HIT_THRESHOLD = 250 SWING_THRESHOLD = 150 # Set to the length in seconds of the "on.wav" file POWER_ON_SOUND_DURATION = 1.7 NUM_PIXELS = 73 # Number of pixels used in project NEOPIXEL_PIN = board.EXTERNAL_NEOPIXELS enable = digitalio.DigitalInOut(board.EXTERNAL_POWER) enable.direction = digitalio.Direction.OUTPUT enable.value = True strip = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1, auto_write=False) strip.fill(0) # NeoPixels off ASAP on startup strip.show() # i2s audio audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) wave_file = None # Set up accelerometer on I2C bus, 4G range: i2c = board.I2C() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller accel = adafruit_lis3dh.LIS3DH_I2C(i2c) accel.range = adafruit_lis3dh.RANGE_4_G COLOR_IDLE = COLOR # 'idle' color is the default COLOR_HIT = ALT_COLOR # "hit" color is ALT_COLOR set above COLOR_SWING = (255, 255, 255) # "swing" color white def play_wav(name, 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). """ global wave_file # pylint: disable=global-statement print("playing", name) if wave_file: wave_file.close() try: wave_file = open('sounds/' + name + '.wav', 'rb') wave = audiocore.WaveFile(wave_file) audio.play(wave, loop=loop) except OSError: pass # we'll just skip playing then def power_on(sound, duration): """ Animate NeoPixels with accompanying sound effect for power on. :param sound: sound name (similar format to play_wav() above) :param duration: estimated duration of sound, in seconds (>0.0) """ prev = 0 start_time = time.monotonic() # Save audio start time play_wav(sound) while True: elapsed = time.monotonic() - start_time # Time spent playing sound if elapsed > duration: # Past sound duration? break # Stop animating animation_time = elapsed / duration # Animation time, 0.0 to 1.0 threshold = int(NUM_PIXELS * animation_time + 0.5) num = threshold - prev # Number of pixels to light on this pass if num != 0: strip[prev:threshold] = [ALT_COLOR] * num strip.show() prev = threshold def mix(color_1, color_2, weight_2): """ Blend between two colors with a given ratio. :param color_1: first color, as an (r,g,b) tuple :param color_2: second color, as an (r,g,b) tuple :param weight_2: Blend weight (ratio) of second color, 0.0 to 1.0 :return (r,g,b) tuple, blended color """ if weight_2 < 0.0: weight_2 = 0.0 elif weight_2 > 1.0: weight_2 = 1.0 weight_1 = 1.0 - weight_2 return (int(color_1[0] * weight_1 + color_2[0] * weight_2), int(color_1[1] * weight_1 + color_2[1] * weight_2), int(color_1[2] * weight_1 + color_2[2] * weight_2)) # List of swing wav files without the .wav in the name for use with play_wav() swing_sounds = [ 'swing1', 'swing2', 'swing3', 'swing4', ] # List of hit wav files without the .wav in the name for use with play_wav() hit_sounds = [ 'hit1', 'hit2', 'hit3', 'hit4', ] mode = 0 # Initial mode = OFF # Setup idle pulse idle_brightness = IDLE_PULSE_BRIGHTNESS_MIN # current brightness of idle pulse idle_increment = 0.01 # Initial idle pulse direction # Main loop while True: if mode == 0: # If currently off... enable.value = True power_on('on', POWER_ON_SOUND_DURATION) # Power up! play_wav('idle', loop=True) # Play idle sound now mode = 1 # Idle mode # Setup for idle pulse idle_brightness = IDLE_PULSE_BRIGHTNESS_MIN idle_increment = 0.01 strip.fill([int(c*idle_brightness) for c in COLOR]) strip.show() elif mode >= 1: # If not OFF mode... x, y, z = accel.acceleration # Read accelerometer accel_total = x * x + z * z # (Y axis isn't needed, due to the orientation that the Prop-Maker # Wing is mounted. Also, square root isn't needed, since we're # comparing thresholds...use squared values instead.) if accel_total > HIT_THRESHOLD: # Large acceleration = HIT TRIGGER_TIME = time.monotonic() # Save initial time of hit play_wav(random.choice(hit_sounds)) # Start playing 'hit' sound COLOR_ACTIVE = COLOR_HIT # Set color to fade from mode = 3 # HIT mode elif mode == 1 and accel_total > SWING_THRESHOLD: # Mild = SWING TRIGGER_TIME = time.monotonic() # Save initial time of swing play_wav(random.choice(swing_sounds)) # Randomly choose from available swing sounds # make a larson scanner animation_time strip_backup = strip[0:-1] for p in range(-1, len(strip)): for i in range (p-1, p+2): # shoot a 'ray' of 3 pixels if 0 <= i < len(strip): strip[i] = COLOR_SWING strip.show() time.sleep(SWING_BLAST_SPEED) if 0 <= (p-1) < len(strip): strip[p-1] = strip_backup[p-1] # restore previous color at the tail strip.show() while audio.playing: pass # wait till we're done mode = 2 # we'll go back to idle mode elif mode == 1: # Idle pulse idle_brightness += idle_increment # Pulse up if idle_brightness > IDLE_PULSE_BRIGHTNESS_MAX or \ idle_brightness < IDLE_PULSE_BRIGHTNESS_MIN: # Then... idle_increment *= -1 # Pulse direction flip strip.fill([int(c*idle_brightness) for c in COLOR_IDLE]) strip.show() time.sleep(IDLE_PULSE_SPEED) # Idle pulse speed set above elif mode > 1: # If in SWING or HIT mode... if audio.playing: # And sound currently playing... blend = time.monotonic() - TRIGGER_TIME # Time since triggered if mode == 2: # If SWING, blend = abs(0.5 - blend) * 2.0 # ramp up, down strip.fill(mix(COLOR_ACTIVE, COLOR, blend)) # Fade from hit/swing to base color strip.show() else: # No sound now, but still SWING or HIT modes play_wav('idle', loop=True) # Resume idle sound mode = 1 # Return to idle mode
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 sword:
-
COLOR
andALT_COLOR
affect the NeoPixel color of the sword. -
IDLE_PULSE_SPEED
andSWING_BLAST_SPEED
affect the NeoPixel pulse speed. -
IDLE_PULSE_BRIGHTNESS_MIN
and_MAX
affect the NeoPixel brightness range. -
HIT_THRESHOLD
andSWING_THRESHOLD
affect the motion sensitivity of the sword.
# CUSTOMISE COLORS HERE: COLOR = (0, 120, 120) # Default idle is light blue ALT_COLOR = (255, 50, 0) # hit color is orange # CUSTOMISE IDLE PULSE SPEED HERE: 0 is fast, above 0 slows down IDLE_PULSE_SPEED = 0 # Default is 0 seconds SWING_BLAST_SPEED = 0.0007 # CUSTOMISE BRIGHTNESS HERE: must be a number between 0 and 1 IDLE_PULSE_BRIGHTNESS_MIN = 0.2 # Default minimum idle pulse brightness IDLE_PULSE_BRIGHTNESS_MAX = 1 # Default maximum idle pulse brightness # CUSTOMISE SENSITIVITY HERE: smaller numbers = more sensitive to motion HIT_THRESHOLD = 250 SWING_THRESHOLD = 150
NUM_PIXELS = 73 # Number of pixels used in project NEOPIXEL_PIN = board.EXTERNAL_NEOPIXELS enable = digitalio.DigitalInOut(board.EXTERNAL_POWER) enable.direction = digitalio.Direction.OUTPUT enable.value = True strip = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1, auto_write=False) strip.fill(0) # NeoPixels off ASAP on startup strip.show() # i2s audio audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA) wave_file = None # Set up accelerometer on I2C bus, 4G range: i2c = board.I2C() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller accel = adafruit_lis3dh.LIS3DH_I2C(i2c) accel.range = adafruit_lis3dh.RANGE_4_G
Functions
There are a few functions that are used throughout the loop. play_wav()
plays the sound effects that are contained in the /sounds folder.
def play_wav(name, 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). """ global wave_file # pylint: disable=global-statement print("playing", name) if wave_file: wave_file.close() try: wave_file = open('sounds/' + name + '.wav', 'rb') wave = audiocore.WaveFile(wave_file) audio.play(wave, loop=loop) except OSError: pass # we'll just skip playing then
The power_on()
function animates the NeoPixels and plays the sound effect when the sword powers up.
def power_on(sound, duration): """ Animate NeoPixels with accompanying sound effect for power on. :param sound: sound name (similar format to play_wav() above) :param duration: estimated duration of sound, in seconds (>0.0) """ prev = 0 start_time = time.monotonic() # Save audio start time play_wav(sound) while True: elapsed = time.monotonic() - start_time # Time spent playing sound if elapsed > duration: # Past sound duration? break # Stop animating animation_time = elapsed / duration # Animation time, 0.0 to 1.0 threshold = int(NUM_PIXELS * animation_time + 0.5) num = threshold - prev # Number of pixels to light on this pass if num != 0: strip[prev:threshold] = [ALT_COLOR] * num strip.show() prev = threshold
The mix()
function mixes the two NeoPixel colors together.
def mix(color_1, color_2, weight_2): """ Blend between two colors with a given ratio. :param color_1: first color, as an (r,g,b) tuple :param color_2: second color, as an (r,g,b) tuple :param weight_2: Blend weight (ratio) of second color, 0.0 to 1.0 :return (r,g,b) tuple, blended color """ if weight_2 < 0.0: weight_2 = 0.0 elif weight_2 > 1.0: weight_2 = 1.0 weight_1 = 1.0 - weight_2 return (int(color_1[0] * weight_1 + color_2[0] * weight_2), int(color_1[1] * weight_1 + color_2[1] * weight_2), int(color_1[2] * weight_1 + color_2[2] * weight_2))
The Loop
In the main loop, the mode
number determines the functionality of the sword. The first time the code runs, mode
is 0
. This triggers the sword to initiate the power_on
function and then go into idle mode where mode
is 1
.
if mode == 0: # If currently off... enable.value = True power_on('on', POWER_ON_SOUND_DURATION) # Power up! play_wav('idle', loop=True) # Play idle sound now mode = 1 # Idle mode # Setup for idle pulse idle_brightness = IDLE_PULSE_BRIGHTNESS_MIN idle_increment = 0.01 strip.fill([int(c*idle_brightness) for c in COLOR]) strip.show()
If mode
is 1
, then the accelerometer is read. If the accelerometer's reading is greater than the movement thresholds then the sword will either perform the hit or swing modes with light and sound effects. Otherwise, the sword remains in idle mode and the NeoPixels pulse.
elif mode >= 1: # If not OFF mode... x, y, z = accel.acceleration # Read accelerometer accel_total = x * x + z * z # (Y axis isn't needed, due to the orientation that the Prop-Maker # Wing is mounted. Also, square root isn't needed, since we're # comparing thresholds...use squared values instead.) if accel_total > HIT_THRESHOLD: # Large acceleration = HIT TRIGGER_TIME = time.monotonic() # Save initial time of hit play_wav(random.choice(hit_sounds)) # Start playing 'hit' sound COLOR_ACTIVE = COLOR_HIT # Set color to fade from mode = 3 # HIT mode elif mode == 1 and accel_total > SWING_THRESHOLD: # Mild = SWING TRIGGER_TIME = time.monotonic() # Save initial time of swing play_wav(random.choice(swing_sounds)) # Randomly choose from available swing sounds # make a larson scanner animation_time strip_backup = strip[0:-1] for p in range(-1, len(strip)): for i in range (p-1, p+2): # shoot a 'ray' of 3 pixels if 0 <= i < len(strip): strip[i] = COLOR_SWING strip.show() time.sleep(SWING_BLAST_SPEED) if 0 <= (p-1) < len(strip): strip[p-1] = strip_backup[p-1] # restore previous color at the tail strip.show() while audio.playing: pass # wait till we're done mode = 2 # we'll go back to idle mode elif mode == 1: # Idle pulse idle_brightness += idle_increment # Pulse up if idle_brightness > IDLE_PULSE_BRIGHTNESS_MAX or \ idle_brightness < IDLE_PULSE_BRIGHTNESS_MIN: # Then... idle_increment *= -1 # Pulse direction flip strip.fill([int(c*idle_brightness) for c in COLOR_IDLE]) strip.show() time.sleep(IDLE_PULSE_SPEED) # Idle pulse speed set above elif mode > 1: # If in SWING or HIT mode... if audio.playing: # And sound currently playing... blend = time.monotonic() - TRIGGER_TIME # Time since triggered if mode == 2: # If SWING, blend = abs(0.5 - blend) * 2.0 # ramp up, down strip.fill(mix(COLOR_ACTIVE, COLOR, blend)) # Fade from hit/swing to base color strip.show() else: # No sound now, but still SWING or HIT modes play_wav('idle', loop=True) # Resume idle sound mode = 1 # Return to idle mode
Text editor powered by tinymce.