postrunner 0.11.0 → 1.0.4

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: d8de6ac38768c84e90eb47ddb49429d7a74160b1
4
- data.tar.gz: 7816eba71ed5b6f90eef7d7ae15080c19d548624
2
+ SHA256:
3
+ metadata.gz: 49a5f5f39dfa3c4aa542ad58f086e6c6a193420274ef291c4a1f5b009665734b
4
+ data.tar.gz: cccd8e865768fbbd7d7266e4579753ac8ee261f0a35b4551b291e03f4e4684ee
5
5
  SHA512:
6
- metadata.gz: 3302269a229d363cfc75bbea304879f06c37e8c2e6c1706ebece3ce6b662b02a026315df59e7494de230c5c3cab3fb81b925bd0ef89f4b81b1280805b422b138
7
- data.tar.gz: e8179a0afb54a171ba200480cf498523688acffa9f3568b8a2ae1e272a5bdc4c863449c6c43b201dafe6223afeb64f64ca3520799e93317f4aec8114cdcb64f2
6
+ metadata.gz: 05b6cb51addbb4f366e9779e3d69c3307078b64a0256b82833a8016dcdee64ee4b2347038ba7f8921cd0a7fe530913d6d3eee1b48258935adf71f9ff06c7ec35
7
+ data.tar.gz: 075c12e07e651813fa59fefb15d4702510679dba7cbe23aaed7f27400655c72e68f913343820b631b514e6a03e3dfde4b9269441fabfe75bdcd5d51f567907db
data/README.md CHANGED
@@ -2,26 +2,50 @@
2
2
 
3
3
  PostRunner is an application to manage FIT files such as those
4
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.
5
+ Forerunner 25 (FR25), Fenix 3, Fenix 3HR, Fenix 5, Fenix 5+ or Fenix6.
6
+ It allows you to import the files from the device and analyze the
7
+ data. In addition to the common features like plotting pace, heart
8
+ rates, elevation and other captured values it also provides a heart
9
+ rate variability (HRV) analysis. It can also update satellite orbit
10
+ prediction (EPO) data on the device to speed-up GPS fix times.
11
+ Unfortunately, the download mechanism for CPE files used by the
12
+ devices with GPS chipsets from Sony is still unknown and hence
13
+ unsupported. Postrunner is an offline alternative to Garmin Connect.
14
+ The software has been developed and tested on Linux but should work on
15
+ other operating systems as well.
13
16
 
14
17
  ## Installation
15
18
 
16
19
  PostRunner is a [http://www.ruby-lang.org](Ruby) application. You need
17
- to have a Ruby 2.0 or later runtime environment installed. This
20
+ to have a Ruby 2.4 or later runtime environment installed. This
18
21
  application was developed and tested on Linux but may work on other
19
- operating systems as well.
22
+ operating systems as well. You can either install it as root for all
23
+ users on the computer or as a particular user for just this user.
24
+
25
+ ### System-wide installation as root user
20
26
 
21
27
  ```
22
28
  $ gem install postrunner
23
29
  ```
24
30
 
31
+ On some Linux distributions using sudo might resolve in permission
32
+ problems as the installed packages are not readable for normal users.
33
+ This typically results in 'cannot load such file' type error messages.
34
+
35
+ ### Installation as non-privileged user
36
+
37
+ ```
38
+ gem install --user-install postrunner
39
+ ```
40
+
41
+ This will install PostRunner and all dependency packages in your .gem
42
+ directory. You then need to add the binary path to your PATH variable
43
+ in your .profile or .bashrc or .whatever file. The path is typically
44
+ .gem/ruby/<version>/bin. Watch out, on some Linux distributions the
45
+ version number of ruby gets added to the binary name, e. g.
46
+ postrunner.ruby2.7. You can use a symbolic link or alias to safe some
47
+ typing.
48
+
25
49
  ## Usage
26
50
 
27
51
  ### Importing FIT files
@@ -29,7 +53,7 @@ $ gem install postrunner
29
53
  To get started you need to connect your device to your computer and
30
54
  mount it as a disk drive. Only devices that expose their data as FAT file
31
55
  system are supported. Older devices use proprietary drivers and are
32
- not supported by postrunner. Once the device is mounted find out the
56
+ not supported by PostRunner. Once the device is mounted find out the
33
57
  full path to the directory that contains your FIT files. You can then
34
58
  import all files on the device.
35
59
 
@@ -128,7 +152,7 @@ $ postrunner show :1
128
152
  ## Contributing
129
153
 
130
154
  PostRunner is currently work in progress. It does some things I want
131
- with files from my Garmin FR620. It's certainly possible to do more
155
+ with files from my Garmin devices. It's certainly possible to do more
132
156
  things and support more devices. Patches are welcome!
133
157
 
134
158
  1. Fork it ( https://github.com/scrapper/postrunner/fork )
@@ -380,7 +380,7 @@ module PostRunner
380
380
  end
381
381
 
382
382
  def activity_sub_type
383
- ActivitySubTypes[@sub_sport] || 'Undefined "#{@sub_sport}"'
383
+ ActivitySubTypes[@sub_sport] || "Undefined #{@sub_sport}"
384
384
  end
385
385
 
386
386
  def distance(timestamp, unit_system)
@@ -316,7 +316,7 @@ module PostRunner
316
316
  if zone.type == 18
317
317
  total_time = 0.0
318
318
  if zone.time_in_hr_zone
319
- zone.time_in_hr_zone.each { |tiz| total_time += tiz }
319
+ zone.time_in_hr_zone.each { |tiz| total_time += tiz if tiz }
320
320
  end
321
321
  break if total_time <= 0.0
322
322
  if zone.heart_rate_zones
@@ -354,12 +354,22 @@ module PostRunner
354
354
  def local_value(fdr, field, format, units)
355
355
  unit = units[@unit_system]
356
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
357
362
  return '-' unless value
358
363
  "#{format % [value, unit]}"
359
364
  end
360
365
 
361
366
  def pace(fdr, field, show_unit = true)
362
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
363
373
  case @unit_system
364
374
  when :metric
365
375
  "#{speedToPace(speed)}#{show_unit ? ' min/km' : ''}"
@@ -53,7 +53,8 @@ module PostRunner
53
53
  def generate_html(doc)
54
54
  doc.unique(:activityview_style) {
55
55
  doc.head {
56
- [ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js',
56
+ [ 'jquery/jquery-3.5.1.min.js', 'flot/jquery.flot.js',
57
+ #'flot/jquery.flot.time.js' ].each do |js|
57
58
  'flot/jquery.flot.time.js' ].each do |js|
58
59
  doc.script({ 'language' => 'javascript',
59
60
  'type' => 'text/javascript', 'src' => js })
@@ -30,7 +30,7 @@ module PostRunner
30
30
  :unit => select_unit('min/km'),
31
31
  :graph => :line_graph,
32
32
  :colors => '#0A7BEE',
33
- :show => @sport == 'running' || @sport == 'multisport',
33
+ :show => @sport == 'running' || @sport == 'multisport'
34
34
  },
35
35
  {
36
36
  :id => 'speed',
@@ -40,9 +40,17 @@ module PostRunner
40
40
  :colors => '#0A7BEE',
41
41
  :show => @sport != 'running'
42
42
  },
43
+ {
44
+ :id => 'Power_18FB2CF01A4B430DAD66988C847421F4',
45
+ :label => 'Power',
46
+ :unit => select_unit('Watts'),
47
+ :graph => :line_graph,
48
+ :colors => '#FFAC2E',
49
+ :show => @sport == 'running' || @sport == 'multisport'
50
+ },
43
51
  {
44
52
  :id => 'altitude',
45
- :label => 'Elevation',
53
+ :label => 'Altitude',
46
54
  :unit => select_unit('m'),
47
55
  :graph => :line_graph,
48
56
  :colors => '#5AAA44',
@@ -73,6 +81,14 @@ module PostRunner
73
81
  :colors => '#900000',
74
82
  :show => false
75
83
  },
84
+ {
85
+ :id => 'respiration_rate',
86
+ :label => 'Respiration Rate',
87
+ :unit => 'brpm',
88
+ :graph => :line_graph,
89
+ :colors => '#9cd6ef',
90
+ :show => true
91
+ },
76
92
  {
77
93
  :id => 'performance_condition',
78
94
  :label => 'Performance Condition',
@@ -119,6 +135,22 @@ module PostRunner
119
135
  [ '#FF5558', nil ] ],
120
136
  :show => @sport == 'running' || @sport == 'multisport'
121
137
  },
138
+ {
139
+ :id => 'Form_Power_18FB2CF01A4B430DAD66988C847421F4',
140
+ :label => 'Form Power',
141
+ :unit => select_unit('Watts'),
142
+ :graph => :line_graph,
143
+ :colors => '#CBBB58',
144
+ :show => @sport == 'running' || @sport == 'multisport'
145
+ },
146
+ {
147
+ :id => 'Leg_Spring_Stiffness_18FB2CF01A4B430DAD66988C847421F4',
148
+ :label => 'Leg Spring Stiffness',
149
+ :unit => select_unit('kN/m'),
150
+ :graph => :line_graph,
151
+ :colors => '#358C88',
152
+ :show => @sport == 'running' || @sport == 'multisport'
153
+ },
122
154
  {
123
155
  :id => 'stance_time',
124
156
  :label => 'Ground Contact Time',
@@ -149,6 +181,14 @@ module PostRunner
149
181
  :colors => '#A88BBB',
150
182
  :show => @sport == 'cycling'
151
183
  },
184
+ {
185
+ :id => "Air_Power_18FB2CF01A4B430DAD66988C847421F4",
186
+ :label => 'Air Power',
187
+ :unit => select_unit('Watts'),
188
+ :graph => :line_graph,
189
+ :colors => '#919498',
190
+ :show => @sport == 'running' || @sport == 'multisport'
191
+ },
152
192
  {
153
193
  :id => 'temperature',
154
194
  :label => 'Temperature',
@@ -221,7 +261,7 @@ EOT
221
261
  end
222
262
 
223
263
  def java_script
224
- s = "$(function() {\n"
264
+ s = "$(document).ready(function() {\n"
225
265
 
226
266
  s << tooltip_div
227
267
  @charts.each do |chart|
@@ -295,7 +335,18 @@ EOT
295
335
  # finish the line and start a new one later.
296
336
  data_set << [ (last_timestamp - start_time + 1).to_i * 1000, nil ]
297
337
  end
298
- if (value = r.get_as(chart[:id], chart[:unit] || ''))
338
+ value = r.get_as(chart[:id], chart[:unit] || '')
339
+ if value.nil? && chart[:id] == 'speed'
340
+ # If speed field doesn't exist the value might be in the
341
+ # enhanced_speed field.
342
+ value = r.get_as('enhanced_speed', chart[:unit] || '')
343
+ end
344
+ if value.nil? && chart[:id] == 'altitude'
345
+ # If altitude field doesn't exist the value might be in the
346
+ # enhanced_elevation field.
347
+ value = r.get_as('enhanced_elevation', chart[:unit] || '')
348
+ end
349
+ if value
299
350
  if chart[:id] == 'pace'
300
351
  # Slow speeds lead to very large pace values that make the graph
301
352
  # hard to read. We cap the pace at 20.0 min/km to keep it
@@ -44,7 +44,7 @@ module PostRunner
44
44
  def generate_html(doc)
45
45
  doc.unique(:dailymonitoringview_style) {
46
46
  doc.head {
47
- [ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js',
47
+ [ 'jquery/jquery-3.5.1.min.js', 'flot/jquery.flot.js',
48
48
  'flot/jquery.flot.time.js' ].each do |js|
49
49
  doc.script({ 'language' => 'javascript',
50
50
  'type' => 'text/javascript', 'src' => js })
@@ -126,6 +126,19 @@ module PostRunner
126
126
  t.new_row
127
127
  end
128
128
 
129
+ if (ant_id = device.ant_id)
130
+ t.cell('ANT ID:')
131
+ t.cell(ant_id)
132
+ t.new_row
133
+ end
134
+
135
+ if ant_id && (sensor_settings = find_settings_by_ant_id(ant_id)) &&
136
+ (calibration_factor = sensor_settings.calibration_factor)
137
+ t.cell('Calibration Factor')
138
+ t.cell('%.1f' % calibration_factor)
139
+ t.new_row
140
+ end
141
+
129
142
  if (rx_ok = device.rx_packets_ok) && (rx_err = device.rx_packets_err)
130
143
  t.cell('Packet Errors:')
131
144
  t.cell('%d%%' % ((rx_err.to_f / (rx_ok + rx_err)) * 100).to_i)
@@ -148,6 +161,14 @@ module PostRunner
148
161
  tables
149
162
  end
150
163
 
164
+ private
165
+
166
+ def find_settings_by_ant_id(ant_id)
167
+ @fit_activity.sensor_settings.find do |sensor|
168
+ sensor.ant_id == ant_id
169
+ end
170
+ end
171
+
151
172
  end
152
173
 
153
174
  end
@@ -75,7 +75,7 @@ module PostRunner
75
75
  case event.event
76
76
  when 'timer'
77
77
  name = "Timer (#{event.event_type.gsub(/_/, ' ')})"
78
- value = event.timer_trigger
78
+ value = '-'
79
79
  when 'course_point'
80
80
  name = 'Course Point'
81
81
  value = event.message_index
@@ -121,12 +121,19 @@ module PostRunner
121
121
  when 'comm_timeout'
122
122
  name 'Communication timeout'
123
123
  value = event.comm_timeout
124
+ when 'off_course'
125
+ name = 'Off Course'
126
+ value = '-'
124
127
  when 'recovery_hr'
125
128
  name = 'Recovery heart rate'
126
129
  value = "#{event.recovery_hr} bpm"
127
130
  when 'recovery_time'
128
131
  name = 'Recovery time'
129
- value = "#{secsToDHMS(event.recovery_time * 60)}"
132
+ if event.recovery_time
133
+ value = "#{secsToDHMS(event.recovery_time * 60)}"
134
+ else
135
+ value = '-'
136
+ end
130
137
  when 'recovery_info'
131
138
  name = 'Recovery info'
132
139
  mins = event.recovery_info
@@ -140,6 +147,9 @@ module PostRunner
140
147
  when 'lactate_threshold_speed'
141
148
  name = 'Lactate Threshold Pace'
142
149
  value = pace(event, 'lactate_threshold_speed')
150
+ when 'functional_threshold_power'
151
+ name = 'Functional Threshold Power'
152
+ value = "#{event.functional_threshold_power} W"
143
153
  else
144
154
  name = event.event
145
155
  value = event.data
@@ -253,7 +253,7 @@ module PostRunner
253
253
  end
254
254
 
255
255
  def activity_sub_type
256
- ActivitySubTypes[@sub_sport] || 'Undefined'
256
+ ActivitySubTypes[@sub_sport] || "Undefined #{@sub_sport}"
257
257
  end
258
258
 
259
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, 2018 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
@@ -83,7 +83,8 @@ module PostRunner
83
83
  return nil
84
84
  end
85
85
  else
86
- # 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.
87
88
  path = @store['file_store'].fit_file_dir(File.basename(fit_file_name),
88
89
  long_uid, type)
89
90
  fq_fit_file_name = File.join(path, File.basename(fit_file_name))
@@ -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.
@@ -94,8 +144,11 @@ module PostRunner
94
144
 
95
145
  # Generate a String that uniquely identifies the device that generated
96
146
  # the FIT file.
97
- id = extract_fit_file_id(fit_entity)
98
- long_uid = "#{id[:manufacturer]}-#{id[:product]}-#{id[:serial_number]}"
147
+ unless (id = extract_fit_file_id(fit_entity))
148
+ return nil
149
+ end
150
+ long_uid = "#{id[:numeric_manufacturer]}-" +
151
+ "#{id[:numeric_product]}-#{id[:serial_number]}"
99
152
 
100
153
  # Make sure the device that created the FIT file is properly registered.
101
154
  device = register_device(long_uid)
@@ -187,6 +240,7 @@ module PostRunner
187
240
  @store['records'].generate_html_reports
188
241
  generate_html_index_pages
189
242
  end
243
+
190
244
  # Determine the right directory for the given FIT file. The resulting path
191
245
  # looks something like /home/user/.postrunner/devices/garmin-fenix3-1234/
192
246
  # activity/5A.
@@ -450,42 +504,23 @@ module PostRunner
450
504
  return nil
451
505
  end
452
506
 
453
- if fid.manufacturer == 'garmin' &&
454
- fid.garmin_product == 'fr920xt'
455
- # Garmin Fenix3 with firmware before 6.80 is reporting 'fr920xt' in
456
- # the file_id section but 'fenix3' in the first device_info section.
457
- # To tell the Fenix3 apart from the FR920XT we need to look into the
458
- # device_info section for all devices with a garmin_product of
459
- # 'fr920xt'.
460
- fit_entity.device_infos.each do |di|
461
- if di.device_index == 0
462
- return {
463
- :manufacturer => di.manufacturer,
464
- :product => di.garmin_product || di.product,
465
- :serial_number => di.serial_number
466
- }
467
- end
468
- end
469
- Log.error "Fit entity has no device info for 0"
470
- return nil
471
- else
472
- # And for all properly developed devices we can just look at the
473
- # file_id section.
474
- if fid.manufacturer.nil? ||
475
- fid.manufacturer[0..'Undocumented value'.length - 1] ==
476
- 'Undocumented value'
477
- Log.error "Cannot store FIT files for unknown manufacturer " +
478
- fid.manufacturer
479
- return nil
507
+ fit_entity.device_infos.each do |di|
508
+ # Not all FIT file have indexed device sections. In case the device
509
+ # index is nil we'll take the first entry.
510
+ if (di.device_index.nil? || di.device_index == 0) &&
511
+ di.numeric_manufacturer && di.numeric_product
512
+ return {
513
+ :manufacturer => di.manufacturer,
514
+ :product => di.garmin_product || di.product,
515
+ :numeric_manufacturer => di.numeric_manufacturer,
516
+ :numeric_product => di.numeric_product,
517
+ :serial_number => di.serial_number || 0
518
+ }
480
519
  end
481
- fid.serial_number ||= 0
482
-
483
- return {
484
- :manufacturer => fid.manufacturer,
485
- :product => fid.garmin_product || fid.product,
486
- :serial_number => fid.serial_number
487
- }
488
520
  end
521
+
522
+ Log.error "Fit entity has no device info section"
523
+ return nil
489
524
  end
490
525
 
491
526
  def register_device(long_uid)
@@ -502,7 +537,8 @@ module PostRunner
502
537
  @store.new(FFS_Device, short_uid, long_uid)
503
538
 
504
539
  # Create the directory to store the FIT files of this device.
505
- create_directory(File.join(@devices_dir, long_uid), long_uid)
540
+ create_directory(File.join(@devices_dir, long_uid),
541
+ long_uid)
506
542
  end
507
543
 
508
544
  @store['devices'][long_uid]