BLE-Smart-LED-application-nRF52

How to build the simplest nRF52 BLE Peripheral application (Lightbulb use case)

Let’s face it…

One of the hardest things when working with BLE is simply getting started.

Whether it’s the setup of the IDE, the configuration of the project, or the implementation the BLE application.

I’ve been there… I’ve felt lost, not knowing where and how to start… This is especially true since I was trying to learn the technology itself (BLE) in addition to learning an SDK, platform, and IDE all at once! It just felt overwhelming and way too many things to learn at one time.

Lately, I’ve been focusing on one platform/chipset: Nordic’s nRF52. This is due to one main reason: I’ve found it to be the most developer-friendly platform out there. (It also helps that you get a FREE commercial license for a professional IDE: Segger Embedded Studio (SES)!)

There’s nothing wrong with the other platforms and chipsets, but it also helps if you stick to one platform that you feel comfortable with (at least for a given period of time, especially in the beginning of your journey in learning a technology).

Ok, enough with the rant, and let’s get into what this post is all about:

To guide you through setting up and developing the most basic BLE peripheral application: a smart BLE lightbulb application you can control from your smartphone.

In the previous blog post (The complete cross-platform nRF development tutorial), we went over how to set up the IDE of choice for developing nRF52 applications (Segger Embedded Studio) on any platform (macOS, Windows, Linux). In this post, we’ll focus on developing the BLE peripheral application, building it, debugging it, and finally testing it from a mobile phone application.

The simplest BLE Peripheral application

We’re going to build a very simple BLE lightbulb application that allows you to turn ON/OFF an LED on the nRF52840 development kit. The Peripheral application will also expose the battery level of a coin-cell battery installed in the development kit. The peripheral will notify the Central (mobile phone application in our case) when the battery level gets updated.

Keeping the example dead-simple is extremely important. You can always customize and expand your application once you’ve learned the basics.

But if you start with a complex application, you can get lost, and ultimately become frustrated before you get something working.

…which is why we’re keeping it simple in this application!

Prerequisites

To follow along with this example, you’ll need to know the basics of BLE. It does not require you to have in-depth knowledge of BLE. In fact, I recommend you do not spend too much time going through the theory and skip right into developing a BLE application and getting your hands dirty once you’ve gone through learning the basics.

If you’re looking to learn the basics, or simply need a refresher, I recommend checking out my FREE 7-day crash course on the Basics of Bluetooth Low Energy (BLE). Sign up by filling out the form in the bottom right-hand corner of the webpage:

Alternatively, you can sign up at the following link: FREE 7-day crash course on Bluetooth Low Energy (BLE).

So, what hardware & software do I need?

For this tutorial, you’ll need the following:

  • nRF52840 development kit (although any nRF5x development will work, with some modifications)
  • A PC running either Windows, Linux, or macOS (for development)
  • Segger Embedded Studio
  • nRF5 SDK (in our case, we’re using the latest SDK provided by Nordic: nRF5 SDK version 15.0.0)
  • A mobile phone
  • BLE client application running on the mobile phone (such as Nordic’s nRF Connect or LightBlue)

Getting Started

Installing the SDK

We’ve already covered how to install Segger Embedded Studio (the FREE License IDE used for nRF5x development) in a previous post. Here, we’ll focus on the steps that follow the installation.

Next, let’s download the latest nRF5 SDK (version 15.0.0) from Nordic’s website.

Once you have it downloaded, place it in a new folder. To make things easier, we’ll put it in a folder alongside the application we’ll be developing.

In my setup, I’ll be creating a new folder named “BLE Projects” which contains the SDK folder as well as another folder for the application.

Folder Structure Setup

Create a new folder to hold the SDK as well as the application (I named it BLE Projects):

  • Copy the SDK folder to the “BLE Projects” folder
  • [Optional] Rename the SDK folder to “nRF5_SDK_Current”. This way, if I need to migrate to a newer SDK, all I have to do is place it in that folder, and my current projects will not have to be updated (other than any changes needed for the APIs to work with the newer SDK)
  • Navigate to the folder examples/ble_peripheral/ and copy the folder ble_app_template to the BLE Projects folder
  • Copying the example application folder outside of the SDK makes it easier to keep it self-contained and independent of any changes you make to the SDK folder (such as installing a newer SDK)
  • Rename ble_app_template to another meaningful name that describes our simple application: ble_lightbulb

Now, it’s time for a little clean-up!

The example application folder contains a few unnecessary files and folders (related to other IDEs). I also find that it contains too many nested folders that make it harder to find important files. We’ll do the following:

  • Move the sdk_config.h file to the root folder of the application (located under pca10056/s140/config)
  • Move the SES Solution file (ble_app_template_pca10056_s140.emProject) along with the flash_placement.xml file to the root application folder (both of these files are located under pca10056/s140/ses).
  • You can also move the *.jlink and *.emSession files, but those are not necessary as SES will re-create them once you open the Solution.
  • Delete the ble_app_template.eww file
  • Delete the pca* folders (don’t forget to move the files listed above before you do this!)

Here is what the resulting folder looks like for me:

SES Project File Configuration

Now that we’ve got the folder structure set up, we need to update the SES project file to reflect these changes (this is something we’ll only have to do once).

First, rename the file “ble_app_template_pca10056_s140.emProject” to “”ble_lightbulb_pca10056_s140.emProject“.

Next, open the file in your text editor of choice.

