Generic Attribute Profile (GATT) Layer

The GATT layer contains the APIs for discovering services and characteristics and transferring data between devices and is built on top of the Attribute Protocol (ATT).

The Attribute Protocol (ATT) transfers data between Bluetooth Low Energy devices on a dedicated L2CAP channel (channel ID 0x04).

As soon as a connection is established between devices, the GATT APIs are readily available. No initialization is required because the L2CAP channel is automatically created.

To identify the GATT peer instance, the same deviceId value from the GAP layer (obtained in the gConnEvtConnected_cconnection event) is used.

There are two GATT roles that define the two devices exchanging data over ATT:

  • GATT Server – the device that contains a GATT Database, which is a collection of services and characteristics exposing meaningful data. Usually, the Server responds to requests and commands sent by the Client. However, it can be configured to send data on its own through notificationsand indications.

  • GATT Client – the “active” device that usually sends requests and commands to the Server to discover Services and Characteristics on the Server’s Database and to exchange data.

There is no fixed rule deciding which device is the Client and which one is the Server. Any device may initiate a request at any moment. Therefore, it temporarily acts as a Client, at which the peer device may respond, provided it has the Server support and a GATT Database.

Often, a GAP Central acts as a GATT Client to discover Services and Characteristics and obtain data from the GAP Peripheral, which usually has a GATT database. Many standard Bluetooth Low Energy profiles assume that the Peripheral has a database and must act as a Server. However, this is by no means a general rule.

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

Server APIs

Once the GATT Database has been created and the required security settings have been registered with Gap_RegisterDeviceSecurityRequirements, all ATT Requests and Commands and attribute access security checks are handled internally by the GATT Server module.

Besides this automatic functionality, the application may use GATT Server APIs to send Notifications and Indication and, optionally, to intercept Clients’ attempts to write certain attributes.

Server callback

The first GATT Server call is the installation of the Server Callback, which has the following prototype:

**typedef ****void** (* gattServerCallback_t )
(
    deviceId_t             deviceId,    /*!< Device ID identifying the active connection. */
    gattServerEvent_t *    pServerEvent /*!< Server event. */
);

For EATT, the following signature should be used:

typedef void (*gattServerEnhancedCallback_t) ( deviceId_t deviceId, bearerId_t bearerId, gattServerEvent_t* pServerEvent );

The callback can be installed with:

bleResult_t **GattServer\_RegisterCallback**
(
    gattServerCallback_t callback
);

The EATT server callback should be installed using the following API:

bleResult_t **GattServer\_RegisterEnhancedCallback**
(
     gattServerEnhancedCallback_t callback
);

The first member of the gattServerEvent_t structure is the eventType, an enumeration type with the following possible values:

  • gEvtMtuChanged_c: Signals that the Client-initiated MTU Exchange Procedure has completed successfully and the ATT_MTU has been increased. The event data contains the new value of the ATT_MTU. Is it possible that the application flow depends on the value of the ATT_MTU, for example, there may be specific optimizations for different ATT_MTU ranges. This event is not triggered if the ATT_MTU was not changed during the procedure.

  • gEvtHandleValueConfirmation_c: A Confirmation was received from the Client after an Indication was sent by the Server.

  • gEvtAttributeWritten_c, gEvtAttributeWrittenWithoutResponse_c: See Attribute write notifications.

  • gEvtCharacteristicCccdWritten_c: The Client has written a CCCD. The application should save the CCCD value for bonded devices with Gap_SaveCccd.

  • gEvtError_c: An error occurred during a Server-initiated procedure.

  • gEvtLongCharacteristicWritten_c: A long characteristic was written.

  • gEvtInvalidPduReceived_c: An invalid PDU was received from Client. Application decides if disconnection is required.

  • gEvtAttributeRead_c: An attribute registered with GattServer_RegisterHandlesForReadNotifications is being read.

Parent topic:Server APIs

Sending notifications and indications

The APIs provided for these Server-initiated operations are very similar.

All of the following APIs have an enhanced counterpart of the form GattServer_Enhanced[procedure]. A bearerId parameter was added to specify on which bearer the transaction should take place. A value of 0 for the bearerId identifies the Unenhanced ATT bearer. Values higher than0 are used to identify the Enhanced ATT bearer used for the ATT procedure.

bleResult_t **GattServer\_SendNotification**
(
    deviceId_t     deviceId,
    uint16_t       handle
);
bleResult_t **GattServer\_SendIndication
**(
    deviceId_t     deviceId,
    uint16_t       handle
);

Only the attribute handle needs to be provided to these functions. The attribute value is automatically retrieved from the GATT Database.

Note: It is the application developer’s responsibility to check if the Client designated by the deviceId has previously activated Notifications/Indications by writing the corresponding CCCD value. To do that, the following GAP APIs should be used:

bleResult_t **Gap\_CheckNotificationStatus
**(
    deviceId_t     deviceId,
    uint16_t       handle,
    bool_t *       pOutIsActive
);
bleResult_t **Gap\_CheckIndicationStatus**
(
    deviceId_t     deviceId,
    uint16_t       handle,
    bool_t *       pOutIsActive
    );

