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

nRF52 Remote Control (BLE Central) use case

In any BLE application, there are (at least) two devices involved: a BLE Peripheral device and a BLE Central device.

Usually, the BLE Central is a smartphone, but that doesn’t mean it has to be!

What if:

  • you do not want to have to launch an app everytime you want to control a BLE Peripheral?
  • you want to have a dedicated device that acts as the remote control for the Peripheral device?
  • you want to utilize Bluetooth 5 features such as the long-range feature (Coded PHY)? (which currently does not exist in any smartphone)
  • you want to learn more about how a BLE Central works and how to develop one yourself?

In this case, a dedicated BLE Central device can make a lot more sense than using a smartphone!

Novel Bits blog reader Kevin asks:

I have been looking at many bluetooth 5 blogs and tutorials. Many concentrate on peripheral examples and never provide a central example. Would you please consider writing a simple example for custom project on nrf52840pdk which mirrors and works with your example in

The example ( Kevin is referring to is the BLE Lightbulb application example we discussed in my previous tutorial: How to build a smart BLE Lightbulb application using nRF52.

In this blog post, we’ll be going over how to build a remote control device based on the nRF52840 preview development kit provided by Nordic Semiconductor. This development kit will be used to connect to and control the BLE lightbulb application we built in the previous tutorial.


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. For this specific post, knowing a little bit more about BLE Central applications will help.

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)
  • The BLE Lightbulb application we built in the previous tutorial running on another nRF52 development kit

Application Overview

The (BLE Central) application we’ll be developing in this tutorial will cover the following functionality:

  • Scan for the BLE Lightbulb we built in the previous tutorial. It will specifically look for a BLE Peripheral device advertising with the name “BLE_Lightbulb“.
  • During scanning, LED1 on the BLE Central development kit will be ON
  • Once it finds the target BLE device, it will connect to it and discover its services and characteristics looking for two specific ones:
    • LED Service (E54B0001-67F5-479E-8711-B3B99198CE6C)
    • LED2 Characteristic (E54B0002-67F5-479E-8711-B3B99198CE6C)
  • Once connected, LED1 on the BLE Central development kit will be turned OFF and LED2 will be ON
  • Once connected, the user can turn ON and OFF the target BLE Peripheral’s LED2 light by:
    • Pressing Button 1 to turn ON the LED
    • Pressing Button 2 to turn OFF the LED

Getting Started

Installing the SDK (if you haven’t done so already)

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 following the structure in the previous tutorial. I’ll be creating a new folder named “ble_lightbulb_remote_control” under the folder “BLE Projects” I created previously.

Figure 1: BLE Projects folder structure

Inside this new folder “ble_lightbulb_remote_control”, I’ll be adding and developing the code for this application.

Here’s what the folder will look like at the end of development:

Figure 2: ble_lightbulb_remote_control folder structure

Anatomy of a BLE Central Application (nRF52)

There are a few basic elements and functionalities that make up a BLE Central in an nRF52 application. These are:

  • Enabling the BLE Central functionality via the macro (defined in sdk_config.h):
    #define PM_CENTRAL_ENABLED 1
  • Enabling the Database Discovery module via the macro (defined in sdk_config.h):

    The Database Discovery module is what handles discovering the GATT Services and Characteristics on a BLE Peripheral device by the BLE Central application.

  • Enabling the number of links for the BLE Central application via the macro (defined in sdk_config.h):

    Based on this count, we also need to update the Total Link Count (also in sdk_config.h):


    In our case, we only need one BLE Central link (no Peripheral links), so we’ll be setting the Total Link Count to 1.

  • A BLE Central Client module that defines the different Services and Characteristics of the target BLE Peripheral device the Central will connect to and interact with. We will be developing this code from scratch in our example.

In addition to enabling these modules, we’ll be using the following generic nRF modules:

Now, let’s go over some of the most important parts of the application!

Looking for the source code to this post?
Jump straight to the downloads section at the bottom of this tutorial.

LED Service Client

The most important part of our BLE Central application is the Client module that makes the Central aware of the Services and Characteristics of the target BLE Peripheral device. This makes it possible to read and write to the Characteristics of interest as well as subscribe to Notifications and Indications.

For our application, we need to create a Client that understands the LED Service we defined and implemented in our previous LED Lightbulb application tutorial.

