Skip to Content

TechEd Las Vegas in 2013 I had the pleasure of teaming up with Greg Myers from EV Technologies . Quantum Fit was our solution to the explosion of fitness devices without a unified platform. To date, fragmentation of the data is still an issue. This blog will cover the devices that were strapped to me that were posting data to and receiving notifications from HANA.

The application collected biometric, distance and environmental data. This was all posted to an HANA database via XSJS. The HANA application would react to changes in the data to trigger notifications to the athlete. The data was also consumed via an athletes dashboard showing real time data for the athlete.

The demo shows our working solution, after watching the 6 minutes of geeky entertainment I’ll go over how we built the device side of the solution.

Smart phone

The center of our Quanitified Self was the smart phone. In this instance we used the iPhone 5S. The overriding purpose was to read the data from the connected devices, device itself, push alerts to the Pebble and post device readings to our HANA System running on AWS.

Location and motion readings were read from the device.

The secondary purpose of the iPhone was to display the current heart rate.

IMG_4202.PNG

iOS has the CoreMotion Framework that provides access to the Step Count. We enabled both distance determination for outdoor with CoreLocation, but for indoor on stage CoreLocation would not work. In hindsight the step count did not work so well on stage either as there was no guaranteed refresh period from the M7 chip and it slipped outside the time window for the demo.

Accessing the CoreMotion data is relatively straightforward as shown in the code example below.

#include <CoreMotion/CMStepCounter.h>


#pragma mark – Core Motion

-(void) startStepCount {

if ([CMStepCounter isStepCountingAvailable] == YES) {

        CMStepCounter *stepCounter = [[CMStepCounter alloc] init];

        [stepCounter startStepCountingUpdatesToQueue:[NSOperationQueue currentQueue] updateOn:20 withHandler:^(NSInteger numberOfSteps, NSDate *timestamp, NSError *error) {

steps = numberOfSteps;  // Double check on this step count value

self.stepCount.text = [NSString stringWithFormat:@”%d steps”,steps];

           

[self updatePace];

        } ];

    }

}

Pebble Watch

The Pebble Watch at the time was not being used (nor enabled) as a step counter. We looked at creating our own Pebble application but it made no sense as the Pebble comes with a default sports application that the mobile application can use. The following coding is from the original iPhone application. Since this was written Pebble have made a significant SDK update so this may no longer work with the latest SDK.

/*

*  PBPebbleCentral delegate methods

*/

– (void)pebbleCentral:(PBPebbleCentral*)central watchDidConnect:(PBWatch*)watch isNew:(BOOL)isNew {

    [self setTargetWatch:watch];

}

– (void)pebbleCentral:(PBPebbleCentral*)central watchDidDisconnect:(PBWatch*)watch {

[[[UIAlertView alloc] initWithTitle:@”Disconnected!” message:[watch name] delegate:nil cancelButtonTitle:@”OK” otherButtonTitles:nil] show];

    if (_targetWatch == watch || [watch isEqual:_targetWatch]) {

        [self setTargetWatch:nil];

    }

}

– (void)setTargetWatch:(PBWatch*)watch {

    _targetWatch = watch;

[watch  sportsGetIsSupported:^(PBWatch *watch, BOOL isAppMessagesSupported) {

   

[watch appMessagesSetUUID:PBSportsUUID];

        [watch sportsAppLaunch:^(PBWatch *watch, NSError *error) {

NSString *message = @””;

message = error ? [error localizedDescription] : @”Sports App Launch Request”;

        }];

    }];

   

}

-(void) sendDataToWatch {

    NSTimeInterval minsPerMile = cMetersInMile / (paceMetersPerSecond * 60);

    NSString *minsPerMileString = [NSString stringWithFormat:@”%0.2f”,minsPerMile];

   

NSDictionary *update = [[NSDictionary alloc] initWithObjectsAndKeys:[NSString stringWithFormat:@”%d”,self.heartRate],PBSportsTimeKey,

  [NSString stringWithFormat:@”%0.1f”,distanceInMeters/cMetersInMile],PBSportsDistanceKey,minsPerMileString,PBSportsDataKey ,nil];

   

PBSportsUpdate *upd = [[PBSportsUpdate alloc] init];

    upd.time = 3.95f;

    [_targetWatch sportsAppUpdate:update onSent:^(PBWatch *watch, NSError *error) {

        NSString *message = @””;

        if (error) {

message = [error localizedDescription];

        }

    }];

}

/wp-content/uploads/2015/01/pebblescreen_622391.png

The other feature we used on the Pebble was notifications. This is default behaviour for the watch and required no extra development on our part. The notifications received by the phone are mirrored on the watch.

Smart Water Bottle

The water bottle is where I had most of the fun. We used an Arduino UNO, Redbear Bluetooth BLEMini module and DS18B20 temperature sensor. We had other peripherals such as humidity readers connected but chose not to use them in the demo as we already had a lot to show in 6 minutes.

FullSizeRender.jpg

The coding for the Arduino below and still works when tested yesterday. The temperatue monitor is an I2C serial device which needs the OneWire library for reading data. There are a lot of examples on how to use this, it is one of the many getting started devices.

The RedBear BLEMini was used for the Bluetooth support. Using the serial port the temperature readings were written through the device.

#include <Arduino.h>

#include <SoftwareSerial.h>

#include <OneWire.h>

SoftwareSerial BLEMini(0, 1);

byte blink;

//Temperature chip i/o

/* OneWire */

int DS18S20_Pin = 2; //DS18S20 Signal pin on digital 2

OneWire ds(DS18S20_Pin); // on digital pin 2

void setup()

{

  BLEMini.begin(57600);

  pinMode(12, OUTPUT); 

  Serial.begin(57600);

}

unsigned char buf[16] = {0};

unsigned char len = 0;

void loop()

{

  float temp = getTemp();

  char tempData[10];

  sprintf(tempData,”%2.2f”,temp);

  printDouble(temp,100);

  blink ^= 0x01;

  digitalWrite(12, blink);

  delay(1000);

}

void printDouble( double val, unsigned int precision){

// prints val with number of decimal places determine by precision

// NOTE: precision is 1 followed by the number of zeros for the desired number of decimial places

// example: printDouble( 3.1415, 100); // prints 3.14 (two decimal places)

  Serial.print (int(val));  //prints the int part

  Serial.print(“.”); // print the decimal point

  unsigned int frac;

  if(val >= 0)

      frac = (val – int(val)) * precision;

  else

      frac = (int(val)- val ) * precision;

  Serial.println(frac,DEC) ;

}

float getTemp(){

  //returns the temperature from one DS18S20 in DEG Celsius

  byte data[12];

  byte addr[8];

  if ( !ds.search(addr)) {

   //no more sensors on chain, reset search

   ds.reset_search();

   return -1000;

  }

  if ( OneWire::crc8( addr, 7) != addr[7]) {

   Serial.println(“CRC is not valid!”);

   return -1000;

  }

  if ( addr[0] != 0x10 && addr[0] != 0x28) {

   Serial.print(“Device is not recognized”);

   return -1000;

  }

  ds.reset();

  ds.select(addr);

  ds.write(0x44,1); // start conversion, with parasite power on at the end

  byte present = ds.reset();

  ds.select(addr); 

  ds.write(0xBE); // Read Scratchpad

  for (int i = 0; i < 9; i++) { // we need 9 bytes

    data[i] = ds.read();

  }

  ds.reset_search();

  byte MSB = data[1];

  byte LSB = data[0];

  float tempRead = ((MSB << 8) | LSB); //using two’s compliment

  float TemperatureSum = tempRead / 16;

  // Convert to f

  TemperatureSum = (TemperatureSum * 1.8 ) +32;

  return TemperatureSum;

}

Notifications

For the demo we chose to use SMS. The quickest way to implement this was to use Twilio the following code is as simple as it gets to send an SMS message. The PHP script was hosted on an external server and called via events from HANA when specific thresholds were crossed in the biometric data.

<?php

// Include the PHP Twilio library. You need to download the library from

// twilio.com/docs/libraries, and move it into the folder containing this

// file.

requireServices/Twilio.php“;

// Set our AccountSid and AuthToken from twilio.com/user/account

$AccountSid =abcdefghijklmnopqrstuvw“;

$AuthToken = abcdefghijklmnopqrstuvw “;

// Instantiate a new Twilio Rest Client

    $client = new Services_Twilio($AccountSid, $AuthToken);

/* Your Twilio Number or Outgoing Caller ID */

$from =+1 xxx-xxx-xxxx‘;

// make an associative array of server admins. Feel free to change/add your

