Creating Bluetooth profiles in the TI BLE stack

  • Tutorial


In the first part of the article, we tuned the development tools, tried to figure out how the code works, how and how to debug it, but we did not write a single line of code. Correct this in the second part. We will write our own BLE profile for CC2541.


1. Problem statement

Suppose we need a profile for our device that will support the following functions:
- setting and reading the values ​​of two PWM channels (from 0 to 255),
- receiving the state of pressing two buttons,
- reading the status of one of these buttons.

In fact, we need four characteristics of the profile - two responsible for the state of the PWM channels and two notifying the state of the buttons.

2. UUID of service and characteristics

As stated inof the previous part , a mere mortal cannot use 16-bit service addresses. To do this, at least you need to be an Associated Member Bluetooth SIG. In general, 128-bit UUID services are allocated for us. To generate the UUID of our service, we will use this service .

We will need five UUIDs - four for features and one for service. In order for the UUIDs to go in order, we select the “Time / node based” algorithm. After generation, we get something like this set of UUIDs:


Now everything is ready to start writing profile code. Call our HabrProfile profile and add it to the SimpleBLEPerepherial project.

3. Creating the header file

In the \ Projects \ Profiles folder of the stack, create the HabrProfile folder, and in it the files habrprofile.h and habrprofile.c
Next, add the files to the SimpleBLEPerepherial project in the PROFILES folder.
The header file should contain:
-UUID of the profile and the characteristics that your received
; -Naming profile attributes (to make it convenient to access them from the main program)
-declaring functions for working with characteristics and the profile from an external program
-Defining the type of callback function of the profile call

The contents of the file habrprofile.h
#define HABR_UUID(uuid) 0x66, 0x9a, 0x0c, 0x20, 0x00, 0x08, 0xa9, 0xb4, 0xe4, 0x11, 0xd7, 0x85, uuid, 0x50 , 0x2e, 0x51
#define HH_BUTTON1_STATE_ATTR           0
#define HH_BUTTON2_STATE_ATTR           1
#define HH_CHANNEL1_PWM_ATTR              2
#define HH_CHANNEL2_PWM_ATTR              3
#define HH_SERVICE_UUID                 0x10
#define HH_BUTTON1_STATE_UUID           0x11
#define HH_BUTTON2_STATE_UUID           0x12
#define HH_CHANNEL1_PWM_UUID              0x13
#define HH_CHANNEL2_PWM_UUID              0x14
#define HH_SERVICE                 0x00000001
typedef void (*habrControlCB_t)( uint8 paramID ) ;
typedef struct
{
  habrControlCB_t        pfnHabrCB;  // Called when some att changed
} HarbCBs_t;
extern bStatus_t Habr_AddService();
extern bStatus_t Habr_RegisterAppCBs( HarbCBs_t *appCallbacks );
extern bStatus_t Habr_SetParameter( uint8 param, uint8 len, void *value );
extern bStatus_t Habr_GetParameter( uint8 param, void *value );



The first thing we are faced with is that 15 bytes out of 16 of our UUIDs match. Accordingly, it is reasonable to combine them into a common define, however, given the fact that the byte order in Bluetooth is big-endian, and in the UUID record we received is little endian. Therefore, the byte record in the define is upside down.
The AddService and RegisterAppCBs functions are used to register a profile on the stack and bind the program callback functions to the profile.
The functions SetParameter and GetParameter are needed to control the values ​​of profile characteristics.
In addition, we will need to make handlers for the events of setting and reading variables according to the protocol, but more on that later. First, let's mark the profile table in the executable file.

4. Services table

So, we have four characteristics, two of which can notify the user application about a change in the value of the characteristic. As stated in the first part of the article, to initialize one variable for reading or writing, three records are required in the device table, for the notified variable four, that is, for all profile variables we need 14 records, adding a record declaring the profile to them, we get 15 records .

The most important thing now is to set the device table correctly.
The first thing to do is to form the UUID of the profile and characteristics into variables in the form:
CONST uint8 HhServUUID[ATT_UUID_SIZE] =
{ 
  HABR_UUID(HH_SERVICE_UUID)
};

Next, we define the variables / constants that will be responsible for the parameters of specific characteristics:
static uint8 hhButton1CharProps = GATT_PROP_NOTIFY;                   //определит параметры доступа к переменной
static uint8 hhButton1Value     = 0x00;               //определит значение по умолчанию
static gattCharCfg_t hhButton1Config[GATT_MAX_NUM_CONN];        //параметры уведомления - только для переменных NOTIFY
static uint8 hhButton1UserDesc[]="Button 1 variable\0";                        //текстовое описание характеристики

