postrunner 0.9.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +10 -10
- data/lib/postrunner/Activity.rb +2 -1
- data/lib/postrunner/ActivityListView.rb +1 -1
- data/lib/postrunner/ActivitySummary.rb +109 -7
- data/lib/postrunner/ChartView.rb +25 -6
- data/lib/postrunner/DeviceList.rb +1 -1
- data/lib/postrunner/EPO_Downloader.rb +5 -3
- data/lib/postrunner/FFS_Activity.rb +6 -5
- data/lib/postrunner/FFS_Device.rb +17 -3
- data/lib/postrunner/FitFileStore.rb +79 -16
- data/lib/postrunner/Main.rb +26 -11
- data/lib/postrunner/RecordListPageView.rb +1 -1
- data/lib/postrunner/UserProfileView.rb +42 -17
- data/lib/postrunner/version.rb +2 -2
- data/postrunner.gemspec +13 -13
- data/spec/FitFileStore_spec.rb +7 -7
- data/spec/spec_helper.rb +1 -1
- metadata +20 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bb8a80028ee50a8f543d589b44fd41523208483a1743d29ec86e37525ff84a13
|
4
|
+
data.tar.gz: 9834c6dc556e9e118fca144a8edd33e9cecc51eb9bc4487d43218973f5dae719
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8544b0aca1e86a9ca4e90aa437684a9fc56ac925cefe115ac9453816d1373242fcf6aa94d86ea779ea947ebb982b318141c45df43197d77a78b2375aeb5f6724
|
7
|
+
data.tar.gz: d5354f1f321685cb7181c75cee537d36695bddb0cecea175cd703de678af9055db10e089740f6719f1c6097e3fc2a2713951ae396b558b3ffc39a5bbfbe9f722
|
data/README.md
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# PostRunner
|
2
2
|
|
3
3
|
PostRunner is an application to manage FIT files such as those
|
4
|
-
produced by Garmin products like the Forerunner 620 (FR620)
|
5
|
-
3
|
6
|
-
analyze the data. In addition to
|
7
|
-
|
8
|
-
heart rate variability (HRV)
|
9
|
-
orbit prediction (EPO) data on
|
10
|
-
It is an offline alternative to
|
11
|
-
|
12
|
-
systems as well.
|
4
|
+
produced by Garmin products like the Forerunner 620 (FR620),
|
5
|
+
Forerunner 25 (FR25), Fenix 3, Fenix 3HR and Fenix 5. It allows you to
|
6
|
+
import the files from the device and analyze the data. In addition to
|
7
|
+
the common features like plotting pace, heart rates, elevation and
|
8
|
+
other captured values it also provides a heart rate variability (HRV)
|
9
|
+
analysis. It can also update satellite orbit prediction (EPO) data on
|
10
|
+
the device to speed-up GPS fix times. It is an offline alternative to
|
11
|
+
Garmin Connect. The software has been developed and tested on Linux
|
12
|
+
but should work on other operating systems as well.
|
13
13
|
|
14
14
|
## Installation
|
15
15
|
|
@@ -99,7 +99,7 @@ command while you have your device mounted via USB.
|
|
99
99
|
$ postrunner update-gps
|
100
100
|
```
|
101
101
|
|
102
|
-
This was tested on the FR620 and will probably also work on the FR220.
|
102
|
+
This was tested on the FR620 and FR25 and will probably also work on the FR220.
|
103
103
|
Other devices may work, but you use this at your own risk. This
|
104
104
|
feature will download a file called EPO.BIN and copy it to
|
105
105
|
GARMIN/REMOTESW/EPO.BIN.
|
data/lib/postrunner/Activity.rb
CHANGED
@@ -95,6 +95,7 @@ module PostRunner
|
|
95
95
|
'challenge' => 'Challenge',
|
96
96
|
'indoor_skiing' => 'Indoor Skiing',
|
97
97
|
'cardio_training' => 'Cardio Training',
|
98
|
+
'virtual_activity' => 'Virtual Activity',
|
98
99
|
'all' => 'All'
|
99
100
|
}
|
100
101
|
|
@@ -379,7 +380,7 @@ module PostRunner
|
|
379
380
|
end
|
380
381
|
|
381
382
|
def activity_sub_type
|
382
|
-
ActivitySubTypes[@sub_sport] ||
|
383
|
+
ActivitySubTypes[@sub_sport] || "Undefined #{@sub_sport}"
|
383
384
|
end
|
384
385
|
|
385
386
|
def distance(timestamp, unit_system)
|
@@ -26,7 +26,7 @@ module PostRunner
|
|
26
26
|
|
27
27
|
def initialize(ffs)
|
28
28
|
@ffs = ffs
|
29
|
-
@unit_system = @ffs.store['config']['unit_system']
|
29
|
+
@unit_system = @ffs.store['config']['unit_system'].to_sym
|
30
30
|
@page_size = 20
|
31
31
|
@page_no = -1
|
32
32
|
@last_page = (@ffs.activities.length - 1) / @page_size
|
@@ -87,6 +87,7 @@ module PostRunner
|
|
87
87
|
{ :metric => 'km', :statute => 'mi'}) ])
|
88
88
|
end
|
89
89
|
t.row([ 'Time:', secsToHMS(session.total_timer_time) ])
|
90
|
+
t.row([ 'Elapsed Time:', secsToHMS(session.total_elapsed_time) ])
|
90
91
|
t.row([ 'Avg. Speed:',
|
91
92
|
local_value(session, 'avg_speed', '%.1f %s',
|
92
93
|
{ :metric => 'km/h', :statute => 'mph' }) ])
|
@@ -115,15 +116,15 @@ module PostRunner
|
|
115
116
|
local_value(session, 'avg_vertical_oscillation', '%.1f %s',
|
116
117
|
{ :metric => 'cm', :statute => 'in' }) ])
|
117
118
|
t.row([ 'Vertical Ratio:',
|
118
|
-
session.
|
119
|
-
"#{session.
|
119
|
+
session.avg_vertical_ratio ?
|
120
|
+
"#{session.avg_vertical_ratio}%" : '-' ])
|
120
121
|
t.row([ 'Avg. Ground Contact Time:',
|
121
122
|
session.avg_stance_time ?
|
122
123
|
"#{session.avg_stance_time.round} ms" : '-' ])
|
123
|
-
t.row([ 'Avg.
|
124
|
-
session.
|
125
|
-
"#{session.
|
126
|
-
"#{100.0 - session.
|
124
|
+
t.row([ 'Avg. Stance Time Balance:',
|
125
|
+
session.avg_stance_time_balance ?
|
126
|
+
"#{session.avg_stance_time_balance}% L / " +
|
127
|
+
"#{100.0 - session.avg_stance_time_balance}% R" : ';' ])
|
127
128
|
end
|
128
129
|
if @activity.sport == 'cycling'
|
129
130
|
t.row([ 'Avg. Cadence:',
|
@@ -145,6 +146,14 @@ module PostRunner
|
|
145
146
|
t.row([ 'Aerobic Training Effect:', session.total_training_effect ])
|
146
147
|
end
|
147
148
|
|
149
|
+
if (p_epoc = peak_epoc) > 0.0
|
150
|
+
t.row([ 'Peak EPOC:', "%.0f ml/kg" % p_epoc ])
|
151
|
+
end
|
152
|
+
|
153
|
+
if (trimp = trimp_exp) > 0.0
|
154
|
+
t.row([ 'TRIMP:', trimp.round ])
|
155
|
+
end
|
156
|
+
|
148
157
|
rec_info = @fit_activity.recovery_info
|
149
158
|
t.row([ 'Ignored Recovery Time:',
|
150
159
|
rec_info ? secsToDHMS(rec_info * 60) : '-' ])
|
@@ -307,7 +316,7 @@ module PostRunner
|
|
307
316
|
if zone.type == 18
|
308
317
|
total_time = 0.0
|
309
318
|
if zone.time_in_hr_zone
|
310
|
-
zone.time_in_hr_zone.each { |tiz| total_time += tiz }
|
319
|
+
zone.time_in_hr_zone.each { |tiz| total_time += tiz if tiz }
|
311
320
|
end
|
312
321
|
break if total_time <= 0.0
|
313
322
|
if zone.heart_rate_zones
|
@@ -345,12 +354,22 @@ module PostRunner
|
|
345
354
|
def local_value(fdr, field, format, units)
|
346
355
|
unit = units[@unit_system]
|
347
356
|
value = fdr.get_as(field, unit)
|
357
|
+
if value.nil? && field == 'avg_speed'
|
358
|
+
# New fit files used 'enhanced_avg_speed' instead of the older
|
359
|
+
# 'avg_speed'.
|
360
|
+
value = fdr.get_as('enhanced_avg_speed', unit)
|
361
|
+
end
|
348
362
|
return '-' unless value
|
349
363
|
"#{format % [value, unit]}"
|
350
364
|
end
|
351
365
|
|
352
366
|
def pace(fdr, field, show_unit = true)
|
353
367
|
speed = fdr.get(field)
|
368
|
+
if speed.nil? && field == 'avg_speed'
|
369
|
+
# New fit files used 'enhanced_avg_speed' instead of the older
|
370
|
+
# 'avg_speed'.
|
371
|
+
speed = fdr.get('enhanced_avg_speed')
|
372
|
+
end
|
354
373
|
case @unit_system
|
355
374
|
when :metric
|
356
375
|
"#{speedToPace(speed)}#{show_unit ? ' min/km' : ''}"
|
@@ -361,6 +380,89 @@ module PostRunner
|
|
361
380
|
end
|
362
381
|
end
|
363
382
|
|
383
|
+
def trimp_exp
|
384
|
+
# According to Bannister/Morton
|
385
|
+
# TRIMPexp = sum(D x HRr x 0.64e^y)
|
386
|
+
# Where
|
387
|
+
#
|
388
|
+
# D is the duration in minutes at a particular Heart Rate
|
389
|
+
# HRr is the Heart Rate as a fraction of Heart Rate Reserve
|
390
|
+
# y is the HRr multiplied by 1.92 for men and 1.67 for women.
|
391
|
+
return 0.0 unless (user_data = @fit_activity.user_data.first)
|
392
|
+
|
393
|
+
user_profile = @fit_activity.user_profiles.first
|
394
|
+
hr_zones = @fit_activity.heart_rate_zones.first
|
395
|
+
session = @fit_activity.sessions[0]
|
396
|
+
|
397
|
+
unless (user_profile && (rest_hr = user_profile.resting_heart_rate)) ||
|
398
|
+
(hr_zones && (rest_hr = hr_zones.resting_heart_rate))
|
399
|
+
# We must have a valid resting heart rate to compute TRIMP.
|
400
|
+
return 0.0
|
401
|
+
end
|
402
|
+
unless (user_data && (max_hr = user_data.max_hr)) ||
|
403
|
+
(hr_zones && (max_hr = hr_zones.max_heart_rate))
|
404
|
+
# We must have a valid maximum heart rate to compute TRIMP.
|
405
|
+
return 0.0
|
406
|
+
end
|
407
|
+
unless (session && session.avg_heart_rate &&
|
408
|
+
avg_hr = session.avg_heart_rate)
|
409
|
+
return 0.0
|
410
|
+
end
|
411
|
+
|
412
|
+
sex_factor = user_data.gender == 'male' ? 1.92 : 1.67
|
413
|
+
|
414
|
+
# Instead of using the average heart rate for the whole activity we
|
415
|
+
# apply the equation for each heart rate sample and accumulate them.
|
416
|
+
sum = 0.0
|
417
|
+
prev_timestamp = nil
|
418
|
+
@activity.fit_activity.records.each do |r|
|
419
|
+
# We need a valid timestmap and a valid previous timestamp. If they
|
420
|
+
# are more than 10 seconds appart we discard the values as there was
|
421
|
+
# likely a pause in the activity.
|
422
|
+
if prev_timestamp && r.timestamp && r.heart_rate &&
|
423
|
+
r.timestamp - prev_timestamp <= 10
|
424
|
+
# Compute the heart rate as fraction of the heart rate reserve
|
425
|
+
hr_r = (r.heart_rate - rest_hr).to_f / (max_hr - rest_hr)
|
426
|
+
|
427
|
+
duration_min = (r.timestamp - prev_timestamp) / 60.0
|
428
|
+
#sum += duration_min * hr_r * 0.64 * Math.exp(sex_factor * hr_r)
|
429
|
+
sum += duration_min * hr_r * 0.64 * Math.exp(sex_factor * hr_r)
|
430
|
+
end
|
431
|
+
|
432
|
+
prev_timestamp = r.timestamp
|
433
|
+
end
|
434
|
+
|
435
|
+
sum
|
436
|
+
|
437
|
+
# Alternatively here is an avarage HR based implementation
|
438
|
+
# hr_r = (session.avg_heart_rate - rest_hr).to_f / (max_hr - rest_hr)
|
439
|
+
# duration_min = session.total_elapsed_time / 60.0
|
440
|
+
# duration_min * hr_r * 0.64 * Math.exp(sex_factor * hr_r)
|
441
|
+
end
|
442
|
+
|
443
|
+
def peak_epoc
|
444
|
+
# Peak EPOC value according to figure 2 in the following white paper by
|
445
|
+
# FristBeat:
|
446
|
+
# https://www.firstbeat.com/wp-content/uploads/2015/10/white_paper_training_effect.pdf
|
447
|
+
unless @fit_activity.physiological_metrics &&
|
448
|
+
(pm = @fit_activity.physiological_metrics.last) &&
|
449
|
+
(te = pm.aerobic_training_effect)
|
450
|
+
return 0.0
|
451
|
+
end
|
452
|
+
unless (user_data = @fit_activity.user_data.first) &&
|
453
|
+
(ac = user_data.activity_class)
|
454
|
+
return 0.0
|
455
|
+
end
|
456
|
+
|
457
|
+
# The following formula was taken from
|
458
|
+
# http://www.movescount.com/apps/app10020404-EPOC_from_TE
|
459
|
+
# It apparently approximates the graph in figure 2 in the FirstBeat
|
460
|
+
# paper.
|
461
|
+
epoc = -11.0 + te * (20.0 + te * (-47.0/4.0 + te * (3.0 - te / 4.0)))
|
462
|
+
(-102.0 + te * (759.0 / 4.0 + te * (-2867.0 / 24.0 +
|
463
|
+
te * (139.0 / 4.0 - 73.0 / 24.0 * te))) - epoc) / 10.0 * ac + epoc
|
464
|
+
end
|
465
|
+
|
364
466
|
end
|
365
467
|
|
366
468
|
end
|
data/lib/postrunner/ChartView.rb
CHANGED
@@ -73,6 +73,14 @@ module PostRunner
|
|
73
73
|
:colors => '#900000',
|
74
74
|
:show => false
|
75
75
|
},
|
76
|
+
{
|
77
|
+
:id => 'respiration_rate',
|
78
|
+
:label => 'Respiration Rate',
|
79
|
+
:unit => 'brpm',
|
80
|
+
:graph => :line_graph,
|
81
|
+
:colors => '#9cd6ef',
|
82
|
+
:show => true
|
83
|
+
},
|
76
84
|
{
|
77
85
|
:id => 'performance_condition',
|
78
86
|
:label => 'Performance Condition',
|
@@ -290,12 +298,23 @@ EOT
|
|
290
298
|
last_value = nil
|
291
299
|
last_timestamp = nil
|
292
300
|
@activity.fit_activity.records.each do |r|
|
293
|
-
if last_timestamp && (r.timestamp - last_timestamp) >
|
301
|
+
if last_timestamp && (r.timestamp - last_timestamp) > 10.0
|
294
302
|
# We have a gap in the values that is longer than 5 seconds. We'll
|
295
303
|
# finish the line and start a new one later.
|
296
|
-
data_set << [ (
|
304
|
+
data_set << [ (last_timestamp - start_time + 1).to_i * 1000, nil ]
|
305
|
+
end
|
306
|
+
value = r.get_as(chart[:id], chart[:unit] || '')
|
307
|
+
if value.nil? && chart[:id] == 'speed'
|
308
|
+
# If speed field doesn't exist the value might be in the
|
309
|
+
# enhanced_speed field.
|
310
|
+
value = r.get_as('enhanced_speed', chart[:unit] || '')
|
311
|
+
end
|
312
|
+
if value.nil? && chart[:id] == 'altitude'
|
313
|
+
# If altitude field doesn't exist the value might be in the
|
314
|
+
# enhanced_elevation field.
|
315
|
+
value = r.get_as('enhanced_elevation', chart[:unit] || '')
|
297
316
|
end
|
298
|
-
if
|
317
|
+
if value
|
299
318
|
if chart[:id] == 'pace'
|
300
319
|
# Slow speeds lead to very large pace values that make the graph
|
301
320
|
# hard to read. We cap the pace at 20.0 min/km to keep it
|
@@ -310,7 +329,7 @@ EOT
|
|
310
329
|
min_value = value if (min_value.nil? || min_value > value)
|
311
330
|
end
|
312
331
|
end
|
313
|
-
|
332
|
+
if value
|
314
333
|
data_set << [ (r.timestamp - start_time).to_i * 1000, value ]
|
315
334
|
end
|
316
335
|
last_value = value
|
@@ -337,7 +356,7 @@ EOT
|
|
337
356
|
#{chart[:colors] ? "color: \"#{chart[:colors]}\"," : ''}
|
338
357
|
lines: { show: true#{chart[:id] == 'pace' ? '' :
|
339
358
|
', fill: true'} } } ],
|
340
|
-
{ xaxis: { mode: "time" },
|
359
|
+
{ xaxis: { mode: "time", min: 0.0 },
|
341
360
|
grid: { markings: lap_marks, hoverable: true }
|
342
361
|
EOT
|
343
362
|
if chart[:id] == 'pace'
|
@@ -417,7 +436,7 @@ EOT
|
|
417
436
|
" fillColor: \"#{chart[:colors][index][0]}\", " +
|
418
437
|
" fill: true, radius: 2 } }"
|
419
438
|
end.join(', ')
|
420
|
-
s << "], { xaxis: { mode: \"time\" }, " +
|
439
|
+
s << "], { xaxis: { mode: \"time\", min: 0.0 }, " +
|
421
440
|
(chart[:id] == 'gct_balance' ? gct_balance_yaxis(data_sets) : '') +
|
422
441
|
" grid: { markings: lap_marks, hoverable: true } });\n"
|
423
442
|
s << lap_mark_labels(chart_id, start_time)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = DeviceList.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2015, 2018 by Chris Schlaeger <cs@taskjuggler.org>
|
7
7
|
#
|
8
8
|
# This program is free software; you can redistribute it and/or modify
|
9
9
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -12,6 +12,7 @@
|
|
12
12
|
|
13
13
|
require 'uri'
|
14
14
|
require 'net/http'
|
15
|
+
require 'net/https'
|
15
16
|
|
16
17
|
module PostRunner
|
17
18
|
|
@@ -20,7 +21,7 @@ module PostRunner
|
|
20
21
|
# devices pick up this file under GARMIN/GARMIN/REMOTESW/EPO.BIN.
|
21
22
|
class EPO_Downloader
|
22
23
|
|
23
|
-
@@URI = URI('
|
24
|
+
@@URI = URI('https://omt.garmin.com/Rce/ProtobufApi/EphemerisService/GetEphemerisData')
|
24
25
|
# This is the payload of the POST request. It was taken from
|
25
26
|
# http://www.kluenter.de/garmin-ephemeris-files-and-linux/. It may contain
|
26
27
|
# a product ID or serial number.
|
@@ -33,7 +34,8 @@ module PostRunner
|
|
33
34
|
|
34
35
|
# Create an EPO_Downloader object.
|
35
36
|
def initialize
|
36
|
-
@
|
37
|
+
@https = Net::HTTP.new(@@URI.host, @@URI.port)
|
38
|
+
@https.use_ssl = true
|
37
39
|
@request = Net::HTTP::Post.new(@@URI.path, initheader = @@HEADER)
|
38
40
|
@request.body = @@POST_DATA
|
39
41
|
end
|
@@ -57,7 +59,7 @@ module PostRunner
|
|
57
59
|
|
58
60
|
def get_epo_from_server
|
59
61
|
begin
|
60
|
-
res = @
|
62
|
+
res = @https.request(@request)
|
61
63
|
rescue => e
|
62
64
|
Log.error "Extended Prediction Orbit (EPO) data download error: " +
|
63
65
|
e.message
|
@@ -93,6 +93,7 @@ module PostRunner
|
|
93
93
|
'challenge' => 'Challenge',
|
94
94
|
'indoor_skiing' => 'Indoor Skiing',
|
95
95
|
'cardio_training' => 'Cardio Training',
|
96
|
+
'virtual_activity' => 'Virtual Activity',
|
96
97
|
'all' => 'All'
|
97
98
|
}
|
98
99
|
|
@@ -177,7 +178,7 @@ module PostRunner
|
|
177
178
|
|
178
179
|
def events
|
179
180
|
load_fit_file
|
180
|
-
puts EventList.new(self, @store['config']['unit_system']).to_s
|
181
|
+
puts EventList.new(self, @store['config']['unit_system'].to_sym).to_s
|
181
182
|
end
|
182
183
|
|
183
184
|
def show
|
@@ -190,12 +191,12 @@ module PostRunner
|
|
190
191
|
|
191
192
|
def sources
|
192
193
|
load_fit_file
|
193
|
-
puts DataSources.new(self, @store['config']['unit_system']).to_s
|
194
|
+
puts DataSources.new(self, @store['config']['unit_system'].to_sym).to_s
|
194
195
|
end
|
195
196
|
|
196
197
|
def summary
|
197
198
|
load_fit_file
|
198
|
-
puts ActivitySummary.new(self, @store['config']['unit_system'],
|
199
|
+
puts ActivitySummary.new(self, @store['config']['unit_system'].to_sym,
|
199
200
|
{ :name => @name,
|
200
201
|
:type => activity_type,
|
201
202
|
:sub_type => activity_sub_type }).to_s
|
@@ -244,7 +245,7 @@ module PostRunner
|
|
244
245
|
|
245
246
|
def generate_html_report
|
246
247
|
load_fit_file
|
247
|
-
ActivityView.new(self, @store['config']['unit_system'])
|
248
|
+
ActivityView.new(self, @store['config']['unit_system'].to_sym)
|
248
249
|
end
|
249
250
|
|
250
251
|
def activity_type
|
@@ -252,7 +253,7 @@ module PostRunner
|
|
252
253
|
end
|
253
254
|
|
254
255
|
def activity_sub_type
|
255
|
-
ActivitySubTypes[@sub_sport] ||
|
256
|
+
ActivitySubTypes[@sub_sport] || "Undefined #{@sub_sport}"
|
256
257
|
end
|
257
258
|
|
258
259
|
def distance(timestamp, unit_system)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FFS_Device.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2015, 2016, 2018, 2020 by Chris Schlaeger <cs@taskjuggler.org>
|
7
7
|
#
|
8
8
|
# This program is free software; you can redistribute it and/or modify
|
9
9
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -23,7 +23,7 @@ module PostRunner
|
|
23
23
|
# dashes. All objects are transparently stored in the PEROBS::Store.
|
24
24
|
class FFS_Device < PEROBS::Object
|
25
25
|
|
26
|
-
attr_persist :activities, :monitorings, :short_uid, :long_uid
|
26
|
+
attr_persist :activities, :monitorings, :metrics, :short_uid, :long_uid
|
27
27
|
|
28
28
|
# Create a new FFS_Device object.
|
29
29
|
# @param p [PEROBS::Handle] p
|
@@ -41,6 +41,7 @@ module PostRunner
|
|
41
41
|
def restore
|
42
42
|
attr_init(:activities) { @store.new(PEROBS::Array) }
|
43
43
|
attr_init(:monitorings) { @store.new(PEROBS::Array) }
|
44
|
+
attr_init(:metrics) { @store.new(PEROBS::Array) }
|
44
45
|
end
|
45
46
|
|
46
47
|
# Add a new FIT file for this device.
|
@@ -61,6 +62,11 @@ module PostRunner
|
|
61
62
|
entities = @monitorings
|
62
63
|
type = 'monitoring'
|
63
64
|
new_entity_class = FFS_Monitoring
|
65
|
+
elsif fit_entity.is_a?(Fit4Ruby::Metrics)
|
66
|
+
entity = metrics_by_file_name(File.basename(fit_file_name))
|
67
|
+
entities = @metrics
|
68
|
+
type = 'metrics'
|
69
|
+
new_entity_class = FFS_Metrics
|
64
70
|
else
|
65
71
|
Log.fatal "Unsupported FIT entity #{fit_entity.class}"
|
66
72
|
end
|
@@ -77,7 +83,8 @@ module PostRunner
|
|
77
83
|
return nil
|
78
84
|
end
|
79
85
|
else
|
80
|
-
# Don't add the entity if has deleted before and overwrite isn't
|
86
|
+
# Don't add the entity if it has deleted before and overwrite isn't
|
87
|
+
# true.
|
81
88
|
path = @store['file_store'].fit_file_dir(File.basename(fit_file_name),
|
82
89
|
long_uid, type)
|
83
90
|
fq_fit_file_name = File.join(path, File.basename(fit_file_name))
|
@@ -130,6 +137,13 @@ module PostRunner
|
|
130
137
|
@monitorings.find { |a| a.fit_file_name == file_name }
|
131
138
|
end
|
132
139
|
|
140
|
+
# Return the metrics with the given file name.
|
141
|
+
# @param file_name [String] Base name of the fit file.
|
142
|
+
# @return [FFS_Activity] Corresponding FFS_Metrics or nil.
|
143
|
+
def metrics_by_file_name(file_name)
|
144
|
+
@metrics.find { |a| a.fit_file_name == file_name }
|
145
|
+
end
|
146
|
+
|
133
147
|
# Return all monitorings that overlap with the time interval given by
|
134
148
|
# from_time and to_time.
|
135
149
|
# @param from_time [Time] start time of the interval
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitFileStore.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2015, 2016, 2018 by Chris Schlaeger <cs@taskjuggler.org>
|
7
7
|
#
|
8
8
|
# This program is free software; you can redistribute it and/or modify
|
9
9
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -44,7 +44,7 @@ module PostRunner
|
|
44
44
|
# Setup non-persistent variables.
|
45
45
|
def restore
|
46
46
|
@data_dir = @store['config']['data_dir']
|
47
|
-
# Ensure that we have
|
47
|
+
# Ensure that we have a Hash in the store to hold all known devices.
|
48
48
|
@store['devices'] = @store.new(PEROBS::Hash) unless @store['devices']
|
49
49
|
|
50
50
|
@devices_dir = File.join(@data_dir, 'devices')
|
@@ -69,8 +69,58 @@ module PostRunner
|
|
69
69
|
end
|
70
70
|
|
71
71
|
# Version upgrade logic.
|
72
|
-
def handle_version_update
|
73
|
-
|
72
|
+
def handle_version_update(from_version, to_version)
|
73
|
+
if from_version <= Gem::Version.new('0.12.0')
|
74
|
+
# PostRunner up until version 0.12.0 was using a long_uid with
|
75
|
+
# manufacturer name and product name. This was a bad idea since unknown
|
76
|
+
# devices were resolved to their numerical ID. In case the unknown ID
|
77
|
+
# was later added to the dictionary in fit4ruby version update, it
|
78
|
+
# resolved to its name and the device was recognized as a new device.
|
79
|
+
# Versions after 0.12.0 only use the numerical versions for the device
|
80
|
+
# long_uid and directory names.
|
81
|
+
uid_remap = {}
|
82
|
+
@store['devices'].each do |uid, device|
|
83
|
+
old_uid = uid
|
84
|
+
|
85
|
+
if (first_activity = device.activities.first)
|
86
|
+
first_activity.load_fit_file
|
87
|
+
if (fit_activity = first_activity.fit_activity)
|
88
|
+
if (device_info = fit_activity.device_infos.first)
|
89
|
+
new_uid = "#{device_info.numeric_manufacturer}-" +
|
90
|
+
"#{device_info.numeric_product}-#{device_info.serial_number}"
|
91
|
+
|
92
|
+
uid_remap[old_uid] = new_uid
|
93
|
+
puts first_activity.fit_file_name
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
@store.transaction do
|
100
|
+
pwd = Dir.pwd
|
101
|
+
base_dir_name = @store['config']['devices_dir']
|
102
|
+
Dir.chdir(base_dir_name)
|
103
|
+
|
104
|
+
uid_remap.each do |old_uid, new_uid|
|
105
|
+
if Dir.exist?(old_uid) && !Dir.exist?(new_uid) &&
|
106
|
+
!File.symlink?(old_uid)
|
107
|
+
# Rename the directory from the old (string) scheme to the
|
108
|
+
# new numeric scheme.
|
109
|
+
FileUtils.mv(old_uid, new_uid)
|
110
|
+
# Create a symbolic link with that points the old name to
|
111
|
+
# the new name.
|
112
|
+
File.symlink(new_uid, old_uid)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Now update the long_uid in the FFS_Device object
|
116
|
+
@store['devices'][new_uid] = device = @store['devices'][old_uid]
|
117
|
+
device.long_uid = new_uid
|
118
|
+
@store['devices'].delete(old_uid)
|
119
|
+
end
|
120
|
+
|
121
|
+
Dir.chdir(pwd)
|
122
|
+
end
|
123
|
+
end
|
74
124
|
end
|
75
125
|
|
76
126
|
# Add a file to the store.
|
@@ -80,27 +130,23 @@ module PostRunner
|
|
80
130
|
# @return [FFS_Activity or FFS_Monitoring] Corresponding entry in the
|
81
131
|
# FitFileStore or nil if file could not be added.
|
82
132
|
def add_fit_file(fit_file_name, fit_entity = nil, overwrite = false)
|
83
|
-
|
84
|
-
if @store['fit_file_md5sums'].include?(md5sum)
|
85
|
-
# The FIT file is already stored in the DB.
|
86
|
-
return nil unless overwrite
|
87
|
-
end
|
88
|
-
|
89
|
-
# If we the file hasn't been read yet, read it in as a
|
133
|
+
# If the file hasn't been read yet, read it in as a
|
90
134
|
# Fit4Ruby::Activity or Fit4Ruby::Monitoring entity.
|
91
135
|
unless fit_entity
|
92
136
|
return nil unless (fit_entity = read_fit_file(fit_file_name))
|
93
137
|
end
|
94
138
|
|
95
139
|
unless [ Fit4Ruby::Activity,
|
96
|
-
Fit4Ruby::Monitoring_B
|
140
|
+
Fit4Ruby::Monitoring_B,
|
141
|
+
Fit4Ruby::Metrics ].include?(fit_entity.class)
|
97
142
|
Log.fatal "Unsupported FIT file type #{fit_entity.class}"
|
98
143
|
end
|
99
144
|
|
100
145
|
# Generate a String that uniquely identifies the device that generated
|
101
146
|
# the FIT file.
|
102
147
|
id = extract_fit_file_id(fit_entity)
|
103
|
-
long_uid = "#{id[:
|
148
|
+
long_uid = "#{id[:numeric_manufacturer]}-" +
|
149
|
+
"#{id[:numeric_product]}-#{id[:serial_number]}"
|
104
150
|
|
105
151
|
# Make sure the device that created the FIT file is properly registered.
|
106
152
|
device = register_device(long_uid)
|
@@ -192,6 +238,7 @@ module PostRunner
|
|
192
238
|
@store['records'].generate_html_reports
|
193
239
|
generate_html_index_pages
|
194
240
|
end
|
241
|
+
|
195
242
|
# Determine the right directory for the given FIT file. The resulting path
|
196
243
|
# looks something like /home/user/.postrunner/devices/garmin-fenix3-1234/
|
197
244
|
# activity/5A.
|
@@ -451,7 +498,8 @@ module PostRunner
|
|
451
498
|
|
452
499
|
def extract_fit_file_id(fit_entity)
|
453
500
|
unless (fid = fit_entity.file_id)
|
454
|
-
Log.
|
501
|
+
Log.error 'FIT file has no file_id section'
|
502
|
+
return nil
|
455
503
|
end
|
456
504
|
|
457
505
|
if fid.manufacturer == 'garmin' &&
|
@@ -466,17 +514,31 @@ module PostRunner
|
|
466
514
|
return {
|
467
515
|
:manufacturer => di.manufacturer,
|
468
516
|
:product => di.garmin_product || di.product,
|
517
|
+
:numeric_manufacturer => di.numeric_manufacturer,
|
518
|
+
:numeric_product => di.numeric_product,
|
469
519
|
:serial_number => di.serial_number
|
470
520
|
}
|
471
521
|
end
|
472
522
|
end
|
473
|
-
Log.
|
523
|
+
Log.error "Fit entity has no device info for 0"
|
524
|
+
return nil
|
474
525
|
else
|
475
526
|
# And for all properly developed devices we can just look at the
|
476
527
|
# file_id section.
|
528
|
+
if fid.manufacturer.nil? ||
|
529
|
+
fid.manufacturer[0..'Undocumented value'.length - 1] ==
|
530
|
+
'Undocumented value'
|
531
|
+
Log.error "Cannot store FIT files for unknown manufacturer " +
|
532
|
+
fid.manufacturer
|
533
|
+
return nil
|
534
|
+
end
|
535
|
+
fid.serial_number ||= 0
|
536
|
+
|
477
537
|
return {
|
478
538
|
:manufacturer => fid.manufacturer,
|
479
539
|
:product => fid.garmin_product || fid.product,
|
540
|
+
:numeric_manufacturer => di.numeric_manufacturer,
|
541
|
+
:numeric_product => di.numeric_product,
|
480
542
|
:serial_number => fid.serial_number
|
481
543
|
}
|
482
544
|
end
|
@@ -496,7 +558,8 @@ module PostRunner
|
|
496
558
|
@store.new(FFS_Device, short_uid, long_uid)
|
497
559
|
|
498
560
|
# Create the directory to store the FIT files of this device.
|
499
|
-
create_directory(File.join(@devices_dir, long_uid),
|
561
|
+
create_directory(File.join(@devices_dir, long_uid),
|
562
|
+
long_uid)
|
500
563
|
end
|
501
564
|
|
502
565
|
@store['devices'][long_uid]
|
data/lib/postrunner/Main.rb
CHANGED
@@ -57,11 +57,13 @@ module PostRunner
|
|
57
57
|
create_directory(@db_dir, 'PostRunner data')
|
58
58
|
ensure_flat_file_db
|
59
59
|
@db = PEROBS::Store.new(File.join(@db_dir, 'database'),
|
60
|
-
{ :engine => PEROBS::FlatFileDB
|
60
|
+
{ :engine => PEROBS::FlatFileDB,
|
61
|
+
:progressmeter =>
|
62
|
+
PEROBS::ConsoleProgressMeter.new })
|
61
63
|
# Create a hash to store configuration data in the store unless it
|
62
64
|
# exists already.
|
63
65
|
cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
|
64
|
-
cfg['unit_system'] ||=
|
66
|
+
cfg['unit_system'] ||= 'metric'
|
65
67
|
cfg['version'] ||= VERSION
|
66
68
|
# First day of the week. 0 means Sunday, 1 Monday and so on.
|
67
69
|
cfg['week_start_day'] ||= 1
|
@@ -105,7 +107,7 @@ module PostRunner
|
|
105
107
|
|
106
108
|
opts.separator <<"EOT"
|
107
109
|
|
108
|
-
Copyright (c) 2014, 2015, 2016, 2017 by Chris Schlaeger
|
110
|
+
Copyright (c) 2014, 2015, 2016, 2017, 2018, 2019, 2020 by Chris Schlaeger
|
109
111
|
|
110
112
|
This program is free software; you can redistribute it and/or modify it under
|
111
113
|
the terms of version 2 of the GNU General Public License as published by the
|
@@ -252,7 +254,7 @@ weekly [ <YYYY-MM-DD> ]
|
|
252
254
|
week. If no date is given, yesterday's week will be used.
|
253
255
|
|
254
256
|
|
255
|
-
<fit file> An absolute or relative name of a .FIT file.
|
257
|
+
<fit file> An absolute or relative name of a .FIT or .fit file.
|
256
258
|
|
257
259
|
<ref> The index or a range of indexes to activities in the database.
|
258
260
|
:1 is the newest imported activity
|
@@ -437,12 +439,12 @@ EOT
|
|
437
439
|
|
438
440
|
def process_files(files_or_dirs, command)
|
439
441
|
if files_or_dirs.empty?
|
440
|
-
Log.abort("You must provide at least one .FIT file name.")
|
442
|
+
Log.abort("You must provide at least one .FIT or .fit file name.")
|
441
443
|
end
|
442
444
|
|
443
445
|
files_or_dirs.each do |fod|
|
444
446
|
if File.directory?(fod)
|
445
|
-
Dir.glob(File.join(fod, '*.FIT')).each do |file|
|
447
|
+
Dir.glob(File.join(fod, '*.FIT'), File::FNM_CASEFOLD).each do |file|
|
446
448
|
process_file(file, command)
|
447
449
|
end
|
448
450
|
else
|
@@ -472,6 +474,12 @@ EOT
|
|
472
474
|
# @return [TrueClass, FalseClass] true if file was successfully imported,
|
473
475
|
# false otherwise
|
474
476
|
def import_fit_file(fit_file_name)
|
477
|
+
md5sum = FitFileStore.calc_md5_sum(fit_file_name)
|
478
|
+
if @ffs.store['fit_file_md5sums'].include?(md5sum)
|
479
|
+
# The FIT file is already stored in the DB.
|
480
|
+
return nil unless @force
|
481
|
+
end
|
482
|
+
|
475
483
|
begin
|
476
484
|
fit_entity = Fit4Ruby.read(fit_file_name)
|
477
485
|
rescue Fit4Ruby::Error
|
@@ -528,8 +536,8 @@ EOT
|
|
528
536
|
Log.error("You must specify 'metric' or 'statute' as unit system.")
|
529
537
|
end
|
530
538
|
|
531
|
-
if @db['config']['unit_system']
|
532
|
-
@db['config']['unit_system'] = args[0]
|
539
|
+
if @db['config']['unit_system'] != args[0]
|
540
|
+
@db['config']['unit_system'] = args[0]
|
533
541
|
@ffs.change_unit_system
|
534
542
|
end
|
535
543
|
end
|
@@ -589,10 +597,17 @@ EOT
|
|
589
597
|
|
590
598
|
def handle_version_update
|
591
599
|
if @db['config']['version'] != VERSION
|
592
|
-
|
593
|
-
@
|
600
|
+
puts "Work needed"
|
601
|
+
from_version = Gem::Version.new(@db['config']['version'])
|
602
|
+
to_version = Gem::Version.new(VERSION)
|
603
|
+
|
604
|
+
Log.warn "PostRunner version upgrade from #{from_version} to " +
|
605
|
+
"#{to_version} started."
|
606
|
+
@ffs.handle_version_update(from_version, to_version)
|
607
|
+
|
594
608
|
@db['config']['version'] = VERSION
|
595
|
-
Log.
|
609
|
+
Log.warn "PostRunner version upgrade from #{from_version} to " +
|
610
|
+
"#{to_version} completed."
|
596
611
|
end
|
597
612
|
end
|
598
613
|
|
@@ -33,7 +33,7 @@ module PostRunner
|
|
33
33
|
# @param page_count [Fixnum] Number of total pages
|
34
34
|
# @param page_index [Fixnum] Index of the page
|
35
35
|
def initialize(ffs, records, page_count, page_index)
|
36
|
-
#@unit_system = ffs.store['config']['unit_system']
|
36
|
+
#@unit_system = ffs.store['config']['unit_system'].to_sym
|
37
37
|
@records = records
|
38
38
|
|
39
39
|
views = ffs.views
|
@@ -18,20 +18,22 @@ module PostRunner
|
|
18
18
|
|
19
19
|
class UserProfileView
|
20
20
|
|
21
|
+
include Fit4Ruby::Converters
|
22
|
+
|
21
23
|
def initialize(fit_activity, unit_system)
|
22
24
|
@fit_activity = fit_activity
|
23
25
|
@unit_system = unit_system
|
24
26
|
end
|
25
27
|
|
26
28
|
def to_html(doc)
|
27
|
-
return nil if @fit_activity.
|
29
|
+
return nil if @fit_activity.user_data.empty?
|
28
30
|
|
29
31
|
ViewFrame.new('user_profile', 'User Profile', 600, profile,
|
30
32
|
true).to_html(doc)
|
31
33
|
end
|
32
34
|
|
33
35
|
def to_s
|
34
|
-
return '' if @fit_activity.
|
36
|
+
return '' if @fit_activity.user_data.empty?
|
35
37
|
profile.to_s
|
36
38
|
end
|
37
39
|
|
@@ -39,31 +41,54 @@ module PostRunner
|
|
39
41
|
|
40
42
|
def profile
|
41
43
|
t = FlexiTable.new
|
42
|
-
|
43
|
-
|
44
|
+
|
45
|
+
user_data = @fit_activity.user_data.first
|
46
|
+
user_profile = @fit_activity.user_profiles.first
|
47
|
+
hr_zones = @fit_activity.heart_rate_zones.first
|
48
|
+
|
49
|
+
if user_data && user_data.height
|
44
50
|
unit = { :metric => 'm', :statute => 'ft' }[@unit_system]
|
45
|
-
height =
|
51
|
+
height = user_data.get_as('height', unit)
|
46
52
|
t.cell('Height:', { :width => '40%' })
|
47
53
|
t.cell("#{'%.2f' % height} #{unit}", { :width => '60%' })
|
48
54
|
t.new_row
|
49
55
|
end
|
50
|
-
if
|
56
|
+
if (user_data && user_data.weight) ||
|
57
|
+
(user_profile && user_profile.weight)
|
51
58
|
unit = { :metric => 'kg', :statute => 'lbs' }[@unit_system]
|
52
|
-
weight =
|
59
|
+
weight = (user_profile && user_profile.get_as('weight', unit)) ||
|
60
|
+
(user_data && user_data.get_as('weight', unit))
|
53
61
|
t.row([ 'Weight:', "#{'%.1f' % weight} #{unit}" ])
|
54
62
|
end
|
55
|
-
t.row([ 'Gender:',
|
56
|
-
t.row([ 'Age:', "#{
|
57
|
-
|
58
|
-
|
59
|
-
t.row([ '
|
63
|
+
t.row([ 'Gender:', user_data.gender ]) if user_data.gender
|
64
|
+
t.row([ 'Age:', "#{user_data.age} years" ]) if user_data.age
|
65
|
+
if (user_profile && (rest_hr = user_profile.resting_heart_rate)) ||
|
66
|
+
(hr_zones && (rest_hr = hr_zones.resting_heart_rate))
|
67
|
+
t.row([ 'Resting Heart Rate:', "#{rest_hr} bpm" ])
|
68
|
+
end
|
69
|
+
if (max_hr = user_data.max_hr) ||
|
70
|
+
(max_hr = hr_zones.max_heart_rate)
|
71
|
+
t.row([ 'Max. Heart Rate:', "#{max_hr} bpm" ])
|
72
|
+
end
|
73
|
+
if user_profile && (date = user_profile.time_last_lthr_update)
|
74
|
+
t.row([ 'Last Lactate Threshold Update:', date ])
|
75
|
+
end
|
76
|
+
if user_data && (lthr = user_data.running_lactate_threshold_heart_rate)
|
77
|
+
t.row([ 'Running LT Heart Rate:', "#{lthr} bpm" ])
|
78
|
+
end
|
79
|
+
if user_profile && (speed = user_profile.functional_threshold_speed)
|
80
|
+
unit = { :metric => 'min/km', :statute => 'min/mile' }[@unit_system]
|
81
|
+
t.row([ 'Running LT Pace:', "#{speedToPace(speed)} #{unit}" ])
|
60
82
|
end
|
61
|
-
if
|
62
|
-
t.row([ 'Activity Class:',
|
83
|
+
if (activity_class = user_data.activity_class)
|
84
|
+
t.row([ 'Activity Class:', activity_class ])
|
63
85
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
86
|
+
# It's unlikely that anybody ever cares about the METmax value.
|
87
|
+
#if (metmax = user_data.metmax)
|
88
|
+
# t.row([ 'METmax:', "#{metmax} MET" ])
|
89
|
+
#end
|
90
|
+
if (vo2max = @fit_activity.vo2max)
|
91
|
+
t.row([ 'VO2max:', "#{'%.1f' % vo2max} ml/kg/min" ])
|
67
92
|
end
|
68
93
|
t
|
69
94
|
end
|
data/lib/postrunner/version.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = version.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2015, 2016, 2017 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2015, 2016, 2017, 2020 by Chris Schlaeger <cs@taskjuggler.org>
|
7
7
|
#
|
8
8
|
# This program is free software; you can redistribute it and/or modify
|
9
9
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -11,5 +11,5 @@
|
|
11
11
|
#
|
12
12
|
|
13
13
|
module PostRunner
|
14
|
-
VERSION = '0.
|
14
|
+
VERSION = '1.0.0'
|
15
15
|
end
|
data/postrunner.gemspec
CHANGED
@@ -11,14 +11,14 @@ GEM_SPEC = Gem::Specification.new do |spec|
|
|
11
11
|
spec.summary = %q{Application to manage and analyze Garmin FIT files.}
|
12
12
|
spec.description = %q{PostRunner is an application to manage FIT files
|
13
13
|
such as those produced by Garmin products like the Forerunner 620 (FR620),
|
14
|
-
Fenix 3, Fenix 3HR, Fenix 5 (S and X). It allows you to
|
15
|
-
the device and analyze the data. In addition to the
|
16
|
-
plotting pace, heart rates, elevation and other captured
|
17
|
-
provides a heart rate variability (HRV) and sleep analysis. It
|
18
|
-
satellite orbit prediction (EPO) data on the device to
|
19
|
-
It is an offline alternative to Garmin Connect. The
|
20
|
-
developed and tested on Linux but should work on other
|
21
|
-
well.}
|
14
|
+
Forerunner 25 (FR25), Fenix 3, Fenix 3HR, Fenix 5 (S and X). It allows you to
|
15
|
+
import the files from the device and analyze the data. In addition to the
|
16
|
+
common features like plotting pace, heart rates, elevation and other captured
|
17
|
+
values it also provides a heart rate variability (HRV) and sleep analysis. It
|
18
|
+
can also update satellite orbit prediction (EPO) data on the device to
|
19
|
+
speed-up GPS fix times. It is an offline alternative to Garmin Connect. The
|
20
|
+
software has been developed and tested on Linux but should work on other
|
21
|
+
operating systems as well.}
|
22
22
|
spec.homepage = 'https://github.com/scrapper/postrunner'
|
23
23
|
spec.license = "GNU GPL version 2"
|
24
24
|
|
@@ -26,14 +26,14 @@ well.}
|
|
26
26
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
27
27
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
28
28
|
spec.require_paths = ["lib"]
|
29
|
-
spec.required_ruby_version = '>=2.
|
29
|
+
spec.required_ruby_version = '>=2.4'
|
30
30
|
|
31
|
-
spec.add_dependency 'fit4ruby', '~>
|
32
|
-
spec.add_dependency 'perobs', '~> 4.
|
31
|
+
spec.add_dependency 'fit4ruby', '~> 3.6.0'
|
32
|
+
spec.add_dependency 'perobs', '~> 4.2.0'
|
33
33
|
spec.add_dependency 'nokogiri', '~> 1.6'
|
34
34
|
|
35
35
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
36
36
|
spec.add_development_dependency 'rake', '~> 0.9.6'
|
37
|
-
spec.add_development_dependency 'rspec', '~> 3.
|
38
|
-
spec.add_development_dependency 'yard', '~> 0.
|
37
|
+
spec.add_development_dependency 'rspec', '~> 3.6.0'
|
38
|
+
spec.add_development_dependency 'yard', '~> 0.9.20'
|
39
39
|
end
|
data/spec/FitFileStore_spec.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = PostRunner_spec.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2015, 2016, 2020 by Chris Schlaeger <cs@taskjuggler.org>
|
7
7
|
#
|
8
8
|
# This program is free software; you can redistribute it and/or modify
|
9
9
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -51,7 +51,7 @@ describe PostRunner::FitFileStore do
|
|
51
51
|
expect(@activities[-1]).not_to be_nil
|
52
52
|
|
53
53
|
expect(@ffs.devices.length).to eq(1)
|
54
|
-
expect(@ffs.devices.include?('
|
54
|
+
expect(@ffs.devices.include?('1-2050-123456790')).to be true
|
55
55
|
expect(@ffs.activities.length).to eq(1)
|
56
56
|
expect(@ffs.ref_by_activity(@activities[0])).to eq(1)
|
57
57
|
end
|
@@ -60,7 +60,7 @@ describe PostRunner::FitFileStore do
|
|
60
60
|
expect(@ffs.add_fit_file(@fit_file_names[0])).to be_nil
|
61
61
|
|
62
62
|
expect(@ffs.devices.length).to eq(1)
|
63
|
-
expect(@ffs.devices.include?('
|
63
|
+
expect(@ffs.devices.include?('1-2050-123456790')).to be true
|
64
64
|
expect(@ffs.activities.length).to eq(1)
|
65
65
|
end
|
66
66
|
|
@@ -69,8 +69,8 @@ describe PostRunner::FitFileStore do
|
|
69
69
|
expect(@activities[-1]).not_to be_nil
|
70
70
|
|
71
71
|
expect(@ffs.devices.length).to eq(2)
|
72
|
-
expect(@ffs.devices.include?('
|
73
|
-
expect(@ffs.devices.include?('
|
72
|
+
expect(@ffs.devices.include?('1-2050-123456790')).to be true
|
73
|
+
expect(@ffs.devices.include?('1-2050-123456791')).to be true
|
74
74
|
expect(@ffs.activities.length).to eq(2)
|
75
75
|
expect(@ffs.ref_by_activity(@activities[1])).to eq(1)
|
76
76
|
end
|
@@ -80,8 +80,8 @@ describe PostRunner::FitFileStore do
|
|
80
80
|
expect(@activities[-1]).not_to be_nil
|
81
81
|
|
82
82
|
expect(@ffs.devices.length).to eq(2)
|
83
|
-
expect(@ffs.devices.include?('
|
84
|
-
expect(@ffs.devices.include?('
|
83
|
+
expect(@ffs.devices.include?('1-2050-123456790')).to be true
|
84
|
+
expect(@ffs.devices.include?('1-2050-123456791')).to be true
|
85
85
|
expect(@ffs.activities.length).to eq(3)
|
86
86
|
expect(@ffs.ref_by_activity(@activities[2])).to eq(1)
|
87
87
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -56,7 +56,7 @@ def create_fit_file_store
|
|
56
56
|
store['config'] = store.new(PEROBS::Hash)
|
57
57
|
store['config']['data_dir'] = @work_dir
|
58
58
|
store['config']['html_dir'] = @html_dir
|
59
|
-
store['config']['unit_system'] =
|
59
|
+
store['config']['unit_system'] = 'metric'
|
60
60
|
@ffs = store['file_store'] = store.new(PostRunner::FitFileStore)
|
61
61
|
@records = store['records'] = store.new(PostRunner::PersonalRecords)
|
62
62
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: postrunner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Schlaeger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fit4ruby
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 3.6.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 3.6.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: perobs
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 4.
|
33
|
+
version: 4.2.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 4.
|
40
|
+
version: 4.2.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: nokogiri
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,39 +86,39 @@ dependencies:
|
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: 3.
|
89
|
+
version: 3.6.0
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: 3.
|
96
|
+
version: 3.6.0
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: yard
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.
|
103
|
+
version: 0.9.20
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.
|
110
|
+
version: 0.9.20
|
111
111
|
description: |-
|
112
112
|
PostRunner is an application to manage FIT files
|
113
113
|
such as those produced by Garmin products like the Forerunner 620 (FR620),
|
114
|
-
Fenix 3, Fenix 3HR, Fenix 5 (S and X). It allows you to
|
115
|
-
the device and analyze the data. In addition to the
|
116
|
-
plotting pace, heart rates, elevation and other captured
|
117
|
-
provides a heart rate variability (HRV) and sleep analysis. It
|
118
|
-
satellite orbit prediction (EPO) data on the device to
|
119
|
-
It is an offline alternative to Garmin Connect. The
|
120
|
-
developed and tested on Linux but should work on other
|
121
|
-
well.
|
114
|
+
Forerunner 25 (FR25), Fenix 3, Fenix 3HR, Fenix 5 (S and X). It allows you to
|
115
|
+
import the files from the device and analyze the data. In addition to the
|
116
|
+
common features like plotting pace, heart rates, elevation and other captured
|
117
|
+
values it also provides a heart rate variability (HRV) and sleep analysis. It
|
118
|
+
can also update satellite orbit prediction (EPO) data on the device to
|
119
|
+
speed-up GPS fix times. It is an offline alternative to Garmin Connect. The
|
120
|
+
software has been developed and tested on Linux but should work on other
|
121
|
+
operating systems as well.
|
122
122
|
email:
|
123
123
|
- cs@taskjuggler.org
|
124
124
|
executables:
|
@@ -334,7 +334,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
334
334
|
requirements:
|
335
335
|
- - ">="
|
336
336
|
- !ruby/object:Gem::Version
|
337
|
-
version: '2.
|
337
|
+
version: '2.4'
|
338
338
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
339
339
|
requirements:
|
340
340
|
- - ">="
|
@@ -342,7 +342,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
342
342
|
version: '0'
|
343
343
|
requirements: []
|
344
344
|
rubyforge_project:
|
345
|
-
rubygems_version: 2.2
|
345
|
+
rubygems_version: 2.7.6.2
|
346
346
|
signing_key:
|
347
347
|
specification_version: 4
|
348
348
|
summary: Application to manage and analyze Garmin FIT files.
|