Here are the modifications we’re going to make to the project file (I’ve already made all the changes and provided the full source code available for you to download if you want to skip these steps, at the end of this post):

  • Rename the Solution and Project names:
    <solution Name="ble_app_template_pca10056_s140" target="8" version="2">
      <project Name="ble_app_template_pca10056_s140">
    

    to:

    <solution Name="ble_lightbulb_pca10056_s140" target="8" version="2">
      <project Name="ble_lightbulb">
    
  • Replace any references to the SDK (since we copied the application folder outside the SDK).
    Replace “../../../../..” with “nRF5_SDK_current”
    E.g.:

          <file file_name="../../../../../../components/libraries/experimental_log/src/nrf_log_backend_rtt.c" />
          <file file_name="../../../../../../components/libraries/experimental_log/src/nrf_log_backend_serial.c" />
          <file file_name="../../../../../../components/libraries/experimental_log/src/nrf_log_backend_uart.c" />
          <file file_name="../../../../../../components/libraries/experimental_log/src/nrf_log_default_backends.c" />
          <file file_name="../../../../../../components/libraries/experimental_log/src/nrf_log_frontend.c" />
          <file file_name="../../../../../../components/libraries/experimental_log/src/nrf_log_str_formatter.c" />
    

    replaced with:

          <file file_name="../nRF5_SDK_current/components/libraries/experimental_log/src/nrf_log_backend_rtt.c" />
          <file file_name="../nRF5_SDK_current/components/libraries/experimental_log/src/nrf_log_backend_serial.c" />
          <file file_name="../nRF5_SDK_current/components/libraries/experimental_log/src/nrf_log_backend_uart.c" />
          <file file_name="../nRF5_SDK_current/components/libraries/experimental_log/src/nrf_log_default_backends.c" />
          <file file_name="../nRF5_SDK_current/components/libraries/experimental_log/src/nrf_log_frontend.c" />
          <file file_name="../nRF5_SDK_current/components/libraries/experimental_log/src/nrf_log_str_formatter.c" />

Next, open the Solution (*.emProject file) in SES.

Now, make the following changes to the Project and the source files:

  • In main.c, change the advertised name:
    #define DEVICE_NAME                     "BLE_Lightbulb"                       /**< Name of device. Will be included in the advertising data. */
  • Also in main.c, add the following lines (highlighted):
    #include "ble_bas.h"
    #include "services/led_service.h"
    
    #include "Battery Level/battery_voltage.h"
    
    
    #define DEVICE_NAME                     "BLE_Lightbulb"                       /**< Name of device. Will be included in the advertising data. */
    #define MANUFACTURER_NAME               "NordicSemiconductor"                   /**< Manufacturer. Will be passed to Device Information Service. */
    #define APP_ADV_INTERVAL                300                              /**< The advertising interval (in units of 0.625 ms. This value corresponds to 187.5 ms). */
    
    // Corresponds to LED2 on the development kit
    #define LIGHTBULB_LED                   BSP_BOARD_LED_1                         /**< LED to be toggled with the help of the LED Button Service. */
    
    #define APP_ADV_DURATION                18000                                   /**< The advertising duration (180 seconds) in units of 10 milliseconds. */
  • Also, make sure you add the code to instantiate the LED Service object. This is done by calling a macro as following (macro will be defined later in led_service.h):
    BLE_LED_SERVICE_DEF(m_led_service);
  • Remove any peer manager references (only needed for security features):
    • Change
      static void advertising_init(bool erase_bonds);

      to:

      static void advertising_init();
    • Remove the following block of code:
      /**@brief Function for handling Peer Manager events.
       *
       * @param[in] p_evt  Peer Manager event.
       */
      static void pm_evt_handler(pm_evt_t const * p_evt)
      {
          ret_code_t err_code;
      
          switch (p_evt->evt_id)
          {
              case PM_EVT_BONDED_PEER_CONNECTED:
              {
                  NRF_LOG_INFO("Connected to a previously bonded device.");
              } break;
      
              case PM_EVT_CONN_SEC_SUCCEEDED:
              {
                  NRF_LOG_INFO("Connection secured: role: %d, conn_handle: 0x%x, procedure: %d.",
                               ble_conn_state_role(p_evt->conn_handle),
                               p_evt->conn_handle,
                               p_evt->params.conn_sec_succeeded.procedure);
              } break;
      
              case PM_EVT_CONN_SEC_FAILED:
              {
                  /* Often, when securing fails, it shouldn't be restarted, for security reasons.
                   * Other times, it can be restarted directly.
                   * Sometimes it can be restarted, but only after changing some Security Parameters.
                   * Sometimes, it cannot be restarted until the link is disconnected and reconnected.
                   * Sometimes it is impossible, to secure the link, or the peer device does not support it.
                   * How to handle this error is highly application dependent. */
              } break;
      
              case PM_EVT_CONN_SEC_CONFIG_REQ:
              {
                  // Reject pairing request from an already bonded peer.
                  pm_conn_sec_config_t conn_sec_config = {.allow_repairing = false};
                  pm_conn_sec_config_reply(p_evt->conn_handle, &conn_sec_config);
              } break;
      
              case PM_EVT_STORAGE_FULL:
              {
                  // Run garbage collection on the flash.
                  err_code = fds_gc();
                  if (err_code == FDS_ERR_NO_SPACE_IN_QUEUES)
                  {
                      // Retry.
                  }
                  else
                  {
                      APP_ERROR_CHECK(err_code);
                  }
              } break;
      
              case PM_EVT_PEERS_DELETE_SUCCEEDED:
              {
                  advertising_start(false);
              } break;
      
              case PM_EVT_PEER_DATA_UPDATE_FAILED:
              {
                  // Assert.
                  APP_ERROR_CHECK(p_evt->params.peer_data_update_failed.error);
              } break;
      
              case PM_EVT_PEER_DELETE_FAILED:
              {
                  // Assert.
                  APP_ERROR_CHECK(p_evt->params.peer_delete_failed.error);
              } break;
      
              case PM_EVT_PEERS_DELETE_FAILED:
              {
                  // Assert.
                  APP_ERROR_CHECK(p_evt->params.peers_delete_failed_evt.error);
              } break;
      
              case PM_EVT_ERROR_UNEXPECTED:
              {
                  // Assert.
                  APP_ERROR_CHECK(p_evt->params.error_unexpected.error);
              } break;
      
              case PM_EVT_CONN_SEC_START:
              case PM_EVT_PEER_DATA_UPDATE_SUCCEEDED:
              case PM_EVT_PEER_DELETE_SUCCEEDED:
              case PM_EVT_LOCAL_DB_CACHE_APPLIED:
              case PM_EVT_LOCAL_DB_CACHE_APPLY_FAILED:
                  // This can happen when the local DB has changed.
              case PM_EVT_SERVICE_CHANGED_IND_SENT:
              case PM_EVT_SERVICE_CHANGED_IND_CONFIRMED:
              default:
                  break;
          }
      }
    • Delete the following block:
      /**@brief Function for the Peer Manager initialization.
       */
      static void peer_manager_init(void)
      {
          ble_gap_sec_params_t sec_param;
          ret_code_t           err_code;
      
          err_code = pm_init();
          APP_ERROR_CHECK(err_code);
      
          memset(&sec_param, 0, sizeof(ble_gap_sec_params_t));
      
          // Security parameters to be used for all security procedures.
          sec_param.bond           = SEC_PARAM_BOND;
          sec_param.mitm           = SEC_PARAM_MITM;
          sec_param.lesc           = SEC_PARAM_LESC;
          sec_param.keypress       = SEC_PARAM_KEYPRESS;
          sec_param.io_caps        = SEC_PARAM_IO_CAPABILITIES;
          sec_param.oob            = SEC_PARAM_OOB;
          sec_param.min_key_size   = SEC_PARAM_MIN_KEY_SIZE;
          sec_param.max_key_size   = SEC_PARAM_MAX_KEY_SIZE;
          sec_param.kdist_own.enc  = 1;
          sec_param.kdist_own.id   = 1;
          sec_param.kdist_peer.enc = 1;
          sec_param.kdist_peer.id  = 1;
      
          err_code = pm_sec_params_set(&sec_param);
          APP_ERROR_CHECK(err_code);
      
          err_code = pm_register(pm_evt_handler);
          APP_ERROR_CHECK(err_code);
      }
      
      
      /**@brief Clear bond information from persistent storage.
       */
      static void delete_bonds(void)
      {
          ret_code_t err_code;
      
          NRF_LOG_INFO("Erase bonds!");
      
          err_code = pm_peers_delete();
          APP_ERROR_CHECK(err_code);
      }
    • Change
      /**@brief Function for starting advertising.
       */
      static void advertising_start(bool erase_bonds)
      {
          if (erase_bonds == true)
          {
              delete_bonds();
              // Advertising is started by PM_EVT_PEERS_DELETED_SUCEEDED event
          }
          else
          {
              ret_code_t err_code = ble_advertising_start(&m_advertising, BLE_ADV_MODE_FAST);
      
              APP_ERROR_CHECK(err_code);
          }
      }

      to

      /**@brief Function for starting advertising.
       */
      static void advertising_start()
      {
          ret_code_t err_code = ble_advertising_start(&m_advertising, BLE_ADV_MODE_FAST);
          APP_ERROR_CHECK(err_code);
      }
      
    • Remove the highlighted line:
          services_init();
          conn_params_init();
          peer_manager_init();
    • Change
          advertising_start(erase_bonds);

      to

          advertising_init();
    • Modify the following (we are not using any buttons in our application, so we only need to initialize the LEDs):
      /**@brief Function for initializing buttons and leds.
       *
       * @param[out] p_erase_bonds  Will be true if the clear bonding button was pressed to wake the application up.
       */
      static void buttons_leds_init(bool * p_erase_bonds)
      {
          ret_code_t err_code;
          bsp_event_t startup_event;
      
          err_code = bsp_init(BSP_INIT_LEDS | BSP_INIT_BUTTONS, bsp_event_handler);
          APP_ERROR_CHECK(err_code);
      
          err_code = bsp_btn_ble_init(NULL, &startup_event);
          APP_ERROR_CHECK(err_code);
      
          *p_erase_bonds = (startup_event == BSP_EVENT_CLEAR_BONDING_DATA);
      }
      

      to

       *
       * @details Initializes all LEDs used by the application.
       */
      static void leds_init()
      {
          ret_code_t err_code;
          
          err_code = bsp_init(BSP_INIT_LEDS, bsp_event_handler);
          APP_ERROR_CHECK(err_code);
      }
      
    • In main(), modify
      buttons_leds_init(&erase_bonds);

      to

      leds_init();
  • Change the debug print out for the main application:
    NRF_LOG_INFO("Template example started.");

    to

    NRF_LOG_INFO("BLE Lightbulb example started.");
  • We need to add a write handler for the LED settings in main.c. This function will get used for the custom service and characteristic we’ll be implementing in our application.
    /**@brief Function for handling write events to the LED characteristic.
     *
     * @param[in] p_led_service  Instance of LED Service to which the write applies.
     * @param[in] led_state      Written/desired state of the LED.
     */
    static void led_write_handler(uint16_t conn_handle, ble_led_service_t * p_led_service, uint8_t led_state)
    {
        if (led_state)
        {
            bsp_board_led_on(LIGHTBULB_LED);
            NRF_LOG_INFO("Received LED ON!");
        }
        else
        {
            bsp_board_led_off(LIGHTBULB_LED);
            NRF_LOG_INFO("Received LED OFF!");
        }
    }

