Creating a custom profile using NXP BLE stack – Server

キャンセル
次の結果を表示 
表示  限定  | 次の代わりに検索 
もしかして: 

Creating a custom profile using NXP BLE stack – Server

Creating a custom profile using NXP BLE stack – Server

Bluetooth® Low Energy (or BLE) is a wireless technology that allows the exchange of information between a device that contains data (Server) and a device that requests that data (Client). Servers are usually small battery powered devices connected to sensors or actuators to gather data or perform some actions while clients are usually devices that use that information in a system or for display to a user (most common client devices are the Smartphones).

When creating a custom BLE profile, we need to consider that it will need to be implemented on both Server and Client. Server will include the database of all the information that can be accessed or modified while the Client will require drivers to access and handle the data provided by the server.

This post explains how to implement a custom profile in the server side using the NXP BLE stack. As example, a custom Potentiometer reporter is implemented on a MKW40Z160.

Generic Attribute Profile

Before implementing a custom profile, we need to be familiarized with the way BLE exchanges information. The Generic Attribute Profile (GATT) establishes how to exchange all profile and user data over a BLE connection. All standard BLE profiles are based on GATT and must comply with it to operate correctly.

GATT defines two communication roles: Server and Client.

  • The GATT Server stores the data to be transported and accepts GATT requests, commands and confirmations from the client.
  • The GATT Client accesses data on the remote GATT server via read, write, notify or indicate operations.

pastedImage_2.png

Figure 1 GATT Client-Server

GATT data is exposed using attributes that are organized to describe the information accessible in a GATT server. These are Profile, Service, Characteristic and Descriptor. Profiles are high level definitions that determine the behavior of the application as a whole (i.e. Heart Rate Monitor, or Temperature Sensor). Profiles are integrated by one or more Services that define individual functionalities (i.e. a Heart Rate Monitor requires a Heart Rate Sensor and a Battery Measurement Unit). Services are integrated by one or more characteristics that hold individual measurements, control points or other data for a service (i.e. Heart Rate Sensor might have a characteristic for Heart Rate and other for Sensor Location). Finally Descriptors define how characteristics must be accessed.

pastedImage_5.png

Figure 2 GATT database structure

Adding a New Service to the GATT Database

The GATT database in a server only includes attributes that describe services, characteristics and descriptors. Profiles are implied since they are a set of predefined services. In the NXP Connectivity Software, macros are used to define each of the attributes present in the database in an easier way.

Each service and characteristic in a GATT database has a Universally Unique Identifier (UUID). These UUID are assigned by Bluetooth Org on adopted services and characteristics. When working with custom profiles, a proprietary UUID must be assigned. In the NXP connectivity Software, custom UUIDs are defined in the file gatt_uuid128.h. Each new UUID must be defined using the macro UUID128 (name, bytes) where name is an identifier that will help us to reference the UUID later in the code. Byte is a sequence of 16-bytes (128-bits) which are the custom UUID. Following is an example of the definition of the Potentiometer service and the Potentiometer Relative Value characteristic associated to it.

/* Potentiometer Service */
UUID128(uuid_service_potentiometer, 0xE0, 0x1C, 0x4B, 0x5E, 0x1E, 0xEB, 0xA1, 0x5C, 0xEE, 0xF4, 0x5E, 0xBA, 0x04, 0x56, 0xFF, 0x02)

/* Potentiometer Characteristic */
UUID128(uuid_characteristic_potentiometer_relative_value, 0xE0, 0x1C, 0x4B, 0x5E, 0x1E, 0xEB, 0xA1, 0x5C, 0xEE, 0xF4, 0x5E, 0xBA, 0x04, 0x57, 0xFF, 0x02)
‍‍‍‍‍

Once proper UUIDs have been stablished, the new service must be added to the GATT database. It is defined in the file gatt_db.h. Simple macros are used to include each of the attributes in the proper order. Following code shows the implementation of the potentiometer service in gatt_db file.

PRIMARY_SERVICE_UUID128(service_potentiometer, uuid_service_potentiometer)
    CHARACTERISTIC_UUID128(char_potentiometer_relative_value, uuid_characteristic_potentiometer_relative_value, (gGattCharPropRead_c | gGattCharPropNotify_c))
        VALUE_UUID128(value_potentiometer_relative_value, uuid_characteristic_potentiometer_relative_value, (gPermissionFlagReadable_c ), 1, 0x00)
        CCCD(cccd_potentiometer)
        DESCRIPTOR(cpfd_potentiometer, gBleSig_CharPresFormatDescriptor_d, (gPermissionFlagReadable_c), 7, gCpfdUnsigned8BitInteger, 0x00, 
                   0xAD/*Unit precentage UUID in Little Endian (Lower byte)*/, 
                   0x27/*Unit precentage UUID in Little Endian (Higher byte)*/, 
                   0x01, 0x00, 0x00)
