Exercise 1

Creating a custom service and characteristics

In this exercise, we will learn how to create our own custom service and characteristics. We will practice using the GATT API in nRF Connect SDK, which is again based on Zephyr RTOS, to create and add services and characteristics to our board’s GATT table.

For educational purposes, we will implement our own custom LED Button Service (LBS), which will be called my_lbs to separate it from the actual implementation of LBS in nRF Connect SDK.

LBS is a custom service created by Nordic with two characteristics that allow you to control the LEDs and monitor the state of the buttons on your Nordic board.

In this exercise, we will focus on the client-initiated GATT operations Read and Write. In the next exercise, we will add the server-initiated Notify operation.

Exercise steps

In the GitHub repository for this course, go to the base code for this exercise, found in lesson4/blefund_less4_exer1.

1. Define the 128-bit UUIDs for the GATT service and its characteristics.

Add the following lines to the file my_lbs.h.

#define BT_UUID_LBS_VAL 
	BT_UUID_128_ENCODE(0x00001523, 0x1212, 0xefde, 0x1523, 0x785feabcd123)

/** @brief Button Characteristic UUID. */
#define BT_UUID_LBS_BUTTON_VAL 
	BT_UUID_128_ENCODE(0x00001524, 0x1212, 0xefde, 0x1523, 0x785feabcd123)

/** @brief LED Characteristic UUID. */
#define BT_UUID_LBS_LED_VAL 
	BT_UUID_128_ENCODE(0x00001525, 0x1212, 0xefde, 0x1523, 0x785feabcd123)


#define BT_UUID_LBS           BT_UUID_DECLARE_128(BT_UUID_LBS_VAL)
#define BT_UUID_LBS_BUTTON    BT_UUID_DECLARE_128(BT_UUID_LBS_BUTTON_VAL)
#define BT_UUID_LBS_LED       BT_UUID_DECLARE_128(BT_UUID_LBS_LED_VAL)

Notice how the base UUID used for the service is 0xXXXXXXXX, 0x1212, 0xefde, 0x1523, 0x785feabcd123 and how the first part 0xXXXXXXXX is incremented by one for each attribute.

As we discussed earlier, the UUID is intended to represent the type of an attribute. The GATT client uses the UUID to know how to treat the value data. We will see later that nRF Connect for Mobile recognizes these UUIDs “types” and therefore presents us with a custom GUI to interact with it.

Note

Notice that in the header file my_lbs.h, there is a definition of the struct my_lbs_cb which has the two members led_cb and button_cb.

The purpose of this structure is to facilitate decoupling of the code responsible for controlling the LEDs and monitoring the buttons (in our case main.c) from the Bluetooth LE connectivity code (my_lbs.c). These two members are simply function pointers to allow storing two functions in your application code to be triggered anytime the button characteristic is read, or the LED characteristic is written and provide/update the data needed. The function my_lbs_init() does the actual assigning of these pointers from main.c to my_lbs.c.

2. Create and add the service to the Bluetooth LE stack.

Now we will statically add the service to the attributes table of our board (the GATT server) using the BT_GATT_SERVICE_DEFINE() macros to statically create and add a service.

Add the following code in my_lbs.c

BT_GATT_SERVICE_DEFINE(my_lbs_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_LBS),
/* STEP 3 - Create and add the Button characteristic */

/* STEP 4 - Create and add the LED characteristic. */

);

The above code creates and adds an empty primary service to the attribute table and assigns it the UUID defined in BT_UUID_LBS.

3. Create and add the custom Button characteristic.

We will use the BT_GATT_CHARACTERISTIC() macro to statically create and add characteristics inside the service.

BT_GATT_CHARACTERISTIC() API

The first parameter to add is the UUID of the characteristic, BT_UUID_LBS_BUTTON, defined in step 1. Then the second and third parameters are the attribute properties and attribute permissions for the characteristic. We are only adding the Read operation for now, so we will set them to BT_GATT_CHRC_READ and BT_GATT_PERM_READ respectively.

The fourth parameter is the read callback. This is a callback function that is triggered whenever someone tries to read the Button characteristic. We will call this read_button and define it in a later step.

The fifth parameter will be set to NULL as we are not supporting the Write operation on the Button characteristic.

