Bonded Bluetooth Pairing in Zephyr

Mar, 05 2022

In the previous post, we discussed the difference between connecting with your device and pairing with your device. In this tutorial, we'll cover how to implement "just works" Bluetooth Low Energy pairing in Zephyr on an nRF52.

I'll assume that you have already implemented a custom peripheral as described in this post.

If not, you can clone the git repo and start from there.

First, we'll need to add some additional lines in our prj.conf file. Add the following lines:

# Enable bonding CONFIG_BT_SETTINGS=y CONFIG_FLASH=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FLASH_MAP=y CONFIG_NVS=y CONFIG_SETTINGS=y CONFIG_BT_SMP_SC_PAIR_ONLY=y CONFIG_BT_MAX_PAIRED=3 # This is needed for the BLE security on-changed callback CONFIG_BT_SMP=y

Your prj.conf file should match with this one.

Next, head over to your custom service (src/services/led_service.c) and change some of the characteristic's configuration settings. Replace the BT_GATT_SERVICE_DEFINE macro with the following:

/* LED Service Declaration and Registration */ BT_GATT_SERVICE_DEFINE(led_service, BT_GATT_PRIMARY_SERVICE(BT_UUID_MY_SERVICE), BT_GATT_CHARACTERISTIC(BT_UUID_MY_SERVICE_RX, BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT, NULL, on_receive, NULL), BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), );

You'll notice that the only thing that changed from the previous service definition is the BT_GATT_PERM_WRITE flags. They are now set to BT_GATT_PERM_WRITE_ENCRYPT. Changing this flag prompts iOS to initiate system pairing when trying to write an encrypted characteristic.

Your led_service.c file should now match mine.

Head on over to src/main.c and find the bt_conn_cb structure defined as conn_callbacks. Replace its definition with the following:

// Wire up all the callbacks we defined above to the bt struct static struct bt_conn_cb conn_callbacks = { .connected = connected, .disconnected = disconnected, .security_changed = security_changed, .le_param_req = le_param_req, .le_param_updated = le_param_updated, };

Now we'll need to define the security_changed callback. Let's do that now, just above the conn_callback definition.

static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) { char addr[BT_ADDR_LE_STR_LEN]; bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); if (!err) { printk("Security changed: level %u\n", level); } else { printk("Security failed: level %u err %d\n", level, err); } }

Next we'll need to add a few pairing specific callbacks. Just underneath the conn_callbacks definition, add the following code:

static void auth_cancel(struct bt_conn *conn) { char addr[BT_ADDR_LE_STR_LEN]; bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); printk("Pairing cancelled\n"); } static void pairing_complete(struct bt_conn *conn, bool bonded) { char addr[BT_ADDR_LE_STR_LEN]; bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); printk("Pairing completed\n"); } static void pairing_failed(struct bt_conn *conn, enum bt_security_err reason) { char addr[BT_ADDR_LE_STR_LEN]; bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); printk("Pairing failed, reason %d\n", reason); } static struct bt_conn_auth_cb conn_auth_callbacks = { .passkey_display = NULL, .passkey_confirm = NULL, .cancel = auth_cancel, .pairing_confirm = NULL, .pairing_complete = pairing_complete, .pairing_failed = pairing_failed};

Similar to the conn_callbacks, we now have a bt_conn_auth_cb struct which takes a number of callbacks related to Bluetooth pairing state changes.

Take note of the .passkey_display and .passkey_confirm properties. Setting these to NULL is what makes the pairing scheme "just works" rather than something more strict. If your peripheral device has a keypad or display available during pairing, here is where you would interface with them.

Next, in the bt_ready function, just underneath the bt_conn_cb_register(&conn_callbacks) call, let's register our conn_auth_callbacks.

bt_conn_auth_cb_register(&conn_auth_callbacks);

Just beneath the register call we now need to load our settings:

settings_load();

To make this call, we'll need to pull in the settings library. Add the following header to the top of your main.c file:

#include <settings/settings.h>

Finally, there's a helpful debugging call to make in your main function after you initialize your Bluetooth stack:

int ret = bt_unpair(BT_ID_DEFAULT, BT_ADDR_LE_ANY); printk("Attempting to unpair device %d\n", ret);

This deletes pairing information from your peripheral device after each re-boot. This will help us avoid any errors when you choose "forget device" on your central (iOS or Android device), thus preventing our peripheral from referencing the outdated encryption information.

Double check that your main.c looks like mine.

Now when you connect to your peripheral device using your phone, your mobile operating system should display a pairing prompt. iOS pairing prompt

You can check out the finished git repo here.