// own phone number and name here.

    $people = array(

xxxxxxxxxx=>Johnny“,

    );

    foreach ($people as $to => $name) {

// Send a new outgoing SMS

$body =Missing data $name“;

        if (isset($_GET[text])) {

          $body = $_GET[text];

        }

        $client->account->sms_messages->create($from, $to, $body);

echoSent message to $name: $body“;

    }

?>

Adidas HRM

The Heart Rate Monitor was simply one that supported Bluetooth notifications to allow us to get regular readings. The iPhone app subscribed to receive notifications using the CoreBluetooth Framework. The following code is an extract from the application. Subscribing and listening to the bluetooth devices requires that the devices are found, connected to , characteristics determined and the data read.

#define cAdidasHRMUUID @“0BC904FB-D617-A18E-A6B7-9385378F0A2E”

/*

Request CBCentralManager to scan for heart rate peripherals using service UUID 0x180D

*/

– (void) startCBScan {

  

// https://developer.bluetooth.org/gatt/services/Pages/ServicesHome.aspx

[cm scanForPeripheralsWithServices:[NSArray arrayWithObjects:[CBUUID UUIDWithString:cBiscuitServiceUUID],nil] options:nil];

}

/**

* Invoked when the central discovers a peripheral while scanning.

*/

– (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)aPeripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI

{

/* Make sure we don’t pick up a rogue device */

    NSString *cfuuidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, [aPeripheral UUID]));

    if (![knownDevices containsObject:cfuuidString]) {

        return;

    }

  

    NSMutableArray *peripherals = [self mutableArrayValueForKey:@”heartRateMonitors”];

    if( ![self.heartRateMonitors containsObject:aPeripheral] ) {

        [peripherals addObject:aPeripheral];

    }

  

// Wait until we have all expected peripherals

    if ([peripherals count] == [knownDevices count]) {

NSMutableArray *uuids = [[NSMutableArray alloc] initWithCapacity:5];

        for (CBPeripheral *per in peripherals)

        {

[uuids addObject:per.UUID];

        }

[cm retrievePeripherals:uuids];

    }

}

/*

Invoked when the central manager retrieves the list of known peripherals.

Automatically connect to first known peripheral

*/

– (void)centralManager:(CBCentralManager *)central didRetrievePeripherals:(NSArray *)peripherals

{

    [self stopScan];

  

/* If there are any known devices, automatically connect to it.*/

    for (CBPeripheral *per in peripherals)

    {

[cm connectPeripheral:per options:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:CBConnectPeripheralOptionNotifyOnDisconnectionKey]];

    }

}

/*

Invoked whenever a connection is succesfully created with the peripheral.

Discover available services on the peripheral

*/

– (void) centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral

{

    [aPeripheral setDelegate:self];

    [aPeripheral discoverServices:nil];

}

/*

Invoked whenever an existing connection with the peripheral is torn down.

Reset local variables

*/

– (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)aPeripheral error:(NSError *)error

{

    if( aPeripheral )

    {

        [aPeripheral setDelegate:nil];

        aPeripheral = nil;

    }

}

/*

Invoked whenever the central manager fails to create a connection with the peripheral.

*/

– (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)aPeripheral error:(NSError *)error

{

[self addLogEntry:[NSString stringWithFormat:@”Fail to connect to peripheral: %@ with error = %@”, aPeripheral, [error localizedDescription]]];

    if( aPeripheral )

    {

        [aPeripheral setDelegate:nil];

        aPeripheral = nil;

    }

}

#pragma mark – CBPeripheral delegate methods

/*

Invoked upon completion of a -[discoverServices:] request.

Discover available characteristics on interested services

*/

