You are on page 1of 33

Bluetooth low energy Characteristics, a

beginner's tutorial
MartinBL 18 Mar 2016

Before we begin
Table of content
Topics that will be covered include:

1. Before we begin
2. Basic theory
3. Attribute tables in nRFConnect Bluetooth Low Energy application
4. Description of the example
5. Adding a characteristic
6. Updating the characteristic and sending notifications

Equipment and software


To complete this tutorial you will need the following:

[nRF52 DK]( https://www.nordicsemi.com/eng/Products/Bluetooth-low-energy/nRF52-DK)


[nRF52 Dongle or an additional development kit](
https://www.nordicsemi.com/eng/Products/nRF52840-Dongle)
Keil uVision V5.xx or SEGGER Embedded Studio (SES).
[nRF Connect for Desktop](https://www.nordicsemi.com/eng/Products/Bluetooth-low-
energy/nRF-Connect-for-Desktop). You can also use nRF Connect for mobile found in both
Google Play on Android and App Store for iPhone.
[SDK V15.0.0]( https://developer.nordicsemi.com/nRF5_SDK/nRF5_SDK_v15.x.x/).
[Softdevice S132 V6.x.x]( https://www.nordicsemi.com/eng/nordic/Products/nRF52832/S132-
SD-v6/67248).
[Example files](https://github.com/NordicSemiconductor/nrf5-ble-tutorial-
characteristic/tree/master) found on github. There is also a folder you can look at with the
finished code including solutions to the challenges.
Other kits, dongles and software versions might work as well, but this is what I have used. This tutorial
will not cover how to install and setup the software.

Necessary prior knowledge


This tutorial is intended to be a natural continuation of the BLE Services, a beginner's tutorial and BLE
Advertising, a beginner's tutorial. It is recommended, but not necessary, to go through these tutorials
first. It is expected that you have basic knowledge of how to use Keil or SES and the nRF Connect BLE
app. Read the tutorial Setting up an example project on the nRF51 DK to learn how to use your
equipment and compile your first BLE application.

If you run into troubles please browse devzone, look for documentation on our Infocenter, and read
the user guides for your kits. You can also check out the "Solution" folder for a proposed solution. I
urge you to post any questions you might have on the forum and not below the tutorial. This makes
it easier for other users and Nordic employees to see and search for your questions and you will
most likely get a faster response(!).

Some basic theory


The Attribute Protocol and the Generic Attribute Profile
My goal in this tutorial is to have as little theory as possible, but still get you up and running with a
“springboard” application. An application it is easy to customize and expand upon. Yet the Attribute
Protocol (ATT) and Generic Attribute Profile (GATT) are so essential to BLE that there is no way around
a brief introduction. Basic knowledge of the ATT and the GATT is useful later in this tutorial, although
when you make simple applications and work with Nordic’s SoftDevices and SDKs these are not
concepts you have to worry much about. You should be able to skip the theory and still complete the
tutorial, but as mentioned we will refer to the following concepts as we go.

Attribute Protocol (ATT)


As shown in the figure below the BLE protocol is built upon a number of layers.
Notice how the application layer is right above the GATT which in turn is built upon the ATT. The ATT is
based on a Client <–> Server relationship. The server holds information like sensor values, the state of
a light switch, position data, etc. This information is organized in a table, referred to as an attribute
table. Each attribute in the table is a value or piece of information with some associated properties. So
when a client wants the sensor data it refers to e.g. row 11 in the table. If it wants the state of the light
switch it might refer to row 16, and so on. Here is an excerpt from the Bluetooth Core Specification V4.2
(Hereafter referred to as BCS):

Vol 3:, Part F, Ch. 2, “Protocol Overview”:

The attribute protocol defines two roles; a server role and a client role. It allows a server to expose
a set of attributes to a client that are accessible using the attribute protocol. An attribute is a
discrete value that has the following three properties associated with it: (1) an attribute type,
defined by a UUID, (2) an attribute handle, (3) a set of permissions that are defined by each higher
layer specification that utilizes the attribute; these permissions cannot be accessed using the
Attribute Protocol.
The attribute type specifies what the attribute represents. Bluetooth SIG defined attribute types are
defined in the Bluetooth SIG assigned numbers page, and used by an associated higher layer
specification. Non-Bluetooth SIG attribute types may also be defined.
Let us relate this to a typical application, namely the Heart Rate Profile. In Table 1 each and every row is
an attribute, and each attribute has a handle, a type, a set of permissions, and a value.
Table 1:

Attribute Handles
The attribute handle uniquely identifies an attribute on a server, allowing a client to reference the
attribute in read or write requests. To simplify things then handle can be considered as the row number
in the attribute table. Although the handle number might not be sequential. The handles are 16-bit
numbers and you will see later on that the SoftDevice use handles extensively to refer to various
attributes. From a programmer’s perspective it is actually a quite effective way to pass values and
information between functions. It makes it easy for your application to keep track of the attributes and
to grab whatever information it needs. The handle numbers vary depending on how many attributes
you have.

Attribute Types (UUIDs)


A Universally Unique ID (UUID) is a 16 or 128-bit value used to identify the type of every attribute. In
Table 1 there are five different types of attributes; one of type “Service Declaration” (0x2800), two of
type “Characteristic Declaration” (0x2803), one of type “Heart Rate Measurement Characteristic Value”
(0x2A37), one of type “Body Sensor Location Characteristic Value” (0x2A38), and finally one of type
“Descriptor Declaration” (0x2902). Six attributes in total. There are more attribute types, and later on in
the tutorial we will make our own.

Attribute Permissions
Permissions define some rules of how you can interact with a specific attribute. It defines whether an
attribute should be readable and/or writeable and what kind of authorization is required to do the
operations. Note that these permissions only apply to the attribute value, not the handle, type, and the
permission field itself. This allows a client to look through a server’s attribute table and discover what
the server can provide. Although not necessarily read and write the values.

Attribute Values
The value can be anything. It can be a heart rate value measured in beats per minute, the state of a
light switch, or a string like “Hello World”. And sometimes it is information about where to find other
attributes and their properties. For example, in the Service Declaration attribute in Table 1 (handle
0x000E) the value holds a UUID (0x180D) identifying what kind of service it is (notice the "Assigned
Number" field for the Heart Rate service, almost at the top of this page). The Characteristic Declaration
value (handle 0x000F) holds information about the subsequent Characteristic Value Declaration
(Properties, Handle, and Type). Finally, the Heart Rate Measurement Characteristic Value (handle
0x0010) eventually holds the actual number of heart beats per minute.

The Generic Attribute Profile (GATT)


The concept of the GATT is to group attributes in an attribute table together in a very specific and
logical order. The heart rate profile in Table 1 is an example of such a group.

Service Declaration attribute


At the top of each group you always have a Service Declaration attribute. Its type is always 0x2800, the
standard UUID for Service Declarations. Its handle depends on how many attributes are already
present in the table. Its permissions are always Read Only without any authentication or authorization
required. The value is another UUID defining what kind of service this is. In Table 1 the value is 0x180D,
the Bluetooth SIG defined UUID for a Heart Rate Service. Later in the tutorial we will make our own
attribute table with a service declaration containing our own custom UUID.

Characteristic Declaration
Immediately after this follows a Characteristic Declaration (there is a rare exception to this rule, but that
is out of scope for this tutorial). The Characteristic Declaration is similar to the Service Declaration. The
type is always 0x2803, the standard UUID for Characteristic Declarations. And the permissions are
always Read Only without any authentication or authorization required. The value however, contains
some interesting data. It always contains a handle, a UUID, and a set of properties. These three
elements describe the subsequent Characteristic Value Declaration. The handle naturally points to the
Characteristic Value Declaration’s place in the attribute table. The UUID describes what type of
information or value we can expect to find in the Characteristic Value Declaration. For example, a
temperature value, the state of a light switch, or some custom arbitrary value. And finally, the properties
describe how the characteristic value can be interacted with. Table 2 shows the characteristic properties
bit field. Don’t worry about understanding the details of the table now. We will circle back to it later.

Now you might wonder why we have read/write permissions for an attribute and read/write properties
for the characteristic value. Shouldn’t they always be the same? And that is a legitimate question. The
properties for the characteristic value are actually only guidelines for the client, used in the GATT and
application layers. They are just clues, if you will, of what the client can expect from the Characteristic
Value Declaration attribute. The permissions for the attribute (on the ATT layer) will always overrule the
characteristic value properties (on the GATT layer). Now you might ask again “but why do we need both
permissions and properties?”. And the simple, but disappointing, answer is: “Because the Bluetooth
Core Specification says so”. It is confusing, but has implications for how we will set up our characteristic
later so it needs to be said.

Characteristic Value Declaration


After the Characteristic Declaration follows the Characteristic Value Declaration. This is the attribute
that finally contains the actual value. And again, the value might be a temperature value, the state of a
light switch, etc., etc. The Characteristic Value Declaration’s type is the same as specified in the
Characteristic Declaration’s value field and the permissions are defined in the application layer (or us).
Descriptor Declaration
After the Characteristic Value Declaration follows either

1. a new Characteristic Declaration (there can be many characteristics grouped in a service).


2. a new Service Declaration (there can be many services in a table).
3. a Descriptor Declaration.

In the case of the Heart Rate Measurement Characteristic in Table 1 the Characteristic Value
Declaration is followed by a Descriptor Declaration. The descriptor is an attribute with additional
information about the characteristic. There are several kinds of descriptors, but in this tutorial we will
only deal with the Client Characteristic Configuration Descriptor (CCCD). More about this later.

Attribute tables in nRF Connect Bluetooth


Low Energy app
Let us now compare what we just learned with the information displayed in the nRF Connect app when
we run the Heart Rate Service example from the SDK (examples/ble_peripheral/ble_app_hrs). After
you have connected your device the nRF Connect app should now show the entire attribute table.Each
line is an attribute and they are grouped in services and with characteristics as subgroups.

The following screenshots show the list of services displayed in the nRF Connect BLE app when
connected to my nRF52 DK board running the HRS example.
Figure 1:
Here our example uses two services that are typically used in a heart rate monitor sports watch for
example, a Heart Rate Service and a Battery Service. Our device have also a Device Information service.

To view the handle and UUID of an attribute, move the mouse pointer over the attribute name and a
hover text would be displayed.
Figure 2:
As you can see in the figure above, the type of attribute (UUID) for the Hear Rate Service is 0x180D. To
view the characteristics of a service, click the Expand/Collapse icon.

Figure 3:
Here we see that the Heart Rate service have two characteristics
(https://www.bluetooth.com/specifications/gatt/viewer?
attributeXmlFile=org.bluetooth.service.heart_rate.xml):

A Heart Rate Measurement characteristic, which is mandatory as discussed before, which holds the
heart rate value and is used to send a heart rate measurement.
(https://www.bluetooth.com/specifications/gatt/viewer?
attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml)
An optional Body Sensor Location characteristic, which is used to describe the intended location of
the heart rate measurement for the device.
(https://www.bluetooth.com/specifications/gatt/viewer?
attributeXmlFile=org.bluetooth.characteristic.body_sensor_location.xml)

The Heart Rate Measurement Characteristic ‘s type of attribute (UUID) is 0x2A37 and the “notify”
property is also shown. If you want to view the descriptors you can again click the Expand/Collapse
icon to expand the characteristics.

Figure 4:
Finally, we can see the values of the CCCD attribute. You can see the predefined descriptor UUID
(0x2902) and that the value is 0x0000. This means that notification is currently switched off. If a service
or a characteristic does not have any child attributes, the list is empty when you click the
Expand/Collapse icon.

The example
It is time to get our hands dirty! I figured that as an example we will make a readable and writeable
characteristic first. Then, after we have enjoyed manually reading and writing some data, we will
include the temperature of the nRF5x's CPU core in the characteristic. In case you didn't know already
the nRF5x actually has its own internal temperature sensor. It is not very accurate, but it is the simplest
way for us to get started with some actual dynamic sensor values. Finally, we will enable notification to
update the temperature values. First thing first: download the example code. To compile it, extract the
example to "your_SDK_folder\examples\ble_peripheral". If you need help with this please have a look
at this thread on devzone: [Compiling github projects]
(https://devzone.nordicsemi.com/question/35068/compiling-github-projects/). Open the project file
and hit “Build”. It should compile without any errors and if you browse through the files you will see that
it pretty much continues where the [service tutorial](https://devzone.nordicsemi.com/tutorials/8/ble-
services-a-beginners-tutorial/) ended.

The important files in this tutorial are our_service.c, our_service.h, and of course main.c. In these files
you will find various comments beginning with:

// FROM_SERVICE_TUTORIAL:

// ALREADY_DONE_FOR_YOU:

// OUR_JOB: Step X.X

FROM_SERVICE_TUTORIAL means that this is code we looked at in the previous tutorial. A few places
you will see ALREADY_DONE_FOR_YOU. This marks very basic code that I have already prepared for
you so that we don't have to go through the less important stuff in detail. Finally, OUR_JOB: Step X.X
indicates what and when things need to be done in the tutorial.

To-do list
This is what needs to be done to get our characteristic up and running:
1. Step 1: Add service. This was completed in the last tutorial when we made our own custom service
with a custom base UUID and a service UUID.
2. Step 2: Declare and configure the characteristic.
3. Step 3: Add a CCCD to let the characteristic send notifications at regular intervals or whenever the
temperature values are changing.

This is what our attribute table should include to achieve all this:
Table: 3
Step 1: Add the service
As mentioned above this step was completed in the last tutorial, but let us take a quick look at what
our work looks like in the nRF Connect app so far. It should look something like this:

The highlighted line is the Service Declaration declaring our service. Unfortunately, the nRF Connect
app does not display the attribute type for the declarations, but as we know from Table 1 attribute type
for the Service Declaration is 0x2800. When we hover over the Service Declaration for our new service
we can see that the attribute handle is 0x000C, and the attribute value is our 128-bit custom UUID.

Step 2: Add the Characteristic


Calling the SoftDevice function sd_ble_gatts_characteristic_add() is our first goal. This function
will add both the Characteristic Declaration and the Characteristic Value Declaration to our attribute
table. However, in order to get there we have to do a few things first.
sd_ble_gatts_characteristic_add() takes four parameters:

@param[in] uint16_t service_handle.

@param[in] ble_gatts_char_md_t const * p_char_md

@param[in] ble_gatts_attr_t const * p_attr_char_value

@param[out] ble_gatts_char_handles_t * p_handles

As you can see the function has three input parameters and one output parameter. The three input
parameters need to be populated with details that will define the characteristic attributes. These
parameters define the properties, read/write permissions, descriptors, characteristic values, etc. In fact,
you have more than 70 properties at your disposal when you are defining your characteristic. It sounds
daunting, but don't give up and throw away your dev kit just yet. Most of the parameters are optional
and almost all of them work just fine if you initialize them to zero using memset(&parameter, 0,
sizeof(parameter));. Pretty neat!

We will start by populating only the essential parameters we need to get started. In fact, all we need to
do is to choose where in memory to store the characteristic attributes and define a characteristic value
type using our custom UUID. In the function our_char_add() I have already declared all the necessary
variables for you and initialized them to zero. The order of which I have declared them might be a little
confusing, but since the necessary parameters are nested into structures, and these structures might be
nested into other structures, this is unfortunately the way it has to be. I have also selected names that
might seem a little cryptic, but these are naming conventions used throughout the SDKs and I chose to
go for consistency.

The three most important variables are:


1. ble_gatts_attr_md_t attr_md, The Attribute Metadata: This is a structure holding permissions
and authorization levels required by characteristic value attributes. It also holds information on
whether or not the characteristic value is of variable length and where in memory it is stored.
2. ble_gatts_char_md_t char_md, The Characteristic Metadata: This is a structure holding the
value properties of the characteristic value. It also holds metadata of the CCCD and possibly other
kinds of descriptors.
3. ble_gatts_attr_t attr_char_value, The Characteristic Value Attribute: This structure holds
the actual value of the characteristic (like the temperature value). It also holds the maximum length
of the value (it might e.g. be four bytes long) and it's UUID.

So let's start populating the parameters. To make things a little easier I have tried to assign each step a
number:

Step 2.A, Use custom UUID to define characteristic value type


The characteristic will need its own UUID just as the service did. And in fact, we can add this the exact
same way as we did with the service UUID. So type in the following at Step 2.A inside the
our_char_add() function:

uint32_t err_code;

ble_uuid_t char_uuid;

ble_uuid128_t base_uuid = BLE_UUID_OUR_BASE_UUID;


char_uuid.uuid = BLE_UUID_OUR_CHARACTERISTC_UUID;

err_code = sd_ble_uuid_vs_add(&base_uuid, &char_uuid.type);

APP_ERROR_CHECK(err_code);

This will use the same base UUID as the service, but add a different 16-bit UUID for the characteristic.
The UUID is defined as 0xBEEF in our_service.h. Note that this base UUID was added to the vendor
specific table in the previous tutorial when we created the custom service. All subsequent calls with the
same base UUID will return a reference to the same ID in the table. This way we will save some memory
by not needing to store a large array of 128-bit long IDs.

Step 2.B, Configure the Attribute Metadata


The following three lines are the bare minimum of what we need to describe the attributes of the
characteristic. The only thing the lines do is to decide where in memory to store the attributes. We are
going to store it in the SoftDevice (aka the stack) controlled part of memory and hence, we use
BLE_GATTS_VLOC_STACK. The only other valid option is to use BLE_GATTS_VLOC_USER to store the
attributes in the user controlled part of memory.

ble_gatts_attr_md_t attr_md;

memset(&attr_md, 0, sizeof(attr_md));

attr_md.vloc = BLE_GATTS_VLOC_STACK;

In the attribute metadata structure ble_gatts_attr_md_t you also have the option to define the
permissions with associated authorization requirements. For example if you need Man In The Middle
protection (MITM) or a passkey to access your attribute. We will circle back to this.
Step 2.C, Configure the Characteristic Value Attribute
Now that we have made our self a new UUID and decided where to store the characteristic we will
store this information in the Characteristic Value Attribute:

ble_gatts_attr_t attr_char_value;

memset(&attr_char_value, 0, sizeof(attr_char_value));

attr_char_value.p_uuid = &char_uuid;

attr_char_value.p_attr_md = &attr_md;

Step 2.D, Add handles for the characteristic to our struct


We need to add a variable holding the relevant handles for our characteristic in our service structure.
So head over to the ble_os_t definition in our_service.h and add the line shown below:

typedef struct

uint16_t conn_handle;

uint16_t service_handle;
// OUR_JOB: Step 2.D, Add handles for our characteristic

ble_gatts_char_handles_t char_handles;

}ble_os_t;

As you can see, the ble_os_t struct already has a field for the service declaration handle. The
connection handle, conn_handle, is there to keep track of the current connection and don't really have
anything to do with the attribute table handles. If you go to the definition of
ble_gatts_char_handles_t you can see that our new variable can hold 16-bit handles for the
characteristic value, user descriptor, its CCCD, and also something called Server Characteristic
Configuration Descriptor (SCCD) which is not within the scope of this tutorial.

Step 2.E, Add the new characteristic to the service


Then we are ready to add the new characteristic to our attribute table and it is as simple as this:

err_code = sd_ble_gatts_characteristic_add(p_our_service->service_handle,

&char_md,

&attr_char_value,

&p_our_service->char_handles);

APP_ERROR_CHECK(err_code);

As you can see, we are giving the SoftDevice information about what service the characteristic belongs
to (the service_handle), the Characteristic Metadata, and the Characteristic Value Attributes. The stack
then processes the parameters and initiates the characteristic. Then it stores the handle values of our
characteristic into our p_our_serice structure.

Now compile and download your application to the kit. Make sure that you have remembered to
program the SoftDevice to the kit as well. Open the nRF Connect BLE app, connect to your kit, and do
a service discovery. You should see something like this:

As you can see there is a new characteristic in our service, but it doesn't do anything useful at all. It has
no value and you can neither read from it nor write to it. This is what happens when we initialize the
read/write attribute permissions to zero.

Step 2.F, Add read/write properties to our characteristic value


To be able to read and write to the characteristic there are a couple of things we need to do. First go to
Step 2.F in our_char_add() and add the following lines:

ble_gatts_char_md_t char_md;

memset(&char_md, 0, sizeof(char_md));

char_md.char_props.read = 1;

char_md.char_props.write = 1;

This will populate the characteristic metadata with read and write properties. Do another service
discovery in nRF Connect and note that the "Properties" field in the Characteristic Declaration has
changed to "Read, Write":

Now you can try to read some values to the characteristic by pressing the “Read” button or type in
some hexadecimal number in the value field and hit the “Write” button.

But, wait, what is this?? You are still getting READ and WRITE_NOT_PERMITTED errors?

You should, if you have followed the tutorial to the letter so far. This is because what we just did was
just setting the properties in the characteristic declaration and as discussed earlier they are just
guidelines. So even though these properties are exposed when your nRF Connect app does a service
discovery we have yet to set the required permissions for read and write operations. Before we have
done this the SoftDevice doesn't know what to allow and simply denies any reads and writes of the
characteristic.
Step 2.G, Set read/write permissions to our characteristic
So let's add some read/write permissions. Since this is a beginners tutorial we will keep it simple and
leave the doors wide open. No security, encryption, or passkey needed. A simple way of doing this is to
use the macro BLE_GAP_CONN_SEC_MODE_SET_OPEN(). Go to Step 2.G in our_char_add() and add
the following two lines:

BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm);

BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.write_perm);

Now do yet another service discovery. When you try to read from the characteristic now then this
should pop up in the log window nRF Connect:

Great success! We have just read 0, zero(!), bytes from our new characteristic because we have not yet
assigned a value or a value length to it.

Next, try to write e.g. the value 0x12 to the characteristic. You should get the following error because the
value attribute length is initialized to zero:

Step 2.H, Set characteristic length


So how do we fix the errors above? Go to Step 2.H and add the following lines:

attr_char_value.max_len = 4;

attr_char_value.init_len = 4;

uint8_t value[4] = {0x12,0x34,0x56,0x78};

attr_char_value.p_value = value;

This will set the initial length, and also the maximum allowed length, of the characteristic value to 4
bytes. And just to be a little thorough we will also set the initial value to 12-34-56-78. Once again, try to
read from your characteristic and then write a couple of bytes. You should see that the value is
updated. Even if you disconnect and reconnect the new value should be retained.

Challenge 1:

1. Try to write a single byte to the characteristic. E.g. '12'.


2. Try to write four bytes to the characteristic. E.g. '12-34-56-78'.
3. Try to write five or more bytes to the characteristic. E.g. '12-34-56-78-90'.

What happens? Why are you not allowed to write more than four bytes to your characteristic and how
can you fix it?
Challenge 2:

Try to experiment with the attr_md permissions and the following permission macros:

1. BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS()
2. BLE_GAP_CONN_SEC_MODE_SET_OPEN()
3. BLE_GAP_CONN_SEC_MODE_SET_ENC_NO_MITM()

Try to do some reads and writes. See what happens and watch out for error messages in the nRF
Connect log window. Hint on number 3: If you click "Bond" in nRF Connect and bond with your kit you
will automatically add encryption (ENC) to your BLE link. However, you are not required to use Man In
The Middle (MITM) protection when bonding.

Step 3: Client Characteristic Configuration Descriptor


(CCCD)
So this is pretty cool, right? Not cool enough though. Let us add some dynamic temperature data, and
while we are at it, let's also make our kit a little more independent and useful by making it periodically
push data to our client. To achieve this we will need to:

1. Add some sort of push functionality.


2. Do some housekeeping of our BLE connection and services.
3. Set up a timer to trigger the temperature updates.

The BCS defines two ways of "pushing" data:


Vol 3: Part G, Ch. 4.10 & 4.11:

Indication - This sub-procedure is used when a server is configured to indicate a Characteristic


Value to a client and expects an Attribute Protocol layer acknowledgement that the indication was
successfully received.
Notification - This sub-procedure is used when a server is configured to notify a Characteristic
Value to a client without expecting any Attribute Protocol layer acknowledgment that the
notification was successfully received.
The subtle, but important difference here is that by using indication your kit (the server) will require an
application level acknowledgment in return after every transmission of updated data. By using
notification, on the other hand, our kit will just "mindlessly" transmit data and not care about whether it
is acknowledged by the application in the other end or not. So sticking to the "keep it simple"-
philosophy of this tutorial we will use the latter. To add notification functionality we need to add a
descriptor to our attribute table, namely the Client Characteristic Configuration Descriptor (CCCD). The
BLE Core Specifications has the following to say about the CCCD:

Vol 3: Part G, Ch 3.3.3.3:

The Client Characteristic Configuration declaration is an optional characteristic descriptor that


defines how the characteristic may be configured by a specific client [...]
What is written in between these cryptic lines is that the CCCD is a writable descriptor that allows the
client, i.e. your nRF Connect for Desktop or phone, to enable or disable notification or indication, on
your kit.

Step 3.A, Configuring CCCD metadata


This is what we need to do to add the CCCD to our characteristic.

ble_gatts_attr_md_t cccd_md;

memset(&cccd_md, 0, sizeof(cccd_md));

BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.read_perm);

BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.write_perm);

