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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 30fc3ab83ce783076286638ede5faf67d2b5adbf
4
- data.tar.gz: 465e09b0a7dac4cfdbcb129886f50afae1802680
2
+ SHA256:
3
+ metadata.gz: bb8a80028ee50a8f543d589b44fd41523208483a1743d29ec86e37525ff84a13
4
+ data.tar.gz: 9834c6dc556e9e118fca144a8edd33e9cecc51eb9bc4487d43218973f5dae719
5
5
  SHA512:
6
- metadata.gz: aa31ebe5811736bbcba8324d54a0dbb0bd4b19435544471236aac7563ad623002e5c4c2f7cec6e0095da7dfabbb6bdd25f10637cf88905a30d7dc35be6a2f459
7
- data.tar.gz: 801afc538448f0615c3d2095559f6fb945dd14cabeb3d55d3c1faa8c323d49e38522edca58ca6b44572cb79c332ae78bb8c4dbf1c22e224e4d3bf32996e8079e
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) and Fenix
5
- 3 or Fenix 3HR. It allows you to import the files from the device and
6
- analyze the data. In addition to the common features like plotting pace,
7
- heart rates, elevation and other captured values it also provides a
8
- heart rate variability (HRV) analysis. It can also update satellite
9
- orbit prediction (EPO) data on the device to speed-up GPS fix times.
10
- It is an offline alternative to Garmin Connect. The software has been
11
- developed and tested on Linux but should work on other operating
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.
@@ -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] || 'Undefined'
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.vertical_ratio ?
119
- "#{session.vertical_ratio}%" : '-' ])
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. Ground Contact Time Balance:',
124
- session.avg_gct_balance ?
125
- "#{session.avg_gct_balance}% L / " +
126
- "#{100.0 - session.avg_gct_balance}% R" : ';' ])
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
@@ -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) > 5.0
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 << [ (r.timestamp - start_time + 1).to_i * 1000, nil ]
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 (value = r.get_as(chart[:id], chart[:unit] || ''))
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
- unless last_value.nil? && value.nil?
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('http://omt.garmin.com/Rce/ProtobufApi/EphemerisService/GetEphemerisData')
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
- @http = Net::HTTP.new(@@URI.host, @@URI.port)
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 = @http.request(@request)
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] || 'Undefined'
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 true.
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 an Array in the store to hold all known devices.
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
- # Nothing here so far.
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
- md5sum = FitFileStore.calc_md5_sum(fit_file_name)
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 ].include?(fit_entity.class)
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[:manufacturer]}-#{id[:product]}-#{id[:serial_number]}"
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.fatal 'FIT file has no file_id section'
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.fatal "Fit entity has no device info for 0"
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), 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]
@@ -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'] ||= :metric
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'].to_s != args[0]
532
- @db['config']['unit_system'] = args[0].to_sym
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
- Log.warn "PostRunner version upgrade detected."
593
- @ffs.handle_version_update
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.info "Version upgrade completed."
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.user_profiles.empty?
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.user_profiles.empty?
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
- profile = @fit_activity.user_profiles.first
43
- if profile.height
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 = profile.get_as('height', unit)
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 profile.weight
56
+ if (user_data && user_data.weight) ||
57
+ (user_profile && user_profile.weight)
51
58
  unit = { :metric => 'kg', :statute => 'lbs' }[@unit_system]
52
- weight = profile.get_as('weight', unit)
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:', profile.gender ]) if profile.gender
56
- t.row([ 'Age:', "#{profile.age} years" ]) if profile.age
57
- t.row([ 'Max. Heart Rate:', "#{profile.max_hr} bpm" ]) if profile.max_hr
58
- if (lthr = profile.running_lactate_threshold_heart_rate)
59
- t.row([ 'Running LTHR:', "#{lthr} bpm" ])
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 profile.activity_class
62
- t.row([ 'Activity Class:', profile.activity_class ])
83
+ if (activity_class = user_data.activity_class)
84
+ t.row([ 'Activity Class:', activity_class ])
63
85
  end
64
- if profile.metmax
65
- t.row([ 'METmax:', "#{profile.metmax} MET" ])
66
- t.row([ 'VO2max:', "#{'%.1f' % (profile.metmax * 3.5)} ml/kg/min" ])
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
@@ -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.9.0'
14
+ VERSION = '1.0.0'
15
15
  end
@@ -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 import the files from
15
- the device and analyze the data. In addition to the common features like
16
- plotting pace, heart rates, elevation and other captured values it also
17
- provides a heart rate variability (HRV) and sleep analysis. It can also update
18
- satellite orbit prediction (EPO) data on the device to speed-up GPS fix times.
19
- It is an offline alternative to Garmin Connect. The software has been
20
- developed and tested on Linux but should work on other operating systems as
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.0'
29
+ spec.required_ruby_version = '>=2.4'
30
30
 
31
- spec.add_dependency 'fit4ruby', '~> 1.6.1'
32
- spec.add_dependency 'perobs', '~> 4.0.0'
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.4.1'
38
- spec.add_development_dependency 'yard', '~> 0.8.7'
37
+ spec.add_development_dependency 'rspec', '~> 3.6.0'
38
+ spec.add_development_dependency 'yard', '~> 0.9.20'
39
39
  end
@@ -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?('garmin-fenix3-123456790')).to be true
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?('garmin-fenix3-123456790')).to be true
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?('garmin-fenix3-123456790')).to be true
73
- expect(@ffs.devices.include?('garmin-fenix3-123456791')).to be true
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?('garmin-fenix3-123456790')).to be true
84
- expect(@ffs.devices.include?('garmin-fenix3-123456791')).to be true
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
@@ -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'] = :metric
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.9.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: 2017-11-12 00:00:00.000000000 Z
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: 1.6.1
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: 1.6.1
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.0.0
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.0.0
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.4.1
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.4.1
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.8.7
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.8.7
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 import the files from
115
- the device and analyze the data. In addition to the common features like
116
- plotting pace, heart rates, elevation and other captured values it also
117
- provides a heart rate variability (HRV) and sleep analysis. It can also update
118
- satellite orbit prediction (EPO) data on the device to speed-up GPS fix times.
119
- It is an offline alternative to Garmin Connect. The software has been
120
- developed and tested on Linux but should work on other operating systems as
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.0'
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.5
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.