Modify and read characteristics with read and write requests

Document created by Pablo Noriega Navarro Employee on Jul 19, 2016Last modified by Pablo Noriega Navarro Employee on Jul 28, 2016
Version 1Show Document
  • View in full screen mode

Bluetooth Low Energy, through the Generic Attribute Profile (GATT), supports various ways to send and receive data between clients and servers. Data can be transmitted through indications, notifications, write requests and read requests. Data can also be transmitted through the Generic Access Profile (GAP) by using broadcasts. Here however, I'll focus on write and read requests.


Write and read requests are made by a client to a server, to ask for data (read request) or to send data (write request). In these cases, the client first makes the request, and the server then responds, by either acknowledging the write request (and thus, writing the data) or by sending back the value requested by the client.

 

To be able to make write and read requests, we must first understand how BLE handles the data it transmits. To transmit data back and forth between devices, BLE uses the GATT protocol. The GATT protocol handles data using a GATT database. A GATT database implements profiles, and each profile is made from a collection of services. These services each contain one or more characteristics. A BLE characteristic is made of attributes. These attributes constitute the data itself, and the handle to reference, access or modify said data.

 

To have a characteristic that is able to be both written and read, it must be first created. This is done precisely in the GATT database file ( gatt_db.h ):

/* gatt_db.h */

/* Custom service*/ 
PRIMARY_SERVICE_UUID128(service_custom, uuid_custom_service)

    /* Custom characteristic with read and write properties */
    CHARACTERISTIC_UUID128(char_custom, uuid_custom_char, (gGattCharPropRead_c | gGattCharPropWrite_c))

        /* Custom length attribute with read and write permissions*/
        VALUE_UUID128_VARLEN(value_custom, uuid_custom_char, (gPermissionFlagReadable_c | gPermissionFlagWritable_c), 50, 1, 0x00)

 

The custom UUIDs are defined in the gatt_uuid128.h file:

/* gatt_uuid128.h */

/* Custom 128 bit UUIDs*/ 
UUID128(uuid_custom_service, 0xE0, 0x1C, 0x4B, 0x5E, 0x1E, 0xEB, 0xA1, 0x5C, 0xEE, 0xF4, 0x5E, 0xBA, 0x00, 0x01, 0xFF, 0x01)
UUID128(uuid_custom_char, 0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x17, 0x28, 0x39, 0x4A, 0x5B, 0x6C, 0x7D, 0x8E, 0x9F, 0x00)

 

With this custom characteristic, we can write and read a value of up to 50 bytes (as defined by the variable length value declared in the gatt_db.h file, see code above). Remember that you also need to implement the interface and functions for the service. For further information and guidance in how to make a custom profile, please refer to the BLE application developer's guide (BLEDAG.pdf, located in <KW40Z_connSw_install_dir>\ConnSw\doc\BLEADG.pdf.

 

Once a connection has been made, and you've got two (or more) devices connected, read and write requests can be made. I'll first cover how to make a write and read request from the client side, then from the server side.

 

Client

 

To make a write request to a server, you'll need to have the handle for the characteristic you want to modify. This handle should be stored once the characteristic discovery is done. Obviously, you also need the data that is going to be written.

 

The following function needs a pointer to the data and the size of the data. It also uses the handle to tell the server what characteristic is going to be written:

static void SendWriteReq(uint8_t* data, uint8_t dataSize)
{   
    gattCharacteristic_t characteristic;
    characteristic.value.handle = charHandle;     // Previously stored characteristic handle

    GattClient_WriteCharacteristicValue( mPeerInformation.deviceId, &characteristic,
                                         dataSize, data, FALSE,
                                         FALSE, FALSE, NULL);
}


uint8_t wdata[15] = {"Hello world!\r"};
uint8_t size = sizeof(wdata);

SendWriteReq(wdata, size);

 

The data is send with the GattClient_WriteCharacteristicValue() API. This function has various configurable parameters to establish how to send the data. The function's parameters are described with detail on the application developer's guide, but basically, you can determine whether you need or not a response for the server, whether the data is signed or not, etc.

 

Whenever a client makes a read or write request to the server, there is a callback procedure triggered,  to which the program then goes. This callback function has to be registered though. You can register the client callback function using the App_RegisterGattClientProcedureCallback() API:

App_RegisterGattClientProcedureCallback(gattClientProcedureCallback);