Note: It is necessary to use these two functions with the Gap_SaveCccd only for bonded devices, because the data is saved in NVM and reloaded at reconnection. For devices that do not bond, the application may also use its own bookkeeping mechanism.

There is an important difference between sending Notifications and Indications:

  • The latter can only be sent one at a time. In addition, the application must wait for the Client Confirmation (signaled by the gEvtHandleValueConfirmation_c Server event, or by a gEvtError_c event with gGattClientConfirmationTimeout_c error code) before sending a new Indication. Otherwise, a gEvtError_c event with gGattIndicationAlreadyInProgress_c error code is triggered.

  • The Notifications can be sent consecutively.

Parent topic:Server APIs

Attribute write notifications

When the GATT Client reads and writes values from/into the Server’s GATT Database, it uses ATT Requests.

The GATT Server module implementation manages these requests and, according to the database security settings and the Client’s security status (authenticated, authorized, and so on), automatically sends the ATT Responses without notifying the application.

There are however some situations where the application needs to be informed of ATT packet exchanges. For example, a lot of standard profiles define, for certain Services, some, so-called, Control-Point Characteristics. These are Characteristics whose values are only of immediate significance to the application. Writing these Characteristics usually triggers specific actions.

For example, consider a fictitious Smart Lamp. It has Bluetooth Low Energy connectivity in the Peripheral role and it contains a small GATT Database with a Lamp Service (among other Services). The Lamp Service contains two Characteristics: the Lamp State Characteristic (LSC) and the Lamp Action Characteristic (LAC).

LSC is a “normal” Characteristic with Read and Write properties. Its value is either 0, lamp off, or 1, lamp on). Writing the value sets the lamp in the desired state. Reading it provides its current state, which is only useful when passing the information remotely.

The LAC has only one property, which is Write Without Response. The user can use the Write Without Response procedure to write only the value 0x01 (all other values are invalid). Whenever the user writes 0x01 in LAC, the lamp switches its state.

The LAC is a good example of a Control-Point Characteristic for these reasons:

  • Writing a certain value (in this case 0x01) triggers an action on the lamp.

  • The value the user writes has immediate significance only (“0x01 switches the lamp”) and is never used again in the future. For this reason, it does not need to be stored in the database.

Obviously, whenever a Control-Point Characteristic is written, the application must be notified to trigger some application-specific action.

The GATT Server allows the application to register a set of attribute handles as “write-notifiable”, in other words, the application wants to receive an event each time any of these attributes is written by the peer Client.

All Control-Point Characteristics in the GATT Database must have their Value handle registered. In fact, the application may register any other handle for write notifications for its own purposes with the following API:

bleResult_t **GattServer\_RegisterHandlesForWriteNotifications**
(
    uint8_t         handleCount,
    const uint16_t *      aAttributeHandles
);

The handleCount is the size of the aAttributeHandles array and it cannot exceed gcGattMaxHandleCountForWriteNotifications_c.

After an attribute handle has been registered with this function, whenever the Client attempts to write its value, the GATT Server Callback is triggered with one of the following event types:

  • gEvtAttributeWritten_c is triggered when the attribute is written with a Write procedure (ATT Write Request). In this instance, the application has to decide whether the written value is valid and whether it must be written in the database, and, if so, the application must write the value with the GattDb_WriteAttribute, see GATT database application interface. At this point, the GATT Server module does not automatically send the ATT Write Response over the air. Instead, it waits for the application to call this function:

bleResult_t **GattServer\_SendAttributeWrittenStatus**
(
    deviceId_t     deviceId,
    uint16_t       attributeHandle,
    uint8_t        status
);

This API also has an enhanced counterpart, which adds the bearerId parameter.

The value of the status parameter is interpreted as an ATT Error Code. It must be equal to the gAttErrCodeNoError_c (0x00) if the value is valid and it is successfully processed by the application. Otherwise, it must be equal to a profile-specific error code (in interval 0xE0-0xFF) or an application-specific error code (in interval 0x80-0x9F).

  • gEvtAttributeWrittenWithoutResponse_c is triggered when the attribute is written with a Write Without Response procedure (ATT Write Command). Because this procedure expects no response, the application may process it and, if necessary, write it in the database. Regardless of whether the value is valid or not, no response is needed from the application.

  • gEvtLongCharacteristicWritten_c is triggered when the Client has completed writing a Long Characteristic value; the event data includes the handle of the Characteristic Value attribute and a pointer to its value in the database.

Attributes can also be registered for read notifications using the followng API:

bleResult_t GattServer_RegisterHandlesForReadNotifications
(
 uint8_t handleCount,
 const uint16_t* aAttributeHandles
);

To unregister one or more handles from the list for either write or read, the following APIs can be used:


bleResult_t GattServer_UnregisterHandlesForWriteNotifications
(
 uint8_t handleCount,
 const uint16_t* aAttributeHandles
);

bleResult_t GattServer_UnregisterHandlesForReadNotifications
(
 uint8_t handleCount,
 const uint16_t* aAttributeHandles
);

Parent topic:Server APIs

Parent topic:Generic Attribute Profile (GATT) Layer