BLE Lightbulb Application Design

Let’s talk about the design of our application, and what elements we need to add to implement our smart BLE-controlled lightbulb.

The application allows the user to:

  • Discover the BLE Peripheral named “BLE_Lightbulb” from a BLE Central application
  • Connect to the Peripheral and discover the Services and Characteristics exposed by the Peripheral
  • Turn ON/OFF LED2 on the nRF52840 development kit as well as read the status of the LED (whether ON or OFF)
  • Subscribe to Notifications to the Battery Level Characteristic exposed by the Peripheral inside the Battery Service

LED Service + LED 2 Characteristic

To be able to control the LED on the development kit from a BLE client (a smartphone application in our case), we’ll need to implement a custom service and characteristic for the LED.

iIn a previous post (Custom Services and Characteristics [BLE MIDI use case]), we described how to create custom services and characteristics, but here’s all you need to know:

  • We need to create one service. We’ll call it led_service.
  • In that service, we’ll add a single characteristic dedicated to LED 2 on our development kit (LED 1 will be used to indicate the state of the BLE peripheral: Advertising or Connected). Let’s call this characteristic: led_2_char.
  • For each (service and characteristic), we’ll need to choose a custom UUID (to learn more about custom/vendor-specific UUIDs and how to pick them, refer to this blog post (Creating custom UUIDs).
  • We’ll define the UUIDs as following:
    • LED Service UUID: E54B0001-67F5-479E-8711-B3B99198CE6C
    • LED 2 Characteristic UUID: E54B0002-67F5-479E-8711-B3B99198CE6C
  • The LED 2 Characteristic will have the following permissions:
    • Write-enabled
    • Read-enabled
    • NO Notifications or Indications enabled (since we’ll be controlling the LED from the BLE client)

Implementation

Instead of including the implementation of the LED service in the main.c file, we’ll be including them in a separate folder and files.

Let’s create two files: led_service.h & led_service.c.

We’ll be placing these in a separate folder named Services.

  • Create a folder named Services in the root folder of the application (from your Operating System’s file explorer)
  • Right-click on Project ‘ble_lightbulb’
  • Click New Folder
    Create new project folder SES
  • Rename New Folder to Services
  • Now you should have an empty Services folder
  • Right-click on Services and choose to Add New File
  • Choose C File (.c), and type led_service.c in the Name field
  • Under Location, click Browse and navigate to the Services folder
  • Select Services and click Choose

  • Click OK
  • Repeat the steps above but choose Header File (.h) instead and enter led_service.h in the Name field

Here are the contents of the source files. We’ll list the full source code first and then go through the content explaining it.

led_service.h

/*
 * The MIT License (MIT)
 * Copyright (c) 2018 Novel Bits
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 */

#ifndef LED_SERVICE_H
#define LED_SERVICE_H

#include <stdint.h>
#include "boards.h"
#include "ble.h"
#include "ble_srv_common.h"
#include "nrf_sdh_ble.h"

/**@brief   Macro for defining a ble_led_service instance.
 *
 * @param   _name   Name of the instance.
 * @hideinitializer
 */

#define BLE_LED_SERVICE_BLE_OBSERVER_PRIO 2

#define BLE_LED_SERVICE_DEF(_name)                                                                          \
static ble_led_service_t _name;                                                                             \
NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                         \
                     BLE_LED_SERVICE_BLE_OBSERVER_PRIO,                                                     \
                     ble_led_service_on_ble_evt, &_name)

