Lockdowns, amateur running, and some desktop R&D

With no gyms open for over a month now, YouTube HIIT workouts fast losing their excitement and my living room floor resenting me for even thinking about bringing out my skipping rope - I've gone back to basics and started to incorporate old fashioned running into my routine.

I've loathed running as a means of fitness due to my perceived monotony of it - but I love systems and applying systems to data to make progress.

On my radar there have been a few different systems for adapting training based on heart rate metrics - and in this post I'll explore a few of them and what we can get from wearable fitness trackers to implement these systems.

As a beginner the general advice is "just stick with a basic routine for a couple of months before you try optimize"... but where's the fun in that?

Three methods I've been exploring recently related to heart rate, and heart rate variation are:

  • Heart Rate Zone Training
    • Defining Aerobic (LT1) and Anaerobic (LT2) thresholds to run in based on max heart rate
    • In laymens terms, running within a range of bpm to ensure your training the right energy system
  • Heart rate variation (HRV) modulated training
    • Using HRV as a function of recovery to modulate training day to day
  • Heart Rate Zone Training using Detrended Fluctuation Analysis (DFA α1)
    • Using HRV during a workout to estimate more precisely the thresholds for your LT1 and LT2 zones

HRV

For a while now a key signal that has been recognized as important for wellbeing is your HRV. This is simply the variation in time between each of your heartbeats or:

RMSSD: the root mean squared of successive differences.

This calculation is done using what is known as RR-intervals. I've been using HRV for a while to moderate my training day to day, essentially on days where my HRV is trending higher, I'll work harder. And days where it's trending lower, I'll take it easier.

One thing I haven't been able to experiment with yet is training method 3, Aerobic threshold estimation using HRV. There's a great article about it here which goes into detail about how to use it, but the tldr; version is - using a stastical method to analyse your RR intervals over a window of time (2 minutes is suggested) during constant or progressive effort, you can form personalised workout zones.

Since my fitness tracker doesn't provide this out of the box, I wanted to know, what information exactly does it send?

*It should be noted that generally photoplethysmography (how most wrist-based heart rate monitors measure heart rate features) doesn't give accurate readings for RR intervals, so it is recommended that this is done with chest straps or legit ECG data.

The Journey

The first thing I wanted to do was to see what signals my wearable is sending, and thanks to google it turns out it wasn't to hard to see. A quick google search led me to the open-source application Bluetility

Bluetility

In the above screenshot, you can see you are able to view a list of devices broadcasting signals via Bluetooth Low Energy (BLE). Digging deeper into the data my wearable (WHOOP) was sending, I can see multiple "services" available and within the Heart Rate service a characteristic I can subscribe to. But what did this information mean?

Well it turns out that most interoperable heart rate monitors transmit data in the following binary format: Heart Rate Service 1.0 Specification.

Having never worked with BLE before, it was awesome to find that there is a standardized specification that devices can implement for their domain. Here is a great diagram of how we can interpret the first byte of the data sent

HRM Profile

Credit: http://www.mariam.qa/post/hr-ble/

  1. HR Data Format: 1-bit that indicates if HR values are in the format of UINT8 or UINT16.
  2. Sensor Contact (SC): 2-bits indicating whether the SC feature is supported or not, and if supported whether the device in good or poor contact with the skin.
  3. Energy Expended (EE): 1-bit that indicates the presence of the Energy Expended in the HRM characteristic.
  4. RR-Intervals (RR): 1-bit that tells whether RR-intervals measurements are present in the HRM characteristic.
  5. RFFU: 3-bits Reserved for Future Use

This is great, given I know my Whoop enables broadcast to other apps, i'm pretty sure it will implement this specification.

Getting and interpreting the data

I wasn't keen on translating each packet of data sent by hand and discovered a little JS magic to get this information in the browser

Using the Bluetooth API implemented in most browsers, we're able to subscribe to the heart rate measurment characteristic on the device

onConnect = () => {
    navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
    .then(device => device.gatt.connect())
    .then(server => server.getPrimaryService('heart_rate'))
    .then(service => service.getCharacteristic('heart_rate_measurement'))
    .then(characteristic => characteristic.startNotifications())
    .then(characteristic => {          
        characteristic.addEventListener('characteristicvaluechanged', handleCharacteristicValueChanged); 
    })                                                                                                    
    .catch(error => { console.error(error); });
}

And below we're able to parse and interpret the payload sent from the device

// the value returned by the event is a DataView structure: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
handleCharacteristicValueChanged = ({target: {value}}) => {
    // we get the first byte of the data so we can interpret interpret the flags
    const firstByte = value.getUint8(0);
    const binaryValue = firstByte.toString(2).padStart(8, '0');
    console.log('Flags: ', binaryValue);

    // get the first bit, and if its zero, the heart rate is in the second byte
    // if its 1, the heart rate is in the second and third bytes - as per the specification
    const heartRateFormat = binaryValue[0] === '0' ? 'Uint8' : 'Uint16';
    const heartrate = value[`get${heartRateFormat}`](1);
    console.info('Heart rate: ', heartrate); 

    // grab all the rr intervals sent by the device, if any
    // we start at the 3rd byte, and we increment by 2 bytes each time
    // as each rr interval is 2 bytes long
    // and we've seen that energy expendature isn't sent by whoop, and heart rate is 1 byte long
    const rrIntervalArray = [] ;
    const rrIntervalArrayLength = (value.byteLength - 2) / 2 ;

    for (let i = 0 ; i < rrIntervalArrayLength ; i++)
    {
        const rrIntervalValue = value.getUint16(2 + i * 2, true) ;
        // according to the specification, RR intervals are represented in units of 1/1024 seconds 
        rrIntervalArray.push(rrIntervalValue / 1024) ;
    }
    
    const rrIntervals = rrIntervalArray.join(", ") ;
    console.log('RR intervals: ', rrIntervals);
}

After connecting my Whoop, this produced some promising results!

script.js:18 Flags:  00000000
script.js:24 Heart rate:  58
script.js:39 RR intervals:  
script.js:18 Flags:  00000000
script.js:24 Heart rate:  58
script.js:39 RR intervals:  
script.js:18 Flags:  00000000
script.js:24 Heart rate:  58
script.js:39 RR intervals:  
script.js:18 Flags:  00000000
script.js:24 Heart rate:  58
script.js:39 RR intervals:  
script.js:18 Flags:  00010000
script.js:24 Heart rate:  60
script.js:39 RR intervals:  1.6943359375
script.js:18 Flags:  00010000
script.js:24 Heart rate:  60
script.js:39 RR intervals:  1.0224609375
script.js:18 Flags:  00010000
script.js:24 Heart rate:  61
script.js:39 RR intervals:  0.6455078125
script.js:18 Flags:  00010000
script.js:24 Heart rate:  61
script.js:39 RR intervals:  1.474609375

This is super neat, we can see in the data emitted that heart rate is sent - and after connecting for a few seconds, RR intervals are emitted too.

More to come, it seems like we could build something to work with this..