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