.
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:
- 0-config.csv
- 0-sdata.csv
- 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)

# ----------- 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; } }
> 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
>
.
Thank you for using my code and referencing it! If possible, would you reference my github as well?
https://github.com/paulvangentcom/heartrate_analysis_python
Thanks!
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.
Looks great & promising!