Now that CircuitPython is installed on the CPB, we can move on to installing the project software.

Install Bluefruit Connect & Neopixel libraries

The project code requires two code libraries to assist with Bluetooth communication. Click the link below to download the CircuitPython library bundle:

Unzip the library bundle, and open the lib folder inside. You'll need to copy three libraries from this folder to the CIRCUITPY drive's lib folder.

  • Locate the folders named adafruit_bluefruit_connect & adafruit_ble and copy them to the CIRCUITPY drive's lib folder. 
  • Locate the file named neopixel.mpy and copy it to the CIRCUITPY drive's lib folder.

Your CIRCUITPY drive's file structure should now look like this:

Upload code

Copy the code below and paste it into a new text file.

Save the text file as code.py to the root of the CIRCUITPY drive.

"""
LED Disco Tie with Bluetooth
=========================================================
Give your suit an sound-reactive upgrade with Circuit
Playground Bluefruit & Neopixels. Set color and animation
mode using the Bluefruit LE Connect app.

Author: Collin Cunningham for Adafruit Industries, 2019
"""
# pylint: disable=global-statement

import time
import array
import math
import audiobusio
import board
import neopixel

from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
from adafruit_bluefruit_connect.packet import Packet
from adafruit_bluefruit_connect.color_packet import ColorPacket
from adafruit_bluefruit_connect.button_packet import ButtonPacket

ble = BLERadio()
uart_service = UARTService()
advertisement = ProvideServicesAdvertisement(uart_service)

# User input vars
mode = 0 # 0=audio, 1=rainbow, 2=larsen_scanner, 3=solid
user_color= (127,0,0)

# Audio meter vars
PEAK_COLOR = (100, 0, 255)
NUM_PIXELS = 10
NEOPIXEL_PIN = board.A1
# Use this instead if you want to use the NeoPixels on the Circuit Playground Bluefruit.
# NEOPIXEL_PIN = board.NEOPIXEL
CURVE = 2
SCALE_EXPONENT = math.pow(10, CURVE * -0.1)
NUM_SAMPLES = 160

# Restrict value to be between floor and ceiling.
def constrain(value, floor, ceiling):
    return max(floor, min(value, ceiling))

# Scale input_value between output_min and output_max, exponentially.
def log_scale(input_value, input_min, input_max, output_min, output_max):
    normalized_input_value = (input_value - input_min) / \
                             (input_max - input_min)
    return output_min + \
        math.pow(normalized_input_value, SCALE_EXPONENT) \
        * (output_max - output_min)

# Remove DC bias before computing RMS.
def normalized_rms(values):
    minbuf = int(mean(values))
    samples_sum = sum(
        float(sample - minbuf) * (sample - minbuf)
        for sample in values
    )

    return math.sqrt(samples_sum / len(values))

def mean(values):
    return sum(values) / len(values)

def volume_color(volume):
    return 200, volume * (255 // NUM_PIXELS), 0

# Set up NeoPixels and turn them all off.
pixels = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=0.1, auto_write=False)
pixels.fill(0)
pixels.show()

mic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA,
                       sample_rate=16000, bit_depth=16)

# Record an initial sample to calibrate. Assume it's quiet when we start.
samples = array.array('H', [0] * NUM_SAMPLES)
mic.record(samples, len(samples))
# Set lowest level to expect, plus a little.
input_floor = normalized_rms(samples) + 10
# Corresponds to sensitivity: lower means more pixels light up with lower sound
input_ceiling = input_floor + 500
peak = 0

def wheel(wheel_pos):
    # Input a value 0 to 255 to get a color value.
    # The colours are a transition r - g - b - back to r.
    if wheel_pos < 0 or wheel_pos > 255:
        r = g = b = 0
    elif wheel_pos < 85:
        r = int(wheel_pos * 3)
        g = int(255 - wheel_pos*3)
        b = 0
    elif wheel_pos < 170:
        wheel_pos -= 85
        r = int(255 - wheel_pos*3)
        g = 0
        b = int(wheel_pos*3)
    else:
        wheel_pos -= 170
        r = 0
        g = int(wheel_pos*3)
        b = int(255 - wheel_pos*3)
    return (r, g, b)

