The Apple Notification Center Service (ANCS) allows iOS devices to act as a provider of notification alerts to Bluetooth Low Energy (BLE) accessories, such as the Apple Watch, or in our case, the Circuit Playground Bluefruit!

We've created a simple program that allows the CPB to pair with your iOS device over Bluetooth and then it will receive ANCS notifications to display on the TFT Gizmo.

In general, we recommend installing the libraries mentioned on the previous page for your Circuit Playground Bluefruit projects, however you can get away with a subset of them for this one. You can see them listed in the image here.

You'll also need to add the adafruit_ble_apple_notification_center.mpy file from the library bundle as seen in the image here.

Once you save the code.py, graphics, and .wav file to your CIRCUITPY drive as directed below, this image is what your drive should look like.

Click the "Download: Project Zip" link in the code block below to get all the files from the project's GitHub repo.

Then, uncompress the zip file and open the code.py file in Mu, then save it to your CPB's CIRCUITPY drive as code.py. 

"""
This demo shows the latest icons from a connected Apple device on a TFT Gizmo screen.

The A and B buttons on the CircuitPlayground Bluefruit can be used to scroll through all active
notifications. The screen's backlight will turn off after a certain number of seconds to save power.
New notifications or pressing the buttons should turn it back on.
"""

import time
import board
import digitalio
import displayio
import adafruit_ble
from adafruit_ble.advertising.standard import SolicitServicesAdvertisement
from adafruit_ble_apple_notification_center import AppleNotificationCenterService
from adafruit_gizmo import tft_gizmo
from audiocore import WaveFile
from audiopwmio import PWMAudioOut as AudioOut

# Enable the speaker
speaker_enable = digitalio.DigitalInOut(board.SPEAKER_ENABLE)
speaker_enable.direction = digitalio.Direction.OUTPUT
speaker_enable.value = True

audio = AudioOut(board.SPEAKER)

# This is a whitelist of apps to show notifications from.
APP_ICONS = {
    "com.tinyspeck.chatlyio": "/ancs_slack.bmp",
    "com.basecamp.bc3-ios": "/ancs_basecamp.bmp",
    "com.apple.MobileSMS": "/ancs_sms.bmp",
    "com.hammerandchisel.discord": "/ancs_discord.bmp",
    "com.apple.mobilecal": "/ancs_ical.bmp",
    "com.apple.mobilephone": "/ancs_phone.bmp"
}

BLOCKLIST = []
DELAY_AFTER_PRESS = 15
DEBOUNCE = 0.1
DIM_TIMEOUT = 20   # Amount of timeout to turn off backlight
DIM_LEVEL = 0.05

a = digitalio.DigitalInOut(board.BUTTON_A)
a.switch_to_input(pull=digitalio.Pull.DOWN)
b = digitalio.DigitalInOut(board.BUTTON_B)
b.switch_to_input(pull=digitalio.Pull.DOWN)

file = open("/triode_rise.wav", "rb")
wave = WaveFile(file)

def play_sound():
    audio.play(wave)
    time.sleep(1)

def find_connection():
    for connection in radio.connections:
        if AppleNotificationCenterService not in connection:
            continue
        if not connection.paired:
            connection.pair()
        return connection, connection[AppleNotificationCenterService]
    return None, None

class Dimmer:
    def __init__(self):
        self._update_time = time.monotonic()
        self._level = DIM_LEVEL
        self._timeout = DIM_TIMEOUT

    def update(self):
        self._update_time = time.monotonic()

    def check_timeout(self):
        if a.value or b.value:
            self._update_time = time.monotonic()
        if time.monotonic() - self._update_time > self._timeout:
            if display.brightness > self._level:
                display.brightness = self._level
        else:
            if display.brightness == self._level:
                display.brightness = 1.0

dimmer = Dimmer()

# Start advertising before messing with the display so that we can connect immediately.
radio = adafruit_ble.BLERadio()
advertisement = SolicitServicesAdvertisement()
advertisement.complete_name = "CIRCUITPY"
advertisement.solicited_services.append(AppleNotificationCenterService)

def wrap_in_tilegrid(open_file):
    odb = displayio.OnDiskBitmap(open_file)
    return displayio.TileGrid(odb, pixel_shader=displayio.ColorConverter())