// LED service:                     E54B0001-67F5-479E-8711-B3B99198CE6C
//   LED 2 characteristic:   E54B0002-67F5-479E-8711-B3B99198CE6C

// 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: E54B0000-67F5-479E-8711-B3B99198CE6C
#define BLE_UUID_LED_SERVICE_BASE_UUID  {0x6C, 0xCE, 0x98, 0x91, 0xB9, 0xB3, 0x11, 0x87, 0x9E, 0x47, 0xF5, 0x67, 0x00, 0x00, 0x4B, 0xE5}

// Service & characteristics UUIDs
#define BLE_UUID_LED_SERVICE_UUID  0x0001
#define BLE_UUID_LED_2_CHAR_UUID   0x0002

// Forward declaration of the custom_service_t type.
typedef struct ble_led_service_s ble_led_service_t;

typedef void (*ble_led_service_led_write_handler_t) (uint16_t conn_handle, ble_led_service_t * p_led_service, uint8_t new_state);

/** @brief LED Service init structure. This structure contains all options and data needed for
 *        initialization of the service.*/
typedef struct
{
    ble_led_service_led_write_handler_t led_write_handler; /**< Event handler to be called when the LED Characteristic is written. */
} ble_led_service_init_t;

/**@brief LED Service structure.
 *        This contains various status information
 *        for the service.
 */
typedef struct ble_led_service_s
{
    uint16_t                            conn_handle;
    uint16_t                            service_handle;
    uint8_t                             uuid_type;
    ble_gatts_char_handles_t            led_2_char_handles;
    ble_led_service_led_write_handler_t led_write_handler;

} ble_led_service_t;

// Function Declarations

/**@brief Function for initializing the LED Service.
 *
 * @param[out]  p_led_service  LED Service structure. This structure will have to be supplied by
 *                                the application. It will be initialized by this function, and will later
 *                                be used to identify this particular service instance.
 *
 * @return      NRF_SUCCESS on successful initialization of service, otherwise an error code.
 */
uint32_t ble_led_service_init(ble_led_service_t * p_led_service, const ble_led_service_init_t * p_led_service_init);

/**@brief Function for handling the application's BLE stack events.
 *
 * @details This function handles all events from the BLE stack that are of interest to the LED Service.
 *
 * @param[in] p_ble_evt  Event received from the BLE stack.
 * @param[in] p_context  LED Service structure.
 */
void ble_led_service_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context);

#endif /* LED_SERVICE_H */

led_service.c

/*
 * The MIT License (MIT)
 * Copyright (c) 2018 Novel Bits
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 */

#include <string.h>

#include "nrf_log.h"
#include "led_service.h"

static const uint8_t LED2CharName[] = "LED 2";

/**@brief Function for handling the Connect event.
 *
 * @param[in]   p_led_service  LED service structure.
 * @param[in]   p_ble_evt      Event received from the BLE stack.
 */
static void on_connect(ble_led_service_t * p_led_service, ble_evt_t const * p_ble_evt)
{
    p_led_service->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
}

/**@brief Function for handling the Disconnect event.
 *
 * @param[in]   p_bas       LED service structure.
 * @param[in]   p_ble_evt   Event received from the BLE stack.
 */
static void on_disconnect(ble_led_service_t * p_led_service, ble_evt_t const * p_ble_evt)
{
    UNUSED_PARAMETER(p_ble_evt);
    p_led_service->conn_handle = BLE_CONN_HANDLE_INVALID;
}

/**@brief Function for handling the Write event.
 *
 * @param[in] p_led_service   LED Service structure.
 * @param[in] p_ble_evt       Event received from the BLE stack.
 */
static void on_write(ble_led_service_t * p_led_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;

    if (   (p_evt_write->handle == p_led_service->led_2_char_handles.value_handle)
        && (p_evt_write->len == 1)
        && (p_led_service->led_write_handler != NULL))
    {
        p_led_service->led_write_handler(p_ble_evt->evt.gap_evt.conn_handle, p_led_service, p_evt_write->data[0]);
    }
}

/**@brief Function for adding the LED 2 characteristic.
 *
 */