‍‍‍‍‍‍‍‍

PRIMARY_SERVICE_UUID128 (service_name, service_uuid) defines a new service in the GATT database with a custom 128-bit UUID. It requires two parameters; service_name is the name of this service in the code and it is used later during the program implementation. Service_uuid is the identifier for the service UUID previously defined in gatt_uuid128.h.

CHARACTERISTIC_UUID128 (characteristic_name, characteristic_uuid, flags) defines a new characteristic inside the previously defined service with a custom 128-bit UUID. It requires three parameters; characteristic_name is the name of the characteristic in the code, characteristic_uuid is the identifier for the characteristic UUID previously defined in gatt_uuid128.h. Finally, flags is a concatenation of all the characteristic properties (read, write, notify, etc.).

VALUE_UUID128 (value_name, characteristic_uuid, permission_flags, number_of_bytes, initial_values…) defines the value in the database of the previously defined characteristic. Value_name is an identifier used later in the code to read or modify the characteristic value. Characteristic_uuid is the same UUID identifier for the previously defined characteristic. Permission_flags determine how the value can be accessed (read, write or both). Number of bytes define the size of the value followed by the initial value of each of those bytes.

CCCD (cccd_name) defines a new Client Characteristic Configuration Descriptor for the previously defined characteristic. Cccd_name is the name of the CCCD for use later in the code. This value is optional depending on the characteristic flags.

DESCRIPTOR (descriptor_name, descriptor_format, permissions, size, descriptor_bytes…) defines a descriptor for the previously defined characteristic. Descriptor_name defines the name for this descriptor. Descriptor_format determines the type of descriptor. Permissions stablishes how the descriptor is accessed. Finally the size and descriptor bytes are added.

All the macros used to fill the GATT database are properly described in the BLEADG (included in the NXP Connectivity Software documentation) under chapter 7 “Creating a GATT Database”.

Implementing Drivers for New Service

Once the new service has been defined in gatt_db.h, drivers are required to handle the service and properly respond to client requests. To do this, two new files need to be created per every service added to the application; (service name)_service.c and (service name)_interface.h.

The service.c file will include all the functions required to handle the service data, and the interface.h file will include all the definitions used by the application to refer to the recently created service. It is recommended to take an existing file for reference.

Interface header file shall include the following.

  1. Service configuration structure that includes a 16-bit variable for Service Handle and a variable per each characteristic value in the service.
    /*! Potentiometer Service - Configuration */
    typedef struct psConfig_tag
    {
        uint16_t    serviceHandle;                 /*!<Service handle */
        uint8_t     potentiometerValue;            /*!<Input report field */
    } psConfig_t;
    ‍‍‍‍‍‍‍
  2. Function declarations for the start service and stop service functions. These are required to initialize/deinitialize a service.
    bleResult_t Ps_Start(psConfig_t *pServiceConfig);
    bleResult_t Ps_Stop(psConfig_t *pServiceConfig);
    ‍‍‍
  3. Function declarations for subscribe and unsubscribe functions required to subscribe/unsubscribe a specific client to a service.
    bleResult_t Ps_Subscribe(deviceId_t clientDeviceId);
    bleResult_t Ps_Unsubscribe();
    ‍‍‍
  4. Depending on your application, functions to read, write, update a specific characteristic or a set of them.
    bleResult_t Ps_RecordPotentiometerMeasurement (uint16_t serviceHandle, uint8_t newPotentiometerValue);

