CircuitPython Setup
To get started, you'll want to set up your HalloWing by following this guide. When you're ready, and can upload code to the board return here.
Adafruit really likes using the Mu editor to edit the CircuitPython code. See this guide on loading and using Mu.
More details on using with CircuitPython 6 can be found here.
Libraries
You'll also need a couple of libraries for this project. Follow this guide on adding libraries. The only two you'll need are the neopixl.mpy and adafruit_lis3dh.mpy files from the Circuit Python bundle in the 'lib' folder, so just drag those two from your downloaded, unziped 'lib' folder onto the HalloWing.
Sound Effects
The code was designed to call on five different sound effects depending on your actions. Here are the actions and related sounds:
- Press A2 to turn on lightsaber: on.wav
- Hold lightsaber mostly still: idle.wav
- Swing lightsave: swing.wav
- Hit lightsaber: hit.wav
- Press A2 to turn off lightsaber: off.wav
Download these two .zip archives containing the sound effects sets for standard and unicorn modes. Then, unzip them. Depending on which sound effects set you want to use, drag the entire sounds folder for that set onto the HalloWing.
Lightsaber Code
Here is the code for a standard lightsaber. This is a "standard" lightsaber, with a single color blade. (Check out the Unicorn Mode code down below for multicolor action!)
You can customize it by changing the COLOR values -- we have some presets, but you can use any RGB color combination you like.
Copy the code, then paste it into Mu and save it to your HalloWing with the name code.py (or main.py) the board will automatically run the file named code.py (or main.py) so you must use one of those names.
# SPDX-FileCopyrightText: 2018 John Edgar Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""LASER SWORD (pew pew) example for Adafruit Hallowing & NeoPixel strip"""
# pylint: disable=bare-except
import time
import math
import audioio
import audiocore
import busio
import board
import touchio
import neopixel
import adafruit_lis3dh
# CUSTOMIZE YOUR COLOR HERE:
# (red, green, blue) -- each 0 (off) to 255 (brightest)
COLOR = (0, 100, 255) # jedi
#COLOR = (255, 0, 0) # sith
# CUSTOMIZE SENSITIVITY HERE: smaller numbers = more sensitive to motion
HIT_THRESHOLD = 250
SWING_THRESHOLD = 125
NUM_PIXELS = 30 # NeoPixel strip length (in pixels)
NEOPIXEL_PIN = board.EXTERNAL_NEOPIXEL # Pin where NeoPixels are connected
STRIP = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1, auto_write=False)
STRIP.fill(0) # NeoPixels off ASAP on startup
STRIP.show()
TOUCH = touchio.TouchIn(board.A2) # Rightmost capacitive touch pad
AUDIO = audioio.AudioOut(board.A0) # Speaker
MODE = 0 # Initial mode = OFF
# Set up accelerometer on I2C bus, 4G range:
I2C = busio.I2C(board.SCL, board.SDA)
try:
ACCEL = adafruit_lis3dh.LIS3DH_I2C(I2C, address=0x18) # Production board
except:
ACCEL = adafruit_lis3dh.LIS3DH_I2C(I2C, address=0x19) # Beta hardware
ACCEL.range = adafruit_lis3dh.RANGE_4_G
# "Idle" color is 1/4 brightness, "swinging" color is full brightness...
COLOR_IDLE = (int(COLOR[0] / 4), int(COLOR[1] / 4), int(COLOR[2] / 4))
COLOR_SWING = COLOR
COLOR_HIT = (255, 255, 255) # "hit" color is 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).
"""
try:
wave_file = open('sounds/' + name + '.wav', 'rb')
wave = audiocore.WaveFile(wave_file)
AUDIO.play(wave, loop=loop)
except:
return
def power(sound, duration, reverse):
"""
Animate NeoPixels with accompanying sound effect for power on / off.
@param sound: sound name (similar format to play_wav() above)
@param duration: estimated duration of sound, in seconds (>0.0)
@param reverse: if True, do power-off effect (reverses animation)
"""
start_time = time.monotonic() # Save function start time
play_wav(sound)
while True:
elapsed = time.monotonic() - start_time # Time spent in function
if elapsed > duration: # Past sound duration?
break # Stop animating
fraction = elapsed / duration # Animation time, 0.0 to 1.0
if reverse:
fraction = 1.0 - fraction # 1.0 to 0.0 if reverse
fraction = math.pow(fraction, 0.5) # Apply nonlinear curve
threshold = int(NUM_PIXELS * fraction + 0.5)
for pixel in range(NUM_PIXELS): # Fill NeoPixel strip
if pixel <= threshold:
STRIP[pixel] = COLOR_IDLE # ON pixels BELOW threshold
else:
STRIP[pixel] = 0 # OFF pixels ABOVE threshold
STRIP.show()
if reverse:
STRIP.fill(0) # At end, ensure strip is off
else:
STRIP.fill(COLOR_IDLE) # or all pixels set on
STRIP.show()
while AUDIO.playing: # Wait until audio done
pass
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))
# Main program loop, repeats indefinitely
while True:
if TOUCH.value: # Capacitive pad touched?
if MODE == 0: # If currently off...
power('on', 1.7, False) # Power up!
play_wav('idle', loop=True) # Play background hum sound
MODE = 1 # ON (idle) mode now
else: # else is currently on...
power('off', 1.15, True) # Power down
MODE = 0 # OFF mode now
while TOUCH.value: # Wait for button release
time.sleep(0.2) # to avoid repeated triggering
elif MODE >= 1: # If not OFF mode...
ACCEL_X, ACCEL_Y, ACCEL_Z = ACCEL.acceleration # Read accelerometer
ACCEL_SQUARED = ACCEL_X * ACCEL_X + ACCEL_Z * ACCEL_Z
# (Y axis isn't needed for this, assuming Hallowing is mounted
# sideways to stick. Also, square root isn't needed, since we're
# just comparing thresholds...use squared values instead, save math.)
if ACCEL_SQUARED > HIT_THRESHOLD: # Large acceleration = HIT
TRIGGER_TIME = time.monotonic() # Save initial time of hit
play_wav('hit') # Start playing 'hit' sound
COLOR_ACTIVE = COLOR_HIT # Set color to fade from
MODE = 3 # HIT mode
elif MODE == 1 and ACCEL_SQUARED > SWING_THRESHOLD: # Mild = SWING
TRIGGER_TIME = time.monotonic() # Save initial time of swing
play_wav('swing') # Start playing 'swing' sound
COLOR_ACTIVE = COLOR_SWING # Set color to fade from
MODE = 2 # SWING mode
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_IDLE, BLEND))
STRIP.show()
else: # No sound now, but still MODE > 1
play_wav('idle', loop=True) # Resume background hum
STRIP.fill(COLOR_IDLE) # Set to idle color
STRIP.show()
MODE = 1 # IDLE mode now
Here is the code for a Unicorn Mode lightsaber with multicolor rainbow effects!
Copy the code, then paste it into Mu and save it to your HalloWing with the name code.py (or main.py) the board will automatically run the file named code.py (or main.py) so you must use one of those names.
# SPDX-FileCopyrightText: 2018 John Edgar Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""UNICORN SWORD example for Adafruit Hallowing & NeoPixel strip"""
# pylint: disable=bare-except
import time
import math
import random
import board
import busio
import audioio
import audiocore
import touchio
import neopixel
import adafruit_lis3dh
from neopixel_write import neopixel_write
# CUSTOMIZE SENSITIVITY HERE: smaller numbers = more sensitive to motion
HIT_THRESHOLD = 250
SWING_THRESHOLD = 125
NUM_PIXELS = 30 # NeoPixel strip length (in pixels)
NEOPIXEL_PIN = board.EXTERNAL_NEOPIXEL # Pin where NeoPixels are connected
STRIP = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1, auto_write=False)
STRIP.fill(0) # NeoPixels off ASAP on startup
STRIP.show()
TOUCH = touchio.TouchIn(board.A2) # Rightmost capacitive touch pad
AUDIO = audioio.AudioOut(board.A0) # Speaker
MODE = 0 # Initial mode = OFF
FRAMES = 10 # Pre-calculated animation frames
# Set up accelerometer on I2C bus, 4G range:
I2C = busio.I2C(board.SCL, board.SDA)
try:
ACCEL = adafruit_lis3dh.LIS3DH_I2C(I2C, address=0x18) # Production board
except:
ACCEL = adafruit_lis3dh.LIS3DH_I2C(I2C, address=0x19) # Beta hardware
ACCEL.range = adafruit_lis3dh.RANGE_4_G
def hsv_to_rgb(hue, saturation, value):
"""
Convert HSV color (hue, saturation, value) to RGB (red, green, blue)
@param hue: 0=Red, 1/6=Yellow, 2/6=Green, 3/6=Cyan, 4/6=Blue, etc.
@param saturation: 0.0=Monochrome to 1.0=Fully saturated
@param value: 0.0=Black to 1.0=Max brightness
@returns: red, green, blue eacn in range 0 to 255
"""
hue = hue * 6.0 # Hue circle = 0.0 to 6.0
sxt = math.floor(hue) # Sextant index is next-lower integer of hue
frac = hue - sxt # Fraction-within-sextant is 0.0 to <1.0
sxt = int(sxt) % 6 # mod6 the sextant so it's always 0 to 5
if sxt == 0: # Red to <yellow
red, green, blue = 1.0, frac, 0.0
elif sxt == 1: # Yellow to <green
red, green, blue = 1.0 - frac, 1.0, 0.0
elif sxt == 2: # Green to <cyan
red, green, blue = 0.0, 1.0, frac
elif sxt == 3: # Cyan to <blue
red, green, blue = 0.0, 1.0 - frac, 1.0
elif sxt == 4: # Blue to <magenta
red, green, blue = frac, 0.0, 1.0
else: # Magenta to <red
red, green, blue = 1.0, 0.0, 1.0 - frac
invsat = 1.0 - saturation # Inverse-of-saturation
red = int(((red * saturation) + invsat) * value * 255.0 + 0.5)
green = int(((green * saturation) + invsat) * value * 255.0 + 0.5)
blue = int(((blue * saturation) + invsat) * value * 255.0 + 0.5)
return red, green, blue
# Unlike the single-color laser sword example which can compute and fill
# the NeoPixel strip on the fly, this version is doing a bunch of color
# calculations which would slow things down too much when also trying to
# read the accelerometer. Instead, the 'idle' color state of the sword,
# plus each of two animations (swinging and hitting) are pre-computed at
# program start and stored in bytearrays...these can be quickly issued
# to the NeoPixel strip later as needed.
IDLE = bytearray(NUM_PIXELS * STRIP.bpp)
SWING_ANIM = [bytearray(NUM_PIXELS * STRIP.bpp) for i in range(FRAMES)]
HIT_ANIM = [bytearray(NUM_PIXELS * STRIP.bpp) for i in range(FRAMES)]
IDX = 0
for PIXEL in range(NUM_PIXELS): # For each pixel along strip...
HUE = PIXEL / NUM_PIXELS # 0.0 to <1.0
RED, GREEN, BLUE = hsv_to_rgb(HUE, 1.0, 0.2)
IDLE[IDX + STRIP.order[0]] = RED # Store idle color for pixel
IDLE[IDX + STRIP.order[1]] = GREEN
IDLE[IDX + STRIP.order[2]] = BLUE
for frame in range(FRAMES): # For each frame of animation...
FRAC = frame / (FRAMES - 1) # 0.0 to 1.0
RED, GREEN, BLUE = hsv_to_rgb(HUE + FRAC, FRAC, 1.0 - 0.8 * FRAC)
HIT_ANIM[frame][IDX + STRIP.order[0]] = RED
HIT_ANIM[frame][IDX + STRIP.order[1]] = GREEN
HIT_ANIM[frame][IDX + STRIP.order[2]] = BLUE
RED, GREEN, BLUE = hsv_to_rgb(HUE + FRAC, 1.0, 1.0 - 0.8 * FRAC)
SWING_ANIM[frame][IDX + STRIP.order[0]] = RED
SWING_ANIM[frame][IDX + STRIP.order[1]] = GREEN
SWING_ANIM[frame][IDX + STRIP.order[2]] = BLUE
IDX += 3
# Go back through the hit animation and randomly set one
# pixel per frame to white to create a sparkle effect.
for frame in range(FRAMES):
IDX = random.randint(0, NUM_PIXELS - 1) * 3
HIT_ANIM[frame][IDX] = 255
HIT_ANIM[frame][IDX + 1] = 255
HIT_ANIM[frame][IDX + 2] = 255
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).
"""
try:
wave_file = open('sounds/' + name + '.wav', 'rb')
wave = audiocore.WaveFile(wave_file)
AUDIO.play(wave, loop=loop)
except:
return
def power(sound, duration, reverse):
"""
Animate NeoPixels with accompanying sound effect for power on / off.
@param sound: sound name (similar format to play_wav() above)
@param duration: estimated duration of sound, in seconds (>0.0)
@param reverse: if True, do power-off effect (reverses animation)
"""
start_time = time.monotonic() # Save function start time
play_wav(sound)
while True:
elapsed = time.monotonic() - start_time # Time spent in function
if elapsed > duration: # Past sound duration?
break # Stop animating
fraction = elapsed / duration # Animation time, 0.0 to 1.0
if reverse:
fraction = 1.0 - fraction # 1.0 to 0.0 if reverse
fraction = math.pow(fraction, 0.5) # Apply nonlinear curve
threshold = int(NUM_PIXELS * fraction + 0.5)
idx = 0
for pixel in range(NUM_PIXELS): # Fill NeoPixel strip
if pixel <= threshold:
STRIP[pixel] = ( # BELOW threshold,
IDLE[idx + STRIP.order[0]], # fill pixels with
IDLE[idx + STRIP.order[1]], # IDLE pattern
IDLE[idx + STRIP.order[2]])
else:
STRIP[pixel] = 0 # OFF pixels ABOVE threshold
STRIP.show()
idx += 3
if reverse:
STRIP.fill(0) # At end, ensure strip is off
STRIP.show()
else:
neopixel_write(STRIP.pin, IDLE) # or all pixels set on
while AUDIO.playing: # Wait until audio done
pass
# Main program loop, repeats indefinitely
while True:
if TOUCH.value: # Capacitive pad touched?
if MODE == 0: # If currently off...
power('on', 3.0, False) # Power up!
play_wav('idle', loop=True) # Play background hum sound
MODE = 1 # ON (idle) mode now
else: # else is currently on...
power('off', 2.0, True) # Power down
MODE = 0 # OFF mode now
while TOUCH.value: # Wait for button release
time.sleep(0.2) # to avoid repeated triggering
elif MODE >= 1: # If not OFF mode...
ACCEL_X, ACCEL_Y, ACCEL_Z = ACCEL.acceleration # Read accelerometer
ACCEL_SQUARED = ACCEL_X * ACCEL_X + ACCEL_Z * ACCEL_Z
# (Y axis isn't needed for this, assuming Hallowing is mounted
# sideways to stick. Also, square root isn't needed, since we're
# just comparing thresholds...use squared values instead, save math.)
if ACCEL_SQUARED > HIT_THRESHOLD: # Large acceleration = HIT
TRIGGER_TIME = time.monotonic() # Save initial time of hit
play_wav('hit') # Start playing 'hit' sound
ACTIVE_ANIM = HIT_ANIM
MODE = 3 # HIT mode
elif MODE == 1 and ACCEL_SQUARED > SWING_THRESHOLD: # Mild = SWING
TRIGGER_TIME = time.monotonic() # Save initial time of swing
play_wav('swing') # Start playing 'swing' sound
ACTIVE_ANIM = SWING_ANIM
MODE = 2 # SWING mode
elif MODE > 1: # If in SWING or HIT mode...
if AUDIO.playing: # And sound currently playing...
BLEND = time.monotonic() - TRIGGER_TIME # Time since triggered
BLEND *= 0.7 # 0.0 to 1.0 in ~1.4 sec
if MODE == 2: # If SWING,
BLEND = abs(0.5 - BLEND) * 2.0 # ramp up, down
if BLEND > 1.0:
BLEND = 1.0
elif BLEND < 0.0:
BLEND = 0.0
FRAME = int(BLEND * (FRAMES - 1) + 0.5)
neopixel_write(STRIP.pin, ACTIVE_ANIM[FRAME])
else: # No sound now, but still MODE > 1
play_wav('idle', loop=True) # Resume background hum
neopixel_write(STRIP.pin, IDLE) # Show idle pattern
MODE = 1 # IDLE mode now
Page last edited January 22, 2025
Text editor powered by tinymce.