πŸ“œ ⬆️ ⬇️

Creating Bluetooth profiles in the BLE TI stack



In the first part of the article, we set up development tools, tried to figure out how the code works, how to debug it, but did not write a single line of code. Fix it in the second part. Write your own BLE profile for CC2541.


1. Statement of the problem
')
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),
- getting the state of pressing two buttons,
- reading the state of one of these buttons.

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

2. UUID service and characteristics

As mentioned in the previous section , the 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 need five UUIDs - four for features and one for the service. In order for the UUID to go in order, choose the algorithm "Time / node based". After generation, we get something like this UUID set:


Now everything is ready to start writing the profile code. Let's call our HabrProfile profile and add it to the SimpleBLEPerepherial project.

3. Creating a header file

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

Contents of the habrprofile.h file
#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 encounter is that 15 bytes from our 16 UUIDs are the same. Accordingly, it is reasonable to combine them into a common define, considering, however, the fact that the byte order in Bluetooth is big-endian, and in the UUID record we received is little endian. Therefore, the record of bytes in Define is flipped.
The AddService and RegisterAppCBs functions are used to register the profile in 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 need to make handlers for setting events and reading variables over the protocol, but more on that later. First, mark the profile table in the executable file.

4. Table of services

So, we have four characteristics, two of which can notify the user application of the change in the value of the characteristic. As mentioned in the first part of the article, to initialize one variable to read or write, three entries in the device table are required, for a variable to be notified, four entries, that is, for all profile variables, we need 14 entries, adding an entry to them, declaring the profile, we get 15 entries .

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 the characteristics into variables in the form:
 CONST uint8 HhServUUID[ATT_UUID_SIZE] = { HABR_UUID(HH_SERVICE_UUID) }; 

Next, we define 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 in 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. Let us clarify this difference in our example. In our profile there is a notification from both buttons and there is a possibility to read the state of one of them (the second).
Then for the first setting of the characteristic - GATT_PROP_NOTIFY, but no permission to read or write.
For the second characteristic setting - GATT_PROP_NOTIFY | GATT_PROP_READ, in addition, read permission must be declared in the GATT table of the device (otherwise a callback with a read request will not be called) - GATT_PERMIT_READ.

In more detail - in the full attribute table:

Table of profile attributes, variables 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 called from the main program for:
- profile registration,
- the purpose of the callback function,
- read variables
- records of variables.

To register a profile on the stack, you first need to declare profile callback functions β€” just those functions that are called when an external event occurs β€” a request to read or write a characteristic, as well as a function 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 }; 


Ok, now you can register the profile in the stack and check the correctness of the profile characteristics table. In addition to directly calling GATTServApp_RegisterService, the function of registering a profile on the stack also involves registering a callback function when the connection status changes 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 ); } 


Verify the attribute table. To do this, in SimpleBLEPerepherial.c in the SimpleBLEPeripheral_Init function, call Habr_AddService, having previously added the header header (and not forgetting to add the path to the heading for the compiler, the line "$ PROJ_DIR $ \ .. \ .. \ Profiles \ HabrProfile"). Let us run through the debug board, connect to it via the BLE Device Monitor and check the resulting attribute table:

It is important to verify the UUID, profile composition. If all is well, go ahead.

I will omit the description of the function

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 any reason to dwell on the callback registration function. A little closer look at the function of writing and reading the values ​​of variables, and first of all the function of writing values ​​in the profile. Here you should pay attention to the fact that it is necessary to make a call to GATTServApp_ProcessCharCfg - this function will provide the actual notification.

Things are easy - add functions for handling stack events.
6. BLE stack function callback

As it was said above, the stack events will be handled by three functions - an attribute value read request callback, an attribute record read request callback, a connection status callback.
Teaching a profile to give its characteristics to reading is very simple (especially in our case, when all characteristics are values ​​of the same type of uint8) - for this you need to make sure that we are dealing with the right characteristics. The stack in response from the function receives three values ​​- status, pLen (so it is important to always set the exact value of pLen) and pValue. All three values ​​are passed on and can be obtained by us at 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 - whether everything works correctly (by the way, we expect a read error for the variable of the first button):


The variables are written to the profile in the same way, but in the reading function we grouped the variables β€” here it is not advisable to do this, because I want the callback called by the profile to understand which particular characteristic has been changed. This is achieved by defining the notify variable. If it has been set, then this function will call the function in the user application with the notify parameter.
In addition, in addition to processing the recording of the PWM values, this function turns on (and off) the notification, if the value for the attribute of the configuration of the reported characteristic has been recorded β€” this is achieved by calling the GATTServApp_ProcessCCCWriteReq () function;

Record service characteristics
 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 ); } 



Profile is almost ready. The last thing to add to it is a function that disables the notification of variables when the connection is lost.
The function that turns off the notification when communication 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 user application

Tear off from the periphery and make this scenario:
When setting the value of the PWM1 channel, the same value is passed to us via the variable Button1. In the same way, we associate PWM2 and Button2.
For this we need in the file SimpleBLEPerepherial:
- Announce the callback profile,
- Register it in the profile,
- Implement the algorithm.

Let's start. 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. Anyway, all stack stackbacks are built this way.

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

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


In the hh_WriteAttrCB function, we have already implemented the transmission to the callback of information about which characteristic was written. 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 be sure of the console:

Integration with the controller's periphery is suggested for the reader to do it yourself.
Thanks for attention!

Source: https://habr.com/ru/post/246089/


All Articles