5. Create a Custom Profile Service

5.1. How does it work

Having added our first Characteristic, the Control Point, let’s take a look on the existing custom BLE Profile Service Characteristics, their values and properties. In the table below you can find all characteristics defined by the service, along with their properties and lengths.

Name

Properties

Length

Description

Control Point

Write

1

Accept commands from peer

LED State

Write no response

1

Toggles an LED connected to a GPIO

ADC Value 1

Read, Notify

2

Reads sample from an ADC channel

ADC Value 2

Read

2

Reads sample from an ADC channel

Button state

Read, Notify

1

Reads the current state of a push button connected to a GPIO

Indicate able

Read, Indicate

20

Demonstrate indications

Long value

Read, Write, Notify

50

Demonstrate writes to long characteristic value

These are the characteristics that have already been implemented in the ble_app_profile project. In this section we will take a closer look in the code snippets that implement our server’s functionality, based on the LED State Characteristic. For this reason we begin with a discussion on the SDK’s API for implementing the user’s application code.

5.1.1. User callback functions

The file user_callback_config.h in folder user_config/ is the communication point between the SDK functions and the user code. All of the key events that take place during our device’s operation, either the ones that are related with the device or peripheral initialization, or events during the BLE operation, can be handled by appropriate callbacks which the SDK will call when the event is started or finished. For example, take a look at the next code snippet:

static const struct arch_main_loop_callbacks user_app_main_loop_callbacks = {
 .app_on_init            = user_app_init,

 // By default the watchdog timer is reloaded and resumed when the system wakes up.
 // The user has to take into account the watchdog timer handling (keep it running,
 // freeze it, reload it, resume it, etc), when the app_on_ble_powered() is being
 // called and may potentially affect the main loop.
 .app_on_ble_powered     = NULL,

 // By default the watchdog timer is reloaded and resumed when the system wakes up.
 // The user has to take into account the watchdog timer handling (keep it running,
 // freeze it, reload it, resume it, etc), when the app_on_system_powered() is being
 // called and may potentially affect the main loop.
 .app_on_system_powered  = NULL,

 .app_before_sleep       = NULL,
 .app_validate_sleep     = NULL,
 .app_going_to_sleep     = NULL,
 .app_resume_from_sleep  = NULL,
};

You can leave an entry as NULL if the default SDK operation suits your needs, or you can extend your functionality by assigning a callback function in the appropriate field. Above there is only one user callback function assigned, the user_app_init that will be called when we initialize our device. We recommend you to start the name of all of your callbacks with user_, as this will enhance the consistency of your code and it will be less prone to errors.

Below you can see the message flow between the SDK, the user configuration settings, and the user application.

api_msg_flow

Figure 8 Message flow diagram between the SDK and the User App

When the user code has finished with any user defined initialization, it must call the default_app_on_init() and this will return the code flow control back to the SDK. For BLE related events, there is another structure defined, part of which is shown below.

