Introduction
The MTU (Maximum Transmission Unit) in Bluetooth LE, is an informational parameter that indicates to the remote device, the maximum number of bytes that the local can handle in such channel, for example, the ATT_MTU for KW36 is fixed in 247 bytes. A few applications require to have long characteristics defined in the GATT database, and sometimes the length of the characteristic exceeds the MTU negotiated by the client and server Bluetooth LE devices. For this scenario, the Bluetooth LE specification defines a procedure to write and read the characteristic of interest. In summary, it consists in perform multiple writes and reads on the same characteristic value, using specific commands. For the "write long characteristic value" procedure, these commands are ATT_PREPARE_WRITE_REQ and ATT_EXECUTE_WRITE_REQ. For the "read long characteristic value" procedure, these commands are ATT_READ_REQ and ATT_READ_BLOB_REQ. This document provides an example of how to write and read long characteristic values, from the perspective of Client and Server devices.
APIs to Write and Read Characteristic Values
Write Characteristic Values
The GattClient_WriteCharacteristicValue API is used to perform any write operation. It is implemented by the GATT Client device.
The following table describes the input parameters.
Input Parameters
Description
deviceId_t deviceId
Device ID of the peer device.
gattCharacteristic_t * pCharacteristic
Pointer to a gattCharacteristic struct type. This struct must contain a valid handle of the characteristic value in the "value.handle" field. The handle of the characteristic value that you want to write is commonly obtained after the service discovery procedure.
uint16_t valueLength
This value indicates the length of the array pointed by aValue.
const uint8_t * aValue
Pointer to an array containing the value that will be written to the GATT database.
bool_t withoutResponse
If true, it means that the application wishes to perform a "Write Without Response", in other words, when the command will be ATT_WRITE_CMD or ATT_SIGNED_WRITE_CMD.
bool_t signedWrite
If withoutResponse and signedWrite are both true, the command will be ATT_SIGNED_WRITE_CMD. If withoutResponse is false, this parameter is ignored.
bool_t doReliableLongCharWrites
This field must be set to true if the application needs to write a long characteristic value.
const uint8_t * aCsrk
If withoutResponse and signedWrite are both true, this pointer must contain the CSRK to sign the data. Otherwise, this parameter is ignored.
Read Characteristic Values
The GattClient_ReadCharacteristicValue API is used to perform read operations. It is implemented by the GATT Client device.
The following table describes the input parameters.
Input Parameters
Description
deviceId_t deviceId
Device ID of the peer device.
gattCharacteristic_t * pIoCharacteristic
Pointer to a gattCharacteristic struct type. This struct must contain a valid handle of the characteristic value in the "value.handle" field. The handle of the characteristic value that you want to write is commonly obtained after the service discovery procedure. As well, the "value.paValue" field of this struct, must point to an array which will contain the characteristic value read from the peer.
unit16_t maxReadBytes
The length of the characteristic value that should be read.
This API takes care of the long characteristics, so there is no need to worry about a special parameter or configuration.
The following sections provide a functional example of how to write and read long characteristics. This example was based on the temperature collector and temperature sensor SDK examples. The example also shows how to create a custom service at the GATT database and how to discover its characteristics.
Bluetooth LE Server (Temperature Sensor)
Modifications in gatt_uuid128.h
Define the 128 bit UUID of the "custom service" which will be used for this example. Add the following code:
/* Custom service */
UUID128(uuid_service_custom, 0xE0, 0x1C, 0x4B, 0x5E, 0x1E, 0xEB, 0xA1, 0x5C, 0xEE, 0xF4, 0x5E, 0xBA, 0x00, 0x01, 0xFF, 0x01)
UUID128(uuid_char_custom, 0xE0, 0x1C, 0x4B, 0x5E, 0x1E, 0xEB, 0xA1, 0x5C, 0xEE, 0xF4, 0x5E, 0xBA, 0x01, 0x01, 0xFF, 0x01)
Modifications in gatt_db.h
Define the characteristics of the "custom service", for this example, our service will have just one characteristic, it can be written or read, and it has a variable-length limited to 400 bytes (remember that the ATT_MTU of KW36 is 247 byte, so with this length, we ensure long writes and reads). Add the following code:
PRIMARY_SERVICE_UUID128(service_custom, uuid_service_custom)
CHARACTERISTIC_UUID128(char_custom, uuid_char_custom, (gGattCharPropWrite_c | gGattCharPropRead_c))
VALUE_UUID128_VARLEN(value_custom, uuid_char_custom, (gPermissionFlagWritable_c | gPermissionFlagReadable_c), 400, 1)
Modifications in app_preinclude.h
One of the most important considerations to write and read long characteristics is the memory allocation needed for this. You must increment the current "AppPoolsDetails_c" configuration, the "_block_size_" and "_number_of_blocks_". Please ensure that "_block_size_" is aligned with 4 bytes. Once you have found the configuration that works in your application, please follow the steps in Memory Pool Optimizer on MKW3xA/KW3xZ Application Note, to found the best configuration without waste memory resources. For this example, configure "AppPoolsDetails_c" as follows:
/* Defines pools by block size and number of blocks. Must be aligned to 4 bytes.*/
#define AppPoolsDetails_c \
_block_size_ 264 _number_of_blocks_ 8 _eol_
Bluetooth LE Client (Temperature Collector)
Modifications in gatt_uuid128.h
Define the 128 bit UUID of the "custom service" which will be used for this example. Add the following code:
/* Custom service */
UUID128(uuid_service_custom, 0xE0, 0x1C, 0x4B, 0x5E, 0x1E, 0xEB, 0xA1, 0x5C, 0xEE, 0xF4, 0x5E, 0xBA, 0x00, 0x01, 0xFF, 0x01)
UUID128(uuid_char_custom, 0xE0, 0x1C, 0x4B, 0x5E, 0x1E, 0xEB, 0xA1, 0x5C, 0xEE, 0xF4, 0x5E, 0xBA, 0x01, 0x01, 0xFF, 0x01)
Modifications in temperature_collector.c
1. Define the following variables at the "Private type definitions" section:
typedef struct customServiceConfig_tag
{
uint16_t hService;
uint16_t hCharacteristic;
} customServiceConfig_t;
typedef struct appCustomInfo_tag
{
tmcConfig_t tempClientConfig;
customServiceConfig_t customServiceClientConfig;
}appCustomInfo_t;
typedef enum
{
mCustomServiceWrite = 0,
mCustomServiceRead
}customServiceState_t;
2. Add two arrays of 400 bytes, one to send and the other to receive the data from the server in "Private memory declarations" section:
/* Dummy array for custom service */
uint8_t mWriteDummyArray[400];
uint8_t mReadDummyArray[400];
3. Define a new function in "Private functions prototypes" section, to write and read the characteristic value:
static void BleApp_SendReceiveCustomService (customServiceState_t state);
4. Locate the "BleApp_Config" function, add the following code here to fill the "mWriteDummyArray" with a known pattern before to write our custom characteristic.
static void BleApp_Config(void)
{
uint16_t fill_pattern;
/* Fill pattern to write long characteristic */
for (fill_pattern = 0; fill_pattern<400; fill_pattern++)
{
mWriteDummyArray[fill_pattern] = (uint8_t)fill_pattern;
}
/* Configure as GAP Central */
BleConnManager_GapCommonConfig();
...
...
}
5. Locate the "BleApp_StoreServiceHandles" function. Modify this function to include our custom service in the service discovery procedure. This is to save the handle of the custom characteristic since it is used by GattClient_WriteCharacteristicValue and GattClient_ReadCharacteristicValue APIs.
static void BleApp_StoreServiceHandles
(
gattService_t *pService
)
{
uint8_t i,j;
if ((pService->uuidType == gBleUuidType128_c) &&
FLib_MemCmp(pService->uuid.uuid128, uuid_service_temperature, 16))
{
/* Found Temperature Service */
mPeerInformation.customInfo.tempClientConfig.hService = pService->startHandle;
for (i = 0; i < pService->cNumCharacteristics; i++)
{
if ((pService->aCharacteristics[i].value.uuidType == gBleUuidType16_c) &&
(pService->aCharacteristics[i].value.uuid.uuid16 == gBleSig_Temperature_d))
{
/* Found Temperature Char */
mPeerInformation.customInfo.tempClientConfig.hTemperature = pService->aCharacteristics[i].value.handle;
for (j = 0; j < pService->aCharacteristics[i].cNumDescriptors; j++)
{
if (pService->aCharacteristics[i].aDescriptors[j].uuidType == gBleUuidType16_c)
{
switch (pService->aCharacteristics[i].aDescriptors[j].uuid.uuid16)
{
/* Found Temperature Char Presentation Format Descriptor */
case gBleSig_CharPresFormatDescriptor_d:
{
mPeerInformation.customInfo.tempClientConfig.hTempDesc = pService->aCharacteristics[i].aDescriptors[j].handle;
break;
}
/* Found Temperature Char CCCD */
case gBleSig_CCCD_d:
{
mPeerInformation.customInfo.tempClientConfig.hTempCccd = pService->aCharacteristics[i].aDescriptors[j].handle;
break;
}
default:
; /* No action required */
break;
}
}
}
}
}
}
else if ((pService->uuidType == gBleUuidType128_c) &&
FLib_MemCmp(pService->uuid.uuid128, uuid_service_custom, 16))
{
/* Found Custom Service */
mPeerInformation.customInfo.customServiceClientConfig.hService = pService->startHandle;
if (pService->cNumCharacteristics > 0U &&
pService->aCharacteristics != NULL)
{
/* Found Custom Characteristic */
mPeerInformation.customInfo.customServiceClientConfig.hCharacteristic = pService->aCharacteristics[0].value.handle;
}
}
else
{
;
}
}
6. Develop the "BleApp_SendReceiveCustomService" as shown below. This function is used to write and read the custom characteristic values using long operations. Focus your attention in this function, here is the example of how to use GattClient_WriteCharacteristicValue and GattClient_ReadCharacteristicValue APIs to write and read long characteristic values. Note that the "characteristic" struct was filled before to use the last APIs, with the handle of our custom characteristic and a destination address to receive the value read from the peer. Note that the "doReliableLongCharWrites" field must be TRUE to allow long writes using GattClient_WriteCharacteristicValue.
static void BleApp_SendReceiveCustomService (customServiceState_t state)
{
bleResult_t bleResult;
gattCharacteristic_t characteristic;
/* Verify if there is a valid peer */
if (gInvalidDeviceId_c != mPeerInformation.deviceId)
{
/* Fill the characteristic struct with a read destiny and the custom service handle */
characteristic.value.handle = mPeerInformation.customInfo.customServiceClientConfig.hCharacteristic;
characteristic.value.paValue = &mReadDummyArray[0];
/* Try to write the custom characteristic value */
if(mCustomServiceWrite == state)
{
bleResult = GattClient_WriteCharacteristicValue(mPeerInformation.deviceId,
&characteristic,
(uint16_t)400,
&mWriteDummyArray[0],
FALSE, FALSE, TRUE, NULL);
/* An error occurred while trying to write the custom characteristic value, disconnect */
if(gBleSuccess_c != bleResult)
{
(void)Gap_Disconnect(mPeerInformation.deviceId);
}
}
/* Try to read the custom characteristic value */
else
{
bleResult = GattClient_ReadCharacteristicValue(mPeerInformation.deviceId,
&characteristic,
(uint16_t)400);
/* An error occurred while trying to read the custom characteristic value, disconnect */
if(gBleSuccess_c != bleResult)
{
(void)Gap_Disconnect(mPeerInformation.deviceId);
}
}
}
}
7. Modify the "BleApp_GattClientCallback" as shown below. In this function, we implement the "BleApp_SendReceiveCustomService" which writes or reads the characteristic depending on the input parameter "state". The expected behavior of this example is, first, write the 400-byte pattern contained in the mWriteDummyArray to our custom characteristic value, just after to write the characteristic descriptor of the temperature service (which is indicated by this callback in the gGattProcWriteCharacteristicDescriptor_c event). When the write has been executed successfully, it is indicated in this callback, by the "gGattProcWriteCharacteristicValue_c" event, therefore, here we can execute our function to read the characteristic value. Then "gGattProcReadCharacteristicValue_c" event is triggered if the read has been completed, here, we compare the value written with the value read from the GATT server and, if both are the same, the green RGB led should turn on indicating that our long characteristic has been written and read successfully, otherwise, the GATT client disconnects from the GATT server.
static void BleApp_GattClientCallback(
deviceId_t serverDeviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
if (procedureResult == gGattProcError_c)
{
attErrorCode_t attError = (attErrorCode_t)(uint8_t)(error);
if (attError == gAttErrCodeInsufficientEncryption_c ||
attError == gAttErrCodeInsufficientAuthorization_c ||
attError == gAttErrCodeInsufficientAuthentication_c)
{
/* Start Pairing Procedure */
(void)Gap_Pair(serverDeviceId, &gPairingParameters);
}
BleApp_StateMachineHandler(serverDeviceId, mAppEvt_GattProcError_c);
}
else
{
if (procedureResult == gGattProcSuccess_c)
{
switch(procedureType)
{
case gGattProcReadCharacteristicDescriptor_c:
{
if (mpCharProcBuffer != NULL)
{
/* Store the value of the descriptor */
BleApp_StoreDescValues(mpCharProcBuffer);
}
break;
}
case gGattProcWriteCharacteristicDescriptor_c:
{
/* Try to write to the custom service */
BleApp_SendReceiveCustomService(mCustomServiceWrite);
}
break;
case gGattProcWriteCharacteristicValue_c:
{
/* If write to the custom service was completed, try to read the custom service */
BleApp_SendReceiveCustomService(mCustomServiceRead);
}
break;
case gGattProcReadCharacteristicValue_c:
{
/* If read to the custom service was completed, compare write and read buffers */
if(FLib_MemCmp(&mWriteDummyArray[0], &mReadDummyArray[0], 400))
{
Led3On();
}
else
{
(void)Gap_Disconnect(mPeerInformation.deviceId);
}
}
break;
default:
{
; /* No action required */
break;
}
}
BleApp_StateMachineHandler(serverDeviceId, mAppEvt_GattProcComplete_c);
}
}
/* Signal Service Discovery Module */
BleServDisc_SignalGattClientEvent(serverDeviceId, procedureType, procedureResult, error);
}
Modifications in app_preinclude.h
One of the most important considerations to write and read long characteristics is the memory allocation needed for this. You must increment the current "AppPoolsDetails_c" configuration, the "_block_size_" and "_number_of_blocks_". Please ensure that "_block_size_" is aligned with 4 bytes. You can know when the current configuration of pools do not satisfy the application requirements if the return value of either "GattClient_WriteCharacteristicValue" or "GattClient_ReadCharacteristicValue " is "gBleOutOfMemory_c" instead of "gBleSuccess_c" (If it is the case, the device will disconnect to the peer according to the code in step 6 in "Modifications in temperature_collector.c"). Once you have found the configuration that works in your application, please follow the steps in Memory Pool Optimizer on MKW3xA/KW3xZ Application Note, to found the best configuration without waste memory resources. For this example, configure "AppPoolsDetails_c" as follows:
/* Defines pools by block size and number of blocks. Must be aligned to 4 bytes.*/
#define AppPoolsDetails_c \
_block_size_ 112 _number_of_blocks_ 6 _eol_ \
_block_size_ 256 _number_of_blocks_ 3 _eol_ \
_block_size_ 280 _number_of_blocks_ 2 _eol_ \
_block_size_ 432 _number_of_blocks_ 1 _eol_
Please let us know any question regarding this topic.
View full article