nRF Connect SDK Bluetooth Low Energy tutorial part 1: Custom Service in Peripheral role

Scope

The goal of this tutorial is to set-up your own custom Bluetooth Low Energy service on an nRF5340 acting in a peripheral role. We will set up a new Bluetooth LE project from scratch. The finished project will be provided as a reference in a zipped folder and can be unzipped anywhere inside the nRF Connect SDK root's subdirectories. 
If you want to use another nRF5 device you can do that, just replace the target board in Part 7: Testing. 

Prerequisites

It is assumed that you’ve already gone through the nRF Connect SDK Tutorial series - Part 0 to 3, and that you have some previous experience with Bluetooth LE. 

This tutorial was created with ncs/nrf v1.2.99-dev1. To use this version of nRF Connect SDK checkout this commit, go to nRF Connect SDK root folder, and type "west update". Alternatively you can use the Toolchain Manager app in nRF Connect For Desktop.

The finished code is provided as a reference at the bottom of this tutorial. 

I suggest you use an editor that allows you to easily find definitions and declarations. My favorite is Microsoft’s Visual Studio Code. By opening the entire nRF Connect SDK folder in VS Code I can jump to definitions and declarations that are otherwise hard to find, and I can easily search through the entire nRF Connect SDK SDK or an arbitrary folder for text. 

Table of content

Scope

Prerequisites

How-to

    Step 1: Set up your projects build configuration

    Step 2: Enable the Bluetooth host stack

    Step 3: Set up connection callbacks

    Step 4: Create your service

    Step 5: Configure Advertisement and Scan Response data

    Step 6: Set up the main loop to send an incrementing number every 2000ms

    Part 7: Testing

Notes

Reference code

    Main.c

    my_service.c

    my_service.h

How-to

Step 1: Set up your projects build configuration

We will need to create a new folder in nRF Connect SDK's root or any of its subdirectories, called my_service that must contain four files; main.c, CMakeLists.txt, Kconfig, and prj.conf, and it must also contain your service source and header file. The main.c file must be placed in a folder called src as its path is referenced in CMakeLists.txt. 
For more information about CMakeLists, Kconfig and prj.conf read nRF Connect SDK Tutorial - Part 2.

Project Structure

|
|---CMakeLists.txt
|---Kconfig
|---prj.conf
|---services
   |---my_service.c
   |---my_service.h
|---src
   |---main.c

CMakeLists should contain the following:

#
# Copyright (c) 2018 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
#
cmake_minimum_required(VERSION 3.8)

include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE)
project(NONE)

# NORDIC SDK APP START

target_sources(app PRIVATE
  src/main.c services/my_service.c
)

# NORDIC SDK APP END
zephyr_library_include_directories(.)

If you want to use SES you can now open the project with the CMake file that you just created.

Kconfig should contain the following:

#
# Copyright (c) 2018 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
#

# Sources Kconfig.zephyr in the Zephyr root directory.
#
# Note: All 'source' statements work relative to the Zephyr root directory (due
# to the $srctree environment variable being set to $ZEPHYR_BASE). If you want
# to 'source' relative to the current Kconfig file instead, use 'rsource' (or a
# path relative to the Zephyr root).

source "$ZEPHYR_BASE/Kconfig.zephyr"

proj.conf should contain the following:

#
# Copyright (c) 2018 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
#

CONFIG_BT=y
CONFIG_BT_DEBUG_LOG=y
CONFIG_BT_MAX_CONN=1
CONFIG_BT_L2CAP_TX_BUF_COUNT=5
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="My_Device"
CONFIG_BT_DEVICE_APPEARANCE=962

CONFIG_HEAP_MEM_POOL_SIZE=2048

# This example requires more workqueue stack
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048

Other useful configs are:

You can find the complete list of configs at https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/kconfig/index-all.html

Main.c should contain the following includes and defines:

/*
 * Copyright (c) 2018 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
 */

#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 <drivers/gpio.h>
#include <soc.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/uuid.h>
#include <bluetooth/gatt.h>

#include "../services/my_service.h"


#define DEVICE_NAME 		CONFIG_BT_DEVICE_NAME // Set in proj.conf
#define DEVICE_NAME_LEN        	(sizeof(DEVICE_NAME) - 1)

static K_SEM_DEFINE(ble_init_ok, 0, 1);

static void error(void)
{
	while (true) {
		printk("Error!\n");
		/* Spin for ever */
		k_sleep(K_MSEC(1000)); //1000ms
	}
}

void main(void)
{
	
}

If you want to use SES you can now open the project with the CMake file that you just created.

Step 2: Enable the Bluetooth host stack

You enable the stack by calling: err = bt_enable(bt_ready); in main() of main.c. 

bt_ready is a callback that contains code that you want to run after the Bluetooth host is enabled. For instance, you’ll want to catch any error code that the bt subsystem returns, as well as configure connection callbacks, initialize your service, and start advertising. 

static void bt_ready(int err)
{
	if (err) {
		printk("BLE init failed with error code %d\n", err);
		return;
	}

	k_sem_give(&ble_init_ok);
}

Step 3: Set up connection callbacks

Add to main.c a ble connection status callback, struct bt_conn_cb, with bt_conn_cb_register(). They are called by the Bluetooth host when one of the following Bluetooth LE events have occurred;

  • A connection has been established
  • A connection has been broken
  • There’s a pending connection parameter update request
  • A connection’s parameter has been updated
  • Remote information has been retrieved from the remote peer.
  • A remote identity has been resolved
  • A connection’s security level has changed.

In this tutorial, we won’t cover any security aspects, so the latter two events are not included, nor will we cover the event where remote information has been retrieved by the peer.
The connection parameter update request callback can return true or false to accept or reject the requested parameters. The callback can also adjust the parameters prior to accepting the request. 


Several bt_conn_cb can be registered to enable other parts of the application to get callbacks, like your services. By passing callback handlers with NULL you can opt-out of receiving certain callbacks, this is useful if you’re only interested in handling a subset of the callbacks in a part of the application, like the connected and disconnected events. 

struct bt_conn *my_connection;

static void connected(struct bt_conn *conn, u8_t err)
{
	struct bt_conn_info info; 
	char addr[BT_ADDR_LE_STR_LEN];

	my_connection = conn;

	if (err) 
	{
		printk("Connection failed (err %u)\n", err);
		return;
	}
	else if(bt_conn_get_info(conn, &info))
	{
		printk("Could not parse connection info\n");
	}
	else
	{
		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);
	}
}

static void disconnected(struct bt_conn *conn, u8_t reason)
{
	printk("Disconnected (reason %u)\n", reason);

	my_connection = NULL;
}

static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param)
{
	//If acceptable params, return true, otherwise return false.
	return true; 
}

static void le_param_updated(struct bt_conn *conn, u16_t interval, u16_t latency, u16_t timeout)
{
	struct bt_conn_info info; 
	char addr[BT_ADDR_LE_STR_LEN];
	
	if(bt_conn_get_info(conn, &info))
	{
		printk("Could not parse connection info\n");
	}
	else
	{
		bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
		
		printk("Connection parameters updated!	\n\
		Connected to: %s						\n\
		New Connection Interval: %u				\n\
		New Slave Latency: %u					\n\
		New Connection Supervisory Timeout: %u	\n"
		, addr, info.le.interval, info.le.latency, info.le.timeout);
	}
}


static struct bt_conn_cb conn_callbacks = {
	.connected		    = connected,
	.disconnected   	= disconnected,
	.le_param_req		= le_param_req,
	.le_param_updated	= le_param_updated
};

Add the following to bt_ready() in main.c:

    //Configure connection callbacks
	bt_conn_cb_register(&conn_callbacks);

The application is now set up to forward Bluetooth LE events to our event handlers in main.c.

Step 4: Create your service

Right now we have an empty header and source file my_service.c and my_service.h, we start by adding the following snippet to both the source and header file:

#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 <soc.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/addr.h>
#include <bluetooth/uuid.h>
#include <bluetooth/gatt.h>

We need an initialization function to call from main, add the following function definition to my_service.c:

int my_service_init(void)
{
    int err = 0;
   
    memset(&data_rx, 0, MAX_TRANSMIT_SIZE);
    memset(&data_tx, 0, MAX_TRANSMIT_SIZE);
    
    return err;
}

And the declaration to my_service.h:

int my_service_init(void);

We will require at least two transmission buffers, one for transmitting and one for receiving, add the following to my_service.c:

#define MAX_TRANSMIT_SIZE 240
u8_t data_rx[MAX_TRANSMIT_SIZE];
u8_t data_tx[MAX_TRANSMIT_SIZE];

We need a 128-bit UUID for our service, I used https://uuidonline.net/ to generate my UUID, 75c276c3-8f97-20bc-a143-b354244886d4.
Add it to my_service.h:

/*Note that UUIDs have Least Significant Byte ordering */
#define MY_SERVICE_UUID 0xd4, 0x86, 0x48, 0x24, 0x54, 0xB3, 0x43, 0xA1, \
			 	0xBC, 0x20, 0x97, 0x8F, 0xC3, 0x76, 0xC2, 0x75

We also need two UUIDs for our TX and RX characteristics, generate them and add them to my_service.h:

#define RX_CHARACTERISTIC_UUID 0xA6, 0xE8, 0xC4, 0x60, 0x7E, 0xAA, 0x41, 0x6B, \
0x95, 0xD4, 0x9D, 0xCC, 0x08, 0x4F, 0xCF, 0x6A

#define TX_CHARACTERISTIC_UUID 0xED, 0xAA, 0x20, 0x11, 0x92, 0xE7, 0x43, 0x5A, \
0xAA, 0xE9, 0x94, 0x43, 0x35, 0x6A, 0xD4, 0xD3

We need to declare our UUIDs, add the following to my_service.c: 

#define BT_UUID_MY_SERIVCE      BT_UUID_DECLARE_128(MY_SERVICE_UUID)
#define BT_UUID_MY_SERIVCE_RX   BT_UUID_DECLARE_128(RX_CHARACTERISTIC_UUID)
#define BT_UUID_MY_SERIVCE_TX   BT_UUID_DECLARE_128(TX_CHARACTERISTIC_UUID)

We need to define and register our service and its characteristics. By using the following helper macro we statically register our Service in our BLE host stack.
Add the following to my_service.c:

/* LED Button Service Declaration and Registration */
BT_GATT_SERVICE_DEFINE(my_service,
BT_GATT_PRIMARY_SERVICE(BT_UUID_MY_SERIVCE),
BT_GATT_CHARACTERISTIC(BT_UUID_MY_SERIVCE_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_SERIVCE_TX,
			       BT_GATT_CHRC_NOTIFY,
			       BT_GATT_PERM_READ,
                   NULL, NULL, NULL),
BT_GATT_CCC(lbslc_ccc_cfg_changed,
        BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);

Next we create functions for sending and receiving data. Add the following to my_service.c:

/* This function is called whenever the RX Characteristic has been written to by a Client */
static ssize_t on_receive(struct bt_conn *conn,
			  const struct bt_gatt_attr *attr,
			  const void *buf,
			  u16_t len,
			  u16_t offset,
			  u8_t flags)
{
    const u8_t * buffer = buf;
    
	printk("Received data, handle %d, conn %p, data: 0x", attr->handle, conn);
    for(u8_t i = 0; i < len; i++){
        printk("%02X", buffer[i]);
    }
    printk("\n");

	return len;
}

/* This function is called whenever a Notification has been sent by the TX Characteristic */
static void on_sent(struct bt_conn *conn, void *user_data)
{
	ARG_UNUSED(user_data);

    const bt_addr_le_t * addr = bt_conn_get_dst(conn);
        
	printk("Data sent to Address 0x %02X %02X %02X %02X %02X %02X \n", addr->a.val[0]
                                                                    , addr->a.val[1]
                                                                    , addr->a.val[2]
                                                                    , addr->a.val[3]
                                                                    , addr->a.val[4]
                                                                    , addr->a.val[5]);
}

/* This function is called whenever the CCCD register has been changed by the client*/
void on_cccd_changed(const struct bt_gatt_attr *attr, u16_t value)
{
    ARG_UNUSED(attr);
    switch(value)
    {
        case BT_GATT_CCC_NOTIFY: 
            // Start sending stuff!
            break;

        case BT_GATT_CCC_INDICATE: 
            // Start sending stuff via indications
            break;

        case 0: 
            // Stop sending stuff
            break;
        
        default: 
            printk("Error, CCCD has been set to an invalid value");     
    }
}
                        

/* LED Button Service Declaration and Registration */
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),
);

/* This function sends a notification to a Client with the provided data,
given that the Client Characteristic Control Descripter has been set to Notify (0x1).
It also calls the on_sent() callback if successful*/
void my_service_send(struct bt_conn *conn, const u8_t *data, uint16_t len)
{
    /* 
    The attribute for the TX characteristic is used with bt_gatt_is_subscribed 
    to check whether notification has been enabled by the peer or not.
    Attribute table: 0 = Service, 1 = Primary service, 2 = RX, 3 = TX, 4 = CCC.
    */
    const struct bt_gatt_attr *attr = &my_service.attrs[3]; 

    struct bt_gatt_notify_params params = 
    {
        .uuid   = BT_UUID_MY_SERVICE_TX,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = on_sent
    };

    // Check whether notifications are enabled or not
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY)) 
    {
        // Send the notification
	    if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled on the selected attribute\n");
    }
}

Add the following function declaration to my_service.h, it is the function we call from main whenever we want to send a notification:

void my_service_send(struct bt_conn *conn, const u8_t *data, uint16_t len);

We initialize our service by adding the following to bt_ready() in main.c:

    //Initalize services
	err = my_service_init();


You can find the complete reference code at the end of this tutorial. 

Step 5: Configure Advertisement and Scan Response data

We want to advertise as connectable, with the device name, stop advertising when connected, and allow anyone to connect or request scan data. We also want to advertise for a minimum interval of 100ms and a maximum of 1000ms. 
Add the following to bt_ready() in main.c:

    //Start advertising
	err = bt_le_adv_start(BT_LE_ADV_PARAM(
							BT_LE_ADV_OPT_CONNECTABLE
							|BT_LE_ADV_OPT_ONE_TIME
							|BT_LE_ADV_OPT_USE_NAME,
							160, 	// units of 0.625ms
							1600),	// units of 0.625ms
						ad, ARRAY_SIZE(ad),
						sd, ARRAY_SIZE(sd));

	if (err) 
	{
		printk("Advertising failed to start (err %d)\n", err);
		return;
	}

Now that we’ve got the advertising params we want, we will set the advertising data.

The advertising data format is an array of bt_data type. We want to advertise some flags,  our name, and a UUID of a service that we support. This will require two sets of bt_data structs because we can’t fit both our full name and UUID in a single advertisement packet, we will therefore have use scan response data to fit our second bt_data struct.

First, we need to set our advertising flags, BT_LE_AD_GENERAL and BT_LE_AD_NO_BREDR.
BT_LE_AD_GENERAL means we’re advertising indefinitely, as opposed to BT_LE_AD_LIMITED. BT_LE_AD_NO_BREDR means that we do not support BR/EDR (aka BT classic).

The definitions from Advertisement Data Types and Flags are found in hci.h.

Add the following to main.c:

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, MY_SERVICE_UUID),
};

The define DEVICE_NAME is set in proj.conf as the define CONFIG_BT_DEVICE_NAME.

Step 6: Set up the main loop to send an incrementing number every 2000ms

void main(void)
{
	
	int err = 0;
	u32_t number = 0;

	printk("Starting Nordic BLE peripheral tutorial\n");

	
	err = bt_enable(bt_ready);

	if (err) 
	{
		printk("BLE initialization failed\n");
		error(); //Catch error
	}
	
	/* 	Bluetooth stack should be ready in less than 100 msec. 								\
																							\
		We use this semaphore to wait for bt_enable to call bt_ready before we proceed 		\
		to the main loop. By using the semaphore to block execution we allow the RTOS to 	\
		execute other tasks while we wait. */	
	err = k_sem_take(&ble_init_ok, K_MSEC(500));

	if (!err) 
	{
		printk("Bluetooth initialized\n");
	} else 
	{
		printk("BLE initialization did not complete in time\n");
		error(); //Catch error
	}

	err = my_service_init();

	for (;;) 
	{
		// Main loop
		my_service_send(my_connection, (u8_t *)&number, sizeof(number));
		number++;
		k_sleep(K_MSEC(1000)); // 1000ms
	}
}


Part 7: Testing

The snippets of code are not provided in sequential order and you will need to move them around a bit to allow the compiler to include them accordingly, see the reference code for comparison. 

Build the application, either using SES or command line: west build -b nrf5340_dk_nrf5340_cpuapp

Flash the application, either using SES or command line: west flash. 

You also need to build and flash the Bluetooth LE Controller stack that runs on the network core. Build and flash the sample in <..>\ncs\zephyr\samples\bluetooth\hci_rpmsg.


To test your code you will need a Bluetooth LE client that is able to interact with your service, the nRF Connect app for mobile or desktop is suited for this task. You will also need to open a UART terminal and connect to the nRF5340 DK.

Our device is named "My_Device" and it will advertise as soon as the Bluetooth host stack has been initialized. Connect to it and enable notifications for the TX characteristic. You should now see that the client is receiving a number that increments every 2 seconds. 

You should also write to the RX characteristic in the client app and be able to observe the data printed out in the UART terminal. 

Notes

In main you will see the use of a semaphore:

// Create a semaphore that starts a 0 with a maximum number of 1
static K_SEM_DEFINE(ble_init_ok, 0, 1); 

// Increment the semaphore by 1
k_sem_give(&ble_init_ok);

// Decrement the semaphore by one, issue a warning if not able to decrement within 100ms.  
err = k_sem_take(&ble_init_ok, K_MSEC(100));

This semaphore is used as a blocking global flag. By using the Zephyr kernel's semaphore implementation instead of a variable such as a bool, we allow the RTOS to prioritize the execution of another thread or putting the CPU to sleep while we wait for the semaphore to become available. This is used throughout the Zephyr RTOS and sample applications. 
See https://docs.zephyrproject.org/1.9.0/kernel/synchronization/semaphores.html for more details. 


Reference code

Main.c

/*
 * Copyright (c) 2018 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: LicenseRef-BSD-5-Clause-Nordic
 */

/*
	Built with nrf commit 065270c5f016aea2a791d0e1fe34b2e972d92fda
*/

#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 <drivers/gpio.h>
#include <soc.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/uuid.h>
#include <bluetooth/gatt.h>

#include "../services/my_service.h"

#define DEVICE_NAME             CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN         (sizeof(DEVICE_NAME) - 1)

static K_SEM_DEFINE(ble_init_ok, 0, 1);

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, MY_SERVICE_UUID),
};

struct bt_conn *my_connection;

static void connected(struct bt_conn *conn, u8_t err)
{
	struct bt_conn_info info; 
	char addr[BT_ADDR_LE_STR_LEN];

	my_connection = conn;

	if (err) 
	{
		printk("Connection failed (err %u)\n", err);
		return;
	}
	else if(bt_conn_get_info(conn, &info))
	{
		printk("Could not parse connection info\n");
	}
	else
	{
		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);
	}
}

static void disconnected(struct bt_conn *conn, u8_t reason)
{
	printk("Disconnected (reason %u)\n", reason);
}

static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param)
{
	//If acceptable params, return true, otherwise return false.
	return true; 
}

static void le_param_updated(struct bt_conn *conn, u16_t interval, u16_t latency, u16_t timeout)
{
	struct bt_conn_info info; 
	char addr[BT_ADDR_LE_STR_LEN];
	
	if(bt_conn_get_info(conn, &info))
	{
		printk("Could not parse connection info\n");
	}
	else
	{
		bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
		
		printk("Connection parameters updated!	\n\
		Connected to: %s						\n\
		New Connection Interval: %u				\n\
		New Slave Latency: %u					\n\
		New Connection Supervisory Timeout: %u	\n"
		, addr, info.le.interval, info.le.latency, info.le.timeout);
	}
}

static struct bt_conn_cb conn_callbacks = 
{
	.connected				= connected,
	.disconnected   		= disconnected,
	.le_param_req			= le_param_req,
	.le_param_updated		= le_param_updated
};

static void bt_ready(int err)
{
	if (err) 
	{
		printk("BLE init failed with error code %d\n", err);
		return;
	}

	//Configure connection callbacks
	bt_conn_cb_register(&conn_callbacks);

	//Initalize services
	err = my_service_init();

	if (err) 
	{
		printk("Failed to init LBS (err:%d)\n", err);
		return;
	}

	//Start advertising
	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");

	k_sem_give(&ble_init_ok);
}


static void error(void)
{
	while (true) {
		printk("Error!\n");
		/* Spin for ever */
		k_sleep(K_MSEC(1000)); //1000ms
	}
}

void main(void)
{
	
	int err = 0;
	u32_t number = 0;

	printk("Starting Nordic BLE peripheral tutorial\n");

	
	err = bt_enable(bt_ready);

	if (err) 
	{
		printk("BLE initialization failed\n");
		error(); //Catch error
	}
	
	/* 	Bluetooth stack should be ready in less than 100 msec. 								\
																							\
		We use this semaphore to wait for bt_enable to call bt_ready before we proceed 		\
		to the main loop. By using the semaphore to block execution we allow the RTOS to 	\
		execute other tasks while we wait. */	
	err = k_sem_take(&ble_init_ok, K_MSEC(500));

	if (!err) 
	{
		printk("Bluetooth initialized\n");
	} else 
	{
		printk("BLE initialization did not complete in time\n");
		error(); //Catch error
	}

	err = my_service_init();

	for (;;) 
	{
		// Main loop
		my_service_send(my_connection, (u8_t *)&number, sizeof(number));
		number++;
		k_sleep(K_MSEC(1000)); // 1000ms
	}
}

my_service.c

#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 <soc.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/uuid.h>
#include <bluetooth/addr.h>
#include <bluetooth/gatt.h>

#include "my_service.h"

#define BT_UUID_MY_SERVICE      BT_UUID_DECLARE_128(MY_SERVICE_UUID)
#define BT_UUID_MY_SERVICE_RX   BT_UUID_DECLARE_128(RX_CHARACTERISTIC_UUID)
#define BT_UUID_MY_SERVICE_TX   BT_UUID_DECLARE_128(TX_CHARACTERISTIC_UUID)

#define MAX_TRANSMIT_SIZE 240//TODO figure this out

u8_t data_rx[MAX_TRANSMIT_SIZE];
u8_t data_tx[MAX_TRANSMIT_SIZE];

int my_service_init(void)
{
    int err = 0;

    memset(&data_rx, 0, MAX_TRANSMIT_SIZE);
    memset(&data_tx, 0, MAX_TRANSMIT_SIZE);

    return err;
}

/* This function is called whenever the RX Characteristic has been written to by a Client */
static ssize_t on_receive(struct bt_conn *conn,
			  const struct bt_gatt_attr *attr,
			  const void *buf,
			  u16_t len,
			  u16_t offset,
			  u8_t flags)
{
    const u8_t * buffer = buf;
    
	printk("Received data, handle %d, conn %p, data: 0x", attr->handle, conn);
    for(u8_t i = 0; i < len; i++){
        printk("%02X", buffer[i]);
    }
    printk("\n");

	return len;
}

/* This function is called whenever a Notification has been sent by the TX Characteristic */
static void on_sent(struct bt_conn *conn, void *user_data)
{
	ARG_UNUSED(user_data);

    const bt_addr_le_t * addr = bt_conn_get_dst(conn);
        
	printk("Data sent to Address 0x %02X %02X %02X %02X %02X %02X \n", addr->a.val[0]
                                                                    , addr->a.val[1]
                                                                    , addr->a.val[2]
                                                                    , addr->a.val[3]
                                                                    , addr->a.val[4]
                                                                    , addr->a.val[5]);
}

/* This function is called whenever the CCCD register has been changed by the client*/
void on_cccd_changed(const struct bt_gatt_attr *attr, u16_t value)
{
    ARG_UNUSED(attr);
    switch(value)
    {
        case BT_GATT_CCC_NOTIFY: 
            // Start sending stuff!
            break;

        case BT_GATT_CCC_INDICATE: 
            // Start sending stuff via indications
            break;

        case 0: 
            // Stop sending stuff
            break;
        
        default: 
            printk("Error, CCCD has been set to an invalid value");     
    }
}
                        

/* LED Button Service Declaration and Registration */
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),
);

/* This function sends a notification to a Client with the provided data,
given that the Client Characteristic Control Descripter has been set to Notify (0x1).
It also calls the on_sent() callback if successful*/
void my_service_send(struct bt_conn *conn, const u8_t *data, uint16_t len)
{
    /* 
    The attribute for the TX characteristic is used with bt_gatt_is_subscribed 
    to check whether notification has been enabled by the peer or not.
    Attribute table: 0 = Service, 1 = Primary service, 2 = RX, 3 = TX, 4 = CCC.
    */
    const struct bt_gatt_attr *attr = &my_service.attrs[3]; 

    struct bt_gatt_notify_params params = 
    {
        .uuid   = BT_UUID_MY_SERVICE_TX,
        .attr   = attr,
        .data   = data,
        .len    = len,
        .func   = on_sent
    };

    // Check whether notifications are enabled or not
    if(bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY)) 
    {
        // Send the notification
	    if(bt_gatt_notify_cb(conn, &params))
        {
            printk("Error, unable to send notification\n");
        }
    }
    else
    {
        printk("Warning, notification not enabled on the selected attribute\n");
    }
}

my_service.h

#include <zephyr/types.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <zephyr.h>
#include <soc.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/conn.h>
#include <bluetooth/uuid.h>
#include <bluetooth/gatt.h>

#define MY_SERVICE_UUID 0xd4, 0x86, 0x48, 0x24, 0x54, 0xB3, 0x43, 0xA1, \
			            0xBC, 0x20, 0x97, 0x8F, 0xC3, 0x76, 0xC2, 0x75

#define RX_CHARACTERISTIC_UUID  0xA6, 0xE8, 0xC4, 0x60, 0x7E, 0xAA, 0x41, 0x6B, \
			                    0x95, 0xD4, 0x9D, 0xCC, 0x08, 0x4F, 0xCF, 0x6A

#define TX_CHARACTERISTIC_UUID  0xED, 0xAA, 0x20, 0x11, 0x92, 0xE7, 0x43, 0x5A, \
			                    0xAA, 0xE9, 0x94, 0x43, 0x35, 0x6A, 0xD4, 0xD3

/** @brief Callback type for when new data is received. */
typedef void (*data_rx_cb_t)(u8_t *data, u8_t length);

/** @brief Callback struct used by the my_service Service. */
struct my_service_cb 
{
	/** Data received callback. */
	data_rx_cb_t    data_rx_cb;
};

int my_service_init(void);

void my_service_send(struct bt_conn *conn, const u8_t *data, uint16_t len);

If you have any questions or feedback please post a new Question in Devzone :) 