Service source file shall include the following.

  1. A deviceId_t variable to store the ID for the subscribed client.
    /*! Potentiometer Service - Subscribed Client*/
    static deviceId_t mPs_SubscribedClientId;
    ‍‍‍
  2. Function definitions for the Start, Stop, Subscribe and Unsubscribe functions. The Start function may include code to set an initial value to the service characteristic values.
    bleResult_t Ps_Start (psConfig_t *pServiceConfig)
    {    
        /* Clear subscibed clien ID (if any) */
        mPs_SubscribedClientId = gInvalidDeviceId_c;
        
        /* Set the initial value defined in pServiceConfig to the characteristic values */
        return Ps_RecordPotentiometerMeasurement (pServiceConfig->serviceHandle, 
                                                 pServiceConfig->potentiometerValue);
    }
    
    bleResult_t Ps_Stop (psConfig_t *pServiceConfig)
    {
      /* Unsubscribe current client */
        return Ps_Unsubscribe();
    }
    
    bleResult_t Ps_Subscribe(deviceId_t deviceId)
    {
       /* Subscribe a new client to this service */
        mPs_SubscribedClientId = deviceId;
    
        return gBleSuccess_c;
    }
    
    bleResult_t Ps_Unsubscribe()
    {
       /* Clear current subscribed client ID */
        mPs_SubscribedClientId = gInvalidDeviceId_c;
        return gBleSuccess_c;
    }
    ‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
  3. Definition of the service specific functions. It is, the functions used to write, read or notify characteristic values. Our example only implements two; a public function to update a characteristic value in the GATT database, and a local function to issue a notification with the recently updated value to the client.
    bleResult_t Ps_RecordPotentiometerMeasurement (uint16_t serviceHandle, uint8_t newPotentiometerValue)
    {
        uint16_t  handle;
        bleResult_t result;
    
        /* Get handle of Potentiometer characteristic */
        result = GattDb_FindCharValueHandleInService(serviceHandle,
            gBleUuidType128_c, (bleUuid_t*)&potentiometerCharacteristicUuid128, &handle);
    
        if (result != gBleSuccess_c)
            return result;
    
        /* Update characteristic value */
        result = GattDb_WriteAttribute(handle, sizeof(uint8_t), (uint8_t*)&newPotentiometerValue);
    
        if (result != gBleSuccess_c)
            return result;
    
        Ps_SendPotentiometerMeasurementNotification(handle);
    
        return gBleSuccess_c;
    }
    ‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Previous function first obtains the handle value of the characteristic value we want to modify. Handle values are like an index used by the application to access attributes in the database. The UUID for the Potentiometer Relative Value is used to obtain the proper handle by calling GattDb_FindCharValueHandleInService function. Once handle has been obtained, is used in the GattDb_WriteAttribute function to write the new value into the GATT database and it can be accessed by the client. Finally our second function is called to issue a notification.

static void Ps_SendPotentiometerMeasurementNotification
(
  uint16_t handle
)
{
    uint16_t  hCccd;
    bool_t isNotificationActive;

    /* Get handle of CCCD */
    if (GattDb_FindCccdHandleForCharValueHandle(handle, &hCccd) != gBleSuccess_c)
        return;

    if (gBleSuccess_c == Gap_CheckNotificationStatus
        (mPs_SubscribedClientId, hCccd, &isNotificationActive) &&
        TRUE == isNotificationActive)
    {
        GattServer_SendNotification(mPs_SubscribedClientId, handle);
    }
}
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

SendPotentiometerMeasurementNotification sends a notification to the client. It first obtain the handle value of the CCCD we defined in the GATT database for this characteristic. Then, it checks that the CCCD has been written by the client for notifications. If it has, then it sends the notification so the client can perform a read to the characteristic value.

All the functions used to access the GATT database and use the GATT server are better explained in the BLEADG document under chapters 6 and 7. Also instructions on how to create a custom profile are included in chapter 8. BLEADG is part of the NXP Connectivity Software documentation.

Integrating a New Service to an Existing BLE Project

So far a new service has been created in the database and functions to handle it have been defined. Now this new project must be integrated so it can be managed by the NXP Connectivity Stack.

Folder structure of an NXP Connectivity Software project is divided in five different modules. App includes all the application files. Bluetooth contains files related with BLE communications. Framework contains auxiliary software used by the stack for the handling of memory, low power etcetera. KSDK contains the Kinetis SDK drivers for low level modules (ADC, GPIO…) and RTOS include files associated with the operating system.

pastedImage_10.png

Figure 3 Folder structure

Service files must be added to the project under the Bluetooth folder, inside the profiles sub-folder. A new folder must be created for the service.c file and the interface.h file must be added under the interface sub-folder.

pastedImage_13.png

Figure 4 Service files included

Once the files are included in the project, the service must be initialized in the stack. File app.c is the main application file for the NXP BLE stack. It calls all the BLE initializations and application callbacks. The service_interface.h file must be included in this file.

pastedImage_16.png

Figure 5 Interface header inclusion

Then in the local variables definition, a new service configuration variable must be defined for the new service. The type of this variable is the one defined in the service interface file and must be initialized with the service name (defined in gattdb.h) and the initial values for all the characteristic values.

pastedImage_19.png

Figure 6 Service configuration struct

The service now must be initialized. It is performed inside the BleApp_Config function by calling the Start function for the recently added service.

