Bluetooth low energy Custom Profile#
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 Bluetooth LE 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 development boards are:
Other SimpleLink Academy Labs#
Completion of Bluetooth low energy 5 Fundamentals
Software#
CCS 12.1 or IAR 8.50.9 installed with support for CC13xx/CC26xx devices.
BTool (located in the
tools\ble5stack
directory of the SDK installation)
Recommended reading#
Developing a Custom Application Chapter of the TI BLE5-Stack User’s Guide
Generic Attribute Profile (GATT) Chapter of the TI BLE5-Stack User’s Guide
ATT and GATT Introduction#
For Bluetooth low energy, communication occurs over the air according to the
Attribute Protocol (ATT)
. From a Bluetooth LE 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. |
Note
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
.
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 Service
→ Characteristic
→ Value
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 (Handles are assigned dynamically and are used over the air to address attributes.) |
0x2803 (Type of attribute. Tells you how to interpret the value. 0x2803 means that this attribute is a Characteristic Declaration) |
|
Characteristic Declaration (See the table below for an explanation of the Value of this type of attribute.) |
32 |
0xBBAA (Type of attribute. If this was |
41:42:43 (This is the actual value of the Characteristic. Could be anything you want, but a |
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’.
Note
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: |
32 |
|
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.
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
Note
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. |
Note
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#
/**
* @brief GATT Attribute format.
*
* @note
* The list must start with a Service attribute followed by
* all attributes associated with this Service attribute.
*/
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;
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 orATT_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.
Warning
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_WRITE
→ 0x0A
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
},
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 |
F0:FF |
SimpleProfileService Declaration. Readable. |
31 |
0x2803 |
0A:20:00:F1:FF |
‘Characteristic 1’ Declaration. Readable. |
32 |
0xFFF1 |
01 |
‘Char 1’ Value Attribute. Readable/Writable. |
The variables primaryServiceUUID
and characterUUID
are defined in
gatt_uuid.c
(located at \source\ti\ble5stack\host) 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 simple_peripheral_CC26X2R1_LAUNCHXL_tirtos7_ticlang
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 bytesThe array with the UUID is pointed to by a
gattAttrType_t
struct which contains the length of the array and a pointer to the arrayThe
gattAttrType_t
struct is pointed to by theattribute 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 };
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;
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 );
}
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
Where to find the attribute table,
How many attributes it contains, and
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;
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 // Simple GATT 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.
}
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-RTOS7 Queue about which parameter changed. The application task can call the
Service’s GetParameter
in its own context later and act on it.
Warning
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 and Get Parameters#
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;
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 );
The interesting parts in the above are:
simpleProfileChar4
is updated with a new valueGATTServApp_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. |
*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 |
maxLen |
The maximum length you’re allowed to copy into |
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 |
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.
}
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;
}
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. |
*pValue |
Pointer to received data. |
len |
Length of received data pointed to by |
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 |
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;
}
However, for the write callback, there are some more considerations
It’s not safe to rely on the length of the received data, although buffer overflow attacks can be fun.
If there’s a
CCCD
in the attribute table, writes to this will be routed here and must be taken care ofFor 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;
}
Task 1 – Create the Files#
Note
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.
Expand to see solution files
/**********************************************************************************************
* Filename: sunlightService.c
*
* Description: This file contains the implementation of the service.
*
* Copyright (c) 2015-2022, 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;
}
/**********************************************************************************************
* Filename: sunlightService.h
*
* Description: This file contains the sunlightService service definitions and
* prototypes.
*
* Copyright (c) 2015-2022, 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_ */
@@ -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_GPIO_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-RTOS7 clock module
+ */
+static void myClockSwiFxn(uintptr_t arg0)
+{
+ // Can't call blocking TI-RTOS7 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
+
First we have to create the files that will implement our new service.
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
Open the
ProjectZero
project as done in theBLE Basics
lab.Right click on the
Application
folder and choose New → Folder. Call this folderservices
.Right click on the
services
folder and choose New → File. Call this filesunlightService.h
.Paste in the generated content.
Repeat for
sunlightService.c
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.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.
Open the file
Application/project_zero.c
in the editorNear the top of the file, locate the
#include
directivesAdd an include directive for your service header file
Locate the service initialization segment of
ProjectZero_init()
Insert the
SunlightService_AddService(...);
snippet from the application-snippets.c under Example service generator there.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
.Build your project and download to the device
Connect to your device and look at the attribute table. Can you find your service listed?
Note
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.
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
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.
Add Characteristic Declaration attribute and necessary variables and defines.
Add Characteristic Value attribute and necessary variables and defines.
Add Client Characteristic Configuration Descriptor attribute and necessary variables and initialization.
Connect and do a service discovery. It should look something like the below, if service UUID is 16-bit and characteristic UUID 128-bit.
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.
In
sunlightService_ReadAttrCB
in your code, insert an if-clause that compares the requested UUID with the UUID of your characteristic.Optionally, inside the if-clause, set
*pLen
equal to 4, or follow the generic patternCopy the value of
pAttr->pValue
intopValue
. 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 aboutpAttr->pValue
?
Connect and try to read the characteristic. What is the value?
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.
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 calledSUNLIGHTSERVICE_SUNLIGHTVALUE_ID
and has the value0
.
In your application, after you have added your service, add a call to
SunlightService_SetParameter(YOUR_PARAM_IDX, YOUR_LENGTH, &yourFourByteArrayWithSomeValue);
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.
See GATT Server Callbacks, the code example Update and Notify and the explanation.
In
SunlightService_SetParameter(..)
in the case you added for the SunlightValue characteristic, add a call toGATTServApp_ProcessCharCfg(..)
and fill in the parameters as explained, but referring to your service and your data.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.
The value you set should be increased by 1 every time the button is pressed.
Connect, enable notifications and press the button.
Task 6 – Make the Update Periodic#
Here you will add a TI-RTOS7 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-RTOS7, the clock part will be explained in some detail below.
Adding and initializing a clock#
The Clock
struct is part of the TI-RTOS7 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
// Clock struct for Periodic Notifications
static Clock_Struct myClock;
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);
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-RTOS7 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
}
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 */
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-RTOS7, 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));
The Clock
APIs above can be called from any context.
Bonus – A peek at the GATT Builder#
Warning
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 as part of SysConfig. In this task we will look at the preview version available for both CCS and IAR.
Note
Note The GATT Builder Preview example is currently available for the following devices:
LAUNCHXL-CC26x2R1
LAUNCHXL-CC1352P
LAUNCHXL-CC1352R
LP-CC2652PSIP
LP-CC2652RSIP
LP-CC2652RB
Import the
simple_peripheral_gatt_builder_preview
:In CCS this example is found in the Resource Explorer
In IAR this is found at:
\examples\rtos\<your_board>\ble5stack\simple_peripheral_gatt_builder_preview\tirtos7\iar
Open SysConfig.
The GATT Builder tool is found under RF Stacks → BLE → Services.
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#
Warning
The Example Service Generator migration is still underway. At this time, the generator is fully functional, but the visual representation will change in a future release.
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.