EmbeddedRelated.com
Blogs

ANCS and HID: Controlling Your iPhone From Zephyr

Mohammed BillooJune 11, 2024

Introduction

In a previous blog post (https://embeddedrelated.com/showarticle/1630.php), we learned how we can configure our nRF52840 development kit (https://www.nordicsemi.com/Products/Development-ha...) as a Bluetooth Low Energy (BLE) device using The Zephyr Project. Similarly, we learned how to advertise standard and custom capabilities. 

In this blog post, we will take things further and learn how to perform useful and fun tasks on an nRF58240 device using The Zephyr Project. We will learn how to use our nRF58240 and The Zephyr Project to serve as a hands-free device for our iPhone. Specifically, we will perform the following tasks:

  1. Launch Siri
  2. Play the next or previous track if music is being played
  3. Play or pause the current track if music is being played
  4. Raise or lower the phone's volume
  5. Answer or decline an incoming call

Items 1 - 4 above can be performed by emulating a keyboard over BLE using the Human Interface Device (HID) protocol and service provided by BLE. HID predates BLE and is a protocol keyboard use to send keys to a PC. Items 2 - 4 have explicit keys assigned to them to perform the corresponding task. However, item 1, to launch Siri, requires someone to hold down the power button for 2 seconds and release it (try it for yourself if you have an iPhone). 

This article is available in PDF format for easy printing

Item 5 above uses a custom BLE service created by Apple called Apple Notification Center Service (ANCS). ANCS is a mechanism Apple uses to relay incoming notifications to a remote device over BLE. It also allows devices to respond positively or negatively to such notifications. For example, when we receive a call on our iPhone, the notification is sent to remote clients over BLE. Consequently, those clients can respond positively to the notification (to accept the call) or negatively (to decline the call).

If you need a custom embedded software solution leveraging The Zephyr Project, please contact me at mab@mab-labs.com.

Let's get started!

HID

First, we will tackle the simpler task of sending key presses associated with the HID service. As we have learned in previous blog posts, we first need to configure Zephyr to include support for the HID service with the following configuration options:

CONFIG_BT_HIDS=y
CONFIG_BT_HIDS_MAX_CLIENT_COUNT=2
CONFIG_BT_HIDS_DEFAULT_PERM_RW_ENCRYPT=y

Then, in the source code itself, we need to inform the iPhone of the keys we support via an "HID report," which is a series of bytes containing details of the HID service. For example, in our application, the report map consists of the following:

0x05, 0x01,       /* Usage Page (Generic Desktop) */
0x09, 0x06,       /* Usage (Keyboard) */
0xA1, 0x01,       /* Collection (Application) */
0x85, 0x01,       /* Report ID 1 */
/* Keys */
0x05, 0x07,       /* Usage Page (Key Codes) */
0x19, 0xe0,       /* Usage Minimum (224) */
0x29, 0xe7,       /* Usage Maximum (231) */
0x15, 0x00,       /* Logical Minimum (0) */
0x25, 0x01,       /* Logical Maximum (1) */
0x75, 0x01,       /* Report Size (1) */
0x95, 0x08,       /* Report Count (8) */
0x81, 0x02,       /* Input (Data, Variable, Absolute) */
0x95, 0x01,       /* Report Count (1) */
0x75, 0x08,       /* Report Size (8) */
0x81, 0x01,       /* Input (Constant) reserved byte(1) */
0x95, 0x06,       /* Report Count (6) */
0x75, 0x08,       /* Report Size (8) */
0x15, 0x00,       /* Logical Minimum (0) */
0x25, 0x65,       /* Logical Maximum (101) */
0x05, 0x07,       /* Usage Page (Key codes) */
0x19, 0x00,       /* Usage Minimum (0) */
0x29, 0x65,       /* Usage Maximum (101) */
0x81, 0x00,       /* Input (Data, Array) Key array(6 bytes) */
0xC0,             /* End Collection (Application) */
// Report ID 2: Advanced buttons (consumer control)
0x05, 0x0C,      // Usage Page (Consumer)
0x09, 0x01,      // Usage (Consumer Control)
0xA1, 0x01,      // Collection (Application)
0x85, 0x02,      //     Report Id (2 => Consumer Control)
0x15, 0x00,      //     Logical minimum (0)
0x25, 0x01,      //     Logical maximum (1)
0x75, 0x01,      //     Report Size (1)
0x95, 0x01,      //     Report Count (1)
0x09, 0xE9       // Volume Up
0x81, 0x02,      //     Input (Data,Value,Relative,Bit Field)
0x09, 0xEA,      // Volume Down
0x81, 0x02,      //     Input (Data,Value,Relative,Bit Field)
0x09, 0xCD,      // Play pause
0x81, 0x02,      //     Input (Data,Value,Relative,Bit Field)
0x09, 0xE2,      // Mute
0x81, 0x02,      //     Input (Data,Value,Relative,Bit Field)
0x09, 0xB5,      // Next track
0x81, 0x02,      //     Input (Data,Value,Relative,Bit Field)
0x09, 0xB6,      // Prev track
0x81, 0x02,      //     Input (Data,Value,Relative,Bit Field)
0x09, 0x30,      // Power
0x81, 0x02,      //     Input (Data,Value,Relative,Bit Field)
0x09, 0x32,      // Sleep
0x81, 0x02,      //     Input (Data,Value,Relative,Bit Field)
0xC0             // End Collection

The report map consists of the following items:

  • First, we define the usage type of our HID device. Specifically, we inform the iPhone that we are a keyboard for generic desktop use.
  • Then, we have a series of "collections" defining certain HID service characteristics:
    • The first collection outlines details of the application in general. For example, we specify that will be sending key codes to the iPhone.
    • The second collection defines the details of the keys we will send. Specifically, it informs the iPhone that we are a multimedia device (or "consumer control") and that we will be sending keys specified in the introduction of this blog post.

Then, when we wish to send a particular key to the iPhone we simply have to reference the index of the button defined in the report map. For example, the above report map shows that the Volume Up key is first in the report map (or index 0). Thus, we can send the Volume Up key using the following snippet:

#define KEY_PRESS_MAX     6
#define INPUT_REPORT_KEYS_MAX_LEN (1 + 1 + KEY_PRESS_MAX)
#define OUTPUT_REPORT_MAX_LEN  1
#define INPUT_REP_MEDIA_PLAYER_LEN  1
.
.
.
/* HIDS instance. */
BT_HIDS_DEF(hids_obj,
            OUTPUT_REPORT_MAX_LEN,
            INPUT_REP_MEDIA_PLAYER_LEN,
            INPUT_REPORT_KEYS_MAX_LEN);
.
.
.
uint8_t data = BIT(0); // Bit 0 corresponds to volume up in the report map
int err = bt_hids_inp_rep_send(&hids_obj, conn, 1, data, 1, NULL);

After defining the HID service in Zephyr, we can call the "bt_hids_ind_rep_send" function defined in the Zephyr BLE stack. We pass in the HID service instance, the BLE connection data structure, and a byte with the bit corresponding to the key index in the report map set. 

While the above implementation can be used for most of the key presses, launching Siri is the exception. As we mentioned earlier, Siri can be launched by pressing down the power button for 2 seconds and releasing it. Pressing and releasing a specific button can be implemented using the function shown below:

static void button_press_release(bool pressed, int cmd_index)
{
    uint8_t data = 0;
    if (pressed)
        data = BIT(cmd_index);
    bt_hids_inp_rep_send(&hids_obj, conn, 1, data, 1, NULL);
}

The function above sets the bit corresponding to the key press index depending on whether the pressed argument is true or false. Thus, if we want to activate Siri, we can call the above function in the following manner:

button_press_release(true, 6); // power button is index 6
k_sleep(K_MSEC(2000));
button_press_release(false, 6);

ANCS

Now that we understand how we can control key multimedia features on our iPhone from a nRF52840 development kit running Zephyr, we can see how to accept or decline incoming calls using the Apple Notification Client Service. Nordic has a great sample, based on The Zephyr Project, demonstrating how to use ANCS. It can be found here: https://developer.nordicsemi.com/nRF_Connect_SDK/d...

The first thing we need to do to use the ANCS client service in Nordic's Zephyr library is to initialize it using the snippet shown below:

static struct bt_ancs_client ancs_c;
.
.
.
bt_ancs_client_init(&ancs_c);

Then, we need to register which notifications we wish to receive from the iPhone, using the snippet below:

bt_ancs_register_attr(&ancs_c,
                BT_ANCS_NOTIF_ATTR_ID_POSITIVE_ACTION_LABEL,
                attr_posaction, ATTR_DATA_SIZE);

In the above snippet, we are requesting notifications with a "positive action" associated with them. Similarly, we can use the following snippet to request notifications with a "negative action":

bt_ancs_register_attr(&ancs_c,
                BT_ANCS_NOTIF_ATTR_ID_NEGATIVE_ACTION_LABEL,
                attr_negaction, ATTR_DATA_SIZE);

Both of those requests would allow us to receive notifications associated with phone calls.

Finally, we need to subscribe to receive notifications from ANCS using the following snippet:

bt_ancs_subscribe_notification_source(ancs_c,
                        bt_ancs_notification_source_handler);

The second argument passed is the callback called by the Nordic ANCS stack when a notification is sent from an iPhone. The following shows a sample implementation of the function:

static struct bt_ancs_evt_notif notification_latest;
static bool incoming_call = false;
.
.
.
static void bt_ancs_notification_source_handler(struct bt_ancs_client *ancs_c,
                int err, const struct bt_ancs_evt_notif *notif)
{
    notification_latest = *notif;
    if ((notif->category_id == 1)) {
        if (notif->evt_id == 0) {
            printk("MAB %s -- got an incoming call!\n", __func__);
            incoming_call = true;
        } else {
            printk("MAB %s -- no longer receiving an incoming call!\n", __func__);
            incoming_call = false;
        }
    }
}

As we can see in the above snippet, we have two global variables that store the latest notification and whether a call is incoming.  Apple defines which category ID (denoted by category_id above) and event ID (denoted by evt_id above) correspond to an incoming call (that document can be found here: https://developer.apple.com/library/archive/docume...). For example, in the images below, we can see that a category ID of "1" corresponds to an incoming call and that an event ID of "0" corresponds to a notification was added:

Thus, in the above callback, we check if these conditions are met to determine if we have an incoming call and update the corresponding global variables. Thus, if the user intends to take action, all we need to do is send either a positive or negative action against the global variable associated with the most recent notification. This is accomplished using the "bt_ancs_notification_action" function, as shown below:

.
.
.
if (incoming_call) {
    bt_ancs_notification_action(
    &ancs_c, notification_latest.notif_uid,
    BT_ANCS_ACTION_ID_POSITIVE,
    bt_ancs_write_response_handler);
}
.
.
.

Summary

In this blog post, we learned about BLE services that can be used to control an iPhone from a Nordic nRF52840 using The Zephyr Project. Specifically, we learned how to control certain multimedia functionality using the HID service. Finally, we learned how he ANCS client library provided by Nordic in The Zephyr Project can be used to accept or decline an incoming call.



To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: