Tuesday, 9 December 2014

Customizing a Texas Instruments SensorTag

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.

I was wondering if it might be possible to change the firmware on a SensorTag so I could add my own Bluetooth Smart GATT services. I set about researching and ultimately achieved my goal. This blog post documents my experience. I hope you find it useful.

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
Source code for the SensorTag firmware is included in the “Texas Instruments Bluetooth low energy stack and tools” download from http://www.ti.com/tool/ble-stack so I grabbed that.


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.


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.


I then set about defining each service and implementing their respective behaviours.

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


static 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.c


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;
Figure 6 – The gattAttribute_t type

For 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 Encryption
Figure 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 Service
Figure 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.