static uint32_t led_2_char_add(ble_led_service_t * p_led_service)
{
    ble_gatts_char_md_t char_md;
    ble_gatts_attr_t    attr_char_value;
    ble_gatts_attr_md_t attr_md;
    ble_uuid_t          ble_uuid;

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

    char_md.char_props.read          = 1;
    char_md.char_props.write         = 1;
    char_md.p_char_user_desc         = LED2CharName;
    char_md.char_user_desc_size      = sizeof(LED2CharName);
    char_md.char_user_desc_max_size  = sizeof(LED2CharName);
    char_md.p_char_pf                = NULL;
    char_md.p_user_desc_md           = NULL;
    char_md.p_cccd_md                = NULL;
    char_md.p_sccd_md                = NULL;

    // Define the LED 2 Characteristic UUID
    ble_uuid.type = p_led_service->uuid_type;
    ble_uuid.uuid = BLE_UUID_LED_2_CHAR_UUID;

    // 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);

    // 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      = NULL;

    return sd_ble_gatts_characteristic_add(p_led_service->service_handle, &char_md,
                                           &attr_char_value,
                                           &p_led_service->led_2_char_handles);
}

uint32_t ble_led_service_init(ble_led_service_t * p_led_service, const ble_led_service_init_t * p_led_service_init)
{
    uint32_t   err_code;
    ble_uuid_t ble_uuid;

    // Initialize service structure
    p_led_service->conn_handle = BLE_CONN_HANDLE_INVALID;

    // Initialize service structure.
    p_led_service->led_write_handler = p_led_service_init->led_write_handler;

    // Add service UUID
    ble_uuid128_t base_uuid = {BLE_UUID_LED_SERVICE_BASE_UUID};
    err_code = sd_ble_uuid_vs_add(&base_uuid, &p_led_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_led_service->uuid_type;
    ble_uuid.uuid = BLE_UUID_LED_SERVICE_UUID;

    // Set up and add the service
    err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_led_service->service_handle);
    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    // Add the different characteristics in the service:
    //   Button press characteristic:   E54B0002-67F5-479E-8711-B3B99198CE6C
    err_code = led_2_char_add(p_led_service);
    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    return NRF_SUCCESS;
}

void ble_led_service_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
{
    ble_led_service_t * p_led_service = (ble_led_service_t *)p_context;

    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GAP_EVT_CONNECTED:
            on_connect(p_led_service, p_ble_evt);
            break;

        case BLE_GATTS_EVT_WRITE:
            on_write(p_led_service, p_ble_evt);
            break;

        case BLE_GAP_EVT_DISCONNECTED:
            on_disconnect(p_led_service, p_ble_evt);
            break;

        default:
            // No implementation needed.
            break;
    }
}

Let’s explain the most important elements of the implementation:

led_service.h

  • Lines 33-39: define a macro that will be used in main.c to instantiate the LED service data structure
    #define BLE_LED_SERVICE_BLE_OBSERVER_PRIO 2
    
    #define BLE_LED_SERVICE_DEF(_name)                                                                          \
    static ble_led_service_t _name;                                                                             \
    NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                         \
                         BLE_LED_SERVICE_BLE_OBSERVER_PRIO,                                                     \
                         ble_led_service_on_ble_evt, &_name)
    
  • Lines 41-53: define the UUIDs for the LED Service and the LED 2 Characteristic
    // LED service:                     E54B0001-67F5-479E-8711-B3B99198CE6C
    //   LED 2 characteristic:   E54B0002-67F5-479E-8711-B3B99198CE6C
    
    // 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: E54B0000-67F5-479E-8711-B3B99198CE6C
    #define BLE_UUID_LED_SERVICE_BASE_UUID  {0x6C, 0xCE, 0x98, 0x91, 0xB9, 0xB3, 0x11, 0x87, 0x9E, 0x47, 0xF5, 0x67, 0x00, 0x00, 0x4B, 0xE5}
    
    // Service & characteristics UUIDs
    #define BLE_UUID_LED_SERVICE_UUID  0x0001
    #define BLE_UUID_LED_2_CHAR_UUID   0x0002
  •  Lines 55-65: define the led write handler function prototype and the LED service initialization data structure
    // Forward declaration of the custom_service_t type.
    typedef struct ble_led_service_s ble_led_service_t;
    
    typedef void (*ble_led_service_led_write_handler_t) (uint16_t conn_handle, ble_led_service_t * p_led_service, uint8_t new_state);
    
    /** @brief LED Service init structure. This structure contains all options and data needed for
     *        initialization of the service.*/
    typedef struct
    {
        ble_led_service_led_write_handler_t led_write_handler; /**< Event handler to be called when the LED Characteristic is written. */
    } ble_led_service_init_t;
  • Lines 67-79: define the main LED service data structure that stores all the information relevant to the service
    /**@brief LED Service structure.
     *        This contains various status information
     *        for the service.
     */
    typedef struct ble_led_service_s
    {
        uint16_t                            conn_handle;
        uint16_t                            service_handle;
        uint8_t                             uuid_type;
        ble_gatts_char_handles_t            led_2_char_handles;
        ble_led_service_led_write_handler_t led_write_handler;
    
    } ble_led_service_t;
    
  • Lines 81-100: declare the functions needed for initializing the service as well as the event handler
    // Function Declarations
    
    /**@brief Function for initializing the LED Service.
     *
     * @param[out]  p_led_service  LED Service structure. This structure will have to be supplied by
     *                                the application. It will be initialized by this function, and will later
     *                                be used to identify this particular service instance.
     *
     * @return      NRF_SUCCESS on successful initialization of service, otherwise an error code.
     */
    uint32_t ble_led_service_init(ble_led_service_t * p_led_service, const ble_led_service_init_t * p_led_service_init);
    
    /**@brief Function for handling the application's BLE stack events.
     *
     * @details This function handles all events from the BLE stack that are of interest to the LED Service.
     *
     * @param[in] p_ble_evt  Event received from the BLE stack.
     * @param[in] p_context  LED Service structure.
     */
    void ble_led_service_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context);