cccd_md.vloc = BLE_GATTS_VLOC_STACK;
char_md.p_cccd_md = &cccd_md;

char_md.char_props.notify = 1;

1. We declare a metadata structure for the CCCD to hold our configuration.


2. Using the same method as we did on the characteristic, we make sure that no security, encryption
or passkeys are necessary to read or write to the CCCD.
3. Also as we did with the characteristic, we decide where to store the descriptor, and once again we
store it in the SoftDevice controlled part of memory.
4. Then we store the CCCD metadata structure in our characteristic metadata structure.
5. Enable notification by setting the notify property bit.
Compile, download, run, and connect to your kit. Now you should be able to see the CCCD added to
your characteristic:

Notice also that "Notify" is added to the Characteristic Declaration's properties field. The UUID value of
the CCCD is 0x2902 as we have seen earlier and the value is 0x0000 meaning that notification and
indication is currently switched off. If you want you can enable notification right away, but since we
have not yet set up any mechanisms updating the values nothing fancy will happen. Anyway, try to click
the "Enable services" button. You should see that the value field of the CCCD is updated to 0x0001,
meaning that notification is indeed enabled. You can also use the "Write" button and write to the CCCD
manually.

Step 3.B, Housekeeping part 1: Give our service connection handle


a default value
In a simple example like ours housekeeping like this might actually not be necessary. However, it is
crucial when developing slightly more advanced applications. Furthermore, since this sort of
housekeeping is done in virtually every BLE examples in Nordic's SDKs I thought I would make an
exception from our "keep it simple"-philosophy and show you how it can be done.
Our service structure, the ble_os_t, has a field called "conn_handle". This shall hold the handle of the
current connection as provided by the BLE stack and we have to initialize it during system startup.
Naturally, since we are not in a connection at system startup, the handle should be initialized to some
"invalid" value. In the SDKs this value is defined as BLE_CONN_HANDLE_INVALID (with value 0xFFFF). So
head over to the our_service_init() function in our_service.c and type in the following.

