Inside Out memory ball with Adafruit Gemma M0 and Neopixel Jewel
My family’s Halloween costumes this year are Inside Out themed, with my wife being Joy and my daughter being Sadness. I thought it would be fun to create a flickering memory ball that can change from yellow to blue when sadness picks it up.
My build is mostly based on https://learn.adafruit.com/10-minute-neopixel-necklace/introduction, so be sure to look there for any info I might be leaving out!
Materials
- 1 x LiPoly Battery 100mAh
- 1 x Adafruit Gemma M0
- 1 x Neopixel Jewel
- 1 x Battery Charger
- 1 x Button
- Some wire
- Masking tape
- Permanent marker, fine tip
- 1 x Clear bath bomb mold
- 1 x Frosted glass spray
Tools
- Drill
- Soldering iron
- Computer
- Wire stripper
- Needle-nosed pliers
Assembly
- Attach your battery to your battery charger and insert in a USB charger or computer to charge the battery. The light on the charger should be red while it is charging, and then green when fully charged.
- Use masking tape on the outside of the bath bomb mold to protect the surface from overspray. In a well-ventilated area (preferably outdoors) coat the inside of the bath comb mold with frosted glass spray. Allow to dry.
- Pick one half of the mold to be the bottom. Center your button on the bottom, marking the positions of the four prongs with a permanent marker. Drill four small holes at the four marked spots. Insert the button through the holes, then use pliers to bend the prongs in so the button is firmly attached to the mold.
- Follow https://learn.adafruit.com/10-minute-neopixel-necklace/assembly to attach the Neopixel Jewel to the Gemma M0. I decided to solder three wires into the back of the Neopixel Jewel, and the place it back-to-back with the Gemma M0, using some eletrical tape to prevent short circuits. I then soldered the wires to the Gemma M0.
- Solder two long-ish (~4in) to diagonally opposite corners of button, and then solder one to the GND and one to the D2 contact of the Gemma M0.
Code
import time import random import board import adafruit_dotstar import neopixel from digitalio import DigitalInOut, Direction, Pull # Input/Output definitions for NeoPixel Jewel on pin D1 pixpin = board.D1 numpix = 7 pixels = neopixel.NeoPixel(pixpin, numpix, brightness=.3, auto_write=False) # Input/Output definitions for built-in DotStar RGB led on Adafruit Gemma M0. # Note that we do not set up I/O for the red LED on the Gemma, as it should be # off by default. led = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1) # Input/Output definitions for a tactile switch on pin D2. switch = DigitalInOut(board.D2) switch.direction = Direction.INPUT switch.pull = Pull.UP # Define the base colors for the LEDs. The built-in LED is a very different # color than the NeoPixel Jewel, so different base colors. # Base colors for the NeoPixel JEwel rgb_colors = ([255, 80, 0], # joy color [0, 128, 255]) # sadness color # Base colors for the built-in DotStar LED. rgb_colors_back = ([255, 215, 0], # joy color [0, 128, 255]) # sadness color # Variables to be controlled by the button. mode = 0 # 0 is joy, 1 is sadness # We only want the color to switch once when we press the button, so we need # to keep track of whether we already handled the most recent button press. button_press_handled = False # These variables track the state for each pixel. The first 7 entries # correspond to the NeoPixel elements, and the last to the DotStar led. mode_array = [0, 0, 0, 0, 0, 0, 0, 0] # Tracks whether joy or sadness val_array = [1., 1., 1., 1., 1., 1., 1., 1.] # Tracks brightness sat_array = [1., 1., 1., 1., 1., 1., 1., 1.] # Tracks saturation # This is designed so that each time we change the mode it takes 8 steps # for the change to propagate to all elements. def animate_mode_switch(): # If I wasn't so lazy I'd do this with a loop. mode_array[6] = mode_array[5] mode_array[5] = mode_array[1] mode_array[1] = mode_array[0] mode_array[0] = mode_array[4] mode_array[4] = mode_array[2] mode_array[2] = mode_array[3] mode_array[3] = mode_array[7] mode_array[7] = mode def color_adjust(val, sat, color): # val controls the overall brightness of the led and sat controls how # saturated the color is. Rather than do real HSV calculations, we blend # with white to approximate desaturation, and then scale by val to reduce # the brightness. return [int(val*(sat*x + (1-sat)*255)) for x in color] def mutate_sat_and_value(): # Controls how much the val for each pixel can vary in one time step varation_rate = 0.2 for r in range(len(val_array)): cur_val = val_array[r] cur_val = cur_val + varation_rate/2 - varation_rate * random.random() # Clamp the val for each pixel to between 0.05 and 0.5 cur_val = min(cur_val, 0.5) cur_val = max(cur_val, 0.05) val_array[r] = cur_val # Controls how much the sat for each pixel can vary in one time step varation_rate = 0.025 for r in range(len(sat_array)): cur_val = sat_array[r] cur_val = cur_val + varation_rate/2 - varation_rate * random.random() # Clamp the sat for each pixel to between 1.0 and 0.95 cur_val = min(cur_val, 1.0) cur_val = max(cur_val, 0.95) sat_array[r] = cur_val def handle_button(): global button_press_handled global mode # If the button is currently pressed (which correponds to switch.value # being False!), and we didn't aready handle it, then we flip modes. if not switch.value and not button_press_handled: button_press_handled = True mode = 1 - mode # A nice way to switch modes elif switch.value and button_press_handled: button_press_handled = False def update_pixel_colors(): # Compute the color for each pixel. Start with the appropriate base color # based on mode, and then apply the saturation and value to it. # This part handles the NeoPixels for r in range(len(pixels)): pixels[r] = color_adjust(val_array[r], sat_array[r], rgb_colors[mode_array[r]]) pixels.write() # This part handles the DotStar led[0] = color_adjust(val_array[7], sat_array[7], rgb_colors_back[mode_array[7]]) while True: # This is the main loop. handle_button() animate_mode_switch() mutate_sat_and_value() update_pixel_colors() # Anything bigger than 0.1 here seems to make the button handling too # sluggish. Hopefully setting this to 0.1 will save power. time.sleep(0.1)