Scope
This is a discussion on building a Bluetooth application on our new nRF Connect SDK/Zephyr platform with the reflections to our nRF5 SDK which is based on the SoftDevice Bluetooth stack. The goal is to give you an overview of the architecture of the new application, how you interface with the Bluetooth stack, and how you relate it to your code in your previous/original application running on the SoftDevice stack.
This tutorial-based heavily on the sample codes provided in nRF Connect SDK v1.3.1 located at \nrf\samples\bluetooth and \zephyr\samples\bluetooth. Before I continue with the guide I would suggest trying running some of the samples to see how they work. Documentation about the samples can be found here.
It's assumed that you have gone through the SDK's getting started guide and know how to build and run an nRF Connect SDK application.
This Part 1 of the tutorial, you can find other parts here:
Part 2 - Central Role.
Part 3 - Optimize the connection.
Table of Contents
1. Architecture
Below is a quick comparison of the architecture of a SoftDevice based application vs an application with the Zephyr Bluetooth LE stack. It's quite similar, but we can see a clear split between the host and the controller on nRF Connect SDK/Zephyr, compared to the single combined host and controller stack in the SoftDevice. This split in the Zephyr stack allows the nRF5 device to work as a transceiver chip controlled via HCI directly from other hosts, for example, the BlueZ stack on Linux. The split is even more clear on multicore devices such as the nRF5340 where you need to flash the controller stack (hci_rpmsg sample) separately on the network core while running the host and the application in the application core.
Another difference is that the SoftDevice is built as an RTOS agnostic stack. The SoftDevice APIs are implemented using thread-safe Supervisor Calls (SVCs). All application interactions with the stack and libraries are asynchronous and event-driven. There is no linking against a library needed when calling APIs from the application. This allows the SoftDevice to be provided as a pre-compiled stack and you don't have to link and rebuild the SoftDevice when you build your application. On the other hand, there is an RTOS at the core of Zephyr. The Zephyr BLE stack is one of the modules of the RTOS. Some of the main benefits of an RTOS include abstracting away HW and scheduling complexity, allowing critical processes to finish in a deterministic fashion.
In a traditional SoftDevice based application, you have the main loop where you put the CPU to sleep and execute thread (main) level tasks, then you have event/interrupt handlers and SVC calls that run various event handlers whenever an event occurs. In Zephyr, you can have multiple threads executing at the same time, and you might not even have the main loop. This makes it easier to handle more complex applications, and in fact, there is no limitation on the maximum number of concurrent connections in Zephyr. You can organize multiple activities running at the same time without having to think of how to switch the execution between them as it's handled by the Zephyr OS automatically.
The following code is extracted from the peripheral_uart sample. It shows two threads configured to run simultaneously and there's no main() function. The first thread led_blink_thread() will initialize Bluetooth and setup GATT services and advertising. After that, it would just blink the LED. The second thread ble_write_thread() would just wait for data from the UART and send it over BLE. It uses a semaphore to wait for the BLE stack to be ready before entering the loop.
static void led_blink_thread(void) { int blink_status = 0; int err = 0; printk("Starting Nordic UART service example\n"); err = init_uart(); if (err) { error(); } configure_gpio(); bt_conn_cb_register(&conn_callbacks); if (IS_ENABLED(CONFIG_BT_GATT_NUS_SECURITY_ENABLED)) { bt_conn_auth_cb_register(&conn_auth_callbacks); } err = bt_enable(NULL); if (err) { error(); } printk("Bluetooth initialized\n"); k_sem_give(&ble_init_ok); if (IS_ENABLED(CONFIG_SETTINGS)) { settings_load(); } err = bt_gatt_nus_init(&nus_cb); if (err) { printk("Failed to initialize UART service (err: %d)\n", err); return; } err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); if (err) { printk("Advertising failed to start (err %d)\n", err); } for (;;) { dk_set_led(RUN_STATUS_LED, (++blink_status) % 2); k_sleep(K_MSEC(RUN_LED_BLINK_INTERVAL)); } } void ble_write_thread(void) { /* Don't go any further until BLE is initailized */ k_sem_take(&ble_init_ok, K_FOREVER); for (;;) { /* Wait indefinitely for data to be sent over bluetooth */ struct uart_data_t *buf = k_fifo_get(&fifo_uart_rx_data, K_FOREVER); if (bt_gatt_nus_send(NULL, buf->data, buf->len)) { printk("Failed to send data over BLE connection\n"); } k_free(buf); if (rx_disabled) { rx_disabled = false; uart_irq_rx_enable(uart); } } } K_THREAD_DEFINE(led_blink_thread_id, STACKSIZE, led_blink_thread, NULL, NULL, NULL, PRIORITY, 0, 0); K_THREAD_DEFINE(ble_write_thread_id, STACKSIZE, ble_write_thread, NULL, NULL, NULL, PRIORITY, 0, 0);
HW abstraction and modulization also help; you may already have noticed that in nRF Connect SDK there is no predefined Bluetooth stack for each HW chip like we had with different SoftDevice that only offer a specific list of features and support only some specific chips, for example, S132, S112, S140 etc.
Next, we will have a look at some of the main tasks of any Bluetooth application including advertising, Bluetooth event handling, API calls, bonding, and more.
2. Advertising
Let's have a look at a very simple application. The following code is taken from the Zephyr Bluetooth documentation here.
/* * Set Advertisement data. Based on the Eddystone specification: * https://github.com/google/eddystone/blob/master/protocol-specification.md * https://github.com/google/eddystone/tree/master/eddystone-url */ static const struct bt_data ad[] = { BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_NO_BREDR), BT_DATA_BYTES(BT_DATA_UUID16_ALL, 0xaa, 0xfe), BT_DATA_BYTES(BT_DATA_SVC_DATA16, 0xaa, 0xfe, /* Eddystone UUID */ 0x10, /* Eddystone-URL frame type */ 0x00, /* Calibrated Tx power at 0m */ 0x00, /* URL Scheme Prefix http://www. */ 'z', 'e', 'p', 'h', 'y', 'r', 'p', 'r', 'o', 'j', 'e', 'c', 't', 0x08) /* .org */ }; /* Set Scan Response data */ static const struct bt_data sd[] = { BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN), }; static void bt_ready(int err) { if (err) { printk("Bluetooth init failed (err %d)\n", err); return; } printk("Bluetooth initialized\n"); /* Start advertising */ err = bt_le_adv_start(BT_LE_ADV_NCONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); if (err) { printk("Advertising failed to start (err %d)\n", err); return; } printk("Beacon started\n"); } void main(void) { int err; printk("Starting Beacon Demo\n"); /* Initialize the Bluetooth Subsystem */ err = bt_enable(bt_ready); if (err) { printk("Bluetooth init failed (err %d)\n", err); } }
It's super simple. The only two Bluetooth LE API calls are bt_enable() to enable Bluetooth and bt_le_adv_start() to start advertising.
There is no configuration input to bt_enable() and you provide a callback (bt_ready()) to be called when the Bluetooth subsystem is ready. We will look into the stack configuration later.
The bt_le_adv_start() function requires 3 inputs, the advertising parameters, the advertising data packet, and the scan response packet.
For advertising parameters, you can find the list of pre-defined advertising parameters here. Let's take a look at one of them: BT_LE_ADV_CONN_NAME:BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONNECTABLE | BT_LE_ADV_OPT_USE_NAME, \
BT_GAP_ADV_FAST_INT_MIN_2, \
BT_GAP_ADV_FAST_INT_MAX_2, NULL)
- Option: BT_LE_ADV_OPT_CONNECTABLE | BT_LE_ADV_OPT_USE_NAME : Enable connectable advertising and include device Name in advertising packet (don't include device name in your advertising packet if you already use this option). Note if you want to advertise using local identity (static address not new address every time you reset) you should use BT_LE_ADV_OPT_USE_IDENTITY option.
- BT_GAP_ADV_FAST_INT_MIN_2, BT_GAP_ADV_FAST_INT_MAX_2: Advertising interval max and min (by default 100ms and 150ms respectively). This is a little bit different from our SoftDevice configuration, as we only have a single interval configuration when calling sd_ble_gap_adv_set_configure(). In Zephyr, we provide the max and min interval and it's up to the controller to decide the actual interval depending on the other activities.
The content of the advertise data packet (ad[]) and the scan response data packet (sd[]) needs to follow the Bluetooth specification on advertise packets (Chapter 11 Vol 3 Part C - Bluetooth Spec v5.2). You can use the BT_DATA_BYTES and BT_DATA macros to help encode the packets in compliance with the specification.
The setup and initialization of advertising are pretty straightforward forward and we don't have a generic library to automate more complex advertising activities such as the auto switch to slow advertising, advertising timeout, etc as in the nRF5 SDK.
We will cover advertising with a whitelist and extended advertising later.
3. BLE events from the stack.
The next step is how to receive the events from the Bluetooth stack when some activities have occurred.
Here is a code excerpt from the peripheral_lbs sample in our nRF repository. The example shows how to register Bluetooth events to be forwarded to the application and how they are handled.
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)
#define RUN_STATUS_LED DK_LED1
#define CON_STATUS_LED DK_LED2
#define RUN_LED_BLINK_INTERVAL 1000
#define USER_LED DK_LED3
#define USER_BUTTON DK_BTN1_MSK
static bool app_button_state;
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};
static const struct bt_data sd[] = {
BT_DATA_BYTES(BT_DATA_UUID128_ALL, LBS_UUID_SERVICE),
};
static void connected(struct bt_conn *conn, u8_t err)
{
if (err) {
printk("Connection failed (err %u)\n", err);
return;
}
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Connection established! \n\
Connected to: %s \n\
Role: %u \n\
Connection interval: %u \n\
Slave latency: %u \n\
Connection supervisory timeout: %u \n"
, addr, info.role, info.le.interval, info.le.latency, info.le.timeout);
dk_set_led_on(CON_STATUS_LED);
}
static void disconnected(struct bt_conn *conn, u8_t reason)
{
printk("Disconnected (reason %u)\n", reason);
dk_set_led_off(CON_STATUS_LED);
}
#ifdef CONFIG_BT_GATT_LBS_SECURITY_ENABLED
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: %s level %u\n", addr, level);
} else {
printk("Security failed: %s level %u err %d\n", addr, level,
err);
}
}
#endif
static struct bt_conn_cb conn_callbacks = {
.connected = connected,
.disconnected = disconnected,
#ifdef CONFIG_BT_GATT_LBS_SECURITY_ENABLED
.security_changed = security_changed,
#endif
};
#if defined(CONFIG_BT_GATT_LBS_SECURITY_ENABLED)
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Passkey for %s: %06u\n", addr, passkey);
}
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: %s\n", addr);
}
static void pairing_confirm(struct bt_conn *conn)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
bt_conn_auth_pairing_confirm(conn);
printk("Pairing confirmed: %s\n", addr);
}
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: %s, bonded: %d\n", addr, bonded);
}
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 conn: %s, reason %d\n", addr, reason);
}
static struct bt_conn_auth_cb conn_auth_callbacks = {
.passkey_display = auth_passkey_display,
.cancel = auth_cancel,
.pairing_confirm = pairing_confirm,
.pairing_complete = pairing_complete,
.pairing_failed = pairing_failed
};
#else
static struct bt_conn_auth_cb conn_auth_callbacks;
#endif
static void app_led_cb(bool led_state)
{
dk_set_led(USER_LED, led_state);
}
static bool app_button_cb(void)
{
return app_button_state;
}
static struct bt_gatt_lbs_cb lbs_callbacs = {
.led_cb = app_led_cb,
.button_cb = app_button_cb,
};
static void button_changed(u32_t button_state, u32_t has_changed)
{
if (has_changed & USER_BUTTON) {
bt_gatt_lbs_send_button_state(button_state);
app_button_state = button_state ? true : false;
}
}
static int init_button(void)
{
int err;
err = dk_buttons_init(button_changed);
if (err) {
printk("Cannot init buttons (err: %d)\n", err);
}
return err;
}
void main(void)
{
int blink_status = 0;
int err;
printk("Starting Bluetooth Peripheral LBS example\n");
err = dk_leds_init();
if (err) {
printk("LEDs init failed (err %d)\n", err);
return;
}
err = init_button();
if (err) {
printk("Button init failed (err %d)\n", err);
return;
}
bt_conn_cb_register(&conn_callbacks);
if (IS_ENABLED(CONFIG_BT_GATT_LBS_SECURITY_ENABLED)) {
bt_conn_auth_cb_register(&conn_auth_callbacks);
}
err = bt_enable(NULL);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
printk("Bluetooth initialized\n");
if (IS_ENABLED(CONFIG_SETTINGS)) {
settings_load();
}
err = bt_gatt_lbs_init(&lbs_callbacs);
if (err) {
printk("Failed to init LBS (err:%d)\n", err);
return;
}
err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad),
sd, ARRAY_SIZE(sd));
if (err) {
printk("Advertising failed to start (err %d)\n", err);
return;
}
printk("Advertising successfully started\n");
for (;;) {
dk_set_led(RUN_STATUS_LED, (++blink_status) % 2);
k_sleep(K_MSEC(RUN_LED_BLINK_INTERVAL));
}
}
It's slightly different from the way it's handled by the SoftDevice. There you register an observer using the NRF_SDH_BLE_OBSERVER() macro to receive events, and usually in main.c you have a ble_evt_handler() function allowing the application to handle Bluetooth events not processed automatically by other SDK modules. It's quite similar in NCS, but you use bt_conn_cb_register() to register callbacks for specific events such as connected, disconnected, security_changed, le_data_len_updated, le_phy_updated, and more.
Callbacks for authentication pairing events are registered separately using bt_conn_auth_cb_register(). Here you can register event callbacks about pairing_accept, passkey_display, passkey_entry, oob_data_request etc. Note that if your device IO capabilities don't support display for example, then you just simply don't declare passkey_display callback and the stack will automatically remove Display in the device's IO capabilities.
In the peripheral_lbs example what the application does is to send a notification when the button is pressed, by having bt_gatt_lbs_send_button_state() called inside the button_changed() callback function. If you have a look inside bt_gatt_lbs_send_button_state(), the notification is sent by a bt_gatt_notify() call. The function is equivalent to the sd_ble_gatts_hvx() function in the SoftDevice. You need to supply the connection handle, the characteristic's value handle, and the data you want to send. You may want to pay extra attention to the characteristics value handle as it's not calculated automatically by the stack like you might be used to from the SoftDevice but needs to be kept track of it when the attribute table is declared. We will go into this in the service characteristic declaration discussion.
Let's take a look inside dk_button_and_leds.c to find how the button_changed() handler is called when the button is pressed (with debouncing). It's actually executed at thread level instead of at an interrupt level. You can find buttons_scan_fn() is executed using a mechanism called k_work. This mechanism lets a higher priority thread off-load non-urgent processing to a lower priority thread. In Zephyr, Bluetooth APIs should be called in thread level. You can either use a work queue or you can use a semaphore to wait in a thread before you execute a function after you receive an interrupt. Following is a code snippet showing the use of semaphore in a thread to wait for the interrupt when the temperature sampling is ready before sending it over BLE.
static K_SEM_DEFINE(temp_sampling_ready, 0, 1);
void temp_handler(int32_t temperature)
{
printk("temperature %d %d \n",temperature,temperature / 4);
temp= temperature;
k_sem_give(&temp_sampling_ready);
}
void ble_temperature_send(void)
{
int err = 0;
/* Don't go any further until BLE is initailized */
k_sem_take(&ble_init_ok, K_FOREVER);
k_sem_give(&ble_init_ok);
for (;;) {
err= nrfx_temp_measure();
if (err!=NRFX_SUCCESS)
{
printk("error %d\n", err);
}
k_sem_take(&temp_sampling_ready, K_FOREVER);
if (my_connection!=NULL)
{
my_service_temperature_send(my_connection,temp);
}
k_sleep(K_MSEC(100)); // 100ms
}
}
K_THREAD_DEFINE(ble_temperature_send_id, STACKSIZE, ble_temperature_send, NULL, NULL,
NULL, 7, 0, 0);
The temperature sampling was initialized using following code: nrfx_temp_config_t config=NRFX_TEMP_DEFAULT_CONFIG;
err = nrfx_temp_init(&config,temp_handler);
IRQ_CONNECT(TEMP_IRQn,5,nrfx_temp_irq_handler,NULL,0);
4. GATT declaration and callbacks
Next, we find how we can declare services, characteristics, and the callbacks to handle read/write events.
The following code is from the peripheral_lbs example in nRF Connect SDK:
/*
* Copyright (c) 2018 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
*/
/** @file
* @brief LED Button Service (LBS) sample
*/
#include <zephyr/types.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <sys/printk.h>
#include <sys/byteorder.h>
#include <zephyr.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/uuid.h>
#include <bluetooth/gatt.h>
#include <bluetooth/services/lbs.h>
#include <logging/log.h>
LOG_MODULE_REGISTER(bt_gatt_lbs, CONFIG_BT_GATT_LBS_LOG_LEVEL);
static bool notify_enabled;
static bool button_state;
static struct bt_gatt_lbs_cb lbs_cb;
#define BT_UUID_LBS BT_UUID_DECLARE_128(LBS_UUID_SERVICE)
#define BT_UUID_LBS_BUTTON BT_UUID_DECLARE_128(LBS_UUID_BUTTON_CHAR)
#define BT_UUID_LBS_LED BT_UUID_DECLARE_128(LBS_UUID_LED_CHAR)
static void lbslc_ccc_cfg_changed(const struct bt_gatt_attr *attr,
u16_t value)
{
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
}
static ssize_t write_led(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
const void *buf,
u16_t len, u16_t offset, u8_t flags)
{
LOG_DBG("Attribute write, handle: %u, conn: %p", attr->handle, conn);
if (lbs_cb.led_cb) {
lbs_cb.led_cb(*(bool *)buf);
}
return len;
}
#ifdef CONFIG_BT_GATT_LBS_POLL_BUTTON
static ssize_t read_button(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf,
u16_t len,
u16_t offset)
{
const char *value = attr->user_data;
LOG_DBG("Attribute read, handle: %u, conn: %p", attr->handle, conn);
if (lbs_cb.button_cb) {
button_state = lbs_cb.button_cb();
return bt_gatt_attr_read(conn, attr, buf, len, offset, value,
sizeof(*value));
}
return 0;
}
#endif
/* LED Button Service Declaration */
BT_GATT_SERVICE_DEFINE(lbs_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_LBS),
#ifdef CONFIG_BT_GATT_LBS_POLL_BUTTON
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_BUTTON,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ, read_button, NULL,
&button_state),
#else
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_BUTTON,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ, NULL, NULL, NULL),
#endif
BT_GATT_CCC(lbslc_ccc_cfg_changed,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_LED,
BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE,
NULL, write_led, NULL),
);
int bt_gatt_lbs_init(struct bt_gatt_lbs_cb *callbacks)
{
if (callbacks) {
lbs_cb.led_cb = callbacks->led_cb;
lbs_cb.button_cb = callbacks->button_cb;
}
return 0;
}
int bt_gatt_lbs_send_button_state(bool button_state)
{
if (!notify_enabled) {
return -EACCES;
}
return bt_gatt_notify(NULL, &lbs_svc.attrs[2],
&button_state,
sizeof(button_state));
}
Let's focus on the service and characteristic declaration:
/* LED Button Service Declaration */
BT_GATT_SERVICE_DEFINE(lbs_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_LBS),
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_BUTTON,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ, read_button, NULL,
&button_state),
BT_GATT_CCC(lbslc_ccc_cfg_changed,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_LED,
BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE,
NULL, write_led, NULL),
);
A service is defined using the BT_GATT_SERVICE_DEFINE() macro with the service UUID, followed by one or more characteristics definitions below the service.
The characteristics are defined by the BT_GATT_CHARACTERISTIC() macro in which you need to provide the characteristic's UUID, the properties, the permission, event callbacks, and the user_data variable.
You may notice that there is no "base UUID" like in the SoftDevices, and there is no longer a requirement to register the base UUID (128-bit) with the SoftDevice and then use the short UUID (16-bit) in the code. Here we simply use the long UUID to register each characteristic.
The properties are as defined in the BLE specification, and you can combine multiple properties such as READ, NOTIFY etc in a single characteristic.
The same goes for permissions; here you choose the level of security and which operations require which security settings. Note that if you want to have encryption with MITM level, you here need to use _AUTHEN permissions. As an example, the BT_GATT_PERM_READ_AUTHEN permission is equivalent to the BLE_GAP_CONN_SEC_MODE_SET_ENC_WITH_MITM() permission that was used for the SoftDevice.
Next is the concept of read callbacks, write callbacks, and user_data. They are a little bit different from the SoftDevice system. Instead of having to choose a location either in the SoftDevice or in the application memory space to store the characteristic value, a pointer must be provided to a user_data variable. This doesn't need to be the actual value of the characteristic but can be any data you plan to use. This user_data pointer will be provided to the read and write callbacks that you register in the characteristic declaration.
In the read and write callbacks you will also receive a pointer to the buffer. In the read callback, you would need to fill the buffer with the data you want to respond to the read request. Here is a read callback from the lbs example above:
static ssize_t read_button(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
void *buf,
u16_t len,
u16_t offset)
{
const char *value = attr->user_data;
LOG_DBG("Attribute read, handle: %u, conn: %p", attr->handle, conn);
if (lbs_cb.button_cb) {
button_state = (char)lbs_cb.button_cb();
return bt_gatt_attr_read(conn, attr, buf, len, offset, value,
sizeof(*value));
}
return 0;
}
The bt_gatt_attr_read() function will do a memcpy of the value variable into the buffer and the buffer is then sent back to the peer as the read response.
It's similar in the write callback, the value in the write request/command from the peer is provided in the buffer buf variable. In the following code, the data is copied back to the user_data pointer:
static ssize_t write_vnd(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, u16_t len, u16_t offset,
u8_t flags)
{
u8_t *value = attr->user_data;
if (offset + len > sizeof(vnd_value)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
memcpy(value + offset, buf, len);
return len;
}
In nRF5 SDK we have the concept of read/write with authorization where you have an read/write event and will decide to accept the command and reply with sd_ble_gatts_rw_authorize_reply(). In nRF Connect SDK it's not a separated use case like that but rather merged with the "normal" read/write callback. You, in the read/write callback can return an error code if you don't want to accept the read/write command. As you can find the above code, you can return with an error code, for example
return BT_GATT_ERR(BT_ATT_ERR_WRITE_REQ_REJECTED);
The last function is for sending notifications/indications. This is done by calling bt_gatt_notify()/bt_gatt_indicate(). If you don't provide a connection handle in the call, the notification will be sent on all connections. In the function, you need to identify the handle of either the characteristic or characteristic value attribute in the service attribute table. For example in the following service, the characteristic value of BT_UUID_MY_SERVICE_TX and BT_UUID_MY_SERVICE_TEMPERATURE are attrs[3] and attrs[6] respectively.
BT_GATT_SERVICE_DEFINE(my_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 | BT_GATT_PERM_WRITE, NULL, on_receive, NULL), BT_GATT_CHARACTERISTIC(BT_UUID_MY_SERVICE_TX, BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ, NULL, NULL, NULL), BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), BT_GATT_CHARACTERISTIC(BT_UUID_MY_SERVICE_TEMPERATURE, BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ, NULL, NULL, NULL), BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE) );
It can be a little bit tricky to choose the correct index of the characteristic or characteristic value. You may need to check the spec on attribute table forming to get the handle of each record, in the above example we have the ATT table for the service as follows:
0 | Service declare |
1 | RX Char declare |
2 | RX Char value |
3 | TX Char declare |
4 | TX Char value |
5 | TX CCCD |
6 | MY_TEMPERATURE char declare |
7 | MY_TEMPERATURE char value |
8 | MY_TEMPERATURE CCCD. |
5. Project Configuration
As mentioned in #2, the bt_enable() call doesn't contain any configuration of the stack as it's usually configured in project config file prj.conf. This gives flexibility to the Zephyr Bluetooth LE stack compared to SoftDevices. Features can be added or removed based on the requirement of the application, allowing optimization of the memory footprint of the stack.
Following is the project configuration of the peripheral_hids_mouse example:
CONFIG_BT=y
CONFIG_BT_DEBUG_LOG=y
CONFIG_BT_MAX_CONN=2
CONFIG_BT_MAX_PAIRED=2
CONFIG_BT_SMP=y
CONFIG_BT_L2CAP_TX_BUF_COUNT=5
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Nordic_HIDS_mouse"
CONFIG_BT_DEVICE_APPEARANCE=962
CONFIG_BT_GATT_BAS=y
CONFIG_BT_GATT_HIDS=y
CONFIG_BT_GATT_HIDS_MAX_CLIENT_COUNT=2
CONFIG_BT_GATT_UUID16_POOL_SIZE=40
CONFIG_BT_GATT_CHRC_POOL_SIZE=20
CONFIG_BT_CONN_CTX=y
CONFIG_BT_GATT_DIS=y
CONFIG_BT_GATT_DIS_PNP=y
CONFIG_BT_GATT_DIS_MANUF="NordicSemiconductor"
CONFIG_BT_GATT_DIS_PNP_VID_SRC=2
CONFIG_BT_GATT_DIS_PNP_VID=0x1915
CONFIG_BT_GATT_DIS_PNP_PID=0xEEEE
CONFIG_BT_GATT_DIS_PNP_VER=0x0100
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_BT_SETTINGS=y
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_DK_LIBRARY=y
Most of the configurations are self-explanatory and you can read the documentation to find more information about each of them here. There are some important ones:
CONFIG_BT_SMP: Allow pairing and bonding
If bonding (storing bond info) is needed these settings should be enabled:
CONFIG_BT_SETTINGS=y
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_BT_TINYCRYPT_ECC: needed to enable LE Secure connection.
CONFIG_BT_PRIVACY: Enable/Disable privacy mode, this will enable/disable using random address on every boot.
Further reading
Some tutorials that I would suggest going through to get familiar with the Zephyr Bluetooth stack:
If you have any requests on other related topics, please leave a comment below. I will try to keep this blog updated. Some topics in the timeline are whitelisting, directed advertising, central features, service discovery.
Top Comments