Introduction
The Texas Instruments SensorTag is a development kit for Bluetooth Smart developers. It has half a dozen sensors built in (temperature, humidity, pressure, accelerometer, gyroscope and magnetometer) and exposes their data as a series of Bluetooth Smart services. It can also act as a beacon.
See www.ti.com/sensortag for more information on SensorTag.
By the way.... this is not a product recommendation. It's just one developer sharing knowledge with other, like minded people! I'll write about other Bluetooth developer tools in the future.
Goals
To give myself something concrete to do, I decided to pursue the following goal:
Add two new custom services to Texas Instruments SensorTag named the Counter Service and the Random Number Service and defined as follows:
Counter Service: this service has a single characteristic with an 8 bit numeric value. It is possible to write a value directly to the characteristic but more importantly, every time the characteristic is read, its value must be incremented by 1, wrapping back around to 0x00 when at 0xFF.
Random Number Service: This service also has a single characteristic but this one is a 16 bit numeric value. Reading the characteristic returns a random 16 bit number but notifications are also supported and, with notifications enabled, a random value is generated and delivered to the connected GATT client as a notification, once every second.
Tools
To proceed with my project, I needed the right tools. SensorTag contains a Texas Instruments CC2541 chip which can be programmed using the IAR Workbench IDE. To work with SensorTag you need to use version 8.2, which is not the latest version. 8.3 is currently the latest version and it does not work seamlessly “out of the box” with SensorTag.
Figure 1 - IAR Workbench IDE version 8.2
|
I also used
BLE Device Monitor for testing and Smart RF Studio for flashing the SensorTag with my custom
firmware, built using IAR Workbench. The SensorTag Wiki (link at the end) provides
links to other resources you may find useful.
The hardware I used was my laptop, a SensorTag of course and a CC Debugger to allow me to flash the board over USB.
The hardware I used was my laptop, a SensorTag of course and a CC Debugger to allow me to flash the board over USB.
Figure 2 - SensorTag with a CC Debugger connected to it
|
Coding
My first task was to find the SensorTag source code in the BLE stack download. On my machine it’s in C:\Texas Instruments\BLE-CC254x-1.4.0\Projects\ble\SensorTag. I copied it to a new location so I could make changes whilst retaining the original.I changed the device name used in advertising packets to make it easy to identify the customized device as my own.
static uint8 scanRspData[] =
{
// complete name
0x0A, // length of this data
GAP_ADTYPE_LOCAL_NAME_COMPLETE,
0x4D, // 'M'
0x61, // 'a'
0x72, // 'r'
0x74, // 't'
0x69, // 'i'
0x6E, // 'n'
0x54, // 'T'
0x61, // 'a'
0x67, // 'g'
Figure 3 – Device name changed to ‘MartinTag’ in SensorTag.c
Next, using the structure of the standard SensorTag project as a guide, I created source files for each of my two new services; counterservice.c and counterservice.h for the Counter Service and of course randomnumberservice.c and randomnumberservice.h for the Random Number Service.
Service Definition
This is achieved by creating a C array containing pre-defined constants and other values and then making a function call to register them. I needed to do this for each of the two custom services. Let’s look at the definition of each and then how they were registered.static gattAttribute_t counterAttrTable[] =
{
{
// Service declaration
{ ATT_BT_UUID_SIZE, primaryServiceUUID }, /* type */
GATT_PERMIT_READ, /* permissions */
0, /* handle */
(uint8 *)&counterService /* pValue */
},
// Characteristic Declaration
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&counterDataProps
},
// Characteristic Value "Data"
{
{ MW_UUID_SIZE, counterDataUUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
counterData
}
};
Figure 4 – Counter Service definition in counterservice.cstatic gattAttribute_t randomNumberAttrTable[] =
{
{
// Service declaration
{ ATT_BT_UUID_SIZE, primaryServiceUUID }, /* type */
GATT_PERMIT_READ, /* permissions */
0, /* handle */
(uint8 *)&randomNumberService /* pValue */
},
// Characteristic Declaration
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&randomNumberDataProps
},
// Characteristic Value "Data"
{
{ MW_UUID_SIZE, randomNumberDataUUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
randomNumberData
},
// Random Number Client Characteristic Configuration
{
{ ATT_BT_UUID_SIZE, clientCharCfgUUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
(uint8 *) &randomNumberClientCharCfg
},
};
Figure 5 – Random NumberService definition in randomnumberservice.ctypedef 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;
Figure 6 – The gattAttribute_t typeFor example the characteristic containing the Counter Service’s counter value looks like this:
{
{ MW_UUID_SIZE, counterDataUUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
counterData
}
Figure 7 – Counter value characteristic
MW_UUID_SIZE has a value of 16, meaning 16 octets. It indicates the UUID is a 128 bit UUID in other words.
counterDataUUID is defined as
// in counterservice.c
static CONST uint8 counterDataUUID[MW_UUID_SIZE] =
{
MW_UUID(COUNTER_DATA_UUID),
};
// in counterservice.h
#define COUNTER_DATA_UUID 0x9915
#define MW_UUID(uuid) MW_BASE_UUID_128(uuid)
// MW Base 128-bit UUID: 3E09XXXX-293F-11E4-93BD-AFD0FE6D1DFD
#define MW_BASE_UUID_128( uuid ) 0xFD, 0x1D, 0x6D, 0xFE, 0xD0, 0xAF, 0xBD, 0x93, \
0xE4, 0x11, 0x3F, 0x29, LO_UINT16( uuid ), HI_UINT16( uuid ), 0x09, 0x3E
Figure 8 – Counter data value characteristic UUID definition
With a few handy macros, the 16 bit value of 0x9915 which I chose is mapped onto my 128 bit base value of 3E09XXXX-293F-11E4-93BD-AFD0FE6D1DFD to give the resultant 128 bit UUID for my characteristic 3E099915-293F-11E4-93BD-AFD0FE6D1DFD. Permissions are defined in gatt.h and as you can see, you can combine them with logical OR operations. Here’s the full list:
#define GATT_PERMIT_READ 0x01 //!< Attribute is Readable #define GATT_PERMIT_WRITE 0x02 //!< Attribute is Writable #define GATT_PERMIT_AUTHEN_READ 0x04 //!< Read requires Authentication #define GATT_PERMIT_AUTHEN_WRITE 0x08 //!< Write requires Authentication #define GATT_PERMIT_AUTHOR_READ 0x10 //!< Read requires Authorization #define GATT_PERMIT_AUTHOR_WRITE 0x20 //!< Write requires Authorization #define GATT_PERMIT_ENCRYPT_READ 0x40 //!< Read requires Encryption #define GATT_PERMIT_ENCRYPT_WRITE 0x80 //!< Write requires EncryptionFigure 9 – Counter data value characteristic UUID definition
Handle values get allocated at run time so we just supply a default value of 0 to begin with. Finally, our value is in the local variable counterData which is defined as
#define COUNTER_DATA_LEN 1 static uint8 counterData[COUNTER_DATA_LEN] = { 0 };Figure 10 – Counter data value local variable
Hopefully you can see the idea. Having defined my services, I next needed to register them and this required me to make some changes to SensorTag.c:
// Add services GGS_AddService( GATT_ALL_SERVICES ); // GAP GATTServApp_AddService( GATT_ALL_SERVICES ); // GATT attributes DevInfo_AddService(); // Device Information Service IRTemp_AddService (GATT_ALL_SERVICES ); // IR Temperature Service Accel_AddService (GATT_ALL_SERVICES ); // Accelerometer Service Humidity_AddService (GATT_ALL_SERVICES ); // Humidity Service Magnetometer_AddService( GATT_ALL_SERVICES ); // Magnetometer Service Barometer_AddService( GATT_ALL_SERVICES ); // Barometer Service Gyro_AddService( GATT_ALL_SERVICES ); // Gyro Service SK_AddService( GATT_ALL_SERVICES ); // Simple Keys Profile Test_AddService( GATT_ALL_SERVICES ); // Test Profile CcService_AddService( GATT_ALL_SERVICES ); // Connection Control Service Counter_AddService (GATT_ALL_SERVICES ); // Counter Service RandomNumber_AddService (GATT_ALL_SERVICES ); // Random Number ServiceFigure 11 – Registering my new services
The xxxx_AddService functions are implemented in the C file for that service, so counterservice.c and randomnumberservice.c in my case. The function essentially makes one call to a Texas Instruments GATT API function:
// Register GATT attribute list and CBs with GATT Server App status = GATTServApp_RegisterService( counterAttrTable, GATT_NUM_ATTRS( counterAttrTable ), &counterCBs );Figure 12 – Registering the counter service definition and call back functions
GATT_NUM_ATTRS is a macro which counts the number of attributes in the service definition array. The only other thing here, which I’ve not already mentioned is the call backs referenced by &counterCBs. This provides pointers to call back functions relating to operations such as reading and writing GATT attributes that belong to this service:
CONST gattServiceCBs_t counterCBs = { counter_ReadAttrCB, // Read callback function pointer counter_WriteAttrCB, // Write callback function pointer NULL // Authorization callback function pointer };Figure 13 – Counter service call back function pointers
When it comes to implementing service behaviours, as you’ll see, we’re largely concerned with implementing these call back functions. Let’s see what I had to do in this respect next.
Service Implementation
When the counter service’s counter value characteristic is read, I want to increment its value and return it to the client. Similarly, when the random number service’s random number characteristic is read, I want to generate a new, 16 bit random number and return this value. The code in each case is similar so let’s look at the counter service as an example./*********************************************************************
* @fn counter_ReadAttrCB
*
* @brief Read an attribute. Every time a GATT client device wants to read
* from an attribute in the profile, this function gets called.
*
* @param connHandle - connection message was received on
* @param pAttr - pointer to attribute
* @param pValue - pointer to data to be read
* @param pLen - length of data to be read
* @param offset - offset of the first octet to be read
* @param maxLen - maximum length of data to be read
*
* @return Success or Failure
*/
static uint8 counter_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr,
uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen )
{
uint16 uuid;
bStatus_t status = SUCCESS;
// If attribute permissions require authorization to read, return error
if ( gattPermitAuthorRead( pAttr->permissions ) )
{
// Insufficient authorization
return ( ATT_ERR_INSUFFICIENT_AUTHOR );
}
if (utilExtractUuid16(pAttr,&uuid) == FAILURE) {
// Invalid handle
*pLen = 0;
return ATT_ERR_INVALID_HANDLE;
}
switch ( uuid )
{
// No need for "GATT_SERVICE_UUID" or "GATT_CLIENT_CHAR_CFG_UUID" cases;
// gattserverapp handles those reads
case COUNTER_DATA_UUID:
*pLen = COUNTER_DATA_LEN;
// copy current counter value in counterData[0] to the buffer pointed to by pValue
osal_memcpy( pValue, &counterData[0], COUNTER_DATA_LEN );
// increment the counter
counterData[0]++;
break;
default:
*pLen = 0;
status = ATT_ERR_ATTR_NOT_FOUND;
break;
}
return ( status );
}
Figure 14 – Counter service attribute reading
We first check the permissions associated with the attribute being written to. If they allow this operation, we move on to extract the “significant” 16 bit part of the UUID of the attribute and then use it in a switch statement to decide what to do next. In this case, the switch statement only really supports one UUID, that of the only characteristic the counter service has. I’ve highlighted the code involved in writing to this characteristic. It’s pretty simple; we copy the current counter value to a location in memory pointed to by a pointer we received as an argument to the function and then increment our local counter value, ready for the next read operation.
We also allow writing to the counter service’s counter value and handle this in the function counter_WriteAttrCB. Let’s save looking at characteristic writing for when we look at the random number service however.
Random Number Service Implementation
This service generates a random number whenever its one characteristic is read or whenever a notification needs to be created and sent to the connected GATT client. To enable notifications, we need to handle the associated client characteristic configuration descriptor being written to. Let’s take a look at the code for this first. I implemented it in the call back function for characteristic writing in randomnumberservice.c. The function is called RandomNumber_WriteAttrCB./********************************************************************* * @fn RandomNumber_WriteAttrCB * * @brief Handles requests to write to attributes owned by this service. * * @param connHandle - connection message was received on * @param pAttr - pointer to attribute * @param pValue - pointer to data to be written * @param len - length of data * @param offset - offset of the first octet to be written * * @return Success or Failure */ static bStatus_t RandomNumber_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr, uint8 *pValue, uint8 len, uint16 offset ) { bStatus_t status = SUCCESS; uint16 uuid; // If attribute permissions require authorization to write, return error if ( gattPermitAuthorWrite( pAttr->permissions ) ) { // Insufficient authorization return ( ATT_ERR_INSUFFICIENT_AUTHOR ); } if (utilExtractUuid16(pAttr,&uuid) == FAILURE) { // Invalid handle return ATT_ERR_INVALID_HANDLE; } switch ( uuid ) { case RANDOM_NUMBER_DATA_UUID: // Should not get here break; case GATT_CLIENT_CHAR_CFG_UUID: notifications_enabled = *pValue; status = GATTServApp_ProcessCCCWriteReq( connHandle, pAttr, pValue, len, offset, GATT_CLIENT_CFG_NOTIFY ); if (notifications_enabled) { osal_start_timerEx( randomNumber_TaskID, RANDOM_NUMBER_PERIODIC_EVT, DEFAULT_RANDOM_NUMBER_PERIOD ); } else { HalLedSet(HAL_LED_1, HAL_LED_MODE_OFF); } break; default: // Should never get here! status = ATT_ERR_ATTR_NOT_FOUND; break; } return ( status ); }Figure 15 – Writing to the client characteristic configuration descriptor in the random number service
The three key steps steps are as follows: First I use an API function, GATTServApp_ProcessCCCWriteReq to write to the descriptor. If this has resulted in notifications being enabled, I then start a one shot timer using API function osal_start_timerEx. This specifies an event value with the constant name RANDOM_NUMBER_PERIODIC_EVT. You’ll see that this event value pops up in an event handler function elsewhere in the code shortly but basically when the timer expires after DEFAULT_RANDOM_NUMBER_PERIOD (set to 1000ms) it drops an event of this type into an event queue and makes a call back into our code to tell us the event is there.
Let’s look at this code now, which is implemented in SensorTag.c. This is a large function which handles events relating to all of the services which SensorTag imlpements, most of which are concerned with sensor data, so I’ve deleted much of that code to make it easier to see how the random number timer event is handled.
/********************************************************************* * @fn SensorTag_ProcessEvent * * @brief Simple BLE Peripheral Application Task event processor. This function * is called to process all events for the task. Events * include timers, messages and any other user defined events. * * @param task_id - The OSAL assigned task ID. * @param events - events to process. This is a bit map and can * contain more than one event. * * @return events not processed */ uint16 SensorTag_ProcessEvent( uint8 task_id, uint16 events ) { VOID task_id; // OSAL required parameter that isn't used in this function ...... ////////////////////////// // RANDOM NUMBER // ////////////////////////// if ( events & RANDOM_NUMBER_PERIODIC_EVT ) { randomNumberPeriodicTask(gapConnHandle); return (events ^ RANDOM_NUMBER_PERIODIC_EVT); } // other event types from other GATT services.... // Discard unknown events return 0; }Figure 16 – Handling random number notification timer expiry events
As you can
see, all I do here is call a function randomNumberPeriodicTask with an argument
of the GAP connection handle. The function is implemented in
randomnumberservice.c as perhaps you’d expect.
/********************************************************************* * @fn randomNumberPeriodicTask * * @brief Perform a periodic random number notification * * @param none * * @return none */ void randomNumberPeriodicTask( uint16 gapConnHandle ) { uint16 value = GATTServApp_ReadCharCfg( gapConnHandle, randomNumberClientCharCfg ); // If notifications are already enabled start the timer running and send first notification if ( value & GATT_CLIENT_CFG_NOTIFY ) { notifications_enabled = 1; } else { notifications_enabled = 0; } if (getGapRoleState() == GAPROLE_CONNECTED && notifications_enabled) { // send random number notification randomNumberNotify(gapConnHandle); // Restart timer osal_start_timerEx( randomNumber_TaskID, RANDOM_NUMBER_PERIODIC_EVT, DEFAULT_RANDOM_NUMBER_PERIOD ); } }Figure 17 – Initiating random notification and re-establishing the one shot timer
All I do
here is call a function to actually generate a random number and send the
notification and then re-establish the timer so the whole process happens again
and will continue to do so until the client writes a 0 to the descriptor to
disable notifications.
bStatus_t randomNumberNotify( uint16 connHandle ) { HalLedSet(HAL_LED_1, HAL_LED_MODE_TOGGLE); if (notifications_enabled) { attHandleValueNoti_t *pNoti; uint16 r = random_number(); randomNumberData[0] = r & 0xff; randomNumberData[1] = (r >> 8); // Set the handle pNoti->handle = randomNumberAttrTable[RANDOM_NUMBER_VALUE_POS].handle; // Set the length pNoti->len = 2; // Set the value pNoti->value[0] = randomNumberData[0]; pNoti->value[1] = randomNumberData[1]; // Send the notification return GATT_Notification( connHandle, pNoti, FALSE ); } return bleIncorrectMode; } static uint16 random_number( void ) { uint16 r = Onboard_rand(); return r; }Figure 18 – Generating the random number and sending the notification
Most of what happens in randomNumberNotify is concerned with generating the random 16 bit value and preparing the arguments for the notification function call. Sending the notification itself is accomplished by calling API function GATT_Notification. Easy!
Testing and Debugging
During development, if I had a problem, I found that switching SensorTag LEDs on or off was an easy and quick way to verify a given code path was executing. The functions I used were HalLedSet(HAL_LED_1, HAL_LED_MODE_OFF ) and HalLedSet(HAL_LED_1, HAL_LED_MODE_ON ).For testing purposes I used Texas Instruments BLE Device Monitor. I could have used any GATT explorer tool of course.
You can see a short video of BLE Device Monitor being used with my customised SensorTag here.
Summary
I hope this has been useful. Happy hacking!For more information on SensorTag see http://processors.wiki.ti.com/index.php/Bluetooth_SensorTag?INTC=SensorTag&HQS=sensortag-wiki
My source code, with all its flaws is available here.
Provided for educational purposes only. Use at your own risk. No warranty etc etc blah.
No comments:
Post a Comment
Note: only a member of this blog may post a comment.