Note

As of 2021, this document follows the Appropriate Language nomenclature directive from Bluetooth SIG. The SDK project names, APIs and code are not yet ported and use the old nomenclature. For details on how to correlate between the two, please open the PDF document Appropriate Language Mapping Tables.

Introduction

We will create a new custom (or proprietary, as it's called) profile and in the process learn the difficult parts of the GATT and ATT protocols that are used for data exchange between BLE devices.

This tutorial will take about 2-3 hours to complete and requires very little knowledge except some basic familiarity with embedded programming (see more details in Prerequisites).

First, the basic concepts will be described, and then how the TI BLE5-Stack is used to implement a custom service. We will then define our own custom profile (the sunlight profile) and modify the application source code in the Project Zero project to include the required services and modify the behavior according to our own specification.

Prerequisites

Hardware

For this lab, you need one or two Bluetooth-enabled development boards. Supported devices are:

Software

ATT and GATT Introduction

For Bluetooth low energy, communication occurs over the air according to the Attribute Protocol (ATT). From a BLE application point of view however, data is exchanged using the Generic Attribute Protocol (GATT) which can be viewed as a meta-layer on top of ATT. Bluetooth SIG has defined several Profiles for the use of these protocols to ensure interoperability.

A Profile is a written document (or a cryptic voicemail) describing how a number of GATT Services and GATT Characteristics (defined separately from the Profile) should be used to achieve a certain application. These will be briefly described below.

Attribute

An Attribute is the smallest addressable unit of data used by ATT and GATT. It is addressed via a 16-bit handle, it has a type, which is an UUID that can be 16, 32 or 128 bits long, and it has a data field which can be up to 512 bytes long. Note that all 32-bit UUIDs should be converted to 128-bit before sending over the ATT protocol.

Summarized, the Attribute Protocol defines an attribute to consist of

  • Handle – The 'address' of the Attribute when accessed via the Attribute Protocol
  • UUID – The 'type' of the Attribute
  • Value – Array of bytes interpreted differently depending on the UUID (type).
Handle UUID Value
16 bits 16 or 128 bits 1 to 512 bytes.

Length and MTU

Even though the max length of an attribute value is logically 512 bytes, the Maximum Transmission Unit (MTU) size for ATT can be lower - minimum 27 bytes, not accounting for L2CAP and ATT command header. If the ATT value length exceeds the MTU size, the data will need to be split into smaller parts before being sent. This is done by performing a GATT ReadLongCharValue or GATT WriteLongCharValue procedure.

Characteristic

Several of these attributes are needed to define a Characteristic. A Characteristic always consists of at least a Value attribute and a Declaration attribute. The meaning of the Value attribute is defined in the Profile. The Declaration always comes before the value attribute and describes whether the value attribute can be read or written, contains the UUID of the Characteristic and the handle of the Characteristic Value's attribute.

Other attributes of a characteristic can give a description of the characteristic in string format, or describe how the Value should be interpreted, or configure whether the GATT Server may send Notifications about value changes. These are called Descriptors.

Quiz

Which statement is true?

Example Characteristic

A peer device can only address attributes via their handles and can in fact only operate over the air on attributes. There is no concept of services or characteristics in the radio protocol, only attributes.

The hierarchy of ServiceCharacteristicValue is simply meta-data derived from the attribute values and UUIDs. Hence the names Attribute Protocol (ATT) which is the over-the-air protocol and Generic Attribute Protocol (GATT) which is layered on top.

An example of a characteristic as seen 'over the air' in the form of two attributes is shown below:

Handle UUID Value What
.. ... .... ....
31 0x2803 02:20:00:AA:BB Characteristic Declaration
32 0xBBAA 41:42:43 Characteristic Value
33 0x2803 02:22:00:AA:CC Characteristic Declaration
34 0xCCAA 15 Characteristic Value

Handle

In the TI BLE5-Stack, unique handles are assigned starting from 1 when the attribute is registered with the GATT Server. This usually happens during initialization. If the firmware doesn't change, the handle is always the same.

UUID

The UUID tells a peer device how the value of the Attribute should be interpreted. For example, 0x2803 is defined in the specification to mean Characteristic Declaration, and 0xBBAA is something made up for this example. Multiple attributes may share the same UUID so handles are needed to identify a particular attribute instance.

Warning

Proprietary (non-SIG-adopted) services must use 128-bit UUIDs to avoid collision, unless a 16- or 32-bit UUID is acquired from the SIG.

Attribute Value

As described above, the value of an attribute can be up to 512 bytes long, and is given meaning by the UUID of the attribute.

Example Characteristic Declaration

The value of the Characteristic Declaration attribute with handle 31 above is interpreted like this:

Byte(s) Definition Value Meaning
0 Char Value Permissions 02 Permit Read on Characteristic Value
1-2 Char Value's ATT handle 20:00 0x0020 = 32, as seen above
3-n Characteristic UUID AA:BB 0xBBAA
Example Characteristic Value

For the attribute value 41:42:43 of the Characteristic Value attribute it is up to us to define how the value is interpreted, because it's not defined by the Bluetooth SIG. As an ASCII-string it would be 'ABC'.

Characteristic Value

The attribute with handle 32 above gets the moniker Characteristic Value because the Characteristic Declaration says the actual useful value of the characteristic can be found at handle 32, and because its UUID matches the UUID found in the Characteristic Declaration.

Client Characteristic Configuration Descriptor

The name of this descriptor type is perhaps not very poetic. Often abbreviated to CCCD, the descriptor is an attribute with the UUID 0x2902 and is readable and writable.

The value a GATT Client writes to this attribute will determine whether the GATT Server is allowed to send Notifications (if 0x0001 is written) or Indications (if 0x0002 is written).

Discovery

You will notice that the UUID in the attribute value for the Characteristic Declaration, and the UUID of the attribute for the Characteristic Value is identical.

Handle UUID Value
31 0x2803 02:20:00:AA:BB
32 0xBBAA 41:42:43

This is not coincidental. The GATT protocol specifies that a peer device should be able to discover all the services and characteristics by just looking for attributes with the UUID 0x2800 for Primary Service Declarations and 0x2803 for Characteristic Declarations and use the values of these attributes (just metadata) to know the capabilities of the device.

Quiz

The Attribute Protocol is a meta-layer on top of the Generic Attribute Protocol.

Characteristics are used to build Attributes

The interpretation of an Attribute's value-field depends on that attribute's

Service and Profile

A Service is a collection of characteristics. A Profile defines a collection of one or more services and define how services can be used to enable an application or use case. A precise description of GATT, Services, attributes, etc. and how they are related to each other can be found on SIG GATT Overview. You can also read more about Bluetooth Interoperability and Profiles.

An example of a service that uses the above characteristic and one more is found below

What's in a service?

Every attribute from the declaration of a service until the declaration of another service, ordered by attribute handle, is a member of the service.

Handle UUID Value
30 0x2800 AA:AA
31 0x2803 02:20:00:AA:BB
32 0xBBAA 41:42:43
33 0x2803 02:22:00:DC:AC
34 0xACDC 4C:41:53:45:52

If we were a peer device, we would now know that the Service 0xAAAA exists in the GATT Server, and it contains the readable characteristic 0xBBAA at handle 32 with the value 41:42:43.

A Profile could for example tell us that 0xAAAA means AcronymService and 0xBBAA is a RandomAcronymString with no NULL terminator that is always 3 characters long. It could also tell us that 0xACDC means LaserAcronym, a Characteristic which Characteristic Value always reads as the string 'LASER'.

Knowing this, we can make the table above more understandable.

Handle UUID Value What
30 0x2800 AA:AA AcronymService declaration
31 0x2803 02:20:00:AA:BB RandomAcronymString declaration
32 0xBBAA 41:42:43 Always 3 letters, no NULL. Readable.
33 0x2803 02:22:00:DC:AC LaserAcronym declaration
34 0xACDC 4C:41:53:45:52 Always LASER. Readable.

Tool metadata

When you make a custom service, tools like BTool don't know what the UUIDs represent since they're just made up. Edit <BTool_Install>\btool_gatt_uuid.xml to add your own profile.

Implementation of a Service

The central element in a service is the Attribute Table, which is a one to one representation in code of the tables seen above.

When registering a service with the GATT Server, a pointer to the array of attributes is given, and this is interpreted and finalized by the GATT Server. Notice below that we find again handle, UUID/type and value from above.

Attribute Struct

/**
 * GATT Attribute format.
 */
typedef struct attAttribute_t
{
  gattAttrType_t type; //!< Attribute type (2 or 16 octet UUIDs)
  uint8 permissions;   //!< Attribute permissions
  uint16 handle;       //!< Attribute handle - assigned internally by attribute server
  uint8* const pValue; //!< Attribute value - encoding of the octet array is defined in
                       //!< the applicable profile. The maximum length of an attribute
                       //!< value shall be 512 octets.
} gattAttribute_t;

gatt.h :: Attribute Entry – Permissions is not visible externally, but is a signal to the GATT Server.

There are some things to keep in mind when initializing an attribute entry in the attribute table:

  • The UUID is a complex structure
    • Length of UUID (ATT_BT_UUID_SIZE which is 16-bit or ATT_UUID_SIZE which is 128-bit)
    • A pointer to UUID character array of that length.
  • Handle is not initialized by the user, but left as 0.
  • The value is not stored in the attribute table, merely a pointer to the variable holding the value.
  • Permissions tells the GATT Server what requests can be let through to the Service's callbacks.

Characteristic Value Properties

The Characteristic Value Properties found in the Characteristic Declaration must match the permissions of the Characteristic Value's attribute entry, and will let the peer device know what operations can be done on the Characteristic Value attribute, since it can't know the internal permissions of the attribute.

For example, the value of simpleProfileChar1Props below is GATT_PROP_READ | GATT_PROP_WRITE0x0A which matches GATT_PERMIT_READ | GATT_PERMIT_WRITE. The list of these defines is found in the BLE Basics training, and in gattservapp.h (for props) and gatt.h (for permissions).

Attribute table

When used in an attribute table, like for the simple_gatt_profile included in the simple_peripheral sample application, it looks like this:

/*********************************************************************
 * Profile Attributes - Table
 */

static gattAttribute_t simpleProfileAttrTbl[SERVAPP_NUM_ATTR_SUPPORTED] =
{
  // Simple Profile Service
  {
    { ATT_BT_UUID_SIZE, primaryServiceUUID }, /* type */
    GATT_PERMIT_READ,                         /* permissions */
    0,                                        /* handle */
    (uint8 *)&simpleProfileService            /* pValue */
  },

    // Characteristic 1 Declaration
    {
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ,
      0,
      &simpleProfileChar1Props
    },

      // Characteristic Value 1
      {
        { ATT_BT_UUID_SIZE, simpleProfilechar1UUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE,
        0,
        &simpleProfileChar1
      },

simple_gatt_profile.c :: Attribute Table – Service- and Characteristic Declaration and Characteristic Value

A somewhat simplified graphic representation can be seen here. The orange bits correspond to the fields in the struct shown above called Attribute Entry.

The code snippet above is in turn translated to what we are familar with from above, when read over the air.

Handle UUID Value What
30 0x2800
primaryServiceUUID
F0:FF
simpleProfileService
SimpleProfileService Declaration. Readable.
31 0x2803
characterUUID
0A:20:00:F1:FF
simpleProfileChar1Props
'Characteristic 1' Declaration. Readable.
32 0xFFF1
simpleProfilechar1UUID
01
simpleProfileChar1
'Char 1' Value Attribute. Readable/Writable.

The variables primaryServiceUUID and characterUUID are defined in gatt_uuid.c along with other BT SIG defined GATT-related UUIDs such as various descriptor types.

The other variables such as service and characteristic UUIDs and characteristic values and properties must be defined by the service.

Attribute variables

As an overview, these are the variables needed to initialize the attribute table seen above. These can be found in simple_gatt_profile.h and simple_gatt_profile.c under PROFILES/ in the ble5_simple_peripheral app project in the TI BLE5-Stack.

Service Declaration

The Service Declaration is typically a four-stage rocket:

  • The UUID is placed in a #define
  • The #define is used to initialize an array of 2 or 16 bytes
  • The array with the UUID is pointed to by a gattAttrType_t struct which contains the length of the array and a pointer to the array
  • The gattAttrType_t struct is pointed to by the attribute table
// Simple Profile Service UUID
#define SIMPLEPROFILE_SERV_UUID             0xFFF0

// Simple GATT Profile Service UUID: 0xFFF0
CONST uint8 simpleProfileServUUID[ATT_BT_UUID_SIZE] =
{
  LO_UINT16(SIMPLEPROFILE_SERV_UUID), HI_UINT16(SIMPLEPROFILE_SERV_UUID)
};

// Simple Profile Service attribute
static CONST gattAttrType_t simpleProfileService = { ATT_BT_UUID_SIZE, simpleProfileServUUID };

Service Declaration VariablessimpleProfileService is inserted into the attribute table and points to the UUID.

Quiz

Where in the attribute table will a peer device find the Service UUID? Look at the code and table in the previous section before answering.

Characteristic Declaration and Value

Similar to the Service Declaration, the Characteristic's UUID is also defined in a header file, and inserted into an array.

Dissimilarly, the Characteristic UUID is not inserted into the Characteristic Declaration's Value by the user. Instead, it is pointed to directly from the attribute table, and the GATT Server takes care of initializing the handle and characteristic UUID of the declaration.

The value is simply pointed to - but any access to the variable needs to know its length, as this is typically done via a pointer to the value.

// Characteristic UUID
#define SIMPLEPROFILE_CHAR1_UUID            0xFFF1

// Characteristic 1 UUID: 0xFFF1
CONST uint8 simpleProfilechar1UUID[ATT_BT_UUID_SIZE] =
{
  LO_UINT16(SIMPLEPROFILE_CHAR1_UUID), HI_UINT16(SIMPLEPROFILE_CHAR1_UUID)
};

// Simple Profile Characteristic 1 Properties
static uint8 simpleProfileChar1Props = GATT_PROP_READ | GATT_PROP_WRITE;

// Characteristic 1 Value
static uint8 simpleProfileChar1 = 0;

// Client Characteristic Configuration example - not part of SimpleGATTProfile.
static gattCharCfg_t *simpleProfileChar1Config;

Characteristic Declaration and Value variables

Quiz

Where in the attribute table will a peer device find the Characteristic UUID? Look at the code and table in the previous section before answering.

In which Attribute's field is the actual data-value of the Characteristic found by a peer device? Found as simpleProfileChar1 above.

Application Interface

The attribute table and the value variables is the data structure for your new GATT Service. For your application to use the data, the service must implement some minimal API for the application thread to interact with it.

Typically, you will want to at least

  • Add the attribute table to the GATT Server in the stack, so the service is exposed to peers, and requests are routed to your service.
  • Set and Get the values written to and read from the service by peer devices.
  • Execute a callback handler in the application when data is written to your service so you don't have to continuously call the Getter from the application.

AddService

The first thing that has to happen is that an application thread that is registered with ICall needs to call the service's init routine - typically called SomeProfile_AddService(..).

/*********************************************************************
 * @fn      SimpleProfile_AddService
 *
 * @brief   Initializes the Simple Profile service by registering
 *          GATT attributes with the GATT server.
 *
 * @param   services - services to add. This is a bit map and can
 *                     contain more than one service.
 *
 * @return  Success or Failure
 */
bStatus_t SimpleProfile_AddService( uint32 services )
{
  uint8 status;

  // Allocate Client Characteristic Configuration table
  simpleProfileChar4Config = (gattCharCfg_t *)ICall_malloc( sizeof(gattCharCfg_t) *
                                                            MAX_NUM_BLE_CONNS );
  if ( simpleProfileChar4Config == NULL )
  {
    return ( bleMemAllocError );
  }

  // Initialize Client Characteristic Configuration attributes
  GATTServApp_InitCharCfg( LINKDB_CONNHANDLE_INVALID, simpleProfileChar4Config );

  if ( services & SIMPLEPROFILE_SERVICE )
  {
    // Register GATT attribute list and CBs with GATT Server App
    status = GATTServApp_RegisterService( simpleProfileAttrTbl,
                                          GATT_NUM_ATTRS( simpleProfileAttrTbl ),
                                          GATT_MAX_ENCRYPT_KEY_SIZE,
                                          &simpleProfileCBs );
  }
  else
  {
    status = SUCCESS;
  }

  return ( status );
}

simple_gatt_profile.c :: SimpleProfile_AddService() – Initializing a Service: Allocate+init CCCD and tell GATT Server about attribute table.

The above example is from the Simple GATT Service example mentioned earlier.

Because Characteristic 4 is declared with the NOTIFY property and has a Client Characteristic Configuration descriptor attached, the value variable for that descriptor's attribute must be set up. The default state is that notification and indication is disabled until a Client writes to the descriptor.

The other thing that happens here is that the GATT Server is told about

  1. Where to find the attribute table,
  2. How many attributes it contains, and
  3. Which callbacks it should call when a client tries to Read or Write over the air.

The callbacks are the entry points for the GATT Server when it wants data from or has data for your Service, and is defined like this:

// Simple Profile Service Callbacks
CONST gattServiceCBs_t simpleProfileCBs =
{
  simpleProfile_ReadAttrCB,  // Read callback function pointer
  simpleProfile_WriteAttrCB, // Write callback function pointer
  NULL                       // Authorization callback function pointer
};

You will notice that this (gattServiceCBs_t) is a callback structure defined by the GATT Server, and your service simply provides an instance of this structure pointing to your handlers. We will get back to the implementation of these further down.

Application callbacks

When something is done to the Service over the air, the GATT Server will as mentioned above invoke the callback handlers given to GATTServApp_RegisterService. But how do you tell the application that something has changed in the service?

Enter: The application callback. The Service can call this when data is received.

// Callback when a characteristic value has changed
typedef void (*simpleProfileChange_t)( uint8 paramID );

typedef struct
{
  simpleProfileChange_t        pfnSimpleProfileChange;  // Called when characteristic value changes
} simpleProfileCBs_t;

simple_gatt_profile.h :: GATT Service callbacks – Definition of application callback

This is absolutely and completely application dependent. If you would like to also tell the application if somebody has read a value, or you would like to not notify the application about something changing, feel free to change this or leave it out completely.

// Signature of the callback function in the application.
static void SimplePeripheral_charValueChangeCB(uint8_t paramID);


// Simple GATT Profile Callbacks
static simpleProfileCBs_t SimplePeripheral_simpleProfileCBs =
{
  SimplePeripheral_charValueChangeCB // Characteristic value change callback
};

// Registering the callback during application init looks like this.
SimpleProfile_RegisterAppCBs(&SimplePeripheral_simpleProfileCBs);

// Implementation of the callback function in the application.
static void SimplePeripheral_charValueChangeCB(uint8_t paramID)
{
  // Check paramID, call GetParameter to get data, then act on it

  // Note: This executes in the Stack Task's context!
  //       That means no BLE APIs are available. It also means
  //       that you should not spend any significant processing time here.
}

simple_peripheral.c – Using the application callback

Similar to the GATT callbacks to the service, the application declares an instance of the simpleProfileCBs_t callback structure pointing to the callback handler, and gives this to the RegisterAppCBs function when the service is initialized.

It's important to note here that the callback to the application is typically invoked by the Service when it has received a callback from the GATT Server. This means that the RTOS Task context when executing the _charValueChangeCB for example is the Stack Task's context with the Stack Task's priority.

Typically you would want to do as little as possible in the callback handler because of this, so that critical Bluetooth Stack procedures are not delayed. The example application does nothing except send a message to the application's TI-RTOS Queue about which parameter changed. The application task can call the Service's GetParameter in its own context later and act on it.

Receiving a stream

If you are expecting a stream coming in from a peer device, it is recommended to immediately also copy the contents of the written data into a message to the application by using GetParameter in the application callback.

Otherwise new data may be received and placed in the Service's variables before the application task has the time to call GetParameter in its own context.

Set/GetParameter

These are Setters and Getters for the data present in the Characterisic Value attributes. For example:

bStatus_t SimpleProfile_SetParameter( uint8 param, uint8 len, void *value )
{
  bStatus_t ret = SUCCESS;
  switch ( param )
  {
    case SIMPLEPROFILE_CHAR1:
      if ( len == sizeof ( uint8 ) )
      {
        simpleProfileChar1 = *((uint8*)value);
      }
      else
      {
        ret = bleInvalidRange;
      }
      break;

simple_gatt_profile.c :: SimpleProfile_SetParameter – The case statements are indexes defined by the service.

The only thing going on here is that the length is checked (further validation can be added) and simpleProfileChar1 is updated with a new value.

Remember that a pointer to simpleProfileChar1 is in the attribute table, so when a Read Request is received from a peer device, that value will be read out and sent over the air.

The SetParameter can also serve as an interface for sending out notifications if these are enabled.

case SIMPLEPROFILE_CHAR4:
  if ( len == sizeof ( uint8 ) )
  {
    simpleProfileChar4 = *((uint8*)value);

    // See if Notification has been enabled
    GATTServApp_ProcessCharCfg( simpleProfileChar4Config, &simpleProfileChar4, FALSE,
                                simpleProfileAttrTbl, GATT_NUM_ATTRS( simpleProfileAttrTbl ),
                                INVALID_TASK_ID, simpleProfile_ReadAttrCB );

simple_gatt_profile.c :: SimpleProfile_SetParameter – The value is updated, and a notification is transmitted if enabled.

The interesting parts in the above are:

  • simpleProfileChar4 is updated with a new value
  • GATTServApp_ProcessCharCfg is told where to find the CCCD value (simpleProfileChar4Config) to check if notification is allowed by a Client.
  • It is also told how to perform a Read of simpleProfileChar4 as if it were the GATT Server.
    • Pointer to the Characteristic Value: simpleProfileChar4
    • Whether an authenticated link is required.
    • What's the attribute table, and how big is it?
    • If the characteristic is enabled for Indication, which task should receive the Confirmation.
    • What's the stack's interface to read this characteristic (simpleProfile_ReadAttrCB).

If notifications or indications are enabled, simpleProfile_ReadAttrCB will be called to collect the data that will be transmitted.

If this seems like a roundabout way of doing things, it is, but it ensures that you don't need to worry about whether you're allowed by the Client to send data, you don't need to know the dynamic handle of the value, and you don't need to format the ATT Notification packet manually.

For streaming applications however, it is recommended to manually implement the procedures performed by GATTServApp_ProcessCharCfg in order to correctly process the return value from the stack to avoid data loss.

GATT Server Callbacks

After the attribute table is registered in the GATT Server, it will be exposed to connecting GATT Clients. When values are read and written by a GATT Client, the GATT Server will route those requests to the registered callbacks.

Read callback

The Read callback is called as the GATT Server is processing the incoming message, in the Task context of the Stack thread, and is called with some parameters

Parameter Description
*pAttr Pointer to the attribute with the requested value (e.g. simpleProfileChar1) for your convenience
*pValue Pointer to where the read response payload should be copied by the callback.
*pLen Pointer you modify with the length of the data you have copied into *pValue
maxLen The maximum length you're allowed to copy into *pValue (ATT MTU size dependent)
offset If it's a Long Read (because MTU < value length) the peer can request the read starts at an offset into the value.
connHandle If more than one peer is connected, this is the handle of the requesting peer, should you wish to differentiate responses.
method You can return blePending from the callback, and based on the method create your own ATT Rsp within 30 seconds.

A minimal working callback, given that all characteristics are 3 bytes long looks like this:

static bStatus_t myService_ReadAttrCB(uint16_t connHandle,
                                      gattAttribute_t *pAttr,
                                      uint8_t *pValue, uint16_t *pLen,
                                      uint16_t offset, uint16_t maxLen,
                                      uint8_t method)
{
  *pLen = 3;                                // maxLen is always 20 or higher
  memcpy(pValue, pAttr->pValue, *pLen);     // We've made all the value variables at least 3 bytes long.
  return SUCCESS;                           // Tell the GATT Server to send the response from *pValue.
}

Minimal working read callback

Quiz

What happens in the minimal example if an attribute is requested to be read which does not have read permissions?

Considering Characteristic 1 from earlier, what would pAttr->type.uuid be if this read callback is called because a peer wants to read the Characteristic Value of that Characteristic?

If a Read request is sent by a peer device, which attribute's value can be sent as a response by myService_ReadAttrCB?

In the table in the section Attribute table, what is the handle of the Value attribute of that characteristic?

Usually it makes sense to respond differently depending on which attribute is requested. Below is a generic pattern that should work for most cases regardless of attribute UUID length, attribute value length and Attribute Protocol MTU size.

static bStatus_t myService_ReadAttrCB(uint16_t connHandle,
                                      gattAttribute_t *pAttr,
                                      uint8_t *pValue, uint16_t *pLen,
                                      uint16_t offset, uint16_t maxLen,
                                      uint8_t method)
{
  bStatus_t status = SUCCESS;

  // See if request is regarding the Cool Characteristic Value
  if ( ! memcmp(pAttr->type.uuid, myCoolCharUuid, pAttr->type.len) )
  {
    if ( offset > MYCOOLCHAR_LEN )  // Prevent malicious ATT ReadBlob offsets.
    {
      status = ATT_ERR_INVALID_OFFSET;
    }
    else
    {
      *pLen = MIN(maxLen, MYCOOLCHAR_LEN - offset);  // Transmit as much as possible
      memcpy(pValue, pAttr->pValue + offset, *pLen);
    }
  }
  // See if request is regarding some other char value.
  else if ( ! memcmp(...) )
  {
     ...
  }
  else
  {
    // If we get here, that means you've forgotten to add an if clause for a
    // characteristic value attribute in the attribute table that has READ permissions.
    *pLen = 0;
    status = ATT_ERR_ATTR_NOT_FOUND;
  }

  return status;
}

Generic read callback – An alternative is to build a 16-bit UUID from parts of the requested UUID and do a switch-case.

Quiz

  1. What's memcmp? What's compared? What's the compare length?

  2. What would happen if you had an attribute with a 16-bit UUID equal to the beginning of an 128-bit UUID used by another attribute?

Write callback

Similar to the read callback, this callback is also called from from the GATT Server in the Stack Task context.

Parameter Description
*pAttr Pointer to the attribute with the requested value (e.g. simpleProfileChar1) provided for your convenience
*pValue Pointer to received data.
len Length of received data pointed to by *pValue
offset If it's a Long Write (because MTU < value length) the peer can request the write starts at an offset into the value.
connHandle If more than one peer is connected, this is the handle of the requesting peer, should you wish to differentiate responses.
method You can return blePending from the callback, and based on the method create your own ATT Rsp within 30 seconds.

Again, you could imagine a minimal functional callback

static bStatus_t myService_WriteAttrCB(uint16_t connHandle,
                                           gattAttribute_t *pAttr,
                                           uint8_t *pValue, uint16_t len,
                                           uint16_t offset, uint8_t method)
{
  // Copy pValue into the variable we point to from the attribute table.
  memcpy(pAttr->pValue, pValue, len);
  return SUCCESS;
}

Minimal write callback: Received data is copied to the memory location of pAttr->pValue in its entirety.

However, for the write callback, there are some more considerations

  1. It's not safe to rely on the length of the received data, although buffer overflow attacks can be fun.
  2. If there's a CCCD in the attribute table, writes to this will be routed here and must be taken care of
  3. For writes it would make sense to tell the application that the data has changed.

A more generic write callback could look like this:

static bStatus_t myService_WriteAttrCB(uint16_t connHandle,
                                       gattAttribute_t *pAttr,
                                       uint8_t *pValue, uint16_t len,
                                       uint16_t offset, uint8_t method)
{
  bStatus_t status  = SUCCESS;
  uint8_t   paramID = 0xFF;

  // See if request is regarding a Client Characteristic Configuration
  if ( ! memcmp(pAttr->type.uuid, clientCharCfgUUID, pAttr->type.len) )
  {
    // Allow both notifications and indication.
    status = GATTServApp_ProcessCCCWriteReq( connHandle, pAttr, pValue, len,
                                             offset, GATT_CLIENT_CFG_NOTIFY |
                                             GATT_CLIENT_CFG_INDICATE );
  }
  // See if request is regarding the Cool Characteristic Value
  else if ( ! memcmp(pAttr->type.uuid, myCoolCharUuid, pAttr->type.len) )
  {
    if ( offset + len > MYCOOLCHAR_LEN )
    {
      status = ATT_ERR_INVALID_OFFSET;
    }
    else
    {
      // Copy pValue into the variable we point to from the attribute table.
      memcpy(pAttr->pValue + offset, pValue, len);

      // Only notify application if entire expected value is written
      if ( offset + len == MYCOOLCHAR_LEN)
        paramID = MYSERVICE_MYCOOLCHAR;
    }
  }
  // See if request is regarding some other char value.
  else if ( ! memcmp(...) )
  {
     ...
  }
  else
  {
    // If we get here, that means you've forgotten to add an if clause for a
    // characteristic value attribute in the attribute table that has WRITE permissions.
    status = ATT_ERR_ATTR_NOT_FOUND;
  }

  // Let the application know something changed (if it did) by using the
  // callback it registered earlier (if it did).
  if (paramID != 0xFF)
    if ( pAppCBs && pAppCBs->pfnChangeCb )
      pAppCBs->pfnChangeCb( paramID ); // Call app function from stack task context.

  return status;
}

Generic write callback – Looks daunting, but is mostly copy/paste.

Task 1 – Create the Files

Training solution

The solution to these exercises are contained within the expandable tabs. You can simply copy and paste the contents of the solution files into their associated files.

Solution files for sunlightService.c and sunlightService.h are named sunlightService.c and sunlightService.h. Those 2 files are generated with the Example service generator provided in this training module. Note the solution includes the bonus tasks.

You can find the added code by searching for SOLUTION in the project_zero diff below. Most of the added code in project_zero.c will not be needed until Task 4. For the changes that are not in-line in the existing code, the added functions are at the end of the file.

/**********************************************************************************************
 * Filename:       sunlightService.c
 *
 * Description:    This file contains the implementation of the service.
 *
 * Copyright (c) 2015-2019, Texas Instruments Incorporated
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * *  Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * *  Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * *  Neither the name of Texas Instruments Incorporated nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *************************************************************************************************/


/*********************************************************************
 * INCLUDES
 */
#include <string.h>

#include <icall.h>

/* This Header file contains all BLE API and icall structure definition */
#include "icall_ble_api.h"

#include "sunlightService.h"

/*********************************************************************
 * MACROS
 */

/*********************************************************************
 * CONSTANTS
 */

/*********************************************************************
 * TYPEDEFS
 */

/*********************************************************************
* GLOBAL VARIABLES
*/

// sunlightService Service UUID
CONST uint8_t sunlightServiceUUID[ATT_BT_UUID_SIZE] =
{
  LO_UINT16(SUNLIGHTSERVICE_SERV_UUID), HI_UINT16(SUNLIGHTSERVICE_SERV_UUID)
};

// sunlightValue UUID
CONST uint8_t sunlightService_SunlightValueUUID[ATT_UUID_SIZE] =
{
  TI_BASE_UUID_128(SUNLIGHTSERVICE_SUNLIGHTVALUE_UUID)
};
// updatePeriod UUID
CONST uint8_t sunlightService_UpdatePeriodUUID[ATT_UUID_SIZE] =
{
  TI_BASE_UUID_128(SUNLIGHTSERVICE_UPDATEPERIOD_UUID)
};

/*********************************************************************
 * LOCAL VARIABLES
 */

static sunlightServiceCBs_t *pAppCBs = NULL;

/*********************************************************************
* Profile Attributes - variables
*/

// Service declaration
static CONST gattAttrType_t sunlightServiceDecl = { ATT_BT_UUID_SIZE, sunlightServiceUUID };

// Characteristic "SunlightValue" Properties (for declaration)
static uint8_t sunlightService_SunlightValueProps = GATT_PROP_NOTIFY;

// Characteristic "SunlightValue" Value variable
static uint8_t sunlightService_SunlightValueVal[SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN] = {0};

// Characteristic "SunlightValue" CCCD
static gattCharCfg_t *sunlightService_SunlightValueConfig;
// Characteristic "UpdatePeriod" Properties (for declaration)
static uint8_t sunlightService_UpdatePeriodProps = GATT_PROP_READ | GATT_PROP_WRITE;

// Characteristic "UpdatePeriod" Value variable
static uint8_t sunlightService_UpdatePeriodVal[SUNLIGHTSERVICE_UPDATEPERIOD_LEN] = {0};

/*********************************************************************
* Profile Attributes - Table
*/

static gattAttribute_t sunlightServiceAttrTbl[] =
{
  // sunlightService Service Declaration
  {
    { ATT_BT_UUID_SIZE, primaryServiceUUID },
    GATT_PERMIT_READ,
    0,
    (uint8_t *)&sunlightServiceDecl
  },
    // SunlightValue Characteristic Declaration
    {
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ,
      0,
      &sunlightService_SunlightValueProps
    },
      // SunlightValue Characteristic Value
      {
        { ATT_UUID_SIZE, sunlightService_SunlightValueUUID },
        GATT_PERMIT_READ,
        0,
        sunlightService_SunlightValueVal
      },
      // SunlightValue CCCD
      {
        { ATT_BT_UUID_SIZE, clientCharCfgUUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE,
        0,
        (uint8 *)&sunlightService_SunlightValueConfig
      },
    // UpdatePeriod Characteristic Declaration
    {
      { ATT_BT_UUID_SIZE, characterUUID },
      GATT_PERMIT_READ,
      0,
      &sunlightService_UpdatePeriodProps
    },
      // UpdatePeriod Characteristic Value
      {
        { ATT_UUID_SIZE, sunlightService_UpdatePeriodUUID },
        GATT_PERMIT_READ | GATT_PERMIT_WRITE,
        0,
        sunlightService_UpdatePeriodVal
      },
};

/*********************************************************************
 * LOCAL FUNCTIONS
 */
static bStatus_t sunlightService_ReadAttrCB( uint16_t connHandle, gattAttribute_t *pAttr,
                                           uint8_t *pValue, uint16_t *pLen, uint16_t offset,
                                           uint16_t maxLen, uint8_t method );
static bStatus_t sunlightService_WriteAttrCB( uint16_t connHandle, gattAttribute_t *pAttr,
                                            uint8_t *pValue, uint16_t len, uint16_t offset,
                                            uint8_t method );

/*********************************************************************
 * PROFILE CALLBACKS
 */
// Simple Profile Service Callbacks
CONST gattServiceCBs_t sunlightServiceCBs =
{
  sunlightService_ReadAttrCB,  // Read callback function pointer
  sunlightService_WriteAttrCB, // Write callback function pointer
  NULL                       // Authorization callback function pointer
};

/*********************************************************************
* PUBLIC FUNCTIONS
*/

/*
 * SunlightService_AddService- Initializes the SunlightService service by registering
 *          GATT attributes with the GATT server.
 *
 */
extern bStatus_t SunlightService_AddService( uint8_t rspTaskId )
{
  uint8_t status;

  // Allocate Client Characteristic Configuration table
  sunlightService_SunlightValueConfig = (gattCharCfg_t *)ICall_malloc( sizeof(gattCharCfg_t) * linkDBNumConns );
  if ( sunlightService_SunlightValueConfig == NULL )
  {
    return ( bleMemAllocError );
  }

  // Initialize Client Characteristic Configuration attributes
  GATTServApp_InitCharCfg( LINKDB_CONNHANDLE_INVALID, sunlightService_SunlightValueConfig );
  // Register GATT attribute list and CBs with GATT Server App
  status = GATTServApp_RegisterService( sunlightServiceAttrTbl,
                                        GATT_NUM_ATTRS( sunlightServiceAttrTbl ),
                                        GATT_MAX_ENCRYPT_KEY_SIZE,
                                        &sunlightServiceCBs );

  return ( status );
}

/*
 * SunlightService_RegisterAppCBs - Registers the application callback function.
 *                    Only call this function once.
 *
 *    appCallbacks - pointer to application callbacks.
 */
bStatus_t SunlightService_RegisterAppCBs( sunlightServiceCBs_t *appCallbacks )
{
  if ( appCallbacks )
  {
    pAppCBs = appCallbacks;

    return ( SUCCESS );
  }
  else
  {
    return ( bleAlreadyInRequestedMode );
  }
}

/*
 * SunlightService_SetParameter - Set a SunlightService parameter.
 *
 *    param - Profile parameter ID
 *    len - length of data to right
 *    value - pointer to data to write.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate
 *          data type (example: data type of uint16 will be cast to
 *          uint16 pointer).
 */
bStatus_t SunlightService_SetParameter( uint8_t param, uint16_t len, void *value )
{
  bStatus_t ret = SUCCESS;
  switch ( param )
  {
    case SUNLIGHTSERVICE_SUNLIGHTVALUE_ID:
      if ( len == SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN )
      {
        memcpy(sunlightService_SunlightValueVal, value, len);

        // Try to send notification.
        GATTServApp_ProcessCharCfg( sunlightService_SunlightValueConfig, (uint8_t *)&sunlightService_SunlightValueVal, FALSE,
                                    sunlightServiceAttrTbl, GATT_NUM_ATTRS( sunlightServiceAttrTbl ),
                                    INVALID_TASK_ID,  sunlightService_ReadAttrCB);
      }
      else
      {
        ret = bleInvalidRange;
      }
      break;

    case SUNLIGHTSERVICE_UPDATEPERIOD_ID:
      if ( len == SUNLIGHTSERVICE_UPDATEPERIOD_LEN )
      {
        memcpy(sunlightService_UpdatePeriodVal, value, len);
      }
      else
      {
        ret = bleInvalidRange;
      }
      break;

    default:
      ret = INVALIDPARAMETER;
      break;
  }
  return ret;
}


/*
 * SunlightService_GetParameter - Get a SunlightService parameter.
 *
 *    param - Profile parameter ID
 *    value - pointer to data to write.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate
 *          data type (example: data type of uint16 will be cast to
 *          uint16 pointer).
 */
bStatus_t SunlightService_GetParameter( uint8_t param, uint16_t *len, void *value )
{
  bStatus_t ret = SUCCESS;
  switch ( param )
  {
    case SUNLIGHTSERVICE_UPDATEPERIOD_ID:
      memcpy(value, sunlightService_UpdatePeriodVal, SUNLIGHTSERVICE_UPDATEPERIOD_LEN);
      break;

    default:
      ret = INVALIDPARAMETER;
      break;
  }
  return ret;
}


/*********************************************************************
 * @fn          sunlightService_ReadAttrCB
 *
 * @brief       Read an attribute.
 *
 * @param       connHandle - connection message was received on
 * @param       pAttr - pointer to attribute
 * @param       pValue - pointer to data to be read
 * @param       pLen - length of data to be read
 * @param       offset - offset of the first octet to be read
 * @param       maxLen - maximum length of data to be read
 * @param       method - type of read message
 *
 * @return      SUCCESS, blePending or Failure
 */
static bStatus_t sunlightService_ReadAttrCB( uint16_t connHandle, gattAttribute_t *pAttr,
                                       uint8_t *pValue, uint16_t *pLen, uint16_t offset,
                                       uint16_t maxLen, uint8_t method )
{
  bStatus_t status = SUCCESS;

  // See if request is regarding the SunlightValue Characteristic Value
if ( ! memcmp(pAttr->type.uuid, sunlightService_SunlightValueUUID, pAttr->type.len) )
  {
    if ( offset > SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN )  // Prevent malicious ATT ReadBlob offsets.
    {
      status = ATT_ERR_INVALID_OFFSET;
    }
    else
    {
      *pLen = MIN(maxLen, SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN - offset);  // Transmit as much as possible
      memcpy(pValue, pAttr->pValue + offset, *pLen);
    }
  }
  // See if request is regarding the UpdatePeriod Characteristic Value
else if ( ! memcmp(pAttr->type.uuid, sunlightService_UpdatePeriodUUID, pAttr->type.len) )
  {
    if ( offset > SUNLIGHTSERVICE_UPDATEPERIOD_LEN )  // Prevent malicious ATT ReadBlob offsets.
    {
      status = ATT_ERR_INVALID_OFFSET;
    }
    else
    {
      *pLen = MIN(maxLen, SUNLIGHTSERVICE_UPDATEPERIOD_LEN - offset);  // Transmit as much as possible
      memcpy(pValue, pAttr->pValue + offset, *pLen);
    }
  }
  else
  {
    // If we get here, that means you've forgotten to add an if clause for a
    // characteristic value attribute in the attribute table that has READ permissions.
    *pLen = 0;
    status = ATT_ERR_ATTR_NOT_FOUND;
  }

  return status;
}


/*********************************************************************
 * @fn      sunlightService_WriteAttrCB
 *
 * @brief   Validate attribute data prior to a write operation
 *
 * @param   connHandle - connection message was received on
 * @param   pAttr - pointer to attribute
 * @param   pValue - pointer to data to be written
 * @param   len - length of data
 * @param   offset - offset of the first octet to be written
 * @param   method - type of write message
 *
 * @return  SUCCESS, blePending or Failure
 */
static bStatus_t sunlightService_WriteAttrCB( uint16_t connHandle, gattAttribute_t *pAttr,
                                        uint8_t *pValue, uint16_t len, uint16_t offset,
                                        uint8_t method )
{
  bStatus_t status  = SUCCESS;
  uint8_t   paramID = 0xFF;

  // See if request is regarding a Client Characteristic Configuration
  if ( ! memcmp(pAttr->type.uuid, clientCharCfgUUID, pAttr->type.len) )
  {
    // Allow only notifications.
    status = GATTServApp_ProcessCCCWriteReq( connHandle, pAttr, pValue, len,
                                             offset, GATT_CLIENT_CFG_NOTIFY);
  }
  // See if request is regarding the UpdatePeriod Characteristic Value
  else if ( ! memcmp(pAttr->type.uuid, sunlightService_UpdatePeriodUUID, pAttr->type.len) )
  {
    if ( offset + len > SUNLIGHTSERVICE_UPDATEPERIOD_LEN )
    {
      status = ATT_ERR_INVALID_OFFSET;
    }
    else
    {
      // Copy pValue into the variable we point to from the attribute table.
      memcpy(pAttr->pValue + offset, pValue, len);

      // Only notify application if entire expected value is written
      if ( offset + len == SUNLIGHTSERVICE_UPDATEPERIOD_LEN)
        paramID = SUNLIGHTSERVICE_UPDATEPERIOD_ID;
    }
  }
  else
  {
    // If we get here, that means you've forgotten to add an if clause for a
    // characteristic value attribute in the attribute table that has WRITE permissions.
    status = ATT_ERR_ATTR_NOT_FOUND;
  }

  // Let the application know something changed (if it did) by using the
  // callback it registered earlier (if it did).
  if (paramID != 0xFF)
    if ( pAppCBs && pAppCBs->pfnChangeCb )
      pAppCBs->pfnChangeCb(connHandle, paramID, len, pValue); // Call app function from stack task context.

  return status;
}

sunlightService.c

/**********************************************************************************************
 * Filename:       sunlightService.h
 *
 * Description:    This file contains the sunlightService service definitions and
 *                 prototypes.
 *
 * Copyright (c) 2015-2019, Texas Instruments Incorporated
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * *  Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * *  Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * *  Neither the name of Texas Instruments Incorporated nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *************************************************************************************************/


#ifndef _SUNLIGHTSERVICE_H_
#define _SUNLIGHTSERVICE_H_

#ifdef __cplusplus
extern "C"
{
#endif

/*********************************************************************
 * INCLUDES
 */

/*********************************************************************
* CONSTANTS
*/

#ifdef DeviceFamily_CC26X0R2
  #define LINKDB_CONNHANDLE_INVALID CONNHANDLE_INVALID
#endif //DeviceFamily_CC26X0R2

// Service UUID
#define SUNLIGHTSERVICE_SERV_UUID 0xBA55

//  Characteristic defines
#define SUNLIGHTSERVICE_SUNLIGHTVALUE_ID   0
#define SUNLIGHTSERVICE_SUNLIGHTVALUE_UUID 0x2BAD
#define SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN  4

//  Characteristic defines
#define SUNLIGHTSERVICE_UPDATEPERIOD_ID   1
#define SUNLIGHTSERVICE_UPDATEPERIOD_UUID 0x2BAE
#define SUNLIGHTSERVICE_UPDATEPERIOD_LEN  2

/*********************************************************************
 * TYPEDEFS
 */

/*********************************************************************
 * MACROS
 */

/*********************************************************************
 * Profile Callbacks
 */

// Callback when a characteristic value has changed
typedef void (*sunlightServiceChange_t)(uint16_t connHandle, uint8_t paramID, uint16_t len, uint8_t *pValue);

typedef struct
{
  sunlightServiceChange_t        pfnChangeCb;  // Called when characteristic value changes
  sunlightServiceChange_t        pfnCfgChangeCb;
} sunlightServiceCBs_t;



/*********************************************************************
 * API FUNCTIONS
 */


/*
 * SunlightService_AddService- Initializes the SunlightService service by registering
 *          GATT attributes with the GATT server.
 *
 */
extern bStatus_t SunlightService_AddService( uint8_t rspTaskId);

/*
 * SunlightService_RegisterAppCBs - Registers the application callback function.
 *                    Only call this function once.
 *
 *    appCallbacks - pointer to application callbacks.
 */
extern bStatus_t SunlightService_RegisterAppCBs( sunlightServiceCBs_t *appCallbacks );

/*
 * SunlightService_SetParameter - Set a SunlightService parameter.
 *
 *    param - Profile parameter ID
 *    len - length of data to right
 *    value - pointer to data to write.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate
 *          data type (example: data type of uint16 will be cast to
 *          uint16 pointer).
 */
extern bStatus_t SunlightService_SetParameter(uint8_t param, uint16_t len, void *value);

/*
 * SunlightService_GetParameter - Get a SunlightService parameter.
 *
 *    param - Profile parameter ID
 *    value - pointer to data to write.  This is dependent on
 *          the parameter ID and WILL be cast to the appropriate
 *          data type (example: data type of uint16 will be cast to
 *          uint16 pointer).
 */
extern bStatus_t SunlightService_GetParameter(uint8_t param, uint16_t *len, void *value);

/*********************************************************************
*********************************************************************/

#ifdef __cplusplus
}
#endif

#endif /* _SUNLIGHTSERVICE_H_ */

sunlightService.h


@@ -80,6 +80,8 @@
 #include <profiles/project_zero/data_service.h>
 #include <profiles/oad/cc26xx/oad.h>

+#include "sunlightService.h"
+
 /* Includes needed for reverting to factory and erasing external flash */
 #include <ti/common/cc26xx/oad/oad_image_header.h>
 #include <ti/drivers/dpl/HwiP.h>
@@ -144,6 +144,7 @@
#define PZ_SEND_PARAM_UPD_EVT    8  /* Request parameter update req be sent        */
#define PZ_CONN_EVT              9  /* Connection Event End notice                 */
#define PZ_READ_RPA_EVT         10  /* Read RPA event                              */
+#define PZ_MSG_PERIODIC_TIMER   11 /* Timer has expired, set characteristic value */ // SOLUTION

@@ -152,6 +153,9 @@
// Connection interval conversion rate to miliseconds
#define CONN_INTERVAL_MS_CONVERSION           1.25

+// Default timeout of sunlight timer
+#define DEFAULT_SUNLIGHT_TIMEOUT              5000  // SOLUTION
+
 /*********************************************************************
  * TYPEDEFS
  */
@@ -371,6 +377,10 @@ static Clock_Handle button1DebounceClockHandle;
 static uint8_t button0State = 0;
 static uint8_t button1State = 0;

+//Clock struct for Periodic Notifications
+static Clock_Struct myClock; // SOLUTION
+static uint32_t myVar = 0;
+
 // Variable used to store the number of messages pending once OAD completes
 // The application cannot reboot until all pending messages are sent
 static uint8_t numPendingMsgs = 0;
@@ -479,6 +489,11 @@ static void ProjectZero_bootManagerCheck(PIN_Handle buttonPinHandle,
                                          uint8_t revertIo,
                                          uint8_t eraseIo);

+// SOLUTION BEGIN
+static void sunCharChanged(uint16_t connHandle, uint8_t paramID, uint16_t len, uint8_t *pValue);
+static void myClockSwiFxn(uintptr_t arg0);
+// SOLUTION END
+
 /*********************************************************************
  * EXTERN FUNCTIONS
  */
@@ -529,6 +544,13 @@ static oadTargetCBs_t ProjectZero_oadCBs =
     .pfnOadWrite = ProjectZero_processOadWriteCB // Write Callback.
 };

+// SOLUTION: Sunlight Service callback handler.
+// The type Data_ServiceCBs_t is defined in sunlightService.h
+static sunlightServiceCBs_t ProjectZero_Sunlight_ServiceCBs = {
+    .pfnChangeCb = sunCharChanged,
+    .pfnCfgChangeCb = NULL
+};
+
 /*********************************************************************
  * PUBLIC FUNCTIONS
  */
@@ -588,6 +610,17 @@ static void ProjectZero_init(void)
     Queue_construct(&appMsgQueue, NULL);
     appMsgQueueHandle = Queue_handle(&appMsgQueue);

+    // clockParams is only used during init and can be on the stack.
+    Clock_Params myClockParams;
+    // Insert default params
+    Clock_Params_init(&myClockParams);
+    // Set a period, so it times out periodically without jitter
+    myClockParams.period = DEFAULT_SUNLIGHT_TIMEOUT * (1000/Clock_tickPeriod),
+    // Initialize the clock object / Clock_Struct previously added globally.
+    Clock_construct(&myClock, myClockSwiFxn,
+                    0, // Initial delay before first timeout
+                    &myClockParams);
+
     // ******************************************************************
     // Hardware initialization
     // ******************************************************************
@@ -677,6 +710,7 @@ static void ProjectZero_init(void)
     LedService_AddService(selfEntity);
     ButtonService_AddService(selfEntity);
     DataService_AddService(selfEntity);
+    SunlightService_AddService( selfEntity ); // SOLUTION

     // Open the OAD module and add the OAD service to the application
     if(OAD_SUCCESS != OAD_open(OAD_DEFAULT_INACTIVITY_TIME))
@@ -712,6 +746,7 @@ static void ProjectZero_init(void)
     LedService_RegisterAppCBs(&ProjectZero_LED_ServiceCBs);
     ButtonService_RegisterAppCBs(&ProjectZero_Button_ServiceCBs);
     DataService_RegisterAppCBs(&ProjectZero_Data_ServiceCBs);
+    SunlightService_RegisterAppCBs( &ProjectZero_Sunlight_ServiceCBs); // SOLUTION

     // Placeholder variable for characteristic intialization
     uint8_t initVal[40] = {0};
@@ -729,6 +764,19 @@ static void ProjectZero_init(void)
     DataService_SetParameter(DS_STRING_ID, sizeof(initString), initString);
     DataService_SetParameter(DS_STREAM_ID, DS_STREAM_LEN, initVal);

+    // SOLUTION BEGIN
+    // Initalization of characteristics in sunlightService that are readable.
+    uint8_t sunlightService_sunlightValue_initVal[SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN] = {0};
+    SunlightService_SetParameter(SUNLIGHTSERVICE_SUNLIGHTVALUE_ID,
+                                  SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN,
+                                  sunlightService_sunlightValue_initVal);
+
+    uint16_t sunlightService_sunlightPeriod_initVal = DEFAULT_SUNLIGHT_TIMEOUT;
+    SunlightService_SetParameter(SUNLIGHTSERVICE_UPDATEPERIOD_ID,
+                                  SUNLIGHTSERVICE_UPDATEPERIOD_LEN,
+                                  &sunlightService_sunlightPeriod_initVal);
+    // SOLUTION END
+
     // Start Bond Manager and register callback
     VOID GAPBondMgr_Register(&ProjectZero_BondMgrCBs);

@@ -1131,6 +1179,16 @@ static void ProjectZero_processApplicationMessage(pzMsg_t *pMsg)
         ProjectZero_processConnEvt((Gap_ConnEventRpt_t *)(pMsg->pData));
         break;

+      // SOLUTION BEGIN
+      case PZ_MSG_PERIODIC_TIMER:
+      {
+          SunlightService_SetParameter(SUNLIGHTSERVICE_SUNLIGHTVALUE_ID,
+                                        SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN,
+                                        &myVar);
+          myVar++;
+      }
+      // SOLUTION END
+
       default:
         break;
     }
@@ -1258,6 +1316,8 @@ static void ProjectZero_processGapMessage(gapEventHdr_t *pMsg)
                         ANSI_COLOR(FG_GREEN)"%s"ANSI_COLOR(ATTR_RESET),
                       (uintptr_t)addrStr);

+            Clock_start(Clock_handle(&myClock)); //SOLUTION
+
             // If we are just connecting after an OAD send SVC changed
             if(sendSvcChngdOnNextBoot == TRUE)
             {
@@ -1293,6 +1353,8 @@ static void ProjectZero_processGapMessage(gapEventHdr_t *pMsg)
         // Cancel the OAD if one is going on
         // A disconnect forces the peer to re-identify
         OAD_cancel();
+
+        Clock_stop(Clock_handle(&myClock)); //SOLUTION
     }
     break;

@@ -2031,6 +2093,14 @@ static void ProjectZero_handleButtonPress(pzButtonState_t *pState)
         ButtonService_SetParameter(BS_BUTTON0_ID,
                                    sizeof(pState->state),
                                    &pState->state);
+
+          // SOLUTION BEGIN
+        SunlightService_SetParameter(SUNLIGHTSERVICE_SUNLIGHTVALUE_ID,
+                                     SUNLIGHTSERVICE_SUNLIGHTVALUE_LEN,
+                                     &myVar);
+        myVar++;
+          // SOLUTION END
+
         break;
     case CONFIG_PIN_BTN2:
         ButtonService_SetParameter(BS_BUTTON1_ID,
@@ -2978,5 +3048,43 @@ static void ProjectZero_bootManagerCheck(PIN_Handle buttonPinHandle,
     }
 }

+// SOLUTION BEGIN
+/*
+ * @brief   SWI handler function for periodic clock expiry
+ *
+ * @param   arg0 - Passed by TI-RTOS clock module
+ */
+static void myClockSwiFxn(uintptr_t arg0)
+{
+  // Can't call blocking TI-RTOS calls or BLE APIs from here.
+  // .. Send a message to the Task that something is afoot.
+  ProjectZero_enqueueMsg(PZ_MSG_PERIODIC_TIMER, NULL); // Not sending any data here, just a signal
+}
+
+/*
+ * @brief   Callback from Sunlight service to app
+ *          Invoked on characteristic value changed
+ *
+ * @param   connHandle - connection handle on which the transaction occurred
+ * @param   paramID - ID of the char that is written to
+ * @param   len - Length of data written
+ * @param   pValue - Pointer to new data that is written
+ */
+static void sunCharChanged(uint16_t connHandle, uint8_t paramID, uint16_t len, uint8_t *pValue)
+{
+
+    if((paramID == SUNLIGHTSERVICE_UPDATEPERIOD_ID) &&
+        (len == SUNLIGHTSERVICE_UPDATEPERIOD_LEN))
+    {
+        uint16_t myTimeoutInMs = BUILD_UINT16(pValue[0],
+                                              pValue[1]);
+        Clock_stop(Clock_handle(&myClock));
+        Clock_setPeriod(Clock_handle(&myClock), myTimeoutInMs * (1000/Clock_tickPeriod));
+        Clock_start(Clock_handle(&myClock));
+    }
+
+}
+// SOLUTION END
+

project_zero_soln.c

First we have to create the files that will implement our new service.

  1. Scroll down to the Example service generator and generate the shell for your service

    • Service name – sunlightService
    • Service UUID – 0xBA55
      • Use 16-bit Service UUID
  2. Open the ProjectZero project as done in the BLE Basics lab.

  3. Right click on the Application folder and choose New → Folder. Call this folder services.

  4. Right click on the services folder and choose New → File. Call this file sunlightService.h.

  5. Paste in the generated content.

  6. Repeat for sunlightService.c

  7. Add the Application/services to the #include path by opening the project properties, navigating to Build → Arm Compiler → Include Options, and adding ${PROJECT_LOC}/Application/services to the list.

  8. Compile the project.

Task 2 – Register the Service and Verify

Now you have an empty shell for your service which should compile cleanly. Now it's time to add your new service to the GATT Server.

You will notice that the application-snippets.c under Example service generator section contains some code fragments that can be used in the application task to do this.

  1. Open the file Application/project_zero.c in the editor
  2. Near the top of the file, locate the #include directives
  3. Add an include directive for your service header file
  4. Locate the service initialization segment of ProjectZero_init()
  5. Insert the SunlightService_AddService(...); snippet from the application-snippets.c under Example service generator there.
  6. Add the Application/services directory to the #include search paths by accessing the project Properties → Build → ARM Compiler → Include Options and adding a new path to ${PROJECT_LOC}/Application/services.
  7. Build your project and download to the device
  8. Connect to your device and look at the attribute table. Can you find your service listed?

Note

Many mobile devices do not perform a service and characteristic discovery on each connection with a peer, instead relying on GATT information cached from their initial connection with the peer. This means your GATT table may be outdated. If you do not see your new service, refer to manufacturer instructions to clear the Bluetooth cached data from your mobile device.

Task 3 – Add a Characteristic

At this stage, when the GATT Server knows where to read the attribute table of your service, any change to the table is reflected in what a remote client sees when it performs a service and characteristic discovery.

You will now add a characteristic called sunlightValue to your attribute table. The characteristic should have the UUID 0x2BAD and be readable and able to send notifications. The UUID should be 128-bits. You will also implement the handling of read requests, the API for the application to set a value, and the mechanism for sending notifications if this is enabled by the GATT Client. The value should be 4 bytes long.

Quiz

What attributes must be added to the attribute table to accomplish this? Select multiple.

What must the permissions for the Characteristic Value attribute be?

What must the properties - visible to a GATT Client - be?

In this task we will only add the characteristic to the attribute table and verify that it is discovered remotely.

It is recommended to build your service manually the first time. Refer to the description and examples in the Implementation of a Service section above for inspiration.

Note

If you use the Example service generator, then Task 3 and 4 will be done for you by the generated code.

Once you have built your service manually, it is a good idea to go back and compare it with the example service generator.

The only thing not described above is how a Client Characteristic Configuration Descriptor attribute looks.

  • UUID is stored in a common variable: clientCharCfgUUID. Length of this is 16 bits.
  • It has Read and Write permissions
  • The attribute value entry points to a uint8_t casted (uint8_t *)&YourConfigVariable pointer.
  • It should be placed as the next attribute after the Characteristic Value attribute.

Reads to this are handled by the GATT Server, and writes must be handled by the service's Write-callback. In the generated service shell, the write to a CCCD is handled.

However, to avoid writing to NULL (causes an exception), you must also add allocation and initialization of the CCCD variable. Refer to the AddService section for an example.

  1. Add Characteristic Declaration attribute and necessary variables and defines.
  2. Add Characteristic Value attribute and necessary variables and defines.
  3. Add Client Characteristic Configuration Descriptor attribute and necessary variables and initialization.
  4. Connect and do a service discovery. It should look something like the below, if service UUID is 16-bit and characteristic UUID 128-bit.

Quiz

Considering the code for the service implementation, why is 0x2BAD in the middle of a long string of hexadecimal numbers in the picture above, and which statements are true below?


Try to read your attribute. What happens? Why?

Task 4 – Add API and Callback Handling

Now you have exposed a service with a characteristic to the world via the GATT Server, and you have tied remote actions on this service to callbacks in your implementation.

As you observed in the last task, you don't yet get any data when you try to read your characteristic. That will change now.

  1. See GATT Server Callbacks
  2. In sunlightService_ReadAttrCB in your code, insert an if-clause that compares the requested UUID with the UUID of your characteristic.
  3. Optionally, inside the if-clause, set *pLen equal to 4, or follow the generic pattern
  4. Copy the value of pAttr->pValue into pValue. The length is 4, or optionally following the generic pattern and using the #define you have made for the length.
    • What does pAttr point to? What is the data type? What about pAttr->pValue?
  5. Connect and try to read the characteristic. What is the value?
  6. Change the initial value of the Characteristic Value attribute's value field. Re-flash and try to read it again.

Now you have only static data, but what you really want is for your application to update the characteristic data.

  1. See Set/GetParameter
  2. In your service, add a case to SunlightService_SetParameter(..) which accepts a value of 4 bytes and updates the Characteristic Value attribute's value field.
    • That means update the variable pointed to in the attribute table. You may have named this sunlightService_SunlightValueVal.
    • The param is an index you have defined in the service's header file. In the text it's called SUNLIGHTSERVICE_SUNLIGHTVALUE_ID and has the value 0.
  3. In your application, after you have added your service, add a call to SunlightService_SetParameter(YOUR_PARAM_IDX, YOUR_LENGTH, &yourFourByteArrayWithSomeValue);
  4. Connect and read your characteristic. Does it match the value you set it to?

Task 5 – Update and Send Some Data

In this task you will add an attempt to send a notification to the connected GATT Client every time you update the value of the SunlightValue characteristic. You will also update the SunlightValue whenever a button is pressed.

  1. See GATT Server Callbacks, the code example Update and Notify and the explanation.
  2. In SunlightService_SetParameter(..) in the case you added for the SunlightValue characteristic, add a call to GATTServApp_ProcessCharCfg(..) and fill in the parameters as explained, but referring to your service and your data.
  3. In the application task, find ProjectZero_handleButtonPress and add a call to your SetParameter function in the case for Board_BUTTON0 (or BUTTON1, your choice).
    • Since 32-bit variables are 4 bytes long, you can give a pointer to a 32-bit variable to the SetParameter function.
  4. The value you set should be increased by 1 every time the button is pressed.
  5. Connect, enable notifications and press the button.

Task 6 – Make the Update Periodic

Here you will add a TI-RTOS clock object, configure it, start it, and create a Swi handler and a Task context handler. In a real world application, this is maybe where you would read your sensor value and update the attribute value.

Since this workshop is about the BLE SDK and not TI-RTOS, the clock part will be explained in some detail below.

Adding and initializing a clock

The Clock struct is part of the TI-RTOS Clock module. A Clock is set up to have either a periodic timeout or to be one-shot. When it expires it will call the callback function you specified when it was set up. In order to use the Clock module, you first need to add a global Clock_Struct myClock variable. Project Zero also includes Clock structs for button debouncing. You can add the new Clock with the the button debouncing Clock structs.

// Default timeout of sunlight timer
#define DEFAULT_SUNLIGHT_TIMEOUT              5000  // SOLUTION

project_zero.c :: CONSTANTS – Define default period

// Clock struct for Periodic Notifications
static Clock_Struct myClock;

project_zero.c :: Global Variables – Add new Clock Struct

To set it up, add the following code to Project Zero Init some point before it's to be used you need to do something like this:

// clockParams is only used during init and can be on the stack.
Clock_Params myClockParams;
// Insert default params
Clock_Params_init(&myClockParams);
// Set a period, so it times out periodically without jitter
myClockParams.period = DEFAULT_SUNLIGHT_TIMEOUT * (1000/Clock_tickPeriod),
// Initialize the clock object / Clock_Struct previously added globally.
Clock_construct(&myClock, myClockSwiFxn,
                0, // Initial delay before first timeout
                &myClockParams);

project_zero.c :: ProjectZero_init – TI-RTOS Clock Initialization

As you probably noticed, the Clock_construct call takes a callback function as argument. In this case we're telling the Clock interface that there's a callback with the name myClockSwiFxn that it should call whenever this clock object times out.

The signature of this parameter is typedef Void (*Clock_FuncPtr)(UArg); which means the callback function must be a void function which takes an UArg argument, which is really uintptr_t if you want to keep to stdint.h types.

void myClockSwiFxn(uintptr_t arg0)
{
  // Can't call blocking TI-RTOS calls or BLE APIs from here.
  // .. Send a message to the Task that something is afoot.
  ProjectZero_enqueueMsg(PZ_MSG_PERIODIC_TIMER, NULL); // Not sending any data here, just a signal
}

project_zero.c :: myClockSwiFxn – Clock callback

In the above, PZ_MSG_PERIODIC_TIMER doesn't exist yet, and must be added to the defined symbols near the top of project_zero.c.

// Types of messages that can be sent to the user application task from other
// tasks or interrupts. Note: Messages from BLE Stack are sent differently.
#define PZ_SERVICE_WRITE_EVT     0  /* A characteristic value has been written     */
#define PZ_SERVICE_CFG_EVT       1  /* A characteristic configuration has changed  */
#define PZ_UPDATE_CHARVAL_EVT    2  /* Request from ourselves to update a value    */
#define PZ_BUTTON_DEBOUNCED_EVT  3  /* A button has been debounced with new value  */
#define PZ_PAIRSTATE_EVT         4  /* The pairing state is updated                */
#define PZ_PASSCODE_EVT          5  /* A pass-code/PIN is requested during pairing */
#define PZ_ADV_EVT               6  /* A subscribed advertisement activity         */
#define PZ_START_ADV_EVT         7  /* Request advertisement start from task ctx   */
#define PZ_SEND_PARAM_UPD_EVT    8  /* Request parameter update req be sent        */
#define PZ_CONN_EVT              9  /* Connection Event End notice                 */
#define PZ_MSG_PERIODIC_TIMER    10 /* Timer has expired, set characteristic value */

project_zero.c :: CONSTANTS – PZ Message Types

When this is added, the code will compile, but nothing will really happen because the function ProjectZero_processApplicationMessage doesn't know what to do about PZ_MSG_PERIODIC_TIMER messages and will ignore them.

All that remains now is to add the handling of the timeout message, and at some point also start the timer by calling Clock_start(Clock_handle(&myClock));.

  • Add handling that increments a global variable and updates the characteristic value like in the previous task.

Bonus squared: Reconfigure the timeout value

  • Add another characteristic which is writable and readable and accepts 2 bytes (16-bit).
  • Call this characteristic updatePeriod.
  • Add write and read GATT-callback handling.
  • Add registration of application callbacks.
  • Add a YourService_GetParameter case in the service so the application can get the value that's written.
  • Make a callback to the application when the updatePeriod is written to.
  • Change the period of the clock. See Clock_setPeriod
    • It is not recommended to do processing in the callback, since it runs in the BLE Stack's Task context, but for this example it doesn't matter.
    • Note: Calling BLE Stack APIs (icall_ble_api.h) from the context of this callback (stack thread) will never work.

Again, because this isn't really about TI-RTOS, see below how to change the period.

  uint32_t myTimeoutInMs; // Place received period value here
  Clock_stop(Clock_handle(&myClock));
  Clock_setPeriod(Clock_handle(&myClock), myTimeoutInMs * (1000/Clock_tickPeriod));
  Clock_start(Clock_handle(&myClock));

Reconfiguring the clock period

The Clock APIs above can be called from any context.

Bonus – A peek at the GATT Builder

GATT Builder Preview

Please note the GATT Builder tool has not undergone testing and should not be used for production software.

The GATT Builder is a new tool being developed inside CCS/SysConfig. In this task we will look at the preview version.

  1. Import the simple_peripheral_gatt_builder_preview to CCS. This example application is found in the Resource Explorer.

  2. Open SysConfig.

  3. The GATT Builder tool is found under RF StacksBLEServices.

As you can see the simple gatt profile is defined with its services and characteristics. It is possible to add or remove services.

For each characteristic, the following properties can be changed:

  • Name
  • Description
  • UUID and UUID size
  • Value length
  • Default value
  • Properties
  • Permissions

After building the project you can look at the generated files. For the service, the relevant files are ble_gatt_service.c and .h. If you compare these files to simple_gatt_profile.c and .h you will see many similarities.

Example service generator

You can use the example service generator to generate services, characteristics and simple code snippets. Please note the generated code has not been tested.

The Example Service Generator is implemented with Project Zero in mind. Other BLE5-Stack example projects may handle services differently than Project Zero, which means you will have to re-write the application code snippets to suit them. One example is Simple Peripheral which has a different default Characteristic Value change callback function than Project Zero (compare SimplePeripheral_charValueChangeCB to another API such as ProjectZero_DataService_CfgChangeCB).

A Note About Syntax

Please use good C programming practices when selecting a characteristic and service name using the service generator. If there are spaces or otherwise forbidden C syntax, you will experience build errors.

Service


Generated output

The content which is output below is a header file for the generated service. It defines the UUIDs for the service and its characteristics, and the API for the service. It must be #included in code files which wants to use the service APIs.

//C Output
Header file
//C Output
Service implementation
//C Output
Application snippets

References

TI BLE5-Stack User's Guide

Bluetooth SIG

SIG GATT Profiles Overview

Bluetooth Interoperability and Profiles

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.