This necklace code uses the excellent Adafruit Bluefruit app, you can get it free for both Android and iOS. Read more about what this app can do in its official guide: Bluefruit LE Connect for iOS and Android!

Now that you've setup CircuitPython on the ItsyBitsy, loaded the requisite libraries, and downloaded the app from the links above, we can take a look at the code! Skip towards the bottom of this page if you want to see the entire code right away. We'll go through some of the main parts of the code before then.

The main while loop will be structured like this:

Download: file
# Initial empty state
state = ""
 
while True:
    # Advertise when not connected.
    ble.start_advertising(advertisement)
 
    while not ble.connected:
        # do something while ble is not yet connected
 
    while ble.connected:
        # Receive packets from Adafruit Bluefruit app
        # Set the state to prevent redundant hits
        # Act upon the current state
        # Also handle touch interrupt

The behavior when there is no BLE connection is simple: we detect touch, and then randomly choose Y or N to display. If there's no touch, we display a default, very classy, twinkling animation. Now, you can ask a question of the necklace, and the random number gods will give you the answer you seek!

Download: file
if touch.value:
  yes_or_no() # Randomly displays 'Y' for yes and 'N' for no.
else:
  twinkle() # Keep it classy.

When we are connected over BLE, we then watch out for any packets that come in from the app, and we set the value of the state variable accordingly. Setting this state will keep behavior deterministic and will prevent any app double-taps from affecting the necklace. Note that to keep it simple, I've opted to simply use strings and if-statements to track the state here, but you could also use constant variables and/or other data structures to optimize further.

Download: file
# Set state string based on pressed button from Bluefruit app
# and to prevent redundant hits
if isinstance(packet, ButtonPacket) and packet.pressed:
  # UP button pressed
  if packet.button == ButtonPacket.UP and state != "chase":
    state = "chase"
    # DOWN button
    elif packet.button == ButtonPacket.DOWN and state != "comet":
      state = "comet"
      # ...

Finally, we display animations based on the value of the state string, and we choose a different default animation just to indicate to the necklace viewer that Bluetooth is indeed connected. Touch is in a separate if-statement so that it can be triggered in the middle of the other animations.

Download: file
# Touch is handled as a separate state
if touch.value:
    yes_or_no()

# Act upon the state
if state == "chase":
    chase.animate()
elif state == "comet":
    rainbow_comet.animate()
elif state == "rainbowchase":
    rainbow_chase.animate()
elif state == "hello":
    pixels.fill(0)
    scroll_text(packet, SCROLL_TEXT_CUSTOM_WORD)
else:
    chase.animate()

Full code

The rest of the code are organized into utility functions that handle separate animations or responsibilities. You can copy and paste the entire code below into code.py using the mu-editor or any other preferred text editor, and hit save! The code has a few comments spread throughout that will help explain what the code is doing.

import time
import adafruit_dotstar
import board
import random
import touchio
from adafruit_pixel_framebuf import PixelFramebuffer
from adafruit_led_animation.animation.rainbowchase import RainbowChase
from adafruit_led_animation.animation.rainbowcomet import RainbowComet
from adafruit_led_animation.animation.chase import Chase
from adafruit_led_animation.color import PINK

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

################################################################################
# Customize variables

# Set capacitive touch pin
TOUCH_PIN = board.D11

# These are the pixels covered by the brass cap touch
# We will try to avoid using these pixels in the "twinkle" default animation
COVERED_PIXELS = [40,41,42,48,49,50,56,57,58]

# Adjust this higher if touch is too sensitive
TOUCH_THRESHOLD = 3000

# Adjust SCROLL_TEXT_COLOR_CHANGE_WAIT lower to make the color changes for
# the text scroll animation faster
SCROLL_TEXT_COLOR_CHANGE_WAIT = 5

# Change this text that will be displayed when tapping 2 on the
# Bluefruit app control pad (after connecting on your phone)
SCROLL_TEXT_CUSTOM_WORD = "hello world"

# Increase number to slow down scrolling
SCROLL_TEXT_WAIT = 0.05

# How bright each pixel in the default twinkling animation will be
TWINKLE_BRIGHTNESS = 0.1

###############################################################################
# Initialize hardware

touch_pad = TOUCH_PIN
touch = touchio.TouchIn(touch_pad)
touch.threshold = TOUCH_THRESHOLD

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

# Colors
YELLOW = (255, 150, 0)
TEAL = (0, 255, 120)
CYAN = (0, 255, 255)
PURPLE = (180, 0, 255)
TWINKLEY = (255, 255, 255)
OFF = (0, 0, 0)

# Setup Dotstar grid and pixel framebuffer for fancy animations
pixel_width = 8
pixel_height = 8
num_pixels = pixel_width * pixel_height
pixels = adafruit_dotstar.DotStar(board.A1, board.A2, num_pixels, auto_write=False, brightness=0.1)
pixel_framebuf = PixelFramebuffer(
    pixels,
    pixel_width,
    pixel_height,
    rotation=1,
    alternating=False,
    reverse_x=True
)
# Fancy animations from https://learn.adafruit.com/circuitpython-led-animations
rainbow_chase = RainbowChase(pixels, speed=0.1, size=3, spacing=6, step=8)
chase = Chase(pixels, speed=0.1, color=CYAN, size=3, spacing=6)
rainbow_comet = RainbowComet(pixels, speed=0.1, tail_length=5, bounce=True, colorwheel_offset=170)


def scroll_framebuf_neg_x(word, color, shift_x, shift_y):
    pixel_framebuf.fill(0)
    color_int = int('0x%02x%02x%02x' % color, 16)

    # negate x so that the word can be shown from left to right
    pixel_framebuf.text(word, -shift_x, shift_y, color_int)
    pixel_framebuf.display()
    time.sleep(SCROLL_TEXT_WAIT)

def scroll_text(packet, word):
    # scroll through entire length of string.
    # each letter is always 5 pixels wide, plus 1 space per letter
    scroll_len = (len(word) * 5) + len(word)
    color_list = [CYAN, TWINKLEY, PINK, PURPLE, YELLOW]

    color_i = 0
    color_wait_tick = 0
    # start the scroll from off the grid at -pixel_width
    for x_pos in range(-pixel_width, scroll_len):
        # detect touch
        if touch.value:
            pixel_framebuf.fill(0)
            pixel_framebuf.display()
            return;

        # detect new packet
        if isinstance(packet, ButtonPacket) and packet.pressed:
            return;

        color = color_list[color_i]
        scroll_framebuf_neg_x(word, color, x_pos, 0)

        # Only change colors after SCROLL_TEXT_COLOR_CHANGE_WAIT
        color_wait_tick = color_wait_tick + 1
        if color_wait_tick == SCROLL_TEXT_COLOR_CHANGE_WAIT:
            color_i = color_i + 1
            color_wait_tick = 0

        if color_i == len(color_list):
            color_i=0

    # wait a bit before scrolling again
    time.sleep(.5)

# Manually chosen pixels to display "Y"
# in the proper orientation
def yes(color):
    pixels[26] = color
    pixels[27] = color
    pixels[28] = color
    pixels[36] = color
    pixels[44] = color
    pixels[21] = color
    pixels.show()
    time.sleep(0.1)
    pixels.fill(0)

# Manually chosen pixels to display "N"
# in the proper orientation
def no(color):
    pixels[26] = color
    pixels[19] = color
    pixels[12] = color
    pixels[27] = color
    pixels[28] = color
    pixels[29] = color
    pixels[30] = color
    pixels[37] = color
    pixels[44] = color
    pixels.show()
    time.sleep(0.1)
    pixels.fill(0)

def yes_or_no():
    pixels.fill(0)
    print(touch.raw_value)
    value = 0
    pick=0

    pick = random.randint(0,64)
    time.sleep(0.1)

    if pick % 2:
        print('picked yes!');
        yes(PINK)
        time.sleep(1)
    else:
        print('picked no!');
        no(TEAL)
        time.sleep(1)