led_service.c

  • Lines 18-23: include the necessary header files as well as define the string
    #include <string.h>
    
    #include "nrf_log.h"
    #include "led_service.h"
    
    static const uint8_t LED2CharName[] = "LED 2";
    
  • Lines 25-44: define the functions for handling the connection and disconnection events
    /**@brief Function for handling the Connect event.
     *
     * @param[in]   p_led_service  LED service structure.
     * @param[in]   p_ble_evt      Event received from the BLE stack.
     */
    static void on_connect(ble_led_service_t * p_led_service, ble_evt_t const * p_ble_evt)
    {
        p_led_service->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
    }
    
    /**@brief Function for handling the Disconnect event.
     *
     * @param[in]   p_bas       LED service structure.
     * @param[in]   p_ble_evt   Event received from the BLE stack.
     */
    static void on_disconnect(ble_led_service_t * p_led_service, ble_evt_t const * p_ble_evt)
    {
        UNUSED_PARAMETER(p_ble_evt);
        p_led_service->conn_handle = BLE_CONN_HANDLE_INVALID;
    }
  • Lines 46-61: define the function for handling the BLE write event
    /**@brief Function for handling the Write event.
     *
     * @param[in] p_led_service   LED Service structure.
     * @param[in] p_ble_evt       Event received from the BLE stack.
     */
    static void on_write(ble_led_service_t * p_led_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;
    
        if (   (p_evt_write->handle == p_led_service->led_2_char_handles.value_handle)
            && (p_evt_write->len == 1)
            && (p_led_service->led_write_handler != NULL))
        {
            p_led_service->led_write_handler(p_ble_evt->evt.gap_evt.conn_handle, p_led_service, p_evt_write->data[0]);
        }
    }
  •  Lines 63-112: define the function for adding the LED 2 Characteristic
    /**@brief Function for adding the LED 2 characteristic.
     *
     */
    static uint32_t led_2_char_add(ble_led_service_t * p_led_service)
    {
        ble_gatts_char_md_t char_md;
        ble_gatts_attr_t    attr_char_value;
        ble_gatts_attr_md_t attr_md;
        ble_uuid_t          ble_uuid;
    
        memset(&char_md, 0, sizeof(char_md));
        memset(&attr_md, 0, sizeof(attr_md));
        memset(&attr_char_value, 0, sizeof(attr_char_value));
    
        char_md.char_props.read          = 1;
        char_md.char_props.write         = 1;
        char_md.p_char_user_desc         = LED2CharName;
        char_md.char_user_desc_size      = sizeof(LED2CharName);
        char_md.char_user_desc_max_size  = sizeof(LED2CharName);
        char_md.p_char_pf                = NULL;
        char_md.p_user_desc_md           = NULL;
        char_md.p_cccd_md                = NULL;
        char_md.p_sccd_md                = NULL;
    
        // Define the LED 2 Characteristic UUID
        ble_uuid.type = p_led_service->uuid_type;
        ble_uuid.uuid = BLE_UUID_LED_2_CHAR_UUID;
    
        // 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);
    
        // 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      = NULL;
    
        return sd_ble_gatts_characteristic_add(p_led_service->service_handle, &char_md,
                                               &attr_char_value,
                                               &p_led_service->led_2_char_handles);
    }
  • Lines 114-153: define the function used to initialize the LED Service
    uint32_t ble_led_service_init(ble_led_service_t * p_led_service, const ble_led_service_init_t * p_led_service_init)
    {
        uint32_t   err_code;
        ble_uuid_t ble_uuid;
    
        // Initialize service structure
        p_led_service->conn_handle = BLE_CONN_HANDLE_INVALID;
    
        // Initialize service structure.
        p_led_service->led_write_handler = p_led_service_init->led_write_handler;
    
        // Add service UUID
        ble_uuid128_t base_uuid = {BLE_UUID_LED_SERVICE_BASE_UUID};
        err_code = sd_ble_uuid_vs_add(&base_uuid, &p_led_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_led_service->uuid_type;
        ble_uuid.uuid = BLE_UUID_LED_SERVICE_UUID;
    
        // Set up and add the service
        err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_led_service->service_handle);
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    
        // Add the different characteristics in the service:
        //   Button press characteristic:   E54B0002-67F5-479E-8711-B3B99198CE6C
        err_code = led_2_char_add(p_led_service);
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    
        return NRF_SUCCESS;
    }
  • Lines 155-177: define the BLE event handler that gets called by the SoftDevice (to handle the different events such as connection, disconnection, and write events)
    void ble_led_service_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
    {
        ble_led_service_t * p_led_service = (ble_led_service_t *)p_context;
    
        switch (p_ble_evt->header.evt_id)
        {
            case BLE_GAP_EVT_CONNECTED:
                on_connect(p_led_service, p_ble_evt);
                break;
    
            case BLE_GATTS_EVT_WRITE:
                on_write(p_led_service, p_ble_evt);
                break;
    
            case BLE_GAP_EVT_DISCONNECTED:
                on_disconnect(p_led_service, p_ble_evt);
                break;
    
            default:
                // No implementation needed.
                break;
        }
    }

Battery Service

In addition to the custom Service and Characteristic, we’ll be using the Bluetooth SIG defined Battery Service, which includes a Battery Level Characteristic. (No need to create our own custom since we can reuse the SIG-defined Service).

Nordic’s SDK already has this Service implemented, so all we need to do is:

  • Enable the Battery Service in sdk_config.h. You can do so by setting the following macro to 1:
    // <e> BLE_BAS_ENABLED - ble_bas - Battery Service
    //==========================================================
    #ifndef BLE_BAS_ENABLED
    #define BLE_BAS_ENABLED 1
    #endif
  • Add the ble_bas.c file:
    • Right-click on the folder named nRF_BLE under the Project in the Project Explorer window
    • Click on Add Existing File
    • Locate the ble_bas.c file under <SDK folder>/components/ble/ble_services/ble_bas
    • Click on ble_bas.c and hit Add
  • Update NRF_SDH_BLE_VS_UUID_COUNT in sdk_config.h to reflect the number of custom UUIDs (2 in our case: one for the LED Service and one for the LED 2 Characteristic)
    // <o> NRF_SDH_BLE_VS_UUID_COUNT - The number of vendor-specific UUIDs. 
    #ifndef NRF_SDH_BLE_VS_UUID_COUNT
    #define NRF_SDH_BLE_VS_UUID_COUNT 2
    #endif

