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 set up CircuitPython on the ItsyBitsy and downloaded the app from the links above, we can take a look at the code!
Near the bottom of the page is the code. Click the Download Project Bundle button and save the zip file on your computer.
Plug your ItsyBitsy into your computer with a known good USB data+power cable.
In your File Explorer or Finder (depending on your operating system), navigate in the zip to the files for your CircuitPython version. Copy code.py and the lib directory to your ItsyBitsy which shows up as a thumb drive named CIRCUITPY.
Now your board has all the files. The CIRCUITPY drive should have all the files listed below. If not copy them from the zip 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!
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 in the lib folder 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.
# 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!
Page last edited January 18, 2025
Text editor powered by tinymce.