Install Libraries

Download the latest version of the Circuit Python Library Repository using the button above. Be sure the library version matches the version of CircuitPython you just installed (i.e. if you installed CircuitPython 6.x, install the 6.x version of the library files).

Copy the following files into a directory called /lib on your CIRCUITPY drive:

  • adafruit_bus_device (directory)
  • adafruit_circuitplayground (directory)
  • adafruit_led_animation (directory)
  • adafruit_lis3dh.mpy
  • adafruit_thermistor.mpy
  • neopixel.mpy

When you're done, your CIRCUITPY drive should look like this:

Simplified Code

This sample code demonstrates how to use a rotary encoder to control the speed and the brightness of animations from the LED Animations Library. It also uses the rotary encoder's push button to turn the lights on and off with a software toggle switch.

This code also demonstrates how to run animations on two different LED strips or pins at the same time: in this case, the lights on pin A1 and the lights on the face of the Circuit Playground board. 

I've included the full chandelier code, including pixel mapping and multiple layered animations, at the bottom of this page.

# SPDX-FileCopyrightText: 2021 Erin St Blaine for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
Crystal Chandelier with Circuit Playground BlueFruit
Adafruit invests time and resources providing this open source code.
Please support Adafruit and open source hardware by purchasing
products from Adafruit!
Written by Erin St Blaine & Limor Fried for Adafruit Industries
Copyright (c) 2020-2021 Adafruit Industries
Licensed under the MIT license.
All text above must be included in any redistribution.
"""

# pylint: disable=import-error
# pylint: disable=no-member
import board
import digitalio
import rotaryio
import neopixel
from adafruit_circuitplayground import cp

from adafruit_led_animation.group import AnimationGroup
from adafruit_led_animation.animation.comet import Comet
from adafruit_led_animation.animation.solid import Solid
from adafruit_led_animation.color import (
    BLACK,
    PURPLE,
)

NEOPIXEL_PIN = board.A1 # NeoPixels connected here (crystals + rings)
NUM_PIXELS = 92        # Number of pixels in the crystals and rings

MIN_BRIGHTNESS = 1   # Minimum LED brightness as a percentage (0 to 100)
MAX_BRIGHTNESS = 25 # Maximum LED brighrness as a percentage (0 to 100)
MIN_FPS = 2         # Minimum animation speed in frames per second (>0)
MAX_FPS = 8        # Maximum animation speed in frames per second (>0)

# Set initial brightness and speed to center values
BRIGHTNESS = (MIN_BRIGHTNESS + MAX_BRIGHTNESS) // 2
FPS = (MIN_FPS + MAX_FPS) // 2
SPEED = 1 / FPS         # Integer frames-per-second to seconds interval
LEVEL = BRIGHTNESS * 0.01 # Integer brightness percentage to 0.0-1.0 coeff.

button = digitalio.DigitalInOut(board.A5)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
BUTTON_STATE = None
BUTTON_VALUE = None

PIXELS = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=LEVEL,
                           auto_write=False)

# NeoPixels off ASAP on startup
cp.pixels.fill(0) # Onboard pixels
cp.pixels.show()
PIXELS.fill(0) # NeoPixel strip
PIXELS.show()

ENCODER = rotaryio.IncrementalEncoder(board.A2, board.A3)

# LED ANIMATIONS -----------------------------------------------------------

COMET = Comet(PIXELS, speed=SPEED, tail_length=8,
              color=PURPLE, bounce=True)
CP_COMET = Comet(cp.pixels, speed=SPEED, tail_length=8,
                 color=PURPLE, bounce=True)
DARK_RINGS = Solid(PIXELS, color=BLACK)
DARK_CPB = Solid(cp.pixels, color=BLACK)
DARK = AnimationGroup(
        DARK_RINGS,
        DARK_CPB,
        )

# Animations Playlist, reorder as desired. AnimationGroups play at same time
ANIMATIONS = AnimationGroup(
        COMET,
        CP_COMET,
        )


# MAIN LOOP ----------------------------------------------------------------

LAST_POSITION = ENCODER.position
MODE = 1
while True:

    POSITION = ENCODER.position
    if POSITION != LAST_POSITION:
        MOVE = POSITION - LAST_POSITION
        if cp.switch:

            FPS = max(MIN_FPS, min(FPS + MOVE, MAX_FPS))
            SPEED = 1.0 / FPS
            COMET.speed = SPEED
            print("comet speed = ", SPEED)
        else:
            BRIGHTNESS = max(MIN_BRIGHTNESS, min(BRIGHTNESS + MOVE,
                                                 MAX_BRIGHTNESS))
            LEVEL = BRIGHTNESS * 0.1
            if LEVEL > 1:
                LEVEL = 1
            cp.pixels.brightness = LEVEL
            PIXELS.brightness = LEVEL
            print("brightness = ", LEVEL)
        LAST_POSITION = POSITION
    if not BUTTON_VALUE and BUTTON_STATE is None:
        BUTTON_STATE = "pressed"
    if BUTTON_VALUE and BUTTON_STATE == "pressed":
        print("Button pressed.")
        if MODE == 1:
            MODE = 2
        else:
            MODE = 1
        BUTTON_STATE = None
    if MODE == 1:
        ANIMATIONS.animate()
    else:
        DARK.animate()

Download the code.py file or copy/paste the code into your Mu editor window and save it as code.py at the root of your CIRCUITPY drive. You should see a purple comet animation running on the face of your Circuit Playground, and also on any lights you have hooked up to pin A1. 

If you have your rotary encoder connected, test it out. When the tiny slide switch on the Circuit Playground is switched to the left, the speed of the comet will change when you turn the dial. If the switch is flipped to the right, the brightness will change. Press down on the knob to toggle the lights on and off.

Let's delve into the code a bit.

First, we'll import our libraries and the animations we want to use from the LED Animations Library. Check out that guide for a lot more info on using animations.

import board
import digitalio
import rotaryio
import neopixel
from adafruit_circuitplayground import cp

from adafruit_led_animation.group import AnimationGroup
from adafruit_led_animation.animation.comet import Comet
from adafruit_led_animation.animation.solid import Solid
from adafruit_led_animation.color import (
    BLACK,
    PURPLE,
)

Next, we'll set up the NeoPixel strip on pin A1. We don't need to set up the Circuit Playground's onboard pixels - the Circuit Playground library does that for us.

NEOPIXEL_PIN = board.A1 # NeoPixels connected here (crystals + rings)
NUM_PIXELS = 92        # Number of pixels in the crystals and rings

PIXELS = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=LEVEL,
                           auto_write=False)

Next we'll set up the parameters for our brightness and speed controls. We're controlling speed in frames-per-second (FPS). You can play around with these numbers to define your maximum and minimum speed and brightness. Open the REPL and turn the knob to get a readout of the current brightness level or speed.

Be sure to back up your code before you mess around with these numbers too much! There's a possibility of feeding your rotary encoder a reading of 0, which will hang your board. Check the troubleshooting section below if this happens to you.

MIN_BRIGHTNESS = 1   # Minimum LED brightness as a percentage (0 to 100)
MAX_BRIGHTNESS = 25 # Maximum LED brighrness as a percentage (0 to 100)
MIN_FPS = 2         # Minimum animation speed in frames per second (>0)
MAX_FPS = 8        # Maximum animation speed in frames per second (>0)

# Set initial brightness and speed to center values
BRIGHTNESS = (MIN_BRIGHTNESS + MAX_BRIGHTNESS) // 2
FPS = (MIN_FPS + MAX_FPS) // 2
SPEED = 1 / FPS         # Integer frames-per-second to seconds interval
LEVEL = BRIGHTNESS * 0.01 # Integer brightness percentage to 0.0-1.0 coeff.

Next we set up the push-button for our on/off toggle.

button = digitalio.DigitalInOut(board.A5)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
BUTTON_STATE = None
BUTTON_VALUE = None

We set the pixels to OFF initially, then set up our encoder.

# NeoPixels off ASAP on startup
cp.pixels.fill(0) # Onboard pixels
cp.pixels.show()
PIXELS.fill(0) # NeoPixel strip
PIXELS.show()

ENCODER = rotaryio.IncrementalEncoder(board.A2, board.A3)

Next we set up our LED animations. You can add as many different animation types and varietals as you'd like. Just be sure to import the animation from the LED Animations library at the top of the code.

The LED Animations library can only address one pin per animation. Since we're using two different pins: the pixels on A1 and the pixels on the face of the Circuit Playground, we need to use AnimationGroup to make them play at the same time. 

I've set up the COMET animation to play on PIXELS (pin A1) and also on cp.pixels (the onboard pixels). I've also set up a DARK animation on both strips, for use when we press the toggle switch to turn the pixels off.

# LED ANIMATIONS -----------------------------------------------------------

COMET = Comet(PIXELS, speed=SPEED, tail_length=8,
              color=PURPLE, bounce=True) 
CP_COMET = Comet(cp.pixels, speed=SPEED, tail_length=8,
                 color=PURPLE, bounce=True)                      
DARK_RINGS = Solid(PIXELS, color=BLACK)
DARK_CPB = Solid(cp.pixels, color=BLACK)
DARK = AnimationGroup(
        DARK_RINGS, 
        DARK_CPB,
        )

# Animations Playlist, reorder as desired. AnimationGroups play at same time
ANIMATIONS = AnimationGroup(
        COMET,
        CP_COMET,
        )

That's it for setup. Next comes our main loop.  First the code reads the position of the rotary encoder to determine if it's been moved. If the onboard switch (cp.switch) is set to the left, the code will adjust the speed of the COMET animation whenever the switch is turned.

If the switch is set to the right, the code adjusts the brightness level up or down.

LAST_POSITION = ENCODER.position
MODE = 1
while True:

    POSITION = ENCODER.position
    if POSITION != LAST_POSITION:
        MOVE = POSITION - LAST_POSITION
        if cp.switch:
            
            FPS = max(MIN_FPS, min(FPS + MOVE, MAX_FPS))
            SPEED = 1.0 / FPS
            COMET.speed = SPEED
            print("comet speed = ", SPEED)
        else:
            BRIGHTNESS = max(MIN_BRIGHTNESS, min(BRIGHTNESS + MOVE,
                                                 MAX_BRIGHTNESS))
            LEVEL = BRIGHTNESS * 0.1
            if LEVEL > 1:
                LEVEL = 1
            cp.pixels.brightness = LEVEL
            PIXELS.brightness = LEVEL
            print("brightness = ", LEVEL)
        LAST_POSITION = POSITION

Finally, the code looks for button presses and toggles between the COMET animation group and the DARK animation group depending on the button's state.

if not BUTTON_VALUE and BUTTON_STATE is None:
        BUTTON_STATE = "pressed"
    if BUTTON_VALUE and BUTTON_STATE == "pressed":
        print("Button pressed.")
        if MODE == 1:
            MODE = 2
        else:
            MODE = 1
        BUTTON_STATE = None
    if MODE == 1:
        ANIMATIONS.animate()
    else:
        DARK.animate()

Full Code

Here's my full chandelier code. In this version I've added pixel mapping so I can address the crystals separately from the rings, plus I've added more animations and animation groups. You can learn more about these features in the LED Animations Library Guide.

This code runs a half-brightness rainbow animation on the rings and Circuit Playground face, balancing the brightness with the single-pixel crystals, while it runs a speed-controlled comet animation on the crystals themselves.  

# SPDX-FileCopyrightText: 2021 Erin St Blaine for Adafruit Industries
# SPDX-FileCopyrightText: 2021 Limor Fried for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
Crystal Chandelier with Circuit Playground BlueFruit
Adafruit invests time and resources providing this open source code.
Please support Adafruit and open source hardware by purchasing
products from Adafruit!
Written by Erin St Blaine & Limor Fried for Adafruit Industries
Copyright (c) 2020-2021 Adafruit Industries
Licensed under the MIT license.
All text above must be included in any redistribution.
"""

