微波EDA网,见证研发工程师的成长!
首页 > 研发问答 > 无线和射频 > TI蓝牙设计交流 > 请教CC2541中主、从机之间数据传输的过程

请教CC2541中主、从机之间数据传输的过程

时间:10-02 整理:3721RD 点击:

在从机中进行如下设置,就可向主机发送数据。

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.

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).
HandleUUIDValue
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 Valueattribute, 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 Valueshould 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:

HandleUUIDValueWhat
.. ... .... ....
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)DefinitionValueMeaning
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 Declarationsays the actual useful value of the characteristic can be found at handle 32, and because its UUID matches the UUIDfound 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.

HandleUUIDValue
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

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.

HandleUUIDValue
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.

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 Characteristicwhich Characteristic Value always reads as the string 'LASER'.

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

HandleUUIDValueWhat
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 handleUUID/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 or ATT_UUID_SIZE which is 128-bit)
    • A pointer to UUID character array of that length.
  • Handle is not initialized by the user, but left as 0.
  • The value is not stored in the attribute table, merely a pointer to the variable holding the value.
  • Permissions tells the GATT Server what requests can be let through to the Service's callbacks.

Characteristic Value Properties

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

For example, the value of simpleProfileChar1Props below is GATT_PROP_READ | GATT_PROP_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.

Attribute table

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

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

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

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

Attribute variables

As an overview, these are the variables needed to initialize the attribute table seen above. These can be found in 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 the attribute 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

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

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

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

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

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

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

GATT Server Callbacks

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

Read callback

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

ParameterDescription
*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

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

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

Write callback

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

ParameterDescription
*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

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

A more generic write callback could look like this:

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 SOLUTIONin 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.

  1. 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
  1. Open the ProjectZero App and Stack project as done in the BLE Basics lab.

  2. The App project can be renamed to for example SunlightPeripheralApp. It will still refer to the same stack project (which can be shared)

  3. Right click on the Application/ folder and choose New → File. Call this file sunlightService.h.

  4. Paste in the generate content.

  5. Repeat for sunlightService.c

  6. 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.

  1. Open the file Application/project_zero.c in the editor
  2. Near the top of the file, locate the #include directives
  3. Add an include directive for your service header file
  4. Locate the service initialization segment of ProjectZero_init()
  5. Insert your SunlightService_AddService(); there.
  6. Build your project and download to the device
  7. 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.

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

Task 3

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.

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

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

  1. See Set/GetParameter
  2. In your service, add a case to SunlightService_SetParameter(..) which accepts a value of 4 bytes and updates the 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 called SUNLIGHTSERVICE_SUNLIGHTVALUE and has the value 0.
  3. In your application, after you have added your service, add a call to SunlightService_SetParameter(YOUR_PARAM_IDX, YOUR_LENGTH, &yourFourByteArrayWithSomeValue);
  4. Connect and read your characteristic. Does it match the value you set it to?

Task 5 – Update and send some data

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

  1. See GATT Server Callbacks, the code example Update and Notify and the explanation.
  2. In SunlightService_SetParameter(..) in the case you added for the SunlightValue characteristic, add a call to GATTServApp_ProcessCharCfg(..) and fill in the parameters as explained, but referring to your service and your data.
  3. In the application task, find 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.
  4. The value you set should be increased by 1 every time the button is pressed.
  5. Connect, enable notifications and press the button.

Task 6 – 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 voidfunction 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

Copyright © 2017-2020 微波EDA网 版权所有

网站地图

Top