And we enter the characteristics into the gatt table of the device characteristics (an array of type gattAttribute_t) in the form:
{
  gattAttrType_t type;   //  содержит длину handle и указатель на UUID атрибута.
  uint8 permissions;   // права доступа к атрибуту.
  uint16 handle;       // устанавливается стеком самостоятельно - писать 0. 
  uint8* const pValue; // значение (до 512 байт).
} 

This creates a little confusion. On the one hand, we have a variable that defines the access rights to the characteristic (in the previous listing - GATT_PROP_NOTIFY). On the other hand, there is an entry responsible for access rights to the attribute. We clarify this difference in our example. In our profile there is a notification from both buttons and it is possible to read the status of one of them (second).
Then for the first setting of the characteristic - GATT_PROP_NOTIFY, but there is no permission to read or write.
For the second setting of the characteristic - GATT_PROP_NOTIFY | GATT_PROP_READ, in addition, read permission must be declared in the GATT table of the device (otherwise the callback with the read request will not be called) - GATT_PERMIT_READ.

In more detail - in the full table of attributes:

Table of profile attributes, inclusions and variable declarations
#include "bcomdef.h"
#include "OSAL.h"
#include "linkdb.h"
#include "att.h"
#include "gatt.h"
#include "gatt_uuid.h"
#include "gattservapp.h"
#include "habrprofile.h"
#include "OSAL_Clock.h"
#define SERVAPP_NUM_ATTR_SUPPORTED        15
#define UUID_SIZE 16
CONST uint8 hhServUUID[ATT_UUID_SIZE] =
{ 
  HABR_UUID(HH_SERVICE_UUID)
};
CONST uint8 hhButton1UUID[ATT_UUID_SIZE] =
{ 
  HABR_UUID(HH_BUTTON1_STATE_UUID)
};
CONST uint8 hhButton2UUID[ATT_UUID_SIZE] =
{ 
  HABR_UUID(HH_BUTTON2_STATE_UUID)
};
CONST uint8 hhPWM1UUID[ATT_UUID_SIZE] =
{ 
  HABR_UUID(HH_CHANNEL1_PWM_UUID)
};
CONST uint8 hhPWM2UUID[ATT_UUID_SIZE] =
{ 
  HABR_UUID(HH_CHANNEL2_PWM_UUID)
};
static HarbCBs_t *habrahabrAppCBs_t = NULL;
//attribute definitions
static CONST gattAttrType_t hhService = {ATT_UUID_SIZE, hhServUUID};
static uint8 hhButton1CharProps = GATT_PROP_NOTIFY;
static uint8 hhButton1Value     = 0x00;
static gattCharCfg_t hhButton1Config[GATT_MAX_NUM_CONN];
static uint8 hhButton1UserDesc[]="Button 1 variable\0";
static uint8 hhButton2CharProps = GATT_PROP_NOTIFY|GATT_PROP_READ;
static uint8 hhButton2Value     = 0x00;
static gattCharCfg_t hhButton2Config[GATT_MAX_NUM_CONN];
static uint8 hhButton2UserDesc[]="Button 2 variable\0";
static uint8 hhPWM1CharProps = GATT_PROP_READ | GATT_PROP_WRITE;
static uint8 hhPWM1Value = 0x00;
static uint8 hhPWM1UserDesc[] = "PWM 1 variable\0";
static uint8 hhPWM2CharProps = GATT_PROP_READ | GATT_PROP_WRITE;
static uint8 hhPWM2Value = 0x00;
static uint8 hhPWM2UserDesc[] = "PWM 2 variable\0";
//attribute table
static gattAttribute_t HabrProfileAttrTable[15]={
  //Service
  {
      { ATT_BT_UUID_SIZE, primaryServiceUUID }, 
      GATT_PERMIT_READ,                   
      0,                                  
      (uint8 *)&hhServUUID
  },
    //Button1
  { 
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ, 
      0,
      &hhButton1CharProps 
   },
   { 
       {UUID_SIZE, hhButton1UUID },
       0,
       0, 
       (uint8 *)&hhButton1Value
   },
   {
       {ATT_BT_UUID_SIZE , clientCharCfgUUID},
       GATT_PERMIT_READ | GATT_PERMIT_WRITE,
       0,
       (uint8 *)hhButton1Config
   },
   { 
        { ATT_BT_UUID_SIZE, charUserDescUUID },
        GATT_PERMIT_READ, 
        0, 
        hhButton1UserDesc
    } ,
    //Button2
   { 
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ, 
      0,
      &hhButton2CharProps 
   },
   { 
       {UUID_SIZE, hhButton2UUID },
       GATT_PERMIT_READ,
       0, 
       (uint8 *)&hhButton2Value
   },
   {
       {ATT_BT_UUID_SIZE , clientCharCfgUUID},
       GATT_PERMIT_READ | GATT_PERMIT_WRITE,
       0,
       (uint8 *)hhButton2Config
   },
   { 
        { ATT_BT_UUID_SIZE, charUserDescUUID },
        GATT_PERMIT_READ, 
        0, 
        hhButton2UserDesc
    } ,
    //PWM channel 1
     { 
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ, 
      0,
      &hhPWM1CharProps 
   },
   { 
        {UUID_SIZE, hhPWM1UUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE, 
        0,
        (uint8*)&hhPWM1Value 
      },
   { 
        { ATT_BT_UUID_SIZE, charUserDescUUID },
        GATT_PERMIT_READ, 
        0, 
        hhPWM1UserDesc
    } ,
    //PWM channel 2
      { 
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ, 
      0,
      &hhPWM2CharProps 
   },
   { 
     {UUID_SIZE, hhPWM2UUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE, 
        0,
        (uint8*)&hhPWM2Value 
    },
   { 
        { ATT_BT_UUID_SIZE, charUserDescUUID },
        GATT_PERMIT_READ, 
        0, 
        hhPWM2UserDesc
    }
};



5. Custom Functions

The next step is to describe the functions that are called from the main program for:
- registering a profile,
- assigning a callback function,
- reading variables,
- writing variables.

To register a profile on the stack, first of all, you need to declare the profile callback functions - exactly those functions that are called when an external event occurs - a request to read or write a characteristic, as well as a function that is called when the connection status changes.

static uint8 hh_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr, 
                           uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen );
static bStatus_t hh_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr,
                                    uint8 *pValue, uint8 len, uint16 offset );