ncs_ble_tutorial_part1.zip
  • Hi. I am trying to use a Python script and Bleak to read the data from the device. I can see the device name, services, and characteristics just fine. But when I try to read, I get "Protocol Error 0x02: Read Not Permitted" Indeed, you can see from the Python output, that the characteristic seems to be "Notify" only, not read. But I have set the BT_GATT_PERM_READ per the instructions. Why can't I read from this characteristic? 

    Python code:

    import asyncio
    from bleak import BleakScanner
    from bleak import BleakClient
    
    CHAR_ID = 'd3d46a35-4394-e9aa-5a43-e7921120aaed'
    
    async def main():
        devices = await BleakScanner.discover()
        mac_addr = 0
        for d in devices:
            if d.name == "My_Device":
                print("Found my device")
                print(f"Address: {d.address}, Name: {d.name}")
                mac_addr = d.address
        
        async with BleakClient(mac_addr) as client:
            svcs = await client.get_services()
            print("Services:")
            for service in svcs:
                print('\nservice', service.handle, service.uuid, service.description)
    
                characteristics = service.characteristics
    
                for char in characteristics:
                    print('  characteristic', char.handle, char.uuid, char.description, char.properties)
    
                    descriptors = char.descriptors
    
                    for desc in descriptors:
                        print('    descriptor', desc)
                    
            data_bytes = await client.read_gatt_char(CHAR_ID)
            data = bytearray.decode(data_bytes)
            print('Got Data', data)
    
    asyncio.run(main())

    Output from running the above:

    C:\D-drive>python ble_scan.py
    Found my device
    Address: CE:6E:6C:5F:64:39, Name: My_Device
    C:\D-drive\ble_scan.py:25: FutureWarning: This method will be removed future version, use the services property instead.
    svcs = await client.get_services()
    Services:

    service 1 00001801-0000-1000-8000-00805f9b34fb Generic Attribute Profile
    characteristic 2 00002a05-0000-1000-8000-00805f9b34fb Service Changed ['indicate']
    descriptor 00002902-0000-1000-8000-00805f9b34fb (Handle: 4): Client Characteristic Configuration
    characteristic 5 00002b29-0000-1000-8000-00805f9b34fb Client Supported Features ['read', 'write']
    characteristic 7 00002b2a-0000-1000-8000-00805f9b34fb Database Hash ['read']

    service 9 00001800-0000-1000-8000-00805f9b34fb Generic Access Profile
    characteristic 10 00002a00-0000-1000-8000-00805f9b34fb Device Name ['read']
    characteristic 12 00002a01-0000-1000-8000-00805f9b34fb Appearance ['read']
    characteristic 14 00002a04-0000-1000-8000-00805f9b34fb Peripheral Preferred Connection Parameters ['read']

    service 16 75c276c3-8f97-20bc-a143-b354244886d4 Unknown
    characteristic 17 6acf4f08-cc9d-d495-6b41-aa7e60c4e8a6 Unknown ['write-without-response', 'write']
    characteristic 19 d3d46a35-4394-e9aa-5a43-e7921120aaed Unknown ['notify']
    descriptor 00002902-0000-1000-8000-00805f9b34fb (Handle: 21): Client Characteristic Configuration
    Traceback (most recent call last):
    File "C:\D-drive\ble_scan.py", line 44, in <module>
    asyncio.run(main())
    File "C:\Users\jamie\AppData\Local\Programs\Python\Python39\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)
    File "C:\Users\jamie\AppData\Local\Programs\Python\Python39\lib\asyncio\base_events.py", line 642, in run_until_complete
    return future.result()
    File "C:\D-drive\ble_scan.py", line 40, in main
    data_bytes = await client.read_gatt_char(CHAR_ID)
    File "C:\Users\jamie\AppData\Local\Programs\Python\Python39\lib\site-packages\bleak\__init__.py", line 632, in read_gatt_char
    return await self._backend.read_gatt_char(char_specifier, **kwargs)
    File "C:\Users\jamie\AppData\Local\Programs\Python\Python39\lib\site-packages\bleak\backends\winrt\client.py", line 779, in read_gatt_char
    _ensure_success(
    File "C:\Users\jamie\AppData\Local\Programs\Python\Python39\lib\site-packages\bleak\backends\winrt\client.py", line 115, in _ensure_success
    raise BleakError(
    bleak.exc.BleakError: Could not read characteristic handle 19: Protocol Error 0x02: Read Not Permitted

  • Thanks a lot. This is a great, simple explanation to add ble from scratch on nRF connect SDK

  • Yes! Would really like to see a walkthrough for the central side and if possible the combination of the central and peripheral for a device. Thanks @!

  • This is a good peripheral walkthru. Is there walkthru for the central side of this. 

  • Thanks for the tutorial! Really needed it to get going. In the example code, where you show:

    /* 
        The attribute for the TX characteristic is used with bt_gatt_is_subscribed 
        to check whether notification has been enabled by the peer or not.
        Attribute table: 0 = Service, 1 = Primary service, 2 = RX, 3 = TX, 4 = CCC.
    */

    This confused me when looking at some other examples, like zephyr\samples\bluetooth\peripheral_csc. In that case, they have 1 service, 4 characteristics, and 2 CCCs. However, when they refer to the 4th attribute using csc_svc.attrs[8], that made no sense according to your numbering scheme. I dug in a bit further and discovered how it actually works. Hopefully this can be corrected. Thanks!

    /* 
        The attribute for the TX characteristic is used with bt_gatt_is_subscribed 
        to check whether notification has been enabled by the peer or not.
        Attribute table: 0 = Primary service, 1 = RX properties, 2 = RX descriptor, 
        3 = TX properties, 4 = TX descriptor, 5 = CCC.
    */