CircuitPython Code Walkthrough

Inspiration

The code for this project is based on two previous projects on the Adafruit Learn System: John Park's Bluefruit TFT Gizmo ANCS Notifier for iOS and Becky Stern's Buzzing Mindfulness Bracelet. Be sure to check out both of those Learn Guides for additional project inspiration.

Setup

CircuitPython Libraries

The CircuitPython code begins by importing the libraries.

Download: file
import time
import board
import busio
import neopixel
import adafruit_drv2605
import adafruit_ble
from adafruit_ble.advertising.standard import SolicitServicesAdvertisement
from adafruit_ble.services.standard import CurrentTimeService
from adafruit_ble_apple_notification_center import AppleNotificationCenterService
from digitalio import DigitalInOut, Direction

LEDs and Motors

After the libraries are imported, the onboard NeoPixel is setup as pixel_pin. This NeoPixel will serve as the visual notifier on the Buzzy Box.

Download: file
pixel_pin = board.NEOPIXEL
num_pixels = 1

pixel = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.3, auto_write=False)

Up next, the DRV2605L driver breakout is setup to communicate over I2C. This driver is what powers the haptic motor to make the Buzzy Box buzz.

Download: file
i2c = busio.I2C(board.SCL, board.SDA)
drv = adafruit_drv2605.DRV2605(i2c)

In addition to the onboard NeoPixel, the onboard blue LED on the Feather Sense is also used for this project. This LED is located towards the back of the Feather, next to the nRF52840 chip. Later in the code, it will indicate whether or not your iOS device is connected to BLE.

Download: file
blue_led = DigitalInOut(board.BLUE_LED)
blue_led.direction = Direction.OUTPUT

Setting Up BLE

Next in the setup is BLE, allowing for BLE connectivity and functionality to be accessed via the adafruit_ble library.

Download: file
ble = adafruit_ble.BLERadio()
if ble.connected:
    for c in ble.connections:
        c.disconnect()

There are a lot of ways to use BLE, but in this project the Apple Notification Center Service (ANCS) and Current Time Service are used. These two services are called out to be connected to when a BLE connection is established.

Download: file
advertisement = SolicitServicesAdvertisement()

advertisement.solicited_services.append(AppleNotificationCenterService)
advertisement.solicited_services.append(CurrentTimeService)

State Machines

Following the setup are state machines that will be used in the loop. Their purpose is comment below in the code.

Download: file
current_notification = None #  tracks the current notification from ANCS
current_notifications = {} #  array to hold all current notifications from ANCS
cleared = False #  state to track if notifications have been cleared from ANCS
notification_service = None #  holds the array of active notifications from ANCS
all_ids = [] #  array to hold all of the ids from ANCS
hour = 0 #  used to track when it is on the hour for the mindfulness reminder
mindful = False #  state used to track if it is time for mindfulness
vibration = 16 #  vibration effect being used for the haptic motor
blue = (0, 0, 255) #  color blue for the NeoPixel
purple = (255, 0, 255) #  color purple for the NeoPixel
red = (255, 0, 0) #  color red for the NeoPixel
clear = (0, 0, 0) #  allows for NeoPixel to be turned 'off'

Putting the "Fun" in Function

There are two functions that will be called in the loop to control the Feather's NeoPixel and the haptic motor.

blink_pixel() allows you to blink the NeoPixel with parameters for how many time you want it to blink (blinks), how fast you want it to blink (speed) and then the two colors that the NeoPixel will blink between (color1 and color2).

If you were to discretely code multiple blinks for the NeoPixel line by line it could get really long. This lets you blink it as many times as you want with one line of code in the loop.

Download: file
def blink_pixel(blinks, speed, color1, color2):
    for _ in range(0, blinks):
        pixel.fill(color1)
        pixel.show()
        time.sleep(speed)
        pixel.fill(color2)
        pixel.show()
        time.sleep(speed)

vibe() does something similar to blink_pixel() but for the haptic motor. It allows you to vibrate the motor multiple times with parameters for  the number of times you want the motor to vibrate (num_zzz), the motor effect type (effect) and the length of the delay between each vibration (delay).

Download: file
def vibe(num_zzz, effect, delay):
    drv.sequence[0] = adafruit_drv2605.Effect(effect)
    for _ in range(0, num_zzz):
        drv.play()
        time.sleep(delay)
        drv.stop()

Finally, the setup is wrapped up with BLE starting to advertise its signal for your iOS device to connect to.

Download: file
ble.start_advertising(advertisement)

The Loop

Waiting for BLE

The loop begins with some visual notifications to let you know that BLE is not connected. If BLE is not connected, the onboard NeoPixel will be red and the onboard blue LED will be off. "Waiting for connection" will also print out to the REPL. Once BLE is connected, "Connected" will print to the REPL.

Download: file
while True:
    
    blue_led.value = False
    print("Waiting for connection")
    while not ble.connected:
        blue_led.value = False
        pixel.fill(red)
        pixel.show()
        pass
    print("Connected")

BLE Setup: Apple Notification Center Service and CurrentTimeService

A few other things occur once a BLE connection is established. The onboard blue LED will turn on and the NeoPixel will turn off.

The Feather nRF52840 will also pair with your iOS device in order to have access to the Apple Notification Center Service (ANCS).

notification_service holds the connection to ANCS. These notifications are stored in an array. Additionally, current_notifications holds the current notifications that are active in ANCS.

Download: file
while ble.connected:
        blue_led.value = True
        all_ids.clear()
        for connection in ble.connections:
            if not connection.paired:
                connection.pair()
                print("paired")
            cts = connection[CurrentTimeService]
            notification_service = connection[AppleNotificationCenterService]
        current_notifications = notification_service.active_notifications

Tracking Notifications

In this for statement, notification is setup to hold the notification ID's in an array for the current notifications in ANCS. The notification ID has all of the information about the notification: the time stamp, the app name, the information in the notification, etc. The notification ID's are also added to the all_ids array.

Download: file
for notif_id in current_notifications:
            notification = current_notifications[notif_id]
            all_ids.append(notif_id)

Dismissing Notifications

Next, two if statements take care of handling what happens when a notification has been cleared and is no longer active.

In the first if statement, current_notification is reset to None and the NeoPixel, which functions as a visual cue that you have a notification, is turned off.

Download: file
if current_notification and current_notification.removed:
            # Stop showing the latest and show that there are no new notifications.
            current_notification = None
            pixel.fill(clear)
            pixel.show()

The second if statement, checks that current_notification is inactive and that all_ids is empty. If that is correct, then cleared is updated to True to demonstrate that all of the notifications in ANCS are currently cleared.

Download: file
if not current_notification and not all_ids and not cleared:
            cleared = True
            pixel.fill(clear)
            pixel.show()

Indexing Notifications

The next portion revolves around what happens if a notification is active in ANCS.

First, cleared is updated to False and then the notification ID's are indexed. This allows for new notifications to trigger the Feather's NeoPixel and the haptic motor even if older notifications have not been cleared. The newest notification is then stored in notif_id.

Download: file
elif all_ids:
  cleared = False
  if current_notification and current_notification.id in all_ids:
    index = all_ids.index(current_notification.id)
  else:
    index = len(all_ids) - 1
    notif_id = all_ids[index]

Notification Alerts with Haptic Motors and NeoPixels

The following if statement is where the action is for a notification triggering the Feather NRF52840.

If the state of current_notification.id does not match the notification index that was just stored in notif_id, then the code knows that a new notification is present.

current_notification is updated to hold this new notification and category holds all of the metadata for the notification as a string. Storing it in this way allows it to be printed to the REPL and also accessed in the code.

Download: file
if not current_notification or current_notification.id != notif_id:
  current_notification = current_notifications[notif_id]
  category = str(notification).split(" ", 1)[0]

The haptic motor vibrates to alert you to the new notification. This is done with the vibe() function.

Following the haptic motor, the notification's metadata is printed out to the REPL. The information included is the ID number, category of notification, app name, title, subtitle and the message in the notification. The notifications are separated by 36 dashes.

Download: file
vibe(2, vibration, 0.5)
print('-'*36)
print("Msg #%d - Category %s" % (notification.id, category))
print("From app:", notification.app_id)
if notification.title:
      print("Title:", notification.title)
if notification.subtitle:
      print("Subtitle:", notification.subtitle)
if notification.message:
      print("Message:", notification.message)

After the information has been printed to the REPL, the onboard NeoPixel will blink purple with the blink_pixel() function and then stay on until the notifications are cleared, just in case you aren't around to notice the haptic motor or blinking.

Download: file
blink_pixel(2, 0.5, purple, clear)
pixel.fill(purple)
pixel.show()

That wraps up the ANCS notification portion of the loop. Beyond notifications though, you may want to keep track of time while you're deep in emails, video games or Fusion360. Luckily, there's BLE functionality to help with that too.

Mindfulness

Using the CurrentTimeService BLE library, you can access your connected device's clock to sync up with the Feather NRF52840.

In this instance, you'll use CurrentTimeService to check when a new hour begins (9:00, 10:00, 11:00, etc). If it's on the hour, the haptic motor will vibrate and the NeoPixel will blink to alert you. This can help remind you to get up and stretch or to be reminded of the time without having to look at a clock or your devices.

The information from CurrentTimeService is stored in cts after being setup earlier in the loop. The information is stored as an array, with all of the time data separated in their own entries. The minutes for the hour is indexed as 4 in the array. You can check a predetermined value, in this case hour, to see if it matches the current time being held by cts.

In the case of the code, if cts.current_time[4] matches with hour, which is set to 0, then the current time is printed to the REPL along with the string "mindful time". The haptic motor vibrates with the vibe() function and the onboard NeoPixel blinks using the blink_pixel() function, this time in blue.

Download: file
if cts.current_time[4] == hour and not mindful:
	print(cts.current_time[4])
	print("mindful time")
	vibe(5, vibration, 1)
	blink_pixel(5, 1, blue, clear)

The mindful state is set to True and the onboard NeoPixel will remain blue for the duration of the minute.

Download: file
mindful = True
pixel.fill(blue)
pixel.show()
print("hour = ", hour)

Once one minute after the hour occurs, the mindful state is reset to False and the onboard NeoPixel is turned off. The string "mindful time over" is also printed to the REPL. This process will occur every hour alongside the ANCS notifications.

Dropped Signals

The code closes with a safety net for lost BLE connections. If the Feather becomes disconnected from your iOS device, "Disconnected" will be printed to the REPL and the onboard blue LED will turn off. The nRF52840 will begin advertising the BLE connection again and notifcation_service will be reset to None. The loop will go back to the beginning to reconnect.

Download: file
print("Disconnected")
blue_led.value = False
print()
ble.start_advertising(advertisement)
notification_service = None
This guide was first published on May 13, 2020. It was last updated on May 13, 2020.
This page (CircuitPython Code Walkthrough) was last updated on Jun 01, 2020.