postrunner 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.
|