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.
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.
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
- adafruit_ticks
- digitalio
- neopixel.mpy
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)
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
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.
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)
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
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)
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()
Page last edited January 18, 2025
Text editor powered by tinymce.