static void hh_HandleConnStatusCB( uint16 connHandle, uint8 changeType );
CONST gattServiceCBs_t  HH_CBs =
{
  hh_ReadAttrCB,  // Read callback function pointer
  hh_WriteAttrCB, // Write callback function pointer
  NULL               
};


Okay, now you can register the profile on the stack and check the correctness of the profile characteristics table. The function of registering a profile on the stack implies, in addition to directly calling GATTServApp_RegisterService, also registering a callback function when changing the connection status and initializing configuration variables (for those characteristics that should be notified):
bStatus_t Habr_AddService()
{
  uint8 status = SUCCESS;
  GATTServApp_InitCharCfg( INVALID_CONNHANDLE, hhButton1Config ); 
  GATTServApp_InitCharCfg( INVALID_CONNHANDLE, hhButton2Config );
  VOID linkDB_Register( hh_HandleConnStatusCB );  
  status = GATTServApp_RegisterService(HabrProfileAttrTable, GATT_NUM_ATTRS(HabrProfileAttrTable), &HH_CBs );
  return ( status );
}


Check the attribute table. To do this, in SimpleBLEPerepherial.c in the SimpleBLEPeripheral_Init function, call Habr_AddService, after adding the inclusion header (and not forgetting to add the path to the header for the compiler - the line "$ PROJ_DIR $ \ .. \ .. \ Profiles \ HabrProfile"). We’ll sew through the debug board, connect to it through BLE Device Monitor and check the resulting attribute table:

It is important to verify the UUID, the composition of the profile. If all is well, move on.

I will omit the function description

