Skip to Main Content
Engineers at Punch Through Office

How to Delete Bonds with the Nordic nRF5 SDK

When we talk about Bluetooth-connected devices, an inherent part of that discussion is bond management. Bond management consists of storing and deleting information about a paired device so that the pairing process between two authenticated devices doesn’t need to be repeated on every connection. 

When we use wireless headphones, for example, we don’t have to go through the set-up steps every time we want to connect to our phones. Instead, it’s expected that we do it once, and the headphones will automatically connect to the phone when they’re within range of each other.

Bond Management Constraints

While the concept of bonding sounds relatively simple, there are some constraints to consider when designing bond management in a system: 

  • Bond information must be stored in nonvolatile memory, since it must persist across a device reboot. In our headphone example, you wouldn’t want to have to set up your headphones each time its battery died.
  • Flash is a form of nonvolatile memory commonly used to store bond information, but flash operations are slow and therefore typically asynchronous.

Considering these constraints, a bond management module — we’ll call this the Bond Manager — must ensure serial execution of bond operations (e.g. store bond, delete bond). This design constraint often surfaces in the application programming interfaces (APIs) of BLE chip manufacturers as a warning. For example, see Nordic’s warning on the  pm_peer_delete and pm_peers_delete functions:

Warning: use this function only when not connected or connectable. If a peer is or becomes connected or a PM_PEER_DATA_FUNCTIONS function is used during this procedure (until the success or failure event happens), the behavior is undefined.

If you’ve gone digging for a solution online, you’ve probably realized that you’re not alone — many people have asked similar questions on various help forums. You’ve probably come across many threads with responses similar to this, all of which indicate that there’s a very specific order of events that must occur in order to delete bond information of a device that’s currently connected:

  1. Disable scanning on the central device / advertising on the peripheral device. This ensures they will not be connectable once they’re no longer connected to one another.
  2. Sever the BLE connection between the two devices. Now neither are connected or connectable.
  3. Delete the bond information from flash memory.
  4. Re-enable scanning on the central device / advertising on the peripheral device.

Creating a Bond Manager

It seems there are plenty of people looking for a solution to very similar problems, so let’s dive in a little deeper and discuss one way to implement this solution. 

For the sake of a concrete implementation, we’ll use the Nordic SDK to create a Bond Manager. The Bond Manager will serve as a simple and re-usable module to manage bonds, though this implementation will focus only on bond deletion.

First, let’s create a header file (bond_mgr.h) and define our interface.

void        bond_mgr_init();
void        bond_mgr_delete_queued_peers();
ret_code_t  bond_mgr_queue_peer_for_deletion(pm_peer_id_t peer_id_to_del);
bool        bond_mgr_deletion_pending();
ret_code_t  peer_mgr_register_peer_delete_handler(
            peer_delete_handler handler);

Notice there’s a function for queuing the peers for deletion, and another function for performing the deletion. This will allow us to accept multiple requests to delete a peer without performing the deletion immediately, which is particularly useful if the device is connected or connectable when the requests are received. Remember, we can’t delete peers when the device is connected or connectable.

Now, let’s create a file called bond_manager.c. We’ll want to register a peer manager event handler so we know when peer operations have completed. Nordic’s “peer” is the equivalent of a bonded device. 

void bond_mgr_init()
{
    // Register a handler for peer events
    ret_code_t err_code = pm_register(pm_evt_handler);
    APP_ERROR_CHECK(err_code);
}

We’ll circle back to the implementation details of pm_evt_handler later.

Next, we’ll want a way to store the peers that are queued for deletion. There are many ways to do this, but for simplicity’s sake, let’s use an array of peer IDs and a variable to track how many peers are saved in the table at any given time.

#define MAX_NUM_BONDED_DEVICES_STORED 10
static pm_peer_id_t peer_deletion_table[MAX_NUM_BONDED_DEVICES_STORED];
static uint8_t num_peers_to_delete = 0;

Now that we have a way to queue peers for deletion, let’s create functions to add and remove peers from the deletion table and to determine whether peer deletion is currently in progress. Be careful only to add valid and non-duplicate peer IDs.

static ret_code_t add_peer_to_deletion_table(pm_peer_id_t peer_id)
{
    if (peer_id == PM_PEER_ID_INVALID)
    {
        NRF_LOG_WARNING("Peer invalid");
        return NRF_ERROR_INVALID_DATA;
    }
 
    for (uint8_t i = 0; i < MAX_TOTAL_BONDED_DEVICES_STORED; i++)
    {
        // Peer ID is already in the table
        if (peer_deletion_table[i] == peer_id)
        {
            NRF_LOG_WARNING("Peer already saved for deletion");
            return NRF_ERROR_INVALID_PARAM;
        }
 
        // Found the first empty slot in the table, use it
        if (peer_deletion_table[i] == PM_PEER_ID_INVALID)
        {
            NRF_LOG_INFO("Peer successfully saved for deletion");
            peer_deletion_table[i] = peer_id;
 
		//Keep track of how many peers are queued for deletion
            num_peers_to_delete++;
            return NRF_SUCCESS;
        }
    }
 
    // Not enough space in the peer deletion table
    NRF_LOG_WARNING("Peer deletion table is full!");
    return NRF_ERROR_NO_MEM;
}
static void remove_peer_from_deletion_table(pm_peer_id_t peer_id)
{
    NRF_LOG_INFO("Num peers to delete: %d", num_peers_to_delete);
    // Clear peer ID from table
    for (uint8_t i = 0; i < MAX_TOTAL_BONDED_DEVICES_STORED; i++)
    {
        if (peer_deletion_table[i] == peer_id)
        {
            peer_deletion_table[i] = PM_PEER_ID_INVALID;
            //Keep track of how many peers remain in the table
            num_peers_to_delete--;
            //Delete the next queued peer (if any)
            bond_mgr_delete_queued_peers();
        }
    }
}