def twinkle_show():
    pixels.brightness = TWINKLE_BRIGHTNESS
    pixels.show()
    time.sleep(.1)
    if touch.value:
        return;

def twinkle():
    # randomly choose 3 pixels
    spark1 = random.randint(0, num_pixels-1)
    spark2 = random.randint(0, num_pixels-1)
    spark3 = random.randint(0, num_pixels-1)

    # make sure that none of the chosen pixels are covered
    while spark1 in COVERED_PIXELS:
        spark1 = random.randint(0, num_pixels-1)
    while spark2 in COVERED_PIXELS:
        spark2 = random.randint(0, num_pixels-1)
    while spark3 in COVERED_PIXELS:
        spark3 = random.randint(0, num_pixels-1)

    # Control when chosen pixels turn on for dazzling effect
    pixels[spark1] = TWINKLEY
    pixels[spark2] = OFF
    pixels[spark3] = OFF
    twinkle_show()
    pixels[spark1] = TWINKLEY
    pixels[spark2] = TWINKLEY
    pixels[spark3] = OFF
    twinkle_show()
    pixels[spark1] = TWINKLEY
    pixels[spark2] = TWINKLEY
    pixels[spark3] = TWINKLEY
    twinkle_show()
    pixels[spark1] = OFF
    pixels[spark2] = TWINKLEY
    pixels[spark3] = TWINKLEY
    twinkle_show()
    pixels[spark1] = OFF
    pixels[spark2] = OFF
    pixels[spark3] = TWINKLEY
    twinkle_show()

    pixels.fill(OFF)
    pixels.show()
    time.sleep(0.6)

# Initial empty state
state = ""

while True:
    # Advertise when not connected.
    ble.start_advertising(advertisement)

    while not ble.connected:
        if touch.value:
            yes_or_no()
        else:
            twinkle()

    while ble.connected:
        # Set the state
        if uart_service.in_waiting:
            # Packet is arriving.
            packet = Packet.from_stream(uart_service)

            # set state string based on pressed button from Bluefruit app
            # and to prevent redundant hits
            if isinstance(packet, ButtonPacket) and packet.pressed:
                # UP button pressed
                if packet.button == ButtonPacket.UP and state != "chase":
                    state = "chase"
                # DOWN button
                elif packet.button == ButtonPacket.DOWN and state != "comet":
                    state = "comet"
                # 1 button
                elif packet.button == '1' and state != "rainbowchase":
                    state = "rainbowchase"
                # 2 button
                elif packet.button == '2' and state != "hello":
                    state = "hello"

        # Touch is handled as an interrupt state
        if touch.value:
            yes_or_no()

        # Act upon the state
        if state == "chase":
            chase.animate()
        elif state == "comet":
            rainbow_comet.animate()
        elif state == "rainbowchase":
            rainbow_chase.animate()
        elif state == "hello":
            pixels.fill(0)
            scroll_text(packet, SCROLL_TEXT_CUSTOM_WORD)
        else:
            chase.animate()
The Dotstar Matrix can get quite hot when all or most of the LEDs are on, so try to make sure you're testing any new animations on a table and checking how hot it gets first before wearing the necklace. However, the animations included above shouldn't result in too much heat.

Test capacitive touch

Now you can connect the necklace via the microB port to test that the twinkling animation and capacitive touch sensing is working. You may need to adjust the threshold of the capacitive touch if it's too sensitive or not sensitive enough.

Test Bluefruit app interaction

You can now test the interaction with the necklace using the Bluefruit app. Turn on the power switch, and open the Bluefruit app -- you should see your device like so:

 Then, press "Connect" and then "Controller" and then "Control Pad". You can then click on the following buttons in the Control Pad screen and see the animations change in the necklace:

  • Up button
  • Down button
  • "1" button
  • "2" buton

Go ahead and change the corresponding animations in the code to whatever you choose!

We're almost done. Head on over to final assembly to stuff everything into an enclosure!

This guide was first published on Dec 22, 2020. It was last updated on Dec 22, 2020.

This page (CircuitPython code) was last updated on Jan 25, 2021.

Text editor powered by tinymce.