p_our_service->conn_handle = BLE_CONN_HANDLE_INVALID;

Step 3.C, Housekeeping part 2: Responding to connect and


disconnect events
In main.c there is a function called ble_stack_init(). In this function it is possible to register BLE stack
event observers to receive BLE events information from the SoftDevice Handler.
An observer is essentially a piece of code that listens for events.  It consists on a handler function and
its associated parameter – a context. This handler function is called by the SoftDevice when a BLE
stack event has occurs. To keep our service up to date with the latest connection events we want to call
our handler function ble_our_service_on_ble_evt() when a BLE event is observed:

NRF_SDH_BLE_OBSERVER(m_our_service_observer, APP_BLE_OBSERVER_PRIO,
ble_our_service_on_ble_evt, (void*) &m_our_service);

Step 3.D, Housekeeping part 3: Handling BLE events related to our


service
Go back to our_service.c and find the function ble_our_service_on_ble_evt(). Inside we will make
a short switch case statement. In our example the only thing the statement will do is to update the
connection handle stored in the service structure. On a connection event we will store the current
connection handle as provided by the BLE stack. On a disconnect event we will set the handle back to
"invalid".

switch (p_ble_evt->header.evt_id)

case BLE_GAP_EVT_CONNECTED:

p_our_service->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;