static const struct app_callbacks user_app_callbacks = {
 .app_on_connection                  = user_app_connection,
 .app_on_disconnect                  = user_app_disconnect,
 .app_on_update_params_rejected      = NULL,
 .app_on_update_params_complete      = NULL,
 .app_on_set_dev_config_complete     = default_app_on_set_dev_config_complete,
 .app_on_adv_nonconn_complete        = NULL,
 .app_on_adv_undirect_complete       = user_app_adv_undirect_complete,
 .app_on_adv_direct_complete         = NULL,
 .app_on_db_init_complete            = default_app_on_db_init_complete,
 .app_on_scanning_completed          = NULL,
 .app_on_adv_report_ind              = NULL,
 .app_on_get_dev_name                = default_app_on_get_dev_name,
 .app_on_get_dev_appearance          = default_app_on_get_dev_appearance,
 .app_on_get_dev_slv_pref_params     = default_app_on_get_dev_slv_pref_params,
 .app_on_set_dev_info                = default_app_on_set_dev_info,
 .app_on_data_length_change          = NULL,
 .app_on_update_params_request       = default_app_update_params_request,
 .app_on_generate_static_random_addr = default_app_generate_static_random_addr,
 .app_on_svc_changed_cfg_ind         = NULL,
 .app_on_get_peer_features           = NULL,

There are also events defined when we have enabled the BLE security features, but these will be compiled only when we have set the appropriate security feature flag. You can see that it is fairly easy for someone to augment the default operation by registering their callback function at the appropriate event.

5.2. The LED Characteristic

Now that we have added our first characteristic, it will be easy to add the second one.

  1. Open the file user_custs1_def.h in folder user_custom_profile/ and add the Characteristic’s UUID. In addition, define its length and description as we have done in the previous section for the Control Point Characteristic.

#define DEF_SVC1_LED_STATE_UUID_128      {0x4F, 0x43, 0x31, 0x3C, 0x93, 0x92, 0x42, 0xE6, 0xA8, 0x76, 0xFA, 0x3B, 0xEF, 0xB4, 0x87, 0x5A}

#define DEF_SVC1_LED_STATE_CHAR_LEN      1

#define DEF_SVC1_LED_STATE_USER_DESC     "LED State"
  1. Add your custom Service database LED state Characteristic enumeration.

/// Custom1 Service Data Base Characteristic enum
enum
{
 // Custom Service 1
 SVC1_IDX_SVC = 0,

 SVC1_IDX_CONTROL_POINT_CHAR,
 SVC1_IDX_CONTROL_POINT_VAL,
 SVC1_IDX_CONTROL_POINT_USER_DESC,

 SVC1_IDX_LED_STATE_CHAR,
 SVC1_IDX_LED_STATE_VAL,
 SVC1_IDX_LED_STATE_USER_DESC,
 [...]
  1. Open the file user_custs1_def.c. Declare and assign the custom server’s attribute value.

static const uint8_t SVC1_LED_STATE_UUID_128[ATT_UUID_128_LEN]        = DEF_SVC1_LED_STATE_UUID_128;
  1. Add your characteristic declaration, value and description in custom server’s database attributes.

// LED State Characteristic Declaration
[SVC1_IDX_LED_STATE_CHAR]          = {(uint8_t*)&att_decl_char, ATT_UUID_16_LEN, PERM(RD, ENABLE),
                                        0, 0, NULL},

// LED State Characteristic Value
[SVC1_IDX_LED_STATE_VAL]           = {SVC1_LED_STATE_UUID_128, ATT_UUID_128_LEN, PERM(WR, ENABLE) | PERM(WRITE_COMMAND, ENABLE),
                                        DEF_SVC1_LED_STATE_CHAR_LEN, 0, NULL},

// LED State Characteristic User Description
[SVC1_IDX_LED_STATE_USER_DESC]     = {(uint8_t*)&att_desc_user_desc, ATT_UUID_16_LEN, PERM(RD, ENABLE),
                                        sizeof(DEF_SVC1_LED_STATE_USER_DESC) - 1, sizeof(DEF_SVC1_LED_STATE_USER_DESC) - 1,
                                        (uint8_t *) DEF_SVC1_LED_STATE_USER_DESC},

5.3. Adding a GATT command

We will now write the user code which will handle the toggling of the LED state. For your reference, you can take a look at the ble_app_peripheral project, as this implementation is not part of the ble_app_profile project.

  1. Open the file user_custs1_impl.h, located in the ble_app_peripheral/ folder. Define an enumeration which will hold the two states of the LED, on and off.

enum
{
   CUSTS1_LED_OFF = 0,
   CUSTS1_LED_ON,
};
  1. Declare the prototype of the function which will toggle the LED. Note the function arguments’ types, you can read more about them in the Doxygen comments.

/*
****************************************************************************************
 * @brief Led state value write indication handler.
 * @param[in] msgid   Id of the message received.
 * @param[in] param   Pointer to the parameters of the message.
 * @param[in] dest_id ID of the receiving task instance.
 * @param[in] src_id  ID of the sending task instance.
 * @return void
****************************************************************************************
*/
void user_svc1_led_wr_ind_handler(ke_msg_id_t const msgid,
                               struct custs1_val_write_ind const *param,
                               ke_task_id_t const dest_id,
                               ke_task_id_t const src_id);
  1. Go to file user_custs1_impl.c, in which are our server’s handlers for processing BLE events. In there, write a function that will toggle the LED when it has received a Write Command.

void user_svc1_led_wr_ind_handler(ke_msg_id_t const msgid,
                                  struct custs1_val_write_ind const *param,
                                  ke_task_id_t const dest_id,
                                  ke_task_id_t const src_id)
{
    uint8_t val = 0;
    memcpy(&val, &param->value[0], param->length);

    if (val == CUSTS1_LED_ON)
    {
       GPIO_SetActive(GPIO_LED_PORT, GPIO_LED_PIN);
    }
    else if (val == CUSTS1_LED_OFF)
    {
       GPIO_SetInactive(GPIO_LED_PORT, GPIO_LED_PIN);
    }
}
  1. What we need to do now is to combine our handler function with the reception of a request to change the LED state. Go to file user_peripheral.c and find the user_catch_rest_hndl function. In there we will cherry-pick the messages for our LED Characteristic. msgid defines the kind of client request. For changing the state of the LED, this is a Write Command. Cast the message parameters to the appropriate message structure and delegate the message to our handler function.

 switch(msgid)
 {
     case CUSTS1_VAL_WRITE_IND:
     {
         struct custs1_val_write_ind const *msg_param = (struct custs1_val_write_ind const *)(param);

         switch (msg_param->handle)
         {

             case SVC1_IDX_LED_STATE_VAL:
                 user_svc1_led_wr_ind_handler(msgid, msg_param, dest_id, src_id);
                 break;

Compile and run the ble_app_peripheral example. We will now use the LightBlue® application to verify that our server is running properly.