void gattClientProcedureCallback ( deviceId_t deviceId,
                                   gattProcedureType_t procedureType,
                                   gattProcedureResult_t procedureResult,
                                   bleResult_t error )
{
  switch (procedureType)
  {
       /* ... */
       case gGattProcWriteCharacteristicValue_c:
            if (gGattProcSuccess_c == procedureResult)
            {
                 /* Continue */
            }
            else
            {
                 /* Handle error */
            }
            break;
       /* ... */
  }
}

 

Reading an attribute is somewhat similar to writing an attribute, you still need the handle for the characteristic, and a buffer in which to store the read value:

#define size 17

static void SendReadReq(uint8_t* data, uint8_t dataSize)
{
    /* Memory has to be allocated for the characteristic because the
       GattClient_ReadCharacteristicValue() API runs in a different task, so
       it has a different stack. If memory were not allocated, the pointer to
       the characteristic would point to junk. */

    characteristic = MEM_BufferAlloc(sizeof(gattCharacteristic_t));
    data = MEM_BufferAlloc(dataSize);
    
    characteristic->value.handle = charHandle;
    characteristic->value.paValue = data;

    bleResult_t result = GattClient_ReadCharacteristicValue(mPeerInformation.deviceId, characteristic, dataSize);
}

uint8_t rdata[size];
        
SendReadReq(rdata, size);

As mentioned before, a callback procedure is triggered whenever there is a write or read request. This is the same client callback procedure used for the write request, but the event generates a different procedure type:

void gattClientProcedureCallback ( deviceId_t deviceId,
                                   gattProcedureType_t procedureType,
                                   gattProcedureResult_t procedureResult,
                                   bleResult_t error )
{
  switch (procedureType)
  {
       /* ... */
       case gGattProcReadCharacteristicValue_c:
            if (gGattProcSuccess_c == procedureResult)
            {
                 /* Read value length */
                 PRINT(characteristic.value.valueLength);
                 /* Read data */
                 for (uint16_t j = 0; j < characteristic.value.valueLength; j++)
                 {
                      PRINT(characteristic.value.paValue[j]);
                 }
            }
            else
            {
              /* Handle error */
            }
            break;
      /* ... */
  }
}

 

There are some other methods to read an attribute. For further information, refer to the application developer's guide chapter 5, section 5.1.4 Reading and Writing Characteristics.

 

Server

 

Naturally, every time there is a request to either read or write by a client, there must be a response from the server. Similar to the callback procedure from the client, with the server there is also a callback procedure triggered when the client makes a request. This callback function will handle both the write and read requests, but the procedure type changes. This function should also be registered using the  App_RegisterGattServerCallback() API.

 

When there is a read request from a client, the server responds with the read status:

App_RegisterGattServerCallback( gattServerProcedureCallback );

void gattServerProcedureCallback ( deviceId_t deviceId,
                                   gattServerEvent_t* pServerEvent )
{
    switch (pServerEvent->eventType)
    {
        /* ... */
        case gEvtAttributeRead_c:

            GattServer_SendAttributeReadStatus(deviceId, value_custom, gAttErrCodeNoError_c);
                
            break;
        /* ... */
    }
}

 

When there is a write request however, the server should write the received data in the corresponding attribute in the GATT database. To do this, the function GattDb_WriteAttribute() can be used:

void gattServerProcedureCallback ( deviceId_t deviceId,
                                   gattServerEvent_t* pServerEvent )
{
    switch (pServerEvent->eventType)
    {
        /* ... */
        case gEvtAttributeWritten_c:

            if (pServerEvent->eventData.attributeWrittenEvent.handle == value_custom)
            {
                GattDb_WriteAttribute( pServerEvent->eventData.attributeWrittenEvent.handle,
                                       pServerEvent->eventData.attributeWrittenEvent.cValueLength,
                                       pServerEvent->eventData.attributeWrittenEvent.aValue );
             
                GattServer_SendAttributeWrittenStatus(deviceId, value_custom, gAttErrCodeNoError_c);
            }
            break;
        /* ... */
    }
}

 

If you do not register the server callback function, the attribute can still be written in the GATT database (it is actually done automatically), however, if you want something else to happen when you receive a request (turning on a LED, for example), you will need the server callback procedure.

3 people found this helpful

Attachments

    Outcomes