# pylint: disable=import-error
# pylint: disable=no-member
import board
import digitalio
import rotaryio
import neopixel
from adafruit_circuitplayground import cp

from adafruit_led_animation.helper import PixelMap
from adafruit_led_animation.sequence import AnimationSequence
from adafruit_led_animation.group import AnimationGroup
from adafruit_led_animation.animation.rainbow import Rainbow
from adafruit_led_animation.animation.comet import Comet
from adafruit_led_animation.animation.solid import Solid
from adafruit_led_animation.color import (
    BLACK,
    PURPLE,
)

NEOPIXEL_PIN = board.A1 # NeoPixels connected here (crystals + rings)
NUM_PIXELS = 92        # Number of pixels in the crystals and rings

MIN_BRIGHTNESS = 1   # Minimum LED brightness as a percentage (0 to 100)
MAX_BRIGHTNESS = 25 # Maximum LED brighrness as a percentage (0 to 100)
MIN_FPS = 2         # Minimum animation speed in frames per second (>0)
MAX_FPS = 8        # Maximum animation speed in frames per second (>0)

# Set initial brightness and speed to center values
BRIGHTNESS = (MIN_BRIGHTNESS + MAX_BRIGHTNESS) // 2
FPS = (MIN_FPS + MAX_FPS) // 2
SPEED = 1 / FPS         # Integer frames-per-second to seconds interval
LEVEL = BRIGHTNESS * 0.01 # Integer brightness percentage to 0.0-1.0 coeff.

button = digitalio.DigitalInOut(board.A5)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
BUTTON_STATE = None
BUTTON_VALUE = None

PIXELS = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=LEVEL,
                           auto_write=False)

# NeoPixels off ASAP on startup
cp.pixels.fill(0) # Onboard pixels
cp.pixels.show()
PIXELS.fill(0) # NeoPixel strip
PIXELS.show()

ENCODER = rotaryio.IncrementalEncoder(board.A2, board.A3)

# PIXEL MAPS reorder pixels so animations run in different configs ---------

# Crystals Only
PIXEL_MAP_CRYSTALS = PixelMap(PIXELS, [
    84, 85, 86, 87, 88, 89, 90, 91
    ], individual_pixels=True)

# Rings Only
PIXEL_MAP_RINGS = PixelMap(PIXELS, [
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
    24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
    45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
    66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83
    ], individual_pixels=True)



# LED ANIMATIONS -----------------------------------------------------------

RAINBOW = Rainbow(PIXEL_MAP_RINGS, speed=0.01, period=10, step=3)
CP_RAINBOW = Rainbow(cp.pixels, speed=0.01, period=10, step=3)
CRYSTAL_COMET = Comet(PIXEL_MAP_CRYSTALS, speed=SPEED, tail_length=8,
                      color=PURPLE, bounce=True)
DARK_RINGS = Solid(PIXELS, color=BLACK)
DARK_CPB = Solid(cp.pixels, color=BLACK)
DARK = AnimationGroup(
        DARK_RINGS,
        DARK_CPB,
        )

# Animations Playlist, reorder as desired. AnimationGroups play at same time
ANIMATIONS = AnimationSequence(
    AnimationGroup(
        CRYSTAL_COMET,
        RAINBOW,
        CP_RAINBOW,
        ),
    auto_clear=True,
    auto_reset=True,
)

# MAIN LOOP ----------------------------------------------------------------

LAST_POSITION = ENCODER.position
MODE = 1
while True:

    POSITION = ENCODER.position
    if POSITION != LAST_POSITION:
        MOVE = POSITION - LAST_POSITION
        if cp.switch:

            FPS = max(MIN_FPS, min(FPS + MOVE, MAX_FPS))
            SPEED = 1.0 / FPS
            CRYSTAL_COMET.speed = SPEED
            #RAINBOW.speed = SPEED
            #CP_RAINBOW.speed = SPEED
            print("crystal speed = ", SPEED)
        else:
            BRIGHTNESS = max(MIN_BRIGHTNESS, min(BRIGHTNESS + MOVE,
                                                 MAX_BRIGHTNESS))
            LEVEL = BRIGHTNESS * 0.1
            if LEVEL > 2:
                LEVEL = 2
            cp.pixels.brightness = LEVEL/2
            PIXEL_MAP_RINGS.brightness = LEVEL/2
            PIXEL_MAP_CRYSTALS.brightness = LEVEL
            print("ring brightness = ", LEVEL/2, "crystal brightness = ", LEVEL)
        LAST_POSITION = POSITION
    if not BUTTON_VALUE and BUTTON_STATE is None:
        BUTTON_STATE = "pressed"
    if BUTTON_VALUE and BUTTON_STATE == "pressed":
        print("Button pressed.")
        if MODE == 1:
            MODE = 2
        else:
            MODE = 1
        BUTTON_STATE = None
    if MODE == 1:
        ANIMATIONS.animate()
    else:
        DARK.animate()

Troubleshooting

If you're having trouble getting the code to load, head over to the Circuit Playground Bluefruit guide for more detailed instructions and things to try.

Another problem I encountered is that the rotary encoder will hang if it's fed a value of 0. If you're playing with calibrating your numbers and your board's lights turn red, your CIRCUITPY drive vanishes, and pressing the reset button has no effect, this may be what happened.

Don't worry! You can erase and recover the board but you'll need to reinstall CircuitPython and your libraries. Visit the Troubleshooting page from the CircuitPython guide and scroll way down until you find CIRCUITPY Drive Issues. Follow the instructions to wipe your board clean and start over.

This guide was first published on Jan 27, 2021. It was last updated on Sep 30, 2023.

This page (Software) was last updated on Nov 27, 2023.

Text editor powered by tinymce.