break;

case BLE_GAP_EVT_DISCONNECTED:

p_our_service->conn_handle = BLE_CONN_HANDLE_INVALID;

break;

default:

// No implementation needed.

break;

Step 3.E, Update characteristic value


We are closing in on our goal. Now find the function our_temperature_characteristic_update().
This is where we will implement the notification. First implement an if-statement like this:

if (p_our_service->conn_handle != BLE_CONN_HANDLE_INVALID)

Here we check whether or not we are in a valid connection, and this is where the housekeeping comes
in handy. If you try to send out notifications when not in a connection the SoftDevice will get grumpy
and give you errors. That is why we need this if-statement and why we implemented the housekeeping
to keep the connection handle updated. Now, inside the if-statement type in the following:

uint16_t len = 4;

ble_gatts_hvx_params_t hvx_params;

memset(&hvx_params, 0, sizeof(hvx_params));

hvx_params.handle = p_our_service->char_handles.value_handle;

hvx_params.type = BLE_GATT_HVX_NOTIFICATION;

hvx_params.offset = 0;

hvx_params.p_len = &len;

hvx_params.p_data = (uint8_t*)temperature_value;

sd_ble_gatts_hvx(p_our_service->conn_handle, &hvx_params);

First you might wonder what hvx stands for? It is not very intuitive, but it stands for Handle Value X,
where X symbolize either notification or indication as the struct and function can be used for both. So
to do a notification we declare a variable, hvx_params, of type ble_gatts_hvx_params_t. This will
hold the necessary parameters to do a notification and provide them to the sd_ble_gatts_hvx()
function. Here is what we will store in the variable:
1. handle: The SoftDevice needs to know what characteristic value we are working on. In applications
with two or more characteristics naturally we will need to reference the handle of the specific
characteristic value we want to use. Our example only has one characteristic and we will use the
handle stored in p_our_service->char_handles.value_handle.
2. type: The SoftDevice needs to know what "hvx type" we want to do; a notification or indication. As
we are doing a notification we use BLE_GATT_HVX_NOTIFICATION. The other option would be
BLE_GATT_HVX_INDICATION.
3. offset: Your characteristic value might be a sequence of many bytes. If you want to transmit only a
couple of these bytes and the bytes are located in the middle of the sequence you can use the
offset to extract them. Since we want to update all of our four bytes we will set the offset to zero.
4. p_len: The SoftDevice needs to know how many bytes to transmit. There is no need to send 20
bytes every time if you only have four bytes of relevant data. As an example, let's say you have a
characteristic with the following sequence of bytes: 0x01, 0x02, 0x03, 0x04, 0x05 and you want to
send just the 3rd and the 4th byte. Then set offset to 2 and len to 2.
5. p_data: Here we add a pointer to the actual data.