Functions for reading and writing variables from a user application
bStatus_t Habr_RegisterAppCBs( HarbCBs_t *appCallbacks ){
  if ( appCallbacks )
  {
    habrahabrAppCBs_t = appCallbacks;
    return ( SUCCESS );
  }
  else
  {
    return ( bleAlreadyInRequestedMode );
  }
}
bStatus_t Habr_SetParameter( uint8 param, uint8 len, void *value ){
    bStatus_t ret = SUCCESS;
    switch ( param )
    {
    case HH_BUTTON1_STATE_ATTR:
      if(len == sizeof(uint8))
      {
        hhButton1Value = *((uint8*)value);
        GATTServApp_ProcessCharCfg (hhButton1Config, (uint8 *)&hhButton1Value, FALSE, HabrProfileAttrTable 
                                    ,GATT_NUM_ATTRS(HabrProfileAttrTable), INVALID_TASK_ID);
      }
      else{
        ret = bleInvalidRange;
      }
      break;
    case HH_BUTTON2_STATE_ATTR:
      if(len == sizeof(uint8))
      {
        hhButton2Value = *((uint8*)value);
        GATTServApp_ProcessCharCfg (hhButton2Config, (uint8 *)&hhButton2Value, FALSE, HabrProfileAttrTable 
                                    ,GATT_NUM_ATTRS(HabrProfileAttrTable), INVALID_TASK_ID);
      }
      else{
        ret = bleInvalidRange;
      }
      break;
    case HH_CHANNEL1_PWM_ATTR:
      if(len == sizeof(uint8))
      {
          hhPWM1Value = *((uint8*)value);
      }
      else{
        ret = bleInvalidRange;
      }
      break;
    case HH_CHANNEL2_PWM_ATTR:
      if(len == sizeof(uint8))
      {
          hhPWM2Value = *((uint8*)value);
      }
      else{
        ret = bleInvalidRange;
      }
      break;
    default:
       ret = INVALIDPARAMETER;
      break;
    } 
   return(ret);
}



I don’t see the point in stopping the callback registration function. Let's take a closer look at the functions of writing and reading the values ​​of variables, and first of all the function of writing values ​​to the profile. Here it is worth paying attention to the fact that it is necessary to make a call to GATTServApp_ProcessCharCfg - this function will provide the notification itself.

The small thing is to add functions for processing stack events.
6. Callback of the function of the BLE stack.

Processing the events of the stack, as mentioned above, will be carried out by three functions - the callback request for reading the attribute value, the callback request for reading the attribute record, the connection status callback.
To teach a profile to give its characteristics to reading is very simple (especially in our case, when all the characteristics are values ​​of the same uint8 type) - for this you need to make sure that we are dealing with the correct characteristics. The stack receives three values ​​in response from the function - status, pLen (so it is always important to set the exact value of pLen) and pValue. All three values ​​are passed on and can be obtained by us on the receiving side.

Read Service Characteristics
static uint8 hh_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr, uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen )
{
    bStatus_t status = SUCCESS;
    if ( offset > 0 )
    {
      return ( ATT_ERR_ATTR_NOT_LONG );
    }
    if ( pAttr->type.len == ATT_UUID_SIZE )
    {    
      // 128-bit UUID
      uint8 uuid[ATT_UUID_SIZE];
      osal_memcpy(uuid, pAttr->type.uuid, ATT_UUID_SIZE);
      if(osal_memcmp(uuid,hhPWM2UUID,ATT_UUID_SIZE)||
       osal_memcmp(uuid,hhPWM1UUID,ATT_UUID_SIZE)||
       osal_memcmp(uuid,hhButton2UUID,ATT_UUID_SIZE)||
         osal_memcmp(uuid,hhButton1UUID,ATT_UUID_SIZE))
      {
       *pLen = 1;
        pValue[0] = *pAttr->pValue;
      }
    } 
    else
    {
    // 16-bit UUID
      *pLen = 0;
      status = ATT_ERR_INVALID_HANDLE;
    }
  return ( status );
}
}



At the same time, we check the reading of the characteristics - does everything work correctly (by the way, we expect a reading error for the variable of the first button):


Writing variables to the profile occurs in a similar way, however, we grouped the variables in the reading function - this is not advisable here, because I want the profile called the callback understood which particular characteristic was changed. This is achieved by defining the variable notify. If it was installed, then in this function it will call the function in the user application with the notify parameter.
In addition, in addition to processing the recording of PWM values, this function enables (and turns off) the notification if a value has been recorded for the configuration attribute of the notified characteristic - this is achieved by calling the GATTServApp_ProcessCCCWriteReq () function;