Now that we have the Battery Service enabled, we need to implement the actual reading of the battery voltage level from the coin-cell battery installed in the development kit.

The good thing is we don’t have to implement this from scratch. In fact, one of the components within the nRF5 SDK provides exactly what we need!

The component we’ll be borrowing the code from is the Eddystone component (used for a beacon standard called Eddystone).

Navigate to the folder <SDK folder>/components/ble/ble_services/eddystone

There you will find the files:

es_battery_voltage.h
es_battery_voltage_saadc.c

We’ll copy these files into our project folder “ble_lightbulb” under a newly created folder named Battery Level.

Then rename them to “battery_voltage.c” and “battery_voltage.h“:

Now, you’ll need to add this folder to the Project in SES. Follow the same steps we did before with the LED Service files (creating a folder named Battery Level, but then using the Add Existing File option instead of creating a new one).

We’ll make a few slight modifications to functions names and such:

battery_voltage.h

#ifndef BATTERY_VOLTAGE_H__
#define BATTERY_VOLTAGE_H__

#include 

/**@brief Function for initializing the battery voltage module.
 */
void battery_voltage_init(void);

/**@brief Function for reading the battery voltage.
 *
 * @param[out]   p_vbatt       Pointer to the battery voltage value.
 */
void battery_voltage_get(uint16_t * p_vbatt);

#endif // BATTERY_VOLTAGE_H__

battery_voltage.c

#include "battery_voltage.h"
#include "nrf_drv_saadc.h"
#include "sdk_macros.h"
#include "nrf_log.h"

#define ADC_REF_VOLTAGE_IN_MILLIVOLTS  600  //!< Reference voltage (in milli volts) used by ADC while doing conversion.
#define DIODE_FWD_VOLT_DROP_MILLIVOLTS 270  //!< Typical forward voltage drop of the diode (Part no: SD103ATW-7-F) that is connected in series with the voltage supply. This is the voltage drop when the forward current is 1mA. Source: Data sheet of 'SURFACE MOUNT SCHOTTKY BARRIER DIODE ARRAY' available at www.diodes.com.
#define ADC_RES_10BIT                  1024 //!< Maximum digital value for 10-bit ADC conversion.
#define ADC_PRE_SCALING_COMPENSATION   6    //!< The ADC is configured to use VDD with 1/3 prescaling as input. And hence the result of conversion is to be multiplied by 3 to get the actual value of the battery voltage.
#define ADC_RESULT_IN_MILLI_VOLTS(ADC_VALUE) \
    ((((ADC_VALUE) *ADC_REF_VOLTAGE_IN_MILLIVOLTS) / ADC_RES_10BIT) * ADC_PRE_SCALING_COMPENSATION)

static nrf_saadc_value_t adc_buf;                   //!< Buffer used for storing ADC value.
static uint16_t          m_batt_lvl_in_milli_volts; //!< Current battery level.

/**@brief Function handling events from 'nrf_drv_saadc.c'.
 *
 * @param[in] p_evt SAADC event.
 */
static void saadc_event_handler(nrf_drv_saadc_evt_t const * p_evt)
{
    if (p_evt->type == NRF_DRV_SAADC_EVT_DONE)
    {
        nrf_saadc_value_t adc_result;

        adc_result = p_evt->data.done.p_buffer[0];

        m_batt_lvl_in_milli_volts = ADC_RESULT_IN_MILLI_VOLTS(adc_result) + DIODE_FWD_VOLT_DROP_MILLIVOLTS;

        NRF_LOG_INFO("ADC reading - ADC:%d,  In Millivolts: %d\r\n", adc_result, m_batt_lvl_in_milli_volts);
    }
}

void battery_voltage_init(void)
{
    ret_code_t err_code = nrf_drv_saadc_init(NULL, saadc_event_handler);

    APP_ERROR_CHECK(err_code);

    nrf_saadc_channel_config_t config =
        NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_VDD);
    err_code = nrf_drv_saadc_channel_init(0, &config);
    APP_ERROR_CHECK(err_code);

    err_code = nrf_drv_saadc_buffer_convert(&adc_buf, 1);
    APP_ERROR_CHECK(err_code);

    err_code = nrf_drv_saadc_sample();
    APP_ERROR_CHECK(err_code);
}

void battery_voltage_get(uint16_t * p_vbatt)
{
    VERIFY_PARAM_NOT_NULL_VOID(p_vbatt);

    *p_vbatt = m_batt_lvl_in_milli_volts;
    if (!nrf_drv_saadc_is_busy())
    {
        ret_code_t err_code = nrf_drv_saadc_buffer_convert(&adc_buf, 1);
        APP_ERROR_CHECK(err_code);

        err_code = nrf_drv_saadc_sample();
        APP_ERROR_CHECK(err_code);
    }
}

To be able to read the battery level, we have to set up a timer to trigger the reading of the battery voltage in the application’s main.c file.

