User Tools

Site Tools


Sidebar


Site Information


Interactive Maps

Sidebar

scode:perl:heart_rate_app

Heart Rate Monitor App

I needed data for showing to my cardiologist. I found a free, ad-free Heart Rate Android app, SGT Heart Rate - Sport Gear + Wear, that works with my Polar H7 Bluetooth monitor. I ♥ love ♥ the app!!!

Issues

One of the features is that it allows me to "share" results data. SGT sends the data as a CSV file. But, I have several problems with the file:

  • The filename comes as an overly complicated filename, to wit: #date=1475433366577#time=5691803#calories=1275#type=HeartRate#version=4.txt
  • The csv contains 3 lines of informational data [age, weight, gender] at the beginning of the file, and 2 lines of results data [disconnect status (whatever the hell that is), and a line of json data] at the end.
  • In between the above information is the heart rate data in the format: 1475433373541;;Heart rate;116. That is: The time in Unix Epoch Time (including micro seconds);;some column that's always empty;; a column that is always Heart rate, and finally, the actual Beats Per Minute recorded.

My choices are:

  • Manually rename the file to something human readable (e.g. 1601002_1402HR.csv), edit out the 5 lines of information I don't give a crap about (I know how old I am, how much I weigh and I'm a male), use Textpad to delete the extra columns (gotta love Regular Expressions), save the file, and open it in Open Office to create graphs.
  • Write a program to save my sanity.

I like sanity. It keeps me from going crazy. I love Perl. It makes keeping my sanity easier.

I've set the SGT app to record every ten seconds. Every 10 seconds is good for capturing the maximum heart rate. So I would expect 540 readings for a 90 minute workout (90 minutes x 6 readings (1 for every 10 seconds) = 540). Instead, I get over 5,600 (or so) records! That's about 60 records per minute.

Solution

Requirements:

  • Convert the filename to something short, and human readable.
  • Delete the leading and trailing useless (for the non-developer) records.
  • Delete the columns I don't need, leaving only the BPM.
  • Compute the minimum, maximum and average heart rate (before reducing the data).
    • Add header record with Date, computed values (min/max/avg), and placeholders for: Metoprolol dosage, exercise Day number, Steps, Miles and Pace (mph).
  • Delete thousands of records, allowing the user to enter magnitude.
  • Save the data to a new csv file.

This is a (downloadable!) copy of my program as it stood on October 11, 2016.

sgtConverter.pl
#!/usr/bin/perl -w
use strict;
sub fmtTime($;$);   # second arg is "optional"
sub commify ($);
 
my $sPathMain = "C:\\Users\\Lawrence\\Documents\\Personal\\VA_Medical\\BPressure";
my $sPathSep = "\\";
my $sPathDownload = "dl";
my $sPathDone = $sPathMain . $sPathSep . "done" . $sPathSep;
my $sExtTxt = ".txt";
my $sExtCsv = ".csv";
 
my @aFilesIn;
my $sInFile;
my $sOutFile;
my @aLines;
 
######################################################################
my $iDelCnt;    # 1 = 50%, 2 = 33%, 3 = 25%, 4 = 20%, 5 = 17%, etc...
print "1 = 50%, 2 = 33%, 3 = 25%, 4 = 20%, 5 = 17%, etc...\n";
print 'Enter delete count [default = 3]: ';
$iDelCnt = <STDIN>;
chomp $iDelCnt;
$iDelCnt = 3 if not $iDelCnt =~ /^\d+$/;
print "We're going with: $iDelCnt\n";
print "\n";
 
push @aFilesIn, map {glob( $_)} $sPathMain . $sPathSep . $sPathDownload . $sPathSep . '*' . $sExtTxt;
 
###print "Files:\n", join ("\n", @aFilesIn);
###
###print "\nFiles:\n", join ("\n", @aFilesIn);
 