Lastly, we will pass the user data button_state , which is a boolean (0x01 , 0x00) representing the button state (Button pressed or Button released). Note that the user data is optional, but we will use it in the our LBS implementation.

Add the following code inside the service definition:

	BT_GATT_CHARACTERISTIC(BT_UUID_LBS_BUTTON,
			       BT_GATT_CHRC_READ,
			       BT_GATT_PERM_READ, read_button, NULL,
			       &button_state),

4. Create and add the custom LED characteristic.

This step is similar to step 3. The first parameter to add is the UUID of the characteristic, BT_UUID_LBS_BUTTON. The second and third parameters are the attribute properties and attribute permission, in the case of the LED characteristic, we want to support the Write operation, so we will pass BT_GATT_CHRC_WRITE and BT_GATT_PERM_WRITE respectively.

In this case, the fourth parameter is set to NULL, as the LED characteristic will not support the Read operation.

The write callback function, which is triggered whenever someone tries to write to the LED characteristic, is set to write_led and will be defined in step 6. No user data is set in the LED characteristic since we will get this value from the GATT client (the central device).

Add the following code inside the service definition:

	BT_GATT_CHARACTERISTIC(BT_UUID_LBS_LED,
			       BT_GATT_CHRC_WRITE,
			       BT_GATT_PERM_WRITE,
			       NULL, write_led, NULL),

5. Implement the read callback function read_button() of the Button characteristic.

The read callback function is triggered when a request to read the Button characteristic is received. The read callback function must have the bt_gatt_attr_read_func_t function signature, shown below:

bt_gatt_attr_read_func_t function signature

We want the read callback function to call the registered application callback function to read the current value of the button (pressed or released), then call the function bt_gatt_attr_read() to send the value to the GATT client (the central device).

The bt_gatt_attr_read() takes the following parameters:

For the conn, attr, buf, buf_len, and offset, we will simply forward the values passed to us from the stack. While for value and value_len we will rely on the application callback function to update it.

Add the following code in my_lbs.c:

static ssize_t read_button(struct bt_conn *conn,
			  const struct bt_gatt_attr *attr,
			  void *buf,
			  uint16_t len,
			  uint16_t offset)
{
	//get a pointer to button_state which is passed in the BT_GATT_CHARACTERISTIC() and stored in attr->user_data
	const char *value = attr->user_data;

	LOG_DBG("Attribute read, handle: %u, conn: %p", attr->handle,
		(void *)conn);

	if (lbs_cb.button_cb) {
		// Call the application callback function to update the get the current value of the button
		button_state = lbs_cb.button_cb();
		return bt_gatt_attr_read(conn, attr, buf, len, offset, value,
					 sizeof(*value));
	}

	return 0;
}

6. Implement the write callback function write_led() of the LED characteristic.

The write callback function is triggered when a request to write to the LED characteristic is received. The write callback function must have the bt_gatt_attr_write_func_t function signature shown below:

We want the write callback function to read the value received from the central device, stored in buf, and then call the registered application callback function to update the state of the LED.

Add the following code in my_lbs.c :

static ssize_t write_led(struct bt_conn *conn,
			 const struct bt_gatt_attr *attr,
			 const void *buf,
			 uint16_t len, uint16_t offset, uint8_t flags)
{
	LOG_DBG("Attribute write, handle: %u, conn: %p", attr->handle,
		(void *)conn);

	if (len != 1U) {
		LOG_DBG("Write led: Incorrect data length");
		return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
	}

	if (offset != 0) {
		LOG_DBG("Write led: Incorrect data offset");
		return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
	}

	if (lbs_cb.led_cb) {
		//Read the received value 
		uint8_t val = *((uint8_t *)buf);

		if (val == 0x00 || val == 0x01) {
			//Call the application callback function to update the LED state
			lbs_cb.led_cb(val ? true : false);
		} else {
			LOG_DBG("Write led: Incorrect value");
			return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
		}
	}

	return len;
}

7. Include the header file of the customer service my_lbs.h

Now that we are done defining our custom LBS service, we can add it to the main application code.

Add the following line in main.c

#include "my_lbs.h"

8. Controlling the LED.

8.1 Specify controlling LED3 on the board.