We’ll be creating two files: led_service_client.c and led_service_client.h.

Let’s go over the most important sections of each of these source files.


  • We need to include the standard nRF header files, but specifically for our Client module, we need to include the “ble_db_discovery.h” header file to be able to utilize the DB Discovery Module needed for discovering GATT Services and Characteristics:
    #include "ble.h"
    #include "ble_db_discovery.h"
    #include "nrf_sdh_ble.h"
  • Next, we define a macro that we can use to instantiate an instance of the LED Service Client module:
    /**@brief   Macro for defining an led_service_client instance.
     * @param   _name   Name of the instance.
     * @hideinitializer
    #define BLE_LED_SERVICE_CLIENT_DEF(_name)                                                                        
    static ble_led_service_client_t _name;                                                                           
    NRF_SDH_BLE_OBSERVER(_name ## _obs,                                                                 
                         ble_led_service_client_on_ble_evt, &_name)

    This follows the method used by many of the examples and SIG-adopted Services in the Nordic nRF5 SDK.

  • Next, we define the UUIDs for the LED Service and the LED2 Characteristic that the BLE Peripheral exposes:
    // 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
  • Following that, we define the one event we’re interested in: the DB Discovery Complete event. In addition to that, we define a database structure which holds the LED2 Characteristic handle. Finally, we define the LED Event structure that holds all the important information in a BLE event: connection handle, type of event, and the database structure.
    /**@brief LED_service Client event type. */
    typedef enum
        BLE_LED_SERVICE_CLIENT_EVT_DISCOVERY_COMPLETE = 1,  /**< Event indicating that the LED Button Service has been discovered at the peer. */
    } ble_led_service_client_evt_type_t;
    /**@brief Structure containing the handles related to the LED Button Service found on the peer. */
    typedef struct
        uint16_t led2_handle;          /**< Handle of the LED characteristic as provided by the SoftDevice. */
    } led_service_db_t;
    /**@brief LED Event structure. */
    typedef struct
        ble_led_service_client_evt_type_t evt_type;        /**< Type of the event. */
        uint16_t                        conn_handle;     /**< Connection handle on which the event occured.*/
        led_service_db_t         peer_db;         /**< LED Service related handles found on the peer device. This will be filled if the evt_type is @ref BLE_LED_SERVICE_CLIENT_EVT_DISCOVERY_COMPLETE.*/
    } ble_led_service_client_evt_t;
  • Define the function prototype for the event handler that the application assigns for this module:
    typedef void (* ble_led_service_client_evt_handler_t) (ble_led_service_client_t * p_led_service_client, ble_led_service_client_evt_t * p_evt);
  • The data structure for the main LED Service Client structure:
    /**@brief LED Service Client structure. */
    struct ble_led_service_client_s
        uint16_t                              conn_handle;                 /**< Connection handle as provided by the SoftDevice. */
        led_service_db_t                      peer_led_service_db;  /**< Handles related to LED Service on the peer*/
        ble_led_service_client_evt_handler_t  evt_handler;                 /**< Application event handler to be called when there is an event related to the LED service. */
        uint8_t                               uuid_type;                   /**< UUID type. */
  • The last data structure we define is responsible for storing the initialization function:
    /**@brief LED Service Client initialization structure. */
    typedef struct
        ble_led_service_client_evt_handler_t evt_handler;  /**< Event handler to be called by the LED Service Client module whenever there is an event related to the LED Service. */
    } ble_led_service_client_init_t;
  • Following the data structures, we declare the different functions that the main application will call:
    • Module initialization function:
      uint32_t ble_led_service_client_init(ble_led_service_client_t * p_ble_led_service_client, ble_led_service_client_init_t * p_ble_led_service_client_init);
    • Event handler function:
      void ble_led_service_client_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context);
    • Database discovery event handler:
      void ble_led_service_on_db_disc_evt(ble_led_service_client_t * p_ble_led_service_client, const ble_db_discovery_evt_t * p_evt);
    • A function for assigning the handles of the discovered LED Service Characteristic(s):
      uint32_t ble_led_service_client_handles_assign(ble_led_service_client_t *    p_ble_led_service_client,
                                        uint16_t         conn_handle,
                                        const led_service_db_t * p_peer_handles);
    • Finally, the function that gets used to send a write request to the BLE Peripheral to turn ON or OFF the LED:
      uint32_t ble_led_service_led2_setting_send(ble_led_service_client_t * p_ble_led_service_client, uint8_t setting);


  • Implementation of the function responsible for handling the Database Discovery event:
    void ble_led_service_on_db_disc_evt(ble_led_service_client_t * p_ble_led_service_client, ble_db_discovery_evt_t const * p_evt)
        // Check if the Led Button Service was discovered.
        if (p_evt->evt_type == BLE_DB_DISCOVERY_COMPLETE &&
            p_evt->params.discovered_db.srv_uuid.uuid == BLE_UUID_LED_SERVICE_UUID &&
            p_evt->params.discovered_db.srv_uuid.type == p_ble_led_service_client->uuid_type)
            ble_led_service_client_evt_t evt;
            evt.conn_handle = p_evt->conn_handle;
            for (uint32_t i = 0; i < p_evt->params.discovered_db.char_count; i++)
                const ble_gatt_db_char_t * p_char = &(p_evt->params.discovered_db.charateristics[i]);
                switch (p_char->characteristic.uuid.uuid)
                    case BLE_UUID_LED_2_CHAR_UUID:
                        evt.peer_db.led2_handle = p_char->characteristic.handle_value;
            NRF_LOG_DEBUG("Led Service discovered at peer.");
            //If the instance has been assigned prior to db_discovery, assign the db_handles
            if (p_ble_led_service_client->conn_handle != BLE_CONN_HANDLE_INVALID)
                if (p_ble_led_service_client->peer_led_service_db.led2_handle         == BLE_GATT_HANDLE_INVALID)
                    p_ble_led_service_client->peer_led_service_db = evt.peer_db;
            p_ble_led_service_client->evt_handler(p_ble_led_service_client, &evt);

    The function looks for the LED Service and the LED2 Characteristic that’s contained within it. This is necessary since we need to store the handle that gets referenced whenever we need to read or write a value to the LED2 Characteristic.

  • Implementation of the initialization function for the LED Service Client module:
    uint32_t ble_led_service_client_init(ble_led_service_client_t * p_ble_led_service_client, ble_led_service_client_init_t * p_ble_led_service_client_init)
        uint32_t      err_code;
        ble_uuid_t    led_service_uuid;
        ble_uuid128_t led_service_base_uuid = {BLE_UUID_LED_SERVICE_BASE_UUID};
        p_ble_led_service_client->peer_led_service_db.led2_handle   = BLE_GATT_HANDLE_INVALID;
        p_ble_led_service_client->conn_handle                      = BLE_CONN_HANDLE_INVALID;
        p_ble_led_service_client->evt_handler                      = p_ble_led_service_client_init->evt_handler;
        err_code = sd_ble_uuid_vs_add(&led_service_base_uuid, &p_ble_led_service_client->uuid_type);
        if (err_code != NRF_SUCCESS)
            return err_code;
        led_service_uuid.type = p_ble_led_service_client->uuid_type;
        led_service_uuid.uuid = BLE_UUID_LED_SERVICE_UUID;
        return ble_db_discovery_evt_register(&led_service_uuid);

    The most important aspect of this function is that it registers the LED Service UUID

  • Implementation of the function that handles the BLE events within the LED Service Client module:
    void ble_led_service_client_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
        if ((p_context == NULL) || (p_ble_evt == NULL))
        ble_led_service_client_t * p_ble_led_service_client = (ble_led_service_client_t *)p_context;
        switch (p_ble_evt->header.evt_id)
            case BLE_GATTC_EVT_WRITE_RSP:
                on_write_rsp(p_ble_led_service_client, p_ble_evt);
            case BLE_GAP_EVT_DISCONNECTED:
                on_disconnected(p_ble_led_service_client, p_ble_evt);

    This function handles both the disconnection event as well as the Write Response event (in response to a Write Request).

  • Implementation of the function that handles sending the Write Request to turn LED2 On or Off:
    uint32_t ble_led_service_led2_setting_send(ble_led_service_client_t * p_ble_led_service_client, uint8_t status)
        if (p_ble_led_service_client->conn_handle == BLE_CONN_HANDLE_INVALID)
            return NRF_ERROR_INVALID_STATE;
        NRF_LOG_DEBUG("writing LED2 status 0x%x", status);
        tx_message_t * p_msg;
        p_msg              = &m_tx_buffer[m_tx_insert_index++];
        m_tx_insert_index &= TX_BUFFER_MASK;
        p_msg->req.write_req.gattc_params.handle   = p_ble_led_service_client->peer_led_service_db.led2_handle;
        p_msg->req.write_req.gattc_params.len      = sizeof(status);
        p_msg->req.write_req.gattc_params.p_value  = p_msg->req.write_req.gattc_value;
        p_msg->req.write_req.gattc_params.offset   = 0;
        p_msg->req.write_req.gattc_params.write_op = BLE_GATT_OP_WRITE_CMD;
        p_msg->req.write_req.gattc_value[0]        = status;
        p_msg->conn_handle                         = p_ble_led_service_client->conn_handle;
        p_msg->type                                = WRITE_REQ;
        return NRF_SUCCESS;

    Notice it utilizes the LED2 Characteristic handle we stored after the Database Discovery is complete.

  • Finally, the function that assigns any handles of interest:
    uint32_t ble_led_service_client_handles_assign(ble_led_service_client_t    * p_ble_led_service_client,
                                                   uint16_t                      conn_handle,
                                                   const led_service_db_t      * p_peer_handles)
        p_ble_led_service_client->conn_handle = conn_handle;
        if (p_peer_handles != NULL)
            p_ble_led_service_client->peer_led_service_db = *p_peer_handles;
        return NRF_SUCCESS;