Finally we pass this structure into the sd_ble_gatts_hvx(). We also provide the function with the
relevant connection handle. In some applications you might work with several concurrent connections
and this is why the function also needs to know what handle to use. Now we have set up everything
that has to do with the characteristic and notification.

Step 3.F, Update the characteristic with temperature data


The rest is simply a matter of providing some data to send. We need to measure and collect the data
we want to send and we need some mechanism to trigger each measurement. So find and uncomment
the function timer_timeout_handler() in main.c and type in the following:

int32_t temperature = 0;

sd_temp_get(&temperature);

our_temperature_characteristic_update(&m_our_service, &temperature);

nrf_gpio_pin_toggle(LED_4);

1. temperature: We need a temporary variable to store the measured temperature.


2. sd_temp_get(): This is a very simple SoftDevice function that does everything for us. Just pass a
pointer to our temperature variable and your measurements are done. Note that the result of the
sd_temp_get() function is the nRF5 die temperature in 0.25 degrees celsius as documented here.
I will leave it up to you to decide how to present the value. You can e.g. make a function to convert
the value to Fahrenheit.
3. our_temperature_characteristic_update(): Make a call to the characteristic update function we
made in Step 3.E and pass along a pointer to our service variable and the measured temperature.
4. nrf_gpio_pin_toggle(): Toggle LED 4 on the nRF5x DK kit just for show.

