As a continuation of last week’s tutorial (Prototyping BLE apps on the nRF52840 USB Dongle (Part A)), where we covered the following:
- Adding header rows to the USB dongle
- Mounting it on a breadboard
- Connecting an external LED and an external push button to the dongle
- Reuse and modify a Nordic template BLE peripheral example to assign the correct pins to the external LED and button
- Flashing the USB dongle with the firmware (via the Programmer app part of nRF Connect for the desktop)
If you haven’t gone through last week’s tutorial, I highly encourage you to do so before continuing with this post (you can read it here).
In today’s tutorial, we will get to do some of the more fun stuff!
We’ll go through:
- Overview of the hardware design and connections
- Implementing a custom Service comprised of two Characteristics:
- A characteristic used to allow turning the LED on and off
- A characteristic used to report the state of the button (pushed vs. released)
- Implement the button handler to report push and release events via Notifications to the Client (the BLE Central in our case)
- Implement the handler for writes to the LED characteristic to turn on/off the external LED
- Testing functionality by connecting to the dongle from a BLE mobile app (such as nRF Connect or LightBlue)
Hardware Overview
Let’s take a look back at the schematic for our project and review our peripheral connections:
- The LED is connected to Pin #1.10
- The push button is connected to Pin #0.24
And here’s a look at what the circuit looks like on a breadboard:
We could take this a step forward and make the hardware layout more compact. What I’ve done is stick together the two tiny breadboards back-to-back and run some rigid copper wires to reduce the space and make the hardware more stable physically:
Now that we’ve gotten the hardware up and ready, let’s get working on the firmware to fully implement the project.
Custom GATT Design
In our project, we mentioned that we have one custom service and two custom characteristics. The first step in any custom GATT design is assigning UUIDs to each custom service or characteristic. In BLE, user-assigned UUIDs are always 128 bits. The easiest way is to use an online GUID generator like https://www.guidgenerator.com/ (or you could simply assign a manual UUID).
Bluetooth GATT: How to Design Custom Services & Characteristics [MIDI device use case][/yellowbox]
- Once you have the GUID generator website launched, check the Uppercase option, enter 1 in the field “How many GUIDs do you want (1-2000):”.
The reason we will be generating only one UUID is that we can use it as a base and increment specific bytes within it and assign the services and characteristics accordingly. It also makes it easier to relate a specific characteristic to another one and to the parent service associated with this characteristic. - To better explain this, let’s start by generating a UUID:
- Next, take that value and “zero out” the 3rd and 4th significant bytes (shown in bold):
B3045432-E8DF-4E40-A8EC-5EB89C80E29D. - Now, this value will be the base UUID for our service and its characteristics:
B3040000-E8DF-4E40-A8EC-5EB89C80E29D - This also makes it easier to work with within the nRF5 SDK, since it requires custom (also called vendor-specific) UUIDs to be added in two parts: a base and an offset.
This will make a lot more sense when we go over the source code for adding the custom UUIDs. - So, given this base UUID, here are the UUID values we assign to each of the service and characteristics:
- Simple service: B3040001-E8DF-4E40-A8EC-5EB89C80E29D
- LED characteristic: B3040002-E8DF-4E40-A8EC-5EB89C80E29D
- Button characteristic: B3040003-E8DF-4E40-A8EC-5EB89C80E29D
- Simple service: B3040001-E8DF-4E40-A8EC-5EB89C80E29D
- Now that we have the custom UUIDs assigned, it’s time to define the permissions for the different characteristics. The main aspects we’re concerned with are: Read, Write, Notifications, Indications.
- The LED characteristic:
- Write: enabled.
We need to be able to write to this characteristic in order to turn on and off the LED. - Read: enabled.
This is optional for us, but it would be nice to be able to read the status of the LED (on vs. off), so we will enable it. - Notifications: disabled.
We do not need any notifications since, per the system operation, we will only be controlling the LED from the client side (Central side). - Indications: disabled.
Not needed for the same reason we do not need Notifications.
- Write: enabled.
- The Button characteristic:
- Write: disabled.
It does not make sense for us to enable writes for this characteristic since the button changes status based on local physical contact at the server. - Read: enabled.
We want to be able to read the status of the button (whether it’s pressed or released). - Notifications: enabled.
We want to be notified at the Central side whenever the button is pressed and whenever it’s released. - Indications: disabled.
Not needed in this case since we have Notifications enabled.
- Write: disabled.
- The LED characteristic:
- The next step is for us to implement the application in firmware.
Firmware Implementation
We will have three main files in our application:
- main.c
- simple_service.h
- simple_service.c
Let’s start with the implementation of the service, which we’ll call Simple Service.
Service and Characteristics
simple_service.h
Let’s go through each line of the header file for our Simple service.
First, the usual conditional #define needed to avoid duplicate #include of the same header file.
#ifndef SIMPLE_SERVICE_H #define SIMPLE_SERVICE_H
Then we include the files needed:
#include <stdint.h> #include "ble.h" #include "ble_srv_common.h"
The following source code is used to define a macro that can be used to instantiate the Simple service. This technique is implemented by other BLE services that are included in the nRF5 SDK. One of the benefits is that allows the application to instantiate the service and not have to worry about handling the BLE events manually (since the event handler gets assigned in the macro and the SoftDevice passes the BLE events back to this event handler).
#define BLE_SIMPLE_SERVICE_OBSERVER_PRIO 2 #define BLE_SIMPLE_SERVICE_DEF(_name) \ static ble_simple_service_t _name; \ NRF_SDH_BLE_OBSERVER(_name ## _obs, \ BLE_SIMPLE_SERVICE_OBSERVER_PRIO, \ ble_simple_service_on_ble_evt, \ &_name)
The following defines the base UUID and the offset we mentioned earlier. We use the same base for the service and the two characteristics to make it easier to add the UUIDs to the SoftDevice database. Notice that the bytes within the base UUID need to be listed in reverse order since they are stored in little-endian format.
Little Endian means: the least significant byte of the data is placed at the byte with the lowest address, and so on.
// UUID for service auto-generated from www.guidgenerator.com // Simple service: B3040001-E8DF-4E40-A8EC-5EB89C80E29D // LED characteristic: B3040002-E8DF-4E40-A8EC-5EB89C80E29D // Button characteristic: B3040003-E8DF-4E40-A8EC-5EB89C80E29D // The bytes are stored in little-endian format, meaning the // Least Significant Byte is stored first // (reversed from the order they're displayed as) // Base UUID: B3040000-E8DF-4E40-A8EC-5EB89C80E29D #define BLE_UUID_SIMPLE_SERVICE_BASE_UUID {0x9D, 0xE2, 0x80, 0x9C, 0xB8, 0x5E, 0xEC, 0xA8, 0x40, 0x4E, 0xDF, 0xE8, 0x00, 0x00, 0x04, 0xB3} // Service & characteristics UUIDs #define BLE_UUID_SIMPLE_SERVICE_UUID 0x0001 #define BLE_UUID_LED_CHAR_UUID 0x0002 #define BLE_UUID_BUTTON_CHAR_UUID 0x0003
Next, we define the events that we want to report back to the application. These events are ones that we need to act upon at the application level. For example, to turn on/off an LED or record the occurrence of an event (such as in the case of notifications being enabled/disabled). It is good practice to leave these kinds of decisions and actions to happen at the application level. We want to have the service be as dumb as possible so that it can be reused by other applications without having to change the service’s implementation.
/**@brief Simple Service event type. */ typedef enum { BLE_BUTTON_EVT_NOTIFICATION_ENABLED, /**< Button State Notification enabled event. */ BLE_BUTTON_EVT_NOTIFICATION_DISABLED, /**< Button State Notification disabled event. */ BLE_LED_TURN_ON_EVT, /**< LED Turned ON event. */ BLE_LED_TURN_OFF_EVT /**< LED Turned OFF event. */ } ble_simple_service_evt_type_t;
The following is a definition for a custom data structure that will hold an event tied to a specific connection (via the unique connection handle variable conn_handle ).
/**@brief Simple Service event. */ typedef struct { ble_simple_service_evt_type_t evt_type; /**< Type of event. */ uint16_t conn_handle; /**< Connection handle. */ } ble_simple_service_evt_t;
We use forward declarations such as the following to be able to reference a custom type before its implementation. In this case, the type ble_simple_service_t is referenced by the upcoming event handler function before ble_simple_service_t is fully defined.
// Forward declaration of the simple_service_t type. typedef struct ble_simple_service_s ble_simple_service_t;
The following is the prototype definition for the Simple service event handler function.
/**@brief Simple Service event handler type. */ typedef void (* ble_simple_service_evt_handler_t) (ble_simple_service_t * p_simple_service, ble_simple_service_evt_t * p_evt);
The following is the definition of the initialization data structure used by the application to initialize the Simple service. It contains a pointer to an event handler function that gets implemented at the application level to handle the custom events we defined in the enum ble_simple_service_evt_type_t above, a value to hold the initial LED state, a value to hold the initial button state.
/**@brief Simple Service init structure. This contains all options and data needed for * initialization of the service.*/ typedef struct { ble_simple_service_evt_handler_t evt_handler; /**< Event handler to be called for handling events in the Simple Service. */ uint8_t initial_led_state; /**< Initial Primary Data */ uint8_t initial_button_state; /**< Initial Secondary Data */ } ble_simple_service_init_t;
Here we define the main Simple service data structure that holds information such as the connection handle, the service handle, application event handler, UUID type, LED and Button characteristic handles.
/**@brief Simple Service structure. * This contains various status information * for the service. */ struct ble_simple_service_s { uint16_t conn_handle; uint16_t service_handle; ble_simple_service_evt_handler_t evt_handler; uint8_t uuid_type; ble_gatts_char_handles_t led_char_handles; ble_gatts_char_handles_t button_char_handles; };
The following is the function used by the application to initialize the Simple service. It takes in two arguments: a pointer to a Simple service object (which gets instantiated by calling the BLE_SIMPLE_SERVICE_DEF macro) and a pointer to the initialization data structure.
uint32_t ble_simple_service_init(ble_simple_service_t * p_simple_service, const ble_simple_service_init_t * p_simple_service_init);
The following is the event handler function that is referenced by the macro BLE_SIMPLE_SERVICE_DEF and is the function that gets called by the SoftDevice to report any relevant BLE events.
void ble_simple_service_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context);
Lastly, we define the function that is used to update the Button state characteristic value. This function also handles sending a notification to the Client if it has subscribed to Notifications.
ret_code_t ble_button_state_characteristic_update(ble_simple_service_t * p_simple_service, uint8_t data, uint16_t conn_handle);
We end the file by closing out the conditional #define for the header file.
#endif /* SIMPLE_SERVICE_H */
simple_service.c
Now, let’s go over the implementation of the Simple service in the source file simple_service.c.
First, we include the necessary header files, and most importantly simple_service.h.
#include <string.h> #include "nrf_log.h" #include "ble_conn_state.h" #include "simple_service.h"
Next, we define the strings for the two characteristics: the LED characteristic and the Button characteristic.
// Contains two characteristics: // - One for the state of the LED // - Another for data going from the Secondary (Peripheral) device static const uint8_t LEDCharName[] = "LED State"; static const uint8_t ButtonCharName[] = "Button State";
The following two functions handle assigning and clearing the connection handle depending on whether a connection was established or lost.
/**@brief Function for handling the Connect event. * * @param[in] p_simple_service Button service structure. * @param[in] p_ble_evt Event received from the BLE stack. */ static void on_connect(ble_simple_service_t * p_simple_service, ble_evt_t const * p_ble_evt) { p_simple_service->conn_handle = p_ble_evt->evt.gap_evt.conn_handle; }
/**@brief Function for handling the Disconnect event. * * @param[in] p_simple_service Simple service structure. * @param[in] p_ble_evt Event received from the BLE stack. */ static void on_disconnect(ble_simple_service_t * p_simple_service, ble_evt_t const * p_ble_evt) { UNUSED_PARAMETER(p_ble_evt); p_simple_service->conn_handle = BLE_CONN_HANDLE_INVALID; }
For the Write event, we need to handle two events:
- A Write to CCCD of the Button characteristic, which indicates enabling or disabling Notifications.
- A Write to the LED characteristic value, which causes the LED to turn on/off.
In both cases we need to report the event back to the application level by calling its event handler p_simple_service->evt_handler().
/**@brief Function for handling the Write event. * * @param[in] p_simple_service Simple Service structure. * @param[in] p_ble_evt Event received from the BLE stack. */ static void on_write(ble_simple_service_t * p_simple_service, ble_evt_t const * p_ble_evt) { ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write; NRF_LOG_INFO("Write event received"); if ( (p_evt_write->handle == p_simple_service->button_char_handles.cccd_handle) && (p_evt_write->len == 2)) { if (p_simple_service->evt_handler == NULL) { NRF_LOG_INFO("Event handler is NULL"); return; } ble_simple_service_evt_t evt; if (ble_srv_is_notification_enabled(p_evt_write->data)) { NRF_LOG_INFO("Notification enabled"); evt.evt_type = BLE_BUTTON_EVT_NOTIFICATION_ENABLED; } else { NRF_LOG_INFO("Notification disabled"); evt.evt_type = BLE_BUTTON_EVT_NOTIFICATION_DISABLED; } evt.conn_handle = p_ble_evt->evt.gatts_evt.conn_handle; // CCCD written, call application event handler. p_simple_service->evt_handler(p_simple_service, &evt); } else if ( (p_evt_write->handle == p_simple_service->led_char_handles.value_handle) && (p_evt_write->len == 1)) { if (p_simple_service->evt_handler == NULL) { NRF_LOG_INFO("Event handler is NULL"); return; } ble_simple_service_evt_t evt; if (p_evt_write->data[0] == 0x01) { evt.evt_type = BLE_LED_TURN_ON_EVT; evt.conn_handle = p_ble_evt->evt.gatts_evt.conn_handle; p_simple_service->evt_handler(p_simple_service, &evt); } else if (p_evt_write->data[0] == 0x00) { evt.evt_type = BLE_LED_TURN_OFF_EVT; evt.conn_handle = p_ble_evt->evt.gatts_evt.conn_handle; p_simple_service->evt_handler(p_simple_service, &evt); } else { // Invalid value } } }
The following is the function used to add the LED characteristic to the Simple service.
There are a few important things happening in this function:
- Setting the permissions on the Attribute (enabling Writes and Reads). Notice that both Notifications or Indications are not enabled.
- Setting the User Descriptor to a human-readable string “LED State”.
- Defining the UUID for the characteristic (using the base UUID and offset).
- Assigning the initial value of the LED state.
- Finally, adding the characteristic object to the database.
/**@brief Function for adding the LED characteristic. * The permissions should be: Read, Write, No Notifications, No Indications */ static uint32_t led_char_add(ble_simple_service_t * p_simple_service, uint8_t init_value) { ble_gatts_char_md_t char_md; ble_gatts_attr_t attr_char_value; ble_uuid_t ble_uuid; ble_gatts_attr_md_t attr_md; memset(&char_md, 0, sizeof(char_md)); memset(&attr_md, 0, sizeof(attr_md)); memset(&attr_char_value, 0, sizeof(attr_char_value)); // Set permissions on the Characteristic value BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.write_perm); BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm); char_md.char_props.read = 1; char_md.char_props.write = 1; char_md.p_char_user_desc = LEDCharName; char_md.char_user_desc_size = sizeof(LEDCharName); char_md.char_user_desc_max_size = sizeof(LEDCharName); char_md.p_char_pf = NULL; char_md.p_user_desc_md = NULL; char_md.p_sccd_md = NULL; // Define the LED Characteristic UUID ble_uuid.type = p_simple_service->uuid_type; ble_uuid.uuid = BLE_UUID_LED_CHAR_UUID; // Attribute Metadata settings attr_md.vloc = BLE_GATTS_VLOC_STACK; attr_md.rd_auth = 0; attr_md.wr_auth = 0; attr_md.vlen = 0; // Attribute Value settings attr_char_value.p_uuid = &ble_uuid; attr_char_value.p_attr_md = &attr_md; attr_char_value.init_len = sizeof(uint8_t); attr_char_value.init_offs = 0; attr_char_value.max_len = sizeof(uint8_t); attr_char_value.p_value = &init_value; return sd_ble_gatts_characteristic_add(p_simple_service->service_handle, &char_md, &attr_char_value, &p_simple_service->led_char_handles); }
The following is the function used to add the Button characteristic to the Simple service.
There are a few important things happening in this function:
- Setting the permissions on the Attribute (enabling Reads and disabling Writes).
- Setting the CCCD permissions to enable both Write and Read permissions. Enabling Write permission on the CCCD enables Notifications or Indications. Specifically, only Notifications are then allowed via the char_md.char_props.notify = 1; assignment.
- Setting the User Descriptor to a human-readable string “Button State”.
- Defining the UUID for the characteristic (using the base UUID and offset).
- Assigning the initial value of the Button state.
- Finally, adding the characteristic object to the database.
/**@brief Function for adding the Button characteristic. * The permissions should be: Read, No Write, Notifications, No Indications */ static uint32_t button_char_add(ble_simple_service_t * p_simple_service, uint8_t init_value) { ble_gatts_char_md_t char_md; ble_gatts_attr_md_t cccd_md; ble_gatts_attr_t attr_char_value; ble_uuid_t ble_uuid; ble_gatts_attr_md_t attr_md; memset(&char_md, 0, sizeof(char_md)); memset(&cccd_md, 0, sizeof(cccd_md)); memset(&attr_md, 0, sizeof(attr_md)); memset(&attr_char_value, 0, sizeof(attr_char_value)); // Set permissions on the CCCD and Characteristic value BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.read_perm); BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.write_perm); // Permissions for Attribute Value BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(&attr_md.write_perm); BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm); // CCCD settings (needed for notifications and/or indications) cccd_md.vloc = BLE_GATTS_VLOC_STACK; char_md.char_props.read = 1; char_md.char_props.notify = 1; char_md.p_char_user_desc = ButtonCharName; char_md.char_user_desc_size = sizeof(ButtonCharName); char_md.char_user_desc_max_size = sizeof(ButtonCharName); char_md.p_char_pf = NULL; char_md.p_user_desc_md = NULL; char_md.p_cccd_md = &cccd_md; char_md.p_sccd_md = NULL; // Define the Button Characteristic UUID ble_uuid.type = p_simple_service->uuid_type; ble_uuid.uuid = BLE_UUID_BUTTON_CHAR_UUID; // Attribute Metadata settings attr_md.vloc = BLE_GATTS_VLOC_STACK; attr_md.rd_auth = 0; attr_md.wr_auth = 0; attr_md.vlen = 0; // Attribute Value settings attr_char_value.p_uuid = &ble_uuid; attr_char_value.p_attr_md = &attr_md; attr_char_value.init_len = sizeof(uint8_t); attr_char_value.init_offs = 0; attr_char_value.max_len = sizeof(uint8_t); attr_char_value.p_value = &init_value; return sd_ble_gatts_characteristic_add(p_simple_service->service_handle, &char_md, &attr_char_value, &p_simple_service->button_char_handles); }
The following is the function that allows the application to initialize the Simple service and add it to the database.
The two most important operations in this function are:
- Defining the UUID comprised of the base UUID and the offset.
- Calling the functions to add both the LED and Button characteristics.
uint32_t ble_simple_service_init(ble_simple_service_t * p_simple_service, const ble_simple_service_init_t * p_simple_service_init) { uint32_t err_code; ble_uuid_t ble_uuid; // Initialize service structure p_simple_service->conn_handle = BLE_CONN_HANDLE_INVALID; p_simple_service->evt_handler = p_simple_service_init->evt_handler; // Add service UUID ble_uuid128_t base_uuid = {BLE_UUID_SIMPLE_SERVICE_BASE_UUID}; err_code = sd_ble_uuid_vs_add(&base_uuid, &p_simple_service->uuid_type); if (err_code != NRF_SUCCESS) { return err_code; } // Set up the UUID for the service (base + service-specific) ble_uuid.type = p_simple_service->uuid_type; ble_uuid.uuid = BLE_UUID_SIMPLE_SERVICE_UUID; // Set up and add the service err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_simple_service->service_handle); if (err_code != NRF_SUCCESS) { return err_code; } // Add the different characteristics in the service: // LED characteristic: B3040002-E8DF-4E40-A8EC-5EB89C80E29D // Button characteristic: B3040003-E8DF-4E40-A8EC-5EB89C80E29D err_code = led_char_add(p_simple_service, p_simple_service_init->initial_led_state); if (err_code != NRF_SUCCESS) { return err_code; } err_code = button_char_add(p_simple_service, p_simple_service_init->initial_button_state); if (err_code != NRF_SUCCESS) { return err_code; } return NRF_SUCCESS; }
The following function is the event handler that gets called by the SoftDevice whenever a relevant BLE event needs to be reported to the Simple service.
The function handles the following events:
- The Connection event.
- The Disconnection event.
- The Write event.
void ble_simple_service_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context) { if ((p_context == NULL) || (p_ble_evt == NULL)) { return; } ble_simple_service_t * p_simple_service = (ble_simple_service_t *)p_context; switch (p_ble_evt->header.evt_id) { case BLE_GAP_EVT_CONNECTED: on_connect(p_simple_service, p_ble_evt); break; case BLE_GAP_EVT_DISCONNECTED: on_disconnect(p_simple_service, p_ble_evt); break; case BLE_GATTS_EVT_WRITE: on_write(p_simple_service, p_ble_evt); break; default: // No implementation needed. break; } }
The following function handles sending a Notification with the Button state value to a Client that has subscribed to these Notifications.
/**@brief Function for sending notifications with the Button state value. * * @param[in] p_hvx_params Pointer to structure with notification data. * @param[in] conn_handle Connection handle. * * @return NRF_SUCCESS on success, otherwise an error code. */ static ret_code_t button_state_notification_send(ble_gatts_hvx_params_t * const p_hvx_params, uint16_t conn_handle) { ret_code_t err_code = sd_ble_gatts_hvx(conn_handle, p_hvx_params); if (err_code == NRF_SUCCESS) { NRF_LOG_INFO("Button state *notification* has been sent using conn_handle: 0x%04X", conn_handle); } else { NRF_LOG_DEBUG("Error: 0x%08X while sending *notification* with conn_handle: 0x%04X", err_code, conn_handle); } return err_code; }
The last function in this file handles updating the Button state characteristic value.
This gets called from the application level whenever the button gets pressed or released, but only if the Client (BLE Central) has enabled Notifications.
The two most important operations in this function are:
- Updating the database with the new value that’s passed in.
- Calling the function that handles sending Notifications and passing it the updated value.
ret_code_t ble_button_state_characteristic_update(ble_simple_service_t * p_simple_service, uint8_t data, uint16_t conn_handle) { if (p_simple_service == NULL) { NRF_LOG_INFO("SIMPLE Service is NULL"); return NRF_ERROR_NULL; } ret_code_t err_code = NRF_SUCCESS; ble_gatts_value_t gatts_value; // Initialize value struct. memset(&gatts_value, 0, sizeof(gatts_value)); gatts_value.len = sizeof(uint8_t); gatts_value.offset = 0; gatts_value.p_value = &data; // Update database. err_code = sd_ble_gatts_value_set(BLE_CONN_HANDLE_INVALID, p_simple_service->button_char_handles.value_handle, &gatts_value); if (err_code == NRF_SUCCESS) { NRF_LOG_INFO("Button state has been updated: %d", data) } else { NRF_LOG_DEBUG("Error during Button state update: 0x%08X", err_code) return err_code; } // Send value if connected and notifying. ble_gatts_hvx_params_t hvx_params; memset(&hvx_params, 0, sizeof(hvx_params)); hvx_params.handle = p_simple_service->button_char_handles.value_handle; hvx_params.type = BLE_GATT_HVX_NOTIFICATION; hvx_params.offset = gatts_value.offset; hvx_params.p_len = &gatts_value.len; hvx_params.p_data = gatts_value.p_value; if (conn_handle == BLE_CONN_HANDLE_ALL) { ble_conn_state_conn_handle_list_t conn_handles = ble_conn_state_conn_handles(); // Try sending notifications to all valid connection handles. for (uint32_t i = 0; i < conn_handles.len; i++) { if (ble_conn_state_status(conn_handles.conn_handles[i]) == BLE_CONN_STATUS_CONNECTED) { if (err_code == NRF_SUCCESS) { err_code = button_state_notification_send(&hvx_params, conn_handles.conn_handles[i]); } else { // Preserve the first non-zero error code UNUSED_RETURN_VALUE(button_state_notification_send(&hvx_params, conn_handles.conn_handles[i])); } } } } else { err_code = button_state_notification_send(&hvx_params, conn_handle); } return err_code; }
Main Application
main.c
For the main.c source code, we will focus on the implementation specific to our project and leave out the parts that are common to BLE peripheral applications. Of course, the full source code is available for download and includes all the code and project files needed to build it and flash it yourself (you can download the full source code from here).
Now let’s go over the relevant source code.
First, we make sure to include the header file for the Simple service.
// Include the simple service header file #include "simple_service.h"
We modify the Device Name that’s used in the Advertisements to make it easier to find our device.
#define DEVICE_NAME "nRF52 USB Dongle" /**< Name of device. Will be included in the advertising data. */
Next, we define the macro to be used for our external button which is connected to Pin 0.24.
The Button detection delay is needed by the App_Button module to determine when button presses occur.
// External GPIO Button Definitions #define EXTERNAL_BUTTON_1 NRF_GPIO_PIN_MAP(0,24) // Connected to P0.24 #define BUTTON_DETECTION_DELAY APP_TIMER_TICKS(50) /**< Delay from a GPIOTE event until a button is reported as pushed (in number of timer ticks). */
Next, we define the macro for the two LEDs that we are using:
- The internal LED which indicates connectivity (flashing when advertising/not-connected and solid when connected). This is the Green LED onboard the USB dongle.
- The external LED which is controlled via the LED characteristic as part of our custom Simple service. This LED is connected to Pin 1.10.
// LED Definitions #define CONNECTIVITY_LED BSP_BOARD_LED_0 #define MAIN_LED NRF_GPIO_PIN_MAP(1,10) // Connected to P1.10
The timer defined below is needed for flashing our internal LED (aka Connectivity LED).
Every time the timer gets triggered, we switch the state of the LED (on –> off and off –> on) causing the flashing effect.
// Timer Values & Definitions #define SLOW_BLINK_INTERVAL APP_TIMER_TICKS(500) // Timer for Slow flashing LED (turn On/Off LED) APP_TIMER_DEF(m_connectivity_led_flashing_timer_id); // Timer for flashing LED in Disconnected state
This call instantiates the Simple service object.
// Define the Simple Service BLE_SIMPLE_SERVICE_DEF(m_simple_service);
We use a variable to hold the state of the Notifications (whether enabled or disabled). This helps us avoid calls to send Notifications when they are not enabled (subscribed to) by the Client.
bool m_button_notification_enabled = false;
The following two functions handle:
- Initializing the Timer module and creating a timer to handle the Connectivity LED flashing operation.
- The Connectivity LED timeout handler (to invert the LED state).
static void connectivity_timeout_handler(void * p_context) { UNUSED_PARAMETER(p_context); bsp_board_led_invert(CONNECTIVITY_LED); } /**@brief Function for the Timer initialization. * * @details Initializes the timer module. This creates application timers. */ static void timers_init(void) { ret_code_t err_code = app_timer_init(); APP_ERROR_CHECK(err_code); err_code = app_timer_create(&m_connectivity_led_flashing_timer_id, APP_TIMER_MODE_REPEATED, connectivity_timeout_handler); APP_ERROR_CHECK(err_code); }
This is the event handler function that gets passed during the initialization of the Simple service.
The four events handled are:
- Button characteristic value Notification enabled. In this case, we set the m_button_notification_enabled variable.
- Button characteristic value Notification disabled. In this case, we reset the m_button_notification_enabled variable.
- LED characteristic value written to turn On LED. In this case, we reset the GPIO assigned to the LED (LED active state is 0).
- LED characteristic value written to turn Off LED. In this case, we set the GPIO assigned to the LED (LED active state is 0).
static void on_simple_service_evt(ble_simple_service_t * p_simple_service, ble_simple_service_evt_t * p_evt) { ret_code_t err_code; switch (p_evt->evt_type) { case BLE_BUTTON_EVT_NOTIFICATION_ENABLED: m_button_notification_enabled = true; NRF_LOG_INFO("Button notification enabled"); break; case BLE_BUTTON_EVT_NOTIFICATION_DISABLED: m_button_notification_enabled = false; NRF_LOG_INFO("Button notification disabled"); break; case BLE_LED_TURN_ON_EVT: // LED state has been requested to be changed (Response/Ack will be sent automatically by the SoftDevice) // Turn on LED (ACTIVE STATE is 0) nrf_gpio_pin_write(MAIN_LED, 0); break; case BLE_LED_TURN_OFF_EVT: // LED state has been requested to be changed (Response/Ack will be sent automatically by the SoftDevice) // Turn on LED (ACTIVE STATE is 0) nrf_gpio_pin_write(MAIN_LED, 1); break; } }
The following function is used to initialize the services in our application. We only have one service, the Simple service, so that’s the only one that gets initialized.
As part of the initialization, the event handler is assigned and the initial values for both the Button and LED states are set.
/**@brief Function for initializing services that will be used by the application. */ static void services_init(void) { ret_code_t err_code; nrf_ble_qwr_init_t qwr_init = {0}; // Initialize Queued Write Module. qwr_init.error_handler = nrf_qwr_error_handler; err_code = nrf_ble_qwr_init(&m_qwr, &qwr_init); APP_ERROR_CHECK(err_code); ble_simple_service_init_t simple_service_init; simple_service_init.evt_handler = on_simple_service_evt; simple_service_init.initial_led_state = 0; simple_service_init.initial_button_state = 0; err_code = ble_simple_service_init(&m_simple_service, &simple_service_init); APP_ERROR_CHECK(err_code); }
In the Advertising event handler function, we toggle the state of the Connectivity LED to start flashing when Advertising starts (by starting the corresponding timer).
/**@brief Function for handling advertising events. * * @details This function will be called for advertising events which are passed to the application. * * @param[in] ble_adv_evt Advertising event. */ static void on_adv_evt(ble_adv_evt_t ble_adv_evt) { ret_code_t err_code; switch (ble_adv_evt) { case BLE_ADV_EVT_FAST: NRF_LOG_INFO("Fast advertising."); // Set LED state err_code = app_timer_start(m_connectivity_led_flashing_timer_id, SLOW_BLINK_INTERVAL, NULL); APP_ERROR_CHECK(err_code); break; case BLE_ADV_EVT_IDLE: sleep_mode_enter(); break; default: break; } }
In the main BLE event handler function, we handle a few events. Of importance are the Disconnected and Connected events.
- Disconnected event: we clear the m_button_notification_enabled variable.
- Connected event: We stop the flashing timer, turn on the Connectivity LED, and clear the m_button_notification_enabled variable.
/**@brief Function for handling BLE events. * * @param[in] p_ble_evt Bluetooth stack event. * @param[in] p_context Unused. */ static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context) { ret_code_t err_code = NRF_SUCCESS; ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt; switch (p_ble_evt->header.evt_id) { case BLE_GAP_EVT_DISCONNECTED: NRF_LOG_INFO("Disconnected with reason %d.", p_ble_evt->evt.gap_evt.params.disconnected.reason); // LED indication will be changed when advertising starts. m_button_notification_enabled = false; break; case BLE_GAP_EVT_CONNECTED: NRF_LOG_INFO("Connected."); // Stop advertising LED timer, turn on "connected state" LED err_code = app_timer_stop(m_connectivity_led_flashing_timer_id); APP_ERROR_CHECK(err_code); bsp_board_led_on(CONNECTIVITY_LED); m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle; err_code = nrf_ble_qwr_conn_handle_assign(&m_qwr, m_conn_handle); APP_ERROR_CHECK(err_code); m_button_notification_enabled = false; break;
Here we have the function responsible for initializing the LEDs.
We use the BSP module for the onboard LEDs and configure our external LED via the nrf_gpio APIs.
We also clear the external LED (turn it off) by setting the GPIO pin to 1 (since the LED is active state 0).
/**@brief Function for initializing buttons and LEDs. */ static void leds_init(void) { ret_code_t err_code; bsp_board_init(BSP_INIT_LEDS); nrf_gpio_cfg_output(MAIN_LED); nrf_gpio_pin_write(MAIN_LED, 1); }
We also define a function for handling the Button events.
In the case where the event is related to the pin assigned to our external button, we check for the following:
- If the connection handle is valid, the button was pressed/pushed, and Notifications are enabled, then we update the button characteristic value with a value of 0x01.
- If the connection handle is valid, the button was released, and Notifications are enabled, then we update the button characteristic value with a value of 0x00.
/**@brief Function for handling events from the button handler module. * * @param[in] pin_no The pin that the event applies to. * @param[in] button_action The button action (press/release). */ static void button_event_handler(uint8_t pin_no, uint8_t button_action) { ret_code_t err_code; switch (pin_no) { case EXTERNAL_BUTTON_1: if (m_conn_handle != BLE_CONN_HANDLE_INVALID && button_action == APP_BUTTON_PUSH && m_button_notification_enabled) { // Button pushed // Update characteristic value (to 1) err_code = ble_button_state_characteristic_update(&m_simple_service, 0x01, m_conn_handle); APP_ERROR_CHECK(err_code); } else if (m_conn_handle != BLE_CONN_HANDLE_INVALID && button_action == APP_BUTTON_RELEASE && m_button_notification_enabled) { // Button released // Update characteristic value (to 0) err_code = ble_button_state_characteristic_update(&m_simple_service, 0x00, m_conn_handle); APP_ERROR_CHECK(err_code); } break; default: APP_ERROR_HANDLER(pin_no); break; } }
The last function we want to look at is the function for initializing the buttons.
In this function, we create an array to hold the buttons we’re interested in initializing. In our case, we are only interested in initializing our external button.
In the initialization, we also pass in the delay that we defined at the top of the source file.
Finally, we call the app_button_enable() API to enable the buttons.
/**@brief Function for initializing buttons. */ static void buttons_init() { ret_code_t err_code; //The array must be static because a pointer to it will be saved in the button handler module. static app_button_cfg_t buttons[] = { {EXTERNAL_BUTTON_1, false, BUTTON_PULL, button_event_handler} }; err_code = app_button_init(buttons, ARRAY_SIZE(buttons), BUTTON_DETECTION_DELAY); APP_ERROR_CHECK(err_code); err_code = app_button_enable(); APP_ERROR_CHECK(err_code); }
Building and Flashing the Firmware to the nRF52840 USB Dongle
We covered the complete steps to build and flash the firmware to the nRF52840 USB Dongle.
You can find the complete steps here: Building and Flashing the Firmware to the Dongle.
Testing the Application
Finally, we want to test the functionality of our application. We do so in the following video:
Summary
This concludes our implementation for a simple project using the Nordic Semiconductor nRF52840 USB Dongle.
In this series of tutorials, we went over a few important aspects including:
- Hardware preparation and connecting components
- Software requirements and installation
- How to build the firmware and flash it to the nRF52840 USB Dongle
- Designing our own custom GATT including one service and two characteristics
- Implementing the GATT service and its characteristics
- Implementing the code to handle the external button presses and releases
- Implementing the code to turn on/off the external LED
- Testing of the functionality using a mobile app such as nRF Connect
If you found this tutorial useful then please share with others and let me know your thoughts in the comment section below.
…and don’t forget to enter your email below to get access to the full source code and be notified when the next tutorial goes live!