请教CC2541中主、从机之间数据传输的过程
在从机中进行如下设置,就可向主机发送数据。
static attHandleValueNoti_t pReport;
pReport.len = numBytes;
pReport.handle = 0x0035;
osal_memcpy(pReport.value, buf, numBytes);
GATT_Notification( 0, &pReport, FALSE );
在主机中使用GATT_RegisterForInd( simpleBLETaskId );注册ATT Indications/Notifications,然后在使用以下程序主机就可处理从机发来的数据
else if ( ( pMsg->method == ATT_HANDLE_VALUE_NOTI ) ) //通知
{
if( pMsg->msg.handleValueNoti.handle == 0x0035) //CHAR6的通知 串口打印
{
NPI_WriteTransport(pMsg->msg.handleValueNoti.value, pMsg->msg.handleValueNoti.len );
}
}
以上过程和Characteristic 6有什么关系?或者说二者在哪里建立了关系呢?
许多资料里都在讲主、从机之间是利用Characteristic Value进行信息交流的。那么对于Characteristic 的具体操作是什么样的流程呢?
从机发送时用到了pReport.handle = 0x0035;,主机接收时用到了pMsg->msg.handleValueNoti.handle == 0x0035。
其中0x0035与Characteristic 6有什么关系呢?在哪里建立的关系呢?
这个请参考TI_BLE_Software_Developer's_Guide,路径 C:\Texas Instruments\BLE-CC254x-1.4.2.2\Documents
您好,我在您推荐的Texas Instruments CC2540/41 Bluetooth® low energy Software Developer’s Guide v1.4.1文档的5.4.1 GATT Characteristics / Attributes小节中看到对Client Characteristic Configuration的描述是: this allows the GATT server to configure the characteristic to be notified (sent to GATT server) or indicated (sent to GATT server and expect an ACK).
此处是不是有笔误啊, notified和indicated是从server 发送到Client 的吧?notified和indicated后面的括号里的内容是不是应该是sent to GATT Client ?
请您帮忙解答一下疑问。
是写错了,应该是到client
先学习下蓝牙的基本概念吧, BLE深度培训文档
www.deyisupport.com/.../13244.aspx
另外,TI新的Academy,可以快速帮你学习蓝牙
http://dev.ti.com/tirex/#/?link=Software%2FSimpleLink%20CC2640R2%20SDK%2FSimpleLink%20Academy%2FOverview
BLE一些术语的概念
http://dev.ti.com/tirex/content/simplelink_academy_cc2640r2sdk_1_13_03_11/modules/ble_01_custom_profile/ble_01_custom_profile.html
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 BLE SDK
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 ProjectZero
project to include the required services and modify the behavior according to our own specification.
Prerequisites
Other trainings
- Completion of Bluetooth Low Energy Fundamentals workshop
Software
- CCS 7.x installed with support for CC13xx/CC26xx devices
- SimpleLink CC2640R2 SDK 1.40.0
- BTool (located in tools->blestack directory of the SimpleLink CC2640R2 SDK installation.
Hardware
- CC2640R2 LaunchPad
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 length exceeds this, aGATT ReadLongCharValue
or GATT WriteLongCharValue
, which splits the data up, must be performed.
Characteristic
Several of these attributes are needed to define a Characteristic
. A Characteristic
always consists of a at least Value
attribute, where you in your Profile
have decided what the value means, and a Declaration
. 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?
A Characteristic consists of at least a Declaration and a Value attributeThe Attribute Protocol defines what a Service and Characteristic isA Profile describes the Attributes in a Service
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 | 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 BLE Stack the handles are assigned starting from 1 when the attribute is registered with the GATT Server. If the firmware doesn't change, the handle is always the same.
UUID
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.
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.
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 capabilites of the device.
Quiz
The Attribute Protocol is a meta-layer on top of the Generic Attribute Protocol.
Yes it is No, of course not
Characteristics are used to build Attributes
No, it's opposite Indeed they are
The interpretation of an Attribute's value-field depends on that attribute's
Handle UUID/Type Characteristic declaration
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 and Device Monitor don't know what the UUIDs represent since they're just made up. Edit <BTool_Install>\BToolGattUuid.xml
or <DevMon_Install>\config\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
Select text
/** * 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;
Attribute Entry (gatt.h) – 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 orATT_UUID_SIZE
which is 128-bit) - A pointer to UUID character array of that length.
- Length of UUID (
- 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_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 SimpleGATTProfile included in the SimpleBLEPeripheral sample application, it looks like this:
Select text
/* See the code boxes further down to see how the variables in this array are defined. */ static gattAttribute_t simpleProfileAttrTbl[] = { // 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 },
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 | 0x2800primaryServiceUUID |
F0:FFsimpleProfileService |
SimpleProfileService Declaration. Readable. |
31 | 0x2803characterUUID |
0A:20:00:F1:FFsimpleProfileChar1Props |
'Characteristic 1' Declaration. Readable. |
32 | 0xFFF1simpleProfilechar1UUID |
01simpleProfileChar1 |
'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 simpleGATTprofile.h
and simpleGATTprofile.c
under Profiles/
in the SimpleBLEPeripheral project in the BLE SDK.
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 theattribute table
Select text
// 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 Variables – simpleProfileService 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.
Service Declaration attribute's UUID Service Declaration attribute's Value
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.
Select text
// 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 *simpleProfileChar1CharConfig;
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.
Characteristic Declaration attribute's ValueCharacteristic Value attribute's UUID
In which Attribute's field is the actual data-value of the Characteristic found by a peer device? Found as simpleProfileChar1
above.
Characteristic Declaration attribute's ValueCharacteristic Value attribute's ValueCharacteristic Value attribute's Type
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(..)
.
Select text
bStatus_t SimpleProfile_AddService( uint32 services ) { uint8 status; // Allocate Client Characteristic Configuration table simpleProfileChar4Config = (gattCharCfg_t *)ICall_malloc( sizeof(gattCharCfg_t) * linkDBNumConns ); if ( simpleProfileChar4Config == NULL ) { return ( bleMemAllocError ); } // Initialize Client Characteristic Configuration attributes GATTServApp_InitCharCfg( INVALID_CONNHANDLE, 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 ); }
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
- 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:
Select text
// 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 };
GATT Service callbacks – Implemented by the service to handle read-, write- and authorization request.
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.
Select text
// 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;
Definition of application callback – Example use below
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.
Select text
// Signature of the callback function in the application. static void SimpleBLEPeripheral_charValueChangeCB(uint8_t paramID); // Simple GATT Profile Callbacks static simpleProfileCBs_t simpleBLEPeripheral_simpleProfileCBs = { SimpleBLEPeripheral_charValueChangeCB // Characteristic value change callback }; // Registering the callback during application init looks like this. SimpleProfile_RegisterAppCBs(&simpleBLEPeripheral_simpleProfileCBs); // Implementation of the callback function in the application. static void SimpleBLEPeripheral_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. }
Using the application callback – This how it looks from the application.
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:
Select text
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;
SetParameter example – The case
s 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.
Select text
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 );
Update and Notify – The value is updated, and a notification is transmitted if enabled.
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
).
- Pointer to the Characteristic Value:
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:
Select text
static bStatus_t myService_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr, uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen, uint8 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?
Security risk GATT Server will reject before callback
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?
Array of 0xF1, 0xFF ATT_UUID_SIZE Array of 0x03, 0x28
If a Read
request is sent by a peer device, which attribute's value can be sent as a response by myService_ReadAttrCB?
All attributes Descriptor and Value attributes Value attributes
In the table in the section Attribute table, what is the handle of the Value attribute of that characteristic?
30 31 32
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.
Select text
static bStatus_t myService_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr, uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen, uint8 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
-
What's
memcmp
? What's compared? What's the compare length? -
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
Select text
static bStatus_t myService_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr, uint8 *pValue, uint8 len, uint16 offset, uint8 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
- 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 of - For writes it would make sense to tell the application that the data has changed.
A more generic write callback could look like this:
Select text
static bStatus_t myService_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr, uint8 *pValue, uint8 len, uint16 offset, uint8 method ) { bStatus_t status = SUCCESS; uint8_t paramID = 0xFF; // See if request is regarding a Client Characterisic 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_soln.c
and sunlightService.h
. Those 2 files are generated with the Example service generator provided in this training module.
Solution file for project_zero.c is named project_zero_soln.c
. You can find the added code by searching for SOLUTION
in the project_zero_soln.c
. Most of the added code in project_zero_soln.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 custom profile solution file : sunlighService_soln.c
Expand to see custom profile solution file : sunlighService.h
Expand to see custom profile solution file : project_zero_soln.c
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
- Expand to 128-bit not selected
-
Open the
ProjectZero
App and Stack project as done in theBLE Basics
lab. -
The
App
project can be renamed to for exampleSunlightPeripheralApp
. It will still refer to the same stack project (which can be shared) -
Right click on the
Application/
folder and choose New → File. Call this filesunlightService.h
. -
Paste in the generate content.
-
Repeat for
sunlightService.c
-
Compile the project.
Task 2 – Register the service and verify
Now you have an empty shell for your service which on a should compile cleanly. Now it's time to add your new service to the GATT Server.
You will notice that the Application usage snippets 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 editor - Near the top of the file, locate the
#include
directives - Add an include directive for your service header file
- Locate the service initialization segment of
ProjectZero_init()
- Insert your
SunlightService_AddService();
there. - Build your project and download to the device
- Connect to your device and look at the attribute table. Can you find your service listed?
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. 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 atribute table to accomplish this? Select multiple.
Characteristic Declaration Characteristic User Description Characteristic Value Service Declaration Client Configuration Descriptor
What must the permissions for the Characteristic Value attribute be?
GATT_PERMIT_WRITE GATT_PERMIT_READ
What must the properties - visible to a GATT Client - be?
GATT_PROP_WRITE GATT_PROP_WRITE_NO_RSP GATT_PROP_READ GATT_PROP_NOTIFY
In this task we will only add the characteristic to the attribute table and verify that it is discovered remotely.
There are two ways you can solve this. Either skip down to the Example service generator and play around with adding a service, noting the changes your settings have on the output and copy the hopefully correct snippets into your service, or refer to the description and examples in the Implementation of a Service section above and do it manually.
The only thing not described above is how a Client Characterisic 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 Characterisic Configuration Descriptor attribute and necessary variables and initialization.
- Connect and do a service discovery. It should looke 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?
It's expanded from 16- to 128-bit as a matter of convenience.An arbitrary 128-bit string of numbers can be used instead of expanding a 16-bit value.It's placed inside TI_BASE_UUID_128, a predefined macro for expansion to 128-bit.All proprietary services and characteristics must be 128-bit.Shorter UUIDs can be bought from the BT SIG for use with proprietary services.
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.
- See GATT Server Callbacks
- In
sunlightService_ReadAttrCB
in your code, insert an if-clause that compares the requested UUID with the UUID of your characteristic.- For now, disregard the Notification aspect, and don't call
GATTServApp_ProcessCharCfg(..)
when updating the value.
- For now, disregard the Notification aspect, and don't call
- Optionally, inside the if-clause, set
*pLen
equal to 4, or follow the generic pattern - Copy 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
?
- What does
- 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.
- See Set/GetParameter
- In your service, add a case to
SunlightService_SetParameter(..)
which accepts a value of 4 bytes and updates the Characterisic 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
and has the value0
.
- That means update the variable pointed to in the attribute table. You may have named this
- 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
user_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 – Bonus: 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
You first need to include ti/sysbios/knl/Clock.h
and add a global Clock_Struct myClock
variable. 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.
To set it up, at some point before it's to be used you need to do something like this:
Select text
// clockParams is only used during init and can be on the stack. Clock_Params clockParams; // Insert default params Clock_Params_init(&clockParams); // Set a period, so it times out periodically without jitter clockParams.period = 5000 * (1000/Clock_tickPeriod), // 5000 ms, conversion from ms to clock ticks. // Initialize the clock object / Clock_Struct previously added globally. Clock_construct(&myClock, myClockSwiFxn, 0, // Initial delay before first timeout &clockParams);
Initialization of a TI-RTOS clock.
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.
Select text
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. user_enqueueRawAppMsg(APP_MSG_PERIODIC_TIMER, NULL, 0); // Not sending any data here, just a signal }
Example clock swi
In the above, APP_MSG_PERIODIC_TIMER
doesn't exist yet, and must be added to the enum app_msg_types_t
defined near the top of project_zero.c
. When this is added, the code will compile, but nothing will really happen because the function user_processApplicationMessage
doesn't know what to do about APP_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 (
ICallBleAPI.c
) 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.
Select text
uint32_t myTimeoutInMs; // Place received period value here Clock_stop(Clock_handle(&myClock)); Clock_setPeriod(myTimeoutInMs * (1000/Clock_tickPeriod)); Clock_start(Clock_handle(&myClock));
Reconfiguring the clock period
The Clock
APIs above can be called from any context.
Example service generator
Bluetooth Developer Studio
This generator is a light version of a plugin for Bluetooth Developer Studio. Download the TI plugin at BDS Plugins.
See the workshop about Bluetooth Developer Studio for how to use that tool.
Difference to ProjectZero
Note that the output from the generator on this page does not match the implementation of the services and application in ProjectZero exactly, but they can coexist easily.
Similarities to ProjectZero
- Application callback handler immediately sends a message to the application which contains the incoming data.
Differences from ProjectZero
- No log statements
- No callback for Noti/Ind enable
- Application callback handler is unique per service
- Separate application message queue for service messages.
- Signature (parameters) of callback handler does not contain pointer to the received data.
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
Service name
Service UUID
16-bit 128-bit
Add Characteristic Generate
Include comments and #includes in output.
Generated Header File
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.
Select text
//C Output
Header file
Generated Implementation File
Select text
//C Output
Service implementation
Application usage snippets
Select text
//C Output
Application snippets
References
Bluetooth SIG
SIG GATT Profiles Overview