The last file we’ll look at is the main application source file: main.c:


We won’t cover the whole file, but rather focus on the most important sections.

In the following code section, we instantiate:

  • the LED Service Client module
  • the GATT module
  • the Database Discovery module
BLE_LED_SERVICE_CLIENT_DEF(m_ble_led_service_client);           /**< Main structure used by the LED Service client module. */
NRF_BLE_GATT_DEF(m_gatt);                                       /**< GATT module instance. */
BLE_DB_DISCOVERY_DEF(m_db_disc);                                /**< DB discovery module instance. */

Next, we define the Advertised name of the target BLE Peripheral device:

static char const m_target_periph_name[] = "BLE_Lightbulb";     /**< Name of the device we try to connect to. This name is searched in the scan report data*/

Another important function we need to define is the function for scanning of BLE Peripheral devices:

/**@brief Function to start scanning.
static void scan_start(void)
    ret_code_t err_code;
    (void) sd_ble_gap_scan_stop();
    err_code = sd_ble_gap_scan_start(&m_scan_params, &m_scan_buffer);

In this function, we also toggle the BLE Central development kit’s LEDs indicate the current state: Scanning.

The following function initializes the LED Service Client module:

/**@brief LED Service client initialization.
static void led_service_client_init(void)
    ret_code_t       err_code;
    ble_led_service_client_init_t led_service_client_init_obj;
    led_service_client_init_obj.evt_handler = led_service_client_evt_handler;
    err_code = ble_led_service_client_init(&m_ble_led_service_client, &led_service_client_init_obj);

The following function is the event handler that gets assigned to the LED Service Client module during initialization:

/**@brief Handles events coming from the LED Button central module.
static void led_service_client_evt_handler(ble_led_service_client_t * p_led_service_client, ble_led_service_client_evt_t * p_led_service_client_evt)
    switch (p_led_service_client_evt->evt_type)
            ret_code_t err_code;
            err_code = ble_led_service_client_handles_assign(&m_ble_led_service_client,
            NRF_LOG_INFO("LED service discovered on conn_handle 0x%x.", p_led_service_client_evt->conn_handle);
            err_code = app_button_enable();
            // No implementation needed.

It handles the Database Discovery event and assigns the handles.

In the following function, we parse the Advertising Report returned by the stack and look for the target device:

/**@brief Function for handling the advertising report BLE event.
 * @param[in] p_adv_report  Advertising report from the SoftDevice.
static void on_adv_report(ble_gap_evt_adv_report_t const * p_adv_report)
    ret_code_t err_code;
    if (ble_advdata_name_find(p_adv_report->data.p_data,
        // Name is a match, initiate connection.
        err_code = sd_ble_gap_connect(&p_adv_report->peer_addr,
        err_code = sd_ble_gap_scan_start(NULL, &m_scan_buffer);

If we discover the target BLE Peripheral device (our Lightbulb Peripheral), we initiate a connection to the device.

In the BLE event handler function, we start the Database (GATT) Discovery process if we detect that we’re connected:

/**@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;
    // For readability.
    ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
    switch (p_ble_evt->header.evt_id)
        // Upon connection, check which peripheral has connected (HR or RSC), initiate DB
        // discovery, update LEDs status and resume scanning if necessary. */
            err_code = ble_led_service_client_handles_assign(&m_ble_led_service_client, p_gap_evt->conn_handle, NULL);
            err_code = ble_db_discovery_start(&m_db_disc, p_gap_evt->conn_handle);
            // Update LEDs status, and check if we should be looking for more
            // peripherals to connect to.
        } break;