– (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error

{

    for (CBService *aService in aPeripheral.services)

    {

/* Heart Rate Service */

        if ([aService.UUID isEqual:[CBUUID UUIDWithString:@”180D”]] || [aService.UUID isEqual:[CBUUID UUIDWithString:@”180d”]])

        {

[aPeripheral discoverCharacteristics:nil forService:aService];

        }

      

/* Device Information Service */

        if ([aService.UUID isEqual:[CBUUID UUIDWithString:cBiscuitServiceUUID]])

        {

[aPeripheral discoverCharacteristics:nil forService:aService];

        }

    }

}

/*

Invoked upon completion of a -[discoverCharacteristics:forService:] request.

Perform appropriate operations on interested characteristics

*/

– (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error

{

// Temperature measurement

    if ([service.UUID isEqual:[CBUUID UUIDWithString:cBiscuitServiceUUID]])

    {

for (CBCharacteristic *aChar in service.characteristics)

        {

// https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx

/* Set notification on heart rate measurement */

CBUUID *uuid = [CBUUID UUIDWithString:cBiscuitCharacteristic];

if ([aChar.UUID isEqual:uuid])

{

[aPeripheral setNotifyValue:YES forCharacteristic:aChar];

}

        }

    }

  

// Heart Rate Service

    if ([service.UUID isEqual:[CBUUID UUIDWithString:@”180D”]])

    {

for (CBCharacteristic *aChar in service.characteristics)

        {

// https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx

/* Set notification on heart rate measurement */

if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@”2A37″]])

{

[aPeripheral setNotifyValue:YES forCharacteristic:aChar];

}


/* Read body sensor location */

if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@”2A38″]])

{

[aPeripheral readValueForCharacteristic:aChar];

}

          

/* Write heart rate control point */

if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@”2A39″]])

{

uint8_t val = 1;

NSData* valData = [NSData dataWithBytes 🙁 void*)&val length:sizeof(val)];

[aPeripheral writeValue:valData forCharacteristic:aChar type:CBCharacteristicWriteWithResponse];

}

        }

    }

  

if ( [service.UUID isEqual:[CBUUID UUIDWithString:CBUUIDGenericAccessProfileString]] )

    {

for (CBCharacteristic *aChar in service.characteristics)

        {

/* Read device name */

if ([aChar.UUID isEqual:[CBUUID UUIDWithString:CBUUIDDeviceNameString]])

{

[aPeripheral readValueForCharacteristic:aChar];

}

        }

    }

  

    if ([service.UUID isEqual:[CBUUID UUIDWithString:@”180A”]])

    {

for (CBCharacteristic *aChar in service.characteristics)

        {

/* Read manufacturer name */

if ([aChar.UUID isEqual:[CBUUID UUIDWithString:@”2A29″]])

{

[aPeripheral readValueForCharacteristic:aChar];

}

        }

    }

}

/*

* Invoked upon completion of a -[readValueForCharacteristic:] request or on the reception of a notification/indication.

*/

– (void) peripheral:(CBPeripheral *)aPeripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error

{

/* Updated value for heart rate measurement received */

    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@”2A37″]])

    {

        if( (characteristic.value) || !error )

        {

/* Update UI with heart rate data */

[self updateWithHRMData:characteristic.value];

        }

    }

  

/* Updated value for heart rate measurement received */

    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:cBiscuitCharacteristic]])

    {

        if( (characteristic.value) || !error )

        {

/* Update UI with heart rate data */

NSString *temp = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];

temperature = [temp doubleValue];

self.tempLabel.text = temp;

        }

    }

    }

/* Value for device Name received */

else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:CBUUIDDeviceNameString]])

    {

        NSString * deviceName = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];

    }

/* Value for manufacturer name received */

    else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@”2A29″]])

    {

self.manufacturer = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];

    }

}


Lessons Learned

While everything works in development the real world is a less forgiving place. I had spent the evenings (after midnight) running the streets so that my teammate Clint Vosloo could fine tune the HANA scripts. Timezone differences between the East coast of the USA and South Africa were coming into play here. After midnight there are few people running around wearing fitness devices that could interfere, however the same was not true at TechEd. Sitting in the Monday evening keynote I noticed quirks with the app, it would not always connect to my devices. Pulling out the Light Blue blue tooth testing app showed our issue. There were over 100 wearables in the audience including other HRMs and my coding was picking them all up, I had not added the specific device registration I needed. Good that we found this the night before the demo and not in the demo.

If you ever plan to have a demo that is tuned to your heart rate, make sure you take into account your own stage presence. During our many hours (I ran a lot) of tuning we collected lots of profile data on my heart rate. We tuned the alerts to the expected profile. What we had not accounted for was that my heart rate was already over 20bpm more than my usual resting rate when we started, nervous on stage perhaps? This meant that the system was detecting that I was working much harder than I physically was. While the metric was still correct in that my HR was in an abnormal zone it definitely had an affect on the timing of the demo.

Many thanks to Greg Myers, Clint Vosloo and Eric Vallo for teaming up with me on a fun time.

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

Leave a Reply