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.