static void BleApp_Config()
{  
    /* Read public address from controller */
    Gap_ReadPublicDeviceAddress();

    /* Register for callbacks*/
    App_RegisterGattServerCallback(BleApp_GattServerCallback);
   
   .
   .
   .

   mAdvState.advOn = FALSE;
    /* Start services */
    Lcs_Start(&lcsServiceConfig);
    Dis_Start(&disServiceConfig);
    Irs_Start(&irsServiceConfig);
    Bcs_Start(&bcsServiceConfig);
    Ps_Start(&psServiceConfig);
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Finally, subscribe and unsubscribe functions must be added to the proper host callback. In the BleApp_ConnectionCallback function the subscribe function must be called after the gConnEvtConnected_c (device connected) case, and the unsubscribe function must be called after the gConnEvtDisconnected_c (device disconnected) case.

static void BleApp_ConnectionCallback (deviceId_t peerDeviceId, gapConnectionEvent_t* pConnectionEvent)
{
    switch (pConnectionEvent->eventType)
    {
        case gConnEvtConnected_c:
        {

        .
        .
        .

            /* Subscribe client*/
            mPeerDeviceId = peerDeviceId; 
            Lcs_Subscribe(peerDeviceId);
            Irs_Subscribe(peerDeviceId);
            Bcs_Subscribe(peerDeviceId);
            Cts_Subscribe(peerDeviceId);
            Ps_Subscribe(peerDeviceId);
            Acs_Subscribe(peerDeviceId);
            Cps_Subscribe(peerDeviceId);
            Rcs_Subscribe(peerDeviceId);

        .
        .
        .

        case gConnEvtDisconnected_c:
        {
        /* UI */
          Led1Off();
          
          /* Unsubscribe client */
          mPeerDeviceId = gInvalidDeviceId_c;
          Lcs_Unsubscribe();
          Irs_Unsubscribe();
          Bcs_Unsubscribe();
          Cts_Unsubscribe();
          Ps_Unsubscribe();
          Acs_Unsubscribe();
          Cps_Unsubscribe();
          Rcs_Unsubscribe();
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

After this, services can be accessed by a client application.

Handling Notifications and Write Requests

Once the new service has been initialized, it is possible for the client to access GATT database attributes and issue commands (read, write, notify…). Nevertheless, when an attribute is written or a CCCD is set to start notifications, program must be aware of these requests to handle them if required.

Handling Notifications

When a characteristic has been configured as notifiable, the client expects to receive messages from it every time in a while depending on the pre-configured parameters. To indicate this, the client writes the specific CCCD for the characteristic indicating that notifications must start/stop being sent.

When this occurs, BleApp_GattServerCallback is executed in the main program. All the application CCCDs must be monitored when the gEvtCharacteristicCccdWritten_c event is set. This event indicates that a CCCD has been written. A conditional structure must be programmed to determine which CCCD was modified and act accordingly.

static void BleApp_GattServerCallback (deviceId_t deviceId, gattServerEvent_t* pServerEvent)
{
    switch (pServerEvent->eventType)
    {
      case gEvtCharacteristicCccdWritten_c:
        {
            /* 
            Attribute CCCD write handler: Create a case for your registered attribute and
            execute callback action accordingly
            */
            switch(pServerEvent->eventData.charCccdWrittenEvent.handle)
            {
            case cccd_input_report:{
              //Determine if the timer must be started or stopped
              if (pServerEvent->eventData.charCccdWrittenEvent.newCccd){
                // CCCD set, start timer
                TMR_StartTimer(tsiTimerId, gTmrIntervalTimer_c, gTsiUpdateTime_c ,BleApp_TsiSensorTimer, NULL);
#if gAllowUartDebug
                Serial_Print(debugUartId, "Input Report notifications enabled \n\r", gNoBlock_d);
#endif
              }
              else{
                // CCCD cleared, stop timer
                TMR_StopTimer(tsiTimerId);
#if gAllowUartDebug
                Serial_Print(debugUartId, "Input Report notifications disabled \n\r", gNoBlock_d);
#endif
              }
            }
              break;
              
            case cccd_potentiometer:{
              //Determine if the timer must be started or stopped
              if (pServerEvent->eventData.charCccdWrittenEvent.newCccd){
                // CCCD set, start timer
                TMR_StartTimer(potTimerId, gTmrIntervalTimer_c, gPotentiometerUpdateTime_c ,BleApp_PotentiometerTimer, NULL);
#if gAllowUartDebug
                Serial_Print(debugUartId, "Potentiometer notifications enabled \n\r", gNoBlock_d);
#endif
              }
              else{
                // CCCD cleared, stop timer
                TMR_StopTimer(potTimerId);
#if gAllowUartDebug
                Serial_Print(debugUartId, "Potentiometer notifications disabled \n\r", gNoBlock_d);
#endif
              }
            }
              break;
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

In this example, when the gEvtCharacteristicCccdWritten_c is set a switch-case selector is executed to determine the written CCCD. This is done by reading the pServerEvent structure in the eventData.charCccdWrittenEvent.handle field. The obtained handle must be compared with the name of the CCCD defined in gatt_db.h for each notifiable characteristic.

pastedImage_22.png

Figure 7 CCCD name

Once the correct CCCD has been detected, the program must determine if it was set or clear. This is done by reading the pServerEvent structure in the eventData.charCccdWrittenEvent.newCccd and executing an action accordingly. In the example code, a timer is started or stopped. Once this timer reaches its modulo value, a new notification is sent using the Ps_RecordPotentiometerMeasurement function previously defined in the service files (see Implementing Drivers for New Service).

Handling Write Requests

Write request callbacks are not automatically generated like the notification ones. They must be registered during the application initialization. Something to take into account is when this feature is enabled, the written value is not automatically stored in the GATT database. Developers must implement code to do this and perform other application actions if needed.To do this, the GattServer_RegisterHandlesForWriteNotifications function must be called including the handles of all the characteristics that are wanted to generate a callback when written.

* Configure writtable attributes that require a callback action */
    uint16_t notifiableHandleArray[] = {value_led_control, value_buzzer, value_accelerometer_scale, value_controller_command, value_controller_configuration};
    uint8_t notifiableHandleCount = sizeof(notifiableHandleArray)/2;
    bleResult_t initializationResult = GattServer_RegisterHandlesForWriteNotifications(notifiableHandleCount, (uint16_t*)&notifiableHandleArray[0]);
‍‍‍‍

In this example, an array with all the writable characteristics was created. The function that register callbacks requires the quantity of characteristic handles to be registered and the pointer to an array with all the handles.

After a client has connected, the gEvtAttributeWritten_c will be executed inside the function BleApp_GattServerCallback every time one of the configured characteristics has been written. Variable pServerEvent->eventData.attributeWrittenEvent.handle must be read to determine the handle of the written characteristic and perform an action accordingly. Depending on the user application, the GATT database must be updated with the new value. To do this, function GattDb_WriteAttribute must be executed. It is recommended to create a function inside the service.c file that updates the attribute in database.

case gEvtAttributeWritten_c:
        {
            /* 
            Attribute write handler: Create a case for your registered attribute and
            execute callback action accordingly
            */
            switch(pServerEvent->eventData.attributeWrittenEvent.handle){
              case value_led_control:{
                bleResult_t result;
                
                //Get written value
                uint8_t* pAttWrittenValue = pServerEvent->eventData.attributeWrittenEvent.aValue;
                
                //Create a new instance of the LED configurator structure
                lcsConfig_t lcs_LedConfigurator = {
                  .serviceHandle = service_led_control,
                  .ledControl.ledNumber = (uint8_t)*pAttWrittenValue,
                  .ledControl.ledCommand = (uint8_t)*(pAttWrittenValue + sizeof(uint8_t)),
                };
                
                //Call LED update function
                result = Lcs_SetNewLedValue(&lcs_LedConfigurator);
                
                //Send response to client
                BleApp_SendAttWriteResponse(&deviceId, pServerEvent, &result);
                
              }
              break;
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

After all the required actions have been executed, server must send a response to the client. To do this, function GattServer_SendAttributeWrittenStatus is called including the handle and the error code for the client (OK or any other error status).

static void BleApp_SendAttWriteResponse (deviceId_t* pDeviceId, gattServerEvent_t* pGattServerEvent, bleResult_t* pResult){
  attErrorCode_t attErrorCode;
  
  // Determine response to send (OK or Error)
  if(*pResult == gBleSuccess_c)
    attErrorCode = gAttErrCodeNoError_c;
  else{
    attErrorCode = (attErrorCode_t)(*pResult & 0x00FF);
  }
  // Send response to client  
  GattServer_SendAttributeWrittenStatus(*pDeviceId, pGattServerEvent->eventData.attributeWrittenEvent.handle, attErrorCode);
}
‍‍‍‍‍‍‍‍‍‍‍‍

More information on how to handle writable characteristics can be found in the BLEADG Chapter 5 (Included in the NXP Connectivity Software documentation).

References

Bluetooth® Low Energy Application Developer’s Guide (BLEADG)– Included in the NXP Connectivity Software Documentation

FRDM-KW40Z Demo Application - Link

ラベル(3)
コメント

Is there any example with FSCI based client?

評価なし
バージョン履歴
最終更新日:
‎09-10-2020 02:41 AM
更新者: