File Coverage

blib/lib/NetAthlon2/RAW.pm
Criterion Covered Total %
statement 141 145 97.2
branch 48 72 66.6
condition 19 30 63.3
subroutine 15 15 100.0
pod 2 4 50.0
total 225 266 84.5


line stmt bran cond sub pod time code
1             package NetAthlon2::RAW;
2              
3 1     1   1078319 use 5.006;
  1         5  
  1         42  
4 1     1   5 use strict;
  1         1  
  1         37  
5 1     1   4 use warnings;
  1         7  
  1         31  
6              
7 1     1   5 use Carp;
  1         1  
  1         62  
8 1     1   4 use POSIX qw(mktime strftime localtime);
  1         2  
  1         5  
9 1     1   1104 use Socket qw(:DEFAULT :crlf);
  1         4464  
  1         3565  
10              
11             require Exporter;
12              
13             our @ISA = qw(Exporter);
14              
15             # Items to export into callers namespace by default. Note: do not export
16             # names by default without a very good reason. Use EXPORT_OK instead.
17             # Do not simply export all your public functions/methods/constants.
18              
19             # This allows declaration use NetAthlon2::RAW qw(:all);
20             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
21             # will save memory.
22             our %EXPORT_TAGS = ( 'all' => [ qw() ] );
23             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} });
24             our @EXPORT = qw();
25              
26             our $VERSION = '0.31';
27              
28             our $timeDelta = 1;
29              
30             local *FP;
31             local *DIR;
32              
33             sub new {
34 1     1 1 74 my ($class, %opts) = @_;
35 1         2 my ($self);
36              
37 1         1 $self = \%opts;
38 1         2 bless ($self, $class);
39              
40 1         4 return $self;
41             }
42              
43             sub open {
44 7     7 0 20 my ($self, $file) = @_;
45              
46 7 50 33     276 if ( defined $file && -f $file ) {
47 7         64 $self->{'file'} = $file;
48              
49 7 50       35 warn "Opening file ($file)\n" if ( exists $self->{'debug'} );
50 7 50       438 open (*FP, "< $file") || croak "Could not open $file";
51 7         39 return $self;
52             } else {
53 0         0 croak "$file not a file";
54 0         0 return undef;
55             }
56             }
57              
58             sub _parse_preamble {
59 7     7   12 my ($self) = @_;
60              
61 7 50       34 carp "First line not 251"
62             if ( $self->{'RAW'}[0] != 251 );
63              
64 7         10 $self->{'data'}->{'Sample Rate'} = int(${$self->{'RAW'}}[1]);
  7         34  
65              
66 7 50       75 carp "Line 3 not 1"
67             if ( $self->{'RAW'}[2] != 1 );
68              
69             # Hear Rate info
70 7         117 ( $self->{'data'}->{'Heart Rate'}->{'Zone 1'}->{'Max'},
71             $self->{'data'}->{'Heart Rate'}->{'Zone 3'}->{'Max'} ) =
72 7         16 split (/\s/, ${$self->{'RAW'}}[3]);
73 7         89 ( $self->{'data'}->{'Heart Rate'}->{'Zone 1'}->{'Min'},
74             $self->{'data'}->{'Heart Rate'}->{'Zone 3'}->{'Min'} ) =
75 7         19 split (/\s/, ${$self->{'RAW'}}[4]);
76 7         61 ( $self->{'data'}->{'Heart Rate'}->{'Zone 2'}->{'Max'},
77             $self->{'data'}->{'Heart Rate'}->{'Anaerobic Threshold'} ) =
78 7         14 split (/\s/, ${$self->{'RAW'}}[5]);
79 7         71 ( $self->{'data'}->{'Heart Rate'}->{'Zone 2'}->{'Min'},
80             $self->{'data'}->{'Heart Rate'}->{'Aerobic Threshold'} ) =
81 7         15 split (/\s/, ${$self->{'RAW'}}[6]);
82              
83             # Store the start time (converting to UNIX timestamp)
84 7         39 my ($mon, $day, $year, $hour, $h, $min, $m, $sec, $ampm);
85 7 50       186 if ( $self->{'file'} =~ /Bike(\d{4})-(\d{2})-(\d{2}) (\d{1,2})-(\d{2})([ap]m)\.RAW$/ ) {
86 7         261 $year = $1;
87 7         60 $mon = $2;
88 7         16 $day = $3;
89 7         46 $h = $4;
90 7         51 $m = $5;
91 7         13 $ampm = $6;
92              
93 7 50 33     40 warn "\tyear => $year, month => $mon, day => $day\n\thour => $h, minute => $m, $ampm\n"
94             if ( exists $self->{'debug'} && $self->{'debug'} >= 5 );
95              
96 7 50       585 if ( ${$self->{'RAW'}}[8] =~ m/^(\d{2})\.(\d{2})\.(\d{2})$/ ) {
  7         240  
97 7         16 $hour = $1;
98 7         32 $min = $2;
99 7         13 $sec = $3;
100              
101 7 50 33     109 warn "\thour => $hour, minute => $min, second => $sec\n"
102             if ( exists $self->{'debug'} && $self->{'debug'} >= 5 );
103              
104             # FIXME: There is a bug in some of the RAW data files as
105             # the time encoded in the filename is 1 minute after the
106             # time encoded in the file contents, hence the different
107             # comparison for the minute field.
108 7         265 my $mina = $h * 60 + $m;
109 7         17 my $minb = $hour * 60 + $min;
110 7 50       32 carp "Start time mismatch between file name ("
111             . $self->{'file'}
112             . ") and file contents ($hour:$min)"
113             if ( abs($mina-$minb) > $timeDelta );
114              
115             # deal with the noon and midnight hours.
116 7 100 66     78 $hour += 12 if ( $ampm eq 'pm' && $hour < 12 );
117 7 50 33     25 $hour = 0 if ( $ampm eq 'am' && $hour == 12 );
118              
119 7 50 33     26 warn "\thour => $hour\n"
120             if ( exists $self->{'debug'} && $self->{'debug'} >= 5 );
121              
122             } else {
123 0         0 carp "Can't verify performance start time (" . $self->{'RAW'}[8] . ")";
124             }
125 7         113 $self->{'data'}->{'Start Time'} = mktime($sec, $min, $hour, $day, ($mon-1), ($year-1900), 0, 0, -1);
126              
127             # Verify our date and time conversion is correct
128 7         766 my ($d) = strftime ("%Y-%m-%d %H:%M", localtime($self->{'data'}->{'Start Time'}));
129 7 50       631 carp "Failed in Start Date parsing ($year-$mon-$day $hour:$min != $d)\n"
130             if ( $d ne "$year-$mon-$day $hour:$min" );
131             } else {
132 0         0 carp "Can't determine what day this performance data is from";
133             }
134             }
135              
136             sub _parse_summary {
137 7     7   15 my ($self) = @_;
138              
139 7 50       33 carp "Start of summary section line not 254"
140             if ( $self->{'RAW'}[-5] != 254 );
141              
142 7         61 my ($h, $m, $s, $f) = split (/\./, $self->{'RAW'}[-3] );
143 7         18 chomp($f);
144 7         40 $self->{'data'}->{'Elapsed Time'} = ($h * 3600 + $m * 60 + $s + $f / 100) + 0.0;
145              
146             # Reset the last Elapsed Time in the Check Points to match
147             # the total Elapsed Time.
148 7         22 $self->{'data'}->{'Check Points'}->[scalar @{$self->{'data'}->{'Check Points'}} - 1]->{'Elapsed Time'}
  7         21  
149             = $self->{'data'}->{'Elapsed Time'};
150              
151 7 50       27 carp "Second to last line not 256"
152             if ( $self->{'RAW'}[-2] != 256 );
153              
154 7         12 my ($u);
155 7         288 ( $u, $self->{'data'}->{'Distance'}, $self->{'data'}->{'Cadence'} ) =
156 7         14 split (/\s+/, ${$self->{'RAW'}}[-1]);
157             }
158              
159             sub _parse_line {
160 1935     1935   3219 my ($self, $line) = @_;
161 1935         1926 my ($time, $dist);
162 1935         2160 chomp $line;
163 1935         2007 chop $line;
164 1935         12450 my ($hr, $u, $speed, $power, $cadence, $grade, $alt) = split /\s/, $line;
165              
166 1928         3138 $time = $self->{'data'}->{'Sample Rate'} *
167             ( exists $self->{'data'}->{'Check Points'}
168 1935 100       5569 ? scalar @{$self->{'data'}->{'Check Points'}}
169             : 0);
170              
171             # Insert a calculated distance for this Sample Rate,
172             # which will be used in calculating the Average Speed.
173 1935 100       5476 $dist = ( $speed / 10 ) *
174             ( exists $self->{'data'}->{'Check Points'}
175             ? ($self->{'data'}->{'Sample Rate'} / 3600)
176             : 0);
177              
178 1935         1883 push @{$self->{'data'}->{'Check Points'}},
  1935         18817  
179             {
180             'Elapsed Time' => $time,
181             'Calculated Distance' => $dist,
182             'Heart Rate' => $hr,
183             'Grade' => $grade / 10,
184             'Speed' => $speed / 10,
185             'Watts' => $power,
186             'Cadence' => $cadence,
187             'Altitude' => $alt,
188             };
189             }
190              
191             sub _add_averages {
192 7     7   11 my ($self) = @_;
193              
194             # Calculate some useful averages
195 7         25 my ( $c, $cc, $w, $wc, $hr, $hrc, $dist ) = ( 0, 0, 0, 0, 0, 0, 0 );
196             map {
197 1935 100       5516 if ( $_->{'Cadence'} > 0 ) {
  7         60  
198 1923         2459 $c += $_->{'Cadence'}; $cc++;
  1923         1980  
199 1923 100 100     9820 $self->{'data'}->{'Max Cadence'} = $_->{'Cadence'}
200             if ( ! exists $self->{'data'}->{'Max Cadence'} || $self->{'data'}->{'Max Cadence'} < $_->{'Cadence'} );
201             }
202              
203 1935 100       4507 if ( $_->{'Speed'} > 0 ) {
204 1928 100 100     9182 $self->{'data'}->{'Max Speed'} = $_->{'Speed'}
205             if ( ! exists $self->{'data'}->{'Max Speed'} || $self->{'data'}->{'Max Speed'} < $_->{'Speed'} );
206              
207             # There is a bug when you have a warm up time, the first
208             # checkpoint will have an unrealistic large value for Watts
209             # and a zero value for Speed.
210 1928 50       4522 if ( $_->{'Watts'} > 0 ) {
211 1928         2292 $w += $_->{'Watts'}; $wc++;
  1928         2035  
212 1928 100 100     9381 $self->{'data'}->{'Max Watts'} = $_->{'Watts'}
213             if ( ! exists $self->{'data'}->{'Max Watts'} || $self->{'data'}->{'Max Watts'} < $_->{'Watts'} );
214             }
215             }
216 1935 100       5165 if ( $_->{'Heart Rate'} > 0 ) {
217 1857         8272 $hr += $_->{'Heart Rate'}; $hrc++;
  1857         1704  
218 1857 100 100     11369 $self->{'data'}->{'Max Heart Rate'} = $_->{'Heart Rate'}
219             if ( ! exists $self->{'data'}->{'Max Heart Rate'} || $self->{'data'}->{'Max Heart Rate'} < $_->{'Heart Rate'} );
220             }
221 1935 100       14741 $dist += $_->{'Calculated Distance'}
222             if ( ( $_->{'Calculated Distance'} + 0.0 ) > 0 );
223 7         9 } @{$self->{'data'}->{'Check Points'}};
224 7 50       65 $self->{'data'}->{'Average Cadence'} = $c / $cc if ( $cc > 0 );
225 7 50       33 $self->{'data'}->{'Average Watts'} = $w / $wc if ( $wc > 0 );
226 7 100       28 $self->{'data'}->{'Average Heart Rate'} = $hr / $hrc if ( $hrc > 0 );
227              
228             # BUG: The Distance listed in the file is the total distance
229             # ridden, vs the Elapsed Time is not including any warmup time
230             # For example, in the Bike2009-10-25 5-05.RAW test file, the
231             # elapsed time is 2700 seconds (45 minutes), yet the distance traveled
232             # is 16.87, which was covered in 60 minutes. Therefor, need to recaclute
233             # the average speed based on the checkpoint's average speed.
234             #$self->{'data'}->{'Average Speed'} =
235             # $self->{'data'}->{'Distance'} / ($self->{'data'}->{'Elapsed Time'} / 3600);
236 7         14 $self->{'data'}->{'Calculated Distance'} = $dist;
237 7         46 $self->{'data'}->{'Average Speed'} = $dist / ($self->{'data'}->{'Elapsed Time'} / 3600);
238             }
239              
240             sub _verify_parse {
241 7     7   13 my ($self) = @_;
242              
243             # Verify we got good data. The number of sample lines should be close
244             # to Elapsed Time / Sample Rate. The 1.99 instead of 2 was determined
245             # emperically, and probably due to samping error or clock flux.
246 7         42 carp "Check Point Data does not match Sample Rate"
247 7 50       11 if ( scalar @{$self->{'data'}->{'Check Points'}} !=
248             int($self->{'data'}->{'Elapsed Time'} / $self->{'data'}->{'Sample Rate'} + 1.99));
249             }
250              
251             sub parse {
252 7     7 1 419016 my ($self, $file) = @_;
253 7         118 my ($cnt);
254              
255 7 50       99 $self->open($file) || return undef;
256              
257 7 50       39 warn "Reading contents of (" . $file . ")\n" if ( exists $self->{'debug'} );
258              
259             # Change the input record seperator so we get the \r\n eaten by
260             # perl itself, instead of having to code it in the regexes when
261             # parsing the file data.
262             {
263 7 50       13 local ($/) = $CRLF if ( $^O ne 'MSWin32' );
  7         111  
264 7         1990 @{$self->{'RAW'}} = ;
  7         1431  
265 7         200 chomp(@{$self->{'RAW'}});
  7         163  
266             }
267              
268 7         59 $self->close();
269              
270 7         39 croak "Not enough data in file"
271 7 50       15 if ( scalar @{$self->{'RAW'}} < 14 );
272              
273 7         4804 delete $self->{'data'};
274              
275 7         40 $self->_parse_preamble();
276              
277 7         63 $self->_parse_line($self->{'RAW'}[7]);
278 7         18 for ($cnt = 10; $cnt < scalar (@{$self->{'RAW'}}) - 5; $cnt++) {
  1928         4997  
279 1921         4784 $self->_parse_line($self->{'RAW'}[$cnt]);
280             }
281 7         26 $self->_parse_line($self->{'RAW'}[-4]);
282              
283 7         34 $self->_parse_summary();
284              
285 7         31 $self->_add_averages();
286              
287 7         41 $self->_verify_parse();
288              
289 7         43 return ($self->{'data'});
290             }
291              
292             sub close {
293 7     7 0 19 my ($self) = @_;
294              
295 7 50       31 warn "Closing file (" . $self->{'file'} . ")\n" if ( exists $self->{'debug'} );
296 7         100 close (FP);
297             }
298              
299             1;
300             __END__