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.

import time
import board
import busio
import neopixel
import adafruit_drv2605
import adafruit_led_animation.color as color
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 vibration bracelet.

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 vibration bracelet vibrate.

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.

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.

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.

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 commented below in the code.

#  state machines
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

Which app is notifying me?

The vibration bracelet allows you to be away from your phone yet still be aware of any incoming notifications. The APP_COLORS array lists the application ID's of a few commonly used apps. Each app has its own unique application ID that is stored in ANCS. 

In addition to the notification ID's, NeoPixel colors are defined in the code and linked to each ID. These colors are from the adafruit_led_animations CircuitPython library. You can call on predefined colors with, for example, color.RED and have the NeoPixel light-up red without any additional setup.

This array will be used in the loop to have the onboard NeoPixel light-up a specific color depending on the app, letting you know from a distance if you need to check your phone or if it can wait for later.

APP_COLORS = {
    "com.basecamp.bc3-ios": color.YELLOW, #  Basecamp
    "com.apple.MobileSMS": color.GREEN, #  Texts
    "com.hammerandchisel.discord": color.PURPLE, #  Discord
    "com.apple.mobilecal": color.CYAN, #  Calendar
    "com.apple.mobilephone": color.GREEN, #  Phone
    "com.google.ios.youtube": color.ORANGE, #  YouTube
    "com.burbn.instagram": color.MAGENTA, #  Instagram
    "com.apple.mobilemail": color.CYAN #  Apple Email
}
You can also edit this array to have more or less apps depending on your preferences. The same is true for the NeoPixel colors.

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.

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).

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.

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.

while True:
    
    blue_led.value = False
    print("Waiting for connection")
    while not ble.connected:
        blue_led.value = False
        pixel.fill(color.RED)
        pixel.show()
    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.

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.

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.

From the adafruit_led_animations library, color.BLACK means that the NeoPixel is off with RGB values of (0, 0, 0).

if current_notification and current_notification.removed:
            # Stop showing the latest and show that there are no new notifications.
            current_notification = None
            pixel.fill(color.BLACK)
            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.

if not current_notification and not all_ids and not cleared:
            cleared = True
            pixel.fill(color.BLACK)
            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.

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.

The app_id is checked against the array of application ID's in the APP_COLORS array. If the current notification is not from one of those apps, then the NeoPixel will be white. However, if the notification is in that array, then the NeoPixel will light-up as the color defined in the array. The color for the NeoPixel is stored as notif_color.

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.

if not current_notification or current_notification.id != notif_id:
                current_notification = current_notifications[notif_id]
                #  if the notification is from an app that is not
                #  defined in APP_COLORS then the NeoPixel will be white
                if current_notification.app_id not in APP_COLORS:
                    notif_color = color.WHITE
                #  if the notification is from an app defined in
                #  APP_COLORS then the assigned color will show
                else:
                    notif_color = APP_COLORS[current_notification.app_id]
                #  parses notification info into a string
                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.

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 the previously defined notif_color 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.

#  NeoPixel blinks and then stays on until cleared
blink_pixel(2, 0.5, notif_color, color.BLACK)
pixel.fill(notif_color)
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, taking a walk 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.

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, color.BLUE, color.BLACK)

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

mindful = True
pixel.fill(color.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.

if cts.current_time[4] == (hour + 1) and mindful:
            #  NeoPixel turns off
            mindful = False
            pixel.fill(color.BLACK)
            pixel.show()
            print("mindful time over")

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.

print("Disconnected")
blue_led.value = False
print()
ble.start_advertising(advertisement)
notification_service = None

This guide was first published on May 19, 2020. It was last updated on May 19, 2020.

This page (CircuitPython Code Walkthrough) was last updated on Jan 05, 2023.

Text editor powered by tinymce.