foreach $sInFile (@aFilesIn) {
    my @aHR;
    my $iMax;
    my $iMin;
    my $iAvg;
    my $iTmStart;
    my $iTmEnd;
    my $iMinDur;
    my $iNoRecords;
 
    ($sOutFile = $sInFile) =~ s/.*?(\d{10}).*/$1/;
    $sOutFile = fmtTime('YYMMDD', $sOutFile);
    ###print "\nFilename:$sOutFile\n";
 
    open FIN, "<$sInFile" or die "Can't open input $sInFile: $!\n";
    @aLines = <FIN>;
    close FIN;
 
    # calc start time, end time and duration
    ($iTmStart = $aLines[0]) =~ s/^(\d{10}).*/$1/;
    ($iTmEnd = $aLines[$#aLines]) =~ s/^(\d{10}).*/$1/;
    $iMinDur = sprintf("%0.0f", ($iTmEnd - $iTmStart) / 60);
 
    @aHR =  map {s/.*;//; $_;}          # being greedy, delete everything EXCEPT heart rate
            grep /Heart rate/, @aLines; # only lines with "Heart rate"
 
    # get minimum/maximum rates
    $iMax = (sort { $b <=> $a } @aHR)[0];
    chomp $iMax;
    $iMin = (sort { $a <=> $b } @aHR)[0];
    chomp $iMin;
    foreach (@aHR) {$iAvg += $_; }          # get total;
    $iAvg = sprintf("%.0f",($iAvg / @aHR)); # calc the average
 
    # make times readable and add to left of aLines array
    # YYMMDD@HHMM. 71 Min. Day NNN. Max NNN, Avg NNN, Min NNN. NNNN Steps. NNNN Miles. NNN MPH.
    unshift (@aHR,  '"' . fmtTime("YYMMDD\@hhmm", $iTmStart) . " ($iMinDur min). Met: nn.n mg. Day NNN. BPM: Max $iMax, Avg $iAvg, Min $iMin. Stats: NNNNN Steps, NNNN Miles, NNNmph.\"\n");
    #print '"' . fmtTime("YYMMDD\@hhmm", $iTmStart) . " ($iMinDur min). Day NNN. BPM: Max $iMax, Avg $iAvg, Min $iMin. Stats: NNNNN Steps, NNNN Miles, NNNmph.\"\n";
 
    # delete records
    $iNoRecords = scalar @aHR;   # 1-NNN
 
    # $#aLines = offsets (0 to NNN-1)
    for (my $i = $#aHR - $iDelCnt; $i > 0; $i -= $iDelCnt + 1){
        #print sprintf ("Records: %s   \$i: %5s   \$iDelCnt: %d   ByeBye: %s - %s\n",
        #               commify($#aHR), commify($i), $iDelCnt, commify($i), commify($i + $iDelCnt));
        splice(@aHR, $i, $iDelCnt);
    }
 
    # open and create new file
    open (FOUT, ">${sPathDone}${sOutFile}HR.csv");
    print FOUT @aHR;    # to the file
    close FOUT;
    # print @aHR;         # to the human watching
 
    print "Number of Records:\n";
    print sprintf("\tBegin: %5s\n", commify($iNoRecords));
    print sprintf ("\tEnd..: %5s\n", commify(scalar @aHR));
 
}
 
######################################################################
 
sub commify ($){
    my $text = reverse $_[0];
    $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g;
    return scalar reverse $text;
}
 
sub fmtTime($;$) {
    # second arg is optional!
    my ($sFmtStr, $tmTheTime) = @_;
 
    my $sec; my $min; my $hours; my $mday; my $wday; my $yday; my $isdst; my $mon; my $year;
    my @aDay = qw(Sun Mon Tue Wed Thu Fri Sat);
    my @aDAY = qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday);
    my @aSaxon = qw(Sunnandaeg Monandaeg Tiwesdaeg Wodensdaeg Thursdaeg Frigedaeg Saterndaeg);
 
    my @aMon = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/;
    my @aRoMon = qw/Januarie Februarius Martius Aprilis Maius Junius Quintilis Sextilis September October November December/;
    my @aMonth = qw/January February March April May June July August September October November December/;
 
    my $tmDate;
 
    # if they didn't give us a time, get current time
    # tm: 1475758254
    # fl: 1475433366577
    $tmTheTime = time() unless $tmTheTime;
 
    ($sec, $min, $hours, $mday, $mon, $year,
        $wday, $yday, $isdst) = localtime($tmTheTime);
 
    $year += 1900;
    $sFmtStr =~ s/YYYY/$year/;
 
    $year -= 2000;
    $year = sprintf "%02d", $year;
    $sFmtStr =~ s/YY/$year/;
 
    $sFmtStr =~ s/Www/$aDay[$wday]/;
    $sFmtStr =~ s/WWW/$aDAY[$wday]/;
    $sFmtStr =~ s/III/$aSaxon[$wday]/;
    $mday = sprintf "%02d", $mday;
    $sFmtStr =~ s/DD/$mday/;
 
    $sFmtStr =~ s/Mmm/$aMon[$mon]/;
    $sFmtStr =~ s/MONTH/$aMonth[$mon]/;
    $sFmtStr =~ s/XXX/$aRoMon[$mon]/;
    $mon++;
    $mon = sprintf "%02d", $mon;
    $sFmtStr =~ s/MM/$mon/;
 
    $hours = sprintf "%02d", $hours;
    $sFmtStr =~ s/hh/$hours/;
 
    $min = sprintf "%02d", $min;
    $sFmtStr =~ s/mm/$min/;
 
    $sec = sprintf "%02d", $sec;
    $sFmtStr =~ s/ss/$sec/;
 
    $yday++;
    $yday = sprintf "%03d", $yday;
    $sFmtStr =~ s/JJJ/$yday/;
 
    $sFmtStr = $tmTheTime if (0 == length($sFmtStr));
 
    return $sFmtStr;
}
 
######################################################################
 
1;
__END__
 
Collect for header:
YYMMDD@HHMM. 71 Min. Day NNN. Max NNN, Avg NNN, Min NNN. NNNN Steps. NNNN Miles. NNN MPH.

Sample Output

"161002@1436 (95 min). Met: nn.n mg. Day NNN. BPM: Max 142, Avg 129, Min 102. Stats: NNNNN Steps, NNNN Miles, NNNmph."
117
116
116
116
116
115
112
109
108
107
106
104
scode/perl/heart_rate_app.txt · Last modified: 2016/10/14 08:33 by terrill