These two functions will be used internally, when a caller invokes bond_mgr_queue_peer_for_deletion and bond_mgr_delete_queued_peers.

bool bond_mgr_deletion_pending()
{
    return num_peers_to_delete > 0;
}

Bond deletion is pending if there’s at least one peer queued for deletion, and it hasn’t yet been deleted.

When a caller invokes bond_mgr_queue_peer_for_deletion, we’ll add the specified peer ID to the deletion table.

ret_code_t bond_mgr_queue_peer_for_deletion(pm_peer_id_t peer_id_to_del)
{
    return add_peer_to_deletion_table(peer_id_to_del);
}

Then, when a caller invokes bond_mgr_delete_queued_peers, we’ll delete each of the queued peers, one at a time.

void bond_mgr_delete_queued_peers()
{
    if (num_peers_to_delete == 0)
    {
        // TODO: This is where any interested module should
        // be notified that peer deletion is complete!
        return;
    }
 
    for (uint8_t i = 0; i < MAX_TOTAL_BONDED_DEVICES_STORED; i++)
    {
        if (peer_deletion_table[i] != PM_PEER_ID_INVALID)
        {
            ret_code_t err_code = pm_peer_delete(peer_deletion_table[i]);
            if (err_code != NRF_SUCCESS)
            {
                // In this design, if deletion fails, we give up and                      
                // remove it from the deletion table. Depending on
                // your application, you may want to handle this failure
                // differently.
                remove_peer_from_deletion_table(peer_deletion_table[i]);
            }
            else
            {
                NRF_LOG_INFO("Waiting for peer deletion to complete");
                // Peer deletion occurs asynchronously, so a peer
          // delete event (PM_PEER_DELETE_SUCCEEDED or 
          // PM_PEER_DELETE_FAILED) will indicate success or 
          // failure of the deletion
                return;
            }
        }
    }
 
}

We’re deleting one at a time instead of calling pm_peer_delete for all peers in the deletion table, solely because our usages of num_peers_to_delete to track how many peers are left to be deleted, but haven’t taken care to make add_peer_to_deletion_table and remove_peer_from_deletion_table reentrant.

Finally, when the peer deletion is complete, our pm_evt_handler function will be called with the specified event. In this case, we only care about PM_EVT_PEER_DELETE_SUCCEEDED and PM_EVT_PEER_DELETE_FAILED.

static void pm_evt_handler(pm_evt_t const *p_evt)
{
    switch (p_evt->evt_id)
    {
        case PM_EVT_PEER_DELETE_SUCCEEDED:
        {
		// Deletion succeeded! Remove it from the deletion table.
            remove_peer_from_deletion_table(p_evt->peer_id);
        }
        break;
 
        case PM_EVT_PEER_DELETE_FAILED:
        {
		// Again, for this design, if we fail to delete the peer,
           // we’re giving up and removing it from the deletion table,
           // but your application may need to handle it differently.
            remove_peer_from_deletion_table(p_evt->peer_id);
        }
        break;
    }
}

Voila! We now have a reusable, albeit barebones, module to manage bond deletion. 

You’re probably thinking “but wait, we didn’t implement peer_mgr_register_peer_delete_handler.” That’s because the implementation of that function will vary based on your needs. For example, will your application need to support only one or multiple handlers? If multiple, how many? Regardless, at a high level, that function will simply save handler(s) to be invoked when peer deletion is complete.

Designing Bond Deletion Within the Constraints

We now have a Bond Manager that will queue peers for deletion and delete them on request. However, the calling module must know when to request the deletion, in order to ensure bond deletion doesn’t occur while the device is connected or connectable.

The calling module is responsible for performing the following steps in order:

  1. Registering a handler to be notified when bond deletion completes. This will likely be done during initialization, but it can be done anytime before bond deletion is attempted.
  2. Queuing peer(s) for deletion as needed by calling bond_mgr_queue_peer_for_deletion.
  3. (Optional) Disconnect on a peer deletion request. This step is only required if bond deletion is expected to be attempted immediately. Otherwise, skip this step.
  4. Wait for the BLE_EVT_DISCONNECTED event, indicating a disconnect was detected.
  5. Check if there are bonds queued for deletion via bond_mgr_deletion_pending. If there are none queued for deletion, no further action is necessary. In this scenario, we’ll assume there are bonds pending deletion and proceed to the following steps.
  6. Disabling scanning on the central device or advertising on the peripheral device. This ensures that the device isn’t connected or connectable while bond deletion occurs.
  7. Trigger bond deletion of all peers queue in step #2 by calling bond_mgr_delete_queued_peers.
  1. Wait for the handler in step #1 to be notified of the completion of bond deletion.
  2. Re-enable scanning or advertising.

One Size Fits Most

The bond deletion design we walked through in this post is specific to the nRF5 SDK, and assumes no use of a real-time operating system (RTOS). If you’re using a RTOS, the constraints around bond deletion still apply, but the implementation becomes much simpler due to the availability of multiple tasks. If you’re using a different chipset, or the nRF Connect SDK, the Nordic-specific events in our implementation of the Bond Manager won’t apply. However, the same constraints around bonding still exist, so the design still applies and, presumably, can be ported to use with another SDK easily enough. Happy (un)bonding!

Interested in Learning More?

All of our blog posts are written by the engineers at Punch Through—the same engineers who build amazing products for our clients. Need help on your project? Learn more about some of the ways we can help!