Step 3.G, Declare a timer ID and a timer interval


Not quite there yet. We need something to trigger the measurements. A trigger could for example be
the push of a button or some command received from the client side. In our case though, we will use a
timer to periodically measure the temperature. The first thing we need to do is defining the timer
interval and we need a timer ID to identify this particular timer. So, locate Step 3.G somewhere at the
top of main.c and type in the following:
APP_TIMER_DEF(m_our_char_timer_id);

#define OUR_CHAR_TIMER_INTERVAL APP_TIMER_TICKS(1000) // 1000 ms intervals

This will instantiate a timer ID variable and define a timer interval of 1000 ms.

Step 3.H, Initiate the timer


To initialize our timer this is all we have to do:

app_timer_create(&m_our_char_timer_id, APP_TIMER_MODE_REPEATED, timer_timeout_handler);

We pass:

1. Our timer ID
2. An enumeration called APP_TIMER_MODE_REPEATED. This tells the timer library to set up a timer
that triggers at regular intervals. The other option is APP_TIMER_MODE_SINGLE_SHOT which sets
up a timer that triggers only once.
3. A pointer to our timer timeout handler which is supposed to be executed on every timer event.

Step 3.I, Start our timer


Now it is time to start our engines!

Most often in Nordic's libraries the fact that a module is initiated does not mean that it is started, so we
have to call one last function to reach our goal:

app_timer_start(m_our_char_timer_id, OUR_CHAR_TIMER_INTERVAL, NULL);

We pass:

1. Our timer ID variable


2. Our defined timer interval
3. You have the option to pass a general purpose pointer that will be passed to the timeout handler
when the timer expires. We won't bother with this so we will just ignore it and exercise our right to
pass a simple NULL.

Compile, download, connect, and discover services. Now to the moment of truth: Click "Toggle
notifications". If we have done everything right the characteristic value should update every second. You
should see LED 4 and the value line in nRF Connect blink green every second. You should also see
values ticking in in the Log window in nRF Connect like this:

If you now put your finger on the nRF5 chip you should also see that the values are changing. The
reason why the values are Least Significant Byte first is discussed here.

Challenge 1:

Try to alter our_temperature_characteristic_update() so that you only send a notification when


the temperature has changed. Remember that BLE is all about saving energy so why spend resources
on transmitting the same value over and over again? Hint: Use a variable to store the current
temperature value and compare it to the new one on the next measurement.

Challenge 2:
Try to modify the timer so that the temperature value is only measured when you are in a connection.
I.e. start the timer on a connection event and stop it on a disconnect event. Why spend energy on
measurements if you don't use them, right? Hint: Look for a timer start and stop function in the
app_timer library. Then see if you can do some magic in the ble_event_handler() function in main.c.

Summary
And that is it! You have now made a custom attribute table with a basic characteristic transmitting data
from your server to your client. It is somewhat limited though, in the sense that it is only one way
communication and it has no security or other advanced features. However I hope that I achieved my
goal; that you gained at least some new knowledge and that the code is something you can expand
upon.
Once again I urge you to post any questions you have in the Questions-section on the forum, not here.
You will most likely get faster response that way. Positive or negative critique though is very welcome in
the comment section below. Also remember to check out the "Solution" folder on github if you have any
issues.
Vasiliy Baryshnikov
over 3 years ago 
54 comments

