Client APIs
A Client can configure the ATT MTU, discover Services and Characteristics, and initiate data exchanges.
All the functions have the same first parameter: a deviceId which identifies the connected device whose GATT Server is targeted in the GATT procedure. This is necessary because a Client may be connected to multiple Servers at the same time.
First, however, the application must install the necessary callbacks.
Installing client callbacks
There are three callbacks that the Client application must install.
Client procedure callback
All the procedures initiated by a Client are asynchronous. They rely on exchanging ATT packets over the air.
To be informed of the procedure completion, the application must install a callback with the following signature:
**typedef ****void** (* gattClientProcedureCallback_t )
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
);
For EATT, the following signature should be used:
typedef void (*gattClientEnhancedProcedureCallback_t)
(
deviceId_t deviceId,
bearerId_t bearerId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
);
To install this callback, the following function must be called:
bleResult_t **GattClient\_RegisterProcedureCallback**
(
gattClientProcedureCallback_t callback
);
The EATT procedure callback should be installed using the following API:
bleResult_t **GattClient\_RegisterEnhancedProcedureCallback**
(
gattClientEnhancedProcedureCallback_t callback
);
The procedureType parameter can be used to identify the procedure that was started and has reached completion. Only one procedure would be active at a given moment. Trying to start another procedure while a procedure is already in progress returns the error gGattAnotherProcedureInProgress_c.
The procedureResult parameter indicates whether the procedure completes successfully or an error occurs. In the latter case, the error parameter contains the error code.
**void ****gatt ClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
}
}
GattClient_RegisterProcedureCallback(gattClientProcedureCallback);
Parent topic:Installing client callbacks
Notification and indication callbacks
When the Client receives a notification from the Server, it triggers a callback with the following prototype:
**typedef ****void** (* gattClientNotificationCallback_t )
(
deviceId_t deviceId,
uint16_t characteristicValueHandle,
uint8_t * aValue,
uint16_t valueLength
);
The deviceId identifies the Server connection (for multiple connections at the same time). The characteristicValueHandle is the attribute handle of the Characteristic Value declaration in the GATT Database. The Client must have discovered it previously to be able recognize it.
For EATT, the following signature should be used:
typedef void (*gattClientEnhancedNotificationCallback_t)
( deviceId_t deviceId,
bearerId_t bearerId,
uint16_t characteristicValueHandle,
uint8_t* aValue,
uint16_t valueLength );
The callback must be installed with:
bleResult_t **GattClient\_RegisterNotificationCallback**
(
gattClientNotificationCallback_t callback
);
Very similar definitions exist for indications.
The EATT notification callback should be installed using the following API:
bleResult_t **GattClient\_RegisterEnhancedNotificationCallback**
(
gattClientEnhancedNotificationCallback_t callback
)
When receiving a notification or indication, the Client uses the characteristicValueHandle to identify which Characteristic was notified. The Client must be aware of the possible Characteristic Value handles that can be notified/indicated at any time, because it has previously activated them by writing its CCCD (see Reading and writing characteristic descriptors).
When the Client receives a multiple value notification from the Server, it triggers a callback with the following prototype:
typedef void (*gattClientMultipleValueNotificationCallback_t)
(
deviceId_t deviceId,
/*!< Device ID identifying the active connection. */
uint8_t* aHandleLenValue,
/*!< The array of handle, value length, value tuples. */
uint16_t totalLength
/*!< Value array size. */
);
The callback must be installed with:
bleResult_t **GattClient\_RegisterMultipleValueNotificationCallback**
(
gattClientMultipleValueNotificationCallback_t callback
);
When using EATT, the following callback prototype and registration APIs should be used:
typedef void (*gattClientEnhancedMultipleValueNotificationCallback_t)
( deviceId_t deviceId,
/*!< Device ID identifying the active connection. */
bearerId_t bearerId,
/*!< Bearer ID identifing the Enhanced ATT bearer used. */
uint8_t* aHandleLenValue,
/*!< The array of handle, value length, value tuples. */
uint16_t totalLength /*!< Value array size. */
);
Parent topic:Installing client callbacks
Parent topic:Client APIs
MTU exchange
A radio packet sent over the Bluetooth Low Energy contains a maximum of 27 bytes of data for the L2CAP layer. The L2CAP header is 4 bytes long, including the Channel ID. Therefore, all layers above L2CAP, including ATT and GATT, can only send 23 bytes of data in a radio packet (as per Bluetooth 4.1 Specification for Bluetooth Low Energy). This specification also sets the default length of an ATT packet (also called ATT_MTU) to 23. The ATT packet length is set to this value to maintain a logical mapping between radio packets and ATT packets.
Note: This number is fixed and cannot be increased in Bluetooth Low Energy 4.1.
Therefore, any ATT request fits in a single radio packet. If the layer above ATT wishes to send more than 23 bytes of data, the data must be fragmented into smaller packets and multiple ATT requests issued.
Despite this setting, the ATT protocol allows devices to increase the ATT_MTU, only if both can support it. Increasing the ATT_MTU has only one effect: the application does not have to fragment long data. However, it can send more than 23 bytes in a single transaction. The fragmentation is moved on to the L2CAP layer. Over the air though, there would still be more than one radio packet sent.
If the GATT Client supports a larger than default MTU, it must start an MTU exchange as soon as it connects to any server. During the MTU exchange, both devices would send their maximum MTU to the other, and the minimum of the two is chosen as the new MTU.
Consider an example where the Client supports a maximum ATT_MTU of 250, and the server supports a maximum value of 120 for the same attribute. For this case, after MTU exchange, both devices must set the new ATT_MTU value equal to 120.
To initiate the MTU exchange, call the following function from gatt_client_interface.h:
bleResult_t result = **GattClient\_ExchangeMtu**(deviceId, mtu);
**if** (*gBleSuccess\_c* != result)
{
/* Treat error */
}
When having the role of a GATT Client, the value of the maximum supported ATT_MTU of the local device is given as a parameter to the GattClient_ExchangeMtu
API. On the GATT Server side, the application configures this value via the gcGattServerMtu_c
variable that exists in the file ble_globals.c
. The minimum of these two values is chosen as the new MTU for the connection.
When the exchange is complete, the gGattProcExchangeMtu_c procedure type triggers the Client callback.
**void ****gattClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case***gGattProcExchangeMtu\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
/* To obtain the new MTU */
uint16_t newMtu;
bleResult_t result = Gatt_GetMtu(deviceId, &newMtu);
**if** (*gBleSuccess\_c* == result)
{
/* Use the value of the new MTU */
(**void**) newMtu;
}
}
**else**
{
/* Handle error */
}
**break**;
/* ... */
}
}
Note: The Exchange MTU procedure is only available for the unenhanced/legacy bearer. For procedures sent on enhanced bearers, the upper layer must use provided L2CAP APIs to create dedicated L2CAP channels. Each channel has its own MTU value specified upon creation, which can also be reconfigured later.
Parent topic:Client APIs
Service and characteristic discovery
There are multiple APIs that can be used for Discovery. The application may use any of them, according to its necessities.
All of the following APIs have an enhanced counterpart of the form GattClient_Enhanced[procedure]. A bearerIdparameter was added to specify on which bearer the transaction should take place. A value of 0 for the bearer Id identifies the Unenhanced ATT bearer. Values higher than 0 are used to identify the Enhanced ATT bearer used for the ATT procedure.
Discover all primary services
The following API can be used to discover all the Primary Services in a Server’s database:
bleResult_t **GattClient\_DiscoverAllPrimaryServices**
(
deviceId_t deviceId,
gattService_t * aOutPrimaryServices,
uint8_t maxServiceCount,
uint8_t * pOutDiscoveredCount
);
The aOutPrimaryServices parameter must point to an allocated array of services. The size of the array must be equal to the value of the maxServiceCount parameter, which is passed to make sure the GATT module does not attempt to write past the end of the array if more Services are discovered than expected.
The pOutDiscoveredCount parameter must point to a static variable because the GATT module uses it to write the number of Services discovered at the end of the procedure. This number is less than or equal to the maxServiceCount.
If there is equality, it is possible that the Server contains more than maxServiceCount Services, but they could not be discovered as a result of the array size limitation. It is the application developer’s responsibility to allocate a large enough number according to the expected contents of the Server’s database.
In the following example, the application expects to find no more than 10 Services on the Server.
**\#define** mcMaxPrimaryServices_c 10
**static **gattService_t primaryServices[mcMaxPrimaryServices_c];
uint8_t mcPrimaryServices;
bleResult_t result = **GattClient\_DiscoverAllPrimaryServices**
(
deviceId,
primaryServices,
mcMaxPrimaryServices_c,
&mcPrimaryServices
);
**if** (gBleSuccess_c != result)
{
/* Treat error */
}
The operation triggers the Client Procedure Callback when complete. The application may read the number of discovered services and each service’s handle range and UUID.
**void ****gattClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case ***gGattProcDiscoverAllPrimaryServices\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
/* Read number of discovered services */
PRINT( mcPrimaryServices );
/* Read each service's handle range and UUID */
**for** (**int** j = 0; j < mcPrimaryServices; j++)
{
PRINT( primaryServices[j]. startHandle );
PRINT( primaryServices[j]. endHandle );
PRINT( primaryServices[j]. uuidType );
PRINT( primaryServices[j]. uuid );
}
}
**else**
{
/* Handle error */
PRINT( error );
}
**break**;
/* ... */
}
}
Parent topic:Service and characteristic discovery
Discover primary services by UUID
To discover only Primary Services of a known type (Service UUID), the following API can be used:
bleResult_t **GattClient\_DiscoverPrimaryServicesByUuid**
(
deviceId_t deviceId,
bleUuidType_t uuidType,
const bleUuid_t * pUuid,
gattService_t * aOutPrimaryServices,
uint8_t maxServiceCount,
uint8_t * pOutDiscoveredCount
);
The procedure is very similar to the one described in Discover all primary services. The only difference is this time we are filtering the search according to a Service UUID described by two extra parameters: pUuid and uuidType.
This procedure is useful when the Client is only interested in a specific type of Services. Usually, it is performed on Servers that are known to contain a certain Service, which is specific to a certain profile. Therefore, most of the times the search is expected to find a single Service of the given type. As a result, only one structure is usually allocated.
For example, when two devices implement the Heart Rate (HR) Profile, an HR Collector connects to an HR Sensor and may only be interested in discovering the Heart Rate Service (HRS) to work with its Characteristics. The following code example shows how to achieve this. Standard values for Service and Characteristic UUIDs, as defined by the Bluetooth SIG, are located in the ble_sig_defines.h file.
**static **gattService_t heartRateService;
**static **uint8_t mcHrs;
bleResult_t result = **GattClient\_DiscoverPrimaryServicesByUuid**
(
deviceId,
gBleUuidType16_c, /* Service UUID type */
gBleSig_HeartRateService_d, /* Service UUID */
&heartRateService, /* Only one HRS is expected to be found */
1,
&mcHrs
/* Will be equal to 1 at the end of the procedure
if the HRS is found, 0 otherwise */
);
**if** (*gBleSuccess\_c* != result)
{
/* Treat error */
}
In the Client Procedure Callback, the application should check if any Service with the given UUID was found and read its handle range (also perhaps proceed with Characteristic Discovery within that service range).
**void** gattClientProcedureCallback
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case** gGattProcDiscoverPrimaryServicesByUuid_c:
**if** (gGattProcSuccess_c == procedureResult)
{
**if** (1 == mcHrs)
{
/* HRS found, read the handle range */
PRINT( heartRateService. startHandle );
PRINT( heartRateService. endHandle );
}
**else**
{
/* HRS not found! */
}
}
**else**
{
/* Handle error */
PRINT( error );
}
**break**;
/* ... */
}
}
Parent topic:Service and characteristic discovery
Discover included services
Discover all primary services shows how to discover Primary Services. However, a Server may also contain Secondary Services, which are not meant to be used standalone and are usually included in the Primary Services. The inclusion means that all the Secondary Service’s Characteristics may be used by the profile that requires the Primary Service.
Therefore, after a Primary Service has been discovered, the following procedure may be used to discover services (usually Secondary Services) included in it:
bleResult_t **GattClient\_FindIncludedServices**
(
deviceId_t deviceId,
gattService_t * pIoService,
uint8_t maxServiceCount
);
The service structure that pIoService points to must have the aIncludedServices field linked to an allocated array of services, of size maxServiceCount, chosen according to the expected number of included services to be found. This is the application’s choice, usually following profile specifications.
Also, the service’s range must be set (the startHandle and endHandle fields), which may have already been done by the previous Service Discovery procedure (as described in Discover all primary services and Discover primary services by UUID).
The number of discovered included services is written by the GATT module in the cNumIncludedServices field of the structure from pIoService. Obviously, a maximum of maxServiceCount included services is discovered.
The following example assumes the Heart Rate Service was discovered using the code provided in Discover primary services by UUID.
/* Finding services included in the Heart Rate Primary Service */
gattService_t * pPrimaryService = &heartRateService;
**\#define** mxMaxIncludedServices_c 3
**static **gattService_t includedServices[mxMaxIncludedServices_c];
/* Linking the array */
pPrimaryService-> aIncludedServices = includedServices;
bleResult_t result = GattClient_FindIncludedServices
(
deviceId,
pPrimaryService,
mxMaxIncludedServices_c
);
**if** (*gBleSuccess\_c* != result)
{
/* Treat error */
}
When the Client Procedure Callback is triggered, if any included services are found, the application can read their handle range and their UUIDs.
**void** gattClientProcedureCallback
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case** gGattProcFindIncludedServices_c:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
/* Read included services data */
PRINT( pPrimaryService-> cNumIncludedServices );
**for** (**int** j = 0; j < pPrimaryService-> cNumIncludedServices ; j++)
{
PRINT( pPrimaryService-> aIncludedServices [j]. startHandle );
PRINT( pPrimaryService-> aIncludedServices [j]. endHandle );
PRINT( pPrimaryService-> aIncludedServices [j]. uuidType );
PRINT( pPrimaryService-> aIncludedServices [j]. uuid );
}
}
**else**
{
/* Handle error */
PRINT( error );
}
**break**;
/* ... */
}
}
Parent topic:Service and characteristic discovery
Discover all characteristics of a service
The main API for Characteristic Discovery has the following prototype:
bleResult_t **GattClient\_DiscoverAllCharacteristicsOfService**
(
deviceId_t deviceId,
gattService_t * pIoService,
uint8_t maxCharacteristicCount
);
All required information is contained in the service structure pointed to by pIoService, most importantly being the service range (startHandle and endHandle) which is usually already filled out by a Service Discovery procedure. If not, they need to be written manually.
Also, the service structure’s aCharacteristics field must be linked to an allocated characteristic array.
The following example discovers all Characteristics contained in the Heart Rate Service discovered in Section Discover primary services by UUID.
gattService_t* pService = &heartRateService
**\#define** mcMaxCharacteristics_c 10
**static **gattCharacteristic_t hrsCharacteristics[mcMaxCharacteristics_c];
pService->aCharacteristics = hrsCharacteristics;
bleResult_t result = GattClient_DiscoverAllCharacteristicsOfService
(
deviceId,
pService,
mcMaxCharacteristics_c
);
The Client Procedure Callback is triggered when the procedure completes.
**void ****gattClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case***gGattProcDiscoverAllCharacteristics\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
/* Read number of discovered Characteristics */
PRINT(pService-> cNumCharacteristics );
/* Read discovered Characteristics data */
**for** ( uint8_t j = 0; j < pService-> cNumCharacteristics ; j++)
{
/* Characteristic UUID is found inside the value field
to avoid duplication */
PRINT(pService-> aCharacteristics [j]. value . uuidType );
PRINT(pService-> aCharacteristics [j]. value . uuid );
/* Characteristic Properties indicating the supported operations:
* - Read
* - Write
* - Write Without Response
* - Notify
* - Indicate
*/
PRINT(pService-> aCharacteristics [j]. properties );
/* Characteristic Value Handle is used to identify the
Characteristic in future operations */
PRINT(pService-> aCharacteristics [j]. value . handle );
}
}
**else**
{
/* Handle error */
PRINT( error );
}
**break**;
/* ... */
}
}
Parent topic:Service and characteristic discovery
Discover characteristics by UUID
This procedure is useful when the Client intends to discover a specific Characteristic in a specific Service. The API allows for multiple Characteristics of the same type to be discovered, but most often it is used when a single Characteristic of the given type is expected to be found.
Continuing the example from Discover primary services by UUID, assume the Client wants to discover the Heart Rate Control Point Characteristic inside the Heart Rate Service, as shown in the following code.
gattService_t * pService = &heartRateService;
**static **gattCharacteristic_t hrcpCharacteristic;
**static **uint8_t mcHrcpChar;
bleResult_t result = GattClient_DiscoverCharacteristicOfServiceByUuid
(
deviceId,
*gBleUuidType16\_c*,
gBleSig_HrControlPoint_d,
pService,
&hrcpCharacteristic,
1,
&mcHrcpChar
);
This API can be used as in the previous examples, following a Service Discovery procedure. However, the user may want to perform a Characteristic search with UUID over the entire database, skipping the Service Discovery entirely. To do so, a dummy service structure must be defined and its range must be set to maximum, as shown in the following example:
gatt Service_t dummyService;
dummyService. startHandle = 0x0001;
dummyService. endHandle = 0xFFFF;
**static **gattCharacteristic_t hrcpCharacteristic;
**static **uint8_t mcHrcpChar;
bleResult_t result = **GattClient\_DiscoverCharacteristicOfServiceByUuid**
(
deviceId,
*gBleUuidType16\_c*,
gBleSig_HrControlPoint_d,
&dummyService,
&hrcpCharacteristic,
1,
&mcHrcpChar
);
In either case, the value of the mcHrcpChar variable should be checked in the procedure callback.
**void ****gattClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case***gGattProcDiscoverCharacteristicByUuid\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
**if** (1 == mcHrcpChar)
{
/* HRCP found, read discovered data */
PRINT(hrcpCharacteristic. properties );
PRINT(hrcpCharacteristic. value . handle );
}
**else**
{
/* HRCP not found! */
}
}
**else**
{
/* Handle error */
PRINT(error);
}
**break**;
/* ... */
}
}
Parent topic:Service and characteristic discovery
Discover characteristic descriptors
To discover all descriptors of a Characteristic, the following API is provided:
bleResult_t **GattClient\_DiscoverAllCharacteristicDescriptors**
(
deviceId_t deviceId,
gattCharacteristic_t * pIoCharacteristic,
uint16_t endingHandle,
uint8_t maxDescriptorCount
);
The pIoCharacteristic pointer must point to a Characteristic structure with the value.handle field set (either by a discovery operation or by the application) and the aDescriptors field pointed to an allocated array of Descriptor structures.
The endingHandle should be set to the handle of the next Characteristic or Service declaration in the database to indicate when the search for descriptors must stop. The GATT Client module uses ATT Find Information Requests to discover the descriptors, and it does so until it discovers a Characteristic or Service declaration or until endingHandle is reached. Thus, by providing a correct ending handle, the search for descriptors is optimized and the number of packets sent over the air is reduced.
If, however, the application does not know where the next declaration lies and cannot provide this optimization hint, the endingHandle should be set to 0xFFFF.
Continuing the example from Discover characteristics by UUID, the following code assumes that the Heart Rate Control Point Characteristic has no more than 5 descriptors and performs Descriptor Discovery.
**\#define** mcMaxDescriptors_c 5
**static **gattAttribute_t aDescriptors[mcMaxDescriptors_c];
hrcpCharacteristic. aDescriptors = aDescriptors;
bleResult_t result = **GattClient\_DiscoverAllCharacteristicDescriptors**
(
deviceId,
&hrcpCharacteristic,
0xFFFF, /* We do not know where the next Characterstic Service begins */
mcMaxDescriptors_c
);
**if** (*gBleSuccess\_c* != result)
{
/* Handle error */
}
The Client Procedure Callback is triggered at the end of the procedure.
**void ****gattClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case***gGattProcDiscoverAllCharacteristicDescriptors\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
/* Read number of discovered descriptors */
PRINT(hrcpCharacteristic. cNumDescriptors );
/* Read descriptor data */
**for** ( uint8_t j = 0; j < hrcpCharacteristic. cNumDescriptors ; j++)
{
PRINT(hrcpCharacteristic. aDescriptors [j]. handle );
PRINT(hrcpCharacteristic. aDescriptors [j]. uuidType );
PRINT(hrcpCharacteristic. aDescriptors [j]. uuid );
}
}
**else**
{
/* Handle error */
PRINT(error);
}
**break**;
/* ... */
}
}
Parent topic:Service and characteristic discovery
Parent topic:Client APIs
Reading and writing characteristics
All the APIs described in the following sections have an enhanced counterpart of the form GattClient_Enhanced[procedure]. A bearer id parameter was added to specify on which bearer the transaction should take place. A value of 0 for the bearer id identifies the Unenhanced ATT bearer. Values higher than 0 are used to identify the Enhanced ATT bearer used for the ATT procedure.
Characteristic value read procedure
The main API for reading a Characteristic Value is shown here:
bleResult_t **GattClient\_ReadCharacteristicValue**
(
deviceId_t deviceId,
gattCharacteristic_t * pIoCharacteristic,
uint16_t maxReadBytes
);
This procedure assumes that the application knows the Characteristic Value Handle, usually from a previous Characteristic Discovery procedure. Therefore, the value.handle field of the structure pointed by pIoCharacteristic must be completed.
Also, the application must allocate a large enough array of bytes where the received value (from the ATT packet exchange) is written. The maxReadBytes parameter is set to the size of this allocated array.
The GATT Client module takes care of long characteristics, whose values have a greater length than can fit in a single ATT packet, by issuing repeated ATT Read Blob Requests when needed.
The following examples assume that the application knows the Characteristic Value Handle and that the value length is variable, but limited to 50 bytes.
gattCharacteristic_t myCharacteristic;
myCharacteristic. value . handle = 0x10AB;
**\#define** mcMaxValueLength_c 50
**static** uint8_t aValue[mcMaxValueLength_c];
myCharacteristic. value . paValue = aValue;
bleResult_t result = **GattClient\_ReadCharacteristicValue**
(
deviceId,
&myCharacteristic,
mcMaxValueLength_c
);
**if** (*gBleSuccess\_c* != result)
{
/* Handle error */
}
Regardless of the value length, the Client Procedure Callback is triggered when the reading is complete. The received value length is also filled in the value structure.
**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(myCharacteristic. value . valueLength );
/* Read data */
**for** ( uint16_t j = 0; j < myCharacteristic. value . valueLength ; j++)
{
PRINT(myCharacteristic. value . paValue [j]);
}
}
**else**
{
/* Handle error */
PRINT(error);
}
**break**;
/* ... */
}
}
Parent topic:Reading and writing characteristics
Characteristic read by UUID procedure
This API for this procedure is shown here:
bleResult_t **GattClient\_ReadUsingCharacteristicUuid**
(
deviceId_t deviceId,
bleUuidType_t uuidType,
const bleUuid_t* pUuid,
const gattHandleRange_t* pHandleRange,
uint8_t* aOutBuffer,
uint16_t maxReadBytes,
uint16_t* pOutActualReadBytes
);
This provides support for an important optimization, which involves reading a Characteristic Value without performing any Service or Characteristic Discovery.
For example, the following is the process to write an application that connects to any Server and wants to read the device name.
The device name is contained in the Device Name Characteristic from the GAP Service. Therefore, the necessary steps involve discovering all primary services, identifying the GAP Service by its UUID, discovering all Characteristics of the GAP Service and identifying the Device Name Characteristic (alternatively, discovering Characteristic by UUID inside GAP Service), and, finally, reading the device name by using the Characteristic Read Procedure.
Instead, the Characteristic Read by UUID Procedure allows reading a Characteristic with a specified UUID, assuming one exists on the Server, without knowing the Characteristic Value Handle.
The described example is implemented as follows:
**\#define** mcMaxValueLength_c
/* First byte is for handle-value pair length. Next 2 bytes are the handle */
static uint8_t aValue[1 + 2 + mcMaxValueLength_c];
**static** uint16_t deviceNameLength;
bleUuid_t uuid = {
.uuid16 = gBleSig_GapDeviceName_d
};
bleResult_t result = **GattClient\_ReadUsingCharacteristicUuid**
(
deviceId,
*gBleUuidType16\_c*,
&uuid,
&pHandleRange,
aValue,
1 + 2 + mcMaxValueLength_c,
deviceNameLength
);
**if** (*gBleSuccess\_c* != result)
{
/* Handle error */
}
The Client Procedure Callback is triggered when the reading is complete. Because only one air packet is exchanged during this procedure, it can only be used as a quick reading of Characteristic Values with length no greater than ATT_MTU – 1.
**void ****gattClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case ***gGattProcReadUsingCharacteristicUuid\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
/* Read handle-value pair length */
PRINT(aValue[0]);
deviceNameLength -= 1;
/* Read characteristic value handle */
PRINT(aValue[1] | (aValue[2] << 8));
deviceNameLength -= 2;
/* Read value length */
PRINT(deviceNameLength);
/* Read data */
**for** ( uint8_t j = 0; j < deviceNameLength; j++)
{
PRINT(aValue[3 + j]);
}
}
**else**
{
/* Handle error */
PRINT(error);
}
**break**;
/* ... */
}
}
Parent topic:Reading and writing characteristics
Characteristic read multiple procedure
The API for this procedure is shown here:
bleResult_t **GattClient\_ReadMultipleCharacteristicValues**
(
deviceId_t deviceId,
uint8_t cNumCharacteristics,
gattCharacteristic_t * aIoCharacteristics
);
This procedure also allows an optimization for a specific situation, which occurs when multiple Characteristics, whose values are of known, fixed-length, can be all read in one single ATT transaction (usually one single over-the-air packet).
The application must know the value handle and value length of each Characteristic. It must also write the value.handleand value.maxValueLength with the aforementioned values, respectively, and then link the value.paValue field with an allocated array of size maxValueLength.
The following example involves reading three characteristics in a single packet.
**\#define** mcNumCharacteristics_c 3
**\#define** mcChar1Length_c 4
**\#define** mcChar2Length_c 5
**\#define** mcChar3Length_c 6
**static** uint8_t aValue1[mcChar1Length_c];
**static** uint8_t aValue2[mcChar2Length_c];
**static** uint8_t aValue3[mcChar3Length_c];
**static** gattCharacteristic_t myChars[mcNumCharacteristics_c];
myChars[0]. value . handle = 0x0015;
myChars[1]. value . handle = 0x0025;
myChars[2]. value . handle = 0x0035;
myChars[0]. value . maxValueLength = mcChar1Length_c;
myChars[1]. value . maxValueLength = mcChar2Length_c;
myChars[2]. value . maxValueLength = mcChar3Length_c;
myChars[0]. value . paValue = aValue1;
myChars[1]. value . paValue = aValue2;
myChars[2]. value . paValue = aValue3;
bleResult_t result = **GattClient\_ReadMultipleCharacteristicValues**
(
deviceId,
mcNumCharacteristics_c,
myChars
);
**if** (*gBleSuccess\_c* != result)
{
/* Handle error */
}
When the Client Procedure Callback is triggered, if no error occurs, each Characteristic’s value length should be equal to the requested lengths.
**void ****gattClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t p rocedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case***gGattProcReadMultipleCharacteristicValues\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
**for** ( uint8_t i = 0; i < mcNumCharacteristics_c; i++)
{
/* Read value length */
PRINT(myChars[i]. value . valueLength );
/* Read data */
**for** ( uint8_t j = 0; j < myChars[i]. value . valueLength ; j++)
{
PRINT(myChars[i]. value . paValue [j]);
}
}
}
**else**
{
/* Handle error */
PRINT(error);
}
**break**;
/* ... */
}
}
If the server does not know the length of the characteristic values, then the Read Multiple Variable Characteristic Values procedure can be used. This sub-procedure is used to read multiple characteristic values of variable length from a server when the client knows the characteristic value handles. The response returns the characteristic values and their corresponding lengths in the Length Value Tuple List parameter.
bleResult_t **GattClient\_ReadMultipleVariableCharacteristicValues**
(
deviceId_t deviceId,
uint8_t cNumCharacteristics,
gattCharacteristic_t* pIoCharacteristics
);
The following example involves reading three characteristics of variable length in a single packet.
#define mcNumCharacteristics_c 3
#define mcCharLengthMax_c 10
static uint8_t aValue1[mcCharLengthMax _c];
static uint8_t aValue2[mcCharLengthMax _c];
static uint8_t aValue3[mcCharLengthMax _c];
static gattCharacteristic_t myChars[mcNumCharacteristics_c];
myChars[0].value .handle = 0x0015;
myChars[1].value .handle = 0x0025;
myChars[2].value .handle = 0x0035;
myChars[0].value .paValue = aValue1;
myChars[1].value .paValue = aValue2;
myChars[2].value .paValue = aValue3;
bleResult_t result = **GattClient\_ReadMultipleVariableCharacteristicValues**
(
deviceId,
mcNumCharacteristics_c,
pIoCharacteristics
);
if (gBleSuccess_c != result)
{
/* Handle error */
}
The result of this procedure is sent to the application via the GATT procedure callback. The response includes the characteristic value together with a handle, length pair corresponding to each characteristic.
static void **BleApp\_GattClientCallback**
(
deviceId_t serverDeviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
switch (procedureResult)
{
/* ... */
case gGattProcReadMultipleVarLengthCharValues_c:
if (gGattProcSuccess_c == procedureResult)
{
for (uint8_t i = 0; i < mcNumCharacteristics_c; i++)
{
/* Print characteristic handle and length */
PRINT(myChars[i].value.handle);
PRINT(myChars[i].value.valueLength);
for (uint8_t j = 0; j < myChars[i].value.maxValueLength; j++)
{
/* Print characteristic value */
PRINT(myChars[i].value.paValue[j]);
}
}
}
else
{
/* Handle error */
}
break;
}
Parent topic:Reading and writing characteristics
Characteristic write procedure
There is a general API that may be used for writing Characteristic Values:
bleResult_t **GattClient\_WriteCharacteristicValue**
(
deviceId_t deviceId,
const gattCharacteristic_t * pCharacteristic,
uint16_t valueLength,
const uint8_t * aValue,
bool_t withoutResponse,
bool_t signedWrite,
bool_t doReliableLongCharWrites,
const uint8_t * aCsrk
);
It has many parameters to support different combinations of Characteristic Write Procedures.
The structure pointed to by the pCharacteristic is only used for the value.handlefield which indicates the Characteristic Value Handle. The value to be written is contained in the aValue array of size valueLength.
The withoutResponse parameter can be set to TRUE if the application wishes to perform a Write Without Response Procedure, which translates into an ATT Write Command. If this value is selected, the signedWrite parameter indicates whether data should be signed (Signed Write Procedure over ATT Signed Write Command), in which case the aCsrk parameters must not be NULL and contains the CSRK to sign the data with. Otherwise, both signedWrite and aCsrk are ignored.
Finally, doReliableLongCharWrites should be sent to TRUE if the application is writing a long Characteristic Value (one that requires multiple air packets due to ATT_MTU limitations) and wants the Server to confirm each part of the attribute that is sent over the air.
To simplify the application code, the following macros are defined:
**\#define** GattClient_SimpleCharacteristicWrite(deviceId, pChar, valueLength, aValue) \
GattClient_WriteCharacteristicValue\
(deviceId, pChar, valueLength, aValue, FALSE, FALSE, FALSE, NULL)
This is the simplest usage for writing a Characteristic. It sends an ATT Write Request if the value length does not exceed the maximum space for an over-the-air packet (ATT_MTU – 3). Otherwise, it sends ATT Prepare Write Requests with parts of the attribute, without checking the ATT Prepare Write Response data for consistency, and in the end an ATT Execute Write Request.
**\#define** GattClient_CharacteristicWriteWithoutResponse(deviceId, pChar, valueLength, aValue) \
GattClient_WriteCharacteristicValue\
(deviceId, pChar, valueLength, aValue, TRUE, FALSE, FALSE, NULL)
This usage sends an ATT Write Command. Long Characteristic values are not allowed here and trigger a gBleInvalidParameter_c error.
**\#define** GattClient_CharacteristicSignedWrite(deviceId, pChar, valueLength, aValue, aCsrk) \
GattClient_WriteCharacteristicValue\
(deviceId, pChar, valueLength, aValue, TRUE, TRUE, FALSE, aCsrk)
This usage sends an ATT Signed Write Command. The CSRK used to sign data must be provided.
This is a short example to write a 3-byte long Characteristic Value.
gattCharacteristic_t myChar;
myChar. value . handle = 0x00A0; /* Or maybe it was previously discovered? */
**\#define** mcValueLength_c 3
uint8_t aValue[mcValueLength_c] = { 0x01, 0x02, 0x03 };
bleResult_t result = **GattClient\_SimpleCharacteristicWrite**
(
deviceId,
&myChar,
mcValueLength_c,
aValue
);
**if** (*gBleSuccess\_c* != result)
{
/* Handle error */
}
The Client Procedure Callback is triggered when writing is complete.
**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 */
PRINT(error);
}
**break**;
/* ... */
}
}
Parent topic:Reading and writing characteristics
Parent topic:Client APIs
Reading and writing characteristic descriptors
Two APIs are provided for these procedures which are very similar to Characteristic Read and Write.
The only difference is that the handle of the attribute to be read/written is provided through a pointer to an gattAttribute_t structure (same type as the gattCharacteristic_t.value field).
All of the following APIs have an enhanced counterpart of the form GattClient_Enhanced[procedure]. A bearerIdparameter was added to specify on which bearer the transaction should take place. A value of 0 for the bearer Id identifies the Unenhanced ATT bearer. Values higher than 0 are used to identify the Enhanced ATT bearer used for the ATT procedure.
bleResult_t **GattClient\_ReadCharacteristicDescriptor**
(
deviceId_t deviceId,
gattAttribute_t * pIoDescriptor,
uint16_t maxReadBytes
);
The pIoDescriptor->handle is required (it may have been discovered previously by GattClient_DiscoverAllCharacteristicDescriptors). The GATT module fills the value that was read in the fields pIoDescriptor->aValue (must be linked to an allocated array) and pIoDescriptor->valueLength (size of the array).
Writing a descriptor is also performed similarly with this function:
bleResult_t **GattClient\_WriteCharacteristicDescriptor**
(
deviceId_t deviceId,
gattAttribute_t * pDescriptor,
uint16_t valueLength,
uint8_t * aValue
);
Only the pDescriptor->handle must be filled before calling the function.
One of the most frequently written descriptors is the Client Characteristic Configuration Descriptor (CCCD). It has a well-defined UUID (gBleSig_CCCD_d) and a 2-byte long value that can be written to enable/disable notifications and/or indications.
In the following example, a Characteristic’s descriptors are discovered and its CCCD written to activate notifications.
**static** gattCharacteristic_t myChar;
myChar. value . handle = 0x00A0; /* Or maybe it was previously discovered? */
**\#define** mcMaxDescriptors_c 5
**static** gattAttribute_t aDescriptors[mcMaxDescriptors_c];
myChar. aDescriptors = aDescriptors;
/* ... */
{
bleResult_t result = **GattClient\_DiscoverAllCharacteristicDescriptors**
(
deviceId,
&myChar,
0xFFFF,
mcMaxDescriptors_c
);
**if** (*gBleSuccess\_c* != result)
{
/* Handle error */
}
}
/* ... */
**void gattClientProcedureCallback**
(
deviceId_t deviceId,
gattProcedureType_t procedureType,
gattProcedureResult_t procedureResult,
bleResult_t error
)
{
**switch** (procedureType)
{
/* ... */
**case ***gGattProcDiscoverAllCharacteristicDescriptors\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
/* Find CCCD */
**for** ( uint8_t j = 0; j < myChar. cNumDescriptors ; j++)
{
**if** (aDescriptors[j].uuidType && gBleSig_CCCD_d ==myChar.aDescriptors[j].uuid.uuid16) )
{
uint8_t cccdValue[2];
packTwoByteValue(*gCccdNotification\_c*, cccdValue);
bleResult_t result = GattClient_WriteCharacteristicDescriptor
(
deviceId,
&myChar. aDescriptors [j],
2,
cccdValue
);
**if** (*gBleSuccess\_c* != result)
{
/* Handle error */
}
**break**;
}
}
}
**else**
{
/* Handle error */
PRINT(error);
}
**break**;
**case ***gGattProcWriteCharacteristicDescriptor\_c*:
**if** (*gGattProcSuccess\_c* == procedureResult)
{
/* Notification successfully activated */
}
**else**
{
/* Handle error */
PRINT(error);
}
/* ... */
}
}
Parent topic:Client APIs
Resetting procedures
To cancel an ongoing Client Procedure, the following API can be called:
bleResult_t **GattClient\_ResetProcedure \(void\)**;
It resets the internal state of the GATT Client and new procedure may be started at any time.
Parent topic:Client APIs
Parent topic:Generic Attribute Profile (GATT) Layer