Connect the Collar

Connect the ItsyBitsy nRF52840 to a computer with a micro USB cable to program it. Be sure that your cable carries data as well as power.

Connecting the USB cable will also charge the battery. If you've programmed the ItsyBitsy already, you will notice that the LED patterns run whenever the collar is connected to USB power. The slide switch only turns off the power from the LiPo battery. This occurs because the LEDs connect to the ItsyBitsy's Vhi pin, which draws from both USB and battery power. This feature stops the battery from being drained while you're writing code, but may be annoying if you'd just like to leave your collar plugged in for charging.  For this reason, we'll create a "charging" mode in the code that allows us to toggle the animations on and off when the the ItsyBitsy's SW button is pressed.

Install CircuitPython

If you're not familiar with the process, this learn guide gives a good overview of installing CircuitPython on a number of microcontrollers, including the ItsyBitsy nRF52840.

Load the Libraries

If you don't already have it, download the latest CircuitPython release from the link below. Open the zipped file to get the CircuitPython libraries. Copy the following libraries to the ItsyBitsy folder by dragging them into the lib folder in the CIRCUITPY drive that appears when you connect the board to your computer.

  • adafruit_ble
  • adafruit_bluefruit_connect
  • adafruit_bus_device
  • adafruit_debouncer
  • adafruit_led_animation
  • digitalio
  • neopixel.mpy

The Complete Code

Click download and save on your computer. Plug your ItsyBitsy into your computer via a known, good USB cable. The board should show up as a flash drive named CIRCUITPY. Copy the code.py file to that drive's main (root) directory.