display = tft_gizmo.TFT_Gizmo()
group = displayio.Group(max_size=3)
group.append(wrap_in_tilegrid(open("/ancs_connect.bmp", "rb")))
display.show(group)

current_notification = None
current_notifications = {}
all_ids = []
last_press = time.monotonic()
active_connection, notification_service = find_connection()
cleared = False

while True:
    if not active_connection:
        radio.start_advertising(advertisement)

    while not active_connection:
        active_connection, notification_service = find_connection()
        dimmer.check_timeout()

    # Connected
    dimmer.update()
    play_sound()

    with open("/ancs_none.bmp", "rb") as no_notifications:
        group.append(wrap_in_tilegrid(no_notifications))
        while active_connection.connected:
            all_ids.clear()
            current_notifications = notification_service.active_notifications
            for notif_id in current_notifications:
                notification = current_notifications[notif_id]
                if notification.app_id not in APP_ICONS or notification.app_id in BLOCKLIST:
                    continue
                all_ids.append(notif_id)

            # pylint: disable=protected-access
            all_ids.sort(key=lambda x: current_notifications[x]._raw_date)
            # pylint: enable=protected-access

            if current_notification and current_notification.removed:
                # Stop showing the latest and show that there are no new notifications.
                current_notification = None

            if not current_notification and not all_ids and not cleared:
                cleared = True
                dimmer.update()
                group[1] = wrap_in_tilegrid(no_notifications)
            elif all_ids:
                cleared = False
                now = time.monotonic()
                if current_notification and current_notification.id in all_ids and \
                    now - last_press < DELAY_AFTER_PRESS:
                    index = all_ids.index(current_notification.id)
                else:
                    index = len(all_ids) - 1
                if now - last_press >= DEBOUNCE:
                    if b.value and index > 0:
                        last_press = now
                        index += -1
                    if a.value and index < len(all_ids) - 1:
                        last_press = now
                        index += 1
                notif_id = all_ids[index]
                if not current_notification or current_notification.id != notif_id:
                    dimmer.update()
                    current_notification = current_notifications[notif_id]
                    # pylint: disable=protected-access
                    print(current_notification._raw_date, current_notification)
                    # pylint: enable=protected-access
                    app_icon_file = open(APP_ICONS[current_notification.app_id], "rb")
                    group[1] = wrap_in_tilegrid(app_icon_file)

            dimmer.check_timeout()

        # Bluetooth Disconnected
        group.pop()
        dimmer.update()
        active_connection = None
        notification_service = None

Code Explaination

Apple devices centralize the management of notifications into the "Notification Center", which is accessed on the lock screen or after swiping down from the top of the screen. Since this info is centralized, Apple provides access to the current notifications through a Bluetooth Low Energy Service on the device. Bluetooth Services are a collection of data referred to as Characteristics. In CircuitPython libraries we provide definitions for common services like the Apple Notification Center Service (or ANCS for short) so that they are easier to use.

The ANCS library is designed for two main uses. First, it allows one to list all currently active notifications. This is done by reading the active_notifications attribute of the service. Second, it allows you to wait for new notifications to come in and react to them. This is done by looping over wait_for_new_notifications().

Both of these uses provide a Notification object for each active notification. Reading the attributes of these objects will load the data from the peer device as needed. This can make printing slow but it conserves data which in turn saves battery. So, only read the attributes you need to know. Printing the object is useful for debugging but it loads many attributes and therefore, takes time.

The most useful attribute for this project is the app_id. The app_id identifies which app generated the notification. It is not the app name, but may be derived from it or the name of the folks who created the app. In the example code.py we've already added a few app_ids. For example, Twitter is com.atebits.Tweetie2, Slack is com.tinyspeck.chatlyio and GMail is com.google.Gmail. To add support for an app you use, connect to the serial terminal while running the example the Apple Notification Center library's simple test, which prints out all of the current notification's app_ids and title.

This guide was first published on Dec 20, 2019. It was last updated on Dec 20, 2019.

This page (Code with CircuitPython) was last updated on Jun 12, 2021.

Text editor powered by tinymce.