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 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:
# 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!
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.
# 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.
# 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()
To use with CircuitPython, you need to first install a few libraries, into the lib folder on your CIRCUITPY drive. Then you need to update code.py with the example script.
Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, open the directory ItsyBitsy_DotStar_Necklace/ and then click on the directory that matches the version of CircuitPython you're using and copy the contents of that directory to your CIRCUITPY drive.
Your CIRCUITPY drive should now look similar to the following image:

# SPDX-FileCopyrightText: 2020 Anne Barela for Adafruit Industries # # SPDX-License-Identifier: MIT 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()
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!