# SPDX-FileCopyrightText: 2021 Anne Barela for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
Code for the LED Infinity Mirror Collar. Allows the animation sequence 
and color to be controlled by input from the Adafruit Bluefruit App
"""
import board
import random
from rainbowio import colorwheel
import neopixel
import digitalio
from adafruit_debouncer import Debouncer

# LED Animation modules
from adafruit_led_animation.animation.comet import Comet
from adafruit_led_animation.animation.chase import Chase
from adafruit_led_animation.animation.rainbowcomet import RainbowComet
from adafruit_led_animation.animation.pulse import Pulse
from adafruit_led_animation.sequence import AnimationSequence


# Bluetooth modules
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

# NeoPixel control pin
pixel_pin = board.D5

# Number of pixels in the collar (arranged in two rows)
pixel_num = 24

pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.5, auto_write=False)

# Create a switch from the ItsyBity's on-board pushbutton to toggle charge mode
mode_pin = digitalio.DigitalInOut(board.SWITCH)
mode_pin.direction = digitalio.Direction.INPUT
mode_pin.pull = digitalio.Pull.UP
switch = Debouncer(mode_pin)

# Create the animations
comet = Comet(pixels, speed=0.06, color=(180,0,255), tail_length=10, bounce=True)
chase = Chase(pixels, speed=0.05, size=3, spacing=3, color=(0,255,255), reverse=True)
rainbow_comet = RainbowComet(pixels, speed=.06)
pulse = Pulse(pixels, speed=.04, color=(255,0,0), period = 0.2)


# Our animations sequence
seconds_per_animation = 10
animations = AnimationSequence(comet, rainbow_comet, chase, advance_interval=seconds_per_animation, auto_clear=True)
# Current display determines whether we are showing the animation sequence or the pulse animation
current_display = animations

# Mode changes the color of random animations randomly
random_color_mode = True

def random_animation_color(anims):
    if random_color_mode:
        anims.color = colorwheel(random.randint(0,255))
        
animations.add_cycle_complete_receiver(random_animation_color)


# After we complete three pulse cycles, return to main animations list
def pulse_finished(anim):
    global current_display
    current_display = animations

pulse.add_cycle_complete_receiver(pulse_finished)
pulse.notify_cycles = 3


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

# Set charge_mode to True to turn off the LED Animations and Bluetooth
#  e.g. when charging the battery
charge_mode = False

# Checks the ItsyBitsy's switch button
def check_switch():
    global charge_mode
    switch.update()
    if switch.fell:  #Switch changed state
        charge_mode = not charge_mode
        # if display has just been turned off, clear all LEDs, disconnect, stop advertising
        if charge_mode:
            pixels.fill((0,0,0))
            pixels.show()
            if ble.connected:
                for conn in ble.connections:
                    conn.disconnect()
            if ble.advertising:
                ble.stop_advertising()

# Main program loop
while True:
    # Check whether charge mode has been changed
    check_switch()
    if charge_mode:
        pass
    else:
        current_display.animate()
        if ble.connected:
            if uart_service.in_waiting:
                #Packet is arriving
                packet = Packet.from_stream(uart_service)
                if isinstance(packet, ButtonPacket) and packet.pressed:
                    if packet.button == ButtonPacket.BUTTON_1:
                        # Animation colors change to a random new value after every animation sequence
                        random_color_mode = True
                    elif packet.button == ButtonPacket.BUTTON_2:
                        # Animation colors stay the same unless manually changed
                        random_color_mode = False
                    elif packet.button == ButtonPacket.BUTTON_3:
                        # Stay on the same animation
                        animations._advance_interval = None
                    elif packet.button == ButtonPacket.BUTTON_4:
                        # Auto-advance animations
                        animations._advance_interval = seconds_per_animation*1000
                    elif packet.button == ButtonPacket.LEFT:
                        # Go to first animation in the sequence
                        animations.activate(0)
                    elif packet.button == ButtonPacket.RIGHT:
                        # Go to the next animation in the sequence
                        animations.next()
                elif isinstance(packet, ColorPacket):
                    animations.color = packet.color
                    pulse.color = packet.color
                    # temporarily change to pulse display to show off the new color
                    current_display = pulse

        else:
            if not ble.advertising:
                ble.start_advertising(advertisement)

Bluefruit Control

Install the Bluefruit app on your phone or tablet to control the collar remotely. 

When you open the app, the initial screen allows you to select and connect to the the collar. Once connected, select the "Controller" option to be able to change the display using the "Control Pad" and "Color Picker" settings.

The Control Pad has four arrow buttons and four number buttons. Their functions are:

  • left arrow resets the display to the first animation in the sequence
  • right arrow moves to the next animation
  • button 1 selects a mode in which the animation colors change to a random value in each cycle. This mode is the default
  • button 2 sets a mode where the colors stay the same unless manually changed with the color picker.
  • button 3 sets the animation sequence to switch between animations at a regular interval. This is the default mode.
  • button 4 sets the animation sequence to remain on the current animation unless manually changed with the left/right arrow buttons

Code Features and Functionality

The complete code to create Bluetooth-responsive animations on the Infinity Mirror Collar can be found at the bottom of this page. This section will examine how specific features are implemented in the CircuitPython code.

Controlling LED Animations

Writing LED animation code that runs smoothly while switching patterns and responding to button and Bluetooth inputs can be a challenge. Fortunately, the adafruit_led_animation library has a simple interface which handles animation timing and sequencing behind the scenes. This learn guide provides a great overview of how to use the library to easily generate a number of great-looking, customizable sequences.

All you need to know to use the library is how to create different animations, and place them in a sequence. The first step is to import the modules for the required animations, as shown below.

# LED Animation modules
from adafruit_led_animation.animation.comet import Comet
from adafruit_led_animation.animation.chase import Chase
from adafruit_led_animation.animation.rainbowcomet import RainbowComet
from adafruit_led_animation.animation.pulse import Pulse
from adafruit_led_animation.sequence import AnimationSequence
from adafruit_led_animation.color import colorwheel

Once the modules are imported, we'll instantiate different animation objects with various parameters, such as color and speed, that determine their appearance.

The code below creates four animation objects. The first three animations run repeatedly in sequence, and the last one, pulse, displays briefly when the user selects a new color with the Bluefruit App.

The code below creates an AnimationSequence that automatically takes care of running and transitioning between the comet, rainbow_comet, and chase animations we've created.

# Create the animations
comet = Comet(pixels, speed=0.06, color=(180,0,255), tail_length=10, bounce=True)
chase = Chase(pixels, speed=0.05, size=3, spacing=3, color=(0,255,255), reverse=True)
rainbow_comet = RainbowComet(pixels, speed=.06)
pulse = Pulse(pixels, speed=.04, color=(255,0,0), period = 0.2)

# Our animations sequence
seconds_per_animation = 10
animations = AnimationSequence(comet, rainbow_comet, chase, advance_interval=seconds_per_animation, auto_clear=True)

Even though we set the colors of comet and chase animation objects when we created them, we can still change them afterwards, by setting their color property. In the code, we define a random_color_mode variable which, when set to True, changes the animation colors in each cycle.

The AnimationSequence object allows us to register a function which will run after each complete animation cycle using its add_cycle_complete_receiver() method. The code below defines a function that, when random_color_mode is enabled, changes the comet and chase animation colors to a new, randomly selected value at the end of every cycle.

# Mode changes the color of random animations randomly
random_color_mode = True

def random_animation_color(anims):
    if random_color_mode:
        anims.color = colorwheel(random.randint(0,255))
        
animations.add_cycle_complete_receiver(random_animation_color)

Displaying an Animation out of Sequence

The code signals that it has received input from the Bluefruit App color picker by setting the animation colors and displaying a pulse animation in the newly selected color.

Changing the default colors of all of the animations is a simple matter of changing their color property. The animation sequence automatically propagates that change to all of its child animations.

The code switches between the animation sequence and the pulse animation by creating a variable, named current_display, which holds whatever animation or animation sequence is currently being shown. It is initially set to display the animation sequence.

# Current display determines whether we are showing the animation sequence or the pulse animation
current_display = animations

Since both animation and animation display objects have an animate() method, a single function call can run either animation in the main loop.

current_display.animate()

When new color information is received, the default animation colors are changed and the current_display variable is set to the pulse animation.

elif isinstance(packet, ColorPacket):
                    animations.color = packet.color
                    pulse.color = packet.color
                    # temporarily change to pulse display to show off the new color
                    current_display = pulse

The pulse animation will automatically set the display back to the main animation sequence when it finishes, because we have registered a special function that it will run after every three cycles.

# After we complete three pulse cycles, return to main animations list
def pulse_finished(anim):
    global current_display
    current_display = animations

pulse.add_cycle_complete_receiver(pulse_finished)
pulse.notify_cycles = 3

Bluetooth Functionality

The BLE code checks for data packets and processes any button or color packets it receives in the main loop. If it is not connected it will start advertising so that it is available for a connection from the Bluefruit app.

if ble.connected:
            if uart_service.in_waiting:
                #Packet is arriving
                packet = Packet.from_stream(uart_service)
                if isinstance(packet, ButtonPacket) and packet.pressed:
                    if packet.button == ButtonPacket.BUTTON_1:
                        # Animation colors change to a random new value after every animation sequence
                        random_color_mode = True
                    elif packet.button == ButtonPacket.BUTTON_2:
                        # Animation colors stay the same unless manually changed
                        random_color_mode = False
                    elif packet.button == ButtonPacket.BUTTON_3:
                        # Stay on the same animation
                        animations._advance_interval = None
                    elif packet.button == ButtonPacket.BUTTON_4:
                        # Auto-advance animations
                        animations._advance_interval = seconds_per_animation*1000
                    elif packet.button == ButtonPacket.LEFT:
                        # Go to first animation in the sequence
                        animations.activate(0)
                    elif packet.button == ButtonPacket.RIGHT:
                        # Go to the next animation in the sequence
                        animations.next()
                elif isinstance(packet, ColorPacket):
                    animations.color = packet.color
                    pulse.color = packet.color
                    # temporarily change to pulse display to show off the new color
                    current_display = pulse

        else:
            if not ble.advertising:
                ble.start_advertising(advertisement)

Charging Mode

As mentioned earlier, we don't want the LEDs to run if we've only connected the collar to power to charge it. The code contains a charge_mode variable which, when set to True, disables Bluetooth connections and turns off the display. Charge mode is toggled on and off by pressing the SW button on the Itsy Bitsy nRF5840.

# Set charge_mode to True to turn off the LED Animations and Bluetooth
#  e.g. when charging the battery
charge_mode = False

The CircuitPython adafruit_debouncer module, used in the code below lets us easily track and respond to button presses.

# Create a switch from the ItsyBity's on-board pushbutton to toggle charge mode
mode_pin = digitalio.DigitalInOut(board.SWITCH)
mode_pin.direction = digitalio.Direction.INPUT
mode_pin.pull = digitalio.Pull.UP
switch = Debouncer(mode_pin)

The Debouncer module removes the uncertainty as to whether a button press is real or an artifact of voltage fluctuations on a pin. You can learn more about debouncing a switch pin here.  Our code attaches a debouncer object to the ItsyBitsy's SWITCH pin, allowing us to easily track when the state of the pin has changed due to the button being pressed or released.

In each cycle of the main code loop, we call the function check_switch(), shown below, which updates our debouncer and tracks and responds to changes in the pin state. When set to charging mode, the code turns off all the LEDs, disconnects any BLE connections, and turns off BLE advertising.

# Checks the ItsyBitsy's switch button
def check_switch():
    global charge_mode
    switch.update()
    if switch.fell:  #Switch changed state
        charge_mode = not charge_mode
        # if display has just been turned off, clear all LEDs, disconnect, stop advertising
        if charge_mode:
            pixels.fill((0,0,0))
            pixels.show()
            if ble.connected:
                for conn in ble.connections:
                    conn.disconnect()
            if ble.advertising:
                ble.stop_advertising()

This guide was first published on Feb 02, 2021. It was last updated on Feb 02, 2021.

This page (CircuitPython Code) was last updated on Jun 10, 2023.

Text editor powered by tinymce.