1 member is here
Adding Multiple Characteristics to your Service and updating their values, Step by Step:
I used "Bluetooth low energy Characteristics, a beginner's tutorial" to build on top of what they have
(SDK 15)

devzone.nordicsemi.com/.../ble-characteristics-a-beginners-tutorial

Step 1.

You would need to create another 16-bit characteristic UUID


Fullscreen
1 #define BLE_UUID_CHARACTERISTIC_1 0xBEEF // Just a random, but recognizable value
2 #define BLE_UUID_CHARACTERISTIC_2 0xB00B

Step 2.

In our_service.h navigate to Step 2.D where we define the structure ble_os_t.


We need to add the characteristic handles to our structure. We will be using these handle instances
when updating a corresponding characteristic.
Fullscreen
1 ble_gatts_char_handles_t char_handles_1; // Adding handles for the characteristic to our
2 ble_gatts_char_handles_t char_handles_2; // Adding handles for the characteristic to our

The code should look something like that:


Fullscreen
1 typedef struct
2 {
3 uint16_t conn_handle; /**< Handle of the current connection (as provided by the BLE stack
4 uint16_t service_handle; /**< Handle of Our Service (as provided by the BLE stack). */
5 // OUR_JOB: Step 2.D, Add handles for the characteristic attributes to our struct
6 ble_gatts_char_handles_t char_handles_1; // Adding handles for the characteristic to our
7 ble_gatts_char_handles_t char_handles_2; // Adding handles for the characteristic to our
8
9 }ble_os_t;

Step 3.

Add an extra characteristic

In our_services.c create a copy of the our_char_add which will create a new characteristic. You should
assign it a different name. You should assign it a unique UUID. You must assign it a unique characteristic
handle (such as char_handles_1 and char_handles_2). You may choose to set different max_len
depending on your application.
Fullscreen
1 static uint32_t our_char_add_1(ble_os_t * p_our_service)
2 {
3 ...
4
5 // OUR_JOB: Step 2.A, Add a custom characteristic UUID
6 char_uuid.uuid = BLE_UUID_CHARACTERISTIC_1; // specifying our characteristic UUID (16-
7
8 ...
9
10 // OUR_JOB: Step 2.E, Add our new characteristic to the service.
11 err_code = sd_ble_gatts_characteristic_add(p_our_service->service_handle, // adding a
12 &char_md, // adding characteristic metadata to the attribute table
13 &attr_char_value, // adding the attribute characteristic value to the attribute table
14 &p_our_service->char_handles_1); // adding characteristic handles to the attribute tab
15
16 ...
17 }
18
19 static uint32_t our_char_add_2(ble_os_t * p_our_service)
20 {
21

Step 4.

Now we will be initialising those characteristics.


Fullscreen
1 void our_service_init(ble_os_t * p_our_service)
2 {
3 ...
4
5 // OUR_JOB: Call the function our_char_add() to add our new characteristic to the servic
6 our_char_add_1(p_our_service);
7 our_char_add_2(p_our_service);
8
9 ...
10 }

Step 5.

Now let's make a function that updates the characteristics. Basically, you would clone your update
function.
Fullscreen
1 void characteristic_value_update_1(ble_os_t *p_our_service, char machine_serial_number[]
2 {
3 ...
4
5 hvx_params.handle = p_our_service -> char_handles_1.value_handle; // the handle needs to
6
7 ...
8 }
9
10 void characteristic_value_update_2(ble_os_t *p_our_service, char machine_serial_number[]
11 {
12 ...
13
14 hvx_params.handle = p_our_service -> char_handles_2.value_handle; // the handle needs to
15
16 ...
17 }

Enjoy!!

Vasiliy Baryshnikov
over 3 years ago
Receiving (retrieve or write) values from characteristics and making use of them. Step by Step:
I used "Bluetooth low energy Characteristics, a beginner's tutorial" to build on top of what they have
(SDK 15).

Step 1: 
In main.c somewhere at the top of the file add the write handler
Fullscreen
1 // BLE_WRITE:
2 /**@brief Function for handling write events to the LED characteristic.
3 *
4 * @param[in] characteristic1_value value that was received from the phone
5 */
6 // called from our_services.c from on_write();
7 // Make a note of the arguments that are passed to this handler, we will use that later
8 static void characteristic1_value_write_handler(uint32_t characteristic1_value)
9 {
10 NRF_LOG_INFO("We have received the characteristic1 value into our App: %d", charact
11 }
12 // Add other handlers here

Step 2:
In our_service.h file, you need to add the init structure at the top of the file.
Fullscreen
1 // BLE_WRITE:
2 /** @brief Our Service init structure. This structure contains all options and data need
3 * initialization of the service.*/
4 // This is used to pass the write handlers for different characteristics from main.c
5 // This is essentially like public constructor. All of the content will be copied to in
6 // Note that "uint32_t characteristic1_value" part had to match from Step 1
7 typedef void (*ble_os_characteristic1_value_write_handler_t) (uint32_t characteristic1_v
8
9 // Add other handlers here...
10 typedef struct
11 {
12 /**< Event handler to be called when the Characteristic1 is written */
13 ble_os_characteristic1_value_write_handler_t characteristic1_value_write_handler;
14 // Add other handlers here...
15
16 } ble_os_init_t;

Step 3:
In our_service.h file modify the structure.
 
Fullscreen
1 // This structure contains various status information for our service.
2 // The name is based on the naming convention used in Nordics SDKs.
3 // 'ble indicates that it is a Bluetooth Low Energy relevant structure and
4 // os is short for Our Service).
5 typedef struct
6 {
7 uint16_t conn_handle; /**< Handle of the current connection (a
8 uint16_t service_handle; /**< Handle of Our Service (as provided
9
10 ...
11
12 // BLE_WRITE: Write handlers. Upon BLE write, these handler will be called. Their im
13 ble_os_characteristic1_value_write_handler_t characteristic1_value_write_handler; /
14 // Add other handlers here...
15
16 }ble_os_t;

Step 4:
In our_service.c file modify the function.
Fullscreen
1 /**@brief Function for initiating our new service.
2 *
3 * @param[in] p_our_service Our Service structure.
4 * @param[in] init Our Service init structure. (BLE_WRITE)
5 *
6 */
7 void our_service_init(ble_os_t * p_our_service, ble_os_init_t * init)
8 {
9 uint32_t err_code; // Variable to hold return codes from library and softdevice fu
10
11 ...
12
13 // BLE_WRITE: transfer the pointers from the init instance to the module instance
14 p_our_service->characteristic1_value_write_handler = init->characteristic1_value_wri
15
16 ...
17
18 APP_ERROR_CHECK(err_code);

Step 5:
In main.c file modify the function. We have to init our service module and let it know about the write
handler.
Fullscreen
1 /**@brief Function for initializing services that will be used by the application.
2 */
3 static void services_init(void)
4 {
5 ret_code_t err_code;
6 nrf_ble_qwr_init_t qwr_init = {0};
7
8 // BLE_WRITE: Initialize Our Service module.
9 ble_os_init_t init = {0}; // Init Our Service module
10 init.characteristic1_value_write_handler = characteristic1_value_write_handler;
11 // Add other handlers here...
12
13 ...
14
15 // BLE_WRITE: We need to add the init instance pointer to our service instance
16 // Initialize our service
17 our_service_init(&m_our_service, &init);
18
19 ...
20

Step 6:
In main.c file, make sure that the following macro exists.
Fullscreen
1 /**@brief Function for initializing the BLE stack.
2 *
3 * @details Initializes the SoftDevice and the BLE event interrupt.
4 */
5 static void ble_stack_init(void)
6 {
7 ret_code_t err_code;
8
9 ...
10
11 // BLE_WRITE: Make sure this macro exists
12 // OUR_JOB: Step 3.C Call ble_our_service_on_ble_evt() to do housekeeping of ble con
13 // Needed for associating the observer with the event handler of the service
14 NRF_SDH_BLE_OBSERVER(m_our_service_observer, APP_BLE_OBSERVER_PRIO, ble_our_service_
15
16 }

Step 7:
In our_service.c file modify the function.
Fullscreen
1 // ALREADY_DONE_FOR_YOU: Declaration of a function that will take care of some houseke
2 void ble_our_service_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
3 {
4 ble_os_t * p_our_service =(ble_os_t *) p_context;
5 // OUR_JOB: Step 3.D Implement switch case handling BLE events related to our serv
6
7 switch(p_ble_evt -> header.evt_id)
8 {
9
10 ...
11
12 // BLE_WRITE:
13 // Write: Data was received to the module
14 case BLE_GATTS_EVT_WRITE:
15 on_write(p_our_service, p_ble_evt);
16 break;
17
18 //
19 default:
20 // No implementation needed
21

Step 8:
In our_service.c somewhere at the top of the file add this function
Fullscreen
1 // BLE_WRITE:
2 /**@brief Function for handling the Write event.
3 *
4 * @param[in] p_our_service Our Service structure.
5 * @param[in] p_ble_evt Event received from the BLE stack.
6 */
7 static void on_write(ble_os_t * p_our_service, ble_evt_t const * p_ble_evt)
8 {
9 NRF_LOG_INFO("on_write: called");
10
11 ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write
12
13
14
15 if ((p_evt_write->handle == p_our_service->characteristic1_value_write_handler.val
16 {
17 NRF_LOG_INFO("characteristic1_value: Write Happened!");
18
19
20 // Make sure that the data is 4 bytes (or whatever the size of your characteri

Emil_Ekelund
over 2 years ago
in reply to Vasiliy Baryshnikov
Hi,
Thanks a lot this tutorial, it really helped me!
However, I found a small error in the code, in Step 8 in the if-statement
Fullscreen
1 if ((p_evt_write->handle == p_our_service->characteristic1_value_write_handler.v

this line should be changed to


Fullscreen
1 if (p_evt_write->handle == p_our_service->char_handles_2.value_handle)

and my code started working. Otherwise I got an error saying "request for member
‘value_handle’ in something not a structure or union"
I am leaving this here for future reference.

epietrowicz
over 2 years ago
in reply to Emil_Ekelund
This doesn't seem to fix the problem for me as it attaches the handle to the
char_handles_2 from the previous tutorial. Were you actually able to get correct data
from a BLE central device? 

vickynike
5 months ago
in reply to epietrowicz
These are the changes I made to fix error:
In Step 8:
change 
Fullscreen
1 if ((p_evt_write->handle == p_our_service->characteristic1_value_write_h

to 
Fullscreen
1 if ((p_evt_write->handle == p_our_service->char_handles.value_handle))

Or you can check ble_os_t  stuct in our_service.h, and  handler corresponding


to ble_gatts_char_handles_t  is what contains "value_handler" . In my case it is
char_handles.
In our_service.h modify this:
Fullscreen
1 void our_service_init(ble_os_t * p_our_service, ble_os_init_t * init);

wolfman24
over 7 years ago
Hey Nordic! just wanted to say thanks for all the previews and tutorials, they have been a great big help
on me trying to finish my project. I'm here on the characteristics aspect (obviously) and i understand the
code for the most part just not really sure where it all goes. If you guys could post the code for this
tutorial, even though i know you're still working on it, that would save me a lot of time. Thanks again!

Brook Gebremedhin
over 6 years ago
@MartinBL
yes you are right , in that case it works
but what i wanted is to do write to the char value from the MCP mobile app
so i removed
hvx_params.p_data = (uint8_t*)temperature_value; since i want
to write to the char value from the app ,
and i want to print it via uart using
printf("hvx data: %d\r\n", (uint32_t)*hvx_params.p_data); and this
prints
the same value even if i change the char value from the app side.
this is where am stuck at

Vasiliy Baryshnikov
over 3 years ago
Thanks a lot for these three tutorials! Really helped me to understand what is going on!

 View More

You might also like