Also, notice that we set the development kit’s LEDs to the Connected state (instead of the Scanning state).

Next, we need to initialize the development kit’s buttons so we can detect when a button (Button 1 or Button 2) is pressed:

/**@brief Function for initializing the button handler module.
static void buttons_init(void)
    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[] =
        {LEDBUTTON_ON_BUTTON_PIN, false, BUTTON_PULL, button_event_handler},
        {LEDBUTTON_OFF_BUTTON_PIN, false, BUTTON_PULL, button_event_handler}
    err_code = app_button_init(buttons, ARRAY_SIZE(buttons),

We initialized the buttons, but we also need a function to handle the specific button press. Based on that, we set the appropriate setting for LED2 (On or Off) on the BLE Peripheral development kit:

/**@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)
            err_code = ble_led_service_led2_setting_send(&m_ble_led_service_client, 1);
            if (err_code != NRF_SUCCESS &&
                err_code != BLE_ERROR_INVALID_CONN_HANDLE &&
                err_code != NRF_ERROR_INVALID_STATE)
            if (err_code == NRF_SUCCESS)
                NRF_LOG_INFO("LED Service write LED2 state %d", button_action);
            err_code = ble_led_service_led2_setting_send(&m_ble_led_service_client, 0);
            if (err_code != NRF_SUCCESS &&
                err_code != BLE_ERROR_INVALID_CONN_HANDLE &&
                err_code != NRF_ERROR_INVALID_STATE)
            if (err_code == NRF_SUCCESS)
                NRF_LOG_INFO("LED Service write LED2 state %d", button_action);

Finally, in main(), we initialize all the necessary modules and start the Scanning process:

int main(void)
    // Initialize.
    // Start execution.
    NRF_LOG_INFO("BLE Lightbulb Remote Control started.");
    // Turn on the LED to signal scanning.
    // Enter main loop.
    for (;;)


Now that we’ve developed the code for the BLE Central device, we will test it and make sure it works correctly.

To do this, we will flash the BLE Central to one nRF52840 development kit and the BLE Peripheral application to another nRF52840 development kit (explained in the previous tutorial).

To make it easier to follow, I’ve recorded a video explaining the different aspects of the system as well as testing its functionality.

Watch the video here:


To summarize, in this tutorial:

  • We went over the main application’s functionality
  • We went over the most important elements within an nRF52-based BLE Central application
  • We described the structure of the Segger Embedded Studio project and its source files
  • We implemented the LED Service Client module which is needed for discovering and interfacing with the BLE Peripheral’s LED Service and LED2 Characteristic
  • We went over the most important code sections in the BLE Central “Remote Control” application
  • We tested the application to make sure it is working correctly

To be notified when future tutorials are published here, 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!

“Learn The Basics of Bluetooth Low Energy EVEN If You Have No Coding Or Wireless Experience!"

Don't miss out on the latest articles & tutorials. Sign-up for our newsletter today!

Take your BLE knowledge to the next level.

If you’re looking to get access to full video courses covering more topics, then check out the Bluetooth Developer Academy.

As part of all the courses within the Academy, you’ll also be able to download the full source code to use as a reference or use within your own application.

By joining the Bluetooth Developer Academy, you will get access to a growing library of video courses.

The Academy also features access to a private community of Bluetooth experts, developers, and innovators. You’ll get to connect and interact with me and other experts in the Bluetooth space, learn from others’ experiences and knowledge, and share yours as well.

So, what are you waiting for?? Join today!

Get the new "Intro to Bluetooth Low Energy" hardcover book for FREE

This new & updated edition of my best-selling book is specially crafted to help you learn everything you need to get started with BLE development.

Grab your copy for FREE today!