Record service features
static bStatus_t hh_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr,
                                uint8 *pValue, uint8 len, uint16 offset ){
bStatus_t status = SUCCESS;
     uint8 notify = 0xFF;
     if ( pAttr->type.len == ATT_UUID_SIZE )
    {
        const uint8 uuid[ATT_UUID_SIZE] = {
          HABR_UUID(pAttr->type.uuid[12])
        };
        if(osal_memcmp(uuid,hhPWM1UUID,ATT_UUID_SIZE))
        {
            if ( offset == 0 )
            {
              if ( len != 1 ){
                status = ATT_ERR_INVALID_VALUE_SIZE;
                }
            }
            else
            {
                status = ATT_ERR_ATTR_NOT_LONG;
            }
            if ( status == SUCCESS )
            {
              uint8 *pCurValue = (uint8 *)pAttr->pValue;
              *pCurValue = pValue[0];
              notify = HH_CHANNEL1_PWM_ATTR;        
        }
        }
            else if(osal_memcmp(uuid,hhPWM2UUID,ATT_UUID_SIZE)){
                  if ( offset == 0 )
                {
                  if ( len != 1 ){
                    status = ATT_ERR_INVALID_VALUE_SIZE;
                    }
                }
                else
                {
                    status = ATT_ERR_ATTR_NOT_LONG;
                }
                if ( status == SUCCESS )
                {
                  uint8 *pCurValue = (uint8 *)pAttr->pValue;
                  *pCurValue = pValue[0];
                  notify = HH_CHANNEL2_PWM_ATTR;
            }
      }
    }
   else if (pAttr->type.len== ATT_BT_UUID_SIZE)
  {
    uint16 uuid= BUILD_UINT16(pAttr->type.uuid[0],pAttr->type.uuid[1]);
    switch(uuid){
    case GATT_CLIENT_CHAR_CFG_UUID:
      status=GATTServApp_ProcessCCCWriteReq(connHandle, pAttr, pValue, len, offset, GATT_CLIENT_CFG_NOTIFY);
      break;
    default:
      status = ATT_ERR_ATTR_NOT_FOUND;
    }
  }
  else{
    status = ATT_ERR_INVALID_HANDLE;
  }
  // If an attribute changed then callback function to notify application of change
  if ( (notify != 0xFF) && habrahabrAppCBs_t && habrahabrAppCBs_t->pfnHabrCB )
    habrahabrAppCBs_t->pfnHabrCB(notify);  
   return ( status );                               
}



The profile is almost ready. The last thing you need to add to it is a function that will turn off the notification of variables when the connection is lost.
A function that turns off notification when a connection is lost
static void hh_HandleConnStatusCB( uint16 connHandle, uint8 changeType ){
if ( connHandle != LOOPBACK_CONNHANDLE )
  {
    if ( ( changeType == LINKDB_STATUS_UPDATE_REMOVED )      ||
         ( ( changeType == LINKDB_STATUS_UPDATE_STATEFLAGS ) && 
           ( !linkDB_Up( connHandle ) ) ) )
    { 
      GATTServApp_InitCharCfg ( connHandle, hhButton1Config);
      GATTServApp_InitCharCfg ( connHandle, hhButton2Config);
    }
  }
}


Profile is ready! Now make sure that it works correctly.

7. Communication with the user application

Let us tear ourselves away from the periphery and make the following scenario:
When setting the value of the PWM1 channel, the same value is passed to us through the Button1 variable. In the same way, we assign PWM2 and Button2.
To do this, we need in the SimpleBLEPerepherial file:
- Declare the profile callback,
- Register it in the profile,
- Implement the algorithm.

Let's get started. We will declare the actual callback and the structure that will be registered for the execution of the callback. At first glance, such a record may seem too complicated, but if we need to build a profile with several callbacks (for example, if we want to add a notification about reading a variable), this approach will more than justify itself. And indeed, all the stack callbacks are built in this way.

static void habrProfileCB (uint8 paramID);
static HarbCBs_t HabrProfCBStruct =
{
  habrProfileCB          // Characteristic value change callback
};

In the body of the SimpleBLEPeripheral_Init function, register this structure in the profile:
Habr_AddService();
Habr_RegisterAppCBs(&HabrProfCBStruct);


In the hh_WriteAttrCB function, we have already implemented the transfer to the callback of information about which characteristic was recorded. It is only a matter of processing this information now:
static void habrProfileCB (uint8 paramID){
  uint8 u8buffer;
  switch(paramID){
  case HH_CHANNEL1_PWM_ATTR:
    Habr_GetParameter(HH_CHANNEL1_PWM_ATTR, &u8buffer);
    Habr_SetParameter(HH_BUTTON1_STATE_ATTR, sizeof(uint8), &u8buffer);
    break;
  case HH_CHANNEL2_PWM_ATTR:
    Habr_GetParameter(HH_CHANNEL2_PWM_ATTR, &u8buffer);
    Habr_SetParameter(HH_BUTTON2_STATE_ATTR, sizeof(uint8), &u8buffer);
    break;
  default:
    break;
  }
}

Finally, check that everything works. It really works - you can see the console:

Integration with the peripherals of the controller, the reader is invited to do it yourself.
Thanks for attention!

Also popular now: