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.
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
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
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)
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
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 }
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 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")
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
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)
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()
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]
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.
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")
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
Page last edited March 08, 2024
Text editor powered by tinymce.