Parsing RR intervals from Bluetooth heart rate monitor (JavaScript/Cordova)

Many fitness applications require reading the user’s heart rate in beats per minute. Sometimes however, as in the case of heart rate analysis, we need to get a more accurate measure. This is when we need to capture and record then inter-beat intervals (or RR intervals), which represent the time difference between two successive heartbeats. Not all Bluetooth heart rate monitors are accurate enough to capture these intervals and will instead provide a rolling average heart rate. This post will discuss how to parse data received from a Bluetooth heart rate monitor, and how we can use this to access the RR intervals for use in our application. I will be doing this in JavaScript using Cordova and a Bluetooth plugin, however the parsing of the heart rate data will be applicable to any platform, and the code for parsing could quite easily be translated to another language.

 

GATT Specifications

Generic attribute profiles describe the characteristics and services that Bluetooth devices can implement. Basically, companies that develop Bluetooth enabled hardware will conform to these specifications to ensure that they can communicate in a common manner. For example the company Polar will follow the Bluetooth heart rate service when designing their heart rate monitors – it’s the reason why an application can be used with a number of different brands of heart rate monitor – each of the companies build their product in conformance with the various Bluetooth GATT specifications. Each service provides a number of characteristics which are basically descriptions of data that can be received from the device. These services and characteristics are denoted by a hexadecimal identifier.

We are interested in the heart rate service (0x180D) and its heart rate measurement characteristic (0x2A37) which operate over the Bluetooth low energy (BLE) transport dependency. I’d strongly recommend studying the documentation which will give you a much better idea of how it all works; this guide should provide a good foundation for you to understand the basics however.

All data transmitted from a Bluetooth device is in binary format, and if you haven’t worked with binary data before it can take a little getting used to. The heart rate measurement characteristic page (which you should have a quick look at before continuing) describes the format of the binary data it transmits. We will get onto that in a bit – first I’ll cover setting up Cordova and connecting to a Bluetooth device. If you’re not using Cordova, congratulations, you are the bigger man and can skip this section.

 

Cordova

I’ll try not spend too long on this section. The plugin we will be using is Bluetooth Low Energy (BLE) Central Plugin for Apache Cordova by our man Don. You can install this with:

cordova plugin add cordova-plugin-ble-central

Take a look at the link above for the project’s Github page, which describes well the plugin’s API. Before we take a look at the plugin’s methods that we will be using, we will declare the following variable:

heartRateSpec = {
service: ‘180d’,
measurement: ‘2a37’
}

This defines the identifiers for the heart rate service and the heart rate measurement characteristic, which we will need to reference in a few of the methods.

 

ble.isEnabled(success, failure)

Reports if Bluetooth is enabled on the device.

  • success: Success callback function, invoked when Bluetooth is enabled – this should reference a function which scans for a heart rate monitor
  • failure: Error callback function, invoked when Bluetooth is disabled – this function will make use of the next method, prompting the user to turn on Bluetooth on their phone

 

ble.enable(success, failure)

Prompts the user to enable Bluetooth.

  • success: Success callback function, invoked if the user enabled Bluetooth – now that Bluetooth has been enabled, we can call a function which scans for devices
  • failure: Error callback function, invoked if the user does not enabled Bluetooth – a function which informs the user they need to turn on Bluetooth to use the app

 

ble.scan(services, seconds, success, failure)

Scan and discover BLE peripherals.

  • services: List of services to discover, or [] to find all devices – we will use heartRateSpec.service as we are only interested in picking up heart rate monitors
  • seconds: Number of seconds to run discovery – 5 should be good
  • success: Success callback function that is invoked with each discovered device – this should reference a function that attempts to connect to the peripheral (which is passed into the callback function)
  • failure: Error callback function, invoked when error occurs (optional) – this function should alert the user that a heart rate monitor couldn’t be detected

 

ble.connect(device_id, connectSuccess, connectFailure)

Connects to a BLE peripheral. The callback is long running. Success will be called when the connection is successful. Failure is called if the connection fails, or later if the peripheral disconnectsble.scan() must be called before calling connect, so the plugin has a list of available peripherals.

  • device_id: UUID or MAC address of the peripheral – the peripheral information will have been passed into the function which calls this method as a success callback from ble.scan(), we will use the peripheral ID here
  • connectSuccess: Success callback function that is invoked when the connection is successful – this will be a function which informs the user of the successful connection andinvokes the ble.startNotification() method
  • connectFailure: Error callback function, invoked when error occurs or the connection disconnects – a function that informs the user of the failure to connect

 

ble.startNotification(device_id, service_uuid, characteristic_uuid, success, failure)

Registers a callback that is called every time the value of a characteristic changes. This method handles both notifications and indications. The success callback is called multiple times. Raw data is passed from native code to the success callback as an ArrayBuffer – we will explore ArrayBuffer when parsing the data.

  • device_id: UUID or MAC address of the peripheral – we will have stored this information in a variable when it was received by the ble.connect() success callback
  • service_uuid: UUID of the BLE service – we can get this from the variable we created at the start, heartRateSpec.service
  • characteristic_uuid: UUID of the BLE characteristic – as before, but heartRateSpec.characteristic
  • success: Success callback function invoked every time a notification occurs – this will reference a callback which parses and works with the received binary data
  • failure: Error callback function, invoked when error occurs (optional) – we won’t be using this

 

ble.disconnect(device_id, success, failure)

Disconnects the selected device.

  • device_id: UUID or MAC address of the peripheral – we will have stored this information in a variable when it was received by the ble.connect() success callback
  • success: Success callback function that is invoked when the connection is successful (optional) – won’t be using this
  • failure: Error callback function, invoked when error occurs (optional) – won’t use this either

 

Parsing data and getting RR intervals

Below is the method onData() which is set as the success callback function of ble.startNotification() and is fired each time the device receives a reading from the heart rate monitor.

Let’s discuss how onData() works. The argument, buffer, is a typed array. In JavaScript, typed arrays are used when working with binary data and are represented by buffers and views, with the classes ArrayBuffer and DataView. You can think of a buffer as an arbitraty long string of binary data, which can’t be manipulated until we create a ‘view’ from it. A DataView object allows us to cast binary data into a type (such as Int8Array and UInt16Array) which we can work with. For example if  we create a DataView object from the binary data, we have access to methods such as getUint8() which will return an integer representing the binary data. These methods require a parameter denoting the starting bit of the view, and the variable offset is used for this purpose.

The first byte of the data received is always the flags byte – meta-data which gives us information about the structure of the binary so we can interpret it correctly. We can see what each bit of the flags byte refers to from the heart rate measurement characteristic mentioned earlier and use bitwise operators to check the values. First, line 7 checks if RR intervals are present in the reading. If so, we use calculateOffset() – shown below – to find the position of the starting byte for the intervals. Because the heart rate monitor sends data roughly once per second, some reading will contain multiple RR intervals; for this reason we need to determine how many are in each buffer received. This can be calculated simply by subtracting offset from the total number of bytes, and dividing by two (as RR intervals are in UInt16 format, and therefore require two bytes each). After this we can iterate over the readings, using getUint16() to return the interval value. Note the second parameter is true, which indicates that the data is in little-endian format. The docs tell us that RR interval Bluetooth data is captured at a resolution of 1/1024 seconds, and so line 14 is used to convert the interval into milliseconds. We can now pass the values to another part of our program for processing.

Let’s take a quick look at calculateOffset(flags)

This function takes in the flags byte as a parameter, and returns offset, an integer representing the starting byte of the RR interval data. We start by incrementing offset by 1 to account for the flags byte.

The first bit of flags tells us whether the heart rate (in beats per minute, BPM) is represented by one byte (0) or two bytes (1). The heart rate – which, for our purposes we are not interested in – appears after the flags, and so we use the value to increase the offset accordingly. The sensor contact value is also not of interest and will not affect the offset so we can ignore these 2 bits. Next, we will use a similar strategy to check if energy expended data is included in the bitstream, and if so increase the offset.

As mentioned at the start of the article, not all monitors are accurate enough to capture the raw RR interval data. For the purposes of our app, we require RR intervals and I don’t think there is any particularly elegant way of checking if the monitor is accurate enough. The way I have implemented this (which is not shown) is to have a variable called attemptsLeft which starts at 3, and each time onData() is fired and no intervals are present we decrement its value. The value of attemptLeft is checked at the start of the function and if it is smaller than or equal to 0, we can conclude the the monitor doesn’t capture RR intervals and the user is informed that it is not compatible with the application. If there is a better way, let me know or leave a comment!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.