Here are the changes we need to make to implement this functionality:

  • Define the battery timer and the battery level update period:
    /**< Battery timer. */
    APP_TIMER_DEF(m_battery_timer_id);
    
    #define BATTERY_LEVEL_MEAS_INTERVAL     APP_TIMER_TICKS(120000)                 /**< Battery level measurement interval (ticks). */
    
  • Declare two functions for handling the timeout and another for updating the battery level:
    /**@brief Function for handling the Battery measurement timer timeout.
     *
     * @details This function will be called each time the battery level measurement timer expires.
     *
     * @param[in] p_context  Pointer used for passing some arbitrary information (context) from the
     *                       app_start_timer() call to the timeout handler.
     */
    static void battery_level_meas_timeout_handler(void * p_context)
    {
        UNUSED_PARAMETER(p_context);
        NRF_LOG_INFO("Battery Level timeout event");
    
        // Only send the battery level update if we are connected
        if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
        {
            battery_level_update();
        }
    }
    /**@brief Function for updating the Battery Level measurement*/
    static void battery_level_update(void)
    {
        ret_code_t err_code;
    
        uint8_t  battery_level;
        uint16_t vbatt;              // Variable to hold voltage reading
        battery_voltage_get(&vbatt); // Get new battery voltage
    
        battery_level = battery_level_in_percent(vbatt);          //Transform the millivolts value into battery level percent.
        printf("ADC result in percent: %d\r\n", battery_level);
    
        err_code = ble_bas_battery_level_update(&m_bas, battery_level, m_conn_handle);
        if ((err_code != NRF_SUCCESS) &&
            (err_code != NRF_ERROR_INVALID_STATE) &&
            (err_code != NRF_ERROR_RESOURCES) &&
            (err_code != BLE_ERROR_GATTS_SYS_ATTR_MISSING)
           )
        {
            APP_ERROR_HANDLER(err_code);
        }
    }
  • Next, we need to add the timer that triggers the battery level read operation. We add this to the timers_init() function:
    /**@brief Function for the Timer initialization.
     *
     * @details Initializes the timer module. This creates and starts application timers.
     */
    static void timers_init(void)
    {
        // Initialize timer module.
        ret_code_t err_code = app_timer_init();
        APP_ERROR_CHECK(err_code);
    
        // Create timers.
        err_code = app_timer_create(&m_battery_timer_id,
                                    APP_TIMER_MODE_REPEATED,
                                    battery_level_meas_timeout_handler);
        APP_ERROR_CHECK(err_code);
    }
    
  • Add a function to start the timer, and then call it from main()
    /**@brief Function for starting application timers.
     */
    static void application_timers_start(void)
    {
        uint32_t err_code;
    
        // Start application timers.
        err_code = app_timer_start(m_battery_timer_id, BATTERY_LEVEL_MEAS_INTERVAL, NULL);
        APP_ERROR_CHECK(err_code);
    }
  • Enable the SAADC (Analog to digital converter), needed for reading the battery voltage level. Open sdk_config.h and enable the SAADC in two locations:
    // <e> NRFX_SAADC_ENABLED - nrfx_saadc - SAADC peripheral driver
    //==========================================================
    #ifndef NRFX_SAADC_ENABLED
    #define NRFX_SAADC_ENABLED 1
    #endif
    // <e> SAADC_ENABLED - nrf_drv_saadc - SAADC peripheral driver - legacy layer
    //==========================================================
    #ifndef SAADC_ENABLED
    #define SAADC_ENABLED 1
    #endif
  • Add the SAADC driver file. Right-click on “nRF_Drivers” and click on “Add Existing File“. Then navigate to “nRF5_SDK_current/modules/nrfx/drivers/src/” and select the file “nrfx_saadc.c

Initializing the Services

Finally, we need to initialize the LED and Battery Services in the main file (main.c).

To do that, we implement the following function:

/**@brief Function for initializing services that will be used by the application.
 */
static void services_init(void)
{
    uint32_t       err_code;
    ble_bas_init_t bas_init;
    ble_led_service_init_t led_init;

    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);

    // 1. Initialize the LED service
    led_init.led_write_handler = led_write_handler;

    err_code = ble_led_service_init(&m_led_service, &led_init);
    APP_ERROR_CHECK(err_code);

    // 2. Initialize Battery Service.
    memset(&bas_init, 0, sizeof(bas_init));

    // Here the sec level for the Battery Service can be changed/increased.
    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&bas_init.battery_level_char_attr_md.cccd_write_perm);
    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&bas_init.battery_level_char_attr_md.read_perm);
    BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(&bas_init.battery_level_char_attr_md.write_perm);

    BLE_GAP_CONN_SEC_MODE_SET_OPEN(&bas_init.battery_level_report_read_perm);

    bas_init.evt_handler          = NULL;
    bas_init.support_notification = true;
    bas_init.p_report_ref         = NULL;
    bas_init.initial_batt_level   = 100;

    err_code = ble_bas_init(&m_bas, &bas_init);
    APP_ERROR_CHECK(err_code);
}

The function initializes both Services as well as defines the permissions of the Battery Service and Battery Level Characteristic.

This function already gets called from the main function main() :

    gap_params_init();
    gatt_init();
    services_init();
    advertising_init();
    conn_params_init();

Flashing and debugging

Before we build and flash our program to the development kit (nRF52840), we want to enable logging first so we can better follow the state of events and the state of our application.

To get this working:

  • Open sdk_config.h and make sure the following macros are set to the correct debug level (4). This level will enable all debug statements in the debug terminal within SES when you run your application.
    // <o> NRF_LOG_DEFAULT_LEVEL  - Default Severity level
     
    // <0=> Off 
    // <1=> Error 
    // <2=> Warning 
    // <3=> Info 
    // <4=> Debug 
    
    #ifndef NRF_LOG_DEFAULT_LEVEL
    #define NRF_LOG_DEFAULT_LEVEL 4
    #endif

Now, we can flash the application to the development kit.

To do this:

  • Right click on “Project ‘ble_lightbulb’
  • Select “Build
  • Right-click again on “Project ‘ble_lightbulb’
  • Select “Debug → Start Debugging
  • The application will start and stop at main() waiting for you to continue execution
  • You should now see the output in the debug terminal similar to the following:

Testing

Our next and final step is to test our application and make sure it’s working fine. For this, we will use a mobile phone app that acts as a BLE Central.

For this example, we’ll be using an app called LightBlue (available for iOS and Android).

Here’s a video showing the application running on the development kit:

Summary

In this post, we went over how to build a full BLE peripheral application that you can use as a starting point for developing any BLE application on the nRF52 platform. We went over:

  • Setting up the Project and Solution file for the application
  • Fully implementing a BLE application that allows you to turn ON/OFF an LED on the nRF52840 development kit
  • Building, flashing, and debugging the application on the nRF52840 development kit
  • Testing the application from a mobile phone app to make sure it is functioning correctly

To be notified when future blog posts are published here on the Novel Bits blog, be sure to enter your email address in the form below!

If you would like to download the code used in this post, please enter your email address in the form below. You’ll get a .zip containing all the source code, and I will also send you a FREE 9-page Report on the Essential Bluetooth Developer Tools. In addition to that, you will receive exclusive content, tips, and tricks that I don’t post to the blog!