def rainbow_cycle(delay):
    for j in range(255):
        for i in range(NUM_PIXELS):
            pixel_index = (i * 256 // NUM_PIXELS) + j
            pixels[i] = wheel(pixel_index & 255)
        pixels.show()
        time.sleep(delay)

def audio_meter(new_peak):
    mic.record(samples, len(samples))
    magnitude = normalized_rms(samples)

    # Compute scaled logarithmic reading in the range 0 to NUM_PIXELS
    c = log_scale(constrain(magnitude, input_floor, input_ceiling),
                  input_floor, input_ceiling, 0, NUM_PIXELS)

    # Light up pixels that are below the scaled and interpolated magnitude.
    pixels.fill(0)
    for i in range(NUM_PIXELS):
        if i < c:
            pixels[i] = volume_color(i)
        # Light up the peak pixel and animate it slowly dropping.
        if c >= new_peak:
            new_peak = min(c, NUM_PIXELS - 1)
        elif new_peak > 0:
            new_peak = new_peak - 1
        if new_peak > 0:
            pixels[int(new_peak)] = PEAK_COLOR
    pixels.show()
    return new_peak

pos = 0  # position
direction = 1  # direction of "eye"

def larsen_set(index, color):
    if index < 0:
        return
    else:
        pixels[index] = color

def larsen(delay):
    global pos
    global direction
    color_dark = (int(user_color[0]/8), int(user_color[1]/8),
                  int(user_color[2]/8))
    color_med = (int(user_color[0]/2), int(user_color[1]/2),
                 int(user_color[2]/2))

    larsen_set(pos - 2, color_dark)
    larsen_set(pos - 1, color_med)
    larsen_set(pos, user_color)
    larsen_set(pos + 1, color_med)

    if (pos + 2) < NUM_PIXELS:
        # Dark red, do not exceed number of pixels
        larsen_set(pos + 2, color_dark)

    pixels.write()
    time.sleep(delay)

    # Erase all and draw a new one next time
    for j in range(-2, 2):
        larsen_set(pos + j, (0, 0, 0))
        if (pos + 2) < NUM_PIXELS:
            larsen_set(pos + 2, (0, 0, 0))

    # Bounce off ends of strip
    pos += direction
    if pos < 0:
        pos = 1
        direction = -direction
    elif pos >= (NUM_PIXELS - 1):
        pos = NUM_PIXELS - 2
        direction = -direction

def solid(new_color):
    pixels.fill(new_color)
    pixels.show()

def map_value(value, in_min, in_max, out_min, out_max):
    out_range = out_max - out_min
    in_range = in_max - in_min
    return out_min + out_range * ((value - in_min) / in_range)

speed = 6.0
wait = 0.097

def change_speed(mod, old_speed):
    new_speed = constrain(old_speed + mod, 1.0, 10.0)
    return(new_speed, map_value(new_speed, 10.0, 0.0, 0.01, 0.3))

def animate(pause, top):
    # Determine animation based on mode
    if mode == 0:
        top = audio_meter(top)
    elif mode == 1:
        rainbow_cycle(0.001)
    elif mode == 2:
        larsen(pause)
    elif mode == 3:
        solid(user_color)
    return top

while True:
    ble.start_advertising(advertisement)
    while not ble.connected:
        # Animate while disconnected
        peak = animate(wait, peak)

    # While BLE is connected
    while ble.connected:
        if uart_service.in_waiting:
            try:
                packet = Packet.from_stream(uart_service)
            # Ignore malformed packets.
            except ValueError:
                continue

            # Received ColorPacket
            if isinstance(packet, ColorPacket):
                user_color = packet.color

            # Received ButtonPacket
            elif isinstance(packet, ButtonPacket):
                if packet.pressed:
                    if packet.button == ButtonPacket.UP:
                        speed, wait = change_speed(1, speed)
                    elif packet.button == ButtonPacket.DOWN:
                        speed, wait = change_speed(-1, speed)
                    elif packet.button == ButtonPacket.BUTTON_1:
                        mode = 0
                    elif packet.button == ButtonPacket.BUTTON_2:
                        mode = 1
                    elif packet.button == ButtonPacket.BUTTON_3:
                        mode = 2
                    elif packet.button == ButtonPacket.BUTTON_4:
                        mode = 3

        # Animate while connected
        peak = animate(wait, peak)

Once the project code is saved to CIRCUITPY as code.py, the software is all set – time to move on to assembling the hardware.

This guide was first published on Nov 12, 2019. It was last updated on Nov 12, 2019.
This page (Software) was last updated on Jun 23, 2020.