Start coding for Bobbi – the open source ECG monitor

.

Download all files related to this post here

I was asked to put some basic code examples online to help developers get started with the Totem Bobbi Motion + ECG Monitor. For those not in the know: “The Bobbi” is a very cool, *fully* open source ECG and 9-axis motion sensor with low energy bluetooth (BLE 4.0). It has a MicroSD card slot, and works with an AAA battery. It can give you access to raw data (.csv or .txt files), and the hardware is 100% open source. In other words: you decide how to use it.

On this page I will post some scripts and examples that should enable you to start developing quickly. All scripts make use of a basic Bobbi set of data files. An archive with the data and the scripts used in this post can be downloaded here – or via this GitHub repository.

I have divided this post into four sections – each illustrating a different take on basic Bobbi data analysis, each making use of a different programming language. The sections are:

Bobbi export file format definitions

By default, the Bobbi saves the data it collects from its sensors in three files per session, with an auto incrementing session number preceding a format descriptor. That is, for the duration of a session, the Bobbi would save the following three files:

  1. 0-config.csv
  2. 0-sdata.csv
  3. 0-sdataHR.csv

Let’s go over them, one by one:

config.csv

This file is quite straightforward – it contains an overview of which sensors are currently enabled and at which rate these sensors are being sampled:

See what sensors are enabled in this session
Temperature sensor = TRUE;
Accelero sensor = TRUE;
Gyro sensor = TRUE;
Magneto sensor = TRUE;
Sampling frequency = 120 hz

ECG sensor = TRUE;

ECG graph = TRUE;

The resulting data is saved to …-sdata.csv files. One little issue is that the data is sampled at 120 hz while the sdata reports the timing in milliseconds (ms). A future firmware patch will resolve this.

sdata.csv

This file contains the data of all sensors defined in config.csv, samped at the rate defined in that file. It is a basic CSV type file – the only particularity is that its not comma, but semicolon delimited:

ms;degrees;ax;ay;az;gx;gy;gz;mx;my;mz;heartrate;BPM
000000000000;-512;-251;581;1201;-205;-76;-52;-759;-446;258;523;0;
000000000008;-512;-254;587;1217;-324;-82;-22;-759;-446;258;515;0;
...

Each row contains samples from each of the Bobbi’s sensors at the time in milliseconds since the start of a session as represented by the “ms” column. Specifically, the columns represent:

ms              the time in milliseconds since the start of a Bobbi session 
degrees         the value reported by the temperature sensor (accuracy 0.1 degrees C)
ax,ay,az        the values reported by the 3-axis accelerometer at +- 16g      
gx,gy,gz        the values reported by the 3-axis gyroscope at +- 2000 o/s     
mx,my,mz        the values reported by the 3-axis magnetometer at +-16 Gauss    
heartrate       the value reported by the 2 ElectroCardioGram (ECG)  
BPM             heart beats per minute approximation calculated by Bobbi itself

sdataHR.csv

The x-sdataHR.csv file contains just the heartrate column, but sampled at the higher rate of 500Hz. The file is, again, semicolon delimited:

ms;heartrate;BPM
000000000000;465;0;
000000000002;463;0;
...

Apart from the higher sampling rate, and the fact that this file just contains the heartrate and BPM columns, the file format conforms the the sdata.csv files:

ms              the time in milliseconds since the start of a Bobbi session  
heartrate       the value reported by the 2 ElectroCardioGram (ECG)  
BPM             heart beats per minute approximation calculated by Bobbi itself

Bobbi meets Python

Let’s start out by running some Python code – always a great way to start any data science project! By making use of a basic import script, you are but one step away of applying the code of Paul van Gent’s super intro into ECG data analysis* (see also his related Github repo here) to our Bobbi’s data:

At the end of the script, I do a quick plot of a subset of the heart rate data:

Looks good! Lets see if we can follow Paul’s blog and run some first analyses using a slightly adapted version of heartrate.py – for the code, see here.

On importing this file, we can use a few lines of code to see if we are able to do some peak detection:

Again, looking really good:


Of course, when your unfiltered data looks as nice as this, it’s probably time to run some more advanced analyses:

 Which results in a whole lot of useful heart rate related measures, such as:

BPM: 84.4409695074
ibi: 710.555555556
sdnn: 19.1162783712
RR_list: [725.0, 725.0, ...
RR_diff: [0.0, 0.0, 41.66666666666663, ...
RR_sqdiff: [0.0, 0.0, 1736.111111111108, ...
RR_sqdiff: [0.0, 0.0, 1736.111111111108, ...
sdnn: 19.1162783712
sdsd: 18.0151737505
rmssd: 27.0947778018
nn20: [41.66666666666663, 58.33333333333337, ...
nn50: [58.33333333333337]
pnn20: 0.42857142857142855
pnn50: 0.07142857142857142
.
.

Bobbi meets R

On to some R scripts to enable the statistically inclined Bobbi data analysts to quick-start their development! Again, let’s start by importing the data:

.

#########################################################################################
# import bobbi data
#########################################################################################

# set home directory to this file's directory
setwd("C:\\Users\\robin\\PycharmProjects\\TotemHeartRate")

# import the short sample data file
bobbi.df <- read.table("data\\8-sdataHR_sample.csv", sep=";", header=T)
str(bobbi.df)

#########################################################################################
# Lets plot the data
#########################################################################################

# let's see what we've got
tp <- plot(bobbi.df[,c("hart")], type="l", col="blue")
print(tp)

Which nets us the following nice little chart:
Which is all fine and dandy, but importing does not equal data analysis – so let’s continue with a 4-level Daubechie wavelet decomposition to do some peak detection, and calculate the average heart rate value for our sample:
.
# ----------- Totem Bobbi basic data analysis in R ------------
# ----------- Based on: https://tinyurl.com/n3tw7lc -----------

library(ggplot2)
library(wavelets)

# ----------------- Load data, set vars  ----------------------

# set home directory to this file's directory
setwd("C:\\Users\\robin\\PycharmProjects\\TotemHeartRate")

# import the short sample data file
bobbi.df <- read.table("data\\8-sdataHR_sample.csv", sep=";", header=T)

# add an index
bobbi.df$idx <- seq.int(nrow(bobbi.df))

# plot the data
ggplot(data = bobbi.df, aes(x = ms, y = hart)) + geom_line()

# create subset df
df <- data.frame(bobbi.df$idx,bobbi.df$hart)
names(df) <- c("idx", "ecg")

# set the sample frequency
SampleFreq <- 500

# threshold
thr <- 12

# -----------------   Do peak detection  ----------------------

# 4-level decomposition is used with the Daubechie d4 wavelet.
wavelet <- "d4"
level <- 4L

X <- as.numeric(df$ecg)

ecg_wav <- dwt(X, filter=wavelet, n.levels=level, boundary="periodic", fast=TRUE)
str(ecg_wav)

oldpar <- par(mfrow = c(2,2), mar = c(4,4,1.5,1.5) + 0.1)
plot(ecg_wav@W$W1, type = "l")
plot(ecg_wav@W$W2, type = "l")
plot(ecg_wav@W$W3, type = "l")
plot(ecg_wav@W$W4, type = "l")
par(oldpar)

oldpar <- par(mfrow = c(2,2), mar = c(4,4,1.5,1.5) + 0.1)
plot(ecg_wav@V$V1, type = "l")
plot(ecg_wav@V$V2, type = "l")
plot(ecg_wav@V$V3, type = "l")
plot(ecg_wav@V$V4, type = "l")
par(oldpar)

# Coefficients of the second level of decomposition are used for R peak detection.
x <- ecg_wav@W$W2

# Empty vector for detected R peaks
R <- matrix(0,1,length(x))

# While loop for sweeping the L2 coeffs for local maxima.
i <- 2
while (i < length(x)-1) {
  if ((x[i]-x[i-1]>=0) && (x[i+1]-x[i]<0) && x[i]>thr) {
    R[i] <- i
  }
  i <- i+1
}

# Clear all zero values from R vector.
R <- R[R!=0]
Rtrue <- R*4

# Checking results on the original signal
for (k in 1:length(Rtrue)){
  if (Rtrue[k] > 10){
    Rtrue[k] <- Rtrue[k]-10+(which.max(X[(Rtrue[k]-10):(Rtrue[k]+10)]))-1
  } else {
    Rtrue[k] <- which.max(X[1:(Rtrue[k]+10)])
  }
}

Rtrue <- unique(Rtrue)
Rtrue_idx <- df$idx[Rtrue]

# add peaks to df
df$is_peak <- data.frame(df$idx %in% Rtrue_idx)
df$peak_value <- df$is_peak
df$peak_value[df$peak_value == TRUE] <- df$ecg[df$peak_value == TRUE]

rr_dist_total <- Rtrue_idx[length(Rtrue_idx)] - Rtrue_idx[1]
rr_dist_avg <- rr_dist_total/(length(Rtrue_idx)-1)
rr_ms_avg <- rr_dist_avg * 1000/SampleFreq
beats_per_second <- 1000/rr_ms_avg
beats_per_minute <- round(beats_per_second * 60,digits=2)

dev.off()
# plot the data
ggplot(df, aes(x = idx)) + 
  ylim(450, 620) + 
  geom_line(aes(y = ecg), colour="blue") + 
  geom_point(aes(y = peak_value), colour = "red") +
  ggtitle(  paste("Bobbi heart rate", beats_per_minute, "bpm", sep=" ") ) + 
  theme(plot.title = element_text(lineheight=.8, face="bold"))

 Resulting in, among others, the following plot:

Bobbi meets JAVA

Python and R are cool – but there are those of us who would like to integrate Bobby with a mobile phone or tablet. In that case it makes more sense to start coding in Java – the programming language that is used in Android, at the time the dominant platform for developing and deploying mobile device apps.

So lets first import our data again – making use of, for example, the following simple class (Though, if you are working on an Android app, streaming data over Bluetooth might make more sense. For another time, another blog post, I guess!):

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;

public class BobbiReader {

    private static final String FILE_PATH = "data/8-sdataHR_sample.csv";

    public static void main(String[] args) throws IOException {

        // initialize our bobbireader class
        BobbiReader bobbiReader = new BobbiReader();

        // read csv file with ecg heart rate values
        ArrayList<Integer> heartRateList = bobbiReader.readBobbi(FILE_PATH);

        // now do some stuff!

    }

    public ArrayList<Integer> readBobbi(String filename) throws IOException {
        ArrayList<Integer> heartRateList = new ArrayList<Integer>();
        File inFile = new File(filename);
        Scanner scanner = new Scanner(inFile);
        scanner.nextLine();                                        // skip header
        while (scanner.hasNext()) {
            String bobbiRow = scanner.nextLine();
            String[] bobbiRowValues = bobbiRow.split(";");
            heartRateList.add(Integer.parseInt(bobbiRowValues[1])); // get the heart rate - in this file, the second element
        }
        scanner.close();
        return heartRateList;
    }
}

By extending this class, we can once again see if we are able to find the average heart rate of our data:

import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Scanner;

public class BobbiReader {

    private static final String FILE_PATH = "data/8-sdataHR_sample.csv";
    private static final int SAMPLE_FREQUENCY = 500;

    public static void main(String[] args) throws IOException {

        // initialize our bobbireader class
        BobbiReader bobbiReader = new BobbiReader();

        // read csv file with ecg heart rate values
        ArrayList<Integer> heartRateList = bobbiReader.readBobbi(FILE_PATH);

        // get a list of peaks
        ArrayList<Integer> peakList = bobbiReader.getPeaks(heartRateList, 0.993d);

        // print them to the command line
        System.out.println(peakList);

        // lets calculate the heart rate based on our list of peaks and the sample frequency
        int rr_dist_total = peakList.get(peakList.size()-1) - peakList.get(0);
        float rr_dist_avg = rr_dist_total/(peakList.size()-1);
        float rr_ms_avg = rr_dist_avg * 1000/SAMPLE_FREQUENCY;
        float beats_per_second = 1000/rr_ms_avg;
        float beats_per_minute = beats_per_second * 60;
        DecimalFormat twoDForm = new DecimalFormat("#.##");
        System.out.println("Bpm = " + twoDForm.format(beats_per_minute));
    }

    public ArrayList<Integer> readBobbi(String filename) throws IOException {
        ArrayList<Integer> heartRateList = new ArrayList<Integer>();
        File inFile = new File(filename);
        Scanner scanner = new Scanner(inFile);
        scanner.nextLine();                                        // skip header
        while (scanner.hasNext()) {
            String bobbiRow = scanner.nextLine();
            String[] bobbiRowValues = bobbiRow.split(";");
            heartRateList.add(Integer.parseInt(bobbiRowValues[1])); // get the heart rate - in this file, the second element
        }
        scanner.close();
        return heartRateList;
    }

    public ArrayList<Integer> getPeaks(ArrayList<Integer> input, double threshold) {
        double max = 0;
        double peakThreshold;
        ArrayList<Integer> peaks = new ArrayList<Integer>();
        System.out.println("Start getting the peaks");
        for (int i = 0; i < input.size(); i++) {
            if (input.get(i) > max) {
                max = input.get(i);
            }
        }
        System.out.println("Max = " + max);
        peakThreshold = max * threshold;
        for (int i = 0; i < input.size(); i++) {
            if (input.get(i) > peakThreshold) {
                int j = i;

                if (input.get(i) < input.get(i + 1)) {
                    while (input.get(j) < input.get(j + 1) && j > 0) {
                        j--;
                    }
                    if (!peaks.contains(j)) {
                        peaks.add(j);
                    }
                } else {
                    while (input.get(j) > input.get(j + 1)) {
                        j++;
                    }
                    while (input.get(j) < input.get(j + 1)) {
                        j++;
                    }
                    if (!peaks.contains(j)) {
                        peaks.add(j);
                    }
                }
            }
        }
        return peaks;
    }
}
Let’s see what happens if we run this code!
> javac BobbiReader.java
> java BobbiReader
Start getting the peaks
Max = 614.0
[893, 907, 929, 1237, 1252, 1276, 2975, 3015, 3368, 3703, 3728, 4087, 5137]
Bpm = 84.99
> 
.
Yet another method, yet another heart rate! Of course, these are all basic examples – very curious as to all of the cool algorithms, code and apps the Bobbi community will be able to come up with!
.
.
* van Gent, P. (2016). Analyzing a Discrete Heart Rate Signal Using Python. A tech blog about fun things with Python and embedded electronics. Retrieved from: http://www.paulvangent.com/2016/03/15/analyzing-a-discrete-heart-rate-signal-using-python-part-1/

 

Comments

    • Robin van Emden

      Sure thing – I have just added the link to your GitHub Repository. Also, many thanks for making your code available!

      • Thanks! These kinds of things are the reason I make it available. Love the work you’re doing with the bobbi!

        Keep an eye on the repo, I’m optimising the code to improve speed and adding some functionality this week, also dropping some dependencies and exposing an API later.

Leave a Reply