Add the following line in main.c

#define USER_LED                DK_LED3

8.2 Define the application callback function for controlling the LED.

We will simply rely on the Buttons and LED library and call dk_set_led() to set the state of the LED. This function will be called by the write callback function of the LED characteristic, write_led(), and it will pass either True or False.

Add the following code in main.c

static void app_led_cb(bool led_state)
{
	dk_set_led(USER_LED, led_state);
}

9. Monitoring the button.

9.1 Specify monitoring Button 1 on the board.

Add the following line in main.c

#define USER_BUTTON             DK_BTN1_MSK

9.2 Define the application callback function for reading the state of the button.

This function will simply return the global variable app_button_state to the caller (the read callback function of the Button characteristic). Since app_button_state is updated in the button_changed() function already defined in main.c that is called whenever a button is pressed, this variable will represent the state of the button.

Add the following lines in main.c

static bool app_button_cb(void)
{
	return app_button_state;
}

10. Declare a variable app_callbacks of type my_lbs_cb

Declare the variable of type my_lbs_cb and initiate its members to the application callback functions app_led_cb and app_button_cb.

Add the following code in main.c

static struct my_lbs_cb app_callbacks = {
	.led_cb    = app_led_cb,
	.button_cb = app_button_cb,
};

11. Pass the application callback functions stored in app_callbacks to our custom LBS service.

This is done by passing app_callbacks to my_lbs_init(), a function already defined in my_lbs.c to register application callbacks for both the LED and the Button characteristics.

Add the following code in main.c

	err = my_lbs_init(&app_callbacks);
	if (err) {
		printk("Failed to init LBS (err:%d)n", err);
		return;
	}

12. Build and flash the application on your board.

LED1 on your board should be blinking, indicating that your board is advertising.

13. Connect to your board using your smartphone.

Android

Open nRF Connect for Mobile on your smartphone, and connect to your device named “MY_LBS1“, in the Scanner tab.

nRF Connect for Android

Here we can see our LBS service, and nRF Connect for Mobile recoginzes the UUID for the service and its characteristics and labels the service as “Nordic LED Button Service” and its characteristics as “Button” and “LED”. Also, notice that the Button characteristic currently supports only the GATT read operation, while the LED characteristic supports the GATT write operation.

14. Control the LED

In nRF Connect for Mobile, press on the arrow next to the LED characteristic to write to it.

nRF Connect for Android

A pop-up window will appear, allowing you to either turn on or turn off the LED on the board.

nRF Connect for Android

Select ON and then SEND to turn LED3 on. Then select the arrow again, and select OFF to turn off LED3.

15. Read the button status.

Press and hold button 1 on your board while simultaneously pressing the arrow next to the button characteristic to read it. You should see that the value is now updated to the Button pressed.

nRF Connect for Android

In the next exercise, we will add the Notify operation to the Button characteristic so the button status will be updated without the need for us to manually poll read.

iOS

Open nRF Connect for Mobile on your smartphone, and connect to your device named “MY_LBS1“, in the Scanner tab.

nRF Connect for iOS

Here we can see our LBS service, and nRF Connect for Mobile recognizes the UUID for the service and its characteristics and labels the service as “Nordic LED Button Service” and its characteristics as “Button” and “LED”. Also, notice that the Button characteristic currently supports only the GATT read operation, while the LED characteristic supports the GATT write operation.

14. Control the LED

In nRF Connect for Mobile, press on the arrow next to the LED characteristic to write to it.

A pop-up window will appear, allowing you to either turn on or turn off the LED on the board.

nRF Connect for iOS

Select Bool, and then switch the value to True and select Write to turn on LED3. Then select the arrow again, and switch the value to False to turn off LED3.

15. Read the button status.

Press and hold button 1 on your board while simultaneously pressing the arrow next to the button characteristic to read it. You should see that the value is now updated to the Button pressed.

nRF Connect for iOS

In the next exercise, we will add the Notify operation to the Button characteristic so the button status will be updated without the need for us to manually poll read.

Note

Although the values sent are simply 0x00 and 0x01, nRF Connect for Mobile presents these values as Button released and Button pressed for better visualization since